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



一、关于文件的重新认识

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一样。
其他打开方式是以二进制形式打开不怎么用到这里不做详细说明。

在这里插入图片描述

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 } 

fprintffopenfclosefgetsputs等都是有关文件操作的函数常用的文件打开方式有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有两种使用形式一种是只有两个参数一种是有三个参数第二种是针对打开文件不存在的情况需要我们创建一个文件并设定文件的初始权限第一种是针对文件存在的情况无须设定文件初始权限。

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

在这里插入图片描述

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

在这里插入图片描述

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 }

在这里插入图片描述
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 }

在这里插入图片描述

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

如果不设置文件起始权限则创建出来的文件的权限就会是乱码。
在这里插入图片描述

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

2.2 write

1.
在C语言中的写入函数有fputsfprintffwrite等但在系统级别写入接口只有一个write

在这里插入图片描述

 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

在这里插入图片描述

在这里插入图片描述
3.
只将写入的内容改为aaaa打印出来的log.txt的内容就发生了覆盖式写入的现象而不是先将文件原有内容清理然后在重新写入。
在C语言中如果再次以写的方式打开文件会自动将原先文件中的内容清理掉重新向文件写入内容。
自动清空原有数据实际上是通过open系统调用中的第三个宏参数O_TRUNC来实现的。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
4.
所以C语言中打开文件时使用的打开方式为w在底层的open接口中要用三个宏参数O_WRONLYO_CREATO_TRUNC来实现。
C语言中的a打开方式在系统底层实现上只需要将O_TRUNC替换为O_APPEND即可。
可见库函数和系统调用的关系本质就是库函数封装系统调用。

在这里插入图片描述

2.3 read

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

在这里插入图片描述
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 }

在这里插入图片描述
2.
fd值为-1表示文件打开时出现错误返回正数表示文件打开成功。
标准输入标准输出标准错误输出是系统默认打开的三个标准文件系统自定义的三个文件指针stdin、stdout、stderr中一定含有文件描述符。

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

在这里插入图片描述

在这里插入图片描述
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 }

在这里插入图片描述

5.
内存中文件描述符文件描述符表文件控制块进程控制块的关系如下图所示文件描述符表说白了就是一个存储指向文件控制块的指针的指针数组而文件描述符就是这个指针数组的索引进程控制块中会有一个指向文件描述符表的指针。通过文件描述符就可以找到对应的被打开的文件。
操作系统通过这些内核数据结构将被打开的文件和进程联系起来。

在这里插入图片描述

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

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 }    

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

  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 }

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

2.stderr和stdout的区别

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

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

2.
在某些命令使用错误时会将错误信息输出到stderr标准错误输出文件中
例如下面的la指令使用错误错误信息会被输出到stderr中最后显示到stderr对应的终端显示器上。

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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 }      

4.
从原来的键盘中读取数据改为从文件fd中读取数据这就叫做输入重定向。
文件log.txt中的内容作为输入重定向重新输出到显示器中即使fgets获取的方式是stdin也没有关系因为我们使用dup2将stdin中的地址改为了文件log.txt的地址

  8 int main()
  9 {
 10     umask(0000);
 13     int fd = open("log.txt",O_RDONLY);//输入重定向
 14     if(fd<0)
 15     {
 16         perror("open");
 17         return 1;
 18     }
 19 
 20     dup2(fd,0);//由键盘读取改为从fd文件中读取                                                                                                        
 21     char line[64];                  
 22     while(1)                                                                                                                  
 23     {                                                                                                                         
 24         printf("<");                                                                                                          
 25         if(fgets(line,sizeof(line),stdin)==NULL) break;                                                                       
 26         printf("%s",line);                                                                                                    
 27     }     
 28 }       

在这里插入图片描述

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 }

在这里插入图片描述

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

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

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

在这里插入图片描述

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

在这里插入图片描述

五、Linux下一切皆文件

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

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

在这里插入图片描述

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

1.下面是文件描述符表的固定个数
在这里插入图片描述

2.下面是文件描述符表的扩展个数
在这里插入图片描述
3.下面是云服务器下的文件描述符表的最多打开文件个数。
在这里插入图片描述
4.下面是文件控制块的具体内容
在这里插入图片描述

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux

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