【Linux】基础IO --- 系统级文件接口、文件描述符表、文件控制块、fd分配规则、重定向…

简介: 【Linux】基础IO --- 系统级文件接口、文件描述符表、文件控制块、fd分配规则、重定向…

能一个人走的路别抱有任何期待,死不了8c800808b1c34c28b72a453b6e5ec8ea.jpeg


一、关于文件的重新认识


1.空文件也要在磁盘中占据空间,因为文件属性也是数据,保存数据就需要空间。

2.文件=内容+属性

3.文件操作=对内容的操作or对属性的操作or对内容和属性的操作

4.标识一个文件必须有文件路径和文件名,因为这具有唯一性。

5.如果没有指明对应的文件路径,默认是在当前路径下进行文件访问,也就是在当前进程的工作目录下进行文件访问。如果想要改变这个目录,可以通过系统调用chdir来改变。

6.在C语言中,调用fread、fwrite、fopen、fclose、等接口对磁盘中的文件进行操作,实际上必须等到代码和数据加载到内存中,变成进程之后,cpu读取进程对应的代码,然后操作系统才会对文件进行操作,而不是只要我们一调用文件操作的接口就会对文件操作,而是必须将这些接口加载到内存之后,才可以。

所以对文件的操作,本质上就是进程对文件的操作!!!

7.一个文件要被访问,必须先被打开。用户进程可以调用文件打开的相关函数,然后操作系统对磁盘上相应的文件进行处理。在磁盘上的文件可以分为两类,一类是被打开文件,一类是未被打开的文件。

8.所以,文件操作的本质就是进程和被打开文件的关系。


二、语言和系统级的文件操作(语言和系统的联系)


1.C语言有文件操作接口,C++有文件操作接口,JAVA有文件操作接口,python、php、go、shell这些语言都有文件操作接口,这些文件操作接口在不同的语言中都是不一样的。

2.在磁盘中的文件如果想要被进程访问,则一定绕不开操作系统,因为磁盘是硬件,而操作系统是硬件的管理者,所以想要访问文件,必须通过操作系统提供的接口来访问文件,因为直接访问的方式是不安全的,所以必须使用操作系统提供的系统调用接口来访问这些文件。

3.库函数底层必须调用系统调用接口,因为无论什么进程想访问文件,都必须按照操作系统提供的方式来进行访问,所以就算文件操作相关函数千变万化,但是底层是不变的,这些函数最后都会调用系统调用接口,按照操作系统的意愿来合理的访问磁盘上的文件。

4.如果进程想要访问其他硬件,道理也相同,最终都必须按照操作系统的意愿来访问,也就是通过系统调用来访问。


1.C语言文件操作接口(语言级别)

1.1 文件的打开方式


r:以只读的方式打开文件,若文件不存在就会出错。

w:以只写的方式打开文件,文件若存在则清空文件内容重新开始写入,若不存在则创建一个文件。

a:以只写的方式打开文件,文件若存在则从文件尾部以追加的方式进行写入,若不存在则创建一个文件。

r+:以可读写的方式打开文件,若文件不存在就会出错。

w+:以可读写的方式打开文件,其他与w一样。

a+:以可读写的方式打开文件,其他与a一样。

其他打开方式是以二进制形式打开,不怎么用到,这里不做详细说明。

79195fbbcd1645c9ba8156516f569478.png


1.2 文件操作的相关函数

  1 #include <stdio.h>  
  2 #include <stdlib.h>  
  3 #include <string.h>  
  4 #include <unistd.h>  
  5 #define FILE_NAME "log.txt"  
  6 int main()  
  7 {  
  8     FILE*fp = fopen(FILE_NAME,"a");  
  9     //FILE*fp = fopen(FILE_NAME,"w");                                                                                                                
 10     //FILE*fp = fopen(FILE_NAME,"r");
 11     // r、w、r+(读写,文件不存在就出错)、w+(读写,文件不存在就创建文件)、a(append,追加,只写的形式打开文件)、a+(以可读写的方式打开文件)      
 12     if(fp==NULL)         
 13     {                     
 14         perror("fopen");  
 15         exit(1);         
 16     }                    
 17                          
 18     int cnt = 5;         
 19     while(cnt)                                                   
 20     {                                                              
 21         fprintf(fp,"%s:%d\n","hello wyn",cnt--);// 对文件进行写入  
 22     }                    
 23     fclose(fp);                                                                                                                       
 24   //  char buffer[64];                                                                                                                  
 25   //  while(fgets(buffer,sizeof(buffer) - 1,fp) != NULL)// sizeof-1的原因是有给文本末尾留一个位置,让fgets放terminating null character  
 26   //  {                                                       
 27   //      buffer[strlen(buffer)-1]=0;//把换行符改成结束符     
 28   //      puts(buffer);  
 29   //  }                  
 30   //  fclose(fp);        
 31     return 0;            
 32 } 


