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

这篇具有很好参考价值的文章主要介绍了Linux知识点 -- 基础IO(二)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Linux知识点 – 基础IO(二)


一、重定向

1.输出重定向

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

2.输入重定向

Linux知识点 -- 基础IO(二),Linux,linux,服务器
运行结果:
Linux知识点 -- 基础IO(二),Linux,linux,服务器

3.追加重定向

Linux知识点 -- 基础IO(二),Linux,linux,服务器
运行结果:
Linux知识点 -- 基础IO(二),Linux,linux,服务器

4.重定向系统调用

Linux知识点 -- 基础IO(二),Linux,linux,服务器
Linux知识点 -- 基础IO(二),Linux,linux,服务器
oldfd copy to the newfd -> 最后要和oldfd一样
Linux知识点 -- 基础IO(二),Linux,linux,服务器
最终重定向的fd要是3,dup2的运行结果是newfd和oldfd一样,因此这里3是oldfd,1是newfd;

  • 使用dup2实现输出重定向:
    Linux知识点 -- 基础IO(二),Linux,linux,服务器
    运行结果:
    Linux知识点 -- 基础IO(二),Linux,linux,服务器
    输出重定向到了log.txt中;

  • 使用dup2实现追加重定向:
    Linux知识点 -- 基础IO(二),Linux,linux,服务器
    Linux知识点 -- 基础IO(二),Linux,linux,服务器

5.minishell支持重定向

在进程控制章节我们自己写了shell程序,这里我们在其中添加重定向功能;
Linux知识点 -- 基础IO(二),Linux,linux,服务器
Linux知识点 -- 基础IO(二),Linux,linux,服务器
Linux知识点 -- 基础IO(二),Linux,linux,服务器
Linux知识点 -- 基础IO(二),Linux,linux,服务器

  #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的区别

Linux知识点 -- 基础IO(二),Linux,linux,服务器
上面的代码分别向stdout和stderr文件写入了字符;

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

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

7.常规的重定向操作

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

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

8.perror的实现

Linux知识点 -- 基础IO(二),Linux,linux,服务器
Linux知识点 -- 基础IO(二),Linux,linux,服务器
perror是会打印出错误信息的,这是因为函数中使用了strerror接口,来打印错误信息;

二、Linux下一切皆文件

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

三.缓冲区

1.缓冲区

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

这种现象与缓冲区有关;

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

2.关于缓冲区的认识

一般而言:

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

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

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

下面一段代码:
Linux知识点 -- 基础IO(二),Linux,linux,服务器

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

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

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

4.用户缓冲区的位置

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

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

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

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

5.自己设计用户缓冲区

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

运行结果:
Linux知识点 -- 基础IO(二),Linux,linux,服务器文章来源地址https://www.toymoban.com/news/detail-612133.html

到了这里,关于Linux知识点 -- 基础IO(二)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 售前工程师宝典:整理服务器最全知识点

    如何保证服务器可以支持百万用户访问?服务器品牌有哪些?如何选购服务器?对于这些问题,今天我们就一起来看下关于服务器的相关知识。 假如你开发了一个网站或者一个app把他放到服务器上,之后你把它发布到了网上,运行良好,每天有几百人的访问量,用户量不大,

    2024年02月19日
    浏览(42)
  • Linux知识点 -- Linux多线程(四)

    一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程

    2024年02月10日
    浏览(49)
  • Linux知识点 -- Linux多线程(三)

    持有锁的线程会频繁进入临界区申请临界资源,造成其他进程饥饿的问题; 这本身是没有错的,但是不合理; 线程同步:就是线程按照一定的顺序,进行临界资源的访问;主要就是为了解决访问临界资源和理性的问题;在保证数据安全的前提下,让线程能够按照某种特定的

    2024年02月11日
    浏览(50)
  • Linux相关知识点

    Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。 Linux内核 是一个Linux系统的内核,而不是一个操作系统 Linux操作系统 红帽操

    2024年02月11日
    浏览(44)
  • linux运维知识点

    Linux作为一种开源操作系统,被广泛地应用于企业、政府和私人用户的计算机系统中,其优越的稳定性、安全性和灵活性使得 Linux 成为了云计算、大数据和人工智能等最热门领域的重要组成部分。对于从事 Linux 运维的人员来说,了解其知识点和技能是必不可少的。本文将从以

    2024年02月15日
    浏览(52)
  • Linux知识点 -- 进程概念(补充)

    在用户每次使用malloc等函数在进程的堆区申请地址时,用户只需要指定空间的大小,并且会得到一个起始地址,而不会得到结束地址; 因为 堆区的结构都是由 vm_area_struct 管理的,每次malloc都会申请一个该结构体; malloc在堆上申请空间时,只需要知道起始地址,不需要知道结

    2024年02月13日
    浏览(46)
  • 关于Linux同步机制知识点整理

    在Linux系统中,同步机制是操作系统中非常重要的一部分,以下是一些基本要点: 互斥锁 互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,失败的线程B于是就会释放 CPU 让给其他线程

    2024年02月11日
    浏览(48)
  • Linux知识点 -- 进程间通信(二)

    先在内存中申请空间,然后将这段空间映射到不同进程的地址空间中,这就叫做共享内存; 一般都是映射在进程的堆栈之间的共享区; 共享内存不属于任何一个进程,它属于操作系统; 操作系统对共享内存的管理,是先描述再组织,先通过内核数据结构描述共享内存的属性

    2024年02月14日
    浏览(50)
  • 【知识点】linux下启动tomcat

    切换到tomcat安装目录下的bin目录。 如不知安装目录,可以使用: 查找。 进入bin目录,通过命令启动。 (该方式是直接后台启动。当关闭linux会话窗口,tomcat服务也随之关闭。) (该方式启动,会显示日志,不能输入linux命令。当关闭linux会话窗口,tomcat服务也随之关闭。)

    2024年02月08日
    浏览(49)
  • C#知识点-13(进程、多线程、使用Socket实现服务器与客户端通信)

    进程 定义:每一个正在运行的应用程序,都是一个进程  进程不等于正在运行的应用程序。而是为应用程序的运行构建一个运行环境 多线程 这段代码在执行完成之前,程序会被卡死(不能操作程序,包括关闭窗口)。因为我们程序在做一些耗时操作的时候,如果主线程去执

    2024年02月22日
    浏览(92)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包