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

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

文章目录


一、重定向

1.输出重定向


在上面的代码中,fprintf本来是向stdout中打印的,但是stdout关闭了,实际上fprintf事项fd是1的文件中打印,这里log.txt的fd就是1;

运行结果为:

这就叫做输出重定向;
上面的代码将stdout关闭了,并打开log.txt文件,则log.txt文件的fd就是1;
在系统中,stdout就代表着fd为1,所以默认就会向fd为1的文件中打印,而此时fd为1的文件是log.txt,因此就向该文件中打印了;

2.输入重定向

运行结果:

3.追加重定向

运行结果:

4.重定向系统调用



oldfd copy to the newfd -> 最后要和oldfd一样

最终重定向的fd要是3,dup2的运行结果是newfd和oldfd一样,因此这里3是oldfd,1是newfd;

  • 使用dup2实现输出重定向:

    运行结果:

    输出重定向到了log.txt中;

  • 使用dup2实现追加重定向:

5.minishell支持重定向

在进程控制章节我们自己写了shell程序,这里我们在其中添加重定向功能;



c 复制代码
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  #include <unistd.h>
  #include <sys/wait.h>
  #include <sys/types.h>
  #include <fcntl.h>
  #include <sys/stat.h>
  #include <assert.h>
  
  #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的区别

上面的代码分别向stdout和stderr文件写入了字符;

  • 直接运行的结果是所有字符全部打印到显示器上,说明stdout和stderr都对应的是显示器文件;
  • 如果将打印的结果重定向到log.txt中,结果会发生变化,并不是所有的字符都写入了log.txt;

    可以看出:重定向过后,只有向1号fd中写的内容被重定向写入到文件中,2号fd的内容依然打印在显示器上;

这是因为:1和2号fd对应的都是显示器文件,但是是不同的,可以认为是同一个显示器文件被打开了两次;

因此重定向后只有stdout的内容写入了log.txt,而stderr的内容依然打印到了屏幕上;

7.常规的重定向操作

  • 上面的代码中2号fd 的文件无法重定向到普通文件中,经过如下操作:

    执行了这条语句后,1号和2号fd的文件内容都重定向到了log.txt中;

    其中2>&1的意思是把1的地址拷贝给2,则2也指向1的显示器文件了,1和2指向的是同一个显示器文件;

  • 文件拷贝:

    这条指令的意思是先将log.txt的内容重定向输入给cat打印出来,再将打印的结果重定向到back.txt,就相当于把log.txt的内容拷贝给back.txt;

8.perror的实现



perror是会打印出错误信息的,这是因为函数中使用了strerror接口,来打印错误信息;

二、Linux下一切皆文件

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

三.缓冲区

1.缓冲区

  • 由上可知:使用dup2进行输出重定向时,运行程序后,使用cat指令打印log.txt能够直接打印出来;
  • 如果使用系统指令进行重定向:


    直接打印是打印不出来的;
    如果在输出重定向后加上fflush刷新缓冲区,就可以将内容输出到log.txt了:

这种现象与缓冲区有关;

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

2.关于缓冲区的认识

一般而言:

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

所有的设备,永远都倾向于全缓冲;缓冲区满了,才刷新,这样就需要更少次数的IO操作,更少次的外设访问,能够提高效率;

和外部设备IO的时候,数据量的大小不是主要矛盾,和外设预备IO的过程是最耗费时间的;

显示器,是要给用户看的,一方面要照顾效率,一方面还要照顾用户体验;

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

下面一段代码:

  • 正常打印:

    打印出4条;
  • 重定向到log.txt打印:

    就会打印出7条;

    我们是在最后调用的fork,创建子进程之前,上面的语句已经被执行完了;
    向显示器打印时,只打印出了4行文本;
    而向普通文件(磁盘上)打印时,就变成了7行:
    C语言的IO接口是打印了两次;
    系统接口只打印了一次;

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

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

4.用户缓冲区的位置

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

  • 如果在fork之前强制刷新:

    就会变成打印4条:
  • fflush只需传入stdout就能够将数据刷新到缓冲区,就是因为我们打开的文件在进程中的FILE结构体中封装了用户缓冲区的结构;

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

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

5.自己设计用户缓冲区

c 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <stdlib.h>

#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;
}

运行结果:

相关推荐
大树886 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠6 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush46 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5207 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz7 小时前
Maven依赖冲突
java·服务器·maven
不会C语言的男孩8 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈8 小时前
Unix 与 Linux 异同小叙
linux·服务器·unix
程序猿阿伟9 小时前
《Chrome离线扩展安装的底层逻辑与场景落地指南》
服务器·网络·chrome
凡人叶枫9 小时前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
AC赳赳老秦9 小时前
用 OpenClaw 搭建服务器故障应急响应系统,自动处理 80% 常见运维故障
android·运维·服务器·python·rxjava·deepseek·openclaw