当前位置: 首页 > news >正文

【Linux 系统】基础IO——Linux中对文件的理解

13.基础IO(1)

文章目录

  • 13.基础IO(1)
    • 文件的基本概念:内容与属性
    • 文件的打开机制:fopen 和 open
    • 被打开的文件与磁盘文件的区别
    • 文件的内核数据结构
    • 文件与进程的交互方式
    • 标准输入/输出/错误与文件流
    • 系统调用与文件描述符
    • 文件打开模式(r/w/a/a+)与权限控制
    • C 语言与系统调用在文件管理中的应用
    • 权限控制与 `umask` 的影响
    • 位图与标志位参数传递的机制

文件的基本概念:内容与属性

在 Linux 中,文件是操作系统存储数据的基本单位,它包含两部分:内容(即数据本身)和属性(metadata)。属性包括文件名、类型、大小、所有者、权限、创建/修改时间等元信息。据讲稿与相关资料指出,任何文件都包含内容和属性,即使一个文件没有任何内容(空文件),它仍具有名称、权限等属性,并且这些属性也需要占用磁盘空间。例如,用 ls -l 可以看到空文件大小为 0,但目录中仍保留了它的 inode 信息和属性;这是因为文件的属性也记录在磁盘上。

文件的内容则存储在磁盘的数据块中,当文件被打开后才会被加载到内存供进程访问。根据冯诺依曼体系结构,CPU 只能直接访问内存而无法直接访问磁盘,因此要访问一个文件,必须先将文件加载到内存,这个过程即为“打开文件”。综上,文件 = 内容 + 属性,而文件的属性往往保存在 inode 等结构中。

【课外补充】在 Linux 文件系统中,每个文件在磁盘上用 inode(索引节点)来记录其元数据和数据块位置,包括权限、类型、大小、时间戳、硬链接数等信息。例如,ls -il 显示的第一列就是 inode 编号,通过 inode 可以获取文件的各种属性。

文件的打开机制:fopen 和 open

在编程中访问文件时,必须显式地打开文件。C 语言标准库提供 fopen 函数(位于 <stdio.h>)来打开文件,返回一个 FILE* 类型的文件指针供后续读写使用;而在系统调用层面,Linux 提供了 open 系统调用(位于 <fcntl.h>)来打开文件,返回一个非负整数的文件描述符(file descriptor)。两者的主要区别是:fopen 是库函数,会对调用参数进行封装并返回 FILE*open 是直接与内核交互的系统调用,需要传入文件路径、标志位 (flags) 和权限模式 (mode),返回一个 int 型文件描述符。

只有当程序执行到打开文件的语句时,文件才真正被加载到内存中。例如,在下面代码段中,只有当 fopenopen 运行时,文件才被打开:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main() {// 使用 fopen 打开文件,返回 FILE*FILE *fp = fopen("example.txt", "w");if (fp == NULL) {perror("fopen");return 1;}fputs("Hello, world!\n", fp);fclose(fp);// 使用 open 打开文件,返回文件描述符int fd = open("example2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0) {perror("open");return 1;}write(fd, "Hello, world!\n", 14);close(fd);return 0;
}

在上例中,直到程序运行到 fopenopen 并执行成功时,文件才真正被打开并加载到内存中。执行 fopen 返回 FILE* 后,可以通过 fread/fwrite/fputs 等接口对文件内容进行读写;执行 open 返回的文件描述符可以与 readwritelseek 等系统调用配合使用。如果打开失败,fopen 返回 NULLopen 返回 -1,并设置相应的错误码。

被打开的文件与磁盘文件的区别

磁盘上的文件被打开的文件是两个不同的概念。磁盘上的文件是静态存在的,包含文件内容和元数据(属性),但尚未加载到内存。只有当进程调用 fopenopen 并成功时,文件才被打开,它的内容和属性才被加载到内存,并由内核为其分配数据结构以供访问。这种加载过程类似将文件从磁盘映射到内核空间,使得 CPU 可以直接操作它。

