Linux - 基础IO(重定向 - 重定向模拟实现 - shell 当中的 重定向)- 下篇

前言

上一篇博客当中,我们对 文件 在操作系统当中是 如何就管理的,这个问题做了 详细描述,本篇博客将基于上篇 博客当中的内容进行 阐述,如有疑问,请参考上篇博客:

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇-CSDN博客

重定向

文件描述符的 分配规则

我们先来看一个例子:

此时我们先 关闭 0 号文件,也就是 stdin 这个文件,然后在使用 open()系统调用接口来 创建一个新的文件,打印这个新文件的 fd 。输出:
 

发现新文件的 fd 文件描述符是 0,也就是原本的 stdin 这个文件描述符。


 同样,我们把 1 号文件 和 2 号文件都试一试,先是 1 号文件:

输出:

发现并没有输出,因为 printf()函数其中使用的系统调用接口就是 1 号文件的 stdout 这个流。

但其实,输出就是1 。

此时,其实是几输入到文件当中了的,只是当前的进程的 stdout 已经被关闭了,所以进程当中是无法使用 printf()函数打印出信息的:

 


 关闭 2 号文件:

输出:
 

 发现,此时新打开的文件的文件描述符 fd 就是 开始关闭的 2 号文件。

所以,按照以上的 输出,其实我们已经可以得出结论了:

在 Linux 当中的文件描述符的分配规则是:


如果有新的文件要打开,那么就要为这个文件分配一个 新的 没有被其他文件使用过的 文件描述符 fd。

分配是按照 : 从 文件描述符表当中的 0 号下标位置处 开始,寻找最小的 还没有使用过的 数组位置,这个位置的下标就是新文件的 fd 文件描述符

 重定向理解 - 重定向的原理

像上述的,把 1 号文件 也就是 stdout 这个显示器文件 给关闭了, 所以我们使用printf()函数就是不能在显示器当中输出数据了。

但是,上述也说过,虽然不会在 显示器当中输入, 但是会在 log.txt 这个新打开的文件当中,把原本应该输出在显示器当中的数据,写入到 log.txt 这个文件当中。

这是为什么呢?
其实看上述的代码你应该也知道了,因为 write()函数是固定像 1 号文件当中写入 数据,但是 1 号文件在 log.txt 这个新文件打开之前就被 关闭了。

也就是说,在 log.txt 这个新文件打开之前, 1 号 fd 文件描述符已经空闲下来了,所以, 新文件就会直接使用 1 号这个 fd 文件描述符。

所以,在后序循环当中 使用 write()函数在 1  号文件当中写入数据,实际是在 log.txt 当中写入数据。

其实,像上述的过程,就是一种重定向

如上图所示,原本 1 号 文件描述符指向的是 显示器文件,但是现在指向的是 log.txt 这个文件了。

而这个变化的过程, 编译器在执行之时 ,也就是 while()循环当中的代码在执行之时,其实是不知道的,他只知道,现在要向 1 号文件当中写入 数据,但是它不知道 1 号文件,此时是 显示器文件 还是 log.txt 文件,还是其他什么文件,他是不知道的。

 而,上述的过程,就是重定向的 原理

 所以,在上图当中的  fd_array [] 数组当中的 每一个元素存储的是 文件对象的起始地址,修改 几号fd ,其实就是修改  fd_array [] 数组当中的 fd 下标位置的 文件对象的地址地址指针。

而,在代码层面,他不管他现在的任务是要在哪一个文件当中写入数据,这个文件在哪,这个文件的地址是多少,这些他都不关系;它只关心,这是几号文件,要在哪一个 fd 的文件的文件当中写入数据。

 重定向的系统调用接口

有三个调用接口。我们主要来谈谈 dup2()这个函数:

 使用 dup2()函数,就可以不向上述一样显示的使用 close()函数来关闭文件,来实现重定向的 功能,直接使用这个 dup2()函数即可

使用 dup2()函数,就不用在关闭 1 号文件了当我们在 程序当中已经打开了 某一个文件,创建了这个文件的 文件描述符,那么在这个 文件描述符指向的 文件描述符表的 下标位置,就存储了这个文件的 文件对象的地址。

使用 dup2()函数,直接 把 某一个文件 在 文件描述符表当中存储的 文件对象的地址,直接拷贝到 需要重定向的 在 文件描述符表当中存储的 文件对象的地址 当中。如下图所示:

  


