> 技术文档 > 【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)

【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)


小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述


目录

    • 前言
    • 一、重定向
      • 文件描述符的分配规则
      • 文件的重定向
      • 系统调用dup2
    • 二、给shell程序添加重定向
      • 准备工作
      • 铺垫
      • 添加重定向
      • 源文件
      • 思考:程序替换会不会影响文件的重定向
    • 三、标准输出1和标准错误2的区别
    • 四、如何理解一切皆文件
    • 总结

前言

【linux】linux基础IO(一)(c语言文件接口、文件系统调用open,write,close、文件fd)——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】linux基础IO(二)(文件的重定向以及dup2的使用,给shell程序添加重定向,如何理解一切皆文件)


一、重定向

文件描述符的分配规则

  1. 如下,小编打开一个不存在文件以写方式打开,此时就会在当前路径下创建一个文件,并且小编将open的返回值fd进行打印观察,并向文件中写入信息
#include #include #include #include #include #include #define filename \"log.txt\"int main(){ int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd < 0) { perror(\"open\"); return 1; } printf(\"fd: %d\\n\", fd); const char* message = \"hello linux\\n\"; int cnt = 5; while(cnt--) { write(fd, message, strlen(message)); } close(fd); return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
此时文件描述符也就是一个数组下标,即为3,我们知道0是标准输入,1是标准输出,2是标准错误

  1. 那么新创建的文件对应的文件描述符究竟可以不可以是0,1或者2呢?下面小编在打开文件前,先关闭对应的文件描述符2观察一下现象
#include #include #include #include #include #include #define filename \"log.txt\"int main(){ close(2); int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd < 0) { perror(\"open\"); return 1; } printf(\"fd: %d\\n\", fd); const char* message = \"hello linux\\n\"; int cnt = 5; while(cnt--) { write(fd, message, strlen(message)); } close(fd); return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
此时由于文件描述符2被我们在打开文件前进行了close关闭,close会把文件描述符2所对应的文件描述符表中对应的数组下标为2上的指针进行置空,所以新创建的文件在操作系统内对应的文件打开对象的地址就可以填到文件描述符表中对应的数组下标为2的位置处了,所以此时open返回新打开的文件所对应的数组下标2作为新打开文件的文件描述符,同理1也是,0也是

  1. 所以文件描述符对应的分配规则是:从下标0开始,寻找最小的没有被使用的数组位置,它的下标就是新文件的文件描述符

文件的重定向

  1. 我们可以利用close关闭文件实现文件的重定向,让本该输出到显示器上的信息输出到文件中
  2. 下面我们先看不进行重定向,先向显示器输出信息
#include #include #include #include #include #include #define filename \"log.txt\"int main(){ int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd < 0) { perror(\"open\"); return 1; } const char* message = \"hello linux\\n\"; int cnt = 5; while(cnt--) { write(1, message, strlen(message)); } close(fd); return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
由于小编使用了write直接向文件描述符1对应的显示器文件输出信息,并且对于文件仅仅是打开关闭,并没有向文件中写入信息,所以此时信息输出到显示器上,文件中没有信息

  1. 那么此时我们使用close将文件描述符1关闭,此时文件描述符表的数组下标为1的位置的指针就被置空,根据文件描述符的分配规则:从数组下标0开始,依次寻找最小的没有被使用过的数组位置,它的下标就是新文件的文件描述符,所以新创建的文件对应的文件对象的地址就会被填入数组下标1,此时open新创建的文件的文件描述符就为1,所以我们再使用write对文件描述符1进行写入信息就不会写入到显示器文件上,而是会写入到新创建的文件中
#include #include #include #include #include #include #define filename \"log.txt\"int main(){ close(1); int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd < 0) { perror(\"open\"); return 1; } const char* message = \"hello linux\\n\"; int cnt = 5; while(cnt--) { write(1, message, strlen(message)); } close(fd); return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
此时信息没有被写入到显示器文件中,而是被写入到了新创建的文件中