简言之:未打开的文件只能在磁盘上存在;被打开的文件则在内存中有对应结构体供进程访问。根据讲稿和资料的描述,当我们打开一个文件时,操作系统会把文件的内容和属性加载到内存中。在内存中的文件通常用文件对象(如 Linux 内核中的 struct file)来表示,该结构体包含文件的各种属性以及指向文件数据的指针。相比之下,磁盘上的文件只是存储在文件系统上的数据块和 inode 信息。

因此,在一个系统中存在大量磁盘文件,但只有当进程需要时才会打开其中的少数文件。例如,一个 Linux 系统可能有上万文件,但同时被打开的可能只有几百个。操作系统需要跟踪管理哪些文件已打开、对应哪个进程以及何时关闭,保障文件访问的正确性和资源回收。

文件的内核数据结构

在 Linux 内核中,每个被打开的文件都会对应内核级数据结构来表示其状态和属性。常见的结构有 struct file(表示已打开的文件实例)和 struct inode(表示磁盘上的文件元数据)。讲稿中提到,文件在内核中本质上也等于 内容 + 属性,即有对应的结构体来描述,包括文件大小、权限、读写位置等信息。我们可以把 struct file 理解为文件的“内核镜像”,它会保存文件当前位置(offset)、文件系统操作函数指针、引用计数等,当进程通过文件描述符读写文件时,实际上是通过这些结构体进行操作的。

操作系统如何管理被打开的文件呢?每个进程都有一个 task_struct,其中包含一个指向 struct files_struct 的指针。files_struct 中维护了一个文件描述符数组 fd_array,该数组的每个元素指向一个内核中的 struct file 结构。当进程调用 open 打开新文件时,内核会在这个数组中找到一个空闲的下标(例如 3、4、5 等)分配给该文件,并返回该下标作为文件描述符。可以说,系统层面访问文件的唯一途径就是文件描述符。例如,进程启动时默认占用 0、1、2 三个文件描述符(分别对应 stdinstdoutstderr),后续打开的第一个文件会获得描述符 3

此外,struct file 结构中还包含一个引用计数,用以记录有多少个文件描述符或进程引用该文件。这意味着同一个文件可以被多个描述符(甚至不同进程)共享读取或写入。文件操作(读/写/关闭)最终都会通过这些内核结构执行,内核维护的这种数据结构保证了对文件的并发访问和正确释放。

文件与进程的交互方式

在 Linux 中,进程是访问和操作文件的主体。只有进程才能调用 fopenopen 等接口来操作文件。讲稿强调,当程序中出现了 fopenopen 语句,并且进程实际执行到这一句时,文件才被打开。也就是说,即便源代码中有 fopen 调用,如果程序尚未运行或者未执行到该行,文件也不会被打开。执行完成后,进程会得到一个文件指针或文件描述符,通过它可以进行后续的读写操作,最后再调用 fcloseclose 关闭文件释放资源。

一个进程可以同时打开多个文件。实际上,每个进程启动时就自动打开了三个标准流(stdin,stdout,stderr),而用户程序可以根据需要继续打开其他文件。操作系统为每个进程维护独立的文件描述符表,确保各进程对文件的操作互不干扰。当进程结束或主动关闭文件时,内核会关闭对应的 struct file,更新引用计数,并回收内存和描述符。

总之,文件操作的发生总是伴随着一个进程:访问文件的始终是进程,而非静态的代码文本。进程运行时必须先打开文件,此时操作系统将文件加载到内存,并返回用于标识该文件的文件描述符或 FILE*。后续对文件内容的读写,都是通过该进程内的指针或描述符发起的。进程关闭文件后,文件可以从内存中卸载,相关资源被释放。

标准输入/输出/错误与文件流

每个进程默认启动时都会打开三个标准流,用于与外界(键盘、显示器等)进行交互:标准输入stdin,通常对应键盘,文件描述符 0)、标准输出stdout,通常对应显示器,文件描述符 1)和标准错误stderr,也对应显示器或终端,文件描述符 2)。在 C 语言中,这三个标准流都是 FILE* 类型指针,分别指向 stdin,stdout,stderr。例如,printf 默认向 stdout 写入,而 scanf 则从 stdin 读取。虽然键盘和显示器是硬件设备,但 C 标准库将它们抽象为文件流(通过底层的系统调用与操作系统交互),因此对它们的读写操作与普通文件类似。

