Linux知识点 -- 基础IO(二)

Linux知识点 – 基础IO(二)

文章目录

  • Linux知识点 -- 基础IO(二)
  • 一、重定向
    • 1.输出重定向
    • 2.输入重定向
    • 3.追加重定向
    • 4.重定向系统调用
    • 5.minishell支持重定向
    • 6.stdout和stderr的区别
    • 7.常规的重定向操作
    • 8.perror的实现
  • 二、Linux下一切皆文件
  • 三.缓冲区
    • 1.缓冲区
    • 2.关于缓冲区的认识
    • 3.用户缓冲区与内核缓冲区
    • 4.用户缓冲区的位置
    • 5.自己设计用户缓冲区


一、重定向

1.输出重定向

Linux知识点 -- 基础IO(二)_第1张图片
在上面的代码中,fprintf本来是向stdout中打印的,但是stdout关闭了,实际上fprintf事项fd是1的文件中打印,这里log.txt的fd就是1;
运行结果为:
Linux知识点 -- 基础IO(二)_第2张图片
这就叫做输出重定向
上面的代码将stdout关闭了,并打开log.txt文件,则log.txt文件的fd就是1;
在系统中,stdout就代表着fd为1,所以默认就会向fd为1的文件中打印,而此时fd为1的文件是log.txt,因此就向该文件中打印了;

2.输入重定向

Linux知识点 -- 基础IO(二)_第3张图片
运行结果:
Linux知识点 -- 基础IO(二)_第4张图片

3.追加重定向

Linux知识点 -- 基础IO(二)_第5张图片
运行结果:
Linux知识点 -- 基础IO(二)_第6张图片

4.重定向系统调用

Linux知识点 -- 基础IO(二)_第7张图片
在这里插入图片描述
oldfd copy to the newfd -> 最后要和oldfd一样
Linux知识点 -- 基础IO(二)_第8张图片
最终重定向的fd要是3,dup2的运行结果是newfd和oldfd一样,因此这里3是oldfd,1是newfd;

  • 使用dup2实现输出重定向:
    Linux知识点 -- 基础IO(二)_第9张图片
    运行结果:
    在这里插入图片描述
    输出重定向到了log.txt中;

  • 使用dup2实现追加重定向:
    Linux知识点 -- 基础IO(二)_第10张图片
    Linux知识点 -- 基础IO(二)_第11张图片

5.minishell支持重定向

