【Linux】理解系统中一个被打开的文件

news2024/9/29 7:21:58

文件系统

  • 前言
  • 一、C语言文件接口
  • 二、系统文件接口
  • 三、文件描述符
  • 四、struct file 对象
  • 五、stdin、stdout、stderr
  • 六、文件描述符的分配规则
  • 七、重定向
    • 1. 重定向的原理
    • 2. dup2
    • 3. 重谈 stderr
  • 八、缓冲区
    • 1. 缓冲区基础
    • 2. 深入理解缓冲区
    • 3. 用户缓冲区和内核缓冲区
    • 4. FILE

前言

首先我们在前面的学习中,知道了 文件 = 内容 + 属性,那么我们对文件的操作就是分别对内容和属性操作。

当我们要访问一个文件的时候,都是先要把这个文件打开,那么是谁把文件打开呢?答案是进程打开文件,在打开文件前,文件是存放在磁盘上;打开文件后,文件被进程加载到内存中。

一个进程可以通过操作系统打开一个文件,也可以打开多个文件,所以操作系统一定会给进程提供系统调用接口去打开文件;这些文件被加载到内存中,可能会存在多个,同时,加载磁盘上的文件,一定会涉及到访问磁盘设备,这些操作由操作系统来完成;所以在操作系统运行中,可能会打开很多个文件!那么此时操作系统就要将打开的文件进行管理,怎样管理呢?我们前面也学过,先描述,再组织! 一个文件要被打开,一定要现在内核中形成被打开的文件对象!

一、C语言文件接口

我们简单复习一下C语言阶段使用的文件接口,其中详细的内容链接 -> 文件操作.

其中我们复习一下 fopenfputs这个接口,如下介绍:

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

其中 fopen 中的 path 是我们需要打开的文件,mode 是以什么样的方式打开。下面我们分别使用一下以 wa 方式打开,代码如下:

		  1 #include <stdio.h>
		  2 
		  3 int main()
		  4 {
		  5     FILE* fp = fopen("test.txt", "w");                                                                                                        
		  6     if(fp == NULL)
		  7     {
		  8         perror("fopen");
		  9         return 1;
		 10     }
		 11 
		 12     const char* str = "aaaaaaaaaaaaaaaaaa\n";
		 13     int cnt = 10;
		 14     while(cnt)
		 15     {
		 16         fputs(str, fp);
		 17         cnt--;
		 18     }
		 19 
		 20     fclose(fp);
		 21     return 0;
		 22 }

其中以 “w” 方式打开是按照写方式打开,如果文件不存在就创建它;并且以 “w” 方式打开会先清空文件内容!

例如下面场景,我们先运行程序打开 test.txt 写入内容,再重定向重新写入内容,会发现原有内容已经被清空了:

在这里插入图片描述

当以 “a” 方式打开,代码如下:

		  1 #include <stdio.h>
		  2 
		  3 int main()
		  4 {
		  5     FILE* fp = fopen("test.txt", "a");                                                                                                        
		  6     if(fp == NULL)
		  7     {
		  8         perror("fopen");
		  9         return 1;
		 10     }
		 11 
		 12     const char* str = "aaaaaaaaaaaaaaaaaa\n";
		 13     int cnt = 10;
		 14     while(cnt)
		 15     {
		 16         fputs(str, fp);
		 17         cnt--;
		 18     }
		 19 
		 20     fclose(fp);
		 21     return 0;
		 22 }

结果如下,所以我们得出结论,“a” 方式是从文件结尾出开始写入,即追加,不清空文件内原有内容:

在这里插入图片描述

所以我们得出以 “w” 方式打开文件相当于重定向写入,即 echo "xxx" > filename;以 “a” 方式打开文件相当于追加重定向写入,即 echo "xxx" >> filename.

二、系统文件接口

接下来我们认识一下系统给我们提供的系统文件接口 open,下面看一下文档介绍:

在这里插入图片描述

其中 open 的返回值是 fd(文件描述符),如下图介绍:

在这里插入图片描述

我们可以看到,如果文件创建成功会返回新文件的文件描述符,关于文件描述符我们下面再介绍。

其中 open 系统接口第一个参数 pathname 我们都知道,就是需要打开文件的名字;关于第二个参数我们需要介绍一下,关于函数传入标志位的技巧,是 Linux 中常用的传参方式;例如我们想在函数传参的时候传入指定的宏,它就会帮我们执行对应的宏的指令;如下代码:

		  1 #include <stdio.h>  
		  2   
		  3 #define Print1 1      // 0001  
		  4 #define Print2 (1<<1) // 0010  
		  5 #define Print3 (1<<2) // 0100
		  6 #define Print4 (1<<3) // 1000
		  7 
		  8 void Print(int flags)
		  9 {
		 10     if(flags & Print1) printf("hello 1\n"); 
		 11     if(flags & Print2) printf("hello 2\n");
		 12     if(flags & Print3) printf("hello 3\n");
		 13     if(flags & Print4) printf("hello 4\n");
		 14 }
		 15 
		 16 
		 17 int main()
		 18 {
		 19     Print(Print1);
		 20     printf("\n");
		 21     Print(Print1 | Print2);
		 22     printf("\n");
		 23     Print(Print1 | Print2 | Print3);
		 24     printf("\n");
		 25     Print(Print3 | Print4);
		 26     printf("\n");
		 27     Print(Print4);                                                                                                                            
		 28     return 0;
		 29 }

