万里长征——基础IO

目录

文件常识

回顾C语言的文件操作

系统层面的文件操作

文件操作的本质

文件fd的分配规则及重定向

linux下一切皆文件

详谈缓冲区问题


文件常识

1、文件 = 文件内容 + 文件属性

2、空文件也要在磁盘上占据空间。因为空文件虽然内容为空,但它的属性也会占据空间保存。

3、文件操作 = 对文件内容 + 属性的操作

4、标识/查找一个文件,要使用文件路径+文件名(唯一性)

5、如果没有指明对应文件的路径,默认就是在当前路径进行文件访问。

6、当我们代码中使用fopen、fread等函数时,代码完成编译形成可执行程序但没有加载到内存运行,是不会执行文件对应的操作的。所以文件操作的本质是进程对文件的操作

7、一个文件如果没有被打开,不能进行文件访问。文件在磁盘上,而磁盘是硬件,访问硬件是由操作系统来做的。所以用户+进程+OS构成了基本操作模式。文件操作的本质是进程和被打开文件的关系。

8、C、C++、Java等语言都有文件操作接口,它们上层提供的库函数不一样,但是底层实现是一样的,都是OS的操作接口。

回顾C语言的文件操作

1、 首先是fopen

fopen有2个参数,第一个是要打开的文件,第二个是打开的方式。

打开方式有 r(read),w(write),r+(读写,不存在错误),w+(读写,不存在创建)....

以上是以文本类型读取\写入,r+b,w+b是二进制形式读取\写入。

这里先不考虑二进制形式的。

万里长征——基础IO_第1张图片

万里长征——基础IO_第2张图片

上面的代码,以写的方式打开文件"log.txt",如果打开失败返回NULL,标识错误信息最后关闭文件。

万里长征——基础IO_第3张图片

 这里一开始没有创建log.txt文件,以写的方式(w)打开会默认创建文件,大小为0.

 2、fprintf

fprintf是常见的好用的文件操作函数。有3个参数,第一个是文件流,表示要将内容传给谁。

第二个参数和可变参数列表是要传的内容。

万里长征——基础IO_第4张图片        

  万里长征——基础IO_第5张图片

万里长征——基础IO_第6张图片  

 这样就将数据写入log.txt文件了。

3、fopen(读取)、fgets、puts

 fgets的3个参数,第一个是缓冲区s,fgets会将读到的数据保存到缓冲区;第二个是缓冲区大小size;第三个是读取的文件数据流。

返回值,如果成功,返回s,失败返回NULL。

puts(char* s),参数是字符串,打印到显示器。 

万里长征——基础IO_第7张图片

 fgets将fp流中的数据保存到buffer数组中,用puts函数打印到显示器上。注意这里给buffer最后留一个位置以便放\0;因为puts会自带 \n,所以将最后一个置为0去掉一个多余的\n。

万里长征——基础IO_第8张图片

4、fopen(追加方式写入 'a')

  FILE* fp = fopen(FILE_NAME,"w");   

以w方式写入会覆盖原来的数据,以a的方式追加写入不会覆盖,是在原本位置后面写入。

万里长征——基础IO_第9张图片

 万里长征——基础IO_第10张图片

系统层面的文件操作

刚刚回顾了一下C语言层面的文件操作,现在来看看系统层面的文件操作。

1、首先是open系统接口

上面提到的fopen是C库提供的,底层是open系统接口。

 open的返回值:当成功打开文件时返回新的系统描述符(file descriptor),失败时返回-1,并设置errno标识错误原因。  descriptor具体是什么呢?————是系统中的一种描述符,为了高效管理系统中被打开文件而创立的索引。它是一个非负整数(通常是小整数),用于指代被打开的文件。

万里长征——基础IO_第11张图片

 open的参数:第一个是要打开的文件路径(如果是当前路径下就是文件名);第二个是flag,标记位。第三个是mode,文件权限。

关于flag标记位具体来谈一下:

 这些以大写字母和下划线组成的是宏,O_RDONLY : read_only只读;O_WRONLY:write_only只写;O_RDWR:可读可写。

标记位是什么意思呢?————在C语言中,我们有时写函数的时候会加标记位,作为某一情况发生信息或者作为返回值,但一般都是用整形(整数)作为所传的值。

