news 2026/10/19 7:14:15

【Linux】系统I/O

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Linux】系统I/O

目录

两个细节

关于fopen的操作

总结

系统I\O

open

write

close

read

标志位的传递

文本写入和二进制写入的理解

文件描述符

初步理解

文件描述符在内核怎么样发挥作用

重定向

一个现象

dup2

文件重定向的补充


两个细节

关于fopen的操作

如果以w方式打开文件的时候内部有内容的,就会先清空文件,看一下操作。

那么对应到命令行操作的时候,利用echo 输入内容>文件名,的方式输入内容会先清空文件,再写入新的内容。

以a方式打开,不会清空,会在后面追加。类似于命令行echo 内容>>文件名

信息写到文件里面要加上\0吗——不用

\0是C语言的内容,和文件没有关系。

总结

因为shell是C语言写的,因此我们可以推测出,这种命令行和fopen,有着调用和被调用的关系。

系统I\O

fopen,ifstream等流式是语言层的方案,其实系统的接口open才是打开文件的底层方案。

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);
参数含义
pathname要打开或创建的文件路径(如"myfile"或"/etc/passwd")
flags打开方式(如只读、只写、创建等)
mode权限(仅在使用了O_CREAT时有效)
返回值成功返回新的文件描述符(整数),失败返回-1并设置errno

write

#include <unistd.h> ssize_t write(int fd, const void *buf, size_t count);
参数含义
fd文件描述符(就是open的返回值)
buf指向要写入的数据缓冲区的指针(内存地址)
count期望写入的字节数(不是字符数,中文字符通常占多个字节)
返回值含义
> 0成功,返回实际写入的字节数。注意:这个数字可能小于你请求的count(比如磁盘满了)。
-1失败,并设置errno(比如没有写权限、磁盘满等)。
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { perror("open"); return 1; } const char *msg = "Hello write!\n"; // strlen(msg) 不包含 '\0',所以不会把 '\0' 写进文件 ssize_t n = write(fd, msg, strlen(msg)); if (n < 0) { perror("write"); } else { printf("实际写入了 %zd 个字节\n", n); } close(fd); return 0; }

close

#include <unistd.h> int close(int fd);
参数含义
fd要关闭的文件描述符
返回值含义
0成功
-1失败(比如传入了一个已经关闭的fd或无效的fd)

close的本质是:断开进程文件描述符表与内核file结构体的连接。

  • 释放资源:让内核的引用计数减 1。当引用计数降为 0 时,内核才会真正释放该文件占用的所有内存和资源。

  • 刷新数据:关闭文件时,内核会把该文件在内核缓冲区中还没写入磁盘的数据(脏页)刷新到磁盘(或者交给系统异步刷盘)。

  • 释放 fd 编号:关闭后,这个数字(比如 3)就空出来了,下次open新文件时,会优先分配这个最小的空闲编号。

注意:

  • 一定要检查返回值:虽然close很少失败,但在网络编程或写入大量数据后,如果close失败,可能导致数据没有真正写入磁盘。所以严谨的代码应当检查close的返回值。

  • 进程退出自动关闭:当一个进程退出时,操作系统会自动关闭该进程打开的所有文件描述符。

read

ssize_t read(int fd, void *buf, size_t count);
  • 参数:fd是文件描述符,buf是把数据读到哪里的内存地址,count是期望读取的字节数。

  • 返回值:

    • > 0:实际读到的字节数。

    • 0:代表读到了文件末尾(EOF)。

    • -1:读取失败(比如文件被删了,或者fd不对)。

标志位的传递

先看一下手写的代码

#include <stdio.h> #define ONE_FLAG (1<<0) // 0000 0000 0000...0000 0001 #define TWO_FLAG (1<<1) // 0000 0000 0000...0000 0010 #define THREE_FLAG (1<<2) // 0000 0000 0000...0000 0100 #define FOUR_FLAG (1<<3) // 0000 0000 0000...0000 1000 void Print(int flags) { if(flags & ONE_FLAG) { printf("One!\n"); } if(flags & TWO_FLAG) { printf("Two\n"); } if(flags & THREE_FLAG) { printf("Three\n"); } if(flags & FOUR_FLAG) { printf("Four\n"); } } int main() { Print(ONE_FLAG); printf("\n"); Print(ONE_FLAG | TWO_FLAG); printf("\n"); Print(ONE_FLAG | TWO_FLAG | THREE_FLAG); printf("\n"); Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG); printf("\n"); Print(ONE_FLAG | FOUR_FLAG); printf("\n"); return 0; }

这个代码可以预测得到open的falgs,类似运用了对应的宏,代表读写创造文件等。

1. 访问模式(必须且只能选一个)

标志含义
O_RDONLY只读打开
O_WRONLY只写打开
O_RDWR读写打开

2. 附加标志(可选)