结果如下:

在这里插入图片描述

所以 open 的第二个参数实际上是一些系统定义的宏定义,在 open 的介绍文档中有介绍,如下图:

在这里插入图片描述

在这里插入图片描述

当我们想要以什么方式打开该文件时,就传入对应的宏定义,这就是 open 的第二个参数。我们先简单介绍几个宏定义:

			 O_RDONLY: 只读打开
			 O_WRONLY: 只写打开
			 O_RDWR : 读,写打开
			 以上三个宏,必须指定一个且只能指定一个

			 O_CREAT : 若文件不存在,则创建它。需要使用 mode 选项,来指明新文件的访问权限
			 O_APPEND: 追加写
			 O_TRUNC: 打开后清空原内容

接下来我们先介绍另一个系统接口 write,我们先看看 man 手册,如下:

在这里插入图片描述

如上图,write 的参数列表比较好理解,第一个参数 fd 就是需要写入文件的文件描述符;第二个参数 buf 就是需要写入的字符串;第三个参数 count 就是需要写入的个数,注意这里不需要把 \0 的个数加上去,只需要将需要写入字符串的个数填上即可。

下面我们配对使用 openwrite ,如下代码:

		int main()
		{
		    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC);
		    if(fd < 0)
		    {
		        perror("open");
		        return 1;
		    }
		    printf("fd: %d\n", fd);
		
		    const char* str = "xxxxx\n";
		    write(fd, str, strlen(str));
		
		    close(fd);
			return 0;
		}

当我们运行起程序再查看文件属性的时候,会发现以下现象:

在这里插入图片描述

此时我们创建的 test.txt 的权限是乱码的!为什么呢?这就和 open 的第三个参数 mode 有关了,所以我们现在知道 mode 就是需要改写的权限,以八进制形式传入,下面我们对上面代码做修改:

		int main()
		{
		    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
		    if(fd < 0)
		    {
		        perror("open");
		        return 1;
		    }
		    printf("fd: %d\n", fd);
		
		    const char* str = "xxxxx\n";
		    write(fd, str, strlen(str));
		
		    close(fd);
		    return 0;
		}

再次运行程序:

在这里插入图片描述

此时我们发现 test.txt 的权限就恢复正常了,由于有权限掩码的原因,权限为 0664.

此时我们查看 test.txt 中的内容:

在这里插入图片描述

如果我们往文件中追加内容再重新打开文件的话,原有的内容会被清掉,因为我们传的是 O_TRUNC,即打开后清空原内容 ,例如下图:

在这里插入图片描述

当我们将代码中的 open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666) 改为 open("test.txt", O_WRONLY | O_CREAT | O_APPEND, 0666) 后,重新再执行程序后就可以追加内容了,如下图:

在这里插入图片描述

三、文件描述符

