Linux中基础IO
一、回顾C语言文件接口1. 打开文件#include stdio.h int main() { FILE *fp fopen(myfile, w); if (!fp) { printf(fopen error!\n); return 1; } fclose(fp); return 0; }问题myfile会被创建在哪里在当前进程的“当前工作目录”下。那么系统如何知道进程的当前路径Linux 提供了/proc文件系统可以查看运行中进程的信息ps ajx | grep myProc ls /proc/533463 -l重点关注两个符号链接cwd→ 当前进程的工作目录exe→ 当前进程的可执行文件路径✅ 打开文件本质是进程打开文件因此即使不带路径进程也能通过cwd知道文件应该放在哪里。2. 写文件const char *msg hello bit!\n; int count 5; while (count--) { fwrite(msg, strlen(msg), 1, fp); }fwrite是按二进制块写入适合结构体、固定长度数据。写入的是原始内存数据不自动加\0。3. 读文件char buf[1024]; while (1) { size_t s fread(buf, 1, sizeof(buf), fp); if (s 0) { buf[s] 0; printf(%s, buf); } if (feof(fp)) break; }fread返回实际读取的字节数。feof判断是否读到文件末尾。4. 输出到显示器三种标准方式fwrite(msg, strlen(msg), 1, stdout); // 二进制写 printf(hello printf\n); // 格式化输出 fprintf(stdout, hello fprintf\n); // 文件流方式stdin/stdout/stderr都是FILE*类型默认已打开。5. 打开方式总结模式含义r只读位置在文件开头r读写位置在文件开头w只写文件长度截断或创建w读写截断或创建a追加位置在文件末尾a读写追加读从开头写从末尾用w文件会先被清空然后再进行写入二、系统文件I/O1. 标志位的传递方式系统调用中经常需要传递多个选项比如O_WRONLY | O_CREAT。这种写法依赖位掩码技术#define ONE 0001 // 二进制 0001 #define TWO 0002 // 二进制 0010 #define THREE 0004 // 二进制 0100 void func(int flags) { if (flags ONE) printf(ONE ); if (flags TWO) printf(TWO ); if (flags THREE) printf(THREE ); } func(ONE | TWO); // 输出ONE TWO每个标志位独占一个 bit通过|组合通过检测高效且直观。2. 接口介绍2.1 open接口函数原型#include sys/types.h #include sys/stat.h #include fcntl.h int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);参数详解参数类型说明pathnameconst char*要打开或创建的文件路径相对或绝对路径flagsint文件打开方式标志位见下表modemode_t创建文件时的权限仅当flags包含O_CREAT时使用flags 常用标志位标志含义说明访问模式三选一O_RDONLY只读打开不能写O_WRONLY只写打开不能读O_RDWR读写打开可读可写可选标志可组合O_CREAT文件不存在则创建需提供mode参数O_APPEND追加模式每次写入自动到文件末尾O_TRUNC截断打开文件时清空内容O_EXCL排他创建与O_CREAT一起用文件已存在则失败O_SYNC同步写入write等待数据落盘才返回O_NONBLOCK非阻塞模式用于设备文件或管道O_CLOEXEC执行时关闭fork后exec时自动关闭mode 权限详解// mode 格式八进制数三位分别表示所有者、所属组、其他人 0644 // 二进制110 100 100 // 所有者rw- (6) // 所属组r-- (4) // 其他人r-- (4)返回值返回值含义 0成功返回文件描述符非负小整数-1失败设置errno可用perror()查看错误完整示例#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h #include errno.h #include string.h int main() { // 读写打开不存在则创建存在则清空 int fd open(test.txt, O_RDWR | O_CREAT | O_TRUNC, 0644); if (fd -1) { fprintf(stderr, open failed: %s\n, strerror(errno)); return 1; } printf(open success, fd %d\n, fd); // ... 使用 fd 进行读写 ... close(fd); return 0; }2.2close接口函数原型#include unistd.h int close(int fd);参数参数说明fd要关闭的文件描述符由open返回返回值返回值含义0成功关闭-1失败如 fd 无效设置errno2.3read接口#include unistd.h ssize_t read(int fd, void *buf, size_t count);参数详解参数类型说明fdint已打开的文件描述符bufvoid*用户缓冲区指针用于存放读取的数据countsize_t要读取的字节数不能超过buf大小返回值详解返回值含义 0成功读取的字节数可能小于count0读到文件末尾EOF-1失败设置errno2.4 write接口函数原型#include unistd.h ssize_t write(int fd, const void *buf, size_t count);参数详解参数类型说明fdint已打开的文件描述符必须是写模式打开bufconst void*要写入的用户数据缓冲区countsize_t要写入的字节数返回值返回值含义 0实际写入的字节数可能小于count-1失败设置errno3. files_struct文件管理核心结构每个进程的task_struct中包含files_struct它管理进程打开的所有文件进程 task_struct │ └── files_struct │ └── fd_array[] (文件描述符表) │ ┌─────────────┼─────────────┬─────────────┐ │ │ │ │ fd[0] fd[1] fd[2] fd[3] │ │ │ │ ▼ ▼ ▼ ▼ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │struct│ │struct│ │struct│ │struct│ │ file │ │ file │ │ file │ │ file │ │ (终端│ │ (终端│ │ (终端│ │(文件 │ │ 输入)│ │ 输出)│ │ 错误)│ │ out) │ └──────┘ └──────┘ └──────┘ └──────┘ │ │ │ │ ▼ ▼ ▼ ▼ 键盘 屏幕 屏幕 磁盘文件说明每个fd是数组下标0,1,2,3...数组元素指向内核中的struct file对象struct file再指向具体的物理设备或文件磁盘文件会将数据放在文件缓冲区中一个struct file对应一个文件缓冲区对文件内容做任何操作都必须先把文件加载到内核对应的文件缓冲区内。4. 重定向原理【执行前 - 终端输出】 ┌─────────────────┐ 进程 files_struct │ 内核文件表 │ │ │ fd[0] ────────────→│ stdin (键盘) │ fd[1] ────────────→│ stdout (终端) │ ← ls 写入这里 fd[2] ────────────→│ stderr (终端) │ fd[3] ────────────→│ out.txt (文件) │ └─────────────────┘ 【执行 dup2(3, 1) 后】 ┌─────────────────┐ 进程 files_struct │ 内核文件表 │ │ │ fd[0] ────────────→│ stdin (键盘) │ fd[1] ────────────→│ out.txt (文件) │ ← ls 重定向写到这里 fd[2] ────────────→│ stderr (终端) │ fd[3] ────────────→│ out.txt (文件) │ └─────────────────┘ 【结果】ls 写入 fd[1] → 实际写入 out.txt 文件重定向的本质是改变文件描述符与文件之间的映射关系。5. dup2操作dup2可以让我们显式地“复制”一个文件描述符#include unistd.h int dup2(int oldfd, int newfd);它的作用让newfd指向oldfd所指向的文件。如果newfd已经打开会先关闭它。输出重定向示例int fd open(output.txt, O_CREAT | O_WRONLY | O_TRUNC, 0644); dup2(fd, 1); // 让 fd1 指向 output.txt printf(这条消息会写入文件而不是屏幕); fflush(stdout);输入重定向示例int fd open(input.txt, O_RDONLY); dup2(fd, 0); // 让 fd0 指向 input.txt char buf[256]; fgets(buf, sizeof(buf), stdin); // 实际从文件读取追加重定向只需在open时加上O_APPEND标志再用dup2重定向1int fd open(log.txt, O_CREAT | O_WRONLY | O_APPEND, 0644); dup2(fd, 1);6. 重定向操作在 Linux 中每个程序运行时默认打开三个特殊文件标准输入 (stdin)→ 文件描述符0标准输出 (stdout)→ 文件描述符1cout、printf标准错误 (stderr)→ 文件描述符2cerr、perror通过重定向我们可以把它们分别导向文件或合并。1. 只重定向标准输出./stream log.txt //也可以写成 ./stream 1log.txt等价于1仅将stdout写入log.txtstderr仍显示在终端2. 有标准输出和标准错误只重定向标准输出./stream hello cout hello printf hello cerr hello stderr ./stream log.txt hello cerr hello stderr./stream正常运行时cout/printf标准输出和cerr/stderr标准错误都打印到终端。./stream log.txt只把标准输出1重定向到log.txt文件没有重定向标准错误2所以hello cout和hello printf写入log.txt不在屏幕显示hello cerr和hello stderr仍然直接输出到终端屏幕3. 分别重定向 stdout 和 stderr./stream 1log.normal 2log.error1标准输出 →log.normal2标准错误 →log.error4. 将 stdout 和 stderr 重定向到同一个文件覆盖/追加# 方法一分别重定向但使用同一个文件追加模式 ./stream 1log.normal 2log.normal # 方法二将 stderr 合并到 stdout常用 ./stream 1log.normal 212表示追加到文件避免覆盖21表示将stderr重定向到stdout当前指向的位置三、理解“一切皆文件”什么是“一切皆文件”在 Windows 中只有常规文件才是“文件”。但在 Linux 中普通文件是文件如文本、二进制目录是文件硬件设备如磁盘、显示器、键盘也被抽象为文件进程信息如/proc下的内容是文件管道、Socket也是文件你能用read()读取一个文件也能用read()读取键盘输入、管道数据、甚至网络数据。统一接口带来的巨大好处开发者只需掌握一套 API如open、read、write、close就能操作 Linux 中绝大部分资源。几乎所有读取操作read()几乎所有写入操作write()这一统一抽象极大降低了学习成本和开发复杂性。内核中的关键数据结构struct file与struct file_operationsLinux 内核为每个打开的文件维护一个struct file其中包含文件的状态、位置、标志等。struct file { struct inode *f_inode; const struct file_operations *f_op; unsigned int f_flags; fmode_t f_mode; loff_t f_pos; // ... };其中最关键的成员是f_op它指向一个struct file_operations这个结构体里几乎全是函数指针struct file_operations { ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); int (*open)(struct inode *, struct file *); int (*release)(struct inode *, struct file *); // ... };系统调用与驱动的桥梁每个设备、文件类型都可以自定义自己的read/write实现当你调用read()时内核会通过file-f_op-read找到真正属于该设备的读取方法这就是多态在内核中的体现尽管键盘、显示器、普通文件的读写方式完全不同但开发者看到的接口是统一的。一张图理解进程调用 read() ↓ 内核中的 struct file ↓ f_op 指向 struct file_operations ↓ 调用该结构中的 read() 函数指针 ↓ 真正执行普通文件 / 键盘 / 管道 / Socket 的读取逻辑四、缓冲区什么是缓冲区缓冲区是内存中预留的一片区域用于暂存输入或输出数据。根据用途可分为输入缓冲区和输出缓冲区。它像是一个“中转站”在 CPU 与慢速设备如磁盘、打印机之间协调数据流动。为什么要引入缓冲区如果没有缓冲区每次读写文件都需要直接调用系统调用如read/write这会频繁触发 CPU 从用户态切换到内核态导致上下文切换开销巨大严重影响程序性能。引入缓冲区后可以减少系统调用次数一次读取大量数据到缓冲区后续操作直接从缓冲区读取。提高 CPU 利用率CPU 无需等待慢速 I/O 设备可以继续处理其他任务。协调速度差异缓冲区的读写速度远快于磁盘显著提升整体效率。 举个例子打印文档时数据先写入缓冲区打印机慢慢处理CPU 则可以继续执行其他任务。缓冲区刷新触发条件1. 强制刷新2. 刷新条件满足满足这个条件有下面三种情况3. 进程退出缓冲区的三种类型标准 I/O类型刷新条件典型场景全缓冲缓冲区满后才执行系统调用磁盘文件操作行缓冲遇到换行符或缓冲区满默认 1024 字节终端输入输出如 stdin、stdout无缓冲立即执行系统调用标准错误流stderr全缓冲的效率最高普通文件一般用这种方式行缓冲一般是显示器用无缓冲就是立即刷新写透模式WT一个小实验重定向带来的缓冲陷阱close(1); int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666); printf(hello world: %d\n, fd); close(fd);现象log.txt中没有任何内容。原因文件描述符 1stdout被重定向到磁盘文件后缓冲方式从行缓冲变为全缓冲。printf的内容未填满缓冲区因此未写入文件。解决方法调用fflush(stdout)强制刷新。而stderr是无缓冲的因此重定向 2 号描述符后perror的内容会立即写入文件。两级缓冲机制在 Linux 系统中数据从应用程序写入磁盘通常要经过两层缓冲区用户级缓冲区C 标准库提供如FILE结构中的缓冲区内核级缓冲区页缓存 Page Cache由操作系统内核管理数据流向如下应用程序 ↓ 用户级缓冲区printf/fwrite 写入 ↓ 内核缓冲区页缓存 ↓ 磁盘设备为什么需要两层缓冲层级提供者作用刷新方式用户级缓冲区C 标准库减少系统调用次数行缓冲/全缓冲/无缓冲fflush强制刷新内核级缓冲区OS 内核减少磁盘 I/O 次数提高磁盘读写性能由内核管理如fsync强制落盘关键区别fflush只将数据从用户级缓冲区刷新到内核缓冲区并不保证数据写入磁盘。fsync/sync才会强制将内核缓冲区的数据写入磁盘。图示数据写入全流程------------------- ------------------- | 应用程序代码 | | | | printf(hello) | | | ------------------ | 用户空间 | | | | v | | ------------------- | | | C 标准库 FILE 缓冲区 | | | | 用户级缓冲 | | | ------------------ | | | | | | fflush() | | v | | ------------------- | | | 系统调用 | | | | write(fd, ...) | | | ------------------ ------------------- | | 系统调用用户态 → 内核态 v ------------------- ------------------- | 内核页缓存 | | 内核空间 | | Page Cache | | | ------------------ | | | | | | fsync() | | v | | ------------------- | | | 磁盘设备 | | | | 物理存储 | | | ------------------- -------------------数据交给计算机交给硬件本质全是拷贝计算机数据流动的本质一切皆拷贝五、FILE 结构与用户级缓冲区FILE 是什么FILE是 C 标准库中定义的一个结构体用于管理文件流。它封装了文件描述符_fileno用户级缓冲区的指针读/写指针、基址、边界等缓冲区大小、标志位、锁等FILE 的形成过程简化调用fopenFILE *fp fopen(file.txt, r);fopen内部执行调用open系统调用获得一个文件描述符fd动态分配FILE结构体通常在堆上将fd赋值给FILE的_fileno字段根据打开模式如行缓冲、全缓冲初始化缓冲区可选默认可能分配 BUFSIZ 字节返回FILE*指针使用时如fread/fwrite/printf先操作FILE内部的用户缓冲区必要时调用write/read系统调用调用fclose刷新用户缓冲区fflush调用close系统调用关闭文件描述符释放FILE结构体内存图示用户调用 fopen(log.txt, w) │ ▼ ┌──────────────────────────────────────────────┐ │ glibc 中的 fopen 实现 │ │ 1. 调用 open() 系统调用 │ │ └─→ 内核创建 struct file分配 fd如 3 │ │ 2. 分配 FILE 结构体用户态堆内存 │ │ └─→ 填充 _fileno fd │ │ └─→ 分配缓冲区如 1024 字节 │ │ └─→ 设置缓冲类型默认全缓冲或行缓冲 │ │ 3. 返回 FILE* 指针 │ └──────────────────────────────────────────────┘FILE 结构体关键成员// /usr/include/libio.h struct _IO_FILE { int _flags; // 文件状态标志 char* _IO_read_ptr; // 读缓冲区当前指针 char* _IO_read_end; // 读缓冲区结束位置 char* _IO_read_base; // 读缓冲区基址 char* _IO_write_base; // 写缓冲区基址 char* _IO_write_ptr; // 写缓冲区当前指针 char* _IO_write_end; // 写缓冲区结束位置 char* _IO_buf_base; // 缓冲区基址 char* _IO_buf_end; // 缓冲区结束位置 int _fileno; // ← 封装的文件描述符核心 // ... 其他成员 };一个有趣的 fork 现象printf(hello printf\n); fwrite(hello fwrite\n, ...); write(1, hello write\n, ...); fork();正常运行输出一次但重定向到文件后printf和fwrite输出两次write只输出一次。原因重定向到文件后库函数缓冲变为全缓冲。fork时子进程复制了父进程的缓冲区写时拷贝。进程退出时父子进程各自刷新缓冲区导致重复输出。write无缓冲区不受影响。✅ 这说明用户级缓冲区由 C 标准库提供位于系统调用之上。