标志含义
O_CREAT若文件不存在,则创建它。使用此标志必须提供第三个参数mode来指定权限。
O_TRUNC若文件已存在,且以写方式打开,则将其长度截断为 0(清空内容)。
O_APPEND追加模式。每次写操作前,文件偏移量都会自动移动到文件末尾。
O_EXCL配合O_CREAT使用。若文件已存在,则报错(防止覆盖)。
#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { umask(0);//小知识点,设置掩码,默认就是666,不需要&002 int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); //打开文件的时候如果没有就创建,可以写入,同时要清空原来的内容再写入 int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666); //APPEND就是不清空,就要追加了 if(fd < 0) { perror("open"); return 1; } printf("fd: %d\n", fd); const char *msg = "hello bit\n"; int cnt = 5; while(cnt) { // 当做字符来写 write(fd, msg, strlen(msg)); cnt--; } close(fd); return 0; }

文本写入和二进制写入的理解

#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { umask(0); int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); //系统不关心你的文件“模式”,只认操作标识 //int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666); if(fd < 0) { perror("open"); return 1; } printf("fd: %d\n", fd); //const char *msg = "abcd"; int cnt = 1; int a= 1234567; while(cnt) { // 当做字符来写 ---,系统不关心你分几次写,它只看到连续的字节流 char buffer[16]; sprintf(buffer, sizeof(buffer), "%d", a); write(fd, buffer, strlen(buffer)); cnt--; }//无论你怎么转成字符串,进去的只是字节 close(fd); return 0; }

当你调用write时,系统内核的态度是:

“我不管你写的是printf格式化出来的字符串、是一个图片、还是一段二进制代码。我只知道你有fd,你给我一个内存地址(buffer),你告诉我写多少个字节(strlen),我就负责把这些字节原封不动地顺着(文件描述符)塞进磁盘里。其他的一概不管。”

文件描述符

上述代码的fd就是文件描述符

初步理解

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { umask(0); int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); int fd4 = open("log4.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd1 < 0)exit(1); if(fd2 < 0)exit(1); if(fd3 < 0)exit(1); if(fd4 < 0)exit(1); printf("fd1: %d\n", fd1); printf("fd2: %d\n", fd2); printf("fd3: %d\n", fd3); printf("fd4: %d\n", fd4); close(fd1); close(fd2); close(fd3); close(fd4); }

最后文件描述符打印出来分别是3,4,5,6

那0,1,2呢?

描述符宏设备
0stdin标准输入(键盘)
1stdout标准输出(显示器)
2stderr标准错误(显示器)

他们怎么封装的?——和熟悉的FILE*有关,FILE是一个结构体封装了文件描述符

总结:这个fd是系统级别的,OS只认他,他的值决定了文件的流。

文件描述符在内核怎么样发挥作用

1. 建立连接(图上的红色标号 1 和 6)

  • 用户层调用open("log.txt")。

  • OS 系统层收到指令,在磁盘上找到了log.txt。

  • 如图所示,OS 会在内存中为这个打开的文件创建一个庞大的struct file结构体(图中右侧的方框)。这个结构体里包含了文件的所有属性、读写位置、甚至缓存区(Buffer)。

  • 同时,OS 会去磁盘把文件的一部分内容预加载(图注 5)到内核的“缓冲区”中,为了后面读文件提速。

2. 分配门牌号(图上的红色标号 2 和 3)

  • 既然有了struct file,OS 要把这个结构体交给谁呢?交给当前进程。

  • 看图中间,进程的task_struct里有一个files_struct,里面装着fd_array。

  • OS 从下标 0 开始找,发现 0, 1, 2 被占用了,于是把新创建的struct file的地址填入下标为3的位置。

  • 此时,open函数返回了整数3给用户。这个3就是 fd。

3. 读写数据(读文件流程)

  • 你执行read(fd, ...)。

  • OS 看到fd = 3,立刻去当前进程的fd_array中找第 3 号位置。

  • 通过第 3 号位置里的指针(图注 3 的箭头),精准找到了刚才创建的struct file。

  • 然后 OS 把内核缓冲区里的数据,拷贝到你用户层的buffer里。

4. 关闭文件

  • 你执行close(fd)。

  • OS 直接把fd_array第 3 号位置的指针清空(置为 NULL)。

  • 当这个struct file没有其他进程引用时,OS 销毁它,释放资源。这个 3 号下标就空出来了,下次open又会优先分配它。

重定向

在 Linux 中,输入输出重定向完全依赖于底层文件描述符(fd)的分配规则。

回想一下我们之前说的规则:open打开文件时,系统总是分配当前未被使用的、最小的 fd。

close(1); int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); printf("fd: %d\n", fd);

一个现象

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { close(1); int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); printf("fd: %d\n", fd); close(fd); return 0; }

屏幕上什么都不会打印,log.txt文件会被创建,但里面大概率也是空的(或者只有部分内容)。

这个和缓冲区有关。

dup2

