【基础IO】-4-再谈重定向

再谈重定向

1、实验中的有趣现象

还记得我们验证文件描述符fd分配规则的实验吗?我们当时是关闭了0号文件流stdin,然后新建文件,发现新建文件的fd是0。

假如我们关闭fd == 1的文件呢?

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

int main()
{
    close(1); // 关闭stdout

    int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
    printf("log.txt: %d\n", fd);
    fflush(stdout);
    close(fd);

    return 0;
}






本该写入到屏幕上的信息,却写入到了新建的log.txt中------这不就是输出重定向吗?

所以,我们通过关闭fd == 1的文件,再新建文件,然后用printf企图向标准输出流中做写入,结果内容写入到了文件log.txt,从而间接地完成了输出重定向的工作。

2、重新理解重定向

操作系统是只认文件描述符fd的,文件操作相关的系统调用几乎都需要fd的参与。

标准输出流stdout,底层封装的文件描述符fd是fd == 1。未来我们想要对标准输出流做写入,默认是写入到文件描述符为fd == 1的文件中的。







现在关闭了fd == 1的文件,即关闭了stdout标准输出流。然后我们新建文件log.txt,打开log.txt返回的文件描述符就是fd == 1。

而想要继续向标准输出流做写入的时候,由于操作系统只认fd == 1的文件流为标准输出流,所以就会去打开文件表fd_array中找下标为1的文件,结果找到了log.txt。

以上就是输出重定向的全过程。输出重定向的本质是:

  • 做标准输出的时候,进程只会写入到文件描述符fd == 1的文件当中,默认情况下是stdout标准输出流;
  • (打开文件表)fd_array中下标为 1 的文件,由stdout变为了用户指定的文件。

同理,追加输出重定向的本质与输出重定向的非常相似。它们的唯一的区别是:

  • 输出重定向,会清空历史内容;
  • 追加输出重定向,不会清空,只会在文件内容的末尾继续写入。

类似的,输入重定向的本质是:

  • 做标准读取的时候,进程只会读取文件描述符fd == 0的文件,默认情况下是stdin标准输入流;
  • (打开文件表)fd_array中下标为 1 的文件,由stdin变为了用户指定的文件。

3、直接完成重定向的系统调用------dup

重定向操作,本质上是(打开文件表)fd_array中file*指针的交换,即文件描述符fd作为fd_array数组的下标,对应的file*指针,的交换。

如果我们采用上面的"关闭文件再打开"的方式,显得比较麻烦。Linux为我们提供了一个操作系统------dup。




c 复制代码
int dup2(int oldfd, int newfd);



系统调用dup的作用是:

  • 关闭当前newfd指向的文件;
  • 将oldfd上的file*指针,拷贝给newfd。

所以输出重定向,可以这样用dup实现:

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

int main(int argc, char *argv[])
{
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); // 不追加是O_TRUNC(清空)
    dup2(fd, 1);
    printf("%s\n", argv[1]);
    close(fd);

    return 0;
}



追加输出重定向,可以这样用dup实现:

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

int main(int argc, char *argv[])
{
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666); // 追加是O_APPEND
    dup2(fd, 1);
    printf("%s\n", argv[1]);
    close(fd);

    return 0;
}



输入重定向,可以这样用dup实现:

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

int main()
{
    int fd = open("log.txt", O_RDONLY, 0666);
    dup2(fd, 0);
    
    int a = 0;
    float f = 0.0f;
    char c = 0;

    scanf("%d %f %c", &a, &f, &c);
    printf("%d %f %c\n", a, f, c);

    close(fd);

    return 0;
}



学会使用dup之后,我们就可以继续完善我们自主实现的shell,即加上重定向操作。

4、完善自主shell------加入重定向操作

注意我们当前加入的重定向操作,是只针对普通命令,即需要bash创建子进程执行的命令,的重定向操作。

往自主shell中加入重定向操作,需要经过以下两个步骤:

  1. 完善执行命令函数Execute。
    • 定义全局的标识符redir_type,表示重定向操作的具体选项:
      • 不重定向#define NONE_REDIR 0
      • 输入重定向#define IN_REDIR 1
      • 输出重定向#define OUT_REDIR 2
      • 追加输出重定向#define NONE_REDIR 3
    • 以后在Execute中,检测redir_type发生的变化:
      • 若redir_type == 0,不做任何操作
      • 若redir_type == 1,检测到需要做输入重定向,stdin流换成指定文件流dup2(fd, 0)
      • 若redir_type == 2,检测到需要做输出重定向,stdout流换成指定文件流dup2(fd, 1)
      • 若redir_type == 3,检测到需要做追加输出重定向,stdout流换成指定文件流dup2(fd, 1)
  2. 添加函数Redir,对从用户方读取的命令字符串做预处理:
    • 看看字符串中有没有< > >>,以设置redir_type
    • 定义全局的重定向文件名redir_filename,提取字符串中重定向操作符右侧的文件名写入redir_filename



