目录
两个细节
关于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呢?
| 描述符 | 宏 | 设备 |
|---|---|---|
| 0 | stdin | 标准输入(键盘) |
| 1 | stdout | 标准输出(显示器) |
| 2 | stderr | 标准错误(显示器) |
他们怎么封装的?——和熟悉的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 会做两件事:
把新程序(
ls)的代码和数据加载到内存,替换掉旧的代码和数据。重点来了:
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)写进文件