系统调用dup2

  1. 上述进行文件重定向的方式有很多的局限性,如果我想要进行多个重定向,那么就需要重复的打开和关闭工作,并且在可读性上也不是很好,所以我们希望使用一个系统调用dup2可以直接完成对文件的重定向的工作
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  2. dup2可以对文件进行重定向,第一个参数是oldfd,第二个参数是newfd,下面我们来理解一下oldfd和newfd
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  3. dup2是将文件进行重定向,本质其实就是文件描述符表上的文件打开对象的指针的拷贝,以上图为例,我们要将log.txt文件进行重定向到文件描述符1的位置,所以oldfd指的是要进行重定向的文件的文件描述符,对应上图为fd,newfd指的是被重定向的文件的文件描述符,对应上图为1,所以我们再通俗点来讲,针对拷贝,oldfd指的是被拷贝前旧空间的文件描述符,newfd指的是拷贝后新空间的文件描述符
  4. 那么此时我们有了dup2之后,就不再需要在打开文件之前关闭进行被重定向的文件了,通常来讲由于dup2是进行的指针的拷贝,所以进行重定向的文件描述符我们可以使用close进行关闭,因为它已经被重定向到了被重定向的文件描述符的对应位置
#include #include #include #include #include #include #define filename \"log.txt\"int main(){ int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd < 0) { perror(\"open\"); return 1; } dup2(fd, 1); close(fd); const char* message = \"hello linux\\n\"; int cnt = 5; while(cnt--) { write(1, message, strlen(message)); } return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)

  1. 上面演示的是输出重定向,下面演示一下追加重定向,其实追加重定向和输出重定向的本质都是一样的,只不过追加重定向的选项不再是O_TRUNC了,而是O_APPEND选项
#include #include #include #include #include #include #define filename \"log.txt\"int main(){ int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666); if(fd < 0) { perror(\"open\"); return 1; } dup2(fd, 1); close(fd); const char* message = \"hello linux\\n\"; int cnt = 5; while(cnt--) { write(1, message, strlen(message)); } return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
此时就完成了追加重定向,让本该向文件描述符1对应的显示器文件进行显示的,改为了向log.txt文件中进行追加

【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)

  1. 接下来小编讲解一下输入重定向,首先我们先看一下向文件描述符0对应键盘文件进行read读取输入,read的返回值是实际读取的字符个数,由于文件的字符串中没有以\\0进行结尾,c语言的字符串需要以\\0结尾,所以我们可以利用这个判断是否读取成功并且同时将字符串的结尾添加上\\0
  2. read需要依次传入fd,缓冲区,缓冲区的大小,由于我们需要在读取的字符串的结尾添加一个\\0,如果实际读取的大小超过了缓冲区的大小,那么会将缓冲区装满,此时就没有空间在读取字符串的结尾添加\\0了,所以实际传入的缓冲区的大小我们进行减一
#include #include #include #include #include #include int main(){ char buffer[1024]; ssize_t s = read(0, buffer, sizeof(buffer) - 1); if(s > 0) { buffer[s] = \'\\0\'; printf(\"echo # %s\\n\", buffer); } return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
当我们运行起来我们的程序之后,此时会卡住,因为我们的read需要向文件描述符0对应的键盘文件进行读取,由于小编并没有使用键盘进行任何输入,所以此时会卡住,等待键盘资源准备就绪,下面小编使用键盘进行输入
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
此时我们输入的内容就会被read读取并且写到我们的缓冲区buffer进行打印了,为什么echo之后打印多了一个换行,因为小编在输入aaaaaaaaaa的时候按下了回车换行之后才结束读取,回车换行也是一个字符,也一并被read进行读取了

  1. 此时小编打开一个已经存在的文件log.txt,里面存放字符aaaaaaaaa,那么使用dup2将其重定向到文件描述符0中,此时文件描述符0中存放的就是log.txt的文件打开对象的地址了,所以此时使用read向文件描述符0进行读取就不再向键盘文件进行读取了,而是转为向log.txt文件进行读取,由于小编在log.txt文件中已经写入了aaaaaaaaa,所以此时会直接进行read读取并printf打印
