【Linux】基础IO(下):输出重定向、dup2 与 minishell 重定向实战

输出重定向、dup2 与 minishell 重定向实战

一、输出重定向:先看现象

1.1 关闭 1 号后打开文件

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

int main()
{
    close(1);                                          // ① 关闭标准输出
    int fd = open("myfile", O_WRONLY|O_CREAT, 00644);  // ② 打开文件
    if(fd < 0){
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);   // ③ 打印 fd
    fflush(stdout);           // ④ 冲刷 stdout 缓冲
    close(fd);
    exit(0);
}

运行发现:

  • 屏幕上什么都没显示;
  • 打开 myfile 一看------fd: 1 竟然写进了文件里!

现象:

本来应该输出到显示器上的内容,输出到了文件 myfile 当中(fd=1)。

这种现象叫做输出重定向。

shell 里常见的重定向有:>(覆盖)、>>(追加)、<(输入)。

1.2 为什么 printf 的内容跑进了文件?

回顾上一篇的 fd 分配规则:找到 fd_array 中最小未使用的下标。

cpp 复制代码
初始:   [0]=标准输入  [1]=标准输出  [2]=标准错误  [3]=空 ...
close(1):[1]=空 !
open("myfile") → 最小未用下标 = 1 → fd = 1

关键一步发生了:

文件描述符表里 1 号下标原本指向"显示器文件"的 file 对象,现在被改写成了指向 myfile 的 file 对象。

cpp 复制代码
                   task_struct                     files_struct              file 对象
              ┌────────────────────┐        ┌──────────────────┐    ┌──────────────────┐
              │        ...         │        │   file* fd_array[]│    │                  │
              │   struct files*    │───────►│  ┌──────────────┐ │    │  标准输入(键盘)   │
              │        files       │        │  │  0   │ file* │─┼───►│  显示器(原标准输出)│
              └────────────────────┘        │  │  1   │ file* │─┼───►│  myfile(新打开)   │
                                            │  │  2   │ file* │─┼───►│  标准错误(显示器) │
                                            │  │  3   │ file* │─┼───►│  ...              │
                                            │  └──────────────┘ │    └──────────────────┘
                                            └──────────────────┘
                                        ▲
                        1 号下标从"显示器"被改指到"myfile"

1.3 重定向的本质(一句话)

凡是往 1 号文件描述符写的内容,都写到了 myfile 当中,而不再写到标准输出!

  • 重定向不是"把数据从屏幕挪到文件"这种玄学操作;
  • 本质是修改文件描述符表里某个下标的指向------把 1 号(或 0 号)下标从默认设备改指到目标文件;
  • 之后所有对 fd=1 的写操作(printf、write(1,...) 等),自然就写进文件了。

二、dup2 系统调用

2.1 为什么还需要 dup2?

close(1) + open 虽然能完成重定向,但先关再开有窗口期(1 号临时空置,中间若出错整个 stdout 就没了),而且写法繁琐。

Linux 提供了一个"一步到位"的系统调用:dup2。

2.2 函数原型

cpp 复制代码
#include <unistd.h>

int dup2(int oldfd, int newfd);

语义:把 oldfd 指向的 file 对象,拷贝/复制到 newfd 这个下标上。

cpp 复制代码
dup2(fd, 1):
  让 1 号下标也指向 fd 所指向的 file 对象
  → 之后写 1 号 = 写 fd 指向的文件

和 close+open 的区别:

方式 过程 缺点
close(1) + open 先关 1,再 open 抢占 1 1 号有短暂空窗期,步骤繁琐
dup2(fd, 1) 直接把 fd 的指向复制给 1 一步完成,安全可靠

类比:

close+open 是"先辞职再入职",中间岗位空着;

dup2 是"一个人干两份活,把岗位直接挂到同一个员工名下"。