c 复制代码
// Execute中创建子进程的部分
else if (0 == id)
{
    int fd = 0;
    if (IN_REDIR == redir_type)
    {
        fd = open(redir_filename, O_RDONLY);
        dup2(fd, 0);
    }
    else if (OUT_REDIR == redir_type)
    {
        fd = open(redir_filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
        dup2(fd, 1);
    }
    else if (APP_REDIR == redir_type)
    {
        fd = open(redir_filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
        dup2(fd, 1);
    }
    else
    {
        // do nothing
    }

    execvp(argv[0], argv);
    exit(12);
}



c 复制代码
void Bash()
{
    static char *env[256];
    _environ = env;
    LoadEnv();
	while (1)
	{
		PrintPrompt();

        GetCommandLine();
        if (0 == strlen(commandline))
            continue;

        Redir(); // 获取用户输入后,先对可能包含的重定向操作做处理,再分析命令

        ParseCommandLine();

        if (CheckBuildinAndExecute())
            continue;

        Execute();
	}
}



c 复制代码
static void Redir()
{
    char *start = commandline;
    char *end = &commandline[strlen(commandline) + 1]; // 左闭右开
    while (start != end)
    {
        if ('>' == *start)
        {
            if ('>' == *(start + 1))
            {
                redir_type = APP_REDIR;
                *start = '\0';
                start += 2;
            }
            else
            {
                redir_type = OUT_REDIR;
                *start = '\0';
                start++;
            }
            Clear_Left_Space(start); // 宏替换的技巧:1. 直接写不利于观察。2. 写成函数,要传二级指针
            // 过滤空格,找到重定向的文件名
            redir_filename = start;
            break;
        }
        else if ('<' == *start)
        {
            redir_type = IN_REDIR;
            *start = '\0';
            start++;
            Clear_Left_Space(start);
            redir_filename = start;
            break;
        }
        else 
        {
            ++start;
        }
    }
}



处理重定向操作的函数Redir就很有讲究。

首先是寻找重定向操作符,设置redir_type。定义char*指针start遍历输入字符串,

  • 遇到'<',就是输入重定向
  • 遇到'>',
    • 如果start下一位也是'>',那么就是追加输出重定向
    • 否则就只是输出重定向

设置完redir_type之后,我们可以将重定向操作符改写成'\0',这样做是对用户输入字符串做"切割",切出操作符之前的命令(这样后续在解析函数ParseCommandLine中,strtok遇到\0就会停下)。

再然后,就要提取redir_filename。我们可以使start跳过操作符,继续向后找文件名。而操作符之后,

  • 可能直接就是文件名
  • 也可能是许多前导空格

所以我们需要做一个循环操作,过滤掉空格,

  • 直接写循环吗?太冗余了,代码看起来不太简洁。而且这个循环操作以后可能还要用
  • 外部设计成一个函数吗?那就要传递二级指针 ,才不会因为传值拷贝而不能对start进行修改

这里就涉及到一个编程技巧:将此处的循环操作设计成一个宏(替换)。




c 复制代码
#define Clear_Left_Space(pos) do{\ 
	while(isspace(*pos))\ 
		++pos; \
	}while(0)



宏的本质就是替换,代码在预处理阶段就会用这段循环操作替换Clear_Left_Space;同时do{}while(0)中套while循环,可以防止宏函数直接跟;造成的实际循环未执行的情况发生。

当start找到了一个非空的字符,意味着找到了文件名。就可以设置redir_filename。

添加重定向操作,还有一个重要的一点:一定要在下一次输入命令之前,重置redir_type redir_filename!







不然会发生意想不到的问题。

代码演示

相关推荐
Android系统攻城狮1 小时前
Linux Gstreamer深度解析之gst_audio_ring_buffer_parse_caps调用流程与实战(八十)
linux·运维·服务器·gstreamer音视频·音视频进阶
erzong2312 小时前
Linux网络:UDP编程踩坑日记
linux·网络·udp
盐焗鹌鹑蛋2 小时前
【Linux】线程库与封装
linux
lwx9148522 小时前
Deepin操作系统
linux·服务器·网络
先知后行。2 小时前
进程间通信(IPC)
linux·运维·服务器
weixin_571667413 小时前
Windows 通过 SSH 反向隧道向 Ubuntu 转发代理
linux·运维·windows·代理
乔旭伟3 小时前
CentOS 停更迁移实战:vault 源配置、目标选型与迁移工程清单
linux·运维·centos
鬼手点金3 小时前
ReAct 循环
linux·服务器·前端·javascript·react.js·前端框架
皓月盈江3 小时前
Linux Debian下STM32CubeProgrammer串口连接失败解决方案
linux·stm32·debian·串口下载·stm32f103c8t6·usb转ttl·stm32cubepro