『 Linux 』重定向 Redirect(万字)

文章目录

    • 🧸 什么是重定向
      • 🐡 文件描述符的分配规则
      • 🐡 重定向在日常使用中的简单示例
    • 🧸 实现重定向的底层机制
      • 🐡 dup2()
      • 🐡 利用dup2()实现重定向
    • 🧸 在自定义Shell当中添加重定向功能
      • 🐡 重定向与进程替换
    • 🧸 标准输出重定向与标准错误重定向


🧸 什么是重定向

请添加图片描述

重定向是计算机编程和操作系统重的一个概念;

一般指的是改变数据流的方向或将程序的输出从一个位置转移到另一个方式;

一般在shell当中的重定向有

  • 标准输出重定向
  • 标准错误重定向
  • 标准输入重定向

🐡 文件描述符的分配规则

请添加图片描述

在博客 『 Linux 』基础IO/文件IO (万字) 当中介绍了在Linux当中若是打开了一个文件则对应的将会为该文件分配一个int类型的文件描述符用来保证该文件的唯一性;

同时介绍了对应的文件描述符的分配规则;

  • 存在一段代码

    #define FILENAME "log.txt"
    
    int main() {
      close(0);
    //   close(1);
    //   close(2);
      int fd = open(FILENAME, O_CREAT | O_TRUNC | O_WRONLY, 0666);
      printf("%d\n", fd);
      return 0;
    }
    

在该程序当中,将以 O_CREAT | O_TRUNC | O_WRONLY 的方式打开文件,且对应的创建文件时的权限为0666;

同时运行三次程序,分别使用close()接口关闭文件描述符0,1,2;

当关闭文件描述符0并运行该程序时将打印0;

当关闭文件描述符1并运行该程序时将不作打印(文件描述符1对应的是标准输出,而printf正是标准输出操作);

当关闭文件描述符2并运行该程序时将打印2;

这也验证了文件描述符的分配规则:

  • 文件描述符的分配规则为为新打开的文件分配最小的且未被使用的文件描述符;

其他关于文件描述符的内容在此不再进行赘述;


🐡 重定向在日常使用中的简单示例

请添加图片描述

在上文当中提到重定向的种类;

存在一个目录;

在该目录当中的shell命令行当中使用ls对应的将打印出当前目录下的所有目录与文件;

$ ls
makefile  mytest  test.c

其中mytest文件是由test.c文件编译而来;

具体的test.c文件内容暂时不重要;

  • 输出重定向

    ls > log.txt
    

    在这行命令当中,使用了ls将当前目录的目录文件显示到显示屏当中;

    然而在这里使用了重定向>,对应的信息并没有显示到显示屏当中;

    而是被写入至重定向是所指定的log.txt文件当中;

    同时在上一篇博客当中提到;

    实际上>>>重定向的区别即为打开文件的方式不同;

    >将以只写的方式打开文件,而>>将以追加的方式打开文件;

    $ cat log.txt 
    log.txt
    makefile
    mytest
    test.c
    

    无论这条命令被执行多少次,对应的当使用cat将文件当中的内容显示至显示屏当中都会只有一次命令的内容(打开文件时使用了O_TRUNC选项);

  • 追加重定向

    追加重定向与输出重定向不同;

    在上文当中提到追加重定向与输出重定向的打开文件的方式不同;

    ls >> log.txt
    

    故假设这里使用两次该命令;

    ls命令所显示出的目录信息将以追加的方式被重定向至log.txt文件当中;

    $ cat log.txt 
    log.txt
    makefile
    mytest
    test.c
    log.txt
    makefile
    mytest
    test.c
    
  • 输入重定向

    以上文中的例子为例,log.txt文件中当前是存在内容的;

    与上述的输出重定向不同,输入重定向是从文件当中读取数据的;

    使用cat命令可以查看文件内容,连接文件并打印等等功能;

    $ cat
    aaaaa
    aaaaa
    vvvvv
    vvvvv
    bbbbb
    bbbbb
    ^C
    

    当使用cat时,进程将会进入阻塞状态;

    再利用键盘向终端打印的信息将对应的被cat显示出来;

    当其在读取文件时在读到文件结束标志EOF时将会终止进程;

     cat < log.txt 
    

    而在这一行命令当中,使用重定向将标准输入文件流重定向至log.txt文件当中;

    对应的将会直接读取log.txt文件内的信息;

    $ cat < log.txt 
    log.txt
    makefile
    mytest
    test.c
    log.txt
    makefile
    mytest
    test.c
    

    对应的当cat读取到log.txt文件中的文件结束标志EOF时将会停止打印;


🧸 实现重定向的底层机制

请添加图片描述

在上文当中提到了重定向在日常使用中的简单例子,同时也讲了关于输出重定向与追加重定向实际上只是与底层中的打开文件的方式不同;

存在两段代码:

  • 存在一段代码

    #define FILENAME "log.txt"
    int main()
    {
      const char* massage = "hello world\n";
      close(1);
      int fd = open(FILENAME, O_CREAT | O_TRUNC | O_WRONLY, 0666);
      if(fd<0){
        perror("open fail");
      }
      ssize_t wret = write(1, massage, strlen(massage));
      if(wret<0){
        perror("write fail");
      }
      close(fd);
      return 0;
    }
    

    在这段代码当中设置了一个message字符串其内容为hello world\n;

    并关闭文件描述符1;

    在以写入的方式将massage中的内容写入至文件描述符1当中;

    当执行该程序时,对应的信息并没有被打印到显示器当中;

    而在此时使用cat显示对应的log.txt文件时对应的显示出massage当中的信息;

    $ cat log.txt 
    hello world
    