2.3 完整示例:把键盘输入回显到日志文件

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("./log", O_CREAT | O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    close(1);            // ① 关闭标准输出
    dup2(fd, 1);         // ② 让 1 号指向 fd(log 文件)

    for (;;) {           // ③ 循环读键盘
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0) {
            perror("read");
            break;
        }
        printf("%s", buf);    // ④ printf 输出 → 进 log 文件!
        fflush(stdout);       // ⑤ 冲刷缓冲
    }
    return 0;
}

效果:你在终端敲的内容,不再显示在屏幕上,而是全部写进了 log 文件。

2.4 printf 为什么也跟着走了?------stdio 与 fd 的关系

cpp 复制代码
printf 是 C 库的 IO 函数,一般往 stdout 中输出;
但 stdout 底层访问文件的时候,找的还是 fd:1;
而此时 fd:1 下标所指向的内容,已经变成了 log 文件的地址,不再是显示器文件的地址;
所以,输出的任何消息都会往文件中写入,进而完成输出重定向。

核心结论:

printf → stdout → fd:1 → fd_array[1] 指向谁,输出就进谁。

库函数再高级,最终都逃不过"往 fd 写"这一层------重定向能截住 printf 的输出,就是因为改的是 fd 表。

2.5 追加(>>)和输入(<)怎么完成?

  • 输出重定向 >:open(file, O_CREAT|O_WRONLY|O_TRUNC) + dup2(fd, 1)(覆盖写);
  • 追加重定向 >>:open(file, O_CREAT|O_WRONLY|O_APPEND) + dup2(fd, 1)(每次写都到末尾);
  • 输入重定向 <:open(file, O_RDONLY) + dup2(fd, 0)(让 0 号指向文件,read(0,...) 就从文件读)。

三个符号的本质完全一致:

open 目标文件 → dup2 把目标文件的指向复制到 0 或 1 号下标。

区别只在 open 的 flags 和 dup2 的目标下标。


三、3-6-5 在 minishell 中添加重定向功能

目标:

让上一篇写的 mini_shell 支持 ls -a -l > file.txt、cat < file.txt、echo hello >> log。

核****心新增两个函数:

ParseRedir(解析重定向符号)和 DoRedir(子进程里执行重定向)。

3.1 全局设计:重定向状态

cpp 复制代码
// 全局变量与重定向有关
#define NoneRedir  0   // 没有重定向
#define InputRedir 1   // 输入重定向 <
#define OutputRedir 2  // 输出重定向 >
#define AppRedir   3   // 追加输出重定向 >>

int  redir = NoneRedir;    // 当前命令的重定向类型
char *filename = nullptr;  // 重定向目标文件名

3.2 TrimSpace:去掉首尾空格的小工具(宏)

cpp 复制代码
// 跳过字符串前导空白字符
#define TrimSpace(pos) do{            \
    while(isspace(*pos)){             \
        pos++;                        \
    }                                 \
}while(0)

3.3 ParseRedir:从命令末尾往前找重定向符号

设计思路:

重定向符号在命令行的末尾(ls -a -l > file.txt),所以从后往前扫描,找到第一个 <、>、>>,把它和它前面的命令用 \0 隔开,并把后面的部分当作文件名。

cpp 复制代码
void ParseRedir(char command_buffer[], int len)
{
    int end = len - 1;                    // 从缓冲区末尾开始
    while(end >= 0)
    {
        if(command_buffer[end] == '<')    // ① 输入重定向 <
        {
            redir = InputRedir;
            command_buffer[end] = 0;                  // 把 < 替换成 \0,命令和文件分开
            filename = &command_buffer[end] + 1;      // 文件名 = < 后面
            TrimSpace(filename);                      // 去掉文件名前导空格
            break;
        }
        else if(command_buffer[end] == '>')           // ② 输出重定向 > 或 >>
        {
            if(command_buffer[end-1] == '>')          // 前一个还是 > → 追加
            {
                redir = AppRedir;
                command_buffer[end] = 0;              // 把 >> 两个位置都置 0
                command_buffer[end-1] = 0;
                filename = &command_buffer[end]+1;    // 文件名 = >> 后面
                TrimSpace(filename);
                break;
            }
            else                                      // 只有一个 > → 覆盖
            {
                redir = OutputRedir;
                command_buffer[end] = 0;
                filename = &command_buffer[end]+1;
                TrimSpace(filename);
                break;
            }
        }
        else
        {
            end--;                                    // 没找到就往前继续找
        }
    }
}

