🔥keyipatience:个人主页
🎬作者简介:C/C++后端开发学习者
⭐️patience is key in life
再次理解重定向
下面我们再来看一段代码
eg(1):

运行结果:

符合我们的预期,正确的往显示器上打印了
eg(2):./a.out 1->log.txt

即现在1和3都指向了log.txt,只有标准错误还指向stdout,所以只有它打印的内容在显示器上
eg(3)./a/out 1>log.normal 2>log.err

- 程序正常打印内容 → 存入
log.normal - 程序报错信息 → 存入
log.error - 终端屏幕不会打印任何内容
eg(4):追加重定向

或者:

&1代表引用 1 号文件描述符的流向 ;含义:标准错误 (2 号文件描述符),跟着标准输出走。同样到达追加重定向的效果
把标准输出和标准错误分开的原因?
把标准输出和标准错误做分离,从而占有不同的文件描述符,可以提供重定向的方式,把常规信息和错误信息进行分离,方便日志的管理
看一看源码
先把上一节的一张图拿过来,一起理解

task_struct中指向文件描述符表(struct files _struct)的指针

文件描述符表中的struct file*fd_arraty即指针数组,每个指针指向文件的struct file

struct file

文件的一些属性(文件名,文件大小.....)并没有放到struct file而是另一个结构inode里
现在简单理解一点就是我们能通过struct file找到文件的缓冲区和文件的属性(inode)

理解一切皆文件(结合进程,vfs,多态)

- 进程层(调用者) 进程执行
read(fd) / write(fd),进程只知道 fd 编号,完全不知道底层是什么硬件。 - VFS 层(多态中转,基类) 进程 PCB里存有打开文件列表,每一个 fd 对应内核里一个
struct file结构体。struct file内:struct file_operations *f_op(统一基类接口)。 VFS不关心是什么硬件,屏蔽硬件差异,只 根据 fd 找到对应struct file,执行f_op->read/write。 - 驱动层(子类实现) 不同硬件驱动各自重写 read/write,依靠C 语言函数指针实现多态。同一个 read 系统调用:fd 指向键盘 → 执行键盘驱动 readfd 指向磁盘 → 执行磁盘驱动 readfd 指向屏幕 → 执行屏幕驱动 write
进程通过 fd 使用资源,VFS 作为抽象基类利用多态屏蔽硬件差异,各类硬件驱动作为子类实现各自读写,使得进程访问磁盘、显示器、键盘、网卡全部等同于访问普通文件
每个设备都可以有自己的read、write,但⼀定是对应着不同的操作⽅法!!但通过 struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取Linux系统中绝大部分的资源!!这便是"linux下⼀切皆⽂件"的核⼼理解。
缓冲区(内存的一块空间)
例子简单理解
在 Linux 系统中,快递员代表外设硬件 ,负责产生和发送数据;用户代表运行的应用进程 ,负责读取并处理数据;驿站代表内存缓冲区 ,它能临时暂存数据,解除硬件与进程间的同步等待,适配二者的运行速度差异,实现数据异步高效传输。
回顾前面问题
下面我们再来看上一节的一段代码,稍加改动最后再关上文件(close(1))

运行结果:

现在问题是为什么printf的内容在最后close(1)并没有写道log.txt里,而write的系统调用相关函数还能写道log.txt里?

首先我们可以先从相反方面论证一下,printf打印的内容不是直接写到文件的内核缓冲区里,假如是的话,那么在关闭了该文件时,操作系统就会把文件内核缓冲区的内容刷新到外设,我们应该就能看到。
语言层缓冲区:
其实在c语言标准库会为每一个打开的文件创建一个用户级的语言层缓冲区,所以printf打印的内容先是到了这个地方,在语言层对缓冲区的刷新只要满足(1)强制刷新(2)刷新条件满足(3)进程退出,之一都可以刷新到文件的内核缓冲区里,从而交给操作系统,此后就是操作系统的事情了,用户级的文件操作就完成了。
语言层->内核的方式
而c标准库是怎么把语言层的缓冲区的数据拷贝到文件的内核缓冲呢?答案**:fd+系统调用(eg:write)**printf和puts等等底层封装的就是write呀。并且在上面我们讲一切皆文件时也说过了write只关心fd,有了fd,write就能实现。
解释代码原因
现在让我们再来解释一下上面代码的问题,在执行close(1)时,还没有return 0;进程还没有退出,又没有强转刷新,刷新的条件条件也不满足,所以刷新的3个条件一个都不满足,所以不会刷新到文件的内核缓冲区,数据还在语言层的缓冲区里。当执行return 1时进程结束,满足刷新条件,可是通过fd+系统调用的时候,已经找不到fd了呀,文件都已经被你关了。所以就不能把数据从语言层交付到操作系统里。自然文件的内核缓冲区没有内容,刷新到外设也自然不会显示。而write是系统调用函数,直接把数据就写到内核的文件缓冲区里了
怎么能做到刷新呢?
非常简单只要满足刷新的3个条件不就行了吗,所以直接fflush强制刷新一下就可以了


再次理解FILE*
c标准库里的缓冲区通过什么方式怎么能找到呢?
int fflush(FILE*stream)
nt fputs(const char *str, FILE *stream);
int fprintf(FILE *stream, const char *format, ...);
等等这些接口都直接和间接的和stdout->FILE*相关,所以缓冲区也应该和FILE相关
其实FILE这个结构体除了封装的有fd,还有指向缓冲区的相关指针(FILE也是由typedef struct_IO FILE FILE 而来的)

