【Linux】文件描述符与重定向操作

系列文章

收录于【Linux】文件系统 专栏

对于Linux下文件的写入与读取,以及文件原理还有疑惑的可以看看上一篇文章浅谈文件原理与操作。


目录

系列文章

再谈文件描述符

​编辑

IO函数的本质

一切皆文件

文件重定向

原理

系统接口


再谈文件描述符

上一篇文章中,我们就提到了 open 的返回值即 fd,又称为文件描述符,之后我们进行读写操作时就需要用到 fd。

而 fd 究竟是什么来头,下面我们来一起揭秘。

【Linux】文件描述符与重定向操作_第1张图片

我们首先先打开一个文件,再打印返回的 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 数组的下标。

【Linux】文件描述符与重定向操作_第2张图片

因此,只要知道 fd 就能在 fd_arry 中的位置就能找到对应 struct file 的指针,进而找到 struct file。

这样的结构设计同时还实现了进程管理与文件系统解耦合,只用指针进行轻度的连接。

IO函数的本质

不仅如此,每一个 struct file 都有属于自己的缓冲区,本质上我们调用 wirte 函数就是将数据拷贝到这个缓冲区之中,至于什么时候刷新到磁盘中的指定位置,则有 OS 自主决定。

而 read 前便会将磁盘的数据拷贝到缓冲区里,之后找到缓冲区读取数据。

【Linux】文件描述符与重定向操作_第3张图片

即 IO 函数的本质就是拷贝函数,在用户空间和内核空间之间来回拷贝。

一切皆文件

我们常说:“Linux 下一切皆文件。” 这句话又该如何理解呢?而其中最困扰我们的就是如何将外设也看成文件。

由于每种外设读取与写入的方式都不相同,因此需要安装特定的驱动程序才能使用。

【Linux】文件描述符与重定向操作_第4张图片

而有些外设只读不写或是只写不读就没有对应的驱动程序。 

这时候我们再回过头来看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<

 

可以看到,两次 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内部有一个函数便可实现重定向的功能。

【Linux】文件描述符与重定向操作_第5张图片

我们使用 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;
}


好了,今天 文件描述符与重定向操作 的相关内容到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。

你可能感兴趣的:(【Linux】文件系统,Linux,linux,运维,服务器)