Linux 文件描述符与重定向实战:从原理到 minishell 实现
🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:
文章目录
前言:
文件描述符(fd)是 Linux IO 的核心概念,所有文件操作最终都通过文件描述符完成;而重定向(>、>>、<)则是基于文件描述符的经典应用,是 Shell 的核心功能之一。理解文件描述符的分配规则、重定向的底层原理,不仅能帮你搞懂 Linux IO 的本质,还能轻松实现自定义 Shell 的重定向功能。本文从文件描述符的本质、分配规则,到重定向原理,最后落地到 minishell 的重定向功能实现,全程用实战代码验证,让你彻底吃透这两个关键知识点。
一. 文件描述符(fd):Linux IO 的 “身份证”
1.1 什么是文件描述符?
文件描述符是 Linux 内核给打开的文件(广义文件,包括磁盘文件、键盘、显示器等)分配的非负整数,本质是进程files_struct结构体中文件指针数组的下标。通过这个下标,进程能快速找到对应的内核文件对象(struct file),从而完成 IO 操作。

1.2 默认文件描述符:0、1、2
Linux 进程启动时会默认打开 3 个文件描述符,对应 3 个标准流:
fd=0:标准输入(stdin),对应键盘;fd=1:标准输出(stdout),对应显示器;fd=2:标准错误(stderr),对应显示器。
验证代码:
#include<stdio.h>intmain(){// 打印标准流对应的文件描述符printf("stdin: %d\n",stdin->_fileno);// 输出:stdin: 0printf("stdout: %d\n",stdout->_fileno);// 输出:stdout: 1printf("stderr: %d\n",stderr->_fileno);// 输出:stderr: 2return0;}