C语言传标记位,1个整数传1个标记位。
系统中传标记位,1个bit传1个选项,且bit位置不能重复。

比如说,0000 0000作为标记位,一个bit表示一个选项,作为选项的那个bit位只能是1.

不能重复的话只能是0000 0001,0000 0010,0000 0100......  而0000 0011这种是不行的。

所以,每一个宏,对应的数值只有一个bit位,彼此位置不重叠。

现在写一段代码来实际看一下标记位传参:

万里长征——基础IO_第12张图片

万里长征——基础IO_第13张图片

现在我们知道了标记位如何传参,那么来使用open接口打开log.txt文件:

万里长征——基础IO_第14张图片

 这里关闭文件描述符的系统接口叫close:

 运行程序,发现显示无法打开,因为就没有这个文件。

 

 是没有这个文件,但是不是应该会自己创建的吗?刚刚调用fopen函数的时候("w")没有对应文件就自己创建了,为什么这里没有创建呢?

————因为fopen是库函数,使用了对应的"w"选项后C语言本身会帮你执行创建文件的命令。

但是这里是系统接口不一样,它不会自动执行创建命令。要是想创建得添加flag选项:O_CREAT

int fd = open(FILE_NAME,O_WRONLY | O_CREAT);

万里长征——基础IO_第15张图片

现在确实可以自动创建文件了,但是该文件是乱码的形式出现的,无论是内容还是权限都是乱码。这是因为该文件是新创建的,我们没有给文件指明权限。此时就需要用到open接口的第三个参数了。

int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666);

此时指明权限是0666,那么文件就是rw-rw-rw-了吗?

万里长征——基础IO_第16张图片

 非也,是0664,这是因为系统默认权限掩码umask的作用。

最终权限 = 默认 & ~umask.

那么如果我们不想使用系统默认的权限掩码umask,可以改吗?————可以!

万里长征——基础IO_第17张图片

万里长征——基础IO_第18张图片

 可以看到文件的权限此时改变了。但是umask值没有变,这是因为umask(0) 改变的是子进程的umask,子进程执行完对shell的uamsk没有影响。

2、write接口

万里长征——基础IO_第19张图片

 write的参数有3个,第一个是文件描述符fd,第二个是按字节写入的字符串流,第三个是字符串长度。void* buf无类型,也就是说写入的是什么类型都一样,不管是二进制还是文本类型都是按字节写入。会按照二进制 \ 文本分类的是语言。

万里长征——基础IO_第20张图片

 sprintf 函数可以将数据格式化成字符串:

将hello world 和cnt格式化成字符串放入outBuffer中,再写入fd。

这里有个问题:strlen(outBuffer) + 1 吗?

————不需要!C语言中规定了字符串要以\0结尾,但那是语言规定的,这里是系统调用,没有规定说需要\0结尾,如果+1就会多写入一些其它东西。

万里长征——基础IO_第21张图片

 所以不需要+1。还有一个问题:

如果在已经写入文件的基础上,此时改变写入的内容,再编译运行,

 

 会发现:

万里长征——基础IO_第22张图片

 写入的内容没有完全覆盖,只覆盖了前面的一些内容。

fwrite("w") 不是会清空覆盖文件的内容吗?————还是那句话,不要以语言的标准衡量系统,两者是不一样的。系统是按字节写入的,覆盖也是按字节覆盖的。

那想要完全覆盖文件的内容怎么做?————在open接口中再添flag选项 : O_TRUNC

int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);  

 万里长征——基础IO_第23张图片

 此时再编译运行就能完全覆盖文件的内容了。

所以在上层我们用C库调用fwrite函数,看似简单的一步操作,实际上底层要用到O_WRONLY , 

O_CREAT , O_TRUNC选项。

追加写入也是再添加一个选项APPEND,将TRUNC换成APPEND:

int fd = open(FILE_NAME,O_WRONLY,O_CREAT,O_ARREND);

万里长征——基础IO_第24张图片