#include #include #include #include #include #include #define filename \"log.txt\"int main(){ int fd = open(filename, O_RDONLY); if(fd < 0) { perror(\"open\"); return 1; } dup2(fd, 0); close(fd); char buffer[1024]; ssize_t s = read(0, buffer, sizeof(buffer) - 1); if(s > 0) { buffer[s] = \'\\0\'; printf(\"echo # %s\\n\", buffer); } return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
此时不再向键盘文件中进行读取,而是向log.txt文件中进行读取了,此时就完成了输入重定向

  1. 那么对应的此时输入重定向<也是这样的原理,当我们在命令行中仅仅输入cat的时候,它会等待我们键盘输入,键盘输入之后,此时cat会将从键盘文件读取的数据进行打印,那么当我们使用输入重定向<让cat从log.txt文件中进行读取,此时cat就不再会从键盘文件中进行读取了,转而从log.txt文件中进行读取
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  2. 但是在实际中我们并不这样使用,因为上面的都是系统调用,我们在实际使用的大多数使用c语言的库函数进行打印,那么我们看一下在c语言的库函数中能否使用dup2能否完成文件的重定向呢?
#include #include #include #include #include #include #define filename \"log.txt\"int main(){ int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666); if(fd < 0) { perror(\"open\"); return 1; } dup2(fd, 1); close(fd); printf(\"fd: %d\\n\", fd); printf(\"hello printf\\n\"); fprintf(stdout, \"hello fprintf\\n\"); return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
此时c语言的打印函数就不再向显示器文件上打印,而是向我们进行追加重定向的文件中进行输出,所以对于c语言的库函数中的文件重定向工作我们同样也可以使用dup2来完成

  1. 那么对于输出重定向>,追加重定向>>以及输入重定向的原理我们就大体了解了,其实就是进行了dup2文件重定向的操作,文件重定向的本质就是进行文件描述符表中文件描述符对应的文件打开对象的地址的拷贝
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)

二、给shell程序添加重定向

准备工作

  1. 为了更好的给读者友友带来深刻的理解重定向,下面小编带领大家手搓一个简单的重定向,由于之前小编编写过一个自定义shell程序,所以本文会在小编编写的shell程序的基础上进行讲解,不熟悉自定义shell的读者友友可以点击下方蓝字链接进行学习

【linux】自定义shell——bash命令行解释器小程序详情请点击<——

铺垫

  1. 那么我们在当前目录下新建一个目录shell,将原shell程序的源文件myshell.c以及对应的自动化编译工具makefile拷贝至当前的新建目录shell
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  2. 同时要创建出两个变量,rdirfilename用于存储文件名,rdir用于表示重定向方式,对于重定向方式我们设置四个宏,NONE为-1表示文件还未打开(用于初始化rdir),IN_RDIR为0,表示输入重定向(对应命令行为),APPEND_RDIR为2,表示追加重定向(对应命令行为<<)
#define NONE -1#define IN_RDIR 0#define OUT_RDIR 1#define APPEND_RDIR 2const char* rdirfilename = NULL;int rdir = NONE;

添加重定向

  1. 其实给shell程序添加重定向并不困难,只需要根据> >> <分隔字符串,获取文件名,获取重定向方式,然后在子进程执行程序替换前根据不同重定向方式,进行打开文件并且dup2重定向即可
  2. 那么分隔字符串获取文件名,获取重定向方式的操作我们就放在interact交互模块,并且将该操作封装成一个函数check_rdir
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  3. 例如:ls -a -l > log.txt,先遍历字符串找到重定向选项,先区分出输出重定向和输入重定向,由于追加重定向和输出重定向有重复的>字符,所以要先判断输出重定向然后在输出重定向内部在判断追加重定向,这时候就可以将重定向选项给给rdir,由于在命令行中的重定向选项与文件名中间如果有空白字符,那么在选项中可能有空白字符,所以我们要分隔空白字符和文件名应该跳过这些空白字符,那么我们就可以使用isspace进行判断分隔空白字符,如果判断后是空白字符,那么返回一个大于0的数,如果判断后不是空白字符那么返回0,将空白字符跳过后,那么此时pos的位置就是文件名的首元素的位置,将其交给rdirfilename即可
  4. 那么我们针对普通命令就可以做重定向的工作了,在fork的子进程中根据rdir重定向选项区分究竟是输出重定向,输入重定向还是追加重定向,之后在对应的判断中使用不同的打开方式打开,最后open打开返回的文件描述符fd使用dup2重定向到对应的标准输入对应的文件描述符0(输入重定向对应)或标准输出对应的文件描述符1(输出重定向和追加重定向对应)即可

源文件

