尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

为什么 close(1) 之后 printf 的内容进了文件?一文彻底搞懂 Linux 文件描述符与重定向

为什么 close(1) 之后 printf 的内容进了文件?一文彻底搞懂 Linux 文件描述符与重定向 为什么每个进程都能用printf往屏幕上打印为什么close(1)之后再打开文件就能实现输出重定向文件描述符的本质是什么这篇文章从 C 库函数到 Linux 系统调用一层层剥开 IO 的底层原理。目录一、文件是什么内容 属性二、C 语言的文件操作三、Linux 系统调用的文件操作四、flags 的位图传参智慧五、文件描述符的本质六、文件描述符分配规则七、重定向的原理与实现八、一切皆文件九、总结一、文件是什么内容 属性先建立第一个重要认知文件 文件的内容 文件的属性元数据log.txt ├── 内容文本/二进制数据 └── 属性文件名、大小、权限、创建时间、所属用户……未来对文件的操作也分为两类操作内容读写和操作属性chmod、chown 等。1.1 访问文件必须打开文件访问文件之前必须先打开文件——而打开文件之前得先找到文件找到文件 路径 文件名有时候我们不带路径比如fopen(log.txt, w)不是不需要路径而是访问任何文件都必须有路径要么用户自己提供/home/whb/log.txt要么使用进程的cwdcurrent working directory当前工作路径——默认就是cwd/log.txt每个进程启动后都会动态维护自己当前的工作路径。这就是之前讲进程时说的 cwd 属性。1.2 打开文件到底在做什么把文件从磁盘加载到内存。CPU 要访问文件内容而文件在磁盘外设上所以必须先加载到内存。文件 内容 属性所以要么加载内容到内存、要么加载属性到内存——取决于进程接下来要访问什么。对文件进行操作本质是进程通过 CPU 访问内存中的文件。1.3 打开的文件 vs 未打开的文件Linux 中存在大量文件从位置上分两类类型位置今天研究的对象未打开的文件磁盘上文件系统管理❌打开的文件内存中内核管理✅Linux 系统中可以同时存在很多被打开的文件OS 要管理它们——当然是先描述再组织struct XXX { // 文件属性 };为每个被打开的文件创建一个结构体再用链表/数组组织起来。这就是内核文件管理的全部秘密。二、C 语言的文件操作2.1 FILE 是什么const char *filename log.txt; FILE *fp fopen(filename, w); // 默认打开的是 cwd/log.txtFILE 是文件指针文件句柄本质是一个结构体。它里面封装了什么先记住这个问题第五部分揭晓答案。2.2 常用 C 文件函数FILE *fopen(const char *path, const char *mode); // 打开文件 int fputc(int c, FILE *stream); // 写一个字符 int fputs(const char *s, FILE *stream); // 写一个字符串 int getc(FILE *stream); // 读一个字符 char *fgets(char *s, int size, FILE *stream); // 读一行 size_t fread(void *ptr, size_t nmemb, FILE *stream); // 读数据块 size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); // 写数据块2.3 fopen 的 mode 参数详解mode含义r读。文件不存在则失败r读写。不存在不会新建w文件不存在就创建存在就清空就像echo filename重定向写入w读写。不存在会新建存在清空a追加写像echo filename追加重定向a读追加。不存在会新建利用 w 的特性可以写一个清空文件的工具#include stdio.h int main(int argc, char *argv[]) { if (argc ! 2) { printf(Usage: %s filename\n, argv[0]); return 1; } FILE *fp fopen(argv[1], w); // 以 w 打开文件被清空 if (fp NULL) { perror(fopen); return 1; } fclose(fp); return 0; }2.4 读写位置fseek / ftell / rewindint fseek(FILE *stream, long offset, int whence); // 设置读写位置 long ftell(FILE *stream); // 返回当前读写位置 void rewind(FILE *stream); // 回到文件开头读写位置的本质就是一个整数。文件内容类似一个char[]数组读写位置可以近似理解为数组下标。三、Linux 系统调用的文件操作C 库函数能操作文件OS 自己当然也能。Linux 提供了一套系统调用级别的文件接口#include sys/types.h #include sys/stat.h #include fcntl.h // 注意需要 3 个头文件 int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);3.1 flags 传这些宏宏含义O_RDONLY只读O_WRONLY只写O_RDWR读写O_CREAT不存在则创建需要第三个参数 modeO_TRUNC打开时清空对应 fopen 的 wO_APPEND追加写对应 fopen 的 a3.2 open write 完整示例#include stdio.h #include string.h #include unistd.h #include fcntl.h int main() { // 可在代码中设置掩码掩码默认 002就近原则 umask(0); // 新建 写 清空O_TRUNC 对应 fopen 的 w int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666); // 或者打开并追加O_APPEND 对应 fopen 的 a // int fd open(log.txt, O_WRONLY | O_CREAT | O_APPEND, 0666); if (fd 0) { perror(open); return 1; } printf(fd: %d\n, fd); const char *msg 123; write(fd, msg, strlen(msg)); // 注意strlen 不要 1 // \0 是 C 语言的规定和文件内容没有关系 close(fd); return 0; }两个重要细节write写入时用strlen(msg)不要1——\0是 C 字符串的终止符文件里不需要它。Linux 系统调用的写文件不会清空原有内容是直接覆盖。若新内容长度不足就会出现新旧交织的现象——所以需要O_TRUNC写之前清空或O_APPEND追加。四、flags 的位图传参智慧open 的 flags 参数为什么能用|组合多个宏这就是位图bitmap思维——C 语言单形参、多实参的传参智慧#include stdio.h // 每个宏只有一个比特位是 1 #define ONE (10) // 0000 0001 #define TWO (11) // 0000 0010 #define THREE (12) // 0000 0100 #define FOUR (13) // 0000 1000 void Print(int flag) { if (flag ONE) printf(one\n); if (flag TWO) printf(two\n); if (flag THREE) printf(three\n); if (flag FOUR) printf(four\n); } int main() { Print(ONE); // one Print(ONE | TWO); // one two Print(ONE | TWO | THREE); // one two three Print(ONE | TWO | THREE | FOUR); Print(ONE | FOUR); // one four return 0; }原理一个 int 有 32 个比特位每个比特位可以表示一个独立的开关。用|合并多个开关用检测某个开关是否打开。O_WRONLY | O_CREAT | O_TRUNC就是同时打开了只写、创建、清空三个开关。五、文件描述符的本质5.1 为什么 open 的返回值从 3 开始运行上面的代码fd 的值是 3。再打开一个文件是 4、接着是 5……那么0、1、2 呢fd含义C 语言对应0标准输入stdin1标准输出stdout2标准错误stderrextern FILE *stdin; // 一定封装了文件描述符 0 extern FILE *stdout; // 一定封装了文件描述符 1 extern FILE *stderr; // 一定封装了文件描述符 2这验证了库函数和系统调用是上下级关系操作系统只认数字0、1、2C 库的 FILE 结构体里封装了这些数字。所以可以直接用系统调用往屏幕写write(1, msg, strlen(msg)); // 和 printf 效果一样直接打到屏幕5.2 fd 的底层结构文件描述符的本质是什么看内核结构task_struct │ └── struct files_struct *files │ └── struct file *fd_array[] ← 文件描述符表 [0] → stdin 的文件对象 [1] → stdout 的文件对象 [2] → stderr 的文件对象 [3] → log.txt 的文件对象 ...进程和文件的比率是 1:n。我们研究打开文件就是在研究进程和文件的关系。打开一个文件的完整流程open(log.txt, ...) │ ├── 1. 找到文件路径 文件名 / cwd ├── 2. 把文件从磁盘加载到内存内容/属性 ├── 3. 内核创建 struct file 结构体描述该文件 │ 直接或间接包含文件的大部分属性 ├── 4. 内存和磁盘之间存在一个缓冲区 │ 每打开一个文件都会提供一个缓冲区 ├── 5. 把 struct file 的地址填入 fd_array 中 │ 最小未使用的下标 └── 6. 返回这个数组下标fd给用户write 的完整流程write(3, aaaaa) → 系统调用进入内核 → 拿着 fd3 去 files_struct 的 fd_array[] 查表 → 找到对应的 struct file 地址 → 把 aaaaa 拷贝到该文件的缓冲区 → 等待刷新flush到磁盘Linux 中访问打开的文件OS 只认 fd文件描述符。C 语言的 FILE 结构体中一定封装有这个文件描述符。5.3 C / C / Python 为什么各有各的文件操作语言文件操作本质Cfopen/fwrite/fcloseFILE 结构体封装 fdCifstream/ofstream类中一定有一个成员属性是 fdPythonopen()/write()同样是封装为什么每个语言都有自己的文件操作——跨平台性。每种语言通过封装使自己能适配不同的操作系统Windows、Linux、Mac。上层接口统一底层库随平台变化。所以 C 的 ofstream 和 C 的 FILE 是同一个性质内部都装着 fd。六、文件描述符分配规则规则分配文件描述符表中值最小的 没有被使用的fd。实验验证close(2); // 关闭标准错误 int fd open(log.txt, O_WRONLY | O_CREAT, 0666); printf(fd: %d\n, fd); // 输出 2注意一个细节关闭 2 之后之前 3 位置的文件不会自动填充回 2 的位置它待在原地新的 open 才来填补 2 的空位。这条规则是重定向的基石。7. 重定向的原理与实现7.1 输出重定向重定向的本质更改 fd_array 中某个下标的内容指向。C 语言层printf相当于fprintf(stdout, ...)而stdout里封装的 fd 是1。所以// 关闭 stdoutfd1 close(1); // 按照最小未使用规则新文件的 fd 就是 1 int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666); // 语言层并不知道我们改了指向 // printf 依然往 fd1 写 → 实际写进了 log.txt printf(fda: %d\n, 1);重定向前fd[1] → 屏幕 重定向后fd[1] → log.txt printf 不知道只管往 1 里写 → 输出重定向完成输出重定向 vs 追加重定向差别就是打开文件的方式——O_TRUNC清空是O_APPEND追加是。7.2 输入重定向close(0); // 关闭 stdinfd0 int fda open(log.txt, O_RDONLY); // 新文件拿到 fd0 char buffer[64]; fgets(buffer, sizeof(buffer), stdin); // 从 log.txt 读取而不是键盘7.3 dup2对已打开的文件进行重定向上面的方法需要先 close 再 opendup2 一步到位int dup2(int oldfd, int newfd); // 让 newfd 成为 oldfd 的拷贝必要时先关闭 newfd int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666); dup2(fd, 1); // 把 fd 数组下标 fd 的内容拷贝到下标 1 的位置 printf(aaaaaaa\n); // 输出进 log.txt拷贝的是 fd 数组下标中的内容struct file* 指针不是文件本身。7.4 重定向 程序替换 迷你 Shell 的重定向// 子进程中先重定向再 exec int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC, 0666); dup2(fd, 1); close(fd); execl(/usr/bin/ls, ls, -l, NULL); // ls 的输出进 log.txtexec 程序替换不会影响进程的 files_struct 文件描述符表——修改的只是代码区和数据区。fd 表是挂在 task_struct 上的内核数据结构属于进程的身份信息不是程序的内容。所以先 dup2 改好 fd 表、再 exec 换程序新程序天然继承重定向——这正是 Shell 实现ls log.txt的原理。八、一切皆文件Linux 的哲学一切皆文件。键盘、显示器、磁盘、网卡……都能用同一套接口open/read/write/close访问。怎么做到的把struct file_operations理解为基类各个硬件驱动实现为子类struct file_operations { // 基类统一的操作方法表 int (*read)(...); int (*write)(...); int (*open)(...); int (*close)(...); }; 键盘驱动file_operations 子类read 从键盘缓冲区取数据 显示器驱动file_operations 子类write 把数据渲染到屏幕 磁盘驱动file_operations 子类read/write 读写磁盘扇区每一种设备的访问方法都不一样IO 方式不同但上层的接口全都一样——这就是多态上层代码调write(fd, ...)实际执行哪个设备的 write由 fd 指向的 file 对象决定。进程角度看一切皆 struct file。九、总结完整知识链路文件 内容 属性 ↓ 访问前必须打开 把文件从磁盘加载到内存内核 struct file 描述 ↓ 先描述再组织 task_struct → files_struct → fd_array[]文件描述符表 ↓ 返回数组下标 fd open(...) ← 系统调用分配最小未使用的下标 ↓ C 库封装FILE 结构体内部装着 fd跨平台性 ↓ 重定向 更改 fd_array 下标中的内容 closeopen 或 dup2 ↓ exec 不碰 fd 表 → Shell 重定向成立 ↓ 一切皆文件file_operations 的多态核心概念速查问题答案文件 ?内容 属性打开文件在做什么把文件从磁盘加载到内存fd 的本质fd_array[] 数组的下标0、1、2 是什么标准输入、标准输出、标准错误FILE 是什么封装了 fd 的 C 结构体fd 分配规则最小且未使用的下标重定向的本质更改 fd_array 下标中的指向flags 为什么能用 | 组合位图思维一个 bit 一个开关为什么各语言文件操作不同跨平台封装底层都是系统调用本文基于 Linux 基础 IO 学习笔记整理覆盖从 C 库函数到文件描述符、从重定向原理到一切皆文件的完整知识链适合作为系统级 IO 的入门参考。如果觉得有帮助欢迎点赞收藏评论区交流讨论
返回列表