【Linux】基础IO-----重定向与缓冲区

目录

一、文件描述符分配规则:

二、重定向:

1、本质(原理):

2、dup2的使用:

3、添加重定向功能到shell:

4、stdout与stderr:

三、Linux下一切皆文件:

四、缓冲区:

1、引入

2、缓冲区的刷新方案:

3、缓冲区存在的意义:

4、fork和缓冲区

5、拓展:

6、模拟实现C库:


一、文件描述符分配规则:

从0下标开始,寻找 最小的 没有被使用的 数组的位置,它的下标就是新文件的文件描述符

证明:

从上一篇我们了解到了默认打开的新文件的文件描述符的下标是3,因为系统会默认为我们打开三个文件流:stdin,stdout,stderr,分别对应着0,1,2,所以接下来打开文件的文件描述符就是三

那么为什么不是从下标为3开始寻找呢?接下来我们关闭前三个文件之一来进行实验看看

如上,整体思路就是

1、首先关闭1,也就是关闭标准输出流stdout

2、然后再打开一个文件

3、再向文件描述符为1的文件里面写入五行字符串(如果没有关闭stdout,那么应该会在显示器上打印该五行字符串)

4、最后再向标准错误流(也就是文件描述符为2的文件中打印fd,由于这个也是指向显示器的,所以fd这一行会在显示器中打印

如上就是我们看到的现象,

1、首先当前目录中是没有log.txt文件的,

2、执行mytest程序后和预期的一样,会在显示器打印log.txt的文件描述符,这个文件描述符为1,3、因为我们在代码中首先就把stdout给关闭了,并且此时我们查看log.txt文件就发现,本来应该写入到显示器中的字符串写入到了log.txt中

所以就可以证明:打开文件时,给这个文件的文件描述符分配原则就是从0开始的

此时可以发现:本应该输出到一个文件的数据重定向输出到另一个文件中 ,这个就叫做输出重定向

输出重定向是覆盖式输出数据,而 追加重定向 是追加式输出数据
输入重定向就是,将我们本应该从一个文件读取(stdin)数据,现在重定向为从另一个文件读取数据

想要实现上述功能就只需在open打开方式中第二个参数修改宏即可

二、重定向:

1、本质(原理):

在明白文件描述符的概念以及其是怎么分配的,就可以理解重定向的本质了,其实其本质就是修改文件描述符表的下标对应的指针内容

如上,这是一个进程的文件描述符表,里面前3个文件描述符被使用了,然后我们把文件描述符为1的文件close掉(这个操作实际上就是把1中的指针置空,标记为未使用的)然后我们创建了一个新的文件,这样就会从0开始往后找,这个时候发现1位置的地方没有被使用,就把log.txt文件的struct file*这个指针放到1中,

这样,write函数再向文件描述符为1的位置写入字符串实际上就不在是往显示器文件中写了,就是往log.txt文件中写入字符串了,

本来向显示器文件里面写东西转化为向log.txt文件里面写东西,而这个过程就是重定向

2、dup2的使用:

首先,在man函数手册中看看这个函数的参数,返回值,所在的头文件,这是一个系统接口

如上虽然有两个,但是一般是使用dup2的

解析:

返回值:dup2如果调用成功,返回newfd,否则返回-1

dup2函数的形参就是两个文件标识符,这个函数的作用就是将文件描述符表中里面的一个指针覆盖另一个指针,

如上,当新打开一个文件,就会把新开的文件的struct file*指针放在文件描述符下标为3的地方,然后在进行系统调用接口:dup(fd,1)这样就会把fd(3)中的指针覆盖到1中,这样的话明明文件描述符为1应该是指向显示器文件的,经过这次操作后就会指向log.txt文件

此时我们就将输出重定向到了文件log.txt

如下是语言层面的实现,打开一个文件,在输出重定向,也就是新打开文件的文件描述符里面的指针覆盖文件描述符1里面的指针,这样的话,文件描述符1就指向新打开的文件了,那么write函数继续往文件描述符为1的文件写,就不在是往显示器文件写了,就是往新指向的log.txt文件写了

由于open第二个参数里面的宏是追加的,所以写入文件中就是追加写了

cpp 复制代码
int main()
{
	int fd = open("log.txt",O_CREAT|O_WRONLY|O_APPEND,0666);
	if(fd < 0)
	{
		perror("open fail");
		return 1;
	}
	dup2(fd,1);
	int cnt = 5;
	const char* message = "hello world\n";
	while(cnt--)
	{
		write(1,message,strlen(message));
	}
	return 0;
}

如果想变成清空后在写入就将宏O_APPEND修改为O_TRUNC

如果是输入重定向也就是修改一些参数即可

如上,这就是open,以读的形式打开,然后把fd位置的指针覆盖0位置的指针,使0位置的指针不在指向stdin,而是新打开文件的log.txt,这样的话就不在是从键盘文件里面读取了,就是从log.txt文件里面进行读取了

3、添加重定向功能到shell:

思路:

在原先代码上加些功能即可,

如上,增加了这些宏和全局变量,

作用:

flag是作为判断重定向为哪一个,和上面的宏一起组成判断,filename能够拿到重定向后面的文件

然后在初始化中检查当前指令是否有重定向标志(</>/>>)

最后在普通命令的子程序中进行判断,进行对应的程序替换

如下:

整体意思就是

通过上述代码,会不会有个问题,为什么做了重定向的工作,在后面再进行程序替换的时候难道不影响吗?

为了解决上述问题,就要知道一个进程的task_struct,进程替换,和文件打开的关系了,

如下:

在上面的图里面,task_struct和文件管理,是内核数据结构,

而虚拟地址空间,页表,物理内存,是进程数据结构,

这两个是解耦合 关系,在Linux系统中,进程数据结构内核数据结构是分开管理的,这种设计使得系统更加灵活和高效

而对于程序替换中,物理内存,程序和代码加载替换掉物理内存,页表重新映射

在内核数据结构里 并不关心 进程数据结构中是怎么操作的

所以文件的重定向和进程替换之间没有啥影响

4、stdout与stderr:

这两个都是指向显示器的文件的,那么这两个存在的意义是啥呢?

在程序猿的眼中当程序出现问题的时候,只需要关心报错的信息,那么如果无论是正确的信息还是错误的信息都往显示器中打印,那么程序猿对程序调试的效率就会大大下降,所以就有两个流,一个存储错误信息,一个存储正常的信息,那么就可以提高锁定问题的效率了

如下,就可以进行重定向,把正常的信息打印到一个装正常内容的文件中,把错误的信息写入到一个装错误信息的文件中,这样只需看装错误的信息的文件即可

当然,也可以把所有信息都输入到同一个文件中,如下,使用指令:

这就是运行一个程序运行的结果是本来打标准输出到显示器中的,但是后面1>both.txt将 标准输出 重定向到 both.txt中,然后2>&1在将标准错误输出 重定向到 标准输出,但是此时标准输出已经重定向到both.txt了,所以程序运行后无论是在1(标准输出)还是在2(标准错误输出)的结果就写到both.txt了

三、Linux下一切皆文件:

在操作系统看来,无论是是标准输入(键盘),还是标准输出(显示器),或者是其他底层外设,比如说网卡,声卡等等,当对其操作的时候对操作系统来说都会对其创建一个file的结构体对象,

对于操作系统来说,无论是硬件(外设),还是软件(文件),只需要提供相应的读方法和写方法就可以对其进行驱动

如下是例图:

解析:

首先,底层的外设给操作系统提供了读写方法,操作系统在管理这些外设的时候创建了file的结构体,这个结构体里面有一个指针指向另一个结构体,这个结构体通常包含多个函数指针,例如读、写、打开、关闭等操作的函数指针。通过这些函数指针,系统可以调用相应的操作函数来管理各种设备,包括磁盘、网络接口、字符设备等

然后操作系统给我们定义了系统调用,如下

这样当使用系统调用的时候,根据下层文件的不同,找到不同的外设提供的write,read方法,至于这些设备是怎么实现的就不是我们关心的了

在Linux中struct file这一层也叫做**VFS------virtual file system虚拟文件系统,**往上就是展现给用户的,用户看到的就是文件,所以就是Linux下一切皆文件

四、缓冲区:

1、引入

首先通过几个示例来进行理解:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>

int main()
{
	const char* fstr = "hello fwrite\n";
	const char* str = "hello write\n";
	//C
	printf("hello printf\n");	
	fprintf(stdout,"hello fprintf\n");
	fwrite(fstr,strlen(fstr),1,stdout);
	
	//system call
	write(1,str,strlen(str));
	return 0;
}

如上,当进行运行之后,应该会打印四行hello,如下,确实也没问题

并且,如果去掉\n的话,就会打印在一行,也没啥问题

如果在代码最后将stdout这个文件关闭了

那么在屏幕上也会打印四行,但是如果把后面的\n去掉,

可以看到C接口就不会打印在屏幕上了,但是system call依然会打印在屏幕上这是为什么呢,并且如下,尽管当有\n,但是当重定向到文件中的时候依然只有system call能够打印出

上面的这些现象是为什么呢?

首先我们了解,C语言的对文件操作的接口,其底层封装一定是调用了write这个system call,

我们又知道数据的写入并不是直接写到显示器中的,而是先写到缓冲区中,再刷新到显示器或者文件磁盘中的

所以再通过上述的情景引入,我们可以得出一个结论:当上述close(1)的时候,C语言 的文件操作接口中写入的一个缓冲区和system call 中文件操作写入的缓冲区不是同一个缓冲区,C语言中的缓冲区一定不在操作系统内部,不是系统级别的。

所以应该是如下的关系:

如上,我们平时所说的缓冲区是C语言级别的缓冲区,这是一个用户级别的缓冲区,所以当使用文件调用接口将数据写入的时候是写到了这个C语言级别的缓冲区,然后等到某个时机就将C语言级别的缓冲区里的内容写到系统级别的缓冲区,再刷新到磁盘,显示器中

所以当我们通过printf,fprintf的时候就将数据写到了C语言级别的缓冲区中,并且没有\n,的话,往显示器中写入就是行刷新,如果在后面将close(1)的时候1号文件struct file,系统文件缓冲区就被关掉了,就无法向磁盘写入数据了,也就看不到显示结果了

相对应,如果有\n的话,那么就会进行刷新,这样就可以在显示器中看到了,如果是重定向到文件,那么就会是全刷新,这样的话就需要将缓冲区写满才会刷新,所以重定向到文件中,文件就只看得到system call了

2、缓冲区的刷新方案:

这里的刷新方案是指的是语言级别的缓冲区的刷新方案

1、无缓冲:这就是直接刷新,无论往哪里打印,打印什么,直接刷新到系统级别的缓冲区中

2、行缓冲:直到遇到'\n'在进行刷新

3、全缓冲:直到语言级别的缓冲区被写满的时候就会进行刷新

注意:

向显示器中写入的时候,C缓冲区是采用的是行缓冲,

向普通文件中写入的时候,C缓冲区是采用的是全缓冲

3、缓冲区存在的意义:

缓冲区实现其实就是一个buffer数组,在实现的时候配合不同的刷新策略,提高IO效率

缓冲区就像菜鸟驿站,如果没有菜鸟驿站的话,我们要想好朋友寄东西,那么快递公司就一个一个地送,这样效率就很低,但是如果有菜鸟驿站,那么当一个学校的快递都在菜鸟驿站的时候,这个时候一起打包送出去效率就很高了,

CPU的计算速率很快,而磁盘的读取速度相对于 CPU 来说是很慢的,因此需要先将数据写入缓冲区中,依据不同的刷新策略,将数据刷新至内核缓冲区中,供CPU进行使用,这样做的是目的是尽可能的提高效率,节省调用者的时间

当数据进行打印的时候,先将数据打印到用户缓冲区,当某个时机的时候,就将用户缓冲区的数据打印到磁盘里面,由于这种数据进入,数据流出,很像一条河流,所以用户缓冲区也被叫做流

4、fork和缓冲区

如上,当向文件写入的时候可以看到C语言接口写了两遍,但是system call只写了一遍,并且 是采用的是全缓冲,如右边可以看到,当最后经过5秒后,再程序结束后就全部刷新出来了

那么为什么C语言接口写了两遍,但是system call只写了一遍呢?

首先我们知道,当程序结束的时候,语言级别的缓冲区就都会刷新,并且fork创建子进程的时候会拷贝父进程的代码和数据,当子进程刷新缓冲区的时候就相当于对数据进行修改,那么就会发生写实拷贝,所以就会有两份缓冲区的数据了

5、拓展:

exit和_exit

前面我们了解到,_exit是系统接口,exit是封装了_exit,其主要区别是exit在退出的时候会帮我们把用户级别的缓冲区刷新,然后_exit不会,这是因为_exit是系统级别的,他不知道用户级别的缓冲区在哪,所以也就刷新不好,而exit在用户层,就是先刷新语言级别的缓冲区在调用_exit退出

FILE:

​​​​​​FILE是在用户级别的,并不是在系统级别的,因为语言就是属于用户的

文件操作绕不开FILE,用户级别的缓冲区是在FILE这个结构体中进行维护的,这个FILE里面还有对应的维护信息,还有文件描述符等等

所以fopen事实上就是打开一个文件就会获取这个文件的文件描述符,然后在malloc一块空间,将文件描述符,缓冲区等等都保存在FILE这个结构体里面进行维护

6、模拟实现C库:

这里采用三个文件,Mystdio.h文件Mystdio.c文件和main.c文件,一个是声明函数,一个是实现对应函数一个是使用对应函数

Mystdio.h

如上是.h文件的声明,准备实现文件的打开,读写,刷新缓冲区的操作,

其中fileno是文件描述符,flag是刷新策略,outbuffer是维护的缓冲区,out_pos是缓冲区中写到哪儿了的指向

如上是.c文件实现的fopen函数,在底层是进行系统接口调用,然后根据第二个参数选项进行判断,是读写还是追加,打开失败就返回NULL,在进行初始化
如下是fwrite函数的实现,思路就是将所写的字符串拷贝到所维护的缓冲区中,然后在下面在进行缓冲区的刷新策略,这样的话就可以知道是什么时候进行调用write系统接口函数进行刷新

如下是fflush立即刷新缓冲区的函数和关闭缓冲区的函数,

因为关闭缓冲区的时候,如果缓冲区中还有数据就需要进行刷新,那么就可以进行实现fflush在这个函数里面进行判断

最后在main.c文件中进行函数的调用:

如上,这就是往log.txt文件中按行刷新的策略进行写入的,可以看到,每经过一秒后就会进行追加

C语言的跨平台性:

当我们这次实现C语言,其底层是通过Linux的系统调用接口进行实现的,那么我们是不是还可以通过Windows中系统调用接口实现,那么再将这两份代码进行条件编译进行裁剪实现,在Windows下就跑Windows的代码,在Linux下就跑Linux下的代码,这样的话就可以实现语言的跨平台性

相关推荐
好好学操作系统2 分钟前
autodl 保存 数据 跨区
linux·运维·服务器
dbitc5 分钟前
WIN11把WSL2移动安装目录
linux·运维·ubuntu·wsl
KingRumn5 分钟前
Linux同步机制之信号量
linux·服务器·网络
嵌入式学习菌5 分钟前
SPIFFS文件系统
服务器·物联网
旺仔Sec5 分钟前
2026年度河北省职业院校技能竞赛“Web技术”(高职组)赛项竞赛任务
运维·服务器·前端
BullSmall26 分钟前
linux 根据端口查看进程
linux·运维·服务器
herinspace30 分钟前
管家婆软件年结存后快马商城操作注意事项
服务器·数据库·windows
_F_y36 分钟前
Linux:进程间通信
linux
嘻哈baby38 分钟前
Ansible自动化运维入门:从手工到批量部署
运维·自动化·ansible
weixin_462446231 小时前
Kali/ubuntu Linux 中彻底删除 Cursor 编辑器(含 dpkg 非空目录警告解决)
linux·ubuntu·cursor