目录
一.预备阶段
1.认识文件
2.OS对内存文件的管理
3.C库函数和系统调用接口
a.C库函数——fopen
b.系统调用接口——open
二.理解文件描述符
1.一张图,详解文件描述符的由来
2.fd的分配规则
3.从fd的角度理解FILE
三.重定向和缓冲区
1.前置知识——理解向文件写入数据的底层逻辑
2.重定向
a.什么是重定向?
b.输出、输入重定向
c.重定向的底层逻辑
d. dup2
3.缓冲区
a.什么是缓冲区?
b.缓冲区谁提供的??
c.为什么要有缓冲区?
d.缓冲区的刷新机制
e.重点样例
一.预备阶段
1.认识文件
一个文件,通常包括文件的内容数据(文件内存储的东西)和文件的属性数据(描述该文件本身状态的字段),所以,我们对文件进行操作,本质就是对文件内容和文件属性进行操作。
同理,文件的内容是数据,属性也是数据,所以,存储文件必须既存储内容数据,又要存储属性数据 ( 默认指的是磁盘中的文件)。
当我们要访问一个文件的时候,都是要先把这个文件打开,文件打开前,它是普通的磁盘文件。打开文件,就是将文件加载到内存。
所以,根据文件所处位置,我们可以将文件分成两类:①普通的磁盘文件;②加载到内存的文件。
众所周知,我们的磁盘或内存中,会存在大量的文件,由于OS是计算机软硬件资源的管理者,所以OS需要管理这些文件,即能够高效的对这些文件进行增删查改操作,这就是文件系统!!
而在该文章中,咱们主要了解的是“OS对内存文件的管理”,至于磁盘中的文件,博主会在下一篇博客中详解。
2.OS对内存文件的管理
那么,操作系统是如果管理内存文件的? --- 先描述,再组织!!
看过博主往期博客的都知道,在操作系统中,一旦涉及到“管理”这两个字,那一定就有这六个字,先描述,再组织!!
什么是先描述?文件在加载到内存前,OS就需要在内核中创建对应文件的结构体对象,该结构体内存有大量有关对应文件的属性信息,这样一来,当文件被加载到内存后,OS就可以把这个结构体当成对应的文件。
什么是再组织?当描述文件的结构体对象被创建后,OS会把这个结构体放到特定的数据结构中,以便后续OS对该结构体进行操作,这样一来,OS对内存文件的管理,就变成了对特定数据结构中某一结构体对象的增删查改!!
文件是如何加载到内存的?
我们知道,文件被加载到内存后,就变成了系统资源的一部分,而进程是“承担分配系统资源的基本实体”,所以,文件想要被加载到内存,一定是因为系统中的某个或某些进程需要它!!
进程通过操作系统打开文件,由于操作系统内核不允许任何人直接干涉,所以,我们想要使用操作系统的某一功能时,就只能使用操作系统对上层提供的系统调用接口,来间接使用操作系统的功能。
故,我们所学习的C语言打开文件的库函数,其底层一定封装了系统调用接口!!!
d.一个进程可以打开多个文件吗?多个进程可以打开多个文件吗?加载到内存中,被打开的文件可能会存在多个
3.C库函数和系统调用接口
a.C库函数——fopen
FILE* fopen(const char* path, const char* mode);
这个函数的功能是,以读或写的方式打开某一文件(绝对路径或相对路径)。
若不写路径,只写文件名,则path是写在cwd(当前工作目录)下的,而 ls /proc/pid 可以查看某一进程的属性数据,包括cwd,代码中,用chdir(“new_path”)可以改变cwd。
例1:FILE* fp = fopen("log.txt","w");
以写方式打开文件,若文件不存在,则先创建再打开;若文件存在,则先清空再打开。
可知:其与 输出重定向">"的功能相似。
例2:FILE* fp = fopen("log.txt","a");
以追加的方式打开文件,从文件的结尾处以追加的方式写入数据,不清空原有数据。
可知:与追加重定向 ">>" 的功能类似。
b.系统调用接口——open
int open(const char* filename, int flags, mode_t mode);
第一个参数,filename:即想要打开文件的文件名。
第二个参数,flags:
O_RDONLY(以只读的方式打开)
O_WRONLY(以只写的方式打开)
O_RDWR(以读写的方式打开)
O_CREAT(文件不存在,则创建)
O_TRUNC(若文件存在,则清空文件内容)
O_APPEND(若文件存在,则以追加的方式写入数据)
第三个参数,mode:只有当该文件是新创建的,才会用到这个参数,mode是默认权限,可以给新创建的文件设置文件权限。
返回值:若创建失败,则返回-1;若创建成功,则返回对应文件的文件描述符。
比特位级别的标记位使用方式 --- Linux中常用的传参方式
示例1:
int fd = open("log.txt",O_WRONLY | O_CREAT);
新创建一个文件,将其命名为 log.txt,但是由于没给该文件设置权限,导致文件权限乱码:
修改后:
int fd = open("log.txt",O_WRONLY | O_CREAT, 0666);
文件不存在的话,就创建该文件,并且将log.txt文件的默认权限设为0666.
示例2:
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);
以该方式打开文件,运行后,会将文件内原有的内容清零
示例3:
int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND, 0666);:
以该方式打开文件,运行后,会在文件内容后,追加数据
综上所述,我们能够发现,系统调用中的open接口,与C库中的 fopen,有着极其相似的功能,咱们可以得出一个结论:C库中文件相关操作的库函数,其底层都封装了系统调用接口。
二.理解文件描述符
1.一张图,详解文件描述符的由来
通过上图,我们可以发现,所谓的“文件描述符表”,其本质就是一个struct files_struct*类型的数组而已,数组内存放的是文件struct file结构体的地址,而所谓的“文件描述符fd”,它本质就是文件描述符表内元素的下标而已。
例如:
int fda = open("loga.txt",O_WRONLY | O_CREAT | O_APPEND, 0666); // fda = 3
int fdb = open("logb.txt",O_WRONLY | O_CREAT | O_APPEND, 0666); // fdb = 4
int fdc = open("logc.txt",O_WRONLY | O_CREAT | O_APPEND, 0666); // fdc = 5
int fdd = open("logd.txt",O_WRONLY | O_CREAT | O_APPEND, 0666); // fdd = 6
fda、fdb、fdc、fdd它们分别是3、4、5、6,意味着:新打开文件的struct file地址,是从文件描述符表的3号下标开始填充的。
那0、1、2去哪了呢?
程序在运行的时候,默认是把下面这三个程序(文件)打开的:
①标准输入 键盘 stdin 0号下标
②标准输出 显示器 stdout 1号下标
③标准错误 显示器 stderr 2号下标
所以,一个进程最新打开的文件的fd,是从3开始的!!
OS/C语言为什么默认要把0、1、2、stdin、stdout、stderr打开呢??--- 就是为了让程序员默认进行输入、输出和代码编写!!
2.fd的分配规则
当进程新打开一个文件时,OS会遍历该进程对应的文件描述符表(下标由低到高依次遍历),当OS找到表中空余位置时,无论该位置后面是否存在元素,OS都会将新创建文件的struct file地址填充到该位置,并向进程返回该位置对应的数组下标。
3.从fd的角度理解FILE
那FILE究竟是什么东西??
它是一个C语言提供的结构体类型,其内部必定封装了文件描述符!!!
举例验证:
printf("stdin -> fd: %d\n", stdin->_fileno);//0
printf("stdout -> fd: %d\n", stdout->_fileno);//1
printf("stderr -> fd: %d\n", stderr->_fileno);//2
说明,_fileno 字段,就是 FILE 内部封装的文件描述符!
三.重定向和缓冲区
1.前置知识——理解向文件写入数据的底层逻辑
在学习重定向和缓冲区之前,我们不妨先思考一下:当我们调用read、write、fread、fwrite时,OS底层究竟做了哪些是?一张图,咱们扒光它!!
无论读取还是写入数据,都要先把文件数据加载到文件缓冲区内!!
用户对文件中数据的读写的本质:用户通过fd变量,找到struct file* fd_array[]数组中的文件结构体地址,通过地址找到对应的结构体,再通过结构体中的数据属性,找到该文件对应的文件缓冲区(内存中存放文件内容数据的一块空间),对文件缓冲区中的数据进行读取或写入。
2.重定向
a.什么是重定向?
简单来说,就是将本该写入A文件中的数据,转而写入B文件中,这就是重定向(输出)。
b.输出、输入重定向
我们知道,在文件描述符表中,0、1、2三个文件是默认打开的,而根据fd的分配规则我们可以知道,如果我们将1号fd关闭,当我们接着打开一个新文件时,OS为该文件分配的fd就是1。
由于C库中的printf()函数本质是向1号文件(屏幕)写入数据,所以,如果我们此时调用printf的话,就会将本该打印到屏幕上的数据重定向写入到新打开的文件中。
而这,其实就是输出重定向的底层原理。代码如下:
c.重定向的底层逻辑
①上述代码中,为啥显示器上什么都没输出??
--- 因为1号文件描述符,也就是显示器被关了
②为啥本该输出的数据被写到了FILE_NAME文件里??
--- 因为printf()函数只认stdout——标准输出流,而stdout只认1号文件描述符,由于原先1号文件描述符对应的显示器被关了,而新打开文件的文件描述符根据fd的分配规则,被重新分配到了1号位置,所以本该在显示器上数出的数据跑到了FILE_NAME文件中。该过程便可称为输出重定向!
③上述代码中,为啥本该从键盘上读取数据的操作,变成了读取FILE_NAME文件中的数据??
--- 因为0号文件描述符,即键盘文件被关了,而以读的方式打开FILE_NAME文件时,根据fd的分配规则,0号位置被重新分配给了FILE_NAME文件,由于stdin——标准输入流,只认0号数组位置,所以本该从键盘上读取数据的操作变成了从FILE_NAME文件中读取数据。该过程便可称为输入重定向!
所以,重定向的本质,是文件描述符级别的数组内容的拷贝!
那么,我们的输入、输出重定向都需要先关闭0号或1号文件,然后再打开新的文件吗?--- 重定向的底层逻辑都是这样,但是这个操作有人已经为我们封装成了函数接口(dup2),咱们可以直接使用函数来实现重定向操作。
d. dup2
dup2(int dest_fd, int src_fd);
让本该向src_fd文件输入、输出的数据,改向dest_fd文件进行输入、输出。
即在文件描述符表中,让src对应的struct file* 被dest对应的struct file*覆盖!
输出重定向:
输入重定向:
标准输出流和标准错误流
解释:将mytest文件中“原本向标准输出”打印的内容重定向写到normal文件中,将“原本向标准错误流”中打印的内容重定向写到err.log文件中,实现打印错误信息和打印正常信息的分流!
3.缓冲区
a.什么是缓冲区?
--- 缓冲区本质是一块内存空间,我们可以把它看成一个小水池,池水就是数据,当水池内的水不够多时,水池蓄水(数据存储);当水池内的水达到一定条件时,咱们就开闸放水(数据刷新)。
b.缓冲区谁提供的??
--- 若该内存是由用户开辟出来的,如变量、数组等,则称为用户缓冲区;若该缓冲区是C语言提供的,则称为C标准库缓冲区;若是由操作系统提供的,则称为操作系统缓冲区。
c.为什么要有缓冲区?
--- 无论是用户与内核间的数据IO,还是内核与磁盘间的数据IO,它们都是需要花费一定成本的,IO次数越少,所消耗的成本也就越低,所以,缓冲区的存在就是为了减少IO次数,从而降低IO所带来的开销问题。
d.缓冲区的刷新机制
① 无缓冲 --- 缓冲区内一有数据就直接刷新.
② 行缓冲 --- 只有向缓冲区内写入换行字符,即'\n'时,才会刷新数据,否则不刷新.
③ 全缓冲 --- 只有把缓冲区写满了,才刷新;否则不刷新.
④ 强制刷新 --- 调用fflush(stdout),或当进程退出时,OS会自动刷新该进程内缓冲区的数据.
一般对于显示器文件 --- 行缓冲;
对于磁盘上的文件 ---全缓冲.
示例:
答:因为向显示器文件中写入数据的刷新准则是行刷新,而上述代码的 pirntf 中并没有 换行符'\n',所以 printf 仅仅是将 hello world 写入C库缓冲区或内核缓冲区,其内容并没有写入显示器文件中,而sleep 3秒后,代码执行完毕,进程退出,缓冲区内的数据会被强制刷新到显示器文件中(屏幕)。
e.重点样例
为什么多一个fork()时,使用C库函数写入文件的内容打印了两次,而使用系统调用接口写入文件的内容不变??fork()对C库函数的输出函数又起到了怎样的影响??
理解样例:
①当我们直接向显示器打印的时候,显示器文件的刷新方式是行刷新!由于我们的代码输出的所有字符串之前都有\n, 所以在fork()之前,缓冲区上的数据就已经全被刷新出来了,包括systemcall(系统调用)
②重定向到log.txt文件,本质是向磁盘文件写入,系统对于数据的刷新方式已经由行刷新,变成了全缓冲!
③全缓冲意味着缓冲区变大,实际写入的简单数据,不足以把缓冲区写满,fork执行的时候,数据依旧在缓冲区中!
④C语言库函数输出的内容写在C库缓冲区,而系统调用接口输出的数据是直接写到内核缓冲区的,fork后会创建子进程。
⑤当进程退出时,即使我们的数据没有达到刷新的条件,OS也会自动刷新C标准库缓冲区。对于多进程(父子进程)来说,刷新缓冲区属于对其一进程进行“清空”或“写入”操作,所以,fork退出时,刷新缓冲区,就要进行写时拷贝!!
而write是系统调用,没有使用C的缓冲区,直接写入到操作系统,不属于进程了,不发生写时拷贝!
用C语言库函数对文件写入的实质:调用C函数会先把要写的内容写入C缓冲区,当C缓冲区满足刷新条件时,会将缓冲区里的内容通过系统调用接口(write(1,BUFFER))拷贝到对应的文件缓冲区内。