标准流的具体对应关系如下:

  • stdin:标准输入(键盘),文件描述符 0。
  • stdout:标准输出(显示器或终端),文件描述符 1。
  • stderr:标准错误(显示器或终端),文件描述符 2。

例如,下面的代码会从标准输入读取一行,然后再通过标准输出打印出来:

#include <stdio.h>int main() {char buf[100];// 从标准输入(stdin)读一行if (fgets(buf, sizeof(buf), stdin)) {// 将读取到的内容打印到标准输出(stdout)printf("你输入了: %s", buf);}return 0;
}

可以看到,我们使用 stdinstdout 完成了输入输出。由于它们都是 FILE*,底层实际上对应文件描述符 0 和 1。总之,标准输入/输出/错误在实现上也是文件,只是内核默认为每个新进程打开了这三个文件流。

系统调用与文件描述符

在 Linux 中进行文件操作的系统调用有 openclosereadwrite 等。文件描述符是内核为进程打开文件后分配的整数句柄,用于索引进程的文件描述符表。调用 open 时,如果成功,内核返回一个非负整数(通常从 3 开始,因为 0、1、2 已被标准流占用)。这个整数就是文件描述符。例如:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main() {int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0) {perror("open失败");return 1;}printf("打开文件得到文件描述符: %d\n", fd);// 可以使用 write(fd, ..., ...) 和 close(fd)close(fd);return 0;
}

上例中,如果 log.txt 打开成功,将打印出类似 3 这样的文件描述符值。打开失败时 fd-1,并通过 perror 输出错误信息。我们注意到文件描述符是一个索引,它指向内核中某个 struct file 结构。只有通过文件描述符,系统调用才能找到对应的文件资源。

此外,所有针对文件的系统调用(如 readwritelseekclose 等)都以文件描述符作为参数。内核通过进程的 files_struct 中的 fd_array 找到对应的内核文件对象,然后执行操作。因此,系统层面上访问文件的唯一途径就是文件描述符。正因如此,FILE* 之类的用户态结构内部也保存了一个文件描述符(可通过 fp->_fileno 获取),以便通过系统调用完成实际的读写。

在这里插入图片描述

⽽现在知道,⽂件描述符就是从0开始的⼩整数。当我们打开⽂件时,操作系统在内存中要创建相应的数据结构来描述⽬标⽂件。于是就有了file结构体。表⽰⼀个已经打开的⽂件对象。⽽进程执⾏open系统调⽤,所以必须让进程和⽂件关联起来。每个进程都有⼀个指针*files, 指向⼀张表files_struct,该表最重要的部分就是包含⼀个指针数组,每个元素都是⼀个指向打开⽂件的指针!所以,本质上,⽂件描述符就是该数组的下标。所以,只要拿着⽂件描述符,就可以找到对应的⽂件。

在这里插入图片描述

文件打开模式(r/w/a/a+)与权限控制

在 C 标准库的 fopen 中,文件打开模式通过模式字符串指定,常用模式包括:

  • "r":以只读方式打开文件,文件必须存在;读操作从文件开头开始。
  • "r+":以读写方式打开文件,文件必须存在;操作位置在开头。
  • "w":以写方式打开文件,如果文件不存在则创建;如果文件存在则清空原内容,相当于截断文件再写入。
  • "w+":以读写方式打开,效果类似于 w
  • "a":以追加方式打开文件,如果文件不存在则创建;写入时总是追加到文件末尾。
  • "a+":以读写方式打开,写操作追加到末尾。

例如,上述代码示例中 fopen("log.txt", "w") 会创建新文件或清空已有文件,并将数据写入。如果改为 "a" 模式,则不会清空原文件,而是将内容追加到末尾。这些模式与 shell 中的重定向符号类似:> 对应清空写入,>> 对应追加写入。