fprintf,fopen,fclose,fgets,puts,等都是有关文件操作的函数,常用的文件打开方式有r(只读)、w(只写),a(追加)三种。


很容易被忽略的细节:

1.fprintf向文件写入时,换行符也是会被写入到文件当中的

2.fgets在读取文件内容的时候,换行符会被认为是有效字符读取到缓冲字符数组里面的,并且在每行读取结束后,fgets会自动添加null character到缓冲字符数组的每个字符串末尾处。

3.puts在将字符串打印的时候,会自动在字符串末尾追加一个换行符。所以为了防止puts打印两个换行符,在while循环里面将buffer数组里面的换行符改为null character。

4.fgets在读取的时候,以读取到num-1个字符,或换行符,或者文件结束符为止,以先发生者为准,这就是读取一行的内容。所以如果想要读取多行内容,就需要搞一个while循环。


1.3 细节问题

1.在C语言中,如果以w的方式单纯的打开文件,则文件内部的数据会自动被清空。

2.文本类文件在被创建的时候,默认权限是0664,因为文件刚开始的权限是0666,经过和umask0002的取反结果按位与后,最终的权限就变为了0664,顺便提一句,八进制用0开头来表示,十六进制用0x开头来表示。


2.系统级文件操作接口(系统级别)

2.1 open


1.

open用于打开文件,是系统级别的接口,open有两种使用形式,一种是只有两个参数,一种是有三个参数,第二种是针对打开文件不存在的情况,需要我们创建一个文件,并设定文件的初始权限,第一种是针对文件存在的情况,无须设定文件初始权限。


bbe530ff3e964ad78ab6855c46107381.png


2.

第一个参数就是文件的名字,第二个参数flags是指打开文件时的方式,例如O_RDONLY,O_WRONLY,O_RDWR,O_CREAT,O_APPEND等宏,都可以在调用open时作为参数进行传参。

b50cec9eb0c64413ae1161b7ef817382.png


3.

mode_t mode作为第三个参数,代表打开文件不存在时,首先需要创建文件,创建文件的初始权限需要被设置,权限的设置就是通过这个参数来实现的。

e7fa573fc7c34914a7ba001dbbf6fac1.png


4.

文件打开成功,则会返回新的文件描述符,打开失败就会返回-1,虽然现在还不清楚文件描述符是什么,但这不重要下面的2.3会讲到的,现在只要知道文件描述符是一个整数就可以了。


5.

要想理解open的第二个参数,则需要先理解如何使用比特位来传递选项,如果想让函数实现多种功能的话,我们可以利用或运算来讲多个选项 “粘合” 到一起,从而让一个接口同时实现多种不同的功能。利用的原理就是宏整数的32比特位中只有一个比特位是1,且不同的宏的1的位置是不重叠的,这样就可以利用或运算来同时实现多个功能。

 11 每一个宏,对应的数值,只有一个比特位是1,彼此位置不重叠
 12 #define ONE (1<<0)
 13 #define TWO (1<<1)
 14 #define THREE (1<<2) 
 15 #define FOUR (1<<3)
 16 
 17 void show(int flags)
 18 {
 19     if(flags & ONE) printf("one\n");
 20     if(flags & TWO) printf("two\n");
 21     if(flags & THREE) printf("three\n");
 22     if(flags & FOUR) printf("four\n");
 23 
 24 }
 25 int main()
 26 {
 46     show(ONE);
 47     printf("---------------------\n");
 48     show(TWO);
 49     printf("---------------------\n");
 50     show(ONE | TWO);
 51     printf("---------------------\n");
 52     show(ONE | TWO | THREE);
 53     printf("---------------------\n");
 54     show(ONE | TWO | THREE | FOUR);
 55     printf("---------------------\n");
 56 }

7f761fc82ba149ca991864f1f0b5d534.png


6.
这也就是flags参数的不同的宏对应着不同的功能的原理,这些宏实际上就是利用了不同的比特位来表示不同的含义的,实现原理是一样的,但在具体实现上可能和我们上面所讲的简单原理不同,但只要原理相同就够了

 25 int main()
 26 {
 27     umask(0);//将进程的umask值设置为0000
 28 
 29     // C语言中的w选项实际上底层需要调用这么多的选项O_WRONLY O_CREAT O_TRUNC 0666
 30     // C语言中的a选项需要将O_TRUNC替换为O_APPEND
 31     int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666);//设置文件起始权限为0666
 32     if(fd < 0)
 33     {
 34         perror("open");
 35         return 1;//退出码设置为1
 36     }
 37     close(fd);   
 38 }