#include <unistd.h> int dup2(int oldfd, int newfd);
参数含义
oldfd已经打开的、有效的文件描述符(源)
newfd你想要覆盖的目标文件描述符(目标)
  • 成功:返回newfd(目标描述符)

  • 失败:返回-1

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd < 0) exit(1); dup2(fd, 1); close(fd); printf("fd: %d\n", fd); printf("hello world\n"); printf("hello world\n"); printf("hello world\n"); fprintf(stdout, "hello stdout\n"); fprintf(stdout, "hello stdout\n"); fprintf(stdout, "hello stdout\n"); fprintf(stdout, "hello stdout\n"); const char *msg = "hello world\n"; write(fd, msg, strlen(msg)); return 0; }

进程替换会影响重定向的结果吗?

当我们调用fork()创建子进程时,子进程会复制父进程的文件描述符表(fd_array)。

当子进程调用exec替换成新程序(比如ls)时,OS 会做两件事:

  1. 把新程序(ls)的代码和数据加载到内存,替换掉旧的代码和数据。

  2. 重点来了:exec绝对不会动进程的task_struct,也绝对不会动里面的files_struct(文件描述符表)。

所以,无论你把当前程序替换成什么,0号、1号、2号 fd 依然稳稳地指向你之前用dup2设定的目标文件。

文件重定向的补充

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc, char *argv[]) { if(argc != 2) exit(1); int fd = open(argv[1], O_RDONLY); if(fd < 0) exit(1); dup2(fd, 0); close(fd); while(1) { char buffer[64]; if(!fgets(buffer, sizeof(buffer), stdin))break; printf("%s", buffer); } return 0; }
  • 正常情况下,stdin(0 号 fd)指向的是键盘。

  • 当你执行./a.out log.txt时,fd拿到了log.txt。

  • dup2(fd, 0)把 0 号的下标强行指向了log.txt。

  • 这样一来,程序里所有的fgets(..., stdin)都不会再去等键盘输入了,而是去读取 log.txt 文件里的内容。读完后遇到 EOF(文件末尾),fgets返回 NULL,循环结束。

./a.out 1>log.txt 2>&1 # 把屏幕输出(1)和错误(2)写进文件
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 7:13:12

《AI Agent原理与实战》2026版 第18课 MCP 协议深度实战

本文收录于专栏 《AI Agent原理与实战》2026版 —— 专栏系统覆盖 AI Agent 的记忆、工具、插件与实战&#xff0c;点击订阅可跟踪后续更新。 本课定位&#xff1a;框架各自为政的时代正在被一个协议终结&#xff1a;MCP。本课从 NM 集成困境讲到 server/client/transport 架构…

作者头像 李华
网站建设 2026/10/19 7:12:21

企业管理系统设计要点:从组织建档、人事薪酬到项目协同

企业管理系统设计要点&#xff1a;从组织建档、人事薪酬到项目协同企业信息化到一定阶段&#xff0c;常见的问题往往不是缺系统&#xff0c;而是系统太多&#xff1a;人事一套、财务一套、项目进度靠群聊、审批靠签字&#xff0c;同一批人、同一套组织&#xff0c;数据却对不到…

作者头像 李华
网站建设 2026/10/19 7:10:36

黑灯工厂背后AI赋能逻辑:汽车智能制造从数据到闭环的落地路径

在制造业圈子里&#xff0c;“黑灯工厂”这几年被反复提及&#xff0c;汽车行业更是热衷谈这个概念。冲压线、焊装线、总装线上&#xff0c;照明熄灭之后设备依然自动运转&#xff0c;机器人有序焊接、搬运、检测&#xff0c;只有零星几个运维人员拿着终端在巡视。第一次看到这…

作者头像 李华
网站建设 2026/10/19 7:10:03

三种典型语言下的中小学计算思维课堂设计实践指南

1. 从编程课到思维课&#xff1a;这个课堂设计到底在解决什么问题先聊一个我观察了很久的现象。很多中小学的信息科技课&#xff0c;表面上在教编程&#xff0c;实际上一节课下来&#xff0c;学生记住的只是某个积木块的颜色、某个指令的拼写&#xff0c;或者“照着老师的代码抄…

作者头像 李华
网站建设 2026/10/19 7:07:15

Python爬虫实战:按公司名批量抓取LinkedIn员工资料的完整方案

简介&#xff1a;LinkedinSpider是一份基于Python的开源爬虫项目&#xff0c;核心功能是根据公司名称自动抓取LinkedIn上的员工公开资料&#xff0c;常用于人才招聘、市场调研和学术网络分析。压缩包总大小仅5KB&#xff0c;共包含3个文件&#xff0c;核心为一份Python脚本&…

作者头像 李华
网站建设 2026/10/19 7:04:03

SAP顾问如何写出开发零歧义的功能说明书(FS)?

在SAP圈子里&#xff0c;流传着一句大实话&#xff1a;不会写FS的顾问&#xff0c;迟早要“还债”。刚入行的时候&#xff0c;我总觉得功能说明书就是把业务需求“翻译”给开发听&#xff0c;写个大概意思就等着ABAPer交代码。直到某次上线前夜&#xff0c;开发当着项目经理的面…

作者头像 李华