在进程控制章节我们自己写了shell程序,这里我们在其中添加重定向功能;
Linux知识点 -- 基础IO(二)_第12张图片
Linux知识点 -- 基础IO(二)_第13张图片
Linux知识点 -- 基础IO(二)_第14张图片
Linux知识点 -- 基础IO(二)_第15张图片

  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  #include 
  
  #define NUM 1024
  #define SIZE 32
  #define SEP " "
  
  //保存完整的命令行字符串
  char cmd_line[NUM];
  //保存打散之后的命令行字符串
  char* g_argv[SIZE];
  //用于保存环境变量,使其不被刷新覆盖
  char g_myval[64]; 
  
  #define INPUT_REDIR 1
  #define OUTPUT_REDIR 2
  #define APPEND_REDIR 3
  #define NONE_REDIR 0
  
  int redir_status = NONE_REDIR;
  
  
  char* CheckRedir(char* start)
  {
      assert(start);
      char* end = start + strlen(start) - 1;
      while(end >= start)
      {                                                                   
          if(*end == '>')
          {
              if(*(end - 1) == '>')//重定向类型是>> 追加
              {
                  redir_status = APPEND_REDIR;
                  *(end - 1) = '\0';
                  end++;
                  break;
              }
              redir_status = OUTPUT_REDIR;                                
              *end = '\0';
              end++;
              break;
          }
          else if(*end == '<') 
          {
              redir_status = INPUT_REDIR;
              *end = '\0';
              end++;
              break;
          }
          else 
          {
              end--;
          }
      }
  
      if(end >= start)
      {
          //有重定向
          return end;//返回要打开的文件
      }
      else 
      {
          return NULL;
      }
  }
  
  //shell运行原理,让子进程执行命令,父进程等待&&解析命令
  int main()
  {
      extern char** environ;//使用父进程的环境变量,可以通过main函数的参数  ,也可以导入environ指针
        //命令行解释器:一定是一个常驻内存的进程,不退出
      while(1)                                                            
      {
          //1.打印出提示信息 [lmx@localhost myshell]#
          printf("[lmx@localhost myshell]# ");
          fflush(stdout);//由于printf没有加\n,不刷新缓冲区,使用fflush刷>  新
          memset(cmd_line, '\0', sizeof cmd_line);//sizeof可以不使用括号
  
          //2.获取用户的键盘输入,输入的是各种指令和选项:"ls -a -l"
          if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
          {
              continue;
          }
          cmd_line[strlen(cmd_line) - 1] = '\0';//去掉输入时的\n
          //2.1 分析是否有重定向
          //"ls -a -l\n\0"
          char* sep = CheckRedir(cmd_line);
          //原理:从后往前检查命令字符串,发现有">, <, >>"的,就将该字符所  在位置变为\0,能够将命令分为两段,左边是命令, 右边是文件
          
          //3.命令行字符串解析:"ls -a -l" -> "ls" "-a" "-l"
          g_argv[0] = strtok(cmd_line, SEP);//第一次调用,要传入原始字符串
          int index = 1;
          if(strcmp(g_argv[0], "ls") == 0)
          {
             g_argv[index++] = "--color=auto";
          }
          if(strcmp(g_argv[0], "ll") == 0)
          {
              g_argv[0] = "ls";
              g_argv[index++] = "-l";
              g_argv[index++] = "--color=auto";
          }
  
          while(g_argv[index++] = strtok(NULL, SEP));//第二次,如果还要继>  续解析原始字符,传入NULL
          //导入环境变量                                                  
          if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
          {
              strcpy(g_myval, g_argv[1]);//将环境变量保存到全新字符串中,>  不让它被下一个指令刷新,以至于子进程拿不到环境变量
              int ret = putenv(g_myval);
              if(ret == 0)
              {
                  printf("%s export success\n", g_argv[1]);
              }
              continue;
          }
  
          //4.TODO:内置命令,让父进程(shell)自己执行的命令,叫做内置命令  ,内建命令
          //内置命令本质是shell中的一个函数调用
          if(strcmp(g_argv[0], "cd") == 0)//如果命令是cd,改变工作目录,需  要在父进程实现
                                          //子进程的cd变换的只是子进程的路  径,父进程不会变
          {
              if(g_argv[1] != NULL)
              {
                  chdir(g_argv[1]);//改变工作目录
              }
              continue;
          }
  
          //5.fork()
          pid_t id = fork();
          if(id == 0)//child
          {
              if(sep != NULL)
              {
                  int fd = -1;
                  switch(redir_status)                                    
                  {
                      case INPUT_REDIR:
                          fd = open(sep, O_RDONLY);
                          dup2(fd, 0);
                          break;
                      case OUTPUT_REDIR:
                          fd = open(sep, O_WRONLY | O_TRUNC | O_CREAT, 066  6);
                          dup2(fd, 1);
                          break;
                      case APPEND_REDIR:
                          fd = open(sep, O_WRONLY | O_CREAT | O_APPEND, 06  66);
                          dup2(fd, 0);
                          break;
                      default:
                          printf("BUG?\n");
                          break;
                  }
              }
             // printf("child, MYVAL: %s\n", getenv("MYVAL"));
             // printf("child, PATH: %s\n", getenv("PATH"));
              execvp(g_argv[0], g_argv);
              exit(1);
          }
  
          //father
          int status = 0;
          pid_t ret = waitpid(id, &status, 0);//阻塞等待
          if(ret > 0)
          {
              printf("exit code: %d\n", WEXITSTATUS(status));
          }
      }
      return 0;
  }

6.stdout和stderr的区别

Linux知识点 -- 基础IO(二)_第16张图片
上面的代码分别向stdout和stderr文件写入了字符;

  • 直接运行的结果是所有字符全部打印到显示器上,说明stdout和stderr都对应的是显示器文件;
    Linux知识点 -- 基础IO(二)_第17张图片
  • 如果将打印的结果重定向到log.txt中,结果会发生变化,并不是所有的字符都写入了log.txt;
    Linux知识点 -- 基础IO(二)_第18张图片
    可以看出:重定向过后,只有向1号fd中写的内容被重定向写入到文件中,2号fd的内容依然打印在显示器上;

这是因为:1和2号fd对应的都是显示器文件,但是是不同的,可以认为是同一个显示器文件被打开了两次;
因此重定向后只有stdout的内容写入了log.txt,而stderr的内容依然打印到了屏幕上;
Linux知识点 -- 基础IO(二)_第19张图片

7.常规的重定向操作

  • 上面的代码中2号fd 的文件无法重定向到普通文件中,经过如下操作:
    Linux知识点 -- 基础IO(二)_第20张图片
    执行了这条语句后,1号和2号fd的文件内容都重定向到了log.txt中;
    其中2>&1的意思是把1的地址拷贝给2,则2也指向1的显示器文件了,1和2指向的是同一个显示器文件;

  • 文件拷贝
    Linux知识点 -- 基础IO(二)_第21张图片
    这条指令的意思是先将log.txt的内容重定向输入给cat打印出来,再将打印的结果重定向到back.txt,就相当于把log.txt的内容拷贝给back.txt;

8.perror的实现

Linux知识点 -- 基础IO(二)_第22张图片
在这里插入图片描述
perror是会打印出错误信息的,这是因为函数中使用了strerror接口,来打印错误信息;

二、Linux下一切皆文件

Linux知识点 -- 基础IO(二)_第23张图片
所有的Linux文件结构体中都会有读函数和写函数的指针;Linux知识点 -- 基础IO(二)_第24张图片
虽然底层不同的硬件,一定对应的是不同的操作方法;
但是上面的设备都是外设,每一个设备的核心访问函数都可以是read、write,每一个文件中的读写函数指针都可以指向这两个函数;
读写代码的实现是不一样的,但是在操作系统看来,都是读写,没有任何硬件的差别了;
因此,Linux下一切皆文件;

三.缓冲区

1.缓冲区

  • 由上可知:使用dup2进行输出重定向时,运行程序后,使用cat指令打印log.txt能够直接打印出来;
  • 如果使用系统指令进行重定向:
    Linux知识点 -- 基础IO(二)_第25张图片
    Linux知识点 -- 基础IO(二)_第26张图片
    直接打印是打印不出来的;
    如果在输出重定向后加上fflush刷新缓冲区,就可以将内容输出到log.txt了:
    Linux知识点 -- 基础IO(二)_第27张图片
    Linux知识点 -- 基础IO(二)_第28张图片

这种现象与缓冲区有关;

  • 缓冲区:就是一段内存空间;
  • 缓冲区的存在主要是为了提高整机效率,提高用户的响应速度;
  • 缓冲区的刷新策略主要有:
    (1)立即刷新;
    (2)行刷新(行缓冲 \n)
    (3)满刷新(全缓冲)
    特殊情况:
    (1)用户强制刷新(fflush)
    (2)进程退出

2.关于缓冲区的认识

一般而言:

  • 行缓冲的设备文件 – 显示器
  • 全缓冲的设备文件 – 磁盘文件

所有的设备,永远都倾向于全缓冲;缓冲区满了,才刷新,这样就需要更少次数的IO操作,更少次的外设访问,能够提高效率;
和外部设备IO的时候,数据量的大小不是主要矛盾,和外设预备IO的过程是最耗费时间的;
显示器,是要给用户看的,一方面要照顾效率,一方面还要照顾用户体验;

3.用户缓冲区与内核缓冲区

下面一段代码:
Linux知识点 -- 基础IO(二)_第29张图片

  • 正常打印:
    Linux知识点 -- 基础IO(二)_第30张图片
    打印出4条;
  • 重定向到log.txt打印:
    Linux知识点 -- 基础IO(二)_第31张图片
    就会打印出7条;
    Linux知识点 -- 基础IO(二)_第32张图片
    我们是在最后调用的fork,创建子进程之前,上面的语句已经被执行完了;
    向显示器打印时,只打印出了4行文本;
    而向普通文件(磁盘上)打印时,就变成了7行:
    C语言的IO接口是打印了两次;
    系统接口只打印了一次;