而 dup2()函数的两个参数:
 

在上述说过的拷贝的过程当中,谁是 "oldfd" , 谁是 "newfd" 呢?

在上述例子当中, 1 号文件是要被 fd 也就是 log,txt 文件对象地址所拷贝的,1 号 是 被拷贝;fd 是拷贝。

所以,在上述例子当中 1 号是  "newfd"; fd 是 "oldfd" 

dup2(fd , 1);

dup2() 接口例子测试

输出重定向 

 还是上述的例子,只不过,此时不使用 close()关闭文件来实现了,而是使用 dup2()函数来实现:
 

输出:
 

在原本是空的 log.txt 文件当中,在运行 text 可执行程序之后,log.txt 当中已经被写入了数据。

 现在的输出结果和上述 使用 close()关闭文件实现的效果是一样的 。

像上述我使用的是 O_TRUNC ,是先清空 文件当中的内容,然后在从头开始写入数据,对应的就是 ">" 这个输出重定向。

如果我们把 O_TRUNC  换成 O_APPEND,就是追加的方式来写入数据,对应的就是 ">>"这个输出重定向。


( 输入重定向) - 使用 read()系统调用接口 和 dup2()

其中的 count 是我们期望 read()函数读取多少个 字节的内容,返回值 ssize_t(这个是有符号整数) 是实际read()函数读取到多少字节的内容。写入到 buf 当中。

比如,count 期望大小我们填入 1024个字节,fd 文件我们选择 0 号 键盘文件,那么,他就会一直阻塞等待我们在键盘当中输入数据到缓冲区当中读取。

 在 open()函数当中,我们可以使用 O_RDONLT 这个参数,代表的意思就是 只读

 输出:
 


 此时,我们使用read()函数在 log.txt 文件当中读取内容,在读取之前,使用 dup2()函数,把 0 号文件(也就是键盘文件 的 fd 值) ,直接替换为 log.txt 文件对象的地址:

此时输出:
 

此时,运行程序,直接把文件当中的内容给输出出来了。 

 此时我们使用 O_RDONLT 这个参数,就实现了 "<" 输入重定向的操作。

这不就是cat命令吗?

直接使用 cat 命令,就可以等待 我们在键盘当中的输入,然后把输入内容打印出来:

或者是 使用 "<" 来向文件当中,读取文件数据并打印:
 

 使用 printf()向文件当中写入数据

在这个例子当中,我们把 1 号文件,利用 dup2()函数 把 1 号文件的文件指针修改为 log.txt 文件,此时,printf()函数 fprintf()就在  log.txt 文件当中写入数据了
 

在上述输出当中, 在显示器当中没有输出,但是在 log.txt 文件当中已经 有数据了。 

 所以,在上述,就算我们 close(fd),这个程序同样是会在 log.txt 文件当中的输入数据的,因为,此时 log.txt 文件对象的地址不只是 fd 保存的,还有 1 号文件描述符也是保存了 log.txt 的文件对象的地址。

而且,我们是在 1 号文件当中写入的 ,所以,是不会影响在log.txt 文件当中写入数据的。

 shell 当中的 输入/输出重定向 的实现 概述

 上述我们已经介绍了 输入/输出重定向 的简单实现,所以,在shell 当中,其实这些 输入/输出重定向 命令是属于 内建命令,直接在内建命令当中 判断 类似 ">>" ">" "<" 这样的字符子串,就可以判断,当前是不是 输入/输出重定向 的操作,就可以执行上述所实现的逻辑。

ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt

 向上述的操作,我们可以发现,  ">>" ">" "<" 这样的字符子串 都是被空格 分隔开的,我们可以利用 之前在shell 模拟实现当中 分割字符串的操作,来提取出  ">>" ">" "<" 这样的字符子串。从而判断当前是不是  输入/输出重定向 操作。

(对于 shell 的模拟实现,参考这篇博客:Linux - 实现一个简单的 shell-CSDN博客)

其实做的工作非常简单:

在知道了上述的 重定向实现原理之后,其实我们只需要做判断即可:
 

比如上述判断 ">" 和 ">>" ,如果 当前的字符是 ">" 的话,如果下一个字符还是   ">" 的话,说明是 ">>" ,反之亦然;找到  ">" 或者 "<" 先把这两个字符修改为 '\0' 作为分割,因为 在 ">" 或者 "<" 之前的是 要输入或者要输出的数据,而在 ">" 或者 "<" 后面的是 文件名。