从上述代码以及最终结果中可以观察到,原本需要被打印在显示屏当中的信息最终被写入至了文件log.txt当中;

原因与上文当中所提到的文件描述符的分配有关;

  • 当打开一个新文件时,将会为这个文件分配一个最小的且未被使用的文件描述符;

故当关闭文件描述符1后表示该文件描述符并未被占用;

而在此时打开一个新的文件时将为该文件分配文件描述符1;

而在进行写入操作时进程并不知道其本身的文件描述符已经被替换而仍然继续使用该文件描述符操作;

这样的现象可以理解为这种抽象层为写入操作和文件管理进行了解耦合;

而此时将代码当中的open()的打开选项中的O_TRUNC换成O_APPEND并再次运行该程序;

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

由于在上面的操作当中log.txt文件当中已经存在了内容;

而这时将修改后的代码重新编译并运行;

$ cat log.txt 
hello world
hello world

从结果可以看出结果与上述的代码结果没有太大区别;

唯一的区别就是由于打开文件的方式选项不同而该段代码采用追加的方式打开文件故在进行写入的时候将以追加的方式进行写入;

与上文中的重定向指令的结果进行比较可以看出实际上两者别无二致;

实际上也是如此,Shell当中重定向的底层实际上就是替换对应的文件描述符从而使得其能够将原本需要显示在显示器上的内容写入至文件当中;


🐡 dup2()

请添加图片描述

在上文当中提到了重定向大致的底层机制;

那么若是以这种方式实现重定向是否过于复杂,即需要将先将文件描述符进行关闭再打开一个新文件;

  • 是否存在更加便捷的方式使得能够修改文件描述符的指向?

<unistd.h>头文件当中存在这样的接口,即dup(),dup2(),dup3();

其中dup2()较为常用;

NAME
       dup, dup2, dup3 - duplicate a file descriptor

SYNOPSIS
       #include <unistd.h>

       int dup(int oldfd);
       int dup2(int oldfd, int newfd);

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <fcntl.h>              /* Obtain O_* constant definitions */
       #include <unistd.h>

       int dup3(int oldfd, int newfd, int flags);
该节重点讲解dup2( )接口
dup2()  makes  newfd  be  the  copy of oldfd, closing newfd  first if necessary, but note the following:

       *  If oldfd is not a valid file descriptor, then  the  call
          fails, and newfd is not closed.

       *  If  oldfd  is a valid file descriptor, and newfd has the
          same value as  oldfd,  then  dup2()  does  nothing,  and
          returns newfd.
           
RETURN VALUE
       On success, these system calls return the new  descriptor.   On
       error, -1 is returned, and errno is set appropriately.
  • man手册可以看出其对dup2()的解释

    即使newfd称为oldfd的副本并关闭newfd;

    但是在传参的时候需要注意;

    若是oldfd不为有效的文件描述符,则调用失败且不关闭newfd;

    若是oldfdnewfd相同,dup2()则将不进行操作,并且返回newfd;

    当调用成功时将会返回保留下的文件描述符,即oldfd;

    若是调用失败则会返回-1并设置error;

在调用dup2()需要注意函数中的参数;

以正常的理解来说oldfd,newfd既然需要进行覆盖那一半是newfd去覆盖oldfd;

而真正在手册当中的解释是makes newfd be the copy of oldfd, closing newfd first if necessary;

即使oldfd覆盖newfd的内容;

主打一个狸猫换太子;

最终两个作为参数传入的文件描述符将只剩下oldfd;

同时这里的覆盖并不是将文件描述符进行覆盖,已知文件描述符实际上是文件描述符表(指针数组)的一个下标,而下标是常量是不能被进行修改的;

故这里的覆盖指的是将指针数组中下标对应的元素(指针)进行覆盖从而达到改变其指向的效果;


🐡 利用dup2()实现重定向

请添加图片描述

  • 输出重定向

    存在一段代码:

    #define FILENAME "log.txt"
    int main()
    {
      const char* massage = "hello massage\n";
      int fd = open(FILENAME, O_WRONLY | O_TRUNC | O_CREAT, 0666);
      dup2(fd, 1);
      write(1, massage, strlen(massage));
      //省略了调用失败的错误处理
      close(fd);
      return 0;
    }
    

    在这段代码当中定义了一个内容为hello massage\n的字符串massage;

    并以 只读 的方式将文件进行打开;

在打开文件过后调用dup2()函数从而实现文件描述符1拷贝新打开的文件描述符的指向从而指向新打开的文件;

当调用dup2()后再向向文件描述符1中写入数据时对应的文件描述符1已经指向了log.txt文件当中,故原本应写至显示器当中的信息被写入至了log.txt文件当中;

$ cat log.txt 
hello massage

该操作即为输出重定向;