每一次调用fopen时都会在fopen内部new/malloc申请一个FILE对象,并且初始化后返回,stdout和stdin,stderr都是FILE*,所以都会有自己的缓冲区和fd,调用printf/puts等等接口就会默认写在stdout缓冲区里,需要时再使用write系统调用和fd的方式刷新到操作系统内部的文件缓冲区里
刷新条件满足是什么意思,怎么算条件满足?
(1)立即刷新----无缓冲-----写透模式(WT)
(2)缓冲区满了----全缓冲(一般是普通文件)
(3)行刷新----行缓冲(多显示器用,方便我们的阅读习惯)
为什么在语言层要实现c标准库的缓冲区,不直接把数据刷新到文件的内核缓冲区?
先说结论:系统调用是有成本的,频繁的系统调用会降低程序的运行效率
在语言层实现缓冲区,可以先把数据刷新到缓冲区里,在缓冲区满了过后,只要做一次系统调用一次刷新就可以把数据全交给操作系统,相反如果没有语言层的缓冲区,就需要频繁调用系统调用,降低程序的运行效率
所以也可以看出写满缓冲区再刷新,效率是最高的
说完了语言层的缓冲区,我们再来谈谈文件内核缓冲区的刷新条件,在这里我们就简单理解操作系统内部的缓冲区的刷新一定会比语言层更加复杂,但我们不关心由操作系统自主决定刷新方案
只要知道把数据交给操作系统就相当于交给了硬件(eg:给某个人寄快递,只需给快递员即可)
数据交给操作系统,操作系统交给硬件------本质都是数据的拷贝
计算机数据流动的本质:一切皆拷贝
理解终端和重定向后的缓冲模式

问题:为什么打印到显示器上的顺序和写到文件时的顺序不一样?为什么在fork()后打印到显示器上的只有一次而重定向到log.txt里却有两次?
首先我们得搞懂刷新的方式
输出到终端 vs 重定向到文件,缓冲模式不同
- 终端(直接运行程序):
stdout是行缓冲 ,遇到\n就会自动刷新缓冲区,把数据写入内核态。 - 文件(重定向
>):stdout变成全缓冲 ,\n不会触发刷新,数据会一直留在用户态缓冲区里,直到程序结束才一次性写入。
(1)输出到终端时 ,printf/fprintf/fwrite 带的 \n 触发了行缓冲刷新 ,数据已经被写入内核态,缓冲区是空的。调用 fork() 时,父进程的缓冲区是空的,子进程复制到的缓冲区也是空的。父子进程退出时,缓冲区里没有残留数据,所以只会各输出一次。
(2)重定向后,stdout 变成了全缓冲模式 你调用 printf("hello printf\n"); 时,\n 不再触发刷新,数据被留在了父进程的用户态缓冲区里,还没写入内核态缓冲区
调用 fork() 时,子进程复制了父进程的缓冲区 父进程的缓冲区里,已经存了 printf/fprintf/fwrite 的内容,子进程诞生时,会完整复制这份缓冲区。
此时:父进程缓冲区:hello printf\nhello fprintf\nhello fwrite\n子进程缓冲区:和父进程完全一样,也有这三行数据。父子进程退出时,各自刷新自己的缓冲区 程序结束时,父进程会把自己缓冲区里的内容写入文件,子进程也会把自己缓冲区里的内容写入文件。 所以文件里就出现了两次 printf/fprintf/fwrite 的内容。
模拟一下封装简单的glibc的文件
mtstdio.h:

mystdio.c:
#include"mysdio.h"
2 #include<sys/types.h>
3 #include<sys/stat.h>
4 #include<fcntl.h>
5 #include<string.h>
6 #include<stdlib.h>
7 #include<unistd.h>
8 MyFile *BuyFile(int fd,int flag)
9 {
10 MyFile*f=(MyFile*)malloc(sizeof(MyFile));
11 if(f==NULL)return NULL;
12 f->bufflen=0;
13 f->fileno=fd;
14 f->flag=flag;
15 f->flush_method=LINE_FLUSH;//在这里默认实现行刷新
16 memset(f->outbuffer,0,sizeof(f->outbuffer));
17 return f;
18
19 }
20 MyFile *MyFopen(const char*path,const char*mode)
21 {
22 int fd=-1;
23 int flag=0;
24 if(strcmp(mode,"w")==0)
25 {
26 flag=O_CREAT | O_WRONLY | O_TRUNC;
27 fd=open(path,flag,0666);
28 }
29 else if(strcmp(mode,"a")==0)
30 {
31 flag=O_CREAT|O_WRONLY|O_APPEND;
32 fd=open(path,flag,0666);
33 }
34 else if(strcmp(mode,"r")==0)
35 {
36 flag=O_RDWR;
37 fd=open(path,flag);
38
39 }
else
41 {
42 //
43 }
44 if(fd<0)return NULL;
45 return BuyFile(fd,flag);
46
47 }
48 void MyFclose(MyFile *file)
49 {
50 if(file->fileno<0)return ;
51 MyFFlush(file);//关闭文件前要刷新
52 close(file->fileno);
53 free(file);
54
55 }
56 int MyFwrite(MyFile*file,void *str,int len)
57 {
58 //1.拷贝
59 memcpy(file->outbuffer+file->bufflen,str,len);
60 file->bufflen+=len;
61 //2.判断是否满足刷新条件
62 if((file->flush_method&LINE_FLUSH) && file->outbuffer[file->bufflen-1]=='\n')
63 {
64 MyFFlush(file);
65
66 }
67 return 0;
68
69
70 }
71 void MyFFlush(MyFile*file)
72 {
73 //刷新就是把数据拷贝到内核的文件缓冲区里
74 int n=write(file->fileno,file->outbuffer,file->bufflen);
75 (void)n;
76 fsync(file->fileno);//强制从内核文件缓冲区刷新到外设
77 file->bufflen=0;
78 }