举例:输入 ls -a -l > file.txt

cpp 复制代码
原缓冲区: "ls -a -l > file.txt"
从末尾扫描 → 找到 '>'(前面不是 '>',是空格)→ OutputRedir
置 0 后:   "ls -a -l \0 file.txt"
filename = "file.txt"(TrimSpace 去前导空格)

之后 ParseCommand 再用 strtok 分割 "ls -a -l \0" 时,只会得到 ls、-a、-l 三个参数------重定向部分已经被干净地切掉了。

3.4 ParseCommand:按空格分割命令

cpp 复制代码
void ParseCommand(char command_buffer[])
{
    // "ls -a -l -n"
    const char *sep = " ";
    gargv[gargc++] = strtok(command_buffer, sep);   // 第一个参数
    // =是刻意写的(把赋值结果转 bool 再判断)
    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    gargc--;   // 最后一次 strtok 返回 nullptr 被多算了一次
}

3.5 ParseCommandLine:总入口

cpp 复制代码
void ParseCommandLine(char command_buffer[], int len)  // 3. 分析命令
{
    ResetCommandline();                 // 清空上一轮状态
    ParseRedir(command_buffer, len);    // 先解析重定向
    ParseCommand(command_buffer);       // 再解析命令
    // "ls -a -l -n"
    // "ls -a -l -n" > file.txt
    // "ls -a -l -n" < file.txt
    // "ls -a -l -n" >> file.txt
}

3.6 DoRedir:真正的重定向执行(核心)

两条铁律:

复制代码
1. 重定向应该让子进程自己做!
2. 程序替换会不会影响重定向? 不会

为什么必须子进程做?

因为重定向会永久修改进程的 fd 表。

如果在 shell 主进程里 dup2,shell 自己的 stdout 就没了,再也无法打印提示符------所以必须在 fork 出来的子进程里做,父进程(shell)的 fd 表毫发无损。

为什么 exec 不影响?

fd 表属于进程资源,程序替换(exec)只换代码和数据,不换 fd 表------所以子进程先重定向、再 exec,新程序的输出自然进文件。

cpp 复制代码
void DoRedir()
{
    // 1. 重定向应该让子进程自己做!
    // 2. 程序替换会不会影响重定向? 不会
    // 0. 先判断 && 重定向
    if(redir == InputRedir)                       // ① 输入重定向 <
    {
        if(filename)
        {
            int fd = open(filename, O_RDONLY);
            if(fd < 0)
            {
                exit(2);       // 打开输入文件失败
            }
            dup2(fd, 0);       // 0 号指向文件 → read(0) 从文件读
        }
        else
        {
            exit(1);
        }
    }
    else if(redir == OutputRedir)                 // ② 输出重定向 >
    {
        if(filename)
        {
            int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
            if(fd < 0)
            {
                exit(4);       // 创建输出文件失败
            }
            dup2(fd, 1);       // 1 号指向文件 → printf/write(1) 写文件(覆盖)
        }
        else
        {
            exit(3);
        }
    }
    else if(redir == AppRedir)                    // ③ 追加输出重定向 >>
    {
        if(filename)
        {
            int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
            if(fd < 0)
            {
                exit(6);       // 打开追加文件失败
            }
            dup2(fd, 1);       // 1 号指向文件,O_APPEND 保证每次写都在末尾
        }
        else
        {
            exit(5);
        }
    }
    else
    {
        // 没有重定向, Do Nothing!
    }
}

三种重定向对照表:

