【Linux】基础IO(三):文件描述符与重定向


✨道路是曲折的,前途是光明的!

📝 专注C/C++、Linux编程与人工智能领域,分享学习笔记!

🌟 感谢各位小伙伴的长期陪伴与支持,欢迎文末添加好友一起交流!

  • 一、文件描述符
    • [1.1 fd](#1.1 fd)
    • [1.2 补充说明](#1.2 补充说明)
      • [1.2.1 进程创建时默认打开0、1、2文件描述符的底层逻辑](#1.2.1 进程创建时默认打开0、1、2文件描述符的底层逻辑)
      • [1.2.2 磁盘文件与内存文件的核心区别](#1.2.2 磁盘文件与内存文件的核心区别)
      • [1.2.3 文件写入的缓冲区机制](#1.2.3 文件写入的缓冲区机制)
    • [1.3 文件描述符的分配规则](#1.3 文件描述符的分配规则)
  • 二、重定向
    • [2.1 重定向的原理](#2.1 重定向的原理)
      • [2.1.1 输出重定向原理](#2.1.1 输出重定向原理)
      • [2.1.2 追加重定向原理](#2.1.2 追加重定向原理)
      • [2.1.2 输入重定向的原理](#2.1.2 输入重定向的原理)
  • 三、标准输出流与标准错误流的其区别
  • 四、dup2

一、文件描述符

1.1 fd

  1. 文件由进程在运行时打开,一个进程可同时打开多个文件,而系统中存在大量并发运行的进程,这意味着系统任一时刻都可能存在数量庞大的已打开文件。
  2. 为了高效管理这些已打开的文件,操作系统会为每个已打开的文件创建专属的 struct file 结构体,并用双链表将所有该结构体串联起来。如此一来,操作系统对已打开文件的管理,就转化为对这一全局双链表的增、删、查、改等基础操作,大幅简化了文件管理的核心逻辑。(即先描述,在组织)
  3. 但仅靠全局的双链表,无法区分哪些已打开文件归属于特定进程,因此操作系统还需额外建立进程与文件之间的关联关系,以此精准界定每个进程所持有、可操作的文件范围。

那么操作系统是如何建立进程与文件直接的关联关系的呢?

我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据从硬盘加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。

在进程的核心控制结构体 task_struct 中,存在一个指向 files_struct 结构体的指针;而 files_struct 结构体里包含了关键的指针数组 fd_array ------ 这个数组的下标,就是我们实际编程中使用的文件描述符

当进程执行打开 log.txt 文件的操作时,操作系统会按以下逻辑完成文件描述符的绑定:

  1. 首先将磁盘中的 log.txt 文件加载到内存中,并为其创建对应的 struct file 结构体(包含文件的读写位置、权限、关联的磁盘inode等核心信息);
  2. 把这个新创建的 struct file 结构体接入系统管理已打开文件的全局双链表,完成系统层面的文件管理登记;
  3. 将该 struct file 结构体的首地址,填入当前进程 fd_array 数组中下标为3的位置(因0、1、2被标准输入/输出/错误占用),使 fd_array[3] 指针精准指向该文件的 struct file 结构体;
  4. 最后将下标值3作为文件描述符返回给调用进程,进程后续通过这个描述符就能找到对应的 struct file,进而操作 log.txt 文件。

因此,我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。


1.2 补充说明

1.2.1 进程创建时默认打开0、1、2文件描述符的底层逻辑

我们常说"进程创建时会默认打开0、1、2",本质是操作系统为每个新进程完成了硬件设备到文件描述符的绑定:

  1. 0对应标准输入流 (关联键盘)、1对应标准输出流 (关联显示器)、2对应标准错误流(同样关联显示器)。
  2. 键盘和显示器作为硬件设备,会被操作系统识别并纳入文件管理体系: 进程创建时,操作系统会为键盘、显示器分别创建对应的struct file结构体,将这些结构体接入全局的已打开文件双链表,再把3个结构体的首地址依次填入进程files_structfd_array数组下标0、1、2的位置。
  3. 至此,进程无需手动调用open,就默认拥有了对键盘(0)、显示器(1/2)的操作能力。

1.2.2 磁盘文件与内存文件的核心区别

  • 磁盘文件 :存储在磁盘上的静态文件,是文件的"持久化形态",由两部分构成:
    ① 文件内容:文件中实际存储的业务数据(如文本、二进制流);
    ② 文件属性(元信息):文件的基础描述信息,包括文件名、大小、创建时间、权限、所属用户等。
  • 内存文件 :磁盘文件被加载到内存后的动态形态,是操作系统为操作文件创建的"内存映射"。
    磁盘文件与内存文件的关系,类比"程序与进程":程序是磁盘上的静态代码,运行后成为内存中的进程;磁盘文件是静态存储的文件,被打开/加载后成为内存中的文件。
  • 加载规则:文件加载到内存时采用"按需加载"策略------优先加载文件属性(元信息),仅当需要读取、写入文件内容时,才延后加载具体的文件数据,以此节省内存资源。

1.2.3 文件写入的缓冲区机制

向文件写入数据时,并非直接写入磁盘,而是先写入该文件对应的内存缓冲区;操作系统会通过预设策略(如缓冲区满、定时刷新、手动调用fsync),将缓冲区中的数据批量刷新到磁盘,以此减少磁盘IO次数,提升整体读写效率。


1.3 文件描述符的分配规则

c 复制代码
#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的文件,此后文件描述符的分配又会是怎样的呢?

c 复制代码
close(0);

我们发现第一个打开的文件获取到的文件描述符变成了0,而之后打开文件获取到的文件描述符还是从3开始依次递增的。

我们再试试将文件描述符为0和2的文件都关闭。

注意:1不能关闭,因为他是标准输出,关闭了我们无法从显示器观察现象

c 复制代码
close(0);
close(2);

可以看到前两个打开的文件获取到的文件描述符是0和2,之后打开文件获取到的文件描述符才是从3开始依次递增的。

结论: 文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的。


二、重定向

2.1 重定向的原理

上面我们已经了解了文件描述以及文件描述符的分配规则,接下来我们通过实例来看看重定向的本质。


2.1.1 输出重定向原理

输出重定向的原理就是我们本该输出到一个文件的原理输出到另外一个文件中。

如上述图片,我们让本应该输出到"显示器文件"的数据输出到log.txt文件当中,那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭。

c 复制代码
#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文件当中。

注意:

  1. printf 函数默认向 stdout(标准输出流)输出数据,stdout 指向 struct FILE 类型的结构体,该结构体中存储的文件描述符固定为1,因此 printf 本质是向文件描述符为1的文件(显示器)输出数据。
  2. 调用 printf 后,数据不会立即写入操作系统内核,而是先存入C语言标准库维护的用户态缓冲区 ;只有当缓冲区满、遇到换行符(\n)或程序结束时,数据才会批量刷新到操作系统层面。若需让数据即时输出,需通过 fflush(stdout) 手动强制刷新C语言缓冲区,将数据同步到操作系统。

2.1.2 追加重定向原理

我们知道输出重定向的时候会将原有文件里面的内容数据覆盖,而追加重定向是对原有内容数据不进行覆盖,追加再后面输出数据。

如我们想让本应该输出到"显示器文件"的数据追加式输出到log.txt文件当中,那么我们应该先将文件描述符为1的文件关闭,然后再以追加式写入的方式打开log.txt文件,这样一来,我们就将数据追加重定向到了文件log.txt当中。

c 复制代码
#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.2 输入重定向的原理

输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。(如原来是从键盘获取,我们可以让他从文件获取)

我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0。

c 复制代码
#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的文件读取数据。


三、标准输出流与标准错误流的其区别

标准输出流和标准错误流对应的都是显示器,它们有什么区别?

c 复制代码
#include <stdio.h>
int main()
{
	printf("hello printf\n"); //stdout
	perror("perror"); //stderr

	fprintf(stdout, "stdout:hello fprintf\n"); //stdout
	fprintf(stderr, "stderr:hello fprintf\n"); //stderr
	return 0;
}

这样看起来标准输出流和标准错误流并没有区别,都是向显示器输出数据。

但我们若是将程序运行结果重定向输出到文件log.txt当中,我们会发现log.txt文件当中只有向标准输出流输出的两行字符串,而向标准错误流输出的两行数据并没有重定向到文件当中,而是仍然输出到了显示器上。

因为我们使用重定向时,重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。


四、dup2

要完成重定向我们只需进行fd_array数组当中元素的拷贝即可。

  • 例如,我们若是将fd_array[3]当中的内容拷贝到fd_array[1]当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt。

Linux操作系统中,就给我们给了一个实现此功能的函数:dup2

原型如下:

c 复制代码
int dup2(int oldfd, int newfd);
  1. 函数功能

dup2 会将 fd_array[oldfd] 的内容拷贝到 fd_array[newfd] 中,若有必要,会先关闭文件描述符为 newfd 的文件。

  1. 函数返回值

dup2 调用成功时返回 newfd,调用失败时返回 -1。

  1. 使用注意事项
  • 若 oldfd 并非有效的文件描述符,dup2 调用失败,且此时文件描述符为 newfd 的文件不会被关闭;
  • 若 oldfd 是有效的文件描述符,且 newfd 与 oldfd 的值相同,则 dup2 不执行任何操作,直接返回 newfd。

我们将打开文件log.txt时获取到的文件描述符fd和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。

c 复制代码
#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文件当中。


✍️ 坚持用 清晰易懂的图解 + 可落地的代码,让每个知识点都 简单直观!

💡 座右铭 :"道路是曲折的,前途是光明的!"

相关推荐
盼小辉丶2 小时前
PyTorch实战(25)——使用PyTorch构建DQN模型
人工智能·pytorch·深度学习·强化学习
猫猫的小茶馆4 小时前
【Linux 驱动开发】七. 中断下半部
linux·arm开发·驱动开发·stm32·嵌入式硬件·mcu
cyber_两只龙宝4 小时前
LVS-DR模式实验配置及原理详解
linux·网络·云原生·智能路由器·lvs·dr模式
好好学习啊天天向上9 小时前
C盘容量不够,python , pip,安装包的位置
linux·python·pip
时见先生9 小时前
Python库和conda搭建虚拟环境
开发语言·人工智能·python·自然语言处理·conda
a努力。9 小时前
国家电网Java面试被问:混沌工程在分布式系统中的应用
java·开发语言·数据库·git·mysql·面试·职场和发展
Yvonne爱编码9 小时前
Java 四大内部类全解析:从设计本质到实战应用
java·开发语言·python
wqwqweee9 小时前
Flutter for OpenHarmony 看书管理记录App实战:搜索功能实现
开发语言·javascript·python·flutter·harmonyos
li_wen019 小时前
文件系统(八):Linux JFFS2文件系统工作原理、优势与局限
大数据·linux·数据库·文件系统·jffs2