【Linux篇】基础IO - 揭秘重定向与缓冲区的管理机制

发布于:2025-04-08 ⋅ 阅读:(13) ⋅ 点赞:(0)

📌 个人主页: 孙同学_
🔧 文章专栏:Liunx
💡 关注我,分享经验,助你少走弯路!

在这里插入图片描述
在这里插入图片描述

一. 理解重定向

1.1 理解重定向

我们首先得知道文件描述符的分配原则:最小的没有被使用的作为新的fd分配给用户。

先来看现象:
我们关闭了1,所以下次新创建文件时,文件被分配到的文件描述符就是1
在这里插入图片描述

我们运行,发现并没有在屏幕上打印fd: 1
在这里插入图片描述
我们ll发现多了一个log.txtcat log.txt发现它显示fd:1
在这里插入图片描述
本来应该显示到显示器的内容竟然显示在了log.txt文件里面。
不在屏幕上显示是因为我们把标准输出(1)关了,为什么会往文件里写呢?因为1这个位置变成了log.txt文件,这种现象就叫做重定向

解释现象:
我们在打开文件之前首先把文件描述符1close掉了,所以此时的文件描述符1就不再指向标准输出了,当我们open打开新的文件log.txt时,就找到了1,把新打开的log.txt的地址填进来。把1返回给上层用户,所以用户拿到的文件描述符就是1。可是我们接下来用到的printf是C语言提供的函数,它是往stdout中打印的,stdout封装的就是1printf只认stdout中的1,它找的时候就找到了log.txt,所以就写到了log.txt中了。
我们刚才做的在底层更改一个文件描述符内容的指向,这种现象叫做重定向。
在这里插入图片描述

再看一个现象:

我们在printf后面加上close(fd)
在这里插入图片描述
在这里插入图片描述

1.2 dup2

重定向的系统调用dup2

#include <unistd.h>

     int dup2(int oldfd, int newfd);

我们要实现重定向是想让1里面的指针指向新的文件,3如果是我们新创建的文件,那么我们应该把1里面的指针内容方到3里面还是把3里面的指针内容放到1里面呢?答案是把3里面的内容放到1里面,13的一份拷贝,即1fd的一份拷贝,所以oidfd就是fd1newfd,所以传参时dup2(fd,1)
在这里插入图片描述
我们就会发现它就不会再显示器上打了,而打印到了log.txt文件中。
在这里插入图片描述
这次我们dup2后不关闭fd,并且向fd里写入hello world
在这里插入图片描述
会发现hello world被打在了最前面,是因为有缓冲区的存在,先把系统调用里面的值打印出来,然后才是文件的值。
在这里插入图片描述

所以重定向的原理就是操作系统在源代码当中做操作系统级别的文件指针所对应的文件地址的拷贝

1.3 进一步理解重定向

输出重定向:
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 filestruct file结构体中虽然不能存在函数方法,但可以有函数指针,通过函数指针执行对应硬件的读写方法。相当于C语言实现的多态

 我们用户在上层通过文件描述符访问特定文件时,比如说read接口 把上层的数据拷贝到文件缓冲区里,做刷新把内容从文件缓冲区中调用对应的函数指针的write方法写到设备里。所以访问设备都是通过函数指针进行访问的,而大家的函数指针类型名参数都一样。

 所以上层访问底层不同的硬件设备时,上层就不需要知道你是磁盘,显示器,还是鼠标了,就屏蔽了底层的硬件差异

 因此把struct file以上统称为一切皆文件。把struct file这一层称之为虚拟文件系统(VFS)
在这里插入图片描述
📙总结: 一切皆文件是通过VFS即虚拟文件系统来实现的,我们用到的struct file属于虚拟文件系统而不属于具体的文件系统,对我们来说VFS中有文件的基本属性,缓冲区,函数指针。这样就可以通过函数中指针屏蔽掉底层不同的差异。

三. 缓冲区

3.1 什么是缓冲区

缓冲区是内存空间的一部分,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,缓冲区根据其对应的是输入设备还是输出设备分为输入缓冲区和输出缓冲区。相当于"菜鸟驿站“。

3.2 为什么要引入缓冲区

为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。

先看现象:

在这里插入图片描述
此时默认会往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语言接口的使用效率。系统内核也存在文件内核缓冲区,文件内核缓冲区可以提高系统调用的效率


👍 如果对你有帮助,欢迎:

  • 点赞 ⭐️
  • 收藏 📌
  • 关注 🔔

网站公告

今日签到

点亮在社区的每一天
去签到