- 可以继续往后看看下面那张图中的源码部分
1.3 文件描述符的分配规则
- 核心规则:
优先分配当前未使用的最小非负整数。
代码示例:
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){// 打印默认fdprintf("stdin: %d, stdout: %d, stderr: %d\n",stdin->_fileno,stdout->_fileno,stderr->_fileno);// 新建3个文件,观察fd分配int fda =open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC,0666);int fdb =open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC,0666);int fdc =open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC,0666);printf("fda: %d, fdb: %d, fdc: %d\n", fda, fdb, fdc);// 输出:3,4,5// 关闭fda(fd=3),再新建文件,观察是否分配3close(fda);int fdd =open("logd.txt", O_CREAT | O_WRONLY | O_TRUNC,0666);printf("fdd: %d\n", fdd);// 输出:3// 关闭所有fdclose(fdb);close(fdc);close(fdd);return0;}运行结果说明:默认情况下,新打开文件的 fd 从 3 开始分配;关闭某个 fd 后,后续新文件会优先占用该空闲 fd。


1.4 系统调用与库函数的关系
- 系统调用(
open、read、write)直接操作文件描述符,是 IO 的底层接口; - C 库函数(
fopen、fread、fwrite)封装了系统调用,内部通过FILE结构体管理文件描述符(FILE->_fileno)和用户级缓冲区; - 关系:
fopen→open(返回 fd)→FILE结构体封装 fd→fwrite→write(通过 fd 操作文件)。

二. 重定向原理:修改 fd 对应的文件对象
2.1 重定向的本质
重定向的核心是修改文件描述符对应的文件对象。例如:
- 输出重定向(
cat > file.txt):将 fd=1(stdout)原本指向的 “显示器文件”,改为指向 “file.txt”; - 输入重定向(
cat < file.txt):将 fd=0(stdin)原本指向的 “键盘文件”,改为指向 “file.txt”; - 追加重定向(
echo "hello" >> file.txt):将 fd=1 指向 “file.txt”,且写入时追加到文件末尾。
2.2 手动实现重定向:close+open
- 原理:先关闭目标 fd,再打开新文件,利用 fd 分配规则,新文件会自动占用关闭的 fd,从而实现重定向。
- 示例:将 stdout(fd=1)重定向到文件:
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){// 关闭fd=1(stdout)close(1);// 打开文件,fd会分配为1(因为1是当前最小空闲fd)int fd =open("log.txt", O_CREAT | O_WRONLY | O_TRUNC,0666);// 此时printf、fprintf(stdout)都会写入log.txtprintf("hello printf\n");// 写入log.txtfprintf(stdout,"hello fprintf\n");// 写入log.txtclose(fd);return0;}运行后,原本输出到显示器的内容会写入log.txt,验证了输出重定向的本质。

2.3 系统调用 dup2:更优雅的重定向
dup2(oldfd, newfd)函数会将newfd重定向到oldfd对应的文件,自动关闭newfd(若已打开),是实现重定向的标准接口。

示例:用 dup2 实现输出重定向:
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){// 打开文件,获取fd(假设为3)int fd =open("log.txt", O_CREAT | O_WRONLY | O_TRUNC,0666);// w// int fd = open("loga.txt", O_CREAT | O_WRONLY | O_APPEND, 0666); // a// 将fd=1(stdout)重定向到fd对应的文件dup2(fd,1);printf("hello dup2\n");// 写入log.txtfprintf(stdout,"hello stdout\n");// 写入log.txtclose(fd);return0;}dup2无需手动关闭newfd,更简洁可靠,是 Shell 实现重定向的首选接口。
- 补充一个输入的:
intmain(){int fda =open("loga.txt", O_RDONLY);dup2(fda,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(fda);return0;}
三. 实战:给 minishell 添加重定向功能
基于上上篇博客中的myshell.c代码,完善重定向解析和执行逻辑,实现>(输出重定向)、>>(追加重定向)、<(输入重定向)功能。

3.1 核心思路:重定向实现需三步
- 解析命令行:识别重定向符号(
>、>>、<)和目标文件名; - 子进程中执行重定向:利用
dup2修改 fd 对应的文件; - 执行程序替换:重定向不影响程序替换,替换后新程序会沿用修改后的 fd。
3.2 完整实现代码
(1)头文件(myshell.h)&& 主函数(main.c)
#pragmaonce#include<stdio.h>voidbash();#include"myshell.h"intmain(){bash();return0;}(2)核心实现(myshell.c)
#include"myshell.h"#include<stdlib.h>#include<unistd.h>#include<string.h>#include<sys/stat.h>#include<sys/wait.h>#include<sys/types.h>#include<fcntl.h>// 提示符相关staticchar username[32];staticchar hostname[64];staticchar cwd[256];staticchar commandLine[256];// 与命令行相关staticchar* argv[64];staticint argc =0;staticconstchar* sep =" ";// 与退出码有关staticint lastCode =0;// 与环境变量相关,按道理来说是由bash来维护的,从系统配置文件读,但是我们这里直接从系统bash拷贝就行了char** _environ;staticint envc =0;// 重定向相关// ls -a -l > test.txt #defineNoneRedir0#defineInputRedir1#defineOutputRedir2#defineAppRedir3staticint redir_type = NoneRedir;staticchar* redir_filename =NULL;#defineCLEAR_LEFT_SPACE(pos)do{while(isspace(*pos)) pos++;}while(0)staticvoidInitEnv(){externchar** environ;// 系统环境变量数组(以NULL结尾)for(envc =0; environ[envc]; envc++){ _environ[envc]= environ[envc];}}staticvoidPrintAllEnv(){int i =0;for(; _environ[i]; i++){printf("%s\n", _environ[i]);}}staticvoidAddEnv(constchar* val)// argv[1];{ _environ[envc]=(char*)malloc(strlen(val)+1);strcpy(_environ[envc], val); _environ[++envc]=NULL;}staticvoidGetUserName(){char* _username =getenv("USER");strcpy(username,(_username ? _username :"None"));}staticvoidGetHostName(){char* _hostname =getenv("HOSTNAME");strcpy(hostname,(_hostname ? _hostname :"None"));}staticvoidGetCmd(){// char* _cwd = getenv("PWD");// strcpy(cwd, (_cwd ? _cwd : "None"));char _cwd[256];getcwd(_cwd,sizeof(_cwd));if(strcmp(_cwd,"/")==0){strcpy(cwd, _cwd);}else{int end =strlen(_cwd)-1;while(end >=0){if(_cwd[end]=='/'){strcpy(cwd,&_cwd[end +1]);break;} end--;}}}staticvoidPrintPromt(){GetUserName();GetHostName();GetCmd();printf("[%s@%s %s]# ",username, hostname, cwd);fflush(stdout);}staticvoidGetCommandLine(){if(fgets(commandLine,sizeof(commandLine),stdin)!=NULL){ commandLine[strlen(commandLine)-1]=0;}}// 1. yes// 0. no, 普通命令,让后续的执行intCheckBuiltinAndExcute(){int ret =0;if(strcmp(argv[0],"cd")==0){// 内键命令 ret =1;if(argc ==2)// 后面至少需要跟个东西{chdir(argv[1]);}}elseif(strcmp(argv[0],"echo")==0){ ret =1;if(argc ==2){if(argv[1][0]=='$'){if(strcmp(argv[1],"$?")==0){printf("%d\n", lastCode); lastCode =0;}else{// env }}else{printf("%s\n", argv[1]);}}}elseif(strcmp(argv[0],"env")==0){ ret =1;PrintAllEnv();}elseif(strcmp(argv[0],"export")==0){ ret =1;if(argc ==2){AddEnv(argv[1]);}}return ret;}voidExcute(){ pid_t id =fork();if(id <0){perror("fork");return;}elseif(id ==0){// 子进程// 程序替换// 要不要重定向,怎么重定向// filename if(redir_type = InputRedir){int fd =open(redir_filename, O_RDONLY);(void)fd;dup2(fd,0);}elseif(redir_type = OutputRedir){int fd =open(redir_filename, O_CREAT | O_WRONLY | O_TRUNC,0666);(void)fd;dup2(fd,1);}elseif(redir_type = AppRedir){int fd =open(redir_filename, O_CREAT | O_WRONLY | O_APPEND,0666);(void)fd;dup2(fd,1);}else{// do nothing}execvp(argv[0], argv);exit(1);}else{// 父进程int status =0; pid_t rid =waitpid(id,&status,0);(void)rid; lastCode =WEXITSTATUS(status);}}staticvoidParseCommandLine(){// 清空 argc =0;memset(argv,0,sizeof(argv));// 判空if(strlen(commandLine)==0)return;// 分割 argv[argc]=strtok(commandLine, sep);while((argv[++argc]=strtok(NULL, sep)));}voidRedir(){// 核心目标// "ls -a -l >> > < filename"// redir_filename = filename // redir_type = InputRedir char* start = commandLine;char* end = commandLine +strlen(commandLine);while(start < end){// > >> <if(*start =='>'){if(*(start +1)=='>'){// 追加重定向 redir_type = AppRedir;*start =0; start +=2;CLEAR_LEFT_SPACE(start); redir_filename = start;break;}else{// 输出重定向 redir_type = OutputRedir;*start ='\0'; start++;CLEAR_LEFT_SPACE(start); redir_filename = start;break;}}elseif(*start =='<'){// 输入重定向 redir_type = InputRedir;*start ='\0'; start++;CLEAR_LEFT_SPACE(start); redir_filename = start;break;}else{ start++;}}}voidbash(){// 环境变量相关,方便实现通过声明(_environ)就能直接用环境变量staticchar* env[64]; _environ = env;// 除此以外我们还可以通过一个数组存储本地变量// 以及可以通过一个来存储别名…// 初始化读取环境变量InitEnv();while(1){// 每次开始前重置一下重定向文件和状态 redir_type = NoneRedir; redir_filename =NULL;// 第一步: 输出提示命令行PrintPromt();// 第二步: 等待用户输入, 获取用户输入GetCommandLine();// "ls -a -l > filename" -> "ls -a -l" "filename" redir_type// 2.1Redir();// 第三步: 解析字符串,"ls -a -l" -> "ls" "-a" "-l"ParseCommandLine();if(argc ==0)continue;// 第四步: 有些命令, cd echo env等等不应该让子进程执行// 而是让父进程自己执行,这些是内建命令. bash内部的函数if(CheckBuiltinAndExcute())continue;// 第五步: 执行命令Excute();}}关键注意点:
- 重定向必须在子进程中执行:避免修改 Shell 主进程的 fd 映射,导致后续命令异常;
- 程序替换不影响重定向:
execvp会保留子进程的 fd 映射,替换后的程序会沿用重定向后的 fd; - 关闭原 fd:
dup2后需关闭原 fd(如打开的文件 fd),避免 fd 泄漏; - 缓冲区刷新:重定向后 stdout 的缓冲模式会从 “行缓冲” 变为 “全缓冲”,若需实时输出,需用
fflush(stdout)。
结尾:
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 结语:文件描述符是 Linux IO 的底层基石,重定向是其最经典的应用之一。本文从 fd 的本质、分配规则,到重定向原理,再到 minishell 的实战实现,全程用代码验证,让你不仅 “知其然”,更 “知其所以然”。基于这个基础,还可以扩展管道(|)、后台运行(&)等 Shell 高级功能。管道的本质是 “将前一个命令的 stdout 重定向到管道,后一个命令的 stdin 重定向到管道”,核心思路与本文重定向一致。
✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど