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 2

int 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.txt
    char* 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.txt
            pos++;
            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.txt
    check_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;
    }

    // 特殊处理一下ls
    if (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.txt
        interact(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()函数一样。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chihiro1122

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值