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

【AI大模型】DeepSeek + 通义万相高效制作AI视频实战详解

【AI大模型】DeepSeek + 通义万相高效制作AI视频实战详解

目录 一、前言 二、AI视频概述 2.1 什么是AI视频 2.2 AI视频核心特点 2.3 AI视频应用场景 三、通义万相介绍 3.1 通义万相概述 3.1.1 什么是通义万相 3.2 通义万相核心特点 3.3 通义万相技术特点 3.4 通义万相应用场景 四、DeepSeek + 通义万相制作AI视频流程 4.1 DeepSeek + 通义万相制作视频优势 4.1.1 DeepSeek 优势 4.1.2 通义万相视频生成优势 4.2

By Ne0inhk
【DeepSeek微调实践】DeepSeek-R1大模型基于MS-Swift框架部署/推理/微调实践大全

【DeepSeek微调实践】DeepSeek-R1大模型基于MS-Swift框架部署/推理/微调实践大全

系列篇章💥 No.文章01【DeepSeek应用实践】DeepSeek接入Word、WPS方法详解:无需代码,轻松实现智能办公助手功能02【DeepSeek应用实践】通义灵码 + DeepSeek:AI 编程助手的实战指南03【DeepSeek应用实践】Cline集成DeepSeek:开源AI编程助手,终端与Web开发的超强助力04【DeepSeek开发入门】DeepSeek API 开发初体验05【DeepSeek开发入门】DeepSeek API高级开发指南(推理与多轮对话机器人实践)06【DeepSeek开发入门】Function Calling 函数功能应用实战指南07【DeepSeek部署实战】DeepSeek-R1-Distill-Qwen-7B:本地部署与API服务快速上手08【DeepSeek部署实战】DeepSeek-R1-Distill-Qwen-7B:Web聊天机器人部署指南09【DeepSeek部署实战】DeepSeek-R1-Distill-Qwen-7B:基于vLLM 搭建高性能推理服务器10【DeepSeek部署实战】基于Ollama快速部署Dee

By Ne0inhk

DeepSeek各版本说明与优缺点分析_deepseek各版本区别

DeepSeek各版本说明与优缺点分析 DeepSeek是最近人工智能领域备受瞩目的一个语言模型系列,其在不同版本的发布过程中,逐步加强了对多种任务的处理能力。本文将详细介绍DeepSeek的各版本,从版本的发布时间、特点、优势以及不足之处,为广大AI技术爱好者和开发者提供一份参考指南。 1. DeepSeek-V1:起步与编码强劲 DeepSeek-V1是DeepSeek的起步版本,这里不过多赘述,主要分析它的优缺点。 发布时间: 2024年1月 特点: DeepSeek-V1是DeepSeek系列的首个版本,预训练于2TB的标记数据,主打自然语言处理和编码任务。它支持多种编程语言,具有强大的编码能力,适合程序开发人员和技术研究人员使用。 优势: * 强大编码能力:支持多种编程语言,能够理解和生成代码,适合开发者进行自动化代码生成与调试。 * 高上下文窗口:支持高达128K标记的上下文窗口,能够处理较为复杂的文本理解和生成任务。 缺点: * 多模态能力有限:该版本主要集中在文本处理上,缺少对图像、语音等多模态任务的支持。 * 推理能力较弱:尽管在自然语言

By Ne0inhk

用DeepSeek和Cursor从零打造智能代码审查工具:我的AI编程实践

💂 个人网站:【 摸鱼游戏】【神级代码资源网站】【星海网址导航】摸鱼、技术交流群👉 点此查看详情 引言:AI编程革命下的机遇与挑战 GitHub统计显示,使用AI编程工具的开发者平均效率提升55%,但仅有23%的开发者能充分发挥这些工具的潜力。作为一名全栈工程师,我曾对AI编程持怀疑态度,直到一次紧急项目让我彻底改变了看法。客户要求在72小时内交付一个能自动检测代码漏洞、优化性能的智能审查系统,传统开发方式根本不可能完成。正是这次挑战,让我探索出DeepSeek和Cursor这对"黄金组合"的惊人潜力。 一、工具选型:深入比较主流AI编程工具 1.1 为什么最终选择DeepSeek+Cursor? 经过两周的对比测试,我们发现不同工具在代码审查场景的表现差异显著: 工具代码理解深度响应速度定制灵活性多语言支持GitHub Copilot★★★☆★★★★★★☆★★★★Amazon CodeWhisperer★★☆★★★☆★★★★★★☆DeepSeek★★★★☆★★★★★★★☆★★★★☆Cursor★★★☆★★★★☆★★★★★★★★ 关键发现: * Dee

By Ne0inhk