#include #include #include #include #include #include #include #include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024#define DELIM \" \"#define ARGC_SIZE 32#define EXIT_CODE 11#define NONE -1#define IN_RDIR 0#define OUT_RDIR 1#define APPEND_RDIR 2int lastcode = 0;char commandline[LINE_SIZE];char* argv[ARGC_SIZE];char pwd[LINE_SIZE];char myenv[LINE_SIZE];const char* rdirfilename = NULL;int rdir = NONE;char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}void getpwd(){ getcwd(pwd, sizeof(pwd));}void check_rdir(char* cmd){ //ls -a -l > log.txt 我们考虑的重定向方式与文件名中间规范书写默认带空格 char* pos = cmd; while(*pos) { if(*pos == \'>\') { if(*(pos + 1) == \'>\') { // >> 追加重定向 // ls -a -l >> log.txt *(pos++) = \'\\0\'; *(pos++) = \'\\0\'; while(isspace(*pos)) pos++; rdirfilename = pos; rdir = APPEND_RDIR; } else { // > 输出重定向 *(pos++) = \'\\0\'; while(isspace(*pos)) pos++; rdirfilename = pos; rdir = OUT_RDIR; } break; } else if(*pos == \'<\') { //log.txt < log1.txt *(pos++) = \'\\0\'; while(isspace(*pos)) pos++; rdirfilename = pos; rdir = IN_RDIR; break; } pos++; }}void interact(char* cline, int size){ getpwd(); printf(FORMATE, getusrname(), gethostname(), pwd); fgets(cline, size, stdin); //ls -a -l\\n\\0 cline[strlen(cline) - 1] = \'\\0\'; check_rdir(cline);}int splitstring(char* cline, char* _argv[]){ int i = 0; _argv[i++] = strtok(cline, DELIM); while(_argv[i++] = strtok(NULL, DELIM)); if(_argv[0] == NULL) { return 0; } return i - 1;}void normalexcute(char* _argv[]){ int id = fork(); if(id < 0) { perror(\"fork\"); return; } else if(id == 0) { int fd = 0; if(rdir == IN_RDIR) { fd = open(rdirfilename, O_RDONLY); dup2(fd, 0); } else if(rdir == OUT_RDIR) { fd = open(rdirfilename, O_CREAT | O_WRONLY | O_TRUNC, 0666); dup2(fd, 1); } else if(rdir == APPEND_RDIR) { fd = open(rdirfilename, O_CREAT | O_WRONLY | O_APPEND, 0666); dup2(fd, 1); } execvp(_argv[0], _argv); exit(EXIT_CODE); } else { //id > 0 int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { lastcode = WEXITSTATUS(status); } }}int buildcommand(int _argc, char* _argv[]){ if(_argc == 2 && strcmp(_argv[0], \"cd\") == 0) { chdir(_argv[1]); getpwd(); sprintf(getenv(\"PWD\"), \"%s\", pwd); return 0; } else if(_argc == 2 && strcmp(_argv[0], \"export\") == 0) { sprintf(myenv, \"%s\", _argv[1]); putenv(myenv); return 0; } else if(_argc == 2 && strcmp(_argv[0], \"echo\") == 0) { if(strcmp(_argv[1], \"$?\") == 0) { printf(\"%d\\n\", lastcode); lastcode = 0; } else if(*_argv[1] == \'$\') { char* ret = getenv(_argv[1] + 1); if(ret) { printf(\"%s\\n\", ret); } } else { printf(\"%s\\n\", _argv[1]); } return 0; } if(strcmp(_argv[0], \"ls\") == 0) { _argv[_argc++] = \"--color\"; _argv[_argc] = NULL; } return 1;}int main(){ while(1) { //初始化 rdirfilename = NULL; rdir = NONE; //交互 interact(commandline, sizeof(commandline)); //解析命令行 int argc = splitstring(commandline, argv); if(argc == 0) { continue; } //内建命令的执行 int ret = buildcommand(argc, argv); //普通命令的执行 if(ret) { normalexcute(argv); } } return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
那么此时我们的程序就可以进行文件的重定向操作了

思考:程序替换会不会影响文件的重定向