3、读取read

 read的参数有3个,第一个fd读取的文件描述符对应的内容;第二个void* buf也是按字节读取到该buf缓冲区里,第三个是读取的大小。返回值是整数,如果 >0就说明读取成功,并返回读到的字节数;如果返回的是0,表示读到了文件结尾。

 万里长征——基础IO_第25张图片

 这里sizeof(buffer)-1以及buffer[num] = 0 是为什么呢?————读取是按行读取,C语言规定读取字符串碰到 \n停一下读取下一行,\0就停止读取。这里系统不知道读取的是二进制还是文本还是其它东西,所以用户编写的时候最后设置一个\0,默认是按照字符串的形式读取。

文件操作的本质

上面说了,文件操作的本质就是进程与被打开的文件之间的关系。系统中存在大量进程,同时也存在大量被打开的文件(进程可以打开多个文件),操作系统需要对这些文件进行管理。而我们知道管理的本质是先描述,再组织。因此操作系统将这些被打开的文件用struct file结构体描述起来,再放入链表中。管理这些文件也就变成了管理该链表。

万里长征——基础IO_第26张图片

 我们一直忽略了描述符fd,现在来看一下fd对应的值:

万里长征——基础IO_第27张图片

万里长征——基础IO_第28张图片

 我们这打开了4个文件,并分别打印出了对应的描述符。观察发现2个问题:

1、为什么描述符是从3开始的,0 1 2呢?

2、描述符打印的是小整数,和C语言的数组下标有点相似,是否存在什么关联?

————1、先回答第一个问题。

描述符从3开始,明显是0 1 2被占用了,被谁占用了呢?————stdin, stdout, stderr。

C程序下会默认打开这3个流。

万里长征——基础IO_第29张图片

这3个标准输入输出流,对应的分别是键盘和显示器。

在C语言中文件操作中有个FILE* fp = open();  这里的FILE是什么呢?

————它其实是个结构体,里面封装了什么?想想它需要什么,为什么存在。

万里长征——基础IO_第30张图片

 我们知道所谓的C语言提供的文件操作函数fopen, fclose等都是基于系统接口open , close衍生出现的。系统文件操作需要描述符fd,那么C语言中fopen...作为上层封装的函数也需要,如果没有在调用函数的时候底层找不到fd,只有FILE系统是不认识的,所以FILE结构体中一定有描述符fd。

万里长征——基础IO_第31张图片

 可以看到fd:0 1 2是被这3个输入输出流占用了。

2、再回答第二个问题:描述符打印的是小整数,和C语言的数组下标有点相似,是否存在什么关联?画图来解释一下:

万里长征——基础IO_第32张图片

磁盘上的可执行程序加载到内存变成进程,进程被OS调度执行文件操作打开对应的文件。

因为进程可以打开多个文件,所以此时内存中存在大量被打开的文件,该进程要如何找到自己需要的被打开的文件呢?————进程的PCB中task_struct结构体内有一个指针struct file_struct* file指向OS的内核结构体struct files_struct (文件描述符表),该表中有一个指针数组struct file* fd_array[ ]与stuct file建立着映射关系。通过该表,将进程与被打开的文件建立起映射关系。

数组下标0位置对应struct file的stdin(键盘),下标1对应stdout,下标2对应stderr,前3个下标被占用,后面依次是对应被打开的文件。所以文件描述符的本质就是数组下标!

文件fd的分配规则及重定向

文件fd的分配规则

先来看一个场景:

万里长征——基础IO_第33张图片

 我们知道,在C程序下stdin, stdout, stderr这3个流是默认被打开的,并且占用文件描述符表的0,1,2位置,所以我们打开文件fd一般是从3开始。

这里在开头close(0),看看打印的fd 是几:

 fd打印的是0。将close(0)改成close(2):

fd打印的是2。如果同时关闭close(0) close(2):

 fd打印的还是0。似乎关闭了fd在前头的0/2打开文件会占据它们的fd。

由此可以得出文件fd的分配规则:文件被打开时要获取自己的fd,会在文件描述表中从小到大,按顺序寻找最小的且没有被占用的fd。

 所以上面close(0),strin被关闭了不再占用数组的第0位,此时新文件被打开遍历文件描述表,找到最小的且没被占用的第0位,它的fd就成了0.

close(2)    close(0) && close(2)   也是同理。

但是现在又有一个奇怪的现象:

万里长征——基础IO_第34张图片

 当close(1)的时候,发现什么都没有打印。

其实这也可以理解,fd为1的是哪个流?——stdout,标准输入流,它对应的是显示器。