符号 redir 值 open flags dup2 目标 效果
< InputRedir O_RDONLY dup2 (fd, 0) 键盘输入 → 文件输入
> OutputRedir O_CREAT|O_WRONLY|O_TRUNC dup2 (fd, 1) 屏幕输出 → 文件(覆盖)
>> AppRedir O_CREAT|O_WRONLY|O_APPEND dup2 (fd, 1) 屏幕输出 → 文件(追加)

3.7 与执行流程的衔接

cpp 复制代码
// fork 后的子进程里,exec 之前调用 DoRedir:
void ExecuteCommand()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程
        DoRedir();                        // ① 先做重定向(改子进程自己的 fd 表)
        execvp(gargv[0], gargv);          // ② 再程序替换(fd 表不受影响)
        exit(127);                        // ③ exec 失败
    }
    // 父进程:waitpid 回收子进程,继续打印提示符
}
cpp 复制代码
shell 主循环:
  打印提示符 → fgets 读命令行 → ParseCommandLine(解析出 gargv/redir/filename)
  → fork
     ├─ 子进程:DoRedir() → execvp() → 新程序输出自动进文件
     └─ 父进程:waitpid → 继续下一轮循环

完整闭环:

ls -a -l > file.txt 敲下去 → ParseRedir 切出命令和文件名 → fork → 子进程 DoRedir(open file + dup2(fd,1))→ execvp("ls") → ls 的 stdout 全部写进 file.txt → 父进程回收,提示符照常打印。

四、本篇小结

cpp 复制代码
#include <iostream>
#include <ctype.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>
#include <sys/stat.h>
#include <fcntl.h>

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

// 下面是shell定义的全局数据

// 1. 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0; 

// 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;

// 3. 别名映射表
std::unordered_map<std::string, std::string> alias_list;

// 4. 关于重定向,我们关心的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
------------------------------------遇到空格跳过---------------------------------------
//#define TrimSpace(CMD,index) do{
//    while(isspace(*(CMD+index)))
//    {
//        index++;
//    }
//}while(0)

int redir = NONE_REDIR;
std::string filename;

// for test
char cwd[1024];
char cwdenv[2048];

// last exit code
int lastcode = 0;

const char *GetUserName()
{
    const char *name = getenv("USER");
    return name == NULL ? "None" : name;
}

const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    return hostname == NULL ? "None" : hostname;
}

const char *GetPwd()
{
    //const char *pwd = getenv("PWD");
    const char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
        putenv(cwdenv);
    }
    return pwd == NULL ? "None" : pwd;
}

const char *GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL ? "" : home;
}

void InitEnv()
{
    extern char **environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;

    //本来要从配置文件来
    //1. 获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        // 1.1 申请空间
        g_env[i] = (char*)malloc(strlen(environ[i])+1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
    g_env[g_envs] = NULL;

    //2. 导成环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}

//command
bool Cd()
{
    // cd argc = 1
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
    }
    else
    {
        std::string where = g_argv[1];
        // cd - / cd ~
        if(where == "-")
        {
            // Todu
        }
        else if(where == "~")
        {
            // Todu
        }
        else
        {
            chdir(where.c_str());
        }
    }
    return true;
}

void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

// / /a/b/c
std::string DirName(const char *pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir == SLASH) return SLASH;
    auto pos = dir.rfind(SLASH);
    if(pos == std::string::npos) return "BUG?";
    return dir.substr(pos+1);
}

void MakeCommandLine(char cmd_prompt[], int size)
{
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
    //snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}

void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

bool GetCommandLine(char *out, int size)
{
    // ls -a -l => "ls -a -l\n" 字符串
    char *c = fgets(out, size, stdin);
    if(c == NULL) return false;
    out[strlen(out)-1] = 0; // 清理\n
    if(strlen(out) == 0) return false;
    return true;
}

// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
    g_argc = 0;
    // 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
    g_argv[g_argc++] = strtok(commandline, SEP);
    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    g_argc--;
    return g_argc > 0 ? true:false;
}