ec7ce038d1cf451ba0ecd7fda3f75843 (1).png


7.

O_CREAT代表打开文件如果不存在,就创建一个文件,如果没有这个宏,且打开了一个不存在的文件,则会报错,0666是设置的文件的起始权限,如果不想受到父进程shell的umask值0002的影响的话,可以通过系统调用umask()手动设置子进程的umask的值为0,这样起始权限实际上就是最终的文件权限了,因为umask按位取反后是全1,起始权限按位与后不会改变。


如果不设置文件起始权限,则创建出来的文件的权限就会是乱码。

6a53f15d54fd40dd92a6d2476e79659b.png


8.

创建目录的命令mkdir,目录起始权限默认是0777,创建文件的命令touch,文件起始权限是0666,这些命令的实现实际上是要调用系统接口open的,并且在创建文件或目录的时候要在open的第三个参数中设置文件的起始权限。


2.2 write


1.

在C语言中的写入函数有fputs,fprintf,fwrite等,但在系统级别,写入接口只有一个write

8ac5c336fd2249d393e48aac6ea200e6.png

 25 int main()                                  
 26 {                                           
 27     umask(0);//将进程的umask值设置为0000    
 28                                             
 29     // C语言中的w选项实际上底层需要调用这么多的选项O_WRONLY O_CREAT O_TRUNC 0666
 30     // C语言中的a选项需要将O_TRUNC替换为O_APPEND
 31     int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666);//设置文件起始权限为0666
 32     if(fd < 0)                              
 33     {                                       
 34         perror("open");                     
 35         return 1;//退出码设置为1            
 36     }                                       
 37     close(fd);                              
 38     int cnt = 5;                            
 39     char outbuffer[64];                     
 40     while(cnt)                              
 41     {                                       
 42         sprintf(outbuffer,"%s:%d\n","hello linux",cnt--);
 43         //以\0作为字符串的结尾,是C语言的规定,和文件没什么关系,文件要的是字符串的有效内容,不要\0
 44         //除非你就想把\0写到文件里面取,否则strlen()不要+1
 45         write(fd,outbuffer,strlen(outbuffer));
 46     }                                       
 47     printf("fd:%d\n",fd);// 文件描述符的值为3                                                                                                        
 48     close(fd);


2.

如果write写入时第三个参数要多加一个\0的位置,创建出来的log.txt用vim打开时会出现乱码,以\0作为字符串的结束标志,这是C语言的规定和文件没有关系,文件只要存储有效内容就好了,不需要\0,所以在write写入的时候,strlen求长度不要+1

e4d566b6f1d946399905bc45edbf4f92.png


9e39482d9f4848df8654bc2d75d06406.png


3.

只将写入的内容改为aaaa,打印出来的log.txt的内容就发生了覆盖式写入的现象,而不是先将文件原有内容清理,然后在重新写入。

在C语言中,如果再次以写的方式打开文件,会自动将原先文件中的内容清理掉,重新向文件写入内容。

自动清空原有数据,实际上是通过open系统调用中的第三个宏参数O_TRUNC来实现的。

93daf2c8ed764f0c961ae9386d75a72b.png


1bf44d63cdf84de593d5e70b43c02007.png


efe34924c6d64ad9ba8971358278b681.gif


4.

所以C语言中打开文件时,使用的打开方式为w,在底层的open接口中,要用三个宏参数O_WRONLY,O_CREAT,O_TRUNC来实现。

C语言中的a打开方式,在系统底层实现上只需要将O_TRUNC替换为O_APPEND即可。

可见库函数和系统调用的关系,本质就是库函数封装系统调用。

4e7814adb2e64494ae0016e4ad2f75dd.gif


2.3 read


1.

read从一个文件描述符中读取内容,然后将其存到缓冲区buf里面,如果read调用成功,则会返回read读取的字节数,返回0代表读到了文件的结尾。


7103ae9d245448cbb93783dbfda49167.png


2.
我们知道要读取的内容是字符串,所以在数组buffer里面,需要手动设置字符串的末尾为\0,方便printf打印字符串。

3.
0,‘\0’,NULL等字面值实际上都是0,只不过他们的类型不同。

 25 int main()
 26 {
 27     umask(0);//将进程的umask值设置为0000
 28 
 29     // C语言中的w选项实际上底层需要调用这么多的选项O_WRONLY O_CREAT O_TRUNC 0666
 30     // C语言中的a选项需要将O_TRUNC替换为O_APPEND
 31     int fd = open(FILE_NAME,O_RDONLY,0666);//设置文件起始权限为0666
 32     if(fd < 0)
 33     {
 34         perror("open");
 35         return 1;//退出码设置为1
 36     }
 37     char buffer[1024];
 38     ssize_t num = read(fd,buffer,sizeof(buffer)-1);
 39     if(num > 0) buffer[num]=0;//字符数组中字面值0就是\0
 40     printf("%s",buffer);     
 41   close(fd);
 42 }