对于追加重定向而言,其一样是将对应的将文件描述1重定向至新打开的文件当中,只是打开文件的方式不同(以O_APPEND追加的方式打开文件),即open(FILENAME, O_WRONLY | O_APPEND | O_CREAT, 0666)在此不作赘述;

  • 输入重定向

    存在一段代码

    #define FILENAME "log.txt"
    //在FILENAME文件存在且当中存在信息的前提下
    int main() {
      char buf[1024];
      int fd = open(FILENAME, O_RDONLY);
      dup2(fd, 0);
      int len = read(0,buf,sizeof(buf));
      // 省略了调用失败的错误处理
      buf[len] = '\0';
      printf("%s\n", buf);
      close(fd);
      return 0;
    }
    

    在这段代码当中,已知FILENAME文件存在且文件内存在信息;

    并定义了一个buf[]数组作为缓冲区用于存储read()接口从文件当中获取的信息;

    使用read()读取数据并存放至缓冲区buf当中最后将其打印出来;

该段代码的实现方式与输出重定向的机制相同;

都是使用dup2()接口将文件描述符进行重定向;

在正常的情况中(未调用dup2()的前提下);

由于文件描述符0指向的文件为键盘设备,故当执行程序时对应的进程将进入阻塞状态并且等待用户从键盘输入信息;

但由于使用了dup2()接口函数从而将文件描述符0重定向至了log.txt文件当中;

故在这段程序当中将从log.txt文件当中读取数据并放置缓冲区当中接而打印;

该操作即为输入重定向;

在上文当中了解到,实际上dup2()接口并不是进行替换,而是将对应的文件描述符当中的内容进行拷贝覆盖;

那么在这种情况下;

  • 是否会造成占用过多的文件描述符从而导致文件描述符枯竭?

答案是在正常的情况下使用dup2()进行重定向操作时并不会导致过多占用文件描述符从而导致文件描述符枯竭;

而若是调用了过多次dup2()且并未将原先的文件描述符关闭,由于dup2()操作并不是将文件描述符进行替换而是进行拷贝覆盖,原先的文件描述符将持续存在于文件描述符表当中;

当到达一定限制时,dup2()的调用将会因为文件描述符枯竭而调用失败;

故在一般情况下若是使用dup2()接口后需要使用close()将冗余的文件描述符进行关闭从而防止文件描述符枯竭的问题;


🧸 在自定义Shell当中添加重定向功能

请添加图片描述

在博客『 Linux 』进程替换( Process replacement ) 及 简单Shell的实现(万字)当中实现了一个简易的Shell;

但总体来说,这个简易的Shell写的并不好;

无论是从代码结构,可读性,拓展性等等;

  • 在此更新一个版本:

    #include <assert.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    
    #define LEFT "["
    
    #define RIGHT "]"
    
    #define LINE_SIZE 1024  // 命令行缓冲区大小
    
    #define ARGV_SIZE 32
    
    #define DELIM " \t"
    
    #define EXIT_CODE 33
    
    // 获取环境变量为标识符
    /*
        [ USER@HOSTNAME PWD ]label
    */
    int lastcode = 0;
    
    int quit = 0;
    
    char commandline[LINE_SIZE];
    
    char* argv[ARGV_SIZE];
    
    char pwd[LINE_SIZE];
    
    char currvariables[LINE_SIZE];
    
    const char* getUsername() { return getenv("USER"); }
    
    const char* getHostname() { return getenv("HOSTNAME"); }
    
    void getpwd() {
      // return getenv("PWD");
      getcwd(pwd, sizeof(pwd));
    }
    
    void interact(char* cline, size_t size) {
      getpwd();
      printf(LEFT "%s@%s %s" RIGHT "# ", getUsername(), getHostname(), pwd);
    
      char* s = fgets(cline, size, stdin);  // 从键盘获取
    
      size_t len = strlen(cline);
    
      if (len > 1) cline[len - 1] = '\0';
    
      assert(s);
    
      (void)s;
    }
    
    int splitstring(char* cline, char** argv) {
      int i = 0;
      argv[i++] = strtok(cline, DELIM);
      while ((argv[i++] = strtok(NULL, DELIM))) {
        ;
      }
      return i - 1;
    }
    
    void normalExecute(char** _argv) {
      //
      pid_t id = fork();
      if (id < 0) {
        //
        perror("fork error");
        return;
      } else if (id == 0) {
        // 子进程执行程序
        execvp(_argv[0], _argv);
        exit(EXIT_CODE);  // 进程替换失败
      } else {
        // 父进程等待子进程
        int status = 0;
        int ret = waitpid(id, &status, 0);  // 等待成功返回pid 失败则返回-1
        if (ret == id) {
          lastcode = WEXITSTATUS(status);
        }
      }
    }
    
    int buildCommand(int _argc, char** _argv) {
      getpwd();
      if (_argc == 2 && (strcmp(_argv[0], "cd") == 0)) {
        int dirret = chdir(_argv[1]);
        if (dirret != 0) {  // 说明chdir调用失败
          perror("chdir error\n");
        } else {
          sprintf(getenv("PWD"), "%s", pwd);
        }
        return 1;
      }
    
      else if (_argc == 2 && strcmp(_argv[0], "export") == 0) {
        // 两种方法都适用
        // ①
        /* char* _name = strtok(_argv[1],"=");
        char* _value = strtok(NULL, "");
        if(_name&&_value){
          setenv(_name, _value, 1);
    
          //使用putenv的话只是单纯的将字符串的指针写在了环境变量表之中
          //而argv当中的数据是不停变化的
          // 故若是使用putenv的话由于argv的内容在不停变化
          //则会使这个环境变量表的指针指向一个无效的位置
          //故要么将这个环境变量表的指针单独存放一个位置 要么就使用setenv
        }*/
    
        // ②
        strcpy(currvariables, _argv[1]);
        putenv(currvariables);
        return 1;
      }
    
      else if (_argc == 2 && strcmp(_argv[0], "echo") == 0) {
        if (strcmp(_argv[1], "$?") == 0) {
          printf("%d\n", lastcode);
          lastcode = 0;
        } else if (_argv[1][0] == '$') {
          // 说明需要打印的是环境变量
          char* val = getenv(_argv[1] + 1);
          if (val) printf("%s\n", val);
        } else {
          // 说明不属于内建命令可以直接打印
          printf("%s\n", _argv[1]);
        }
        return 1;
      }
    
      else if (strcmp(_argv[0], "ls") == 0) {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
      }
      return 0;
    }
    
    int main() {
      while (!quit) {
        // 获取命令行信息
        interact(commandline, sizeof(commandline));
    
        // debug 打印测试输入信息是否正确
        printf("echo: %s\n", commandline);
    
        // 分割命令行信息
        int argc = splitstring(commandline, argv);  // 分割字符串
        if (argc == 0) continue;
        /*
            如果未进行判断的话 将会发生下列问题:
            当只输入分隔符的情况下数据将不被分割
            对应的argv当中则只有NULL
              由于没有中断此次循环将会继续向下执行
              而在接下来的执行当中 程序将期望存在一个有效的命令
              然而argv[0]为NULL 将会进行报错
        */
    
        //  //用于debug 对指针数组进行打印
        // for (int i = 0; argv[i] != NULL; ++i) {
        //   printf("%s\n", argv[i]);
        // }
    
        // 内建命令
        int flag = buildCommand(argc, argv);
    
        // // 分割命令后将对命令进行处理
        if (!flag) normalExecute(argv);
      }
      return 0;
    }
    
    

    在这个Shell当中以不同接口函数的方式将功能进行分割从而提升其模块化使其更容易理解与维护;

    同时在该程序当中对部分 内建命令 进行了特殊处理;

    在该节当中重点更新该程序当中的重定向接口;

    同时对于重定向接口重点对通常命令进行处理,即代码当中的normalExecute();

