hello,各位小伙伴,本篇文章跟大家一起学习《Linux:文件与fd(被打开的文件)》,感谢大家对我上一篇的支持,如有什么问题,还请多多指教 !
如果本篇文章对你有帮助,还请各位点点赞!!!
话不多说,开始正题:
文章目录
- 前置准备
- 被打开的文件——内存
- 文件IO基础讲解
- 三大流
- 系统调用接口
- 文件描述符`fd`
- 文件描述符表
- 如何理解硬件也是文件
- 文本写入 vs 二进制写入
- `IO`的基本过程----文件内核级缓冲区----重定向
- fd的分配
- 完善`sehll`
- 回归上面的问题
前置准备
问大家一个问题,空文件有没有大小?答案是有:
- 文件 = 内容 + 属性
即使一个文件是空文件,也只是没有内容而已,还是要有相关的属性如:名称、创建时间、权限…… - 在访问任何一个文件之前,都必须打开它,为什么?
想想在访问文件之前,文件在哪?没错,在磁盘上 - 访问一个文件,是谁在访问?
当我们写完一个有fopen
的代码的时候,文件打开了吗?没有
当我们将其转化为可执行文件的时候,文件打开了吗?没有
当我们运行可执行文件的时候,文件打开了吗?当fopen
运行结束,文件打开了!!
所以,访问文件的是进程!!! - 进程是在内存当中的,本质上就是
CPU
在跑进程,但是文件在磁盘中,根据冯诺依曼原理,CPU
只能跑内存上的数据,所以,要想访问文件,就必须把文件加载到内存当中,也就是说,打开文件,其实就是将文件加载到内存当中!! - 将一个文件加载到内存,无非就是加载属性或者内容嘛,一个进程能够打开一个文件,那么能打开多个文件吗?包的!那一个系统里那么多进程,岂不是非常多文件被打开?那么操作系统要不要对其进行管理呢?答案是肯定的!如何管理:
先描述再组织!
- 盲猜一手:在内核中:文件 = 内核数据结构 + 文件内容
- 进程 = 内核数据结构 + 程序的代码和数据;和文件十分相像,所以结论:
我们在研究打开的文件,是在研究进程和文件的关系! - 那没有被打开的文件呢?在哪里?在磁盘中!
以下是本文所要讲解的内容:
- 被打开的文件——内存
- 没有被打开的文件——磁盘
- 二者结合就是文件系统!
被打开的文件——内存
文件IO基础讲解
fopen
有很多个选项:
首先讲解w
:打开文件,若文件不存在会帮助我们创建文件;若文件存在,将会把原先的文件内容清空。
这个操作和命令行中的>
输出重定向很相像,一样是清空原先文件内容,再将内容输出在文件中,但是若>
右侧没有内容,相当于清空文件。
myfile.c:
#include <stdio.h>int main()
{FILE *fp = fopen("log.txt", "w");if(fp == NULL){perror("fopen");return 1;}char buffer[1024];const char *message = "hello";int i = 0;while(i < 10){snprintf(buffer, sizeof(buffer),"%s: %d\n", message, i);fputs(buffer, fp);i++;}fclose(fp);return 0;
}
可以看到,执行了两次myfile
,log.txt
的内容并没有变化,那是当然!
要是把fopen
的w
改为a
:
发现端倪了吧!其实a
就是append
的意思——追加,所以打印结果才会这样,追加并不会将原先的文件清空
在命令行中也有追加重定向>>
:
剩下的选项都比较简单就不讲了
三大流
一个程序在默认启动时给我们打开这三种输出输入流
- stdin 标准输入 键盘
- stdout 标准输出 显示器
- stderr 标准错误 显示器
可以看到这三种输出流都属于FILE*
,这不和fopen
的返回值一模一样吗?
其实这三种输出流都是C语言给我们默认提供的
但是键盘、显示器都是硬件啊,怎么能联系在一起?
没错,我们语言层是不能直接与硬件层直接互通的,必须通过操作系统,才能实现互通
所以,我们所用的C文件接口,底层一定要封装对应的文件类系统调用!
系统调用接口
open:
- 第一个参数
pathname
就是要打开文件的路径名称 - 第二个参数
flags
就是标记位
主要用得到标记位:
这些标记为其实都是些宏,对于flags
看上去是一个整型int
,实际上是一个32比特位的位图,而这些宏,实际上就是只有一个比特位为1的值
其实很好理解,看如下代码:
#include <stdio.h>#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
#define FIVE (1<<4)void Test(int flags)
{if(flags & ONE){printf("ONE\n");}if(flags & TWO){printf("TWO\n");}if(flags & THREE){printf("THREE\n");}if(flags & FOUR){printf("FOUR\n");}if(flags & FIVE){printf("FIVE\n");}
}int main()
{Test(ONE);printf("========================\n");Test(ONE | TWO);printf("========================\n");Test(ONE | TWO | THREE);return 0;
}
我们使用一下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{open("log.txt", O_WRONLY | O_CREAT);return 0;
}
文件是创建出来了,怎么权限是乱码啊?
要记住,我们上述使用的fopen
是已经被包装好的函数,我们在这使用的open是系统级的调用接口,是不会帮助我们设置权限的,也没有权限去帮我们设置默认权限(因为都是系统级别),需要我们在创建的时候设置好默认权限!
怎么设置,我们上述参数还少了一个没讲哦,没错,就是mode_t mode
,所以一般我们都建议使用三参数的open,除非你只读或只写并且文件存在,看如下代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{open("log.txt", O_WRONLY | O_CREAT, 0666);// 一般建议在前面加0return 0;
}
权限变正常了,但是为什么不是我设置的0666
的权限?这是因为权限掩码的问题:
在设置权限的时候,你设置的权限会按位与上权限掩码,然后取反,得到的结果就是最终的权限!
当然了umask
也是可以改变的:
umask(0);
将其设置为0之后,我们设置的权限就是最终的权限:
要注意的是:你在程序里设置的umask
并不会影响系统的umask
,也就是说每个进程都会有属于自己的umask
,继承于父进程的umask
你也可以修改系统的权限掩码:
umask 0777
剩下最后就是open的返回值:
返回值是一个文件描述符,并且是个int
,那到底是什么呢?我们看一下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd1 = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd1 < 0){perror("open");}printf("fd1: %d\n", fd1);return 0;
}
3
是个什么鬼?获取了这个文件描述符后,就可以关闭这个文件close
,也可以在关闭文件前读取文件read
或者写入文件write
:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd1 = open("log.txt", O_WRONLY | O_CREAT, 0666);if(fd1 < 0){perror("open");}const char *buffer = "hello\n";write(fd1, buffer, strlen(buffer));printf("fd1: %d\n", fd1);close(fd1);return 0;
}
写入成功!!!读取也是一样的道理就不多说了
直入主题,要是此时把hello
改成##
会怎样?
const char *buffer = "##";
噢!!!发生了覆盖,根据现象这就很好理解了吧,你并没有告诉程序你要清空原先内容,所以,程序就直接覆盖写入,好理解吧,那要是我想清空文件?O_TRUNC
这不就有了么:
int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
那要是我不想覆盖,而是想追加呢?O_APPEND
int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
其实讲这么多总结出:
fopen、fclose、fwrite、fread
——C库函数open、close、write、read
——系统调用- C库函数就是系统调用封装的
文件描述符fd
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);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);return 0;
}
为什么是这样子?我们知道< 0
就是打开文件失败,但是为什么没有0、1、2
?
还记得上文讲到在默认启动时给我们打开这三种输出输入流
- stdin 标准输入 键盘
- stdout 标准输出 显示器
- stderr 标准错误 显示器
这不就刚刚好对应上我们的0、1、2
了吗,test一下:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{write(1, "hello\n", 6);return 0;
}
测试成功!!!测试一下read
:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{char buffer[128];ssize_t s = read(0, buffer, sizeof(buffer));if(s > 0){buffer[s - 1] = 0;}printf("%s\n", buffer);
}
read
的返回值是:实际上读取到了多少个字符,如果返回值s
大于0,说明读取成功,又因为我们再输入完成后会多输入一个\n
的字符,所以将buffer
最后一个字符设置为0
,字符串最后以\0
结尾顺带把\n
去掉:
对于ssize_t
你可以直接当作是种整数,是一种有符号的整数
再补充一个细节,能不能在写入的时候把\0
也写进去:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);const char *buffer = "aaa";write(fd1, buffer, strlen(buffer) + 1);close(fd1);return 0;
}
看上去没什么问题,实际上问题大了,文件确实能够成功创建,但是当我们vim
进入看一下文件时:
多了一个^@
,兄弟萌,别被骗了啊,我们只是在C、C++语言上字符串以\0
做结尾,文件上并没有说过啊!!
所以写进去会发现是一个不可读取的字符,就是乱码
- 从文件读取要记得加
\0
做结尾 - 写入文件要记得去掉
\0
那请问大家,有什么时候我们会用上0、1、2、3、4……
,答案是:数组下标
文件描述符表
对于操作系统来讲,打开一个文件就是将文件从磁盘中加载到内存当中,会将文件的内容和属性加载进内存
在内存里会有已经打开了的文件描述符0、1、2
,他们以链表的形式串联起来,当新增一个打开的文件的时候,就会插入一个新节点struct_file
,所以打开文件和关闭文件不就是相当于对链表的增删查改了吗?
但是在内存里有成千上百个进程,这个链表又不能与进程强相关,那该怎么办?
在进程的数据结构task_struct
中有struct files_struct *files
,指向一个结构体struct files_struct
,这个结构体里有一个文件描述符数组struct file* fd_array[N]
,这些数组有自己的下标,数组元素指向着不同的文件,要记住:**Linux下一切皆文件!**所以硬件也不除外
所以在我们调用接口打开一个文件的时候,open
就会遍历数组struct file* fd_array[N]
,发现0、1、2
都被占了,找到3
没有被占,于是就把文件地址填写进3
这个位置,并且把3
返回给应用层
在系统层面上访问文件只有一条路:文件描述符,没有其他路可以走!
好,抛出问题:什么是FILE、FILE*
,这些都是stdin、stdout、stderr
的类型
FILE、FILE*
跟上面的图的内容没有任何关系!!
FILE
是一个结构体struct FILE
,是C语言封装好的结构体,里面必然会有很多与文件相关的属性,虽然我不知道有什么,但是肯定有文件描述符fd
,为什么?因为刚刚讲过,在系统层面上访问文件只有一条路:文件描述符,没有其他路可以走!再加上fopen、fclose、fwrite
的底层就是系统调用的接口,所以FILE
结构体肯定有维护着的文件描述符,test一下:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{printf("stdin: %d\n", stdin->_fileno);printf("stdout: %d\n", stdout->_fileno);printf("stderr: %d\n", stderr->_fileno);FILE *fp = fopen("log1.txt", "w");printf("fp: %d\n", fp->_fileno);return 0;
}
测试成功!!!
如何理解硬件也是文件
硬件和操作系统之间会有一个驱动程序来关联,操作系统和语言层之间会有系统调用接口来关联
对于那么多的硬件,操作系统也是要对硬件进行管理的,同样是先描述,再组织,会产生一个结构体struct device
来对硬件进行描述,然后就像task_struct
一样用链表串起来,当需要进行工作时,就放进工作队列
在结构体struct file
里,描述着键盘、屏幕、网卡……
,肯定会有读read
和写write
的方法,由于每种硬件的读和写都是不一样的,在C语言中,结构体本身不能包含方法(函数),但是我们可以使用函数指针,要是没有读或写就直接将函数指针设置为空即可,对于每种不同的硬件,完善不同的读和写,再用函数指针指向相应的函数:
在系统层面做了一层软件的封装,一切皆文件!!虚拟文件系统vfs
对于上图操作,就会联想到C++
的多态。
文本写入 vs 二进制写入
先回答一下为什么语言喜欢做封装?C/C++
显示器显示12345
是怎么显示的?是直接一个字符串12345
,还是说1
、2
、3
、4
、5
一个一个字符显示?
答案是后者!所以显示器也叫做字符设备,我们都知道ACSII
码,系统会将ASCII
解释成不同的字符
我们试一下用write
接口来对显示器输出一个整数:
#include <stdio.h>
#include <unistd.h>int main()
{int a = 12345;write(1, &a, sizeof(a));return 0;
}
为什么会输出90
而不是12345
啊?那是因为显示器只认识字符,当12345
这个数转换为二进制的后,被识别成90
了而已,所以我们要自己手动将12345
转换为字符串的格式,但是这样也太麻烦了,于是系统就有了自己封装好的函数:
也叫做格式化输出,在输入上也是同理,是一个一个字符输入的,叫做输入格式化
这是方便用户操作!
有没有一种可能,当平台不再是Linux,而是Windows、Macos?这个时候如果我们只能使用系统调用接口,在这些平台上不就不能跑了吗?所以C语言封装了接口之后,不管什么平台,我们都只需要对封装好的了接口进行操作,不需要考虑底层的系统调用的接口,这就提高了语言的可移植性!
C语言封装好的库为什么能够消除平台的差异性?那是因为在库里是有源代码的,里面有很多不同平台下的代码,也就是说当你是Linux,就留下Linux版本的接口,其他的全部去掉
文本写入和二进制写入在系统层面是没有区别的,所谓的格式是用户层自己决定的,要形成可执行程序、视频音频……就写二进制,要形成文本文件,就写文本。
为什么C语言喜欢封装:
- 方便用户操作
- 提高了语言的可移植性
IO
的基本过程----文件内核级缓冲区----重定向
在文件描述符表里的元素所指向的文件struct file
(就比如说标准输出、标准输入、标准错误、打开的文件……),都会有操作表
和文件的内核级缓冲区
,所谓的操作表其实就是上述所讲的函数指针的集合,因为不同的文件,输入输出方式不一定一样,文件的内核级缓冲区是什么?看如下代码write
:
write(stdout, "hello", ...);
里面的hello
就是文件内核级缓冲区所存放的东西,进程在调用write
的时候会从task_struct
找到files_struct
,再从files_struct
里面文件描述符表
找到对应的文件,然后根据操作表执行操作,例如write
,系统就会把缓冲区的数据直接刷新到对应的文件中,这个刷新的行为是由操作系统自主决定的,write
其实就相当于把用户里的数据拷贝到缓冲区里头,也就是从用户拷贝到内核,也就是个拷贝函数
要注意:每个文件都有自己的操作表和缓冲区
- 所以我们写Word文档要点保存呢?就是把缓冲区的数据刷新到磁盘里头,为什么不直接写入到磁盘里呢?那是因为磁盘的
IO
速度太慢了 - 同理
read
只不过和write
反过来了,要是缓冲区没有数据,就从磁盘中把数据刷新到缓冲区,再让read
把缓冲区的数据拷贝到用户层 - 所以
write
和read
都是拷贝函数 - 对于修改操作,就是把磁盘中的数据加载到缓冲区(
内存
),然后再进行修改----先读取,再写入 - 缓冲区的存在就是为了提高
IO
效率,减少IO
次数
fd的分配
对于文件描述符是怎么分配的,先看如下代码:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);printf("fd3: %d\n", fd3);printf("fd4: %d\n", fd4);return 0;
}
因为0、1、2
已经被占用,所以这是正确的,但是要是我把0、1、2
其中一个给关闭了呢?
close(0);
发现fd1
变成0了,后面的fd
也紧跟其后变成3、4、5
,那要是我把2
给关了呢?
close(2);
文件描述符在分配的时候会扫描整个文件描述符表,找到最小的并且没有被使用的文件描述符,分配给新打开的文件
但是假如我们把文件描述符1
给关了呢?显示器就不会显示打印结果了,那是不是应该把打印结果写入到log1.txt
里面了呢,毕竟把文件描述符1
分配给他了,看结果:
我们vim log1.txt
:
解释为什么写进去了:当我们把1
给关闭了,打开新的文件,那么文件描述符1
就分配给了新打开的文件,但是printf
这个函数默认把数据刷新给文件描述符1
,此时的1
正是新打开的文件,所以打印结果就会写入新打开的文件
果然写进去了,但是要是程序里把文件关闭了,还会打印吗?
close(fd1);
close(fd2);
close(fd3);
close(fd4);
我们vim log1.txt
:
依然是空白一片,也就是说根本没有把内容给写入进去,凭什么啊?我明明是先printf
才关闭的文件,为什么内容没写入进去
别急,我们在关闭文件前,加个fflush
看看,是不是恍然大悟:
fflush(stdout);
运行程序时找让没有打印结果,但是log1.txt
里面确实是有内容了!为什么刷新了就会写入数据了呢?
fflush(stdout);
实际上是刷新标准输出流stdout
,使得缓冲区中的内容立即写入到实际的输出设备(通常是控制台或终端)。在 C 语言中,stdout
是一个标准的输出流,通常是缓冲的。fflush()
是一个标准库函数,其作用是清空(刷新)输出流的缓冲区。stdout 是文件描述符为 1 的标准输出流,因此 fflush(stdout) 本质上就是刷新文件描述符为 1 的缓冲区。
也就是说并非没有写入数据,只是我们把数据写入到了标准输出的缓冲区里,但是并没有刷新到磁盘中就关闭了文件,当然看不到内容
这就是**重定向!!!**只不过这种做法有点不怎么优雅,我们来看看更优雅的做法:
其实我们并不需要把原先的文件描述符关闭,然后再重新分配,我么直接把新的文件的文件描述符所指向的文件的地址覆盖原先的文件描述符:
然后我们只需要把一开始新的文件描述符(也就是多余的)关闭,不就完成了重定向了吗:
那么问题来了,oldfd
和newfd
是什么?先看代码:
dup2(fd1, 1);
如果要实现我们上述的重定向,将1
重定向到log1.txt
,就应该这么写,这里的newfd
实际上就是被覆盖的那个,因为是被覆盖了,所以就是新的,很好理解,那么oldfd
就是保持不变的那个,因为不变,所以就是旧的
那么被覆盖的文件描述符所指向的文件是会自动关闭的,但是新的文件描述符并不会自动关闭,也就是说会有两个文件描述符指向同一个文件,这样可以的吗?答案是可以的。
在struct file
里是有一个引用计数来记录有多少个文件描述符指向该文件的,这也就意味着以后可以多个进程共享一个文件
当然你也可以自己关闭,我这里建议是不用关闭
我们来玩一下:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC);printf("fd : %d\n", fd);fprintf(stdout, "fd : %d\n", fd);fputs("hello\n", stdout);const char * buffer = "hello\n";fwrite(buffer, 1, strlen(buffer), stdout);return 0;
}
好,我们使用dup2
:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC);dup2(fd, 1);printf("fd : %d\n", fd);fprintf(stdout, "fd : %d\n", fd);fputs("hello\n", stdout);const char * buffer = "hello\n";fwrite(buffer, 1, strlen(buffer), stdout);return 0;
}
大家也可以试一下用O_APPEND
的方式打开文件,追加重定向!
输入重定向,由于简单,直接给代码:
此时我的test.txt
是存在数据的
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{int fd = open("test.txt", O_RDONLY);dup2(fd, 0);char buffer[2048];size_t s = read(0, buffer, sizeof(buffer));if(s > 0){buffer[s] = 0;printf("stdin redir:\n%s\n", buffer);}return 0;
}
现在弄人从显示器上读取就变成了从我的文件test.txt
读取:
完善sehll
好了,现在我们可以实现重定向了,如果再加上命令行参数,我们把上一篇文章的shell
给继续完善,使它支持重定向
以下代码是为了检测到是否重定向:
// 检测是否为空格
#define TrimSpace(pos) do{\while(isspace(*pos)){\pos++;\}\
}while(0)#define NoneRedir 0
#define InputRedir 1
#define OutputRedir 2
#define AppRedir 3int redir = NoneRedir;
char *filename = nullptr;void ParseCommandLine(char buffer[], int len) // 3. 分析命令
{(void)len; // 解除报警gargc = 0;// 重定向redir = NoneRedir;filename = nullptr;int end = len - 1;while(end >= 0){if(buffer[end] == '<'){redir = InputRedir;buffer[end] = 0;filename = &buffer[end] + 1;TrimSpace(filename);break;}else if(buffer[end] == '>'){if(buffer[end - 1] == '>')// 进一步判断是不是追加重定向{redir = AppRedir;buffer[end] = 0;buffer[end - 1] = 0;filename = &buffer[end] + 1;TrimSpace(filename);break;}else{redir = OutputRedir;buffer[end] = 0;filename = &buffer[end] + 1;TrimSpace(filename);break;}}end--; }memset(gargv, 0, sizeof((char*)buffer)); // 为了保证安全,清空历史字符串const char *sep = " "; // 作为分割符gargv[gargc++] = strtok(buffer, sep);while(gargv[gargc++] = strtok(nullptr, sep));gargc--;
}
我们知道,重定向的工作都是子进程去做的,那么程序替换会不会影响重定向?
答案是:不会
虚拟内存mm_struct
通过页表映射到物理内存,程序替换只是将物理内存进行覆盖,不会产生新的进程,必要时写时拷贝,然后修改页表,而我们的文件描述符表在file_struct
里面,是自己的数据结构,所以在程序替换前所做的重定向不会受到影响!!
以下代码是执行重定向命令:
bool ExecuteCommand() // 4. 执行命令
{pid_t id = fork(); // 让子进程去执行命令if(id < 0) return false;if(id == 0){if(redir == InputRedir){if(filename){int fd = open(filename, O_RDONLY, 0666);if(fd < 0){exit(2);}dup2(fd, 0);}else{exit(1);}}else if(redir == OutputRedir){if(filename){int fd = open(filename, O_TRUNC | O_CREAT | O_WRONLY, 0666);if(fd < 0){exit(4);}dup2(fd, 1);}else{exit(3);}}else if(redir == AppRedir){if(filename){int fd = open(filename, O_CREAT | O_APPEND | O_WRONLY, 0666);if(fd < 0){exit(6);}dup2(fd, 1);}else{exit(5);}}else{// do nothing}execvpe(gargv[0], gargv, genv);exit(0);// 不让子进程去跑后面的代码}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){if(WIFEXITED(status)){lastcode = WEXITSTATUS(status);}else{lastcode = 100;}return true;}return false;
}
回归上面的问题
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{close(1);int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd1: %d\n", fd1);//fflush(stdout);close(fd1);return 0;
}
为什么我们在使用printf()
的时候我们不需要fflush
就可以直接将数据从缓冲区刷新到磁盘中,但是fclose(stdout)
之后就需要手动刷新?
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{close(1);int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd1: %d\n", fd1);fflush(stdout);close(fd1);return 0;
}
我们使用的printf、fputs、sacnf……
都是C语言,底层都是系统调用,都是把数据拷贝到另一地方,但是系统调用是有成本的(时间或者空间
)
我们自己写的函数都是有成本的,更别说实现系统调用的函数,成本只会更高,我们的内联、宏就是为了减少我们函数的调用,你看我们自己写的函数都要尽可能的减少调用,更别说系统调用了
再举例,我们的STL
在扩容的时候是不是直接扩一点几倍,进而形成了内存池,实际上就是为了减少扩容的调用,减少成本
所以我们就有了一个用户级的缓冲区,用户级的缓冲区在哪呢?我们怎么没见过?
在我们的struct FILE
里其实有缓冲区,可以理解为char buffer[NUM]
,当收集到足够多的数据的时候,就会直接通过系统调用将缓冲区的数据刷新到内核级缓冲区,相比于直接将数据一个一个刷新到内核级缓冲区,这样的效率是非常高的
- 显示器文件:行刷新
- 普通文件:写满刷新
- 还有个不写入用户级缓冲区:直接调用系统函数
所以你明白了为什么我们在使用printf()
的时候我们不需要fflush
就可以直接将数据从缓冲区刷新到磁盘中,但是fclose(stdout)
之后就需要手动刷新了吗?
其实就是我们将文件描述符1
改成了普通文件,也就是说我们将数据写到了fd1
里面去了,此时还没刷新到内核里面去,但我们调用了close(fd1)
,所以我们就看不到数据
但要是我们不使用close(fd1)
:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>int main()
{close(1);int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd1: %d\n", fd1);//fflush(stdout);//close(fd1);return 0;
}
照样能够刷新出来,那是因为每一个进程在退出的时候,都会自动刷新自己的缓冲区
要是我们将close
换成fclose(stdout)
:
一样能够看到数据,那是因为fclose
是C语言函数,是封装过的,在关闭文件前会刷新缓冲区
对于内核级缓冲区什么时候刷新,一般都是操作系统决定的,但是我们也可以强制刷新fsync
函数:
你学会了吗?
好啦,本章对于《Linux:文件与fd(被打开的文件)》的学习就先到这里,如果有什么问题,还请指教指教,希望本篇文章能够对你有所帮助,我们下一篇见!!!
如你喜欢,点点赞就是对我的支持,感谢感谢!!!