一.文件对象struct file里有什么?
1.被打开文件的属性
2.文件的操作方法集
所谓的操作方法,就是一个个函数。尽管磁盘上文件和各种硬件设备的操作方法各不相同,但是函数类型却是统一的。
故操作方法集是一组函数指针,当struct file被实例化时,使函数指针指向相应的操作函数,而这些函数的具体实现struct file并不需要关心,这些是驱动程序的事情。因此不管是文本文件还是硬件设备,都可以用统一的视角来看待,因此我们说,“Linux一切皆文件”。
3.文件缓冲区
每一个struct file都有一个文件缓冲区,这是操作系统申请的一段内存空间,进程对文件无论进行读还是写,都要先将数据加载到文件缓冲区。
我们在用户层也可以定义自己的文件缓冲区,例如一个字符数组。用户层文件读写的本质就是将用户缓冲区和内核缓冲区的数据进行来回拷贝。
例如将字符串string写入文件中,操作系统先找到进程PCB,然后找到文件描述符表,根据fd找到对应的struct file,将string拷贝到文件缓冲区中。CPU将这条写入指令就算是执行完了,至于内核缓冲区的数据什么时候写入到磁盘,那就由操作系统自己决定了。
4.其它内容
二.fd分配规则
1.进程默认打开标准输入文件(键盘),标准输出文件(显示器),标准错误文件(显示器),分配的fd是0,1,2。
我们说C语言程序会默认打开标准输入流(stdin),标准输出流(stdout),标准错误流(stderr)。其实并不是C语言打开的,而是操作系统打开的,这三个流只不过封装了0,1,2的文件描述符。
2.fd分配规则:在文件描述符表中寻找下标最小的没有被使用位置,分配给打开的文件,下标就是该文件的fd。
注:buf是用户自己定义的用户层缓冲区,count是期望操作的字节数,返回值ssize_t是long long,实际读/写的字节数
我们可以直接使用文件描述符0,1,2来访问键盘和显示器,验证如下:
三.重定向
输出重定向:原本要向显示器文件写入,变成了向其它文件写入
追加重定向:原本要向显示器文件追加写入,变成了向其它文件追加写入
输入重定向:原本要从键盘文件读入,变成了从其它文件读入
现象:像标准输出流写数据,但是显示器上没有显示,数据被写到log.txt去了。
解释:close(1),表示将文件描述符表的1号位置的内容置空,1号文件不再是标准输出文件,暂时空缺。log.txt被open,根据fd分配规则,log.txt被分配到了1号位置,此时的1号文件就是log.txt。但是stdout内部封装的文件描述符1始终不变,标准输出流只认1号文件,而不管它是什么。因此向stdout写数据,实际上是向1号文件log.txt写数据。
上述现象就叫做输出重定向。
输入重定向:
重定向本质:
修改文件描述符表特定下标的数组单元中的内容,上层fd不变,底层fd指向的文件发生变化
四.系统调用接口dup2
功能:实现文件描述符表内容的拷贝,相当于
fd_array[newfd]=fd_array[oldfd]
也就是说,调用dup2后,oldfd和newfd都指向调用之前oldfd指向的文件。那么newfd之前指向的struct file怎么办呢?这要看struct file是否还有别的指针指向,通过引用计数来判断,如果指向它的指针数目为0,则会被操作系统回收。
在调用dup2之前,你可以close(newfd),相当于fd_array[newfd] = NULL,并文件对象的引用计数减1。但这不是必须得,因为调用dup2会覆盖原来的内容,操作系统会自动把引用计数减1
输出重定向:
五.标准错误流vs标准输出流
现象:输出重定向,但是向stderr写的内容还是写入到显示器,只有向stdout写的内容进入log.txt。
解释:shell会先扫描用户输入的指令,如果发现有重定向符号,则先open重定向符号后面的文件,然后使用dup更改文件描述符表中相应的内容,最后才创建子进程执行代码。
这里的>是输出重定向,则shell会执行以下代码:
int fd = open(”log.txt", O-CREAT | O_WRONLY | O_TRUNC, 0666); dup2(fd, 1);
stderr封装的fd是2,所以不会受到影响。
如果我非要将stderr和stdout都重定向到log.txt,有指令可以完成吗?有的!!!
./code > log.txt 2>&1
含义:前面的“>”表示输出重定向,即fd_array[1]=fd_array[fd],后面的"2>&1"意思是fd_array[2]=fd_array[1]。
其实“>”是一种省略的写法,它等价于“1>log.txt”,所以你也可以这样写:
./code 1 > log.txt 2>&1
那么为什么要有stderr呢?
因为便于将进程输出的常规信息和错误信息分开,便于我们排错处理。
例如:
./code 1 > log.txt 2 > log.txt.err
如printf向标准输出流写入,perror向标准错误流写入,这样就能将常规输出和错误信息输出放进不同文件,便于记录错误日志。
六.C标准库缓冲区和内核缓冲区
什么是缓冲区?缓冲区就是一段内存。
1.三种缓冲区区分
根据缓冲区由谁提供可将缓冲区分为三类
- 用户缓冲区。用户定义的一段内存,如一个字符数组,甚至是整型变量。
- C标准库缓冲区。C标准库定义的一段内存,我们平常说的缓冲区一般是指它。每一个FILE结构体都有一个C缓冲区。用户缓冲区和C缓冲区都是用户层面的缓冲区。
- 内核缓冲区。操作系统为每个struct file都定义一个缓冲区。
2.缓冲区的作用
缓冲区的作用是提高效率
一方面,缓冲区可以提高使用者的效率,谁使用就提高谁的效率。例如printf函数直接将数据写入到C标准库,printf就可以返回了,这样就提高了printf或者C语言的效率。系统调用write函数将数据写入到内核缓冲区,就可以返回了,这样提高了write函数或者说操作系统的效率。
另一方面,缓冲区提高发送的效率。因为有缓冲区可以积累一部分数据再统一发送,这样可以提高发送效率。
3.C缓冲区的刷新方法
- 无缓冲(立即刷新)
- 行缓冲(行刷新)
- 全缓冲(缓冲区满了才刷新)
我们研究的缓冲区是C缓冲区,我们认为数据一旦进入到内核缓冲区就相当于进入了文件中了,内核缓冲区的刷新方法我们并不关心,操作系统有它自己的策略。
C缓冲区对于显示器文件行缓冲,对于磁盘上的文件全缓冲。另外,用户可以强制刷新缓冲区(如fflush),进程退出时也会自动刷新缓冲区。
4.C缓冲区详解
看这样一个例子:
fprintf,printf,fputs都是C语言接口,write是系统调用接口,都向1号文件,即显示器输出字符串
将myfile的输出内容重定向,即fd=1的文件变成了log.txt,我们发现也是4条输出信息,有个小细节是write的内容到前面去了。
添加一句fork语句创建子进程
惊奇地发现只有7条语句,系统调用write的信息只有1条,C接口的信息都有两条
解释:
- 向显示器打印时,显示器文件的刷新方式是行缓冲,并且代码输出的所有字符串都有‘\n’,所有fork之前,C缓冲区的数据都已经被刷新了。更不用说系统调用write了,因为它直接向内核缓冲区写入,相当于直接向文件写入,不经过C缓冲区
- 重定向到log.txt,本质是向磁盘文件写入,C缓冲区的刷新方式由行缓冲变成了全缓冲
- 全缓冲意味着简单的数据根本不可能将缓冲区写满,fork时数据还在缓冲区中
- C缓冲区的数据还是属于进程的,因为向C缓冲区写入数据本质上是用户层面数据的拷贝。而内核缓冲区的数据就不属于进程了,它是属于操作系统或者文件的
- 进程退出时,要刷新缓冲区。所谓的刷新缓冲区,是指将C缓冲区的数据拷贝到内核缓冲区,同时清空C缓冲区中的数据。而清空操作会导致“写时拷贝”,即父子进程数据共享,一方写入时发生写时拷贝
- 所以,fork之后,任意一个进程退出时,刷新缓冲区,发生写时拷贝,C缓冲区的数据被重复拷贝到内核缓冲区。而write系统调用没有使用C缓冲区,直接将数据给操作系统,不会发生写时拷贝
5.C缓冲区的意义
提高C的IO接口的效率:
只需将数据从内存拷贝到内存就可以返回了,等后续缓冲区积累的一定的数据再刷新即可,这样就不必频繁的调用系统调用(因为系统调用涉及到状态切换等操作,有一定的时间成本)