对于系统调用 open,访问模式和标志通过参数 flags 指定。常见的标志包括:

  • 访问模式:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)
  • 创建模式:O_CREAT(如果文件不存在则创建)、O_EXCL(配合 O_CREAT 使用,如果文件已存在则打开失败)
  • 截断追加:O_TRUNC(如果文件存在则清空其内容)、O_APPEND(写操作追加到末尾)

这些标志是 位标志,可以通过按位或组合多个选项(位图方式)。每个宏对应一个二进制位,例如 O_CREAT = 0x40。我们可以写 O_WRONLY | O_CREAT 来同时设置只写和创建标志。例如:

int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);

上述调用试图以读写模式打开 data.txt,如果不存在则创建,并在打开时清空原有内容。权限参数 0666 指定新文件的初始权限(随后会受 umask 掩码的影响,详见下一节)。

需要注意的是,open 系统调用本身并不设置文件访问权限,它只是使用提供的参数和当前进程的 umask 决定文件的最终权限。如果我们只读打开已有文件(不需要创建),可以省略权限参数,只传两个参数即可。

C 语言与系统调用在文件管理中的应用

在 C 语言中,文件操作常用标准库函数,如 fopen/fclosefread/fwritefprintf/fscanf 等。这些函数在用户态提供了方便的接口,但它们底层最终都会调用相应的系统调用。简而言之:

  • C 标准库函数(例如 fopen, fclose, fread, fwrite)是一种对系统调用的封装,使用起来更方便,自动管理缓冲区。
  • 系统调用(例如 open, close, read, write)是内核提供的底层接口,功能更原始,需要程序员自己处理缓冲和错误。

讲稿总结道:

fopen, fclose, fwrite, fread —— C 库函数;
open, close, write, read —— 系统调用;
C 库函数就是系统调用的封装。

我们在前面的示例代码中就使用了 fopen/fputsopen/write 的组合。二者的使用基本相同,只是一个返回 FILE*,另一个返回 int fd。C 库函数在底层自动调用了相同功能的系统调用,并通常带有文件缓冲机制。

比如,可以用下面代码分别展示两种方法写文件:

// 使用 C 库函数 fwrite
#include <stdio.h>
int main() {FILE *fp = fopen("test.txt", "w");if (!fp) return 1;fprintf(fp, "Hello, libc!\n");fclose(fp);return 0;
}
// 使用系统调用 write
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {int fd = open("test_sys.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0) return 1;const char *msg = "Hello, syscall!\n";write(fd, msg, strlen(msg));close(fd);return 0;
}

两个示例效果类似,但实现层次不同。第二个示例是直接与内核交互,第一种方式在内部会调用第二种方式并做缓冲。了解两者的差异有助于在需要精细控制时直接使用系统调用,而大部分应用开发可以继续使用更易用的 C 库函数。

权限控制与 umask 的影响

Linux 中每个文件都有访问权限设置,一般用三组 rwx 位表示属主、属组和其它用户的读写执行权限。对于新创建的文件,open 系统调用第三个参数指定了文件的初始权限(如 0666,即默认可读可写),但最终权限还会受进程的文件模式掩码umask)影响。操作系统会将给定的权限值与 umask 做按位与再取反,从而得出文件的实际权限。例如:如果进程的 umask022,新文件的默认权限 0666 会变成 0644(去掉了对“其它用户”的写权限)。

讲稿中演示了这一过程:在 open("log.txt", O_CREAT, 0666) 后发现文件权限并非 0666 而是根据掩码修正后的值。可以在程序中调用 umask(0) 来临时将掩码清零,这样之后创建文件就会严格使用指定的权限。要注意,umask 是进程级的属性,修改当前进程的 umask 不会影响其他进程。

例如:

# 进程默认 umask 为 022,创建文件时权限=0666&~022=0644
$ touch file1.txt
$ ls -l file1.txt
-rw-r--r-- # 改变 umask 后再创建
$ umask 000
$ touch file2.txt
$ ls -l file2.txt
-rw-rw-rw-  # 此时文件权限就是0666 

