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


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言:
  • [一. 文件描述符(fd):Linux IO 的 "身份证"](#一. 文件描述符(fd):Linux IO 的 “身份证”)
    • [1.1 什么是文件描述符?](#1.1 什么是文件描述符?)
    • [1.2 默认文件描述符:0、1、2](#1.2 默认文件描述符:0、1、2)
    • [1.3 文件描述符的分配规则](#1.3 文件描述符的分配规则)
    • [1.4 系统调用与库函数的关系](#1.4 系统调用与库函数的关系)
  • [二. 重定向原理:修改 fd 对应的文件对象](#二. 重定向原理:修改 fd 对应的文件对象)
    • [2.1 重定向的本质](#2.1 重定向的本质)
    • [2.2 手动实现重定向:close+open](#2.2 手动实现重定向:close+open)
    • [2.3 系统调用 dup2:更优雅的重定向](#2.3 系统调用 dup2:更优雅的重定向)
  • [三. 实战:给 minishell 添加重定向功能](#三. 实战:给 minishell 添加重定向功能)
    • [3.1 核心思路:重定向实现需三步](#3.1 核心思路:重定向实现需三步)
    • [3.2 完整实现代码](#3.2 完整实现代码)
  • 结尾:

前言:

文件描述符(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),对应显示器。

验证代码

cpp 复制代码
#include <stdio.h>
int main()
{
    // 打印标准流对应的文件描述符
    printf("stdin: %d\n", stdin->_fileno);  // 输出:stdin: 0
    printf("stdout: %d\n", stdout->_fileno); // 输出:stdout: 1
    printf("stderr: %d\n", stderr->_fileno); // 输出:stderr: 2
    return 0;
}


  • 可以继续往后看看下面那张图中的源码部分

1.3 文件描述符的分配规则

  • 核心规则优先分配当前未使用的最小非负整数。

代码示例

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

int main()
{
    // 打印默认fd
    printf("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),再新建文件,观察是否分配3
    close(fda);
    int fdd = open("logd.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    printf("fdd: %d\n", fdd); // 输出:3
    
    // 关闭所有fd
    close(fdb); close(fdc); close(fdd);
    return 0;
}

运行结果说明:默认情况下,新打开文件的 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)重定向到文件:
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    // 关闭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.txt
    printf("hello printf\n");       // 写入log.txt
    fprintf(stdout, "hello fprintf\n"); // 写入log.txt
    
    close(fd);
    return 0;
}

运行后,原本输出到显示器的内容会写入log.txt,验证了输出重定向的本质。

2.3 系统调用 dup2:更优雅的重定向

dup2(oldfd, newfd)函数会将newfd重定向到oldfd对应的文件,自动关闭newfd(若已打开),是实现重定向的标准接口。

示例:用 dup2 实现输出重定向

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

int main()
{
    // 打开文件,获取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.txt
    fprintf(stdout, "hello stdout\n"); // 写入log.txt
    
    close(fd);
    return 0;
}

dup2无需手动关闭newfd,更简洁可靠,是 Shell 实现重定向的首选接口。

  • 补充一个输入的
cpp 复制代码
int main()
{
   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);
   return 0;
}

三. 实战:给 minishell 添加重定向功能

基于上上篇博客中的myshell.c代码,完善重定向解析和执行逻辑,实现>(输出重定向)、>>(追加重定向)、<(输入重定向)功能。

3.1 核心思路:重定向实现需三步

  • 解析命令行 :识别重定向符号(>>><)和目标文件名;
  • 子进程中执行重定向 :利用dup2修改 fd 对应的文件;
  • 执行程序替换:重定向不影响程序替换,替换后新程序会沿用修改后的 fd。

3.2 完整实现代码

(1)头文件(myshell.h)&& 主函数(main.c)

cpp 复制代码
#pragma once
#include <stdio.h>
void bash();
cpp 复制代码
#include "myshell.h"
int main() 
{
    bash();
    return 0;
}

(2)核心实现(myshell.c)

cpp 复制代码
#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>

// 提示符相关
static char username[32];
static char hostname[64];
static char cwd[256];
static char commandLine[256];
// 与命令行相关
static char* argv[64];
static int argc = 0;
static const char* sep = " ";
// 与退出码有关
static int lastCode = 0;
// 与环境变量相关,按道理来说是由bash来维护的,从系统配置文件读,但是我们这里直接从系统bash拷贝就行了
char** _environ;
static int envc = 0;
// 重定向相关
// ls -a -l > test.txt 
#define NoneRedir 0
#define InputRedir 1
#define OutputRedir 2
#define AppRedir 3
static int redir_type = NoneRedir;
static char* redir_filename = NULL;

#define CLEAR_LEFT_SPACE(pos) do{while(isspace(*pos)) pos++; }while(0)

static void InitEnv()
{
    extern char** environ; // 系统环境变量数组(以NULL结尾)
    for(envc = 0; environ[envc]; envc++)
    {
        _environ[envc] = environ[envc];
    }
}

static void PrintAllEnv()
{
    int i = 0;
    for( ; _environ[i]; i++ )
    {
        printf("%s\n", _environ[i]);
    }
}

static void AddEnv(const char* val) // argv[1];
{
    _environ[envc] = (char*)malloc(strlen(val) + 1);
    strcpy(_environ[envc], val);
    _environ[++envc] = NULL; 
}

static void GetUserName()
{
    char* _username = getenv("USER");
    strcpy(username, (_username ? _username : "None"));
}

static void GetHostName()
{
    char* _hostname = getenv("HOSTNAME");
    strcpy(hostname, (_hostname ? _hostname : "None"));
}

static void GetCmd()
{
   // 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--;
       }
   }
}

static void PrintPromt()
{
    GetUserName();
    GetHostName();
    GetCmd();
    printf("[%s@%s %s]# ",username, hostname, cwd);
    fflush(stdout);
}

static void GetCommandLine()
{
    if(fgets(commandLine, sizeof(commandLine), stdin) != NULL)
    {
        commandLine[strlen(commandLine) - 1] = 0;
    }
}
// 1. yes
// 0. no, 普通命令,让后续的执行
int CheckBuiltinAndExcute()
{
    int ret = 0;
    if(strcmp(argv[0], "cd") == 0)
    {
        // 内键命令
        ret = 1;
        if(argc == 2) // 后面至少需要跟个东西
        {
            chdir(argv[1]);
        }
    }
    else if(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]);
            }
        }
    }
    else if(strcmp(argv[0], "env") == 0)
    {
        ret = 1;
        PrintAllEnv();
    }
    else if(strcmp(argv[0], "export") == 0)
    {
        ret = 1;
        if(argc == 2)
        {
            AddEnv(argv[1]);
        }
    }

    return ret;
}

void Excute()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return;
    }
    else if(id == 0)
    {
        // 子进程
        // 程序替换
        // 要不要重定向,怎么重定向
        // filename 
        if(redir_type = InputRedir)
        {
            int fd = open(redir_filename, O_RDONLY);
            (void)fd;
            dup2(fd, 0);
        }
        else if(redir_type = OutputRedir)
        {
            int fd = open(redir_filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
            (void)fd;
            dup2(fd, 1);
        }
        else if(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);
    }
}
static void ParseCommandLine()
{
     // 清空
     argc = 0;
     memset(argv, 0, sizeof(argv));
     // 判空
     if(strlen(commandLine) == 0) return;
     // 分割
     argv[argc] = strtok(commandLine, sep);
     while((argv[++argc] = strtok(NULL, sep)));
}

void Redir()
{
    // 核心目标
    // "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;
            }
        }

        else if(*start == '<') 
        {
            // 输入重定向
            redir_type = InputRedir;
            *start = '\0';
            start++;
            CLEAR_LEFT_SPACE(start);
            redir_filename = start;
            break;
        }
        else{
            start++;
        }
    }
}
void bash()
{
    // 环境变量相关,方便实现通过声明(_environ)就能直接用环境变量
    static char* 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.1
        Redir();

        // 第三步: 解析字符串,"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)

结尾:

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

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

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

相关推荐
大模型玩家七七11 小时前
基于语义切分 vs 基于结构切分的实际差异
java·开发语言·数据库·安全·batch
历程里程碑11 小时前
Linux22 文件系统
linux·运维·c语言·开发语言·数据结构·c++·算法
恋猫de小郭11 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
岳麓丹枫00112 小时前
PostgreSQL 中 pg_wal 目录里的 .ready .done .history 文件的生命周期
数据库·postgresql
Coder_Boy_12 小时前
技术发展的核心规律是「加法打底,减法优化,重构平衡」
人工智能·spring boot·spring·重构
会飞的老朱14 小时前
医药集团数智化转型,智能综合管理平台激活集团管理新效能
大数据·人工智能·oa协同办公
聆风吟º16 小时前
CANN runtime 实战指南:异构计算场景中运行时组件的部署、调优与扩展技巧
人工智能·神经网络·cann·异构计算
工程师老罗18 小时前
如何在Android工程中配置NDK版本
android
Codebee18 小时前
能力中心 (Agent SkillCenter):开启AI技能管理新时代
人工智能