系列文章
收录于【Linux】文件系统 专栏
对于Linux下文件的写入与读取,以及文件原理还有疑惑的可以看看上一篇文章浅谈文件原理与操作。
目录
系列文章
再谈文件描述符
编辑
IO函数的本质
一切皆文件
文件重定向
原理
系统接口
再谈文件描述符
🍧上一篇文章中,我们就提到了 open 的返回值即 fd,又称为文件描述符,之后我们进行读写操作时就需要用到 fd。
🍧而 fd 究竟是什么来头,下面我们来一起揭秘。
🍧我们首先先打开一个文件,再打印返回的 fd ,观察一下输出的结果。
#define myfile "log.txt"
int main()
{
int fd = open(myfile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
cout << fd << endl;
return 0;
}
🍧输出的是 3,这个 3 有什么特殊的含义吗?是意味着文件是打开的第三个文件吗?
🍧其实,任何一个进程在启动的时候,默认会打开当前进程的三个文件。
标准输入 | 标准输出 | 标准错误 | ||
C语言 | stdin | stdout | stderr | |
CPP | cin | cout | cerr | |
文件类型 | 键盘文件 | 显示器文件 | 显示器文件 | |
fd | 0 | 1 | 2 |
🍧因为一开始打开了三个文件,从而我们第一次打开文件时 fd 便从 3 开始了!讲到这里,你应该能大概猜到 fd 是什么了吧。
🍧没错,就是数组下标。
🍧之前我们讲过,文件是用户通过调用进程打开的,于是在内存之中就会存在一个个包含文件大部分属性的 struct file。当我们找到 struct file 时就意味着我们找到了被打开的文件,而用户能访问到的只有进程的 task_struct。
🍧因此,系统需要将该进程打开的文件管理起来,就有了 file_struct,其中的 fd_array 就负责存储 struct file 的指针。最后 tack_struct 再使用指针与 file_struct 关联起来即可。其中fd就是 fd_array 数组的下标。
🍧因此,只要知道 fd 就能在 fd_arry 中的位置就能找到对应 struct file 的指针,进而找到 struct file。
🍧这样的结构设计同时还实现了进程管理与文件系统解耦合,只用指针进行轻度的连接。
IO函数的本质
🍧不仅如此,每一个 struct file 都有属于自己的缓冲区,本质上我们调用 wirte 函数就是将数据拷贝到这个缓冲区之中,至于什么时候刷新到磁盘中的指定位置,则有 OS 自主决定。
🍧而 read 前便会将磁盘的数据拷贝到缓冲区里,之后找到缓冲区读取数据。
即 IO 函数的本质就是拷贝函数,在用户空间和内核空间之间来回拷贝。
一切皆文件
🍧我们常说:“Linux 下一切皆文件。” 这句话又该如何理解呢?而其中最困扰我们的就是如何将外设也看成文件。
🍧由于每种外设读取与写入的方式都不相同,因此需要安装特定的驱动程序才能使用。
🍧而有些外设只读不写或是只写不读就没有对应的驱动程序。
🍧这时候我们再回过头来看struct file这个结构。
struct file
{
//文件权限
//文件大小
//缓冲区
...
int (*readp)(int fd,char* buffer,int size);
int (*writep)(int fd,char* buffer,int size);
};
🍧其中两个函数指针则分别指向了驱动程序的读写函数。
🍧即:操作系统的文件操作只是将数据拷贝到文件的缓冲区中,之后再调用目标文件的自身的读写操作,完成读写。
🍧每个设备都可以将其驱动程序填入文件的结构体,因此在进程看来外设也是文件。同时,我们在访问 OS 时都是通过进程代为执行,而进程认为一切皆 struct file 但其中究竟是设备还是文件,进程不知道也不关心。由此在用户看来最终就是一切皆文件的现象。
文件重定向
🍧之前我们就在命令行使用过文件重定向的操作,将原本要输出到显示器的信息重定向到文件之中。
echo hello log.txt > log.txt //将语句写入到log.txt文件中
这种操作是如何用代码的实现的呢?下面一起来看看吧。
原理
🍧首先,我们得先了解一下文件描述符的分配规则: 选文件描述表中最小的、没有被使用的数组元素,分配给新文件。
🍧我们可以用一段代码来验证一下。
int main()
{
int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);
cout<<fd1<<endl;
close(fd1);
int fd2 = open("out.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);
cout<<fd2<<endl;
close(fd2);
return 0;
}
🍧可以看到,两次 fd 都是 3,这是因为一开始的 fd 就是从 3 开始的,打印 fd 后关闭文件再打开一个新文件时,文件描述表中还是只有三个文件,于是 fd 还是从 3 开始分配。
🍧这时候,我们便想:前面三个文件能不能关闭呢?我们再次打开文件会发生什么事呢?
#define myfile "log.txt"
int main()
{
close(0);
int fd = open(myfile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
cout << fd << endl;
return 0;
}
🍧很明显,新文件的 fd 便是刚才关闭的 0。若我们关闭1号标准输出文件,然后再调用 printf 或 cout 输出流进行输出呢?
🍧这次程序运行完我们却没有看到显示器有进行输出,我们查看 log.txt 这个文件便会发现,信息被输出到了文件之中。
🍧从操作系统的角度出发,OS 只知道标准输出的 fd 是 1,当遇到需要输出的情况时就找到 fd 对应的文件进行输出。因此,OS 并不知道此时 1 已经不是对应着标准输出这件事,便直接向文件写入。
🍧即重定向的原理便是:在上层不知道的情况下,在OS内部更改进程对应的文件描述符表中特定下标的指向。
系统接口
🍧若是每次要重定向时都要像上面那样先关闭文件,再打开一个新文件匹配 fd 未免有些麻烦。
🍧OS内部有一个函数便可实现重定向的功能。
🍧我们使用 dup2 便可以直接实现函数的重定向,第一个参数为原来的 fd,第二个参数则是想要关闭文件的 fd。
int main()
{
int fd = open(myfile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
printf("hello this is stdout\n");
close(fd);
return 0;
}
🍧借助这个功能,我们可以简单的对运行结果进行一个记录,输出和错误分别记录在不同的文件中。
int main()
{
int fd1 = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open("error.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd1,1);
dup2(fd2,2);
cout << "hello cout" << endl;
cerr << "hello cerr" << endl;
close(fd1);
close(fd2);
return 0;
}
🍧好了,今天 文件描述符与重定向操作 的相关内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。