在上述示例中,可以看到 umask 影响了新文件的权限。此外,open 的权限参数只有在指定 O_CREAT 时才有效,如果只读打开现有文件就不需提供权限参数。

位图与标志位参数传递的机制

在许多系统调用(如 openmmapsocket 等)中,为了同时传递多个选项,Linux 通常采用**位掩码(bitmap)**的方式。即将整型参数的每一位作为独立的开关。这样我们可以用按位或(|)来组合多个选项,仅需一个参数即可表示多个布尔配置。

open 为例,其 flags 参数就是一个 32 位的位图,每个标志宏(如 O_RDONLY=0x0000O_WRONLY=0x0001O_CREAT=0x0040 等)在这个整数中只有一个位为 1。多个标志可以组合,比如 O_WRONLY | O_CREAT | O_TRUNC。下面这个示例片段演示了位掩码的原理(简化示意):

#define ONE   (1<<0)
#define TWO   (1<<1)
#define THREE (1<<2)void Test(int flags) {if (flags & ONE)   printf("ONE\n");if (flags & TWO)   printf("TWO\n");if (flags & THREE) printf("THREE\n");
}int main() {Test(ONE | THREE);  // 输出 ONE 和 THREEreturn 0;
}

open("file", O_WRONLY | O_CREAT) 的调用中,flags 参数的值就是上述位或的结果。内核读取这个整数后,通过与操作检查各个位是否被设置,从而知道用户希望启用哪些功能。这种位图传参机制非常灵活,能够支持在单个参数中传递多个选项,也避免了传递过多单独参数。对开发者来说,只需记住各个宏代表的含义,并使用按位或即可组合使用。

【课外补充】此处介绍的位图传参方法在很多系统调用和库接口中都很常见,不限于文件操作。例如 openfcntlmmap、网络编程的 socket 等调用都使用类似方式传递标志位。

http://www.dtcms.com/a/266677.html

相关文章:

  • 【深度学习新浪潮】如何使用大模型等技术基于序列预测蛋白质的结构,功能和靶点?
  • 【学习笔记】Lean4基础 ing
  • 邮科千兆8光8电工业级交换机互联网的脉搏
  • 洛谷刷题8
  • 云原生Kubernetes系列 | Ingress和Egress网络策略NetworkPolicy结合案例使用详解
  • 5060Ti安装黑屏问题一解
  • 【WIP】【VLAVLM——InternVL系列】
  • Maven编译和打包插件
  • cd-agent更换cd模型(自用)
  • i18next + 原生JS 双引擎:打造前端多语言系统最佳实践
  • Android 网络请求优化全面指南
  • 韩国小说《素食者》读后感
  • C++--多态
  • 全网唯一/Qt结合ffmpeg实现手机端采集摄像头推流到rtsp或rtmp/可切换前置后置摄像头/指定分辨率帧率
  • 在 Minikube 上部署 Kubernetes Deployment 并解决 ImagePullBackOff 问题
  • WPS中配置MathType教程
  • stm32学到什么程度可以找工作?
  • Java学习第十二部分——idea各种项目简介
  • 电阻温升评估的相关测试总结
  • openlayers 判断geojson文件是否在视口内
  • Android BitmapRegionDecoder 详解
  • Ethernet IP与Profinet共舞:网关驱动绿色工业的智慧脉动
  • <tauri><rust><GUI>使用tauri创建一个文件夹扫描程序
  • 深度学习前置知识全面解析:从机器学习到深度学习的进阶之路
  • 《Java修仙传:从凡胎到码帝》第三章:缩进之劫与函数峰试炼
  • 鸿蒙系统(HarmonyOS)4.2 设备上实现无线安装 APK 并调试
  • Python-封装和解构-set及操作-字典及操作-解析式生成器-内建函数迭代器-学习笔记
  • React中的useState 和useEffect
  • 记一次Linux手动设置网卡的过程
  • Spark从入门到实战:安装与使用全攻略