我们在子进程创建后,对子进程进行了重定向的操作,可是子进程执行普通命令要进行程序替换的呀!那么程序替换不会影响文件的重定向吗?即程序替换之后会不会影响原有进程历史打开的文件和各种重定向关系呢?

  1. 从上面的运行结果中我们可以很明显的观察出来,程序替换之后不会影响原有进程历史打开的文件和各种重定向关系,可是为什么呢?
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  2. 关系如上图,其实每当有文件打开的时候,操作系统都会为这个文件创建一个内核级别的文件打开对象,并且在对应的文件和进程关联的文件描述符表中从0开始,寻找一个最小的没有被使用过的数组位置,在这个数组下标中存放文件打开对象的地址,数组的下标作为打开文件的文件描述符
  3. 之前小编讲解的进程的内核数据结构有task_struct(PCB),mm_struct(进程地址空间),页表,今天小编会再引入两个,文件描述符表,文件打开对象也是进程的内核数据结构,我们知道程序替换只是在物理空间上进行的代码和数据的替换,并不会创建新进程,并且对应的调整页表和进程地址空间的字段,对于内核数据结构并不会有太大的影响
  4. 所以进程历史打开的各种文件与进行的各种重定向关系,都和未来的进程程序替换无关,程序替换不影响文件访问

三、标准输出1和标准错误2的区别

  1. 在之前我们仅仅知道标准输出1和标准错误2对应都是同一个显示器,那么它们两个究竟有什么区别呢?下面小编来进行讲解一下
  2. 首先我们知道c语言中的stdout也就是标准输出对应的文件描述符fd是1,stderr是标准错误对应的文件描述符是2,那么我们接下来就以c语言的stdout和stderr进行讲解,下面小编使用fprintf分别向stdout和stderr中进行打印正常信息以及错误信息
#include #include #include #include #include #include int main(){ fprintf(stdout, \"normal message\\n\"); fprintf(stdout, \"normal message\\n\"); fprintf(stdout, \"normal message\\n\"); fprintf(stdout, \"normal message\\n\"); fprintf(stdout, \"normal message\\n\"); fprintf(stderr, \"error message\\n\"); fprintf(stderr, \"error message\\n\"); fprintf(stderr, \"error message\\n\"); fprintf(stderr, \"error message\\n\"); fprintf(stderr, \"error message\\n\"); return 0;}

运行结果如下
【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
由于stdout和stderr对应的都是显示器文件,所以运行程序将正常信息与错误信息都打印到屏幕上,这没毛病

  1. 那么接下来小编要进行一系列文件的重定向操作进行讲解学习了,首先我们将程序运行结果输出重定向到normal.log文件中,那么此时就会出现仅仅是stdout标准输出的正常信息向文件中进行了传输,而stderr标准错误的错误信息则是打印在了显示器上了
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  2. 其实原因是由于标准输出1默认会重定向到文件中,而标准错误2并不会默认重定向到文件中,其实本质是由于重定向选项前默认被添加了文件描述符1,即./mytest 1> normal.log,即将本该输出文件描述符1对应就是显示器的信息输出到我们指定的文件normal.log中,标准错误2并不会默认重定向到文件中,所以此时会出现stdout标准输出1的正常信息向文件中进行了传输,而stderr标准错误2的错误信息则是打印在了显示器上了
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  3. 那么如果我们想要将标准输出和标准错误的信息区分开,分别写入到两个文件中又该如何做呢?这时候我们将标准错误也一并重定向到不同文件中即可./mytest 1> normal.log 2> err.log
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  4. 这样就是实现了正常信息和错误信息的分流到不同的文件,便于程序员查看
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  5. 如果我们想要将正常信息和错误信息都放到同一个文件又该如何做呢?关系示意图如上,即进行./mytest 1> all.log 2>&1 对于./mytest 1> all.log 我们很好理解,将mytest中的stdout标准输出1重定向到all.log文件中,此时文件描述符表中文件描述符1上存储的文件打开对象的地址修改为all.log对应的文件打开对象的地址,此时向文件描述符1输出的正常信息就会被输出到all.log文件中
  6. 那么对于后面的 2>&1 其实就是将标准错误2重定向到标准输出1上,此时文件描述符表中文件描述符2中存储的文件打开对象的地址就被拷贝为文件描述符1中存储的文件打开对象的地址,由于文件描述符表中文件描述符1上存储的文件打开对象的地址已经修改为了all.log对应的文件打开对象的地址,所以文件描述符表中文件描述符2中存储的文件打开对象的地址就被拷贝为all.log的文件打开对象的地址,所以此时向文件描述符2中输出的错误信息就会被输出到all.log文件中,所以此时我们便实现了将正常信息和错误信息都放到同一个文件
    【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)
  7. 上面操作中的第一个1都可以省略./mytest 1> all.log 2>&1,可以省略为./mytest > all.log 2>&1。./mytest 1> normal.log 2> err.log,可以省略为./mytest > normal.log 2> err.log。./mytest 1> normal.log可以省略为./mytest > normal.log

