上一回进程与文件系统我们主要看了很多文件描述符的知识
1.如何理解一切皆文件?
每个设备被打开时,OS给每个文件创建一个自己的struct file 里面填充自己的属性以及自己的缓冲区,其中还有函数指针,里面保存函数地址,通过这个地址可以找到对应外设的read/write方法
所以结合上节课的内容我们再来梳理一下进程是如何打开文件的
首先进程要通过自己PCB中内置的struct files_struct*files指针找到struct files_struct 然后在这个结构体内部找到数组struct file* fd_array[ ]这个进而找到OS为这个进程分配的fd(数组下标),通过下标fd找到数组中对应的内容(file*),通过file*指针找到内存中保存struct file的数据结构,访问这个数据结构,找到那个进程要找的struct file!!!!
如果这个struct file是键盘文件,那么结构体中会有函数指针,指向键盘的读写方法
这就是使用C语言来设计面向对象
我们使用OS的本质:通过进程的方式进行OS的访问
来看操作系统层面:首先明确必须访问fd才能找到文件,任何语言访问外设或文件都必须经历OS
之前我们说fopen的返回值是FILE* 这里的FILE其实是一个结构体,那他和我们上面说的struct file有关系吗? 没有,只是上下层的关系
这个结构体是C语言提供的
所以平常我们在安装VS2019的时候是在安装什么?
安装语言对应的头文件和库
这个FILE结构体一定封装了fd!!!!!但是怎么验证?
这个_fileno就是每个文件的fd
2.重定向
重定向有三种:
- 输出重定向 >
刚才程序运行的结果是0 1 2 ,我们输出重定向到log里面
- 输入重定向 <
将指定文件作为命令的输入设备
此时log是输入设备
相当于直接cat log.txt
- 追加重定向 >>
输出重定向会清空数据,要是不想被清空就使用追加重定向
但是这样的理解太过肤浅,我们深入理解一下重定向的本质
上回我们说过,fd的分配是从下标0开始,遇到第一个没被占用的
看一下这样的代码
1.输出重定向的理解
本该打印到显示器上的东西全都到log.txt中
因为printf只认1,根本不考虑哪个文件fd=1
所以重定向的原理:在上层无法感知的情况下,在OS内部更改进程fd
上面的操作相当于 1>log.txt
也就是log.txt占用了fd=1
2.输入重定向的理解
和刚才其实是一样的scanf函数只认0,他是从0获取数据写到 a b中
所以你关闭了0文件,log.txt分配的fd就是0
自然会从log.txt中拿数据
3.追加重定向的理解
此时open的选项把O_TRUNC改成O_APPEND
现在我想让你把写到stdout的内容打印到log.txt,把写入stderr的内容打印到err.txt怎么搞?
OK,理解一下这条指令
首先运行a.out 把log.txt的fd分配成1,把err.txt的fd分配成2,此时printf只认1,所以hello stdout写入到log.txt fprintf只认2,所以把格式化之后的字符串写入到err.txt中
那如果现在我想把两个字符串都打印到显示器上
首先运行a.out 然后把运行结果(此时应该只有hello stdout)放到log.txt,hello stderr在2文件里,然后把2也写入到显示器文件,这样就可以一起输出啦
其中1>&2是把1重定向成2的地址,也就是用2的地址的fd=1
如果你想把两个字符串汇总到一起写入log.txt
这是把2重定向成1的地址然后./a.out的时候一起把1 的内容重定向到log.txt
但是如果每次重定向都要关闭一个再打开一个文件也太挫了
有没有更好的方法?当然
只要看dup2就行,dup想看懂太简单了
那么请你回答 1 > 一个文件(他的文件描述符为fd)
此时应该如何传参,是fup(1,fd)还是反过来?
应该是dup2(fd,1),这地方确实是有点绕,但是我的理解是:之前我们不是说过要把文件的fd分配成1,所以fd应该是1的一份拷贝
3.缓冲区
之前我们总是说缓冲区缓冲区,缓冲区到底在哪?
又看到这个现象完全懵了
现在我们好好解释一下
那么C库的刷新策略有哪些?
1.无缓冲 (直接写给OS,而不是先放C库的缓冲区)
2.行缓冲(碰到'\n'就刷)
3.全缓冲(缓冲区写满再刷新)
值得注意的是:
显示器采用行缓冲,普通文件采用全缓冲
为什么要有缓冲区?节省调用者的时间,因为系统调用也是花费时间的
缓冲区在哪?在fopen打开文件的时候,会得到FILE结构体,缓冲区就在其中
所以我们来解释上面的问题
首先C库函数会把第一个字符串放在自己的缓冲区,因为是写入到显示器文件采取行刷新,正好字符串末尾有\n直接刷新,即写给操作系统的stdout文件的缓冲区
write是系统调用,直接可以把hello write写到stdout的缓冲区
所以直接在fork之前就完成刷新了,根本没有创建子进程的事情,也就是有没有fork结果都是一样的
在进程退出之前,OS根据自己的刷新策略把stdout缓冲区的内容调用显示器的写方法,把内容显示到屏幕上
(如果没有fork)
再来看一下有fork重定向之后的结果是为什么?
重定向之后 write还是直接写到系统的缓冲区,但是重定向到普通文件,所以C库的刷新策略变成了全缓冲,所以在执行完write语句之后应该是这样的
这部分的数据属于父进程的,是父进程创建的
然后fork起作用了,父子进程在退出的时候都要刷新,谁先刷新谁先进行写时拷贝,所以hello fprintf字符串被写入了两次(写入stdout的缓冲区)
其实到这里我们的理解还不是特别深刻,自己写一个缓冲区就好了
需要用到一个函数,fsync函数,将文件数据同步到硬盘
main.c
1 #include "mystdio.h"
2 #include <string.h>
3
4 #define LOG "log.txt"
5 int main()
6 {
7 MY_FILE*fp=my_fopen(LOG,"w");
8 const char * msg="hello myfwrite";
9 int cnt=5;
10 while(cnt)
11 {
12 char buf[1024];
13 snprintf(buf,sizeof(buf),"%s,%d",msg,cnt--);
14 size_t num=myfwrite(buf,strlen(buf),1,fp);
15 printf("当前成功写入: %lu个字节\n", num);
16 my_fflush(fp);
17 }
18 return 0;
19 }
mystdio.h
1 #pragma once
2 #include <stdio.h>
3 #define NUM 1024
4 typedef struct _MY_FILE
5 {
6 int fd;
7 int current;
8 int flag;
9 char outputbuffer[NUM];
10 }MY_FILE;
11
12 MY_FILE* my_fopen(const char *pathname,const char *mode );
13 size_t myfwrite(const void *ptr,size_t size,size_t nmemb,MY_FILE *stream);
14 int my_fflush(MY_FILE*fp);
15 int my_close(MY_FILE*fp);
mystdio.c