文件磁盘文件——没有被打开
内存文件——被进程中在内存中打开,文件只有被加载到内存中才可以被访问。
进程控制模块PCB内部有一个指针:styruct files_struct* files。这个指针指向一张表,file_struct
他是一个结构体,这个指针指向这个结构体变量。
重点需要关注的是fd_array。里面的那个变量是一个宏定义,根据系统地差别,大小也有差别,是一个指针数组,代表可以打开文件数目地多少。结构体最后一个变量file也是一个结构体,包含了文件地全部信息,大小路径,版本号等。
他们的关系如下。所以所谓地文件描述符地本质是数组下标。每个文件都需要,一个打开的文件方法,内存区域供其读写。这些都在操作系统内核中进行。
fopen->open->fd,然后封装成FILE,用户得到FILE*。
为了进行文件的管理组织,就必定需要文件描述符。
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器 所以输入输出还可以采用如下方式:
#include
2 #include
3 #include
4 #include
5 #include
6 #include
7 int main()
8 {
9 close(0);
10 int fd =open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
11 if(fd<0)
12 {
13 perror("open fail\n");
14 exit(1);
15 }
16 printf("%d\n",fd);
17 close(fd);
18 return 0;
19 }
这段代码默认的是打开一个log.txt的文件,然后输出一个fd的值,但是结果默认是3,0 .1 . 2.被占用,那么如果我们默认关闭文件描述符0呢?我们会 发现文件门描述符输出的都是0,关闭文件描述符2,回答先打印出的文件描述符是2。那么我么可以得到一个结论。
fd的分配规则:最小的,没有被占用的文件描述符分配给新打开的文件描述符。
如果我们close(1)我们执行程序的时候会发现不会有任何东西打印出来。因为012被占用,所以新打开的文件,文件描述符一定是1.
printf()默认往stdout里面打印,printf的文件描述符是1,但是1被关闭,这里1指向了log.txt。如果我们在代码结尾不关闭close(fd),我们打开文件log.txt会发现文件log.txt里面出现了内容。
或者可以加入fllush代码如下
#include
2 #include
3 #include
4 #include
5 #include
6 #include
7 int main()
8 {
9 close(1);
10 int fd =open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
11 if(fd<0)
12 {
13 perror("open fail\n");
14 return 1;
15 }
16 printf("文件按描述符%d\n",fd);
17 fprintf(stdout,"hello world\n");
18 fflush(stdout);
19 close(fd);
20 return 0;
21 }
我们可以看到log.txt出现了内容,本来应该打印到屏幕上的内容打印到了文件里面,产生了输出重定向的现象。那么他的原理是什么呢?
linux上一切皆文件,linux上文件进程默认会打开三个文件,标准输入,标准输出,标准错误,对应的键盘,显示器,显示器。所以每个进程打开默认填充这三个标准文件。而关闭1导致,标准输出的文件描述符,之间指向了新开的log.txt这就是文件重定向的原理。
#include
2 #include
3 #include
4 #include
5 #include
6 #include
7 int main()
8 {
9 close(0);
10 int fd =open("log.txt",O_RDONLY);
11 if(fd<0)
12 {
13 perror("open fail");
14 }
15 printf("fd:%d\n",fd);
16
17 char buffer[64];
18 fgets(buffer,sizeof buffer,stdin);
19 printf("%s\n",buffer);
20
35 }
我们关闭文件描述符0,然后标准输出。然后输出结果我们看一下是什么?
我们看到运行程序的时候没有等待而是直接读取了log.txt。这是因为在关闭标准输入之后新打开的Log.txt默认占据了标准输入。文件由log.txt提供。
接下来我们看一下
#include
2 #include
3 #include
4 #include
5 #include
6 #include
7 int main()
8 {
9 close(0);
10 int fd =open("log.txt",O_WRONLY|O_APPEND|O_CREAT);//输出重定向,读取方式打开,然后情况,没有的话创建文件:appe追加重定向
11 if(fd<0)
12 {
13 perror("open fail");
14 }
15 fprintf(stdout,"you can see success");
16
}
这个就是实现了追加重定向,但是这样子有点不方便。每次都要关闭特定的文件描述符。有系统提供给我们的接口来让我们使用dup2
#include
int dup2(int oldfd, int newfd);`
创建一个新的fd,把进程的文件描述符中fd指针进行拷贝,将oldfd拷贝给newfd。让old和新的一样。那么新的newfd就没有意义,那么就可以关闭newfd。
将3拷贝到1,关闭新打开的3,留下1,这是因为1和3一致,完成了输出重定向。使用也是dup2(3,1)。
#include
2 #include
3 #include
4 #include
5 #include
6 #include
7 int main(int argc,char*argv[])
8 {
9 if(argc!=2)
10 return 2;
11 int fd=open("log.txt",O_WRONLY|O_TRUNC|O_CREAT);
12 if(fd<0)
13 {
14 perror("open fail");
15 return 1;
16 }
17 fprintf(stdout,"%s\n",argv[1]);
18 }
我们先写一份没有重定向的代码运行结果
之后物品们对他进行重定向
我们发现输入重定向已经完成了到og.txt。如上,就是重定向的原理。
接下来讨论一下close()这时候添加了close之后我们发现可以输出到log.txt上,之前则不可以,这个是dep2的特性。这个和dup2有关。
这时候就要和一切皆文件有关了,要先谈谈的Linux的设计哲学,体现在操作系统的软件设计层方面。linux是C语言写的,如何用C语言实现面向对象甚至运行时多态?类内函数指针,指向不同的函数。计算机硬件底层一定是对应不同的操作方法,但是这些设备都是外设,所以每一个设备的核心函数都可以说read和write 根据冯诺伊曼结构IO,所有的设备都可以有自己的read和write,但是实现方法一定是个不一样的。既然每个都不一样,对于用户来说过于麻烦,操作系统就设计了一切皆文件。当打开一个磁盘的文件,内核中创建一个struc fail结构体,显示器也一样,然后让函数指针指向不同的实现函数方法。免去了用户操作,但是打开的文件太多了,那么就先描述再组织。那么再软件层就是一切皆文件。所以在linux操作系统之上就有了一切皆文件的概念。这种概念叫做VFS 虚拟文件系统。
什么是缓冲区——就是一段内存空间。
为什么要有缓冲区?
提高整机效率,快速成本低,提高用户响应速度 。
那么缓存区在哪里?
首先要提到缓冲区的刷新策略,立即刷新,和行刷新 \n 满一行就刷新,满刷新(只有内容满了才会刷新)。例外:用户强制刷新(fflush)或者进程退出。
所有的设备都倾向于全缓冲,全缓冲,大大减少了IO次数,极大的减少了时间浪费。
#include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 int main()
9 {
10 //C语言提供的
11 printf("hello print\n");
12 fprintf(stdout,"fprint\n");
13 const char* str="hello fputs\n";
14 fputs(str,stdout);
15
16
17 const char*buff="hello write\n";
18 write(1,buff,strlen(buff));
W> 19 pid_t id=fork();
20
21 }
运行结果
发现有4条记录,但是清空log.txt重定向一下
发现多了几条,只有操作系统的接口,执行了一次,C语言提供的使用了2次。那么为什么fork会导致这种现象呢?缓冲区一定是C标准库提供的,不然两者结果应该一样。
函数已经被执行完了,不一定代表数据一定会被刷新。fork写时拷贝可能数据被拷贝2份。
fput并不是直接写入到操作系统,而是先写入到缓冲区之中。然后调用write接口写入操作系统内核,那些写入缓冲区有什么好处呢?这样子就只需要写入缓冲区其他的操作都不用管。因为是往显示器打的是行刷新策略。那么最后执行fork的时候一定是函数执行完了,数据已经被刷新了。按理说fork无意义。如果进行了重定向——要向磁盘文件打印——隐形的刷新策略变成了全缓冲,\n没有意义了。fork()执行的时候一定函数执行完了,数据没有刷新,在当前C标准库得到缓冲区中,这部分数据属于父进程数据吗?是的,是属于父进程的数据,fork之后父子各自退出。那么fork之后两个执行流,各自退出。此刻还有特殊的刷新策略,退出时书信。那么刷新到磁盘的时候本质上也是一种写入。可以得到结论,在退出的时候父子进程发生写时拷贝。
我们在fork之前加入fflush,会发现这样的现象又没了,这是因为在fork之前已经刷新了。缓冲区没有数据了,那么为什么里面填了stdout呢?缓冲区在哪里呢?
C语言中打开文件fopen返回值是FILE* 。FILE是结构体,内部封装了fd,以及包含了大量的语言层缓冲区结构。
C语言开的FILE ,文件流。cout cin 类必定有fd文件描述符,缓冲区buffer,operator 运算符重载。拷贝到buffer定期刷新。内核也有内核的缓冲区。而数据一旦拷贝数据就已经属于内核。调用write也不是学到了外设里面,不是直接写到了外设。
示例代码
#include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 #include
9 #define NUM 1024
10
11 //类似于C语言的FILE
12 struct MYfile_
13 {
14 int fd;
15 char buffer[1024];
16 int end;//缓冲区结尾
17 };
18 typedef struct MYfile_ MYfile;
19
20 MYfile*open_(const char*path,const char* mode)
21 {
22 assert(path);
23 assert(mode);
24 MYfile *fq=NULL;
25 if(strcmp(mode,"r")==0)
26 {
27
28 }
29 else if(strcmp(mode,"r+")==0)
30 {
31
32 }
33 else if(strcmp(mode,"w")==0)
34 {
35 int fd=open(path,O_WRONLY|O_TRUNC|O_CREAT,0666);
36 if(fd>=0)
{
38 fq=(MYfile*)malloc(sizeof(MYfile));
W> 39 memset(fq,0,sizeof(fq));
40 fq->fd=fd;
41 }
42 else
43 {
44
45 }
46 // fq=(MsqYfile*)malloc(sizeof(MYfile));
47 }
48 else if(strcmp(mode,"w+"))
49 {
50
51 }
52 else if(strcmp(mode,"a")==0)
53 {
54
55 }
56 else if(strcmp(mode,"a+")==0)
57 {
58
59 }
60 else
61 {
62
63 }
64 return fq;
65 }
66 void fputs_(const char*message,MYfile*fp)
67 {
68 assert(memset);
69 strcpy(fp->buffer+fp->end,message);
70 fp->end+=strlen(message);
71 printf("%s\n",fp->buffer);
72 //暂时没有刷新
73 //那么刷新策略谁执行的?用户通过C语言中代码逻辑完成刷新动作。
74 //那么效率提高体现在哪里?如果输出结果中没有\n的时候就暂时不刷新,等到有\n时再刷新。
75 //暂时不IO
76 if(fp->fd==0)
77 {
78 //标准输入
79 }
80 else if(fp->fd==1)
81 {
82 if(fp->buffer[fp->end-1]=='\n')//\n\o
83 {
84 write(fp->fd,fp->buffer,fp->end);
85 fp->end=0;
86 }
87 //标准输出
88 }
89 else{
90 //其他文件
91 }
92 }
93
94 void fflush_(MYfile*fp)
95 {
96 assert(fp);
97 // printf("%s",fp->buffer);
98 if(fp->end!=0)
99 {//暂且认为刷新了————其实是写到了内核。
100 write(fp->fd,fp->buffer,fp->end);
101 //刷新到屏幕上需要syncfs
102 syncfs(fp->fd);//写入到磁盘。
103 fp->end=0;
104 }
105 }
106 void fclose_(MYfile*fp)
107 {
108 assert(fp);
109 fflush_(fp);
110 close(fp->fd);
111 free(fp);
112 }
113 int main()
114 {
115 MYfile*fq = open_("./log.txt","w");
116 if(fq==NULL)
117 {
118 perror("openfail\n");
119 return 1;
120 }
121 fputs_("11111hello world",fq);
122 fork();
fclose_(fq);
124 return 0;
125 }
126
运行结果如下
完美实现了之前的效果。