在开始关闭了stdout,就无法将内容打印到显示器了。那要打印的数据去哪里了?————在新文件里。

万里长征——基础IO_第35张图片

 首先我们要明确一点:文件描述符表里的指针数组每一个元素类型都是file*,是指针。所以关闭某个表中的文件,其实是将指针的指向改变,不再指向原来的位置。close(1),open"log.txt" 实际上是将fd:1的指针指向新文件,和stdout的联系断开。

万里长征——基础IO_第36张图片

确实不会再显示到显示器上,无打印内容。

fprintf(stdout),系统找的是fd为1的对应的文件,一开始fd为1的stdout对应的是显示器,fd本身没有改变,改变的是指向,现在指向的是新文件,系统还是去找fd:1,所以写入的是新文件。

那么本来打印到显示器的内容现在变成了写入到新文件,我们来看看是不是这样:

 还是没有显示,这是为什么呢?————其实这和缓冲区有关,cat log.txt没有立刻显示出来,系统向显示器打印和向普通文件显示的策略不同,我们强制刷新一下:

 这里fflush刷新的依然是stdout,但和上面说的一样,现在显示就不是在显示器而是在新文件了。

重定向

什么是重定向,其实我们之前就接触过:

万里长征——基础IO_第37张图片

 像上面将原本要向显示器打印的数据通过我们的操作写入到其它文件,就这叫重定向。

重定向有   输出重定向:  >      追加重定向 : >>     输入重定向 : <

重定向的本质是:上层用的fd不变,在内核中改变fd对应的struct file*的地址。

刚刚讲文件fd的分配规则时举的几个例子(先close)也算重定向,但是一般不会那么写,系统给我们提供了重定向接口:

万里长征——基础IO_第38张图片

 这里面dup2用的最多,来具体说一下。

 dup2的参数有2个,oldfd 和newfd,看名字联想意思容易搞错,从该接口的描述入手更好一点。

newfd be the copy of oldfd,新的fd是旧的fd的拷贝,那么最终拷贝的结果是根据oldfd来的。而且这里说的拷贝是覆盖,最终剩下的是oldfd的内容。

这里说的拷贝是将文件描述符里的内容拷贝,不是将文件描述符这个整数本身拷贝!

万里长征——基础IO_第39张图片

 fd:1要指向新文件,所以是将fd:3的内容拷贝给fd:1,让fd:1获取fd:3的数据,指向新文件。

剩下的是fd:3里的内容,oldfd是fd:3。

所以int dup2(int oldfd,int newfd)中,oldfd是fd:3,newfd是fd:1。将newfd写给oldfd。

万里长征——基础IO_第40张图片

 万里长征——基础IO_第41张图片

 

追加重定向,只需要改变open的方式即可:

万里长征——基础IO_第42张图片  

万里长征——基础IO_第43张图片

输入重定向,不再从键盘读取数据,而是从文件读取数据。

先写一个从键盘读取数据的小程序:

    char line[64];
    while(1)
  {
     printf("> ");
    if(fgets(line,sizeof(line),stdin) == NULL)
       break;
    printf("%s",line);                                                                                                                                                                
  }

万里长征——基础IO_第44张图片

 Linux下Crtl +D 表示文件结尾。

输入重定向就是dup2(fd,0); 将fd:0 原本指向stdin改变指向新文件。将fd:3拷贝给fd:0

万里长征——基础IO_第45张图片

 万里长征——基础IO_第46张图片

上面我们了解了重定向的含义,那么Linux下的重定向是如何表现的呢?

 之前说Linux下执行命令,是将命令字符串分割成一段一段的字符串:

"ls -a -l " ----->  "ls", "-a" , "-l"

现在重定向也是先将字符串分割成两部分:"ls -a -l > myfile.txt" -----> "ls -a -l"  "myfile.txt"

然后把重定向符变成 ‘\0',这样前后就以 '\0' 分成两个字符串了。

下面具体来看一下Linux下如何处理重定向信息:

万里长征——基础IO_第47张图片

 首先定义4个重定向类型宏,第一个是无重定向,第二个是输入重定向,第三个是输出重定向,第四个是追加重定向。

然后定义初始重定向类型为无重定向,初始重定向文件为NULL。