既然是重定向,那么则需要判断命令当中是否存在重定向符号>,>><;

// 以下的几个宏表示重定向的属性

#define IN_RDIR 0  // 输入重定向

#define OUT_RDIR 1  // 输出重定向

#define APPEND_RDIR 2  // 追加重定向

#define NONE -1  // 无属性 即非重定向

char* rdirfilename = NULL;  // 文件名

int rdir = NONE;  // rdir默认为-1 表示非重定向

void check_redir(char* cmd) {
  // 检查字符串是否存在重定向
  char* pos = cmd;
  while (*pos) {
    if (*pos == '>') {
      // 输出重定向'>' 或是追加重定向 '>>'
      if (*(pos+1) == '>') {
        /* 追加重定向 */
        // 追加重定向>>
        *pos++ = '\0';
        *pos++ = '\0';
        while (isspace(*pos)) {
          pos++;  

        }
        rdirfilename = pos;  
        // 将全局变量中的文件名设置为pos字符串
        rdir = APPEND_RDIR;  
        // 追加重定向
        break;
      }
      *pos++ = '\0';
      while (isspace(*pos)) {
        pos++;

        }
        rdirfilename = pos;  
        // 将全局变量中的文件名设置为pos字符串
        rdir = OUT_RDIR;     
        // 输出重定向

        break;
    } else if (*pos == '<') {
      // 输出重定向
      *pos++ = '\0';
      while (isspace(*pos)) {
        pos++;  

      }
      rdirfilename = pos;  
      // 将全局变量中的文件名设置为pos字符串
      rdir = IN_RDIR;      
      // 输入重定向
      break;
    } else {
      // 非重定向
      //  do nothing
    }
    pos++;
  }
}

使用#define为重定向操作选项定义几个宏;

  • #define IN_RDIR 0

    该选项表示输入重定向

  • #define OUT_RDIR 1

    该选项表示输出重定向

  • #define APPEND_RDIR 2

    该选项表示追加重定向

  • #define NONE -1

    该选项表示不存在重定向

并定义了两个变量为char* rdirfilename = NULL,int rdir = NONE;

其中rdirfilename表示重定向当中的文件名;

rdir表示是否存在重定向的选项;

在判断是否重定向时采用将用户输入的命令从右向左进行遍历的方式;

当判断到存在重定向操作时需要将用户输入的命令中的重定向符号>,>>,<修改为\0并设置当前重定向的状态(是否需要重定向);

而在该程序当中重点对通常命令进行重定向的更新故需要再normalExecute()接口当中设置当判断为重定向时具体的操作;