void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]->%s\n", i, g_argv[i]);
    }
    printf("argc: %d\n", g_argc);
}

bool CheckAndExecBuiltin()
{
    //如果内键命令做重定向,更改shell的标准输入,输出,错误
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {
    }
    else if(cmd == "alias")
    {
       // std::string nickname = g_argv[1];
       // alias_list.insert(k, v);
    }

    return false;
}
------------------------------------重定向处理---------------------------------------
int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        int fd = -1;
        // 子进程检测重定向情况
        if(redir == INPUT_REDIR)
        {
            fd = open(filename.c_str(), O_RDONLY);
            if(fd < 0) exit(1);
            dup2(fd,0);
            close(fd);
        }
        else if(redir == OUTPUT_REDIR)
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
            if(fd < 0) exit(2);
            dup2(fd, 1);
            close(fd);
        }
        else if(redir == APPEND_REDIR)
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
            if(fd < 0) exit(2);
            dup2(fd, 1);
            close(fd);
        }
        else
        {}
        // 进程替换,会影响重定向的结果吗?不影响
        //child
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    int status = 0;
    // father
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
    return 0;
}

void TrimSpace(char cmd[], int &end)
{
    while(isspace(cmd[end]))
    {
        end++;
    }
}

void RedirCheck(char cmd[])
{
    redir = NONE_REDIR;
    filename.clear();
    int start = 0;
    int end = strlen(cmd)-1;

    //"ls -a -l >> file.txt" > >> <
    while(end > start)
    {
        if(cmd[end] == '<')
        {
            cmd[end++] = 0;
            TrimSpace(cmd, end);
            redir = INPUT_REDIR;
            filename = cmd+end;
            break;
        }
        else if(cmd[end] == '>')
        {
            if(cmd[end-1] == '>')
            {
                //>>
                cmd[end-1] = 0;
                redir = APPEND_REDIR;
            }
            else
            {
                //>
                redir = OUTPUT_REDIR;
            }
            cmd[end++] = 0;
            TrimSpace(cmd, end);
            filename = cmd+end;
            break;
        }
        else
        {
            end--;
        }
    }
}

int main()
{
    // shell 启动的时候,从系统中获取环境变量
    // 我们的环境变量信息应该从父shell统一来
    InitEnv();

    while(true)
    {
        // 1. 输出命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))
            continue;

        // 3. 重定向分析 "ls -a -l > file.txt" -> "ls -a -l" "file.txt" -> 判定重定向方式
        RedirCheck(commandline);

 //       printf("redir: %d, filename: %s\n", redir, filename.c_str());

        // 4. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
        if(!CommandParse(commandline))
            continue;
        //PrintArgv();

        // 检测别名
        // 5. 检测并处理内键命令
        if(CheckAndExecBuiltin())
            continue;

        // 6. 执行命令
        Execute();
    }
    //cleanup();
    return 0;
}
cpp 复制代码
1. 输出重定向现象:close(1) 后 open → fd=1 → 本应上屏的内容写进文件
2. 重定向本质 = 修改 fd 表下标的指向(1 号从显示器改指到 myfile)
3. dup2(oldfd, newfd):把 oldfd 的指向复制给 newfd,一步完成重定向
4. printf → stdout → fd:1 → 指向谁就写谁(库函数逃不出 fd 层)
5. 三种符号统一套路:open + dup2
   <   O_RDONLY                      dup2(fd, 0)
   >   O_CREAT|O_WRONLY|O_TRUNC      dup2(fd, 1)
   >>  O_CREAT|O_WRONLY|O_APPEND     dup2(fd, 1)
6. minishell 实现:
   ParseRedir 从末尾扫 < > >>,置 0 切出 filename
   DoRedir 在 fork 后的子进程里 open + dup2(必须子进程做!)
   exec 不破坏 fd 表 → 先重定向再程序替换
7. 至此 Linux 基础IO 全部收尾:文件概念 → C 接口 → 系统调用 → fd → 重定向/dup2/minishell

