序言
本篇文章的内容和上一篇文章 👉点击查看 紧密相连,所以为了更好的理解本篇文章,需要大家将前置知识准备好哦😇。
本文主要向大家介绍文件的重定向,以及基于用户级别的缓冲区和基于操作系统级别的缓冲区。原来看似简单的文件操作背后居然有这么多东西。
1. 重定向
1.1 引出问题
在上一篇文章的结束,我们提出了序号为 1
的文件描述符指向的是 显示器文件
,之后我们执行了下段代码:
13 int main(){
14
15 umask(0);
16
17 close(1);// 关闭显示器文件
18
19 // 写方式打开文件,并且文件不存在就创建一个,文件的权限是 rw-rw-r--
20 int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
21 if(fd == -1) perror("open");
22 printf("fd = %d.\n", fd);
23
24
25 return 0;
26 }
执行结果并没有在显示器上打印相应的内容,而是如下:
- 增添了一个名为
log.txt
文件 - 文件里面的内容就是需要打印的内容
非常奇怪,我们打印的内容怎么会跑到文件中呢?
1.2 思考推理
我们尝试顺着代码的逻辑进行推理💡,一切都是有迹可循的!!!
首先,close(1);
:这段代码关闭了显示器文件,大家还记得这幅图吗:
这张图大致的描述了系统如何管理的文件,以及进程通过进程描述符表控制文件。现在我们使用 close(1)
操作,这将清除描述符表中下标为 1
的内容:
咦?你只是将下标为 1
的位置置空了呀,并没有真正的释放该文件在内存中的空间呀?好的,知识加餐了哦☀️:
知识加餐:智能指针技术
我们知道 1
号文件对应的是显示器文件,请问在你的系统中,只有现在你控制的进程才会使用显示器文件吗?当然不是,其他有些进程可能也在使用,只是你不知道!在系统中也是如此,可能多个进程控制同一个文件
,所以你不用不代表其他进程不使用!
在上一篇文章中,我们介绍了文件结构体 struct file
里包含的信息,其中就包括:
f_count:表示有多少进程或文件描述符引用了这个文件。当 f_count 降到 0 时,文件将被关闭。
这其实就是一种智能指针技术,确保了资源(动态分配的内存)的自动且安全的释放,避免了内存泄漏,同时也避免了在对象生命周期结束之前过早地删除对象(即“悬挂指针”或“野指针”问题)
。
言归正传,所以现在下标为 1
的位置就空了,之后我们使用了 open()
函数创建了一个新文件,我们是不是需要将该文件的 struct file
指针放入我们的文件描述符表呀,然后操作系统从上到下遍历,发现 1
的位置是空的,代表没有存储任何其他文件结构体指针,那行,就放在这里吧:
现在 1
下标对应的文件就是 log.txt
了哈。
最后我们使用了 printf
函数,现在你只需要记住,后面我们会细讲,这个函数很老实,只会向下标为 1
的文件写入内容,(我们平时往显示器打印内容,说到底就是向显示器文件写入内容呀
),当下标为 1
指向的文件改变了,不关我的事,我只负责写,不负责你是谁!
破案了,这就是底层发生的事!本来写给显示内容,由于显示器文件被释放了,又创建了个文件占据了原来显示器文件所处于进程描述符表中的位置,并且 printf
也只会将内容输入到 下标为 1
的文件中,导致输入到新创建的文件中。
你说了这么多,口说无凭呀!证明一下呢!其实我们只需要运行程序,查看生成的文件内容就好啦,为什么呢?因为:printf("fd = %d.\n", fd);
,我们文件打印的内容本身就是该文件的 fd
,如果该文件的 fd
确实等于 1,那么就没问题:
😄Bingo !!!
1.3 什么是重定向
我们不是应该介绍重定向吗?怎么上面花了大量篇幅说明其他事情呢?其实这就是重定向!准确的说,应该是 输出重定向
。
文件重定向允许用户改变程序的 标准输入(stdin)、标准输出(stdout)或标准错误(stderr)
的 默认流向
。默认情况下,标准输入来自键盘,标准输出和标准错误输出到屏幕(控制台)。通过文件重定向,用户可以 将这些默认流向更改为文件或其他程序
,从而实现更灵活的数据输入和输出处理。
我们将本该输入到屏幕的内容,通过重定向,将改内容输入到了指定文件当中。
1.4 重定向函数 dup2
现在大家应该大致知道什么是文件重定向了,但是不是稍显麻烦了呀,每次我们都需要去关闭指定的文件。
你能够想到的,你的操作系统也肯定想到啦!而且已经为你准备了对应的系统调调用接口函数:
int dup2(int oldfd, int newfd);
oldfd
:待复制的文件描述符,它必须是一个已经打开的文件描述符。newfd
:目标文件描述符,即dup2
函数会将oldfd
复制到的文件描述符。- 返回值:成功时,返回新的文件描述符(即
newfd
),失败返回 -1
就比如我想要将本该输入到 1
对应文件的内容输入到 4
对应的文件中,就应该是:
dup2(4, 1);
举个栗子:
13 int main(){
14
15 umask(0);
16
17 // 写方式打开文件,并且文件不存在就创建一个,文件的权限是 rw-rw-r--
18 int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
19 if(fd == -1) perror("open");
20
21 dup2(fd, 1);
22
23 printf("fd = %d.\n", fd);
24
25
26 return 0;
27 }
2. FILE
在使用系统调用接口 open
打开 / 创建 一个文件时,会返回文件描述符 fd
。而我们使用 C
语言提供给我们的编程接口 fopen
打开 / 创建一个文件时,会返回一个指针 FILE*
。
2.1 FILE
结构体
FILE
其实就是 C
语言封装的结构体,我们可以推断出该结构体里面肯定包含有 fd
。因为 C
语言的封装是基于系统调用接口的,所以你不能脱离我本来就有的内容,可以比我多出一部分内容,不可能少!
现在我们来介绍该结构体中比较重要的包含的信息:
文件状态标志(File Status Flags)
:这些标志用于指示文件流的状态.。文件位置指示器(File Position Indicator)
:这个指示器跟踪当前在文件中的位置,通常是一个表示字节偏移量的整数或指针。缓冲区(Buffer)
:为了提高I/O
操作的效率。文件描述符(File Descriptor)
:该文件描述符是内核用来标识打开文件的。
2.2 stdin、stdout、stderr
在上一篇文章中,我们提到进程在启动时会为我们自动打开三个文件(分别关于 标准输入,标准输出,标准错误输出
),并分配文件描述符 0, 1, 2
。他们到了 C
语言中被封装成了一个 FILE
结构体:
文件描述符 0
:对应于stdin(标准输入)
。默认情况下,它指向用户的键盘输入,但可以通过重定向将其指向文件或其他输入源。文件描述符 1
:对应于stdout(标准输出)
。默认情况下,它用于向终端屏幕输出数据,但同样可以通过重定向将其输出到文件或其他输出设备。文件描述符 2
:对应于stderr(标准错误输出)
。它通常用于输出错误信息,以便即使stdout
被重定向,错误信息仍然能够显示在终端屏幕上。然而,stderr
也可以被重定向。
printf
是一个 C
语言函数,在上面我们说到他只会向 文件标识符为 1
的文件写入内容,其实这是有点不准确的,在 C
语言中,就应该按照他的方式,所以应该说是 printf
函数只会向 stdout
文件流写入内容。
2.3 fprintf
函数介绍
fprintf
比起 printf
灵活些,它可以将格式化的数据写入到 指定
的输出流中:
int fprintf(FILE *stream, const char *format, ...);
FILE *stream
:指向FILE
对象的指针,该FILE
对象标识了要写入数据的输出流。const char *format
:一个格式化字符串,用于指定后续参数如何被格式化和写入输出流。- 返回值:
fprintf
函数返回写入的字符数,如果发生错误,则返回一个负值。
举个栗子:
13 int main(){
14
15 umask(0);
16
17 // 写方式打开文件,并且文件不存在就创建一个,文件的权限是 rw-rw-r--
18 int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
19 if(fd == -1) perror("open");
20
21
22 fprintf(stdout, "fd = %d.\n", fd);
23
24
25 return 0;
26 }
3. 缓冲区
3.1 内核级别的缓冲区
在上一篇文章中👉点击查看 我们介绍了内核级别的缓冲区,该缓冲区被操作系统所控制,主要的功能是减少对硬件的直接访问次数,从而提高系统的整体性能。
在这里就不多赘述了。🐷
3.2 用户级别的缓冲区
大家清细看 FILE
结构体里面包含什么,是不是有一个 缓冲区(Buffer)
,这就是用户级别的缓冲区,该缓冲区被进程所控制。
那为什么要大费周章的再弄一个用户级别的缓冲区呢?就拿些内容举例:我要将我的内容写到一个文件中,最终是不是还是要先写到系统控制的缓冲区当中呢?肯定是的呀,所以要调用系统调用的接口将内容写到缓冲区当中。但是调用系统调用接口也是会有代价的,它涉及到用户态到内核态的切换以及上下文的保存和恢复。通过使用用户级别的缓冲区,程序可以在一次系统调用中传输更多的数据,从而减少系统调用的次数,提高程序的执行效率。
3.3 缓冲区的刷新机制
缓冲区的刷新机制是内存管理中一个重要的概念,将介绍何时缓冲区会进行一次刷新:
-
无缓冲(Unbuffered)
:
数据一写入到缓冲区就立即刷新到磁盘或其他存储介质。
这种策略适用于需要立即处理数据的情况,但可能会降低性能,因为每次写入都会触发系统调用。 -
行缓冲(Line Buffered)
:
当在缓冲区中遇到换行符(\n
)时,缓冲区中的数据会被刷新到磁盘或其他存储介质。
显示器(stdout
)通常采用行缓冲策略,因为它符合人类的阅读习惯,可以逐行显示输出内容。 -
全缓冲(Fully Buffered)
:
当缓冲区被填满时,缓冲区中的数据才会被刷新到磁盘或其他存储介质。
磁盘文件通常采用全缓冲策略,因为它可以累积更多的数据一次性写入磁盘,从而提高效率。 -
用户强制刷新
:
通过调用特定的函数(如C
语言中的fflush
函数)来强制刷新缓冲区中的数据。
这在需要立即将缓冲区中的数据写入磁盘时非常有用。 -
进程退出时刷新
:
当进程退出时,操作系统通常会自动刷新缓冲区中的数据,以确保所有数据都被正确写入磁盘。
这是一种隐式的刷新机制,不需要用户显式调用刷新函数。
4. 总结
在这篇文章中,主要向大家介绍了重定向的概念,方法以及缓冲区这一重要概念,希望大家有所收获。😊