四、如何理解一切皆文件

【linux】linux基础IO(二)(文件的重定向,dup2的使用,给shell程序添加重定向,如何理解一切皆文件)

  1. 在前面文章的讲解中小编经常提到,在linux中一切皆文件,任何外设我们都可以将其看作一个文件,诸如磁盘文件,显示器文件,键盘文件,网卡文件等等,那么我们该如何理解呢?
  2. 首先我们要访问的硬件除了CPU和内存,其余设备大部分都是外设,任何一个外设都要进行IO,即读写操作,磁盘进行读写文件操作,显示器进行写操作,不进行读操作,所以读操作我们可以设置为空,键盘进行读操作,不进行写操作,所以写操作我们可以设置为空,网卡要接收以及发送数据,那么也就是要进行对应的读写操作,所以这些外设虽然读写方法肯定不相同,但是这些外设都要进行IO,既然是要进行IO就要有对应的驱动程序,驱动程序中包含着不同外设对应不同的读写方法,但是虽然各个底层硬件(外设)虽然每个的读写方法都各不相同,但是都要进行读写,所以我们可以将外设的读写抽象出来作为一个共同属性
  3. 那么此时每一个外设我们就可以将其看作一个虚拟的文件,由于一个进程对应多个文件,当文件被打开的时候,此时操作系统势必会有多个被打开的文件,那么多个被打开的文件就要被管理起来,如何管理?先描述再组织,使用一个结构体描述一个打开的文件的诸多信息,例如:在磁盘的什么位置,基本属性,权限,大小,读写位置,谁打开的,以及文件的内核缓冲区信息,同时再添加读写方法作为共同的属性,同时再使用一个指针指向struct operation_func{}这个结构体,这个结构体中包含着两个函数指针分别读写方法的函数指针即writep以及readp,这两个函数指针会指向外设对应的驱动程序中的对应外设各自的读写方法
  4. 那么此时每一个外设就有了对应的文件打开对象,这些文件打开对象会以双链表的形式组织起来,这样就形成了一个虚拟文件系统,打开的文件是要和进程关联起来的,如何关联?将这些文件打开对象的地址放入一个文件描述符表中即可,同时进程PCB中存放这个文件描述表的地址,此时进程和文件就关联起来了
  5. 当我们想要访问外设的时候,以访问外设的读方法为例,此时我们就可以通过进程PCB中的文件描述符表的指针找到文件描述符表,通过文件描述符fd访问指定数组下标位置对一个的文件打开对象的地址,此时就可以访问文件打开对象,而文件打开对象中就包含有对应的struct operation_func{}的地址,此时就可以访问struct operation_func{},访问其中的readp这个读方法,进行调用这个读方法函数指针就会调用对应外设对应驱动程序的读独属于这个外设的读方法,此时上层就不再需要关心我要访问的是各自外设的读方法和写方法究竟是放在哪里以及要调用哪一个读方法和写方法,针对不同外设进行访问要调用的读方法和写方法转化为仅需要调用统一的readp以及writep,这样就实现了上层和底层的解耦,其实这也就是c语言层面的多态,针对不同的外设都可以进行调用统一的readp以及writep接口,由于不同外设对应的驱动程序中对应的读写方法不同,所以不同外设对应的readp以及writep接口的指向也就不一样,所以此时针对不同的外设就可以调用统一的readp以及writep接口访问调用不同外设对应的驱动程序中对应的读写方法,也就是实现了多态的指向谁调用谁
  6. 所以通过上面的原理讲解,此时在linux中,任何一个底层硬件(外设)我们都可以将其看作一个虚拟文件,即linux下一切皆文件
  7. 同时这也是一种面向对象的思想,我只需要关心我是什么对象然后调用对应的接口方法即可,对于底层要调用的接口的实现我并不关心,同样的任何一门高级语言都是采用的面向对象编程,如果仅仅几门语言中有面向对象,那么我们可以说这是那几门语言的特性,但是当任何一门的高级语言中都采用的是面向对象编程,那么此时面向对象就是一种时代的必然

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!