3.文件控制块&&文件描述符&&文件指针的关系



1.
进程可以打开多个文件,对于大量的被打开文件,操作系统一定是要进行管理的,也就是先描述再组织,所以操作系统会为被打开的文件创建对应的内核数据结构,也就是文件控制块FCB,在linux源码中是struct file{}结构体,包含了文件的大部分属性。

  1 #include <assert.h>
  2 #include <stdio.h>
  3 #include <sys/types.h>
  4 #include <sys/stat.h>
  5 #include <fcntl.h>
  6 #include <stdlib.h>
  7 #include <string.h>
  8 #include <unistd.h>
  9 #define FILE_NAME(number) "log.txt"#number
 25 int main()                                                                                                                                  
 26 {                                                                                                                                           
 27     int fd0 = open(FILE_NAME(1),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
 28     int fd1 = open(FILE_NAME(2),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
 29     int fd2 = open(FILE_NAME(3),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
 30     int fd3 = open(FILE_NAME(4),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
 31     int fd4 = open(FILE_NAME(5),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666                                                                                 
 32     printf("fd:%d\n",fd0);
 33     printf("fd:%d\n",fd1);
 34     printf("fd:%d\n",fd2);
 35     printf("fd:%d\n",fd3);
 36     printf("fd:%d\n",fd4);
 37     close(fd0);
 38     close(fd1);
 39     close(fd2);
 40     close(fd3);
 41     close(fd4);  
 42 }


3a36875994cb4a8f991664f9f1d5c893.png


2.

fd值为-1表示文件打开时出现错误,返回正数表示文件打开成功。

标准输入,标准输出,标准错误输出是系统默认打开的三个标准文件,系统自定义的三个文件指针stdin、stdout、stderr中一定含有文件描述符。


3.

文件指针指向的是一个被称为FILE的结构体,该结构一定含有文件描述符,因为在系统底层的接口中,只认文件描述符,才不管FILE结构体什么的,所以C语言的FILE结构体中一定含有系统底层的文件描述符。

7963fdd3e59049bca48b411ad7b10606.png


文件控制块FCB和文件结构体FILE的区别

072edfea64524387a02c5b1654590ad8.png


e675bfa59e8940128ca0efc93bd2db46.png


4.

这就是为什么我们自己打开的文件的文件描述符是从3开始的,因为012被三个文件指针中的文件描述符提前占用了

  1 #include <assert.h>
  2 #include <stdio.h>
  3 #include <sys/types.h>
  4 #include <sys/stat.h>
  5 #include <fcntl.h>
  6 #include <stdlib.h>
  7 #include <string.h>
  8 #include <unistd.h>
  9 #define FILE_NAME(number) "log.txt"#number
 25 int main()                                                                                                                                  
 26 {                                                                                                                                           
 27     int fd0 = open(FILE_NAME(1),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
 28     int fd1 = open(FILE_NAME(2),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
 29     int fd2 = open(FILE_NAME(3),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
 30     int fd3 = open(FILE_NAME(4),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666
 31     int fd4 = open(FILE_NAME(5),O_WRONLY | O_CREAT | O_TRUNC,0666);//设置文件起始权限为0666       
 32     printf("stdin->fd:%d\n",stdin->_fileno);
 33     printf("stdout->fd:%d\n",stdout->_fileno);
 34     printf("stderr->fd:%d\n",stderr->_fileno);                                                                                    
 35     printf("fd:%d\n",fd0);
 36     printf("fd:%d\n",fd1);
 37     printf("fd:%d\n",fd2);
 38     printf("fd:%d\n",fd3);
 39     printf("fd:%d\n",fd4);
 40     close(fd0);
 41     close(fd1);
 42     close(fd2);
 43     close(fd3);
 44     close(fd4);  
 45 }


f00267c62ba8465980292ea8b80a92a4.png


5.

内存中文件描述符,文件描述符表,文件控制块,进程控制块的关系如下图所示,文件描述符表,说白了就是一个存储指向文件控制块的指针的指针数组,而文件描述符就是这个指针数组的索引,进程控制块中会有一个指向文件描述符表的指针。通过文件描述符就可以找到对应的被打开的文件。

操作系统通过这些内核数据结构,将被打开的文件和进程联系起来。


3e1e4dfe012643199f8ab23afbaa9669.png


三、文件描述符的分配规则

1.关闭012文件描述符产生的现象(新打开文件的fd被赋值为0或1或2)


1.
当关闭0或2时,打印出来的log.txt对应的fd的值就是对应的关闭的0或2的值,而当关闭1时,显示器不会显示对应的fd的值。

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 #include <unistd.h>
  6 
  7 int main()
  8 {
  9     //close(0);
 10     //close(1);
 11     //close(2);                                                                                                                                        
 12     umask(0000);                                                                                                             
 13     int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//没有指明文件路径,默认在当前路径下,也就是当前进程的工作目录
 14     if(fd<0)                                                   
 15     {                                                          
 16         perror("open");                                        
 17         return 1;                                              
 18     }                                                          
 19                                                                
 20     printf("open fd:%d\n",fd);                                 
 21     close(fd);                                                 
 22     return 0;                                                  
 23 }    


image.gif


2.

实际上文件描述符在分配时,会从文件描述符表中的指针数组中,从小到大按照顺序找最小的且没有被占用的fd来进行分配,自然而然关闭0时,0对应存储的地址就会由stdin改为新打开的文件的地址,所以打印新的文件的fd值时,就会出现0。

关闭2也是这个道理,fd为2对应的存储的地址会由stderr改为新打开的文件的地址,所以在打印fd时,也就会出现2了。

a8e0277ff1284775832192da30b8bfae.png



3.

但是,当关闭1时,情况就有所不同了,要知道无论是printf还是fprintf等函数,在打印时,实际上都是打印到stdout,也就是对应的显示器文件中,而现在1对应的存储地址不再是显示器文件的地址了,而是变成新打开文件的地址,所以printf或fprintf等函数打印的内容全都到新打开文件中了,只不过由于缓冲区的刷新策略问题,没有立即显示到log.txt文件中。加上fflush(stdout)就可以在log.txt中看到相关内容了。

6f5ad6fa24f5417495b8b5028bbe73a8.png


  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 #include <unistd.h>
  6 
  7 int main()
  8 {
  9     //  close(0);
 10      close(1);
 11     //  close(2);
 12     umask(0000);
 13     int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//没有指明文件路径,默认在当前路径下,也就是当前进程的工作目录
 14     if(fd<0)
 15     {
 16         perror("open");
 17         return 1;
 18     }
 19 
 20     printf("open fd:%d\n",fd);// printf --> stdout
 21     fprintf(stdout,"open fd:%d\n",fd);// fprintf --> stdout 
 22 
 23     fflush(stdout);                                                                                                                                  
 24     close(fd);
 25     return 0;
 26 }


31024602729d4e70ac114bea595fc0a9.gif


4.

当关闭文件描述符1时,本来应该写到stdout对应的显示器文件中的内容,现在写到了log.txt文件中,这样的特性就叫做输出重定向。


2.stderr和stdout的区别


stdin — 标准输入文件

stdout — 标准输出文件

stderr — 标准错误输出文件

标准输入文件对应的终端是键盘,其余两个输出文件对应的终端是显示器,进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。


1.

所以大多数情况下,我们输出的数据都是到标准输出文件stdout中的,例如printf、fprintf、fputs、等函数,都会将内容输出到stdout(标准输出文件)中,最后显示到stdout对应的显示器上。


2.

在某些命令使用错误时,会将错误信息输出到stderr(标准错误输出文件)中。

例如下面的la指令使用错误,错误信息会被输出到stderr中,最后显示到stderr对应的终端显示器上。

4c5f73b6cf4a417bafed2f75f0aa47cf.png


四、重定向(上层用的fd始终不变,内核中更改fd对应的struct file*地址)

1.系统调用dup2进行重定向(新打开文件的struct file*地址复制到0/1/2文件地址中)

483dd071f37f4de69441e66618297cd2.png


1.

通过close关闭1,然后系统将新打开文件的地址分配到对应被关闭的1中的地址,然后打印到stdout的数据,就会被打印到新打开文件中,这样重定向的方式太搓了,完全可以利用系统调用dup2来进行重定向。


2.

如果用dup2来实现,显示到stdout中的内容,写到log.txt中,那就非常简单了,直接dup2(fd,1)即可,这样1对应的地址就被赋值为fd文件的地址了,也就实现了输出重定向。

5b004732b2c84dda8047c3c05a37d138.png

3.
从原来的输出到屏幕改为输出到文件中,这就叫做输出重定向。
而追加重定向的方式也比较简单,只要将文件打开方式中的O_TRUNC替换为O_APPEND即可。

    8 int main()                            
    9 {
   10     umask(0000);                                                           
   11     int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);//输出重定向
E> 12     int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);//追加重定向
   13     if(fd<0)                                                                                                                                       
   14     {                                                                                  
   15         perror("open");                                                                
   16         return 1;                                                                      
   17     }                                                                                  
   18                                                                                        
   19     dup2(fd,1);                                                                        
   20                                                                                        
   21     printf("open fd:%d\n",fd);// printf --> stdout                                     
   22     fprintf(stdout,"open fd:%d\n",fd);// fprintf --> stdout                            
   23                                                                                        
   24     const char* msg = "hello linux";                                                   
   25     write(1,msg,strlen(msg));//向显示器上write                                         
   26                                                                                        
   27     close(fd);                                                                         
   28     return 0;                                                                          
   29 }      


ef57fd484ff944868b89059327554b8e.gif

2.minishell中的重定向(shell派生的子进程利用dup2先完成重定向,之后再进行程序替换,彻底完成重定向工作)


1.

如果要在原来的minishell中实现重定向,其实也比较简单,完成以下步骤即可:

a.将重定向指令按照重定向符号分为指令和目标文件两部分。(记得过滤重定向符号后面的空格)

b.确定重定向的方式后,利用dup2系统调用让fork之后的子进程先完成重定向。

c.最后就是子进程进行相应指令的程序替换,彻底完成子进程的重定向工作。

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include <unistd.h>
    4 #include <sys/types.h>
    5 #include <sys/stat.h>
    6 #include <sys/wait.h>
    7 #include <assert.h>
    8 #include <string.h>
    9 #include <ctype.h>
   10 #include <fcntl.h>
   11 #include <errno.h>
   12 
   13 #define NUM 1024
   14 #define OPT_NUM 64
   15 
   16 #define NONE_REDIR 0
   17 #define INPUT_REDIR 1
   18 #define OUTPUT_REDIR 2
   19 #define APPEND_REDIR 3
   20 
   21 #define trimSpace(start) do{\
   22             while(isspace(*start))\
   23                 ++start;\
   24             }while(0)
   25 
   26 char command_line_array[NUM];
   27 char *myargv[OPT_NUM];//指针数组,每个指针指向命令行被切割后的字符串
   28 int lastcode=0;
   29 int lastsig=0;
   30 int redirType;
   31 char* redirFile;
   32 
   33 void commandCheck(char* command_line_array)
   34 {
   35     assert(command_line_array);
   36     char* start = command_line_array;
   37     char* end = command_line_array + strlen(command_line_array);//end指向\0
   38     
   39     while(start < end)
   40     {                                                                                                                                                                  
   41         if(*start == '>')
   42         {
   43             *start = '\0';
   44             start++;
   45             if(*start == '>')
   46             {
   47                 redirType = APPEND_REDIR;
   48                 start++;
   49             }
   50             else 
   51             {                                                                                                                                                          
   52                 redirType = OUTPUT_REDIR;
   53             }
   54             trimSpace(start);
   55             redirFile = start;
   56             break;
   57         }
   58         else if(*start == '<')
   59         {
   60             *start = '\0';
   61             start++;
   62             trimSpace(start);//过滤重定向符号后面的空格
   63             //填写重定向信息
   64             redirType = INPUT_REDIR;
   65             redirFile = start;
   66             break;
   67         }
   68         else 
   69         {
   70              start++;//如果没有重定向符号,会进入else分支语句,start一直++,while循环最后停止
   71         }
   72     }
   73 
   74 
   75 }
   76 int main()
   77 {   
   78     while(1)
   79     {
   80         redirType = NONE_REDIR;
   81         redirFile = NULL;
   82         errno = 0;//重新执行命令时,保证这些数据都被初始化。
   83 
   84         printf("[%s@%s 当前路径]#",getenv("USER"),getenv("HOSTNAME"));
   85         //获取用户输入
W> 86         char *s=fgets(command_line_array,sizeof(command_line_array)-1,stdin);//读取字节数最大为1023留出一个\0
   87         assert(s!=NULL);
   88         //将获取输入时输入的回车赋值成反斜杠0
   89         command_line_array[strlen(command_line_array)-1] = 0;
   90                                                                                                                                                                        
   91         //将命令行输入的字符串,进行字符串切割,以空格为分隔符
   92         //空格全都换成反斜杠0,或者用strtok
   93         // "ls -a -l -i" > "log.txt"
   94         // "cat" < "log.txt" 
   95         // "ls -a -l -i" >> "log.txt"
   96         
   97         //在命令字符串切割之前,首先需要以重定向符号为基准将命令行切割为目标文件和执行命令两部分,把重定向符号赋值为\0即可
   98         commandCheck(command_line_array);
   99 
  100         myargv[0]=strtok(command_line_array," ");
  101         int i=1;
  102         if(strcmp(myargv[0],"ls") == 0 && myargv[0]!= NULL)//我们自己在ls的命令行参数表中手动加上执行颜色命令。
  103         {
  104             myargv[i++]=(char*)"--color=auto";
  105         }
  106         
W>107         while(myargv[i++]=strtok(NULL," "));
  108         
  109         // 如果是cd命令,不需要创建子进程,让shell进程执行cd命令就可以,本质就是执行系统接口chdir
  110         // 像这种不需要派生子进程执行,而是让shell自己执行的命令,我们称之为内建或内置命令。
  111         if(myargv[0] != NULL && strcmp(myargv[0],"cd")==0)
  112         {
  113             if(myargv[1] != NULL)
  114             {
  115                 chdir(myargv[1]);//将shell进程的工作目录改为cd的路径
  116                 continue;
  117             }
  118         }
  119         // 完成另一个内建命令echo的运行,保证$?可以运行
  120         if(myargv[0]!=NULL && myargv[1]!=NULL && strcmp(myargv[0],"echo")==0)
  121         {
  122             if(strcmp(myargv[1],"$?") == 0)
  123             {
  124                 printf("%d,%d\n",lastcode,lastsig);
  125             }
  126             else
  127             {
  128                 printf("%s\n",myargv[1]);
  129             }
  130             continue;//后面的代码无须继续执行,直接continue即可
  131         }
  132                                                                                                                                                                        
  133         // 最后以NULL结尾,切割的字符串中已经没有字符串时,函数返回NULL
  134 #ifdef DEBUG 
  135         for(int i=0;myargv[i],i++)
  136         {
  137             printf("myargv[%d]:%s\n",myargv[i]);
  138         }
  139 #endif 
  140         //执行命令
  141         pid_t id=fork();
  142         assert(id!=-1);
  143         if(id==0)
  144         {
  145             //子进程进行重定向
  146             switch(redirType)
  147             {
  148                 case NONE_REDIR:
  149                     //什么都不做即可
  150                     break;
  151                 case INPUT_REDIR:
  152                     {
  153                         int fd = open(redirFile,O_RDONLY);
  154                         if(fd < 0)
  155                         {
  156                             perror("open");
  157                             exit(errno);//文件打开失败,命令执行出现错误,没必要进行子进程的程序替换,直接终止子进程即可。
  158                         }
  159                         //重定向的文件已经成功打开
  160                         dup2(fd,0);
  161                     }
  162                     break;
  163                 case APPEND_REDIR:
  164                 case OUTPUT_REDIR:
  165                     {
  166                         umask(0000);
  167                         int flags = O_WRONLY | O_CREAT;
  168                         if(redirType == APPEND_REDIR) flags |= O_APPEND;
  169                         else flags |= O_TRUNC;
  170                         int fd = open(redirFile,flags,0666);
  171                         if(fd < 0)
  172                         {
  173                             perror("open");
  174                             exit(errno);//文件打开失败,命令执行错误,终止子进程。                                                                                     
  175                         }
  176                         //重定向的文件已经成功打开
  177                         dup2(fd,1);
  178                     }
  179                     break;
  180                 default:
  181                     printf("bug?");//重定向只设置了4种类型,现在出现第5种,可能出现了bug
  182                     break;
  183             }
  184             
  185             
  186             execvp(myargv[0],myargv);
  187             exit(1);//如果程序替换失败,直接让子进程退出
  188         }
  189         int status=0;
W>190         pid_t ret = waitpid(id,&status,0);
  191         assert(ret > 0);
  192         lastcode = ((status>>8) & 0xFF);
  193         lastsig = (status & 0x7F);
  194         
  195     }
  196     return 0;
  197 }

dee3b4958aa04319a55c74cdca85c71f.gif

2.

因为命令是子进程执行的,所以重定向的工作也一定是子进程来执行的,但是如何重定向,重定向的类型,重定向的目标文件,这些都是父进程来提供给子进程的。


3.

子进程的重定向是不会影响父进程的,因为进程具有独立性,在创建子进程时,会将父进程的pcb拷贝一份给子进程,除pcb外,mm_struct(虚拟地址空间),页表,文件描述符表等其实也都需要给子进程拷贝一份,所以进程之间是相互独立的,子进程的重定向不会影响父进程。


4.

在给子进程拷贝时,子进程继承了父进程的文件描述符表,但文件控制块是不需要继承的,因为文件控制块属于文件系统部分,而你的子进程或父进程这些东西是属于进程管理部分,这属于两个领域的知识,是不沾边的。

1e1d2b83030245409296f1c462b80e0b.png


5.

执行程序替换的时候,会不会影响曾经的子进程打开的文件呢?

其实是不会的,需要注意的是,无论是文件描述符表还是pcb等等结构,本质上都是内核数据结构,而子进程在进行程序替换时,替换的是代码和数据,并不影响内核数据结构,所以即使子进程进行了程序替换,但原先子进程打开的文件是不会受任何影响的。

260b464c421f4106b8b1ea2df0fe0590.png


五、Linux下一切皆文件


1.

不同的硬件的读写方法一定是不一样的,但在OS看来,一切设备和文件都是struct file内核数据结构,在管理对应的硬件时,虽然硬件的管理方法不在OS层,而是在驱动层,这也没有关系,只需要利用struct file结构体中的函数指针,调用对应的硬件的读写方法即可。


2.

vfs层是Linux内核中的一个软件层,可以使得我们不关心底层硬件读写方式的差别,只用struct file中的函数指针即可管理对应的硬件的读写方式。

1eca0ff5c87842738d276bb8c32a58e9.png


六、看看Linux内核源代码是怎么说的

1.下面是文件描述符表的固定个数


b70544847b8b479cbbd5bdd4d9e2feb5.png

2.下面是文件描述符表的扩展个数


9eb4abecda8d4dad803dfd5ee44aa17a.png

714c57fc7b68425db9f1fe579b7caf71.png


4.下面是文件控制块的具体内容


e9ca23e308f8448eb0107b4639982135.png















































































相关文章
|
6天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
24 3
|
6天前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
18 2
|
26天前
|
Linux
Linux系统之expr命令的基本使用
【10月更文挑战第18天】Linux系统之expr命令的基本使用
75 4
|
15天前
|
Linux 应用服务中间件 Shell
linux系统服务二!
本文详细介绍了Linux系统的启动流程,包括CentOS 7的具体启动步骤,从BIOS自检到加载内核、启动systemd程序等。同时,文章还对比了CentOS 6和CentOS 7的启动流程,分析了启动过程中的耗时情况。接着,文章讲解了Linux的运行级别及其管理命令,systemd的基本概念、优势及常用命令,并提供了自定义systemd启动文件的示例。最后,文章介绍了单用户模式和救援模式的使用方法,包括如何找回忘记的密码和修复启动故障。
38 5
linux系统服务二!
|
15天前
|
Linux 应用服务中间件 Shell
linux系统服务!!!
本文详细介绍了Linux系统(以CentOS7为例)的启动流程,包括BIOS自检、读取MBR信息、加载Grub菜单、加载内核及驱动程序、启动systemd程序加载必要文件等五个主要步骤。同时,文章还对比了CentOS6和CentOS7的启动流程图,并分析了启动流程的耗时。此外,文中还讲解了Linux的运行级别、systemd的基本概念及其优势,以及如何使用systemd管理服务。最后,文章提供了单用户模式和救援模式的实战案例,帮助读者理解如何在系统启动出现问题时进行修复。
37 3
linux系统服务!!!
|
23天前
|
Web App开发 搜索推荐 Unix
Linux系统之MobaXterm远程连接centos的GNOME桌面环境
【10月更文挑战第21天】Linux系统之MobaXterm远程连接centos的GNOME桌面环境
193 4
Linux系统之MobaXterm远程连接centos的GNOME桌面环境
|
25天前
|
Linux 测试技术 网络安全
Linux系统之安装OneNav个人书签管理器
【10月更文挑战第19天】Linux系统之安装OneNav个人书签管理器
41 5
Linux系统之安装OneNav个人书签管理器
|
24天前
|
运维 监控 Linux
Linux系统之部署Linux管理面板1Panel
【10月更文挑战第20天】Linux系统之部署Linux管理面板1Panel
75 3
Linux系统之部署Linux管理面板1Panel
|
6天前
|
安全 网络协议 Linux
本文详细介绍了 Linux 系统中 ping 命令的使用方法和技巧,涵盖基本用法、高级用法、实际应用案例及注意事项。
本文详细介绍了 Linux 系统中 ping 命令的使用方法和技巧,涵盖基本用法、高级用法、实际应用案例及注意事项。通过掌握 ping 命令,读者可以轻松测试网络连通性、诊断网络问题并提升网络管理能力。
24 3
|
9天前
|
安全 Linux 数据安全/隐私保护
在 Linux 系统中,查找文件所有者是系统管理和安全审计的重要技能。
在 Linux 系统中,查找文件所有者是系统管理和安全审计的重要技能。本文介绍了使用 `ls -l` 和 `stat` 命令查找文件所有者的基本方法,以及通过文件路径、通配符和结合其他命令的高级技巧。还提供了实际案例分析和注意事项,帮助读者更好地掌握这一操作。
26 6