万里长征——基础IO_第48张图片

 写一个分析重定向函数commandCheck,从头向尾遍历,当start指针遇到重定向符号时将其改成 '\0'并且打印重定向类型及文件信息。

重定向函数执行完后将字符串以 '\0' 为界分割成两部分,再执行分割字符串函数进行程序替换:

万里长征——基础IO_第49张图片

 因为命令是子进程执行的,所以真正的工作是子进程完成的。但是如何重定向,是父进程给子进程提供信息的。

然后继续完成程序,根据不同的重定向信息采取不同的操作:

万里长征——基础IO_第50张图片

 以重定向类型作为判断条件,当redirType是NONE_REDIR时什么都不做;是INPUT_REDIR时输入重定向,以读的方式打开redirfile文件,重定向到该文件中.......

 重定向完成后执行程序替换,整体代码如下:

    1 #include                                                                                                                                                                    
    2 #include
    3 #include  
    4 #include
    5 #include  
    6 #include      
    7 #include         
    8 #include                
    9 #include
   10 #include
   11                       
   12 #define NUM 1024      
   13 #define OPT_NUM 64         
   14 #define NONE_REDIR 0   
   15 #define INPUT_REDIR 1
   16 #define OUTPUT_REDIR 2
   17 #define APPEND_REDIIR 3          
   18 #define trimSpace(start) do{\
   19     while(*start != ' ') start++;\
   20 }while(0)                  
   21                                           
   22 char lineCommand[NUM];
   23 char* myargv[OPT_NUM]; 
   24 int redirType = NONE_REDIR;
   25 char* redirfile = NULL;   
   26                     
   27                               
   28 void commandCheck(char* commands)
   29 {                       
   30     assert(commands);                     
   31     char* start = commands;
   32     char* end = commands+strlen(commands);
   33 
   34     while(start < end){
   35         if(*start == '>'){       
   36             *start = '\0';        
   37             start++;  
   38             if(*start == '>'){
                *start = '\0';
   40                 start++;
   41                 redirType = APPEND_REDIIR;
   42             }
   43             else{
   44                 redirType = OUTPUT_REDIR;
   45             }
   46                 trimSpace(start);
   47                 redirfile = start;
   48                 break;
   49             }
   50            else if(*start == '<'){
   51             *start = '\0';
   52             start++;
   53             trimSpace(start);
   54             redirType = INPUT_REDIR;
   55             redirfile = start;
   56             break;
   57         }
   58         else{
   59             start++;
   60         }
   61     }
   62 }
   63 
   64 int main()
   65 {
   66     while(1)
   67 {
   68     redirfile = NULL;
   69     redirType = NONE_REDIR;
   70     printf("用户名@主机名 当前路径# ");
   71     fflush(stdout);                                                                                                                                                                  
   72 
   73     //获取用户输入
   74     char*s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);
   75     assert(s != NULL);
   76     (void)s;
  lineCommand[strlen(lineCommand)-1] = 0;
   78    //printf("test :%s\n",lineCommand);
   79     
   80     commandCheck(lineCommand);
   81 
   82     //分割字符串
   83     myargv[0] = strtok(lineCommand," ");
   84     int i = 1;
W> 85     while(myargv[i++] = strtok(NULL," "));
   86    
   87     if(myargv[0] != NULL && strcmp(myargv[0],"cd")== 0){
   88         if(myargv[1] != NULL)
   89             chdir(myargv[1]);
   90         continue;
   91     }
   92 
   93 #ifdef DEBUG
   94     for(int i = 0;myargv[i];++i){
   95         printf("myargv[%d]:%s\n",i,myargv[i]);
   96     }
   97 #endif
   98    
   99     pid_t id = fork();
  100     assert(id != -1);
  101     if(id == 0){
  102     switch(redirType)
  103     {
  104         case NONE_REDIR:
  105             break;
  106         case INPUT_REDIR:
  107             {
  108             int fd = open("redirfile",O_RDONLY);
  109             if(fd < 0){                                                                                                                                                              
  110                 perror("open");
  111                 return 1;
  112             }
  113             dup2(fd,0);
  114             }
        break;
  116         case OUTPUT_REDIR:
  117         case APPEND_REDIIR:
  118         {
  119             int flag = O_WRONLY | O_CREAT;
  120             if(redirType == OUTPUT_REDIR){
  121                 flag |= O_TRUNC;
  122             }
  123             else{
  124                 flag |= O_APPEND;
  125             }
  126             int fd = open("redirfile",flag,0666);
  127             if(fd < 0){
  128                 perror("open");
  129                 return 1;
  130             }
  131             dup2(fd,1);
  132         }
  133             break;
  134         default:
  135             printf("error\n");
  136             break;
  137     }
  138         execvp(myargv[0],myargv);
  139         exit(1);
  140     }
  141     waitpid(id,NULL,0);
  142 }
  143 }                                                                                                                                                                                    
                    

 执行结果如下:

万里长征——基础IO_第51张图片

万里长征——基础IO_第52张图片

万里长征——基础IO_第53张图片

看完程序后有几个问题:

1、重定向是父进程向子进程提供信息的,命令是子进程执行的,那么重定向会影响父进程吗?

不会!画图理解:

万里长征——基础IO_第54张图片

 父进程PCB内有一个指针指向它的文件描述符表,子进程被创建出来要执行重定向命令,那么子进程PCB的指针是指向父进程的表还是子进程拷贝一份父进程的表然后指向它呢?

显然是后者,因为如果子进程的指针指向父进程的文件描述符表,更改的就是父进程的内核数据结构了,而进程具有独立性不能相互影响,所以理应后者。

2、子进程拷贝父进程的文件描述符表,那么要不要拷贝父进程描述表映射的文件呢?

不要!我们要分清楚结构,进程管理模块是内核数据结构,文件描述符表这些结构是可以拷贝的,而文件部分是系统文件,不好直接拷贝一份给子进程。

3、执行程序替换的时候,会不会影响曾经进程打开的重定向文件?

不会!上面整个图可以叫内核数据结构,而进程替换是将磁盘的数据和代码与进程的替换,与内核数据结构是不相关的,它们各种分管自己的部分。像进程程序替换不影响PCB,也不影响其内部的pid等细节。

万里长征——基础IO_第55张图片

linux下一切皆文件

首先说一下文件的关闭。在进程退出的时候,进程控制块PCB会跟着释放,文件描述符表也会释放。表中的file*指针不再指向对应的文件,如果文件没有指针指向就会关闭。

万里长征——基础IO_第56张图片

为什么说Linux下一切皆文件呢?

我们知道操作系统下面是驱动,驱动下面是硬件。OS想访问硬件需要通过对应的驱动接口再访问硬件万里长征——基础IO_第57张图片

 而访问硬件的接口有很多且各式各样,这是底层。在上层要做到将其封装起来用统一的形式进行访问,所以OS实现了一个独立的软件层,通过struct file这个管理文件的结构体,调用驱动。在上层看来不管访问哪个硬件都是一样的接口,所有的设备,统一都是struct file。

struct file
{
    //文件属性
    int type;
    int status;
    int(*readp)();
    int(*writep)();
}

 万里长征——基础IO_第58张图片

 万里长征——基础IO_第59张图片

 struct file中有2个函数指针,readp访问Read接口,writep访问Write接口。最后访问对应硬件。

通过这一系列转化将访问硬件变成对文件的访问,所以说Linux下一切皆文件。

我们平时在用户层对文件进行打开、关闭的操作其实并不是直接说打开就打开,说关闭就关闭的。

有时其实并没有真的关闭文件。

我们知道,一个文件可能同时被多个files_struct里的指针所指向

万里长征——基础IO_第60张图片

 在内核中其实是以引用计数的方式表现文件被指向的情况的:

有一个count整数记录着指向文件的指针个数,每有一个指针指向,count+1,不再指向-1.

当count == 0时文件就被OS关闭了。

所以我们平时说的关闭文件实质上就是当前使用的那个进程退出,它的指针不再指向对应文件,而此时可能还有其它指针指向,文件并不会被关闭,所以还是得看OS。


详谈缓冲区问题

在谈缓冲区之前首先引入一个例子:

万里长征——基础IO_第61张图片

万里长征——基础IO_第62张图片

将其重定向到log.txt中确实如我们所想打印了4条语句。

此时在小程序的最后加一句创建进程fork,再重定向到log.txt查看,发现打印了7条语句:

