我们首先得知道文件描述符的分配原则:最小的没有被使用的作为新的fd
分配给用户。
先来看现象:
我们关闭了1
,所以下次新创建文件时,文件被分配到的文件描述符就是1
我们运行,发现并没有在屏幕上打印fd: 1
我们ll
发现多了一个log.txt
,cat log.txt
发现它显示fd:1
本来应该显示到显示器的内容竟然显示在了log.txt
文件里面。
不在屏幕上显示是因为我们把标准输出(1)关了,为什么会往文件里写呢?因为1
这个位置变成了log.txt
文件,这种现象就叫做重定向。
解释现象:
我们在打开文件之前首先把文件描述符1
给close
掉了,所以此时的文件描述符1
就不再指向标准输出了,当我们open
打开新的文件log.txt
时,就找到了1
,把新打开的log.txt
的地址填进来。把1
返回给上层用户,所以用户拿到的文件描述符就是1
。可是我们接下来用到的printf
是C语言提供的函数,它是往stdout
中打印的,stdout
封装的就是1
,printf
只认stdout
中的1
,它找的时候就找到了log.txt
,所以就写到了log.txt
中了。
我们刚才做的在底层更改一个文件描述符内容的指向,这种现象叫做重定向。
再看一个现象:
我们在printf
后面加上close(fd)
重定向的系统调用dup2
#include <unistd.h>
int dup2(int oldfd, int newfd);
我们要实现重定向是想让1
里面的指针指向新的文件,3
如果是我们新创建的文件,那么我们应该把1
里面的指针内容方到3
里面还是把3
里面的指针内容放到1
里面呢?答案是把3
里面的内容放到1
里面,1
是3
的一份拷贝,即1
是fd
的一份拷贝,所以oidfd
就是fd
,1
是newfd
,所以传参时dup2(fd,1)
我们就会发现它就不会再显示器上打了,而打印到了log.txt
文件中。
这次我们dup2
后不关闭fd
,并且向fd
里写入hello world
会发现hello world
被打在了最前面,是因为有缓冲区的存在,先把系统调用里面的值打印出来,然后才是文件的值。
所以重定向的原理就是操作系统在源代码当中做操作系统级别的文件指针所对应的文件地址的拷贝!
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd < 0) exit(1);//打开失败直接退出
dup2(fd,1);//输出重定向,把本来打印到显示器上的内容,打印到fd中
close(fd);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0) exit(1);//打开失败直接退出
dup2(fd,1);//输出重定向,把本来打印到显示器上的内容,打印到fd中
close(fd);
int fd = open("log.txt",O_RDONLY);
if(fd < 0) exit(1);//打开失败直接退出
dup2(fd,0);//输出重定向,把本来打印到显示器上的内容,打印到fd中
close(fd);
while(1)
{
char buffer[64];
if(!fgets(buffer,sizeof(buffer),stdin)) break;
printf("%s",buffer);
}
重定向 = 打开文件的方式 + dup2
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);//输出重定向,把本来打印到显示器上的内容,打印到fd中
close(fd);
while(1)
{
char buffer[64];
if(!fgets(buffer,sizeof(buffer),stdin)) break;
printf("%s",buffer);
}
return 0;
}
把本来从标准输入(stdin)上获取的数据,从文件里读,此时就可以做任意文件的输入重定向。
我们用fd
把标准输出覆盖后,那么标准输出去哪里了呢?一个文件可以被多个进程打开,文件的struct file
中有引用计数cnt
,当一个进程关闭文件时,引用计数--
,当引用计数减到0
,这个struct file
才会被关掉。
所以当我们把fd
拷贝到1
这个位置时,首先会把stdout
的引用计数做--
,操作系统会判断这个引用计数是否为0
,为0
就会把它释放掉。
重定向的完整写法:
标准输出和标准错误
为什么我们的标准输出写进了log.txt
里,而标准错误还是在显示器上打印?原因是我们标准输出的时候,虽然标准输出和标准错误都指向同一份文件,我们重定向时,它的本质是把1
重定向到新文件,即把新打开的log.txt
文件描述符的地址拷贝到1
里面,可是2
依旧指向标准错误。
但我们如果想让标准输出和标准错误打印在不同的文件里,我们可以
./a/out 1>log.normal 2>log.error
因此我们可以通过重定向未来把常规消息和错误消息进行分离
如果我们想把标准输出和标准错误打印到同一个文件呢?有的同学肯定会想./a.out 1>lg.normal 2>log.normal
,最后文件中只有标准错误的信息,原因是这个文件被打开了两次,打开文件时是先清空再写入,所以最后就只剩标准错误的信息了。
有一个解决办法是./a.out 1>lg.normal 2>>log.normal
,使用追加的方式。
还有一个办法就是
./a.out 1>log.txt 2>&1
其中2>&1
表示的是把1
里面的内容写到2
里面,1>log.txt
表示把log.txt
里面的内容写到1
里面,即把3
写到1
里面。把1
里面的内容写到2
里面,因为1
里面的内容已经被重定向成log.txt
了,把1
里面的内容写到2
,所以2
此时也指向log.txt
,两个就指向同一个文件了。
像磁盘、显示器、键盘,鼠标,网卡这样硬件设备也被抽象成了文件,这些外设都要有自己的读写方法,每一种设备的读写方法都是不一样的。操作系统是对软硬件资源进行管理的,但操作系统并不和这写硬件设备打交道,但操作系统要把这些硬件设备先描述,再组织地管理起来,所以操作系统对设备的管理就转换成了对链表的增删查改。一个进程在打开文件时要创建PCB
,通过文件描述符表找到对应的struct file
,struct file
结构体中虽然不能存在函数方法,但可以有函数指针,通过函数指针执行对应硬件的读写方法。相当于C语言实现的多态。
我们用户在上层通过文件描述符访问特定文件时,比如说read
接口 把上层的数据拷贝到文件缓冲区里,做刷新把内容从文件缓冲区中调用对应的函数指针的write
方法写到设备里。所以访问设备都是通过函数指针进行访问的,而大家的函数指针类型名,参数都一样。
所以上层访问底层不同的硬件设备时,上层就不需要知道你是磁盘,显示器,还是鼠标了,就屏蔽了底层的硬件差异。
因此把struct file
以上统称为一切皆文件。把struct file
这一层称之为虚拟文件系统(VFS)
📙总结: 一切皆文件是通过VFS
即虚拟文件系统来实现的,我们用到的struct file
属于虚拟文件系统而不属于具体的文件系统,对我们来说VFS
中有文件的基本属性,缓冲区,函数指针。这样就可以通过函数中指针屏蔽掉底层不同的差异。
缓冲区是内存空间的一部分,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,缓冲区根据其对应的是输入设备还是输出设备分为输入缓冲区和输出缓冲区。相当于"菜鸟驿站“。
为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。
先看现象:
此时默认会往log.txt
中打印
我们在打印之后把fd
关掉。
此时我们会发现log.txt
的大小为0
我们用系统调用write
加上一段字符串,此时的fd
是没关的
我们会发现内容全被写进来了
当我们关闭fd
我们会发现只有系统调用写进了文件里,而库函数并没有被写入。
现象发生的原因:
这下我们就懂得了打开close
语言层库函数的内容为什么没有打印到文件里了,当我们调用close
时进程还没有结束,因为还没执行到return
,我们的语言层既没有强制刷新
,刷新条件满足
,进程退出
,所以数据会一直在C语言标准库中的语言层缓冲区中。后来close
把文件描述符关了,进程退出了,进程退出之后C语言语言层缓冲区要刷新,调系统调用时发现fd
已经被关了,所以无法把数据从语言层交付到操作系统内,所以数据也无法从文件内核缓冲区刷新到某种硬件上,所以我们就看不到写的内容。
我们如果想在进程退出之前刷新到文件内核缓冲区呢?fflush
💦补充细节: c语言层的缓冲区在哪里?
我们使用的printf/fprintf/fputs/fwrite
的底层都是FILE*
的 ,FILE
是c语言提供的一个结构体,里面封装了fd
和缓冲区,现在就能理解了为什么任何文件都要都一个缓冲区,因为任何一个文件被打开都要有一个FILE*
对象。
数据交给系统交给硬件本质全是拷贝! 计算机数据流动的本质:一切皆拷贝!
再来看一个现象:
为什么往显示器上打印的时候只有四条,而往文件中打印时有七条呢,系统调用只打了一次,而库函数打印了两次?
原因是在fork
的时候,对应语言层缓冲区里面的消息还在缓冲区里,当fork
的时候父子各自都要刷新,所以就会出现两次。
那系统调用为什么没有出现刷新两次的问题呢?
答案是write
执行完后,数据已经写给操作系统了,不存在用户层的刷新问题。
📙总结: 对于写入来讲,用户把自己的字符串拷贝到缓冲区里,就可以通过缓冲区的存在大大减少调用系统调用的次数,提高c语言接口的使用效率。系统内核也存在文件内核缓冲区,文件内核缓冲区可以提高系统调用的效率