目录
1. 回顾c语言文件
学习之前先来简单回顾一下c语言的IO知识,首先fopen打开文件,第一个参数是文件名,如果没有这个文件会对应的在源文件目录下创建一个。fopen第二个参数是权限,即以什么方式来对这个文件进行操作。然后调用对应的函数。操作完之后在使用fclose清理。
1 #include<stdio.h>
2 #include<string.h>
3 int main()
4 {
5 FILE* fp=fopen("log.txt","w");
6
7 if(fp==NULL)
8 {
9 perror("fopen err\n");
10 return 1;
11 }
12 const char* msg="hello world!!";
13 int n=5;
14 while(n--){
15 fwrite(msg,strlen(msg),1,fp);
16 }
17 fclose(fp);
18 return 0;
19 }
把对应的字符串写入log.txt,写入5遍
那么今天在LINUX中我们需要从进程,系统的角度,深刻的理解IO过程。
2. Linux中的文件
假如一个创建一个空文件,那么他在硬盘上是否占空间呢?
肯定是占的。因为一个文件由内容+属性组成的。属性也是数据
我们之前的fwrite就是修改文件内容。
一切语言,默认打开3个文件。
stdin:标准输入,键盘
stdout:标准输出,显示器
stderr:标准错误,显示器
为什么要默认打开三个文件呢?
当计算机发明出来的时候,人们就要与计算机交互,语言是中间载体。是语言帮我们通过系统打开这三个文件。
简单验证一下,我们将fp文件换成stdout显示器文件。他就将这些字符串打印到了显示器中
fwrite(msg,strlen(msg),1,stdout);
所以普通文件和显示器文件在语言层面是没有什么区别的。
3. OPEN系统调用
-
第一个参数是文件名字,可以带路径,不带路径默认在本源文件。
-
第二个参数类型是int,传入的是宏常量,其实各个参数都是一个比特位为1,经过按位或之后通过辨认这个整数中比特位为1的位置,来确定以什么方式打开。
-
第三个参数就是创建文件之后的权限,为八进制,受到掩码的限制。
这就是open这个系统调用的大概描述,fopen是封装起来的库函数,为了更好地进行二次开发。
4. 文件描述符fd
1 #include<stdio.h>
2 #include<sys/stat.h>
3 #include<sys/types.h>
4 #include<fcntl.h>
5 #include<unistd.h>
6 #define SIZE 128
7 int main()
8 {
9 int fd=open("log.txt",O_RDONLY);
10 if(fd<0)
11 {
12 perror("open err!\n");
13 return 0;
14 }
15 char buf[SIZE];
16 read(fd,buf,SIZE);
17 printf("%d\n %s",fd,buf); //这是前面测试read,这里我在外面吧log.txt清空了,这里验证fd
18 int fd1=open("log1.txt",O_RDONLY|O_CREAT);
19 int fd2=open("log2.txt",O_RDONLY|O_CREAT);
20 int fd3=open("log3.txt",O_RDONLY|O_CREAT);
21 int fd4=open("log4.txt",O_RDONLY);
22 int fd5=open("log5.txt",O_RDONLY);
23 printf("%d\n",fd1);
24 printf("%d\n",fd2);
25 printf("%d\n",fd3);
26 printf("%d\n",fd4);
27 printf("%d\n",fd5);
28 close(fd);
29 }
发现fd依次从3开始增长,其实0,1,2就是默认打开的3个文件,stdin,stdout,stderr。后面没有创建成功,所以输出fd为-1.
这也正体现了fopen里面也对O_CREATE进行了封装。
这个fd是一个整数,我们在程序中打开了多个文件,运行起来也就是进程打开了多个文件,那么就要对它进行描述和组织。
5. 文件加载过程
当我们从硬盘打开一个或多个文件,先要把它加载到内存中,用结构体描述,在用链表连接起来。那么怎么与进程关联起来呢,实际上就是tasksruct里存储这指向file_struct的结构体,这个file_struct内有一个结构体指针数组,结构体指针数组内又存储着指向文件的指针,数组的下标对应的指针指向了那个文件。
至此进程就与文件关联起来,而且open成功后就会返回fd,fd就代表了数组下标,也就意味着我们可以通过它找到对应的文件,进行下一步操作。
对于不同文件,比如硬盘上的文件,网卡,显示器,他的读写方法一定是不一样的,而在描述文件的时候,结构体内也加入了函数指针,指向该文件对应的函数。
虽然在底层读写方法的原理是不一样的,但是在我们使用者看来,你们都是文件,虽然最低层实现的方法不一样,但是用函数指针调用却毫无差别,用同一种方法访问的确是不同的代码,就像是面向对象中多态的意思。其实面向对象也就是通过这么一个个工程不断总结出来的。
5.1 总结
在来梳理一下整个过程,open打开文件,加载到内存中,操作系统管理起来。进程调用的open那么这个文件怎么和进程关联起来呢,进程task_struct存储着指向file_struct的指针,file_struct里存储着结构体指针数组,这个数组里面存储着指针,指针指向对应的文件结构体,怎么区分这些指针呢,用数组的下标。也就是说文件结构体(加载到内存的文件)通过数组下标就和进程关联起来。
open成功后,返回数组下标fd,这时你对文件进行操作,怎么找到呢,直接传fd,在调用操作对应的方法。
6. 文件描述符的分配,重定向原理
1 #include<stdio.h>
2 #include<sys/stat.h>
3 #include<sys/types.h>
4 #include<fcntl.h>
5 #include<unistd.h>
6 #define SIZE 128
7 int main()
8 {
9 close(0);
10 int fd=open("log.txt",O_RDONLY|O_CREAT);
11 if(fd<0)
12 {
13 perror("open err!\n");
14 return 0;
15 }
16 printf("%d\n",fd);
17 close(fd);
18
19 }
默认的文件描述符从最小的没有被分配的给分配,默认0,1,2被占用。
假如关掉1,然后重新open一个文件。
1 #include<stdio.h>
2 #include<sys/stat.h>
3 #include<sys/types.h>
4 #include<fcntl.h>
5 #include<unistd.h>
6 #define SIZE 128
7 int main()
8 {
9 close(1);
10 int fd=open("log.txt",O_WRONLY|O_CREAT,0644);
11 if(fd<0)
12 {
13 perror("open err!\n");
14 return 0;
15 }
16 printf("hello world ---%d\n",fd);
17 fflush(stdout);//stdout此时是log.txt文件,是全缓冲,不刷新的话就打印不到文件中,后面详细讲
18 close(fd);
19
20 }
发现原本要打印到显示器上的内容被写入到了log.txt中
其实原理很简单就是下图这样:
那么echo xxx >file
的本质就是先创建子进程,子进程执行,将字符串重定向到file文件里,本质上就是先关闭1,然后open打开文件,1号下标的指针指向file,调用write,字符串写入到file中,子进程执行完,被回收。
那么追加>>就是open打开文件时,参数设置成O_APPEND。
7. 系统调用的fd与库函数
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7
8 int main()
9 {
10 //close(1);
11 //int fd=open("log.txt",O_CREAT|O_APPEND|O_WRONLY);
12
13 const char* str="hello write\n";
14 const char* str1="hello world printf\n";
15 const char* str2="hello world fprintf\n";
16 write(1,str,strlen(str));
17 printf(str1);
18 fprintf(stdout,str2);
fflush(stdout);//假如close掉1,log.txt是文件,文件是全缓冲,不刷新就看不到了。
19 return 0;
20 }
运行打印在了显示器中
当close掉1,fd就会分配到1,原本打印到显示器的内容就被写到了log.txt中
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7
8 int main()
9 {
10 close(1);
11 int fd=open("log.txt",O_CREAT|O_APPEND|O_WRONLY);
12
13 const char* str="hello write\n";
14 const char* str1="hello world printf\n";
15 const char* str2="hello world fprintf\n";
16 write(1,str,strlen(str));
17 printf(str1);
18 fprintf(stdout,str2);
fflush(stdout);//stdout是文件,全缓冲需要刷新
19 return 0;
20 }
查看文件也没问题
与系统调用write不同的是,这两个库函数都会用到stdout这个指针,指向显示器文件。这个指针是file*的结构体,也就是说实际上在这个结构体内一定封装一个fd文件描述符。1被关闭,文件描述符指向了那个log.txt文件。
8. 缓冲区
当我们不close1的时候会打印三个字符串到显示器中。
当我们close1的时候强制刷新打印三个字符串到文件当中。
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7
8 int main()
9 {
10
11 int fd=open("log.txt",O_CREAT|O_APPEND|O_WRONLY,0644);
12
13 const char* str="hello write\n";
14 const char* str1="hello world printf\n";
15 const char* str2="hello world fprintf\n";
16 write(1,str,strlen(str));
17 printf(str1);
18 fprintf(stdout,str2);
19 fork();
20 fflush(stdout);
21 close(fd);
22 return 0;
23 }
那么当我们fork的时候,会创建子进程。
当我们不close1的时候会打印三个字符串到显示器中。(行缓冲,子进程拷贝的缓冲区没有东西,也就没有刷新打印出来)
当我们close1的时候,fork之后强制刷新,文件之中会有5个字符串,其中两个库函数的信息被重复打印。(全缓冲,先放到缓冲区里,子进程拷贝缓冲区,进程结束或遇到fflush,父子进程同时刷新)
也就是说一旦重定向,会影响缓冲方式。
而对于显示器和硬件中的文件写入时缓冲的方式也不同
库函数对应的缓冲方式:
打印到文件时,库函数信息为什么会出现两次,因为fork之前两个打印信息被全缓冲到缓冲区,fork之后子进程复制父进程,缓冲区数据也被复制。之后两个进程执行fflush或进程结束,就一共打印了4次。
write从头到尾只打印一次,也就是说他没有所谓用户级缓冲区,内核级缓冲区不在讨论范围。那么就可以得出一个结论c标准库提供了一个用户级缓冲区。
缓冲区由struct file来维护
9.dup2系统调用实现重定向
之前的重定向是关闭1,然后打开文件,文件就分配了1这个文件描述符。
那么假如文件早已打开怎么实现呢?
操作系统提供了一个系统调用,dup2
这句话的意思就是new是old的一份拷贝,即把3是下标对应一个指针,把这个指针的地址拷贝给1下标的指针,1指针这时就指向了这个文件。
所以传参的时候oldfd3,newfd就是1。可以这么理解
你想要我打印到显示器的东西打印到文件,就得让显示器的fd对应指针,指向你文件的fd对应指针指向的内容,怎么指向,把我指针内容(指针存储地址)拷贝给你。dup2(oldfd,newfd)
把3拷给1。
即现在有两个指针,指向这个文件
那么我们就要关闭fd,因为文件描述符有限,而且不关闭就造成了文件描述符泄漏。