输出重定向、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 对象。
cpptask_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 这个下标上。
cppdup2(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 的关系
cppprintf 是 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隔开,并把后面的部分当作文件名。
cppvoid 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:按空格分割命令
cppvoid 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:总入口
cppvoid 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,新程序的输出自然进文件。
cppvoid 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 回收子进程,继续打印提示符 }
cppshell 主循环: 打印提示符 → 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; }
cpp1. 输出重定向现象: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:
cppelse 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); }