上面的代码,并不影响系统接口,如果有缓冲区,那这个缓冲区一定是由C标准库维护的,因为如果是由OS维护的,那上面的代码应该都是一样的效果;

  • 用户缓冲区:
    C标准库为我们提供了用户级的缓冲区,我们平常使用的就是这个,在执行IO操作时,我们先将数据写入用户缓冲区中,再调用系统的IO接口(read、write等)将数据从用户缓冲区写入到内核缓冲区中,而不是直接写入到文件中;
    一旦拷贝完成,该数据就属于内核数据了,再由OS写入文件;
  • 内核缓冲区
    操作系统中也有内核级的缓冲区,用来接收用户缓冲区的数据,并写入到文件中
    Linux知识点 -- 基础IO(二)_第33张图片
  • 解释现象:
    如果向显示器中打印,刷新策略是行刷新,那么最后执行fork的时候,一定是函数执行完了,且数据已经被刷新了;
    如果对应的程序进行了重定向,要向磁盘文件中打印,隐形的将刷新策略变成了全缓冲,那么字符串最后的\n就没有意义了,这是用来进行行缓冲的;
    因此,在fork的时候,一定是函数已经执行完了,但是数据还没有刷新,还在当前进程对应的C标准库的缓冲区中,这部分数据就是父进程的数据
    而fork一旦执行,创建子进程时发生了写时拷贝,父进程的数据拷贝给了子进程,代码结束后刷新缓冲区,就会将C接口的数据打印两份给磁盘文件;

4.用户缓冲区的位置

FILE结构体中不仅封装了文件描述符fd,也封装了该文件fd对应的语言层缓冲区结构;

  • 如果在fork之前强制刷新:
    Linux知识点 -- 基础IO(二)_第34张图片
    就会变成打印4条:
    Linux知识点 -- 基础IO(二)_第35张图片
  • fflush只需传入stdout就能够将数据刷新到缓冲区,就是因为我们打开的文件在进程中的FILE结构体中封装了用户缓冲区的结构;
    Linux知识点 -- 基础IO(二)_第36张图片

C语言中打开的FILE文件流,必须包含:

  • 文件描述符fd;
  • 缓冲区buffer;

5.自己设计用户缓冲区

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define NUM 1024

struct MyFILE_
{
    int fd;//文件描述符
    char buffer[NUM];//缓冲区
    int end;//当前缓冲区的结尾
};

typedef struct MyFILE_ MyFILE;

MyFILE* fopen_(const char* pathname, const char* mode)
{
    assert(pathname);
    assert(mode);

    MyFILE* fp = NULL;

    if(strcmp(mode, "r") == 0)
    {

    }
    else if(strcmp(mode, "r+") == 0)
    {

    }                                                                     
    else if(strcmp(mode, "w") == 0)
    {
        int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666);
        if(fd  >= 0)
        {
            fp = (MyFILE*)malloc(sizeof(MyFILE));                         
            memset(fp, 0, sizeof(MyFILE));
            fp->fd = fd;
        }
    }
    else if(strcmp(mode, "w+") == 0)
    {

    }
    else if(strcmp(mode, "a") == 0)
    {

    }
    else if(strcmp(mode, "a+") == 0)
    {

    }
    else 
    {

    }

    return fp;
}

void fputs_(const char* message, MyFILE* fp)
{
    assert(message);
    assert(fp);

    strcpy(fp->buffer + fp->end, message); 
    fp->end += strlen(message);

    //暂时没有刷新,刷新策略是用户通过执行C标准库中的代码逻辑,来完成刷新>动作
    //这里效率提高,因为C提供了缓冲区,我们可以通过刷新策略,较少了IO的执>行次数
    
    if(fp->fd == 0)
    {
        //标注输入                                                        
    }
    else if(fp->fd == 1)
    {
        //标准输出
        if(fp->buffer[fp->end - 1] == '\n')//如果缓冲区数据最后以\n结尾,>就立即刷新
        {
            write(fp->fd, fp->buffer, fp->end);
            fp->end = 0;
        }
    }
    else if(fp->fd == 2)
    {
        //标准错误
    }
    else 
    {
        //其他文件
    }
}

void fflush_(MyFILE* fp)
{
    assert(fp);

    if(fp->end != 0)
    {
        write(fp->fd, fp->buffer, fp->end);//将数据写入内核
        syncfs(fp->fd);//将输入写入磁盘
        fp->end = 0;
    }
}

void fclose_(MyFILE* fp)
{                                                                         
    assert(fp);
    fflush_(fp);
    close(fp->fd);
    free(fp);
}

int main()
{
    MyFILE* fp = fopen_("./log.txt", "w");
    if(fp == NULL)
    {
        perror("open file error");
        return 1;
    }

    fputs_("lmx uio", fp);

    fork();

    fclose_(fp);


    return 0;
}

运行结果:
Linux知识点 -- 基础IO(二)_第37张图片

你可能感兴趣的:(Linux,linux,服务器)