跳到主要内容Linux 基础 IO(三):文件描述符与重定向 | 极客日志C
Linux 基础 IO(三):文件描述符与重定向
Linux 系统编程中的文件描述符(FD)概念及其底层实现机制。阐述了进程创建时默认打开的标准输入输出流(0, 1, 2),以及文件描述符的分配规则(从最小未使用下标开始)。详细讲解了输出重定向、追加重定向和输入重定向的原理及代码实现,区分了标准输出流与标准错误流在重定向时的不同行为。最后介绍了 dup2 函数在文件描述符复制与重定向中的应用,通过示例展示了如何将程序输出重定向至文件。
SqlMaster3 浏览 一、文件描述符
1.1 fd
- 文件由进程在运行时打开,一个进程可同时打开多个文件,而系统中存在大量并发运行的进程,这意味着系统任一时刻都可能存在数量庞大的已打开文件。
- 为了高效管理这些已打开的文件,操作系统会为每个已打开的文件创建专属的
struct file 结构体,并用双链表将所有该结构体串联起来。如此一来,操作系统对已打开文件的管理,就转化为对这一全局双链表的增、删、查、改等基础操作,大幅简化了文件管理的核心逻辑。(即先描述,再组织)
- 但仅靠全局的双链表,无法区分哪些已打开文件归属于特定进程,因此操作系统还需额外建立进程与文件之间的关联关系,以此精准界定每个进程所持有、可操作的文件范围。
那么操作系统是如何建立进程与文件之间的关联关系的呢?
我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据从硬盘加载到内存,然后为其创建对应的 task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。
在进程的核心控制结构体 task_struct 中,存在一个指向 files_struct 结构体的指针;而 files_struct 结构体里包含了关键的指针数组 fd_array —— 这个数组的下标,就是我们实际编程中使用的文件描述符。
当进程执行打开 log.txt 文件的操作时,操作系统会按以下逻辑完成文件描述符的绑定:
- 首先将磁盘中的
log.txt 文件加载到内存中,并为其创建对应的 struct file 结构体(包含文件的读写位置、权限、关联的磁盘 inode 等核心信息);
- 把这个新创建的
struct file 结构体接入系统管理已打开文件的全局双链表,完成系统层面的文件管理登记;
- 将该
struct file 结构体的首地址,填入当前进程 fd_array 数组中下标为 3 的位置(因 0、1、2 被标准输入/输出/错误占用),使 fd_array[3] 指针精准指向该文件的 struct file 结构体;
- 最后将下标值 3 作为文件描述符返回给调用进程,进程后续通过这个描述符就能找到对应的
struct file,进而操作 log.txt 文件。
因此,我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。
1.2 补充说明
1.2.1 进程创建时默认打开 0、1、2 文件描述符的底层逻辑
我们常说'进程创建时会默认打开 0、1、2',本质是操作系统为每个新进程完成了硬件设备到文件描述符的绑定:
- 0 对应标准输入流(关联键盘)、1 对应标准输出流(关联显示器)、2 对应标准错误流(同样关联显示器)。
- 键盘和显示器作为硬件设备,会被操作系统识别并纳入文件管理体系: 进程创建时,操作系统会为键盘、显示器分别创建对应的
struct file 结构体,将这些结构体接入全局的已打开文件双链表,再把 3 个结构体的首地址依次填入进程 files_struct 的 fd_array 数组下标 0、1、2 的位置。
- 至此,进程无需手动调用
open,就默认拥有了对键盘(0)、显示器(1/2)的操作能力。
1.2.2 磁盘文件与内存文件的核心区别
- 磁盘文件:存储在磁盘上的静态文件,是文件的'持久化形态',由两部分构成:
① 文件内容:文件中实际存储的业务数据(如文本、二进制流);
② 文件属性(元信息):文件的基础描述信息,包括文件名、大小、创建时间、权限、所属用户等。
- 内存文件:磁盘文件被加载到内存后的动态形态,是操作系统为操作文件创建的'内存映射'。
磁盘文件与内存文件的关系,类比'程序与进程':程序是磁盘上的静态代码,运行后成为内存中的进程;磁盘文件是静态存储的文件,被打开/加载后成为内存中的文件。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
加载规则:文件加载到内存时采用'按需加载'策略——优先加载文件属性(元信息),仅当需要读取、写入文件内容时,才延后加载具体的文件数据,以此节省内存资源。1.2.3 文件写入的缓冲区机制
向文件写入数据时,并非直接写入磁盘,而是先写入该文件对应的内存缓冲区;操作系统会通过预设策略(如缓冲区满、定时刷新、手动调用 fsync),将缓冲区中的数据批量刷新到磁盘,以此减少磁盘 IO 次数,提升整体读写效率。
1.3 文件描述符的分配规则
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main() {
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
printf("fd5:%d\n", fd5);
return 0;
}
若我们在打开这五个文件前,先关闭文件描述符为 0 的文件,此后文件描述符的分配又会是怎样的呢?
我们发现第一个打开的文件获取到的文件描述符变成了 0,而之后打开文件获取到的文件描述符还是从 3 开始依次递增的。
我们再试试将文件描述符为 0 和 2 的文件都关闭。
注意:1 不能关闭,因为他是标准输出,关闭了我们无法从显示器观察现象
可以看到前两个打开的文件获取到的文件描述符是 0 和 2,之后打开文件获取到的文件描述符才是从 3 开始依次递增的。
结论:文件描述符是从最小但是没有被使用的 fd_array 数组下标开始进行分配的。
二、重定向
2.1 重定向的原理
上面我们已经了解了文件描述以及文件描述符的分配规则,接下来我们通过实例来看看重定向的本质。
2.1.1 输出重定向原理
输出重定向的原理就是我们本该输出到一个文件的原理输出到另外一个文件中。
如上述图片,我们让本应该输出到'显示器文件'的数据输出到 log.txt 文件当中,那么我们可以在打开 log.txt 文件之前将文件描述符为 1 的文件关闭。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0) {
perror("open");
return 1;
}
printf("hello Linux\n");
printf("hello Linux\n");
printf("hello Linux\n");
printf("hello Linux\n");
printf("hello Linux\n");
fflush(stdout);
close(fd);
return 0;
}
我们可以看到原先应该输出到屏幕上的内容输出到了我们的 log.txt 文件当中。
printf 函数默认向 stdout(标准输出流)输出数据,stdout 指向 struct FILE 类型的结构体,该结构体中存储的文件描述符固定为 1,因此 printf 本质是向文件描述符为 1 的文件(显示器)输出数据。
- 调用
printf 后,数据不会立即写入操作系统内核,而是先存入C 语言标准库维护的用户态缓冲区;只有当缓冲区满、遇到换行符(\n)或程序结束时,数据才会批量刷新到操作系统层面。若需让数据即时输出,需通过 fflush(stdout) 手动强制刷新 C 语言缓冲区,将数据同步到操作系统。
2.1.2 追加重定向原理
我们知道输出重定向的时候会将原有文件里面的内容数据覆盖,而追加重定向是对原有内容数据不进行覆盖,追加再后面输出数据。
如我们想让本应该输出到'显示器文件'的数据追加式输出到 log.txt 文件当中,那么我们应该先将文件描述符为 1 的文件关闭,然后再以追加式写入的方式打开 log.txt 文件,这样一来,我们就将数据追加重定向到了文件 log.txt 当中。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT, 0666);
if (fd < 0) {
perror("open");
return 1;
}
printf("hello linux!\n");
printf("hello linux!\n");
printf("hello linux!\n");
printf("hello linux!\n");
printf("hello linux!\n");
fflush(stdout);
close(fd);
return 0;
}
看到如下图,我们能发现新的数据已经追加到原来文件数据内容的后面。
2.1.3 输入重定向的原理
输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。(如原来是从键盘获取,我们可以让他从文件获取)
我们可以在打开 log.txt 文件之前将文件描述符为 0 的文件关闭,这样一来,当我们后续打开 log.txt 文件时所分配到的文件描述符就是 0。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
close(0);
int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
if (fd < 0) {
perror("open");
return 1;
}
char str[40];
while (scanf("%s", str) != EOF) {
printf("%s\n", str);
}
close(fd);
return 0;
}
scanf 函数是默认从 stdin 读取数据的,而 stdin 指向的 FILE 结构体中存储的文件描述符是 0,因此 scanf 实际上就是向文件描述符为 0 的文件读取数据。
三、标准输出流与标准错误流的区别
标准输出流和标准错误流对应的都是显示器,它们有什么区别?
#include <stdio.h>
int main() {
printf("hello printf\n");
perror("perror");
fprintf(stdout, "stdout:hello fprintf\n");
fprintf(stderr, "stderr:hello fprintf\n");
return 0;
}
这样看起来标准输出流和标准错误流并没有区别,都是向显示器输出数据。
但我们若是将程序运行结果重定向输出到文件 log.txt 当中,我们会发现 log.txt 文件当中只有向标准输出流输出的两行字符串,而向标准错误流输出的两行数据并没有重定向到文件当中,而是仍然输出到了显示器上。
因为我们使用重定向时,重定向的是文件描述符是 1 的标准输出流,而并不会对文件描述符是 2 的标准错误流进行重定向。
四、dup2
要完成重定向我们只需进行 fd_array 数组当中元素的拷贝即可。
- 例如,我们若是将
fd_array[3] 当中的内容拷贝到 fd_array[1] 当中,因为 C 语言当中的 stdout 就是向文件描述符为 1 文件输出数据,那么此时我们就将输出重定向到了文件 log.txt。
Linux 操作系统中,就给我们给了一个实现此功能的函数:dup2
int dup2(int oldfd, int newfd);
- 函数功能
dup2 会将
fd_array[oldfd] 的内容拷贝到 fd_array[newfd] 中,若有必要,会先关闭文件描述符为 newfd 的文件。
- 函数返回值
dup2 调用成功时返回 newfd,调用失败时返回 -1。
- 使用注意事项
- 若 oldfd 并非有效的文件描述符,dup2 调用失败,且此时文件描述符为 newfd 的文件不会被关闭;
- 若 oldfd 是有效的文件描述符,且 newfd 与 oldfd 的值相同,则 dup2 不执行任何操作,直接返回 newfd。
我们将打开文件 log.txt 时获取到的文件描述符 fd 和 1 传入 dup2 函数,那么 dup2 将会把 fd_array[fd] 的内容拷贝到 fd_array[1] 中,在代码中我们向 stdout 输出数据,而 stdout 是向文件描述符为 1 的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到 log.txt 文件当中。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0) {
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
printf("我跑到这里来啦!!!\n");
fprintf(stdout, "我也跑到这里来啦!!1\n");
return 0;
}
代码运行后,我们即可发现数据被输出到了 log.txt 文件当中。