万里长征——基础IO_第63张图片

 发现其中C语言函数打印了2次,而系统接口write打印了一次,这就与缓冲区有关了,下面我们先来认识一下缓冲区。

认识缓冲区

我们早就知道又缓冲区这个概念了,很多书上也都会提到缓冲区,但我们平时说的缓冲区究竟是什么?————其实缓冲区就是一段内存。

万里长征——基础IO_第64张图片

 进程将数据拷贝给缓冲区,然后缓冲区再将数据交给外设。拷贝的过程可以理解为fwrite函数的作用。与其说fwrite函数是写入到文件的函数,不如说是拷贝函数,将数据从进程拷贝到缓冲区或外设。

缓冲区也不是直接就将进程拷贝来的数据直接交给外设,它有自己的刷新策略。

缓冲区刷新策略

一块数据是一次写入到外设效率高,还是多次少量写入到外设效率高呢?————显然是一次性的。缓冲区刷新也尽量按照这种方式刷新,也要结合具体的设备考虑。

1、立即刷新————无缓冲(少见,效率低)一般用户自己强制刷新

2、行刷新   ————行缓冲(对应设备:显示器)

3、缓冲区满刷新————全缓冲(对应设备:磁盘)效率最高,只需一次IO,等一次外设

还有2种特殊情况:

1、上面说的用户强制刷新(fflush)

2、进程退出自动刷新缓冲区。

缓冲区在哪?

 说了那么多,好像我们从未得知缓冲区在哪。首先肯定不在系统内核中,否则上面小程序fwrite系统接口就不会只打印一次,而是和C语言函数一样打印2次了。

像stdout, stdin, stderr这些文件函数,都有一个共同的指针FILE*,内部还有FILE结构体以及fd文件描述符。我们平时说的缓冲区就在FILE结构体中,它是用户级语言层面的缓冲区。

如何证明?解释一下上面小程序的例子:

1、如何没有重定向,我们看到的是打印4条信息,printf, fprintf, fputs, write,因为这里stdout默认是行刷新,在fork函数之前,将3条C语言函数的打印数据显示到显示器上了,这时FILE结构体内部,进程内部就没有相应的数据了,最后进程退出刷新缓冲区,再将write打印信息显示出来。

2、如果进行了重定向,那么数据不再是写入到显示器,而是写入到普通文件,采用的刷新策略不是行缓冲而是全缓冲了!3条C语言打印信息虽然带了 \n 但是不足以写满缓冲区并刷新。执行fork函数创建子进程,紧接着就是退出进程,不知道先退出哪一个但是只要退出进程就会刷新缓冲区,也就是修改缓冲区,会发生写时拷贝!stdout属于父进程,发生写时拷贝会将3条在缓冲区的信息拷贝刷新到显示器,另一个进程随后退出再次刷新缓冲区,将3条信息以及write打印信息一同刷新到显示器,这就是为什么会出现两次打印7条语句的原因,也证明了上面缓冲区位置的问题。

3、为什么write接口没有打印两次?————因为write接口是系统接口,没有FILE结构体只要fd,所以没有用户级语言层面的缓冲区。

系统级内核缓冲区

刚刚讲的是用户语言级的缓冲区,其实文件写入到磁盘远远没有我们想的那么简单,并不是经过用户级缓冲区就能直接刷新到外设了,其中过程还有一个缓冲区是系统内核级的。

万里长征——基础IO_第65张图片

 进程将数据通过fwrite函数拷贝到用户语言级缓冲区,缓冲区根据设备决定是无缓冲、行缓冲还是全缓冲(一般普通文件全缓冲,显示器文件行缓冲...)

此时刷新不是直接到外设,经过文件描述信息表,由系统接口write函数拷贝到内核缓冲区,此时的刷新策略是由OS自主决定的,不是简单的3大刷新策略,最后才到外设。

像这样将数据交给了内核缓冲区,也就是交给了系统,就与用户/语言层面没关系了,如果OS宕机数据丢失怎么办?

————有一个接口fsync可以解决这种问题:

万里长征——基础IO_第66张图片

 该接口可以强制性的把内核缓冲区的数据刷新到外设。

你可能感兴趣的:(开发语言,linux,操作系统,基础IO,缓冲区)