面试题

一、重定向原理

Q1. 什么是输出重定向?用 close+open 怎么实现?

A:

本来输出到显示器的内容输出到了文件。

实现:

close(1) 关闭标准输出,再 open("myfile", O_WRONLY|O_CREAT),根据 fd 分配规则(最小未用下标),新文件抢到下标 1,之后所有往 1 号写的内容(printf/write)都进文件。


Q2. 重定向的本质是什么?

A:

不是搬数据,而是修改文件描述符表里某个下标的指向------把 1 号(或 0 号)下标从默认设备改指到目标文件的 file 对象。

之后对该 fd 的读写自然落到新文件上。


Q3. 为什么 printf 的内容也会被重定向带走?

A:

printf 是 C 库函数,往 stdout 输出,但 stdout 底层访问时找的还是 fd:1。

当 fd:1 下标已指向目标文件而非显示器时,printf 输出的任何消息都会写进文件。

库函数最终都落到"往 fd 写"这一层,改 fd 表就能截住一切输出。

二、dup2

Q4. dup2 的函数原型和语义?

A:

int dup2(int oldfd, int newfd);(unistd.h)。

把oldfd 所指向的 file 对象复制到 newfd 下标上,之后 newfd 和 oldfd 指向同一个打开文件。


Q5. close(1)+open 和 dup2 的区别?

A:

close+open 是先关 1 再 open 抢占下标,1 号有短暂空窗期、步骤繁琐;

dup2(fd,1) 直接把 fd 的指向复制给 1 号,一步完成、安全可靠,是重定向的首选方案。


Q6. 为什么重定向后 write(1,...) 和 printf 都进文件?

A:

两者最终都通过 fd:1 访问文件:

write 是直接系统调用写 fd 1;

printf 是库函数写 stdout,stdout 底层也找 fd:1。

fd 表里 1 号指向谁,两者就写谁。


Q7. 输出重定向 >、追加重定向 >>、输入重定向 < 的 open flags 和 dup2 目标分别是什么?

A:

>:O_CREAT|O_WRONLY|O_TRUNC,dup2(fd,1)(覆盖写);

>>:O_CREAT|O_WRONLY|O_APPEND,dup2(fd,1)(追加写);

<:O_RDONLY,dup2(fd,0)(文件当键盘)。

区别只在 flags 和 0/1 下标。


Q8. O_TRUNC 和 O_APPEND 在重定向里的作用?

A:

O_TRUNC 打开瞬间清空文件(> 覆盖);

O_APPEND 每次写都定位到末尾(>> 追加)。

这就是 > 和 >> 在文件内容上的本质差异。

三、minishell 重定向实现

Q9. ParseRedir 为什么从命令缓冲区末尾往前扫描?

A:

重定向符号位于命令行末尾(如 ls -a -l > file.txt),从末尾往前找第一个 <、>、>> 效率最高,找到后把符号位置置 0,与命令隔开,符号后面的部分就是文件名。


Q10. ParseRedir 怎么区分 > 和 >>?

A:

找到 > 后检查它前一个字符:command_buffer[end-1] == '>' 则是追加(AppRedir),两个位置都置 0;

否则是覆盖(OutputRedir),只置一个位置。


Q11. 为什么重定向必须让子进程做?

A:

重定向会永久修改进程的 fd 表。

若在 shell 主进程做 dup2,shell 自己的 stdout 就被改没了,无法再打印提示符;

在 fork 出的子进程里做,只影响子进程,父进程 fd 表毫发无损。


Q12. 为什么 exec 不影响已经做好的重定向?

A:

fd 表属于进程资源,程序替换(exec)只换代码和数据段,不换 fd 表。

所以子进程先 DoRedir 再 execvp,新程序的输出依然进目标文件。


Q13. DoRedir 里 exit 的不同错误码有什么用?

A:

区分失败原因:

输入文件打开失败 exit(2)、未给文件名 exit(1)/exit(3)/exit(5)、输出文件创建失败 exit(4)、追加文件打开失败 exit(6)------父进程 waitpid 后可通过 WEXITSTATUS 拿到具体错误。


Q14. ls -a -l > file.txt 在 minishell 中的完整执行链路?

A:

fgets 读命令行 → ParseRedir(置 0 切出 file.txt,redir=OutputRedir)→ ParseCommand(strtok 得 ls/-a/-l)→ fork → 子进程 DoRedir(open(file,O_CREAT|O_WRONLY|O_TRUNC) + dup2(fd,1))→ execvp("ls") → ls 输出全部进 file.txt → 父进程 waitpid 后继续打印提示符。


Q15. ParseCommand 里 while((bool)(gargv[gargc++] = strtok(nullptr, sep))); 后为什么要 gargc--?

A:

最后一次 strtok 返回 nullptr 时,gargc++ 已经执行(多算了一次),退出循环后需要减 1 让 gargc 等于真实参数个数。


Q16. TrimSpace 是干什么的?为什么文件名要 TrimSpace?

A:

跳过字符串前导空白字符(isspace)。

因为 ls -a -l > file.txt 里 > 后面有空格,直接取 &command_buffer[end]+1 会得到 " file.txt",不去空格 open 会失败。

四、综合

Q17. stdin/stdout/stderr 与 0/1/2 的关系?

A:

C 标准库的 stdin/stdout/stderr 是 FILE* 流,底层分别绑定文件描述符 0/1/2。

重定向改 fd 表后,stdio 流跟着变------因为它们访问的 fd 指向变了。


Q18. 重定向和管道有什么区别?(提前铺垫)

A:

重定向是把 fd 指向文件(>/< 目标是普通文件);

管道是把一个进程的 stdout 接到另一个进程的 stdin(目标是管道文件,pipe() 创建)。

本质都是改 fd 表指向,只是目标对象不同------一个是文件,一个是管道。


Q19. 为什么说"一切皆文件"让重定向变得简单?

A:

因为显示器、文件、管道都是"文件",fd 表里放的都是 file 对象指针。

重定向只需把下标指向从"显示器文件"换成"目标文件",无需任何特殊处理------统一抽象是重定向能如此优雅的前提。


Q20. 手撕:写出 DoRedir 中 > 的分支。

A:

cpp 复制代码
else if(redir == OutputRedir)
{
    if(filename)
    {
        int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
        if(fd < 0) exit(4);
        dup2(fd, 1);
    }
    else exit(3);
}

相关推荐
学linux的QQ蛋1 小时前
IMX6 嵌入式多传感器系统整体架构梳理
linux·嵌入式硬件·架构
miofly1 小时前
GitHub 今日推荐|security-audit-skill:让编码代理自己审计漏洞还互相验证
javascript·开源·github
小HANN1 小时前
(架构 + InnoDB 事务 MVCC):《MySQL 底层探秘①:拆解 InnoDB,读懂事务、MVCC 与日志的底层逻辑》
linux·运维·服务器·经验分享
kyle~1 小时前
Linux --- 虚拟内存 1.fork、2.COW、3.mmap、4.page fault、5.TLB、6.页表
linux·运维·服务器
你看我名字1 小时前
LAMP 架构演练:从云主机准备到 Apache、PHP、MySQL 上线验证
运维·ci/cd·架构·kubernetes·jenkins
服务器硬件项目经理1 小时前
第3章GPU vs CPU:为什么AI训练要用GPU
服务器·人工智能·嵌入式硬件·gru·硬件工程·硬件
Techblog of HaoWANG1 小时前
Ubuntu 下将 Ollama 服务配置为监听 0.0.0.0:11434 完整实践
linux·数据库·ubuntu·ollama
似水流年QC2 小时前
LangChain 入门与实战
服务器·数据库·langchain
小HANN2 小时前
Ceph分布式存储深度解析-下篇
linux·运维·服务器·经验分享