通过上面的学习,我们可以开始理解语言和系统的理解了,我们在上面使用的 FILE* fp = fopen("test.txt", "w"); 是C语言库函数的接口,它对应的是系统接口 int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); ;而 FILE* fp = fopen("test.txt", "a"); 对应的是系统接口 int fd = open("test.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);;那么我们得出结论,因为文件只能通过操作系统去访问,不能直接通过语言去访问,所以 fopen 的底层一定是对 open 的封装!

同时,我们肯定会对 fd(文件描述符) 很感兴趣,那么它到底是什么呢?下面我们通过创建多个文件观察 open 的返回值 fd,如下代码:

		int main()
		{
		    int fd1 = open("test1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
		    int fd2 = open("test2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
		    int fd3 = open("test3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
		    int fd4 = open("test4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
		
		    printf("fd1: %d\n", fd1);
		    printf("fd2: %d\n", fd2);
		    printf("fd3: %d\n", fd3);
		    printf("fd4: %d\n", fd4);
		
		    close(fd1);
		    close(fd2);
		    close(fd3);
		    close(fd4);
		    return 0;
		}

结果如下:

在这里插入图片描述

我们可以看到,连续创建文件时 fd 是一串连续的数字,类似于数组的下标。

四、struct file 对象

下面我们开始理解文件在操作系统中的表现;当一个进程需要打开一个在磁盘上的文件时,此时操作系统内可能会存在多个被打开的文件,那么这些文件需要被操作系统通过先描述再组织的形式管理起来;那么当操作系统需要打开一个文件的时候,需要为每个文件创建一个文件对象,在 Linux 中被创建的结构体对象叫做 struct file,即是被打开文件的描述结构体对象。

如果我们打开了很多个文件,那么每个文件都有它自己的 struct file,每个文件可能都由不同的进程打开的,但是最终操作系统都要将这些 struct file 管理起来,那么就可以通过双链表的形式将它们连接起来管理,所以从此往后,对打开文件的管理就转化成了对双链表的增删查改的管理。

那么操作系统如何知道哪个文件是由哪个进程打开的呢?所以操作系统不得不面临一个问题:如何维护进程和打开文件的对应关系?所以操作系统内核中,它为进程设计了一个结构体:struct files_struct ,该结构体也叫做进程文件描述符表,进程文件描述符表里有一个非常重要的数组:struct file* fd_array[],即一个数组指针。此时当一个进程打开一个文件时,操作系统会为该文件创建一个 struct file 对象,再把该对象的地址填入到 struct file* fd_array[] 中没有被使用的数组下标中,最后把该数组的下标返回给上层,其中这个数组的下标就是我们的 fd(文件描述符)

上面所说的理论过程可以通过下图进行理解:

在这里插入图片描述

下面我们重新理解一下 struct file 对象,那么 struct file 对象里面应该有什么呢?答案是内容+属性。首先我们需要知道 struct file 对象中有一些字段可以直接获得文件的所有属性,直接或间接包含如下属性:在磁盘的什么位置,权限、大小、读写位置等。除此之外,struct file 内还需要有自己对应的文件缓冲区,也就是说这是一段内存空间。当我们的文件没有被打开的时候,它是存放在磁盘当中的,当我们打开这个文件时,内核需要帮我们创建 struct file 对象,因为在内核中,struct file 是专门用来管理被打开文件的!

当进程需要读取文件数据的时候,必然是先要将数据加载到内存空间中,即 struct file 的文件缓冲区中;如果我们需要向这个文件中写入呢?同样地,我们也同样先需要将数据加载到内存中,在内存中进行对文件数据的增删查改,不能直接在磁盘中进行,因为需要根据冯诺依曼体系!这个过程是操作系统帮我们完成的,因为操作系统是硬件的管理者!

所以我们在应用层进行数据读写的时候,本质是什么呢?其实,本质就是将内核缓冲区中的数据进行来回拷贝!该过程如下图所示:

在这里插入图片描述

首先如果我们需要写入数据,我们根据进程找到相应的进程描述符表,根据文件描述符找到对应的 struct_file 对象,再将我们写入数据的 buffer 缓冲区拷贝到对应 struct_file 对象中的文件缓冲区中,然后操作系统会帮我们刷新缓冲区中的内容,这就完成了文件数据的写入。

同样地,如果需要读取文件数据,操作系统帮我们将文件的数据加载到文件缓冲区中,我们根据文件描述符找到对应的 struct_file 对象中的文件缓冲区,再将文件缓冲区中的内容拷贝到我们的 buffer 缓冲区中即可完成读取数据。

五、stdin、stdout、stderr

在该数组指针中为什么默认填入的 struct file 对象的地址是从下标 3 开始的呢?0、1、2 去哪里了呢?其实在进程运行的时候,默认把标准输入(键盘:stdin)、标准输出(显示器:stdout)、标准错误(显示器:stderr) 打开了,它们分别代表 0、1、2;那么它们也是文件吗?没错,因为 Linux 下一切皆文件,这个我们下面再解释。

首先我们要知道,操作系统访问一个文件时,只认文件描述符!即只能通过文件描述符访问!其次我们回顾C语言的文件接口中,返回值是 FILE*,那么FILE又是什么呢?其实它是C语言提供的结构体类型,而操作系统访问文件只认文件描述符,所以我们肯定,FILE结构体中必定封装了文件描述符!下面我们可以通过代码验证一下,首先 stdin、stdout、stderr 的返回值都是 FILE*,如下图,所以我们可以通过它们的返回值观察;

在这里插入图片描述

代码如下:

		int main()
		{
		    printf("stdin->fd: %d\n", stdin->_fileno);
		    printf("stdout->fd: %d\n", stdout->_fileno);
		    printf("stderr->fd: %d\n", stderr->_fileno);
		}

结果如下:

在这里插入图片描述

如上图,就验证了我们是对的。那么操作系统为什么要默认把 stdin、stdout、stderr 打开呢?答案是为了让程序员默认进行输入输出代码编写!

那么我们现在就要回答上面的问题了,如何理解Linux下一切皆文件呢?首先我们的外设设备,键盘、显示器、磁盘、网卡…它们都有自己对应的读写方法,而且方法肯定不一样的。

当操作系统打开键盘文件时,创建了对应的 struct file 对象,那么键盘的结构体对象中会有键盘对应的读写的函数指针,该函数指针指向的是键盘底层对应的读写方法!同理,当系统打开显示器、磁盘、网卡等文件也是如此。所以这时候我们就可以忽略底层硬件读写方法的差异,只需要关注软件层 struct file 对象中的有关读写的函数指针即可,在操作系统看来它们的读写方法都是一样的!因为每个 struct file 对象中都会有这样的指针!而该软件层可以称为VFS(虚拟文件系统 ),所以从这一层往上面看就要可以看作一切皆文件!这种情况我们可以看作使用C语言实现了继承和多态!

六、文件描述符的分配规则

由于系统默认把 fd 中的 0、1、2 打开了,所以默认地我们打开一个文件的时候,它的 fd 会是从 3 开始,这毫无疑问。

接下来我们再认识一个系统接口:read,我们先看一下手册:

在这里插入图片描述

如上图,read 就是根据 fd 来进行读取文件缓冲区的数据,将数据读到 buf 中,count 就是 buf 的大小,也就是我们期望读到多少数据;返回值就是实际读到多少的数据。

假设我们现在需要在键盘里面读取数据,读到什么就打印什么,如下代码:

				#include <stdio.h>
				#include <sys/types.h>
				#include <sys/stat.h>
				#include <fcntl.h>
				#include <unistd.h>
				
				int main()
				{
				    char buffer[1024];
				    ssize_t s = read(0, buffer, 1024);
				    if(s > 0)
				    {
				        buffer[s-1] = 0;
				        printf("echo# %s\n", buffer);
				    }
				    return 0;
				}

在这里插入图片描述

所以我们也侧面印证了系统是默认帮我们打开了0号文件描述符标准输入的。

接下来我们再认识一个系统接口:write,就是从对应的 fd 写入数据,我们先看手册:

在这里插入图片描述

接下来我们尝试往标准输出写入数据,如下代码:

				int main()
				{
				    char buffer[1024];
				    ssize_t s = read(0, buffer, 1024);
				    if(s > 0)
				    {
				        buffer[s-1] = 0;
				        write(1, buffer, strlen(buffer));
				        //printf("echo# %s\n", buffer);
				    }
				    return 0;
				}

在这里插入图片描述

如上结果,我们可是从来没有打开过 fd 为0或者1的文件,所以就证明了系统是默认打开了标准输入、标准输出和标准错误。

所以我们得出第一个结论,进程默认已经打开了0、1、2,我们可以直接使用0、1、2进行数据的访问!

接下来我们验证另一个问题,当我们关闭0号 fd 时,再打开一个文件时,会给该文件分配哪一个 fd 呢?下面我们验证一下:

				int main()
				{
				    close(0);
				    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
				    if(fd < 0)
				    {
				        perror("open faild");
				        return 1;
				    }
				
				    printf("fd:%d\n", fd);
				
				    close(fd);
				
				    return 0;
				}

结果如下:

在这里插入图片描述

同样地,如果我们将 2号 fd 关闭时,再打开一个文件,它的 fd 也是 2;这说明了什么呢?

我们得出第二个结论,文件描述符的分配规则是,寻找最小的,没有被使用的数据的位置,分配给指定的打开文件!

如果我们关闭 1号 fd 呢?如果我们关闭 1号 fd,再重新打开一个文件,并把它的 fd 打印出来,我们会发现,什么都没有打印出来,这是为什么呢?很简单,因为我们把 1号 fd 关闭了,把标准输出关闭了!

七、重定向

1. 重定向的原理

上面我们尝试过将 1号 fd 关闭后重新打开一个文件,再打印数据,会发现什么都没有打印出来,但是我们将代码做一下修改,如下:

				int main()
				{
				    close(1);
				    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
				    if(fd < 0)
				    {
				        perror("open faild");
				        return 1;
				    }
				
				    printf("fd: %d\n", fd);
				    printf("stdout->fd: %d\n", stdout->_fileno);
				    fflush(stdout);
				
				    close(fd);
				
				    return 0;
				}

我们打印多了一个语句,并且强制刷新了缓冲区,下面我们观察现象:

在这里插入图片描述

如上图,我们直接执行程序没有打印任何信息,但是我们读取 log.txt 的时候发现,怎么信息都往 log.txt 里面打印了呢?下面我们画一个图理解一下:

在这里插入图片描述

如上图,我们首先关闭了 1号 fd,然后打开 log.txt,根据文件描述符的分配规则,给 log.txt 分配的应该是 1号 fd,所以当前 1号 fd 是指向了 log.txt 的。而 printf 这样的接口只认 stdout,而 stdoutfileno 为1,即 printf 只认文件描述符 1;所以在上层 printf 打印的时候,只会往 1号 fd 里面打印的,而具体地,这个 1号 fd 如果指向显示器,它就往显示器里打印,如果指向 log.txt,就往 log.txt 里打印!

其实这就是重定向的原理!如果我们需要完成重定向功能,其实只需要将文件描述符下标不变,将数组中的内容发生改变即可,也就是说 1号 fd 以前指向的是显示器,我们只需要将新文件的地址填入 1号 fd 中,就会直接向新文件中写入了!

所以,重定向的本质其实就是修改特定文件描述符中的下标内容!

上面我们所实现的是输出重定向的功能,如果我们需要实现追加重定向呢?很简单,只需要修改 open 接口中的参数即可,将 O_TRUNC 改成 O_APPEND 即可,如下:

				int main()
				{
				    close(1);
				    int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
				    if(fd < 0)
				    {
				        perror("open faild");
				        return 1;
				    }
				
				    printf("fd: %d\n", fd);
				    printf("stdout->fd: %d\n", stdout->_fileno);
				    fflush(stdout);
				
				    close(fd);
				
				    return 0;
				}

结果如下图所示:

在这里插入图片描述

接下来我们尝试一下关闭0号 fd 后,再打开 log.txt,从 stdin 中读取数据,即实现输入重定向,如下代码:

				int main()
				{
				    close(0);
				    int fd = open("log.txt", O_RDONLY);
				    if(fd < 0)
				    {
				        perror("open faild");
				        return 1;
				    }
				
				    char buffer[1024];
				    fread(buffer, 1, sizeof(buffer), stdin);
				    printf("%s\n", buffer);
				
				    close(fd);
				
				    return 0;
				}

如上我们使用了 fread 接口,我们可以看一下手册:

在这里插入图片描述

结果如下:

在这里插入图片描述

如上图,本应该从标准输入键盘中读取数据的,但是由于我们关闭了 0号 fd,再打开 log.txt,所以最后从 log.txt 中读取了数据。

2. dup2

但是以上方式实现的重定向太麻烦了,每次都要关闭文件再重新打开文件,有没有简洁一点的方式呢?答案是有的,我们知道,重定向的本质就是将文件描述符表中的下标不变,改变数组中的内容即可,那么我们是不是就可以正常打开一个文件,再将该文件中的文件描述符表中的内容拷贝到 0 号、1 号、2 号 fd 中呢?例如下图:

在这里插入图片描述

那么我们是否有一些接口,可以帮助我们完成文件描述符级别的数组中的内容拷贝呢?答案是有的!以输出重定向为例,我们只需要将新打开的文件的数组中的地址拷贝到 1号 fd 中的数组内容中即可。

这样的接口叫做 dup2 ,下面我们认识一下这个接口,先看一下手册:

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

现在我们以输出重定向为例,我们观察 dup2 的参数,我们应该如何传入参数呢?我们观察该函数的描述,得知最后保留下来的应该是 oldfd,所以我们以输出重定向为例,3号 fd 就是保留下来的,1号 fd 就是被覆盖的,所以 fd 就是 oldfd,1就是 newfd,即 dup(fd, 1);

那么会有一些问题,例如我们将 fd 的内容拷贝到 1号 fd 后,是不是有两个文件指针指向 log.txt 呢?是的,那么 log.txt 怎么知道有几个文件指针指向自己呢?那么其中一个文件指针把 log.txt 关了会不会影响另外一个正在使用 log.txt 的文件指针呢?答案是不会的,因为在内核当中存在引用计数,该引用计数记录的是有多少个文件指针指向自己,当有一个文件指针把它关闭之后,引用计数会减一,并不会直接关闭文件,当引用计数为 0 时,说明已经没有文件指针指向自己,这时候才可以关闭文件。

然后我们使用 dup2() 实现一下输出重定向:

				int main()
				{
				    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
				    if(fd < 0)
				    {
				        perror("open faild");
				        return 1;
				    }
				    dup2(fd, 1);
				
				    printf("hello printf\n");
				    fprintf(stdout, "hello fprintf\n");
				
				    close(fd);
				    return 0;
				}

结果如下:

在这里插入图片描述

但是这跟我们以前用的重定向有什么区别呢,我们以前用的重定向如下:

在这里插入图片描述

我们在命令行是这样使用的,但是这跟我们上面学的又有什么关系呢?首先我们执行的命令行,都是进程在执行,首先要对命令行进行解析,如果识别到命令行中有 >, >>, < 这些符号,说明该指令是要具备重定向功能的;一旦识别到该指令是要具备重定向功能的,那么前半部分就是需要执行的指令,后半部分是我们需要重定向的目标文件。

所以我们可以修改以前模拟实现的 shell,使其具备重定向功能,其中简单实现重定向的 shell 的源码地址为:myshell.

3. 重谈 stderr

那么对于标准输入和标准输出相信我们已经很熟悉了,但是为什么要存在标准错误呢?尤其它指向的也是显示器!我们先来看看,它们都是往显示器上打印信息,如下:

				int main()
				{
				    fprintf(stdout, "hello, stdout\n");
				    fprintf(stderr, "hello, stderr\n");
				    return 0;
				}

在这里插入图片描述

如果我们将可执行程序进行输出重定向呢?如下所示:

在这里插入图片描述

如图,为什么 hello, stderr 还会打印出来呢?很简单,我们的输出重定向只是将 hello, stdout 的信息重定向进 log.txt 里面去了,不关 stderr 的事,所以它还是会打印到显示器上。

如果我们需要将 stdoutstderr 打印到一个文件里,应该怎么做呢?此时我们需要在后面加上 2>&1,即如下:

在这里插入图片描述

这又是什么意思呢?2>&1 我们可以理解成将 1号 fd 数组中的内容放到 2号 fd 中,即现在 2号 fd 中数组的文件指针也指向了 log.txt.

那么为什么要有 stderr 呢?其实我们现在写的重定向,如:./test > log.txt 是一种简略的写法,它的正常的写法应该是:./test 1 > log.txt,如下:

在这里插入图片描述

我们理解了 2>&1 之后,再看上面的写法应该就不难理解了。那么我们还可以像下面这样写,注意 2>&1 中间不能有空格:

在这里插入图片描述

所以我们还可以写成下面这样:./test 1 > log.txt 2>log.txt.error 此时我们就形成了两个文件:

在这里插入图片描述

这样写我们就能理解了,因为我们在进行重定向的时候,我们把 1号 fd 重定向到 log.txt 文件,把 2号 fd 重定向到 log.txt.error 文件里,这样我们以后写程序时,printf 打印的是常规信息,perror 打印的是错误信息,这样我们就可以将正确的信息和错误的信息分别保存到两个不同的文件中!

八、缓冲区

1. 缓冲区基础

首先我们需要知道缓冲区是什么呢?我们在前面也有所了解过,如下图:

在这里插入图片描述

当我们需要进行文件写入或者读取文件时,实际上就是将我们自定义的 buffer 缓冲区写入到C库为我们提供的缓冲区,然后再由C库的缓冲区根据 fd 找到文件对应的文件缓冲区进行写入或读取。本质上缓冲区就是一部分内存!

我们可以举一个例子来比喻缓冲区,我们可以将缓冲区比喻成我们平时的“菜鸟驿站”,“菜鸟驿站”是帮我们将快递运输到另一个地方,而缓冲区则是将数据运输至操作系统中的文件缓冲区!因为有缓冲区的存在,我们可以积累一部分信息再统一发送,这也就提高了发送的效率!所以缓冲区的主要作用是提高效率,提高使用者的效率!

缓冲区因为能够暂存数据,所以必定有一定的刷新方式,其中有以下几种:

  • 无缓冲(立即刷新)
  • 行缓冲(行刷新)
  • 全缓冲(缓冲区满了,再刷新)

以上是一般的策略,当然也有特殊情况,例如:

  1. 强制刷新,如 \nfflush()
  2. 进程在退出的时候,一般要进行刷新缓冲区。

而一般对于显示器文件,默认是进行行刷新;对于磁盘上的文件,默认是全缓冲,即缓冲写满再刷新。

2. 深入理解缓冲区

下面我们看一个例子,如下代码:

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

我们分别调用了三个C语言的接口,fprintfprintffputs,和一个系统接口:write,我们使用这几个接口往显示器上打印信息,结果如下:

在这里插入图片描述

如上图,可以打印到正确的结果。接下来我们对上面的代码进行一些修改,如下:

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

我们只在最后加上了 fork() 创建子进程,我们观察运行的结果:

在这里插入图片描述

结果也是没有问题的,但是我们将执行结果输出重定向到一个文件中的时候呢?如下:

在这里插入图片描述

如上图,为什么重定向到 log.txt 就会发生上面的现象呢?这里涉及到的问题是有点多的,我们一个一个来。

  1. 首先,当我们直接向显示器打印的时候,显示器文件的刷新方式是行刷新!而且我们的代码中输出的所有字符串,都有 \n,所以 fork() 之前,数据全部已经被刷新,包括 system call!所以 fork() 之后缓冲区已经被清空了,即使进程退出需要刷新缓冲区,也没有数据可刷新了!
  2. 当我们重定向到 log.txt 的时候,本质是向磁盘文件中写入,系统对于数据的刷新方式已经由行刷新变成了全缓冲!
  3. 全缓冲意味着缓冲区变大,实际写入的简单数据,不足以把缓冲区写满,即使有 \n 强制进行行刷新,但是由于此时系统默认的刷新方式是全缓冲,所以当 fork() 执行的时候,此时缓冲区的数据依旧在缓冲区中!
  4. 我们可以看到,分别向显示器和文件中打印的时候,系统接口 system call 始终都是只打印一次,反而是C语言的接口在变化,所以我们得出结论,我们目前所谈的“缓冲区”,和操作系统和系统接口是没有关系的;由于fprintf printffputs 底层都是对 write 的封装,所以引起这些变化的,只能和C语言本身有关!所以我们平时用得最多的其实是 C/C++ 提供的语言级别的缓冲区,也就是用户缓冲区
  5. C/C++ 提供的缓冲区,里面一定保存的是用户的数据,属于当前进程在运行时自己的数据!如果我们把数据交给了操作系统,即通过语言级别的缓冲区刷新到文件缓冲区中后,这个数据就属于操作系统了,不属于进程了!
  6. 当进程退出的时候,一般要进行刷新缓冲区,即便数据没有满足刷新条件!那么,刷新缓冲区属不属于清空或者“写入”操作呢?属于!当我们的 fork() 之后,总有一个进程需要先退出,子进程或者父进程,取决于OS的调度,那么当任意一个进程退出的时候,就要对缓冲区进行刷新,即发生了写入操作,就要发生写时拷贝!
  7. 但是我们发现,发生写时拷贝的都是C语言的接口,系统接口write并没有发生写时拷贝,即没有使用C语言的缓冲区,这是为什么呢?很简单,系统调用接口是在C语言之下的,它看不到语言级别的缓冲区,而是将数据直接写入到操作系统的缓冲区了!所以该数据就不属于进程了,也就不会发生写时拷贝了!

3. 用户缓冲区和内核缓冲区

我们通过上面得知,日常我们用得最多的其实是C/C++提供的语言级别的缓冲区,下面我们画一个图理解一下这两者的关系:

在这里插入图片描述

上图就是我们的代码写入到文件中的过程,以及缓冲区刷新的理解,但是为什么要有这个过程呢?我们直接使用 printf 写入到文件里不行吗?我们上面说过,缓冲区的存在就是提高使用者的效率,如果 printf 直接往操作系统里面写,会涉及到用户直接拷贝到操作系统里,这个过程的成本可远比拷贝到C语言缓冲区中再拷贝到操作系统的成本要大!因为缓冲区积累一定量后再往操作系统写入,和用户一次一次往操作系统里写入,明显前者效率更高,提高了 printf 的调用效率!所以 printf 只需要将我们用户定义的缓冲区,写入到C语言的缓冲区,由于大家都是同一个级别的拷贝,所以过程也很简单很快。至于C语言缓冲区,它可以积累上一段时间再一次性写入到操作系统中,只跑一次就能大大提高效率!

那么我们为什么要提高 printf 的调用效率呢?其实 printf 只是众多 IO效率的一个代表,还有许多接口例如:fprintf fputsscanf 等等都需要提高,所以C语言上所有IO级别的接口都需要提供缓冲区,都需要提高它们的效率,这样,我们写C语言的时候调用接口的效率就会非常快,从而使我们代码的效率就非常快了。

4. FILE

通过以前的学习我们知道,任何情况下我们输入输出的时候,都要有一个 FILE 结构体。

  • 因为 IO 相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过 fd 访问的。
  • 所以C库当中的 FILE 结构体内部,必定封装了 fd
  • 既然 FILE 结构体内部已经封装了 fd 了,那么 FILE 里面也就一定为我们提供了一段缓冲区!所以缓冲区是在 FILE 结构体里,所以当我们任意打开一个文件,都会有一个 FILE 结构体,也就是任意一个文件都要在C标准库里通过 FILE 来为我们创建一个属于它自己的文件级别的用户级缓冲区!所以有几个文件被打开,就有几个 FILE,就有几个缓冲区!

例如,我们可以在 /usr/include/stdio.h 路径下的头文件中找到 FILE 结构体的 typedef

在这里插入图片描述

我们还可以在 /usr/include/libio.h 路径下的头文件中找到 FILE 结构体的定义:

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

基于上面的认识,我们可以简单实现一个自己的C库函数,当然我们只是为了更好地了解底层,只是认识级别的实现,源码链接:my_Clib.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1429104.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

查看域控组策略是否在客户端生效

要查看域控制器上的组策略是否已在客户端生效&#xff0c;可以按照以下步骤操作&#xff1a; 使用 RSOP (Resultant Set of Policy): 在客户端计算机上&#xff0c;以管理员身份打开命令提示符或者 PowerShell&#xff0c;并运行 gpresult /h GPReport.html 或 gpresult /v 命令…

10MHz 到 80MHz、10:1 LVDS 并串转换器(串化器)/串并转换器(解串器)MS1023/MS1224

产品简述 MS1023 串化器和 MS1224 解串器是一对 10bit 并串 / 串并转 换芯片&#xff0c;用于在 LVDS 差分底板上传输和接收 10MHz 至 80MHz 的并行字速率的串行数据。起始 / 停止位加载后&#xff0c;转换为负载编 码输出&#xff0c;串行数据速率介于 120Mbps…

Python实现利用仅有像素级标注的json文件生成框标注的json文件,并存放到新文件夹

import json import os # create rectangle labels based on polygon labels, and store in a new folder def create_rectangle_shapes(polygon_shapes):rectangle_shapes []for polygon_shape in polygon_shapes:# 获取多边形的坐标点points polygon_shape[points]# 找到最…

监测Tomcat项目宕机重启脚本(Linux)

1.准备好写好的脚本 #!/bin/sh # 获取tomcat的PID TOMCAT_PID$(ps -ef | grep tomcat | grep -v tomcatMonitor |grep -v grep | awk {print $2}) # tomcat的启动文件位置 START_TOMCAT/mnt/tomcat/bin/startup.sh # 需要监测的一个GET请求地址 MONITOR_URLhttp://localhost:…

消息总线在微服务中的应用

直连式配置中心 上一篇文章介绍了 Spring Cloud 中的分布式配置组件 Config&#xff0c;每个服务节点可以从Config Server 拉取外部配置信息。但是似乎还有一个悬而未决的问题&#xff0c;那就是当服务节点数量非常庞大的时候&#xff0c;我们不可能一台一台服务器挨个去手工触…

django+flask警务案件信息管理系统python-5dg53-vue

1&#xff09;用户在后台页面各种操作可及时得到反馈。 &#xff08;2&#xff09;该平台是提供给多个用户使用的平台&#xff0c;警员使用之前需要注册登录。登录验证后&#xff0c;警员才可进行各种操作[10]。 &#xff08;3&#xff09;管理员用户拥有信息新增&#xff0c;修…

计算机二级C语言的注意事项及相应真题-2-程序修改

目录 11.找出n的所有因子&#xff0c;统计因子的个数&#xff0c;并判断n 是否是”完数”12.计算s所指字符串中含有t所指字符串的数目13.将一个由八进制数字组成的字符串转换为与其面值相等的十进制整数14.根据整型形参m的值&#xff0c;计算如下公式的值15.从低位开始依次取长…

Springboot多种方法处理静态资源:设置并访问静态资源目录

&#xff5e;目录嗷&#xff5e; 静态文件application设置方法 配置详解编写配置优缺点 设置配置类方法 配置详解编写配置优缺点 总结 作者&#xff1a;Mintimate 博客&#xff1a;https://www.mintimate.cn Mintimate’s Blog&#xff0c;只为与你分享 静态文件 静态资源&…

Pycharm python用matplotlib 3D绘图显示空白解决办法

问题原因&#xff1a; matplotlib版本升级之后显示代码变了&#xff0c;修改为新的 # ax Axes3D(fig) # 原代码 ax fig.add_axes(Axes3D(fig)) # 新代码import numpy as np import matplotlib.pyplot as plt from matplotlib import cm from mpl_toolkits.mplot3d import Ax…

pytest的常用插件和Allure测试报告

pytest常用插件 pytest-html插件 安装&#xff1a; pip install pytest-html -U 用途&#xff1a; 生成html的测试报告 用法&#xff1a; ​在.ini配置文件里面添加 addopts --htmlreport.html --self-contained-html 效果&#xff1a; 执行结果中存在html测试报告路…

前端工程化之:webpack1-8(loader)

一、loader webpack 做的事情&#xff0c;仅仅是分析出各种模块的依赖关系&#xff0c;然后形成资源列表&#xff0c;最终打包生成到指定的文件中。 更多的功能需要借助 webpack loaders (加载器)和 webpack plugins (插件)完成。 webpack loader &#xff1a; loader 本质上是…

【Java开发岗面试】八股文—微服务、消息中间件

声明&#xff1a; 背景&#xff1a;本人为24届双非硕校招生&#xff0c;已经完整经历了一次秋招&#xff0c;拿到了三个offer。本专题旨在分享自己的一些Java开发岗面试经验&#xff08;主要是校招&#xff09;&#xff0c;包括我自己总结的八股文、算法、项目介绍、HR面和面试…

python 基础知识点(蓝桥杯python科目个人复习计划32)

今日复习内容&#xff1a;基础算法中的位运算 1.简介 位运算就是对二进制进行操作的运算方式&#xff0c;分为与运算&#xff0c;或运算&#xff0c;异或运算&#xff0c;取反&#xff0c;左移和右移。 &#xff08;1&#xff09;与运算 xyx&y000010100111 (2)或运算 …

OpenHarmony—Hap包签名工具

概述 为了保证OpenHarmony应用的完整性和来源可靠&#xff0c;在应用构建时需要对应用进行签名。经过签名的应用才能在真机设备上安装、运行、和调试。developtools_hapsigner仓 提供了签名工具的源码&#xff0c;包含密钥对生成、CSR文件生成、证书生成、Profile文件签名、Ha…

【安装指南】maven下载、安装与配置详细教程

&#x1f33c;一、概述 maven功能与python的pip类似。 Apache Maven是一个用于软件项目管理和构建的强大工具。它是基于项目对象模型的&#xff0c;用于描述项目的构建配置和依赖关系。以下是一些关键的 Maven 特性和概念&#xff1a; POM&#xff08;Project Object Model&…

MATLAB知识点:矩阵的拼接和重复

​讲解视频&#xff1a;可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇&#xff08;数学建模清风主讲&#xff0c;适合零基础同学观看&#xff09;_哔哩哔哩_bilibili 节选自第3章 3.3.4 矩阵的拼接和重复 有时候我们需要对多个矩…

云原生Kubernetes: Ubuntu 安装 K8S 1.23版本(单Master架构) 及故障恢复

目录 一、实验 1.环境 2.安装 Ubuntu 3.连接Ubuntu 4.master节点安装docker 5.node节点安装docker 6.master节点安装K8S 7.添加K8S工作节点 8.安装网络插件calico 9.故障 10.故障恢复 11.测试k8s网络和coredns 二、问题 1.Ubuntu如何修改镜像源 2.Ubuntu和Windo…

KAFKA节点故障的容错方案

KAFKA节点故障的容错方案 1. broker启动加载逻辑1.1 日志组成和分析1.2 snapshot文件1.3 broker启动流程1.4 LogManager的初始化和启动过程 2. controller高可用1.1 选主逻辑1.2 HA切换1.3 controller的职责 3. partition高可用3.1 ISR列表3.1 选举Leader 4. 疑问和思考4.1 如果…

11:按键

按键 1、按键的相关知识2、独立按键3、CPU如何处理按健4、编程测试&#xff08;用LED1作为指示&#xff09;5 、编程测试&#xff08;用8个LED作为指示&#xff09; 1、按键的相关知识 分为独立按键和矩阵按键 2、独立按键 由图得独立按键右边接地&#xff0c;左边独立连接到…

正则表达式可视化工具regex-vis

什么是正则表达式 &#xff1f; 正则表达式是对字符串操作的一种逻辑公式&#xff0c;就是用事先定义好的一些特定字符、及这些特定字符的组合&#xff0c;组成一个“规则字符串”&#xff0c;这个“规则字符串”用来表达对字符串的一种过滤逻辑。【百度百科】 正则表达式用简短…