跳到主要内容Linux 文件描述符与重定向实战:从原理到 minishell 实现 | 极客日志C
Linux 文件描述符与重定向实战:从原理到 minishell 实现
Linux 文件描述符是内核管理 IO 资源的核心机制,通过非负整数索引进程打开的文件对象。重定向基于修改 fd 指向的文件对象实现,常用 close+open 或 dup2 系统调用。 fd 分配规则、默认流(0/1/2)及重定向原理,并通过 minishell 实战演示如何在子进程中利用 dup2 实现输入输出重定向,涵盖解析命令行、执行重定向及程序替换的关键步骤,帮助理解 Shell 底层 IO 逻辑。
随缘25 浏览 Linux 文件描述符与重定向实战:从原理到 minishell 实现

前言
文件描述符(fd)是 Linux IO 的核心概念,所有文件操作最终都通过文件描述符完成;而重定向(>、>>、<)则是基于文件描述符的经典应用,是 Shell 的核心功能之一。理解文件描述符的分配规则、重定向的底层原理,不仅能帮你搞懂 Linux IO 的本质,还能轻松实现自定义 Shell 的重定向功能。
一、文件描述符(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>
int main() {
printf("stdin: %d\n", stdin->_fileno);
printf("stdout: %d\n", stdout->_fileno);
printf("stderr: %d\n", stderr->_fileno);
;
}
return
0
1.3 文件描述符的分配规则
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
printf("stdin: %d, stdout: %d, stderr: %d\n", stdin->_fileno, stdout->_fileno, stderr->_fileno);
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);
close(fda);
int fdd = open("logd.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("fdd: %d\n", fdd);
close(fdb);
close(fdc);
close(fdd);
return 0;
}
运行结果说明:默认情况下,新打开文件的 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>
int main() {
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
close(fd);
return 0;
}
运行后,原本输出到显示器的内容会写入 log.txt,验证了输出重定向的本质。
2.3 系统调用 dup2:更优雅的重定向
dup2(oldfd, newfd) 函数会将 newfd 重定向到 oldfd 对应的文件,自动关闭 newfd(若已打开),是实现重定向的标准接口。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
dup2(fd, 1);
printf("hello dup2\n");
fprintf(stdout, "hello stdout\n");
close(fd);
return 0;
}
dup2 无需手动关闭 newfd,更简洁可靠,是 Shell 实现重定向的首选接口。
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)
#pragma once
#include <stdio.h>
void bash();
#include "myshell.h"
int main() {
bash();
return 0;
}
#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;
char** _environ;
static int envc = 0;
#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;
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)
{
_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[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;
}
}
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 {
}
} 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) {
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 {
}
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() {
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() {
static char* env[64];
_environ = env;
InitEnv();
while (1) {
redir_type = NoneRedir;
redir_filename = NULL;
PrintPromt();
GetCommandLine();
Redir();
ParseCommandLine();
if (argc == 0)
continue;
if (CheckBuiltinAndExcute())
continue;
Excute();
}
}
- 重定向必须在子进程中执行:避免修改 Shell 主进程的 fd 映射,导致后续命令异常;
- 程序替换不影响重定向:
execvp 会保留子进程的 fd 映射,替换后的程序会沿用重定向后的 fd;
- 关闭原 fd:
dup2 后需关闭原 fd(如打开的文件 fd),避免 fd 泄漏;
- 缓冲区刷新:重定向后 stdout 的缓冲模式会从'行缓冲'变为'全缓冲',若需实时输出,需用
fflush(stdout)。
结语
文件描述符是 Linux IO 的底层基石,重定向是其最经典的应用之一。本文从 fd 的本质、分配规则,到重定向原理,再到 minishell 的实战实现,全程用代码验证,让你不仅'知其然',更'知其所以然'。基于这个基础,还可以扩展管道(|)、后台运行(&)等 Shell 高级功能。管道的本质是'将前一个命令的 stdout 重定向到管道,后一个命令的 stdin 重定向到管道',核心思路与本文重定向一致。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online