即根据文件描述符的属性并调用dup2()重定向对应的文件描述符;

  • 且需要注意

    由于程序当中的rdirfilenamerdir属于全局变量;

    为了确保只有当真正存在重定向操作时才进行对应操作时需要在main()当中或是在check_rdir()当中将这两个变量置空;

  • 整体代码演示(供参考)

    
    
    #include <assert.h>
    #include <ctype.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    
    #define LEFT "["
    
    #define RIGHT "]"
    
    #define LINE_SIZE 1024  // 命令行缓冲区大小
    
    #define ARGV_SIZE 32
    
    #define DELIM " \t"
    
    #define EXIT_CODE 33
    
    // 以下的几个宏表示重定向的属性
    
    #define IN_RDIR 0  // 输入重定向
    
    #define OUT_RDIR 1  // 输出重定向
    
    #define APPEND_RDIR 2  // 追加重定向
    
    #define NONE -1  // 无属性 即非重定向
    
    char* rdirfilename = NULL;  // 文件名
    
    int rdir = NONE;  // rdir默认为-1 表示非重定向
    
    int lastcode = 0;
    
    int quit = 0;
    
    char commandline[LINE_SIZE];
    
    char* argv[ARGV_SIZE];
    
    char pwd[LINE_SIZE];
    
    char currvariables[LINE_SIZE];
    
    const char* getUsername() { return getenv("USER"); }
    
    const char* getHostname() { return getenv("HOSTNAME"); }
    
    extern int buildCommand(int _argc, char** _argv);
    
    void getpwd() {
      // return getenv("PWD");
      getcwd(pwd, sizeof(pwd));
    }
    
    void check_redir(char* cmd) {
      // 检查字符串是否存在重定向
      char* pos = cmd;
      while (*pos) {
        if (*pos == '>') {
          // 输出重定向'>' 或是追加重定向 '>>'
          if (*(pos + 1) == '>') {
            /* 追加重定向 */
            // 追加重定向>>
            *pos++ = '\0';
            *pos++ = '\0';
            while (isspace(*pos)) {
              pos++;
            }
            rdirfilename = pos;
            // 将全局变量中的文件名设置为pos字符串
            rdir = APPEND_RDIR;
            // 追加重定向
            break;
          }
          *pos++ = '\0';
          while (isspace(*pos)) {
            pos++;
          }
          rdirfilename = pos;
          // 将全局变量中的文件名设置为pos字符串
          rdir = OUT_RDIR;
          // 输出重定向
    
          break;
        } else if (*pos == '<') {
          // 输出重定向
          *pos++ = '\0';
          while (isspace(*pos)) {
            pos++;
          }
          rdirfilename = pos;
          // 将全局变量中的文件名设置为pos字符串
          rdir = IN_RDIR;
          // 输入重定向
          break;
        } else {
          // 非重定向
          //  do nothing
        }
        pos++;
      }
    }
    
    void interact(char* cline, size_t size) {
      // 获取环境变量为标识符
      /*
          [ USER@HOSTNAME PWD ]label
      */
      getpwd();
      printf(LEFT "%s@%s %s" RIGHT "# ", getUsername(), getHostname(), pwd);
    
      char* s = fgets(cline, size, stdin);  // 从键盘获取
    
      size_t len = strlen(cline);
    
      if (len > 1) cline[len - 1] = '\0';
    
      assert(s);
    
      (void)s;
    
      check_redir(cline);
    
      return;
    }
    
    int splitstring(char* cline, char** argv) {
      int i = 0;
      argv[i++] = strtok(cline, DELIM);
      while ((argv[i++] = strtok(NULL, DELIM))) {
        ;
      }
      return i - 1;
    }
    
    void normalExecute(char** _argv) {
      //
      pid_t id = fork();
      if (id < 0) {
        //
        perror("fork error");
        return;
      } else if (id == 0) {
        // 子进程执行程序
        int fd = 0;
        int dupret = 0;
        // 判断是否存在重定向 即rdir是否具有参数 (非NONE)
        if (rdir == IN_RDIR) {
          fd = open(rdirfilename, O_RDONLY);
          dupret = dup2(fd, 0);
        } else if (rdir == OUT_RDIR) {
          fd = open(rdirfilename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    
          dupret = dup2(fd, 1);
        } else if (rdir == APPEND_RDIR) {
          fd = open(rdirfilename, O_WRONLY | O_CREAT | O_APPEND, 0666);
          dupret = dup2(fd, 1);
        } else {
          // rdir == NONE
        }
        if (fd < 0) {
          perror("open fail\0");
          return;
        }
        if (dupret < 0) {
          perror("dup fail\0");
          return;
        }
    
        execvp(_argv[0], _argv);
        exit(EXIT_CODE);  // 进程替换失败
      } else {
        // 父进程等待子进程
        int status = 0;
        int ret = waitpid(id, &status, 0);  // 等待成功返回pid 失败则返回-1
        if (ret == id) {
          lastcode = WEXITSTATUS(status);
        }
      }
    }
    
    int buildCommand(int _argc, char** _argv) {
      getpwd();
      if (_argc == 2 && (strcmp(_argv[0], "cd") == 0)) {
        int dirret = chdir(_argv[1]);
        if (dirret != 0) {  // 说明chdir调用失败
          perror("chdir error\n");
        } else {
          sprintf(getenv("PWD"), "%s", pwd);
        }
        return 1;
      }
    
      else if (_argc == 2 && strcmp(_argv[0], "export") == 0) {
        // 两种方法都适用
        // ①
        /* char* _name = strtok(_argv[1],"=");
        char* _value = strtok(NULL, "");
        if(_name&&_value){
          setenv(_name, _value, 1);
    
          //使用putenv的话只是单纯的将字符串的指针写在了环境变量表之中
          //而argv当中的数据是不停变化的
          // 故若是使用putenv的话由于argv的内容在不停变化
          //则会使这个环境变量表的指针指向一个无效的位置
          //故要么将这个环境变量表的指针单独存放一个位置 要么就使用setenv
        }*/
    
        // ②
        strcpy(currvariables, _argv[1]);
        putenv(currvariables);
        return 1;
      }
    
      else if (_argc == 2 && strcmp(_argv[0], "echo") == 0) {
        if (strcmp(_argv[1], "$?") == 0) {
          printf("%d\n", lastcode);
          lastcode = 0;
        } else if (_argv[1][0] == '$') {
          // 说明需要打印的是环境变量
          char* val = getenv(_argv[1] + 1);
          if (val) printf("%s\n", val);
        } else {
          // 说明不属于内建命令可以直接打印
          printf("%s\n", _argv[1]);
        }
        return 1;
      }
    
      else if (strcmp(_argv[0], "ls") == 0) {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
      }
      return 0;
    }
    
    int main() {
      rdir = NONE;
      rdirfilename = NULL;
      while (!quit) {
        // 获取命令行信息
        interact(commandline, sizeof(commandline));
    
        // debug 打印测试输入信息是否正确
        printf("echo: %s\n", commandline);
    
        // 分割命令行信息
        int argc = splitstring(commandline, argv);  // 分割字符串
        if (argc == 0) continue;
        /*
            如果未进行判断的话 将会发生下列问题:
            当只输入分隔符的情况下数据将不被分割
            对应的argv当中则只有NULL
              由于没有中断此次循环将会继续向下执行
              而在接下来的执行当中 程序将期望存在一个有效的命令
              然而argv[0]为NULL 将会进行报错
        */
    
        //  //用于debug 对指针数组进行打印
        // for (int i = 0; argv[i] != NULL; ++i) {
        //   printf("%s\n", argv[i]);
        // }
    
        // 内建命令
        int flag = buildCommand(argc, argv);
        // 重定向暂不考虑内建命令
        // 重定向只修改对应的通常命令
    
        // // 分割命令后将对命令进行处理
        if (!flag) normalExecute(argv);
        // 重定向将在该接口内进行处理
      }
      return 0;
    }
    
    

🐡 重定向与进程替换

请添加图片描述

在上文当中已经完成了shell当中重定向的接口,在此不作赘述;

那么思考一个问题:

  • 进程替换execl接口为什么不会影响已经完成的dup2()重定向操作?

以该图为例;

进程本身就是在磁盘当中的一个可执行文件通过运行从而产生的;

当运行后对应的操作系统将会为了维护这个进程而创建对应的PCB结构体,如图所示即为其中的task_struct;

而其中的task_struct当中存在着一个struct file_struct*files指针;

这个指针指向了一个名为struct file_struct结构体;

在这个结构体当中存在着一个指针数组,即struct file*fd_array[],也就是在上篇博客当中提到的文件描述符表;

其中文件描述符表当中的每个元素(指针)都指向了一个file结构体,每打开一个文件对应的OS将为该文件创建对应的该结构体并进行管理;

上述的这一块的内容在OS当中被称作 文件管理 ;

mm_struct,页表,磁盘等被称作 内存管理 ;

而在 『 Linux 』进程替换( Process replacement ) 及 简单Shell的实现(万字)中提到,进程在进行进程替换的时候并不是创建一个新的进程,而是当进程替换成功时将原本进程当中的代码与数据与替换的代码数据进行替换;

这意味着是同一个容器而容器当中的数据是不同的;

新的代码和是数据将继续延用原本的进程;

也意味着由于并没有创建新的进程故并不会将原本进程当中设置的重定向操作进行恢复或者重置;

而也是这种设定也使得Linux中的 文件管理内存管理 之间的解耦合;


🧸 标准输出重定向与标准错误重定向

请添加图片描述

在上文当中提到了输出重定向也分为了 标准输出重定向标准错误重定向 ;

根据名词即可以了解,标准输出标准错误 实际上也仅仅是文件描述符的不同;

  • 那么什么是标准错误重定向?

存在一段代码:

int main() {

  fprintf(stdout, "normal stdout\n");
  fprintf(stdout, "normal stdout\n");
  fprintf(stdout, "normal stdout\n");
  
  fprintf(stderr, "error stderr\n");
  fprintf(stderr, "error stderr\n");
  fprintf(stderr, "error stderr\n");

  return 0;
}

在这段代码当中调用了fprintf()分别使用stdoutstderr作为参数打印出normal stdout\nerror stderr\n;

当运行程序后结果将与预期相同分别打印出对应的消息至显示器当中;

$ ./mytest 
normal stdout
normal stdout
normal stdout
error stderr
error stderr
error stderr

而若是运行该程序并将对应的信息重定向至一个名为noraml.txt文件当中会发生什么?

$ ./mytest > normal.txt
error stderr
error stderr
error stderr
$ cat normal.txt 
normal stdout
normal stdout
normal stdout

从结果来看,当使用重定向将打印出来的信息写入至normal.txt文件时;

对应的error stderr\n并未被写入至normal.txt文件当中;

原因即为重定向>>>的机制是将文件描述符1重定向至文件当中;

而对应的stderr对应的文件描述符为2,故不能将其重定向至normal.txt文件当中;

而实际上对应的标准错误也有重定向,即为2>2>>;

其与标准输出重定向大致相同,唯一不同的只是文件描述符不同;

若是使用2>2>>将输出的内容重定向至error.txt文件当中时;

$ ./mytest 2> error.txt
normal stdout
normal stdout
normal stdout
$ ./mytest 2>> error.txt
normal stdout
normal stdout
normal stdout
$ cat error.txt 
error stderr
error stderr
error stderr
error stderr
error stderr
error stderr

从结果来看与>不同,当使用标准错误重定向时对应的文件描述符1所打印的内容将不会被写入至error.txt文件当中;

其底层原理与标准输出重定向相同;

而实际上标准输出重定向其符号为1>1>>;

只不过在使用时其1可以进行省略;

一般而言文件描述符2是用来输出错误的内容,而若是需要将一个程序当中的正常输出与错误内容进行区分成两个文件时则可以使用:

./a.out > NORMALFILENAME 2> ERRORFILENAME #./a.out 1> NORMALFILENAME 2> ERRORFILENAME
  • 以上面的程序为例:

    $ ./mytest > normal.txt 2> error.txt
    $ cat normal.txt 
    normal stdout
    normal stdout
    normal stdout
    $ cat error.txt 
    error stderr
    error stderr
    error stderr
    

当使用该命令时对应的将根据不同的文件描述符分别重定向到不同的文件当中;

而若是需要将不同文件描述符对应输出的内容写入同一个文件则可以使用:

./a.out > ALLFILENAME 2>&1 #./a.out 1> ALLFILENAME 2>&1
  • 以上面的程序为例

    $ ./mytest 1> all.txt 2>&1
    $ cat all.txt 
    error stderr
    error stderr
    error stderr
    normal stdout
    normal stdout
    normal stdout
    

以这个例子为例,即表示将文件描述符1重定向到文件all.txt当中;

然后将文件描述符2重定向到已经指向all.txt的文件描述符1当中;

但这里的文件描述符的重定向与上文相同,并不是将文件描述符进行重定向(文件描述符为下标是常量),而是利用dup接口进行实现以至于改变指针的指向;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/624492.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python tensor向量维度转换,不同维度的向量转化为相同的维度,经过全连接层MLP的维度转换,代码实战

问题&#xff1a;在机器学习特征工程中&#xff0c;假如每类特征需要转化为相同的维度进行拼接&#xff0c;那该怎么办呢&#xff1f;接一个全连接层MLP就可以了。 例子&#xff1a;将&#xff08;128,64&#xff09; 维度的向量转化为&#xff08;128,32&#xff09;维。 impo…

通过随机采样和数据增强来解决数据不平衡的问题

什么是类别不平衡 当每个类别的样本不平衡时&#xff0c;即在类别分布之间没有平衡比率时&#xff0c;会出现类别不平衡的问题。 这种失衡可能是轻微的&#xff0c;也可能是严重的。 取决于样本量&#xff0c;比率从1&#xff1a;2到1:10可以理解为轻微的不平衡&#xff0c;比…

基于UDP协议Python通信网络程序(服务器端+客户端)及通信协议在自动驾驶场景应用示例

一、UDP协议 UDP&#xff08;用户数据报协议&#xff09;是一种无连接的传输层协议&#xff0c;具有简单、高效的特点&#xff0c;适用于一些对数据可靠性要求不高的应用场景。UDP的主要特点包括无连接、不可靠和面向数据报。这意味着在发送数据之前不需要建立连接&#xff0c…

撤销 git add 操作(忽略被追踪的文件)

文章目录 引言I git rm命令来取消暂存【推荐】II 撤销特定文件的暂存状态2.1 git rese2.2 git restoresee also引言 应用场景: 修改.gitignoregitignore只能忽略那些原来没有被追踪的文件,如果某些文件已经被纳入了版本管理中,则修改.gitignore是无效的。那么解决方法就是先…

国产化开源鸿蒙系统智能终端RK3568主板在电子班牌项目的应用

国产化开源鸿蒙系统智能终端主板AIoT-3568A、人脸识别算法的的电子班牌方案可支持校园信息发布、人脸识别考勤、考场管理、查询互动等多项功能&#xff0c;助力学校在硬件上实现信息化、网络化、数字化&#xff0c;构建“学校、教师、学生”三个维度的智慧教育空间。 方案优势 …

微软推出的Microsoft Fabric 到底是什么?

近期&#xff0c;总有客户问小编&#xff0c;微软推出的 Microsoft Fabric 是什么&#xff1f;这个产品有什么特别之处呢&#xff1f;希望下面这篇文章能为大家解开一些疑惑。 微软Fabric是2023年5月推出的一个数据分析平台&#xff0c;它将关键数据管理和分析工作负载整合到一…

618值得入手的数码产品怎么选?2024 买过不后悔的数码好物分享

在数字时代的浪潮中&#xff0c;每一次的购物狂欢节都如同一场科技盛宴&#xff0c;让我们有机会接触到最前沿、最实用的数码产品&#xff0c;而“618”无疑是这场盛宴中最为引人瞩目的日子之一。面对琳琅满目的商品&#xff0c;如何选择那些真正值得入手的数码好物&#xff0c…

Java全局异常处理,@ControllerAdvice异常拦截原理解析【简单易懂】

https://www.bilibili.com/video/BV1sS411c7Mo 文章目录 一、全局异常处理器的类型1-1、实现方式一1-2、实现方式二 二、全局异常拦截点2-1、入口2-2、全局异常拦截器是如何注入到 DispatcherServlet 的 三、ControllerAdvice 如何解析、执行3-1、解析3-2、执行 四、其它4-1、设…

pdf怎么标注红色方框?五种PDF标注红色方框方法

pdf怎么标注红色方框&#xff1f;在当今数字化时代&#xff0c;PDF文档已成为我们日常工作和学习中不可或缺的一部分。然而&#xff0c;如何在海量的PDF文件中快速、准确地标注出重要信息&#xff0c;让内容更加醒目呢&#xff1f;今天&#xff0c;我将向大家介绍五种PDF标注红…

AI 图像生成-环境配置

一、python环境安装 Windows安装Python&#xff08;图解&#xff09; 二、CUDA安装 CUDA安装教程&#xff08;超详细&#xff09;-CSDN博客 三、Git安装 git安装教程&#xff08;详细版本&#xff09;-CSDN博客 四、启动器安装 这里安装的是秋叶aaaki的安装包 【AI绘画…

苹果cms:搜索功能的开关与设置

今天有个小伙伴问了个关于苹果cms搜索的问题&#xff1a;直接搜演员搜索不到影片信息&#xff08;如下图&#xff09; 1、我们拿演员王宝强为例&#xff1a;搜索王宝强后结果显示无相关视频 2、但是我们搜索王宝强主演的“大闹天竺”后却能得到关于王宝强的影片信息。这是为什…

【无重复字符的最长字串】

P. S.&#xff1a;以下代码均在VS2019环境下测试&#xff0c;不代表所有编译器均可通过。 P. S.&#xff1a;测试代码均未展示头文件stdio.h的声明&#xff0c;使用时请自行添加。 Problem: 3. 无重复字符的最长子串 文章目录 1、思路2、解题方法3、复杂度4、Code5、结语 1、思…

string容器-构造函数

基本概念 string本质上是一个类string类内部封装了很多成员方法&#xff0c;例如&#xff1a;查找find、拷贝copy&#xff0c;删除delete&#xff0c;替换replace&#xff0c;插入insertstring管理char*所分配的内存&#xff0c;不用担心复制越界和取值越界等&#xff0c;由类…

CentOS7中如何docker-compose

在 CentOS 7 上安装 docker-compose 需要几个步骤 步骤 1: 安装 Docker 首先&#xff0c;确保你已经安装了 Docker。如果没有安装&#xff0c;可以通过以下命令安装&#xff1a; sudo yum update -y sudo yum install -y yum-utils sudo yum-config-manager --add-repo http…

利用MMDetection进行模型微调和权重初始化

目录 模型微调修改第一处&#xff1a;更少的训练回合Epoch修改第二处&#xff1a;更小的学习率Learning Rate修改第三处&#xff1a;使用预训练模型 权重初始化init_cfg 的使用配置初始化器 本文基于 MMDetection官方文档&#xff0c;对模型微调和权重初始化进行第三方讲解。 …

漏桶算法:稳定处理大量突发流量的秘密武器!

漏桶算法的介绍 我们经常会遇到这样一种情况&#xff1a;数据包的发送速率不稳定&#xff0c;而网络的带宽有限。如果在短时间内有大量的数据包涌入&#xff0c;那么网络就会出现拥塞&#xff0c;数据包的丢失率就会增大。为了解决这个问题&#xff0c;人们提出了一种叫做“漏…

RockChip Android8.1 EthernetService分析

一:概述 本篇文章将围绕RK Android8.1 SDK对Ethernet做一次框架分析,包含Framework层和APP层。 当前版本SDK默认只支持一路Ethernet,熟悉Ethernet工作流程后通过修改最终会在系统Setting以太网中呈现多路选项(可以有多种实现方式),博主通过增加ListPreference实现的效果…

鸿蒙内核源码分析(特殊进程篇)

三个进程 鸿蒙有三个特殊的进程&#xff0c;创建顺序如下: 2号进程&#xff0c;KProcess&#xff0c;为内核态根进程.启动过程中创建.0号进程&#xff0c;KIdle为内核态第二个进程&#xff0c;它是通过KProcess fork 而来的.这有点难理解.1号进程&#xff0c;init&#xff0c…

Linux-远程登录

远程登录Linux服务器的两款小工具&#xff1a; 1、Xshell &#xff08;可以远程登录到Linux终端控制台&#xff09; 2、 Xftp (可以与Linux服务器互相传递文件) 家庭/学校免费 - NetSarang Website 下载地址 1、傻瓜式安装Xshell6 2、在Linux主机上查看 Linux主机的…

天府锋巢直播基地运营方——树莓集团:构建3+3+1运营体系

天府锋巢直播产业基地作为一座充满活力和创新精神的成都数字产业园区&#xff0c;自其诞生之初便承载着引领直播产业发展的使命。作为该基地的运营方&#xff0c;树莓集团以其前瞻性的视野和深厚的行业积淀&#xff0c;成功构建了331运营体系&#xff0c;为入驻企业提供全生命周…