我们使用 isspace()函数来判断空格:

 在空格之后就是我们想要的文件名。

找到之后,就要保存 两段的数据 命令 和 文件名


在 shell 父进程 创建子进程的之后 马上  ,就要判断当前是否是 需要执行 重定向操作的(如果是,还有判断 是 哪一个重定向操作):

如上所示,在判断 是哪一个重定向,然后就 按照对应的要求,修改 1 或者0 的其中一个 文件描述符值。

同时,文件打开方式也是区分 不同重定向操作的 步骤。0666 是 八进制 的 666 ,代表 用 open()函数创建的文件,是什么访问权限。(注意要减去 umack)

完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#include <fcntl.h>#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44#define NONE -1
#define IN_RDIR     0
#define OUT_RDIR    1
#define APPEND_RDIR 2int lastcode = 0;
int quit = 0;
extern char** environ;
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char* rdirfilename = NULL;
int rdir = NONE;// 自定义环境变量表
char myenv[LINE_SIZE];
// 自定义本地变量表const char* getusername()
{return getenv("USER");
}const char* gethostname1()
{return getenv("HOSTNAME");
}void getpwd()
{getcwd(pwd, sizeof(pwd));
}void check_redir(char* cmd)
{// ls -al -n// ls -al -n >/</>> filename.txtchar* pos = cmd;while (*pos){if (*pos == '>') // 判断当前是 ">" ">>" 还是 "<"{if (*(pos + 1) == '>') {   // 判断当前是 ">" 还是 ">>" *pos++ = '\0';*pos++ = '\0';while (isspace(*pos)) pos++;rdirfilename = pos;rdir = APPEND_RDIR;break;}else {      // 是 ">"          *pos = '\0';pos++;while (isspace(*pos)) pos++;rdirfilename = pos;rdir = OUT_RDIR;break;}}else if (*pos == '<')  // 是 "<"{*pos = '\0'; // ls -a -l -n < filename.txtpos++;while (isspace(*pos)) pos++;rdirfilename = pos;rdir = IN_RDIR;break;}else {//do nothing}pos++;}
}void interact(char* cline, int size)
{getpwd();printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname1(), pwd);char* s = fgets(cline, size, stdin);assert(s);(void)s;// "abcd\n\0"cline[strlen(cline) - 1] = '\0';//ls -a -l > myfile.txtcheck_redir(cline);  // 在上述打印完 命令行,保存命令之后,用这个函数判断// 命令当中是否有 重定向操作
}int splitstring(char cline[], char* _argv[])
{int i = 0;argv[i++] = strtok(cline, DELIM);while (_argv[i++] = strtok(NULL, DELIM)); // 故意写的=return i - 1;
}// 这个函数主要是实现 有 shell 父进程创建 子进程的过程
void NormalExcute(char* _argv[])
{pid_t 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);}//让子进程执行命令//execvpe(_argv[0], _argv, environ);execvp(_argv[0], _argv);exit(EXIT_CODE);}else {int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid == id){lastcode = WEXITSTATUS(status);}}
}// 这个函数当中是判断 和 实现一些内建命令的
int buildCommand(char* _argv[], int _argc)
{if (_argc == 2 && strcmp(_argv[0], "cd") == 0) {chdir(argv[1]);getpwd();sprintf(getenv("PWD"), "%s", pwd);return 1;}else if (_argc == 2 && strcmp(_argv[0], "export") == 0) {strcpy(myenv, _argv[1]);putenv(myenv);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] == '$') {char* val = getenv(_argv[1] + 1);if (val) printf("%s\n", val);}else {printf("%s\n", _argv[1]);}return 1;}// 特殊处理一下lsif (strcmp(_argv[0], "ls") == 0){_argv[_argc++] = "--color";_argv[_argc] = NULL;}return 0;
}int main()
{while (!quit) {// 1.rdirfilename 用于保存文件名, rdir 保存要输入/输出的数据方式(命令)rdirfilename = NULL;rdir = NONE;// 2. 交互问题,获取命令行, ls -a -l > myfile / ls -a -l >> myfile / cat < file.txtinteract(commandline, sizeof(commandline));// commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"// 3. 子串分割的问题,解析命令行int argc = splitstring(commandline, argv);if (argc == 0) continue;// 4. 指令的判断 // debug//for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);//内键命令,本质就是一个shell内部的一个函数int n = buildCommand(argv, argc);// 5. 普通命令的执行if (!n) NormalExcute(argv);}return 0;
}

shell 当中的 输入/输出重定向  和 进程替换之间的关系

 不知道你有没有发现,我们在 check_redir()函数当中判断,是否需要重定向操作,然后修改对应存储 重定向操作符前后的 命令 和 文件名。

用这两个 存储 重定向操作符前后的 命令 和 文件名 的两个全局变量的值,在 shell 父进程创建子进程 之后 马上,判断当前是否需要执行 重定向的操作。

 但是,如果需要执行重定向,那么 在 修改 文件描述符值 之后,说明此时是在 子进程 当中修改的 文件描述符值。

然后我们要进行程序替换,让子进程执行我们在命令行当中输入的命令。

 那么,在执行 程序替换之时不会替换掉 这些文件描述符表 当中存储的文件对象的地址么吗?

 答案是不会的

首先你要搞清楚的是,一个进程的 各个文件描述符值 是存储在那里的?

  是存储在 struct file_struct 这个结构体 当中的,在这个结构体当中有一个数组,这个数组 就被称作 -- 文件描述符表。在这个数组当中就存储了 这个进程当前所 打开的 各个文件的  文件对象的地址

所以,这是一个机构体,是被 操作系统做管理的结构体对象,所以这个 struct file_struct 这个结构体  和 进程 的 PCB对象一样,都属于 操作系统当中的 内核结构体

而 , 我们进行程序替换,替换的是 代码 数据,把 原本子进程 从 父进程那里拷贝或者共用的代码,进行直接拷贝 或者 写时拷贝的方式,替换为 我们在命令行当中输入 的命令的 代码,而数据也是跟着一起刷新的。

那么 这些 代码 和 数据是存储在哪里的?是存储在 内存的物理空间当中的,要注意区分。

 所以,进程历史打开的所有文件,都在 struct file_struct 这个结构体 当中的 特定数组当中存储了 文件描述符,程序替换不会修改到这个 结构体,所以,程序替换 和 fd 文件描述符 无关

 所以,我们上述在实现之时,才是先把重定向的工作做了,再去程序替换;这样,重定向当中要修改的 文件描述符,和 进程替换无关,那么就不会被修改到。

而且,我们判断 是否需要重定向 的 两个全局变量是在父进程当中存储的,是在创建子进程之前就 已经 完成判断了的,这些数据,在进行程序替换之时都会被替换掉。

 stdout 和 stderr 是区别

 其实两个都是 显示器文件,都是输出重定向来使用的。

两者我们发现,使用 fprintf()函数都可以在 屏幕上打印 数据。

但是,之所以要 使用 两个显示器文件是因为,一个是 normal 正常的 数据输出;另一个 是 error 错误码输出;他想做到一种分流的 作用,把 正常的数据 和 错误的数据分成种方式输出。

为什么呢?

别忘了,输出不仅仅是在 显示器上输出,还可以在 文件当中输出。

所以,可以用两个文件,一个存储 正常的输出数据,一个存储错误输出数据;而我们在使用两种显示器文件输出数据之时,就可以在两个文件当中进程输出不同的数据了。


比如,此时有些 正常数据 和 一些错误的数据要输出:

那么,我们可以使用不同的 显示器文件来 输出,达到分流的目的:
 

 上述这个命令,就是把 mytext 执行文件的输出结果,把 1 号文件的内容输出重定向到 normal.log 当中;把 2 号文件当中的内容 输出重定向到 arr.log 当中。(其中 1 可以不写(这些简写)

这样在两个文件当中就是不同 输出内容了。

 像这样是 先把 1 号文件文件描述符 对应的 文件 当中的内容输入到 all.log 当中,然后把 1 号文件描述符当中的内容写到 2 号文件描述符值当中(2>&1)

 注意,是直接把 1 号文件描述符当中的内容,拷贝到 2 号文件描述符当中,1 号文件描述符当中的内容就是 1 号文件描述符 对应 的文件的 地址。所以,是直接把 1 号文件地址 直接 拷贝到 了 2 号文件描述符当中,相当于是 dup2()函数一样。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git之分支与版本

&#x1f3ac; 艳艳耶✌️&#xff1a;个人主页 &#x1f525; 个人专栏 &#xff1a;《Spring与Mybatis集成整合》《Vue.js使用》 ⛺️ 越努力 &#xff0c;越幸运。 1.开发测试上线git的使用 1.1. 环境讲述 当软件从开发到正式环境部署的过程中&#xff0c;不同环境的作用…

adb and 软件架构笔记

Native Service&#xff0c;这是Android系统里的一种特色&#xff0c;就是通过C或是C代码写出来的&#xff0c;供Java进行远程调用的Remote Service&#xff0c;因为C/C代码生成的是Native代码&#xff08;机器代码&#xff09;&#xff0c;于是叫Native Service。 native服务…

可视化 | echarts中国地图散点图

改编自echarts添加地图散点 &#x1f4da;改编点 roam: false&#xff1a;不允许放缩拖动 地图颜色修改 geo: {show: true,top: 15%,map: name,label: {normal: {show: false},emphasis: {show: true,color: "#fff",}},roam: false,itemStyle: {normal: {areaColor…

深度学习 opencv python 实现中国交通标志识别 计算机竞赛_1

文章目录 0 前言1 yolov5实现中国交通标志检测2.算法原理2.1 算法简介2.2网络架构2.3 关键代码 3 数据集处理3.1 VOC格式介绍3.2 将中国交通标志检测数据集CCTSDB数据转换成VOC数据格式3.3 手动标注数据集 4 模型训练5 实现效果5.1 视频效果 6 最后 0 前言 &#x1f525; 优质…

2023 ChinaJoy后,Flat Ads成为游戏、社交出海的新选择

今年ChinaJoy 展会&#xff0c;共吸引了来自世界各地的 500 多家企业参展&#xff0c;预计吸引超过33万人次参观。ChinaJoy年年有&#xff0c;那今年对于行业来说有什么新变化呢&#xff1f; 01 出海热潮不减&#xff0c;新增客户明显提升 据不完全统计&#xff0c;展会期间前…

Spring源码系列-框架中的设计模式

简单工厂 实现方式&#xff1a; BeanFactory。Spring中的BeanFactory就是简单工厂模式的体现&#xff0c;根据传入一个唯一的标识来获得Bean对象&#xff0c;但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。 实质&#xff1a; 由一个工厂…

No179.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

《詩經别解》——國風·周南·雎鳩​​​​​​​

一、关于古文的一个认识 目前可以阅读的古文经典&#xff0c;大多是经历了几千年的传承。期间的武力战争、文化纷争、宗教侵袭、官僚介入及文人的私人恩怨与流派桎梏&#xff0c;印刷与制作技术&#xff0c;导致这些古文全部都已经面目全非。简单地说&#xff0c;你读到的都是…

4种最常用的LLM应用文本分块策略

在构建 LLM 应用程序时&#xff0c;分块&#xff08;Chunking&#xff09;是将大块文本分解成更小的片段的过程。 这是一项重要的技术&#xff0c;一旦我们使用LLM嵌入内容&#xff0c;它有助于优化我们从矢量数据库返回的内容的相关性。 在这篇博文中&#xff0c;我们将探讨它…

【K8s集群离线安装-kubeadm】

1、kubeadm概述 kubeadm是官方社区推出的一个用于快速部署kubernetes集群的工具。这个工具能通过两条指令快速完成一个kubernetes集群的部署。 2、环境准备 2.1 软件环境 软件版本操作系统CentOS 7Docker19.03.13K8s1.23 2.2 服务器 最小硬件配置&#xff1a;2核CPU、2G内存…

【Proteus仿真】【51单片机】多路温度控制系统

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使用按键、LED、蜂鸣器、LCD1602、DS18B20温度传感器、HC05蓝牙模块等。 主要功能&#xff1a; 系统运行后&#xff0c;默认LCD1602显示前4路采集的温…

同城跑腿服务预约小程序的作用如何

无论是互联网服务化加快还是前几年疫情冲击&#xff0c;在同城生活服务场景中出现了很多商机&#xff0c;如外卖跑腿、校园跑腿、代买代送等&#xff0c;无论公司还是个人都借势不断提升自己品牌的影响力&#xff0c;并且依赖朋友圈不断提升生意营收。 同城跑腿品牌不少&#…