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

📌 个人主页: 孙同学_

🔧 文章专栏: Liunx

💡 关注我,分享经验,助你少走弯路!


文章目录

    • [一. 理解重定向](#一. 理解重定向)
    • [二. 理解一切皆文件](#二. 理解一切皆文件)
    • [三. 缓冲区](#三. 缓冲区)
      • [3.1 什么是缓冲区](#3.1 什么是缓冲区)
      • [3.2 为什么要引入缓冲区](#3.2 为什么要引入缓冲区)

一. 理解重定向

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

cpp 复制代码
#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 进一步理解重定向

输出重定向:
cpp 复制代码
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
   if(fd < 0) exit(1);//打开失败直接退出

   dup2(fd,1);//输出重定向,把本来打印到显示器上的内容,打印到fd中
   close(fd);
追加重定向:
cpp 复制代码
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
   if(fd < 0) exit(1);//打开失败直接退出

   dup2(fd,1);//输出重定向,把本来打印到显示器上的内容,打印到fd中
   close(fd);
输入重定向:
cpp 复制代码
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

任何文件的输出重定向:
cpp 复制代码
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依旧指向标准错误。

但我们如果想让标准输出和标准错误打印在不同的文件里,我们可以

cpp 复制代码
./a/out 1>log.normal 2>log.error

因此我们可以通过重定向 未来把常规消息错误消息 进行分离

如果我们想把标准输出和标准错误打印到同一个文件呢?有的同学肯定会想./a.out 1>lg.normal 2>log.normal,最后文件中只有标准错误的信息,原因是这个文件被打开了两次,打开文件时是先清空再写入,所以最后就只剩标准错误的信息了。

有一个解决办法是./a.out 1>lg.normal 2>>log.normal,使用追加的方式。

还有一个办法就是

cpp 复制代码
./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语言接口的使用效率。系统内核也存在文件内核缓冲区,文件内核缓冲区可以提高系统调用的效率


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

  • 点赞 ⭐️
  • 收藏 📌
  • 关注 🔔
相关推荐
努力努力再努力wz12 分钟前
【Linux实践系列】:用c/c++制作一个简易的进程池
linux·运维·数据库·c++·c
QING6181 小时前
详解:Kotlin 类的继承与方法重载
android·kotlin·app
QING6181 小时前
Kotlin 伴生对象(Companion Object)详解 —— 使用指南
android·kotlin·app
liyongjun63161 小时前
CentOS 下 Zookeeper 常用命令与完整命令列表
linux·服务器·zookeeper·centos
一一Null1 小时前
Android studio 动态布局
android·java·android studio
假女吖☌1 小时前
Maven 编译指定模版
java·开发语言·maven
体育分享_大眼3 小时前
从零搭建高并发体育直播网站:架构设计、核心技术与性能优化实战
java·性能优化·系统架构
琢磨先生David4 小时前
Java 在人工智能领域的突围:从企业级架构到边缘计算的技术革新
java·人工智能·架构
巨可爱熊5 小时前
高并发内存池(定长内存池基础)
linux·运维·服务器·c++·算法
计算机学姐5 小时前
基于SpringBoo的地方美食分享网站
java·vue.js·mysql·tomcat·mybatis·springboot·美食