Linux 文件描述符与重定向实战:从原理到 minishell 实现

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 系统调用与库函数的关系

  • 系统调用(openreadwrite)直接操作文件描述符,是 IO 的底层接口;
  • C 库函数(fopenfreadfwrite)封装了系统调用,内部通过FILE结构体管理文件描述符(FILE->_fileno)和用户级缓冲区;
  • 关系:fopenopen(返回 fd)→FILE结构体封装 fd→fwritewrite(通过 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;
  • 关闭原 fddup2后需关闭原 fd(如打开的文件 fd),避免 fd 泄漏;
  • 缓冲区刷新:重定向后 stdout 的缓冲模式会从 “行缓冲” 变为 “全缓冲”,若需实时输出,需用fflush(stdout)

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:文件描述符是 Linux IO 的底层基石,重定向是其最经典的应用之一。本文从 fd 的本质、分配规则,到重定向原理,再到 minishell 的实战实现,全程用代码验证,让你不仅 “知其然”,更 “知其所以然”。基于这个基础,还可以扩展管道(|)、后台运行(&)等 Shell 高级功能。管道的本质是 “将前一个命令的 stdout 重定向到管道,后一个命令的 stdin 重定向到管道”,核心思路与本文重定向一致。

✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど

Read more

假网站排全网第二,真官网翻五页都找不到!NanoClaw创始人破防:SEO之战,我快要输了

假网站排全网第二,真官网翻五页都找不到!NanoClaw创始人破防:SEO之战,我快要输了

整理 | 苏宓 出品 | ZEEKLOG(ID:ZEEKLOGnews) 自从 OpenClaw 爆火之后,各种“Claw”项目接连出现,其中以安全优化版 NanoClaw 最为知名。它的核心代码仅有 4000 行,却获得了 AI 大牛 Andrej Karpathy 的点赞。 可谁也没想到,这款口碑极佳的开源项目,近来竟被一个仿冒网站抢了风头。 投诉无门之下,NanoClaw 创始人 Gavriel Cohen 在 X 社交平台上无奈发文怒斥:谷歌搜索错误地将假网站排在真官网前面,不仅破坏了项目声誉,还埋下了严重的安全隐患,而他费尽心力,却只能哀叹一句——“我正在为自己的开源项目打 SEO 战,但我快要输了。” 那么,NanoClaw 究竟发生了什么?又是怎么走红的?事情还要从 OpenClaw

By Ne0inhk
曝Windows 12将于今年发布?以AI为核心、NPU成「硬件门槛」,网友吐槽:“不想要的全塞进来了”

曝Windows 12将于今年发布?以AI为核心、NPU成「硬件门槛」,网友吐槽:“不想要的全塞进来了”

整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 当年,微软一句“Windows 10 将是最后一个版本”的表态,让不少用户以为 Windows 进入了“只更新、不换代”的时代。但几年过去,现实却完全不同。 在 Windows 11 发布之后,如今关于 Windows 12 的传闻再次密集出现。从内部代号、代码片段,到硬件厂商的暗示与 OEM 预热标签,种种线索拼在一起,勾勒出一个明显的趋势——这不会只是一次常规升级,而更像是一次围绕 AI 的平台级重构。 更关键的是,这次争议,可能远比当年 TPM 2.0 更大。 精准卡位 Windows 10 退场的时间?

By Ne0inhk
“裸奔龙虾”数量已达27万只,业内人士警告;AI浪潮下,中传“砍掉”翻译等16个专业;薪资谈判破裂,三星电子8.9万人要罢工 | 极客头条

“裸奔龙虾”数量已达27万只,业内人士警告;AI浪潮下,中传“砍掉”翻译等16个专业;薪资谈判破裂,三星电子8.9万人要罢工 | 极客头条

「极客头条」—— 技术人员的新闻圈! ZEEKLOG 的读者朋友们好,「极客头条」来啦,快来看今天都有哪些值得我们技术人关注的重要新闻吧。(投稿或寻求报道:[email protected]) 整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 一分钟速览新闻点! * “裸奔龙虾”已高达27万只!业内人士警告:一旦黑客入侵,敏感信息一秒搬空 * 阿里云 CTO 周靖人代管千问模型一号位,刘大一恒管理更多团队 * 中国传媒大学砍掉翻译、摄影等 16 个本科专业,直言教育要面向人机分工时代 * 雷军放话:小米将很快推出 L3、L4 的驾驶 * 消息称原理想汽车智驾一号位郎咸朋具身智能赛道创业 * vivo 前产品经理宋紫薇创业,瞄准 AI 时尚Agent,获亿元融资 * MiniMax 发布龙虾新技能,股价暴涨超 23% * 薪资谈判破裂,三星电子

By Ne0inhk
Python热度下滑、AI能取代搜索引擎?TIOBE最新榜单揭晓!

Python热度下滑、AI能取代搜索引擎?TIOBE最新榜单揭晓!

整理 | 屠敏 出品 | ZEEKLOG(ID:ZEEKLOGnews) 日前,TIOBE 发布了最新的 3 月编程语言榜单。整体来看,本月排名变化不算大,但榜单中仍然出现了一些值得关注的小波动。  AI 工具能帮大家秒懂最新编程语言趋势? 由于 2 月天数较少,3 月的榜单整体变化有限。借着这次发布,TIOBE CEO Paul Jansen 也回应了一个最近被频繁讨论的问题:为什么 TIOBE 指数仍然依赖搜索引擎统计结果?在大语言模型流行的今天,直接询问 AI 哪些编程语言最流行,是不是更简单? 对此,Jansen 的回答是否定的。 他解释称,TIOBE 指数本质上统计的是互联网上关于某种编程语言的网页数量。而大语言模型的训练数据同样来自这些网页内容,因此从信息来源来看,两者并没有本质区别。换句话说,LLM 的判断,本质上也是建立在这些网页数据之上的。 Python 活跃度仍在下降

By Ne0inhk