彻底搞懂 Linux 基础 IO:文件描述符、重定向、dup2、缓冲区一次讲透!

文章目录

  • 引言
  • [1. 理解"文件"](#1. 理解“文件”)
    • [1.1 什么是文件](#1.1 什么是文件)
      • [1.1.1 狭义文件](#1.1.1 狭义文件)
      • [1.1.2 广义文件](#1.1.2 广义文件)
  • [1.2 文件的本质](#1.2 文件的本质)
  • [2. 回顾 C 文件接口](#2. 回顾 C 文件接口)
    • [2.1 fopen 打开文件](#2.1 fopen 打开文件)
    • [2.2 fwrite 写文件](#2.2 fwrite 写文件)
    • [2.3 fread 读文件](#2.3 fread 读文件)
    • [2.4 模拟 cat 命令](#2.4 模拟 cat 命令)
    • [2.5 stdin、stdout、stderr](#2.5 stdin、stdout、stderr)
  • [3. 系统文件 IO](#3. 系统文件 IO)
    • [3.1 为什么需要系统调用](#3.1 为什么需要系统调用)
    • [3.2 open 打开文件](#3.2 open 打开文件)
    • [3.3 write 写文件](#3.3 write 写文件)
    • [3.4 read 读文件](#3.4 read 读文件)
  • [4. 文件描述符 fd](#4. 文件描述符 fd)
    • [4.1 fd 到底是什么](#4.1 fd 到底是什么)
    • [4.2 fd 的本质原理](#4.2 fd 的本质原理)
    • [4.3 fd 分配规则](#4.3 fd 分配规则)
  • [5. 重定向](#5. 重定向)
    • [5.1 什么是重定向](#5.1 什么是重定向)
    • [5.2 重定向本质](#5.2 重定向本质)
    • 输出重定向原理图
    • [5.3 dup2 实现重定向](#5.3 dup2 实现重定向)
    • [5.4 dup2 示例详解](#5.4 dup2 示例详解)
  • [6. shell 中的重定向](#6. shell 中的重定向)
    • [6.1 识别重定向符号](#6.1 识别重定向符号)
    • [6.2 为什么重定向放在子进程](#6.2 为什么重定向放在子进程)
  • [7. Linux 下一切皆文件](#7. Linux 下一切皆文件)
    • [7.1 为什么这么设计](#7.1 为什么这么设计)
    • [7.2 file 结构体](#7.2 file 结构体)
  • [8. 缓冲区](#8. 缓冲区)
    • [8.1 什么是缓冲区](#8.1 什么是缓冲区)
    • [8.2 为什么需要缓冲区](#8.2 为什么需要缓冲区)
    • [8.3 三种缓冲区](#8.3 三种缓冲区)
  • [9. 系统调用与库函数关系](#9. 系统调用与库函数关系)
  • [10. 基础 IO 高频面试题](#10. 基础 IO 高频面试题)
    • [10.1 为什么 printf 比 write 慢?](#10.1 为什么 printf 比 write 慢?)
    • [10.2 为什么 stderr 默认无缓冲?](#10.2 为什么 stderr 默认无缓冲?)
    • [10.3 dup2 本质是什么?](#10.3 dup2 本质是什么?)
    • [10.4 Linux 为什么说一切皆文件?](#10.4 Linux 为什么说一切皆文件?)
    • [10.5 为什么下面代码能够实现输出重定向?](#10.5 为什么下面代码能够实现输出重定向?)
  • 结语

引言

最近在学习 Linux 基础 IO 的时候,我发现自己虽然会写:fopen(), read(), write(), printf()这些函数。

但很多东西其实一直都只是:会用,但不理解

比如:

  • 文件描述符 fd 到底是什么?
  • 为什么 stdout 是 1?
  • 为什么 close(1) 之后 printf 会输出到文件?
  • dup2 到底做了什么?
  • Linux 为什么说"一切皆文件"?
  • printf 和 write 到底有什么区别?
  • 缓冲区为什么会影响输出?

刚开始学的时候,我甚至以为:

text 复制代码
printf 就是直接打印到屏幕

后来才发现:

text 复制代码
printf 底层最终也是 write

而 write 最终还会进入 Linux 内核。

越往后学越发现:

text 复制代码
Linux 中很多看起来"理所当然"的东西
其实底层都特别巧妙。

尤其是在学习:

  • Shell
  • 网络编程
  • Socket
  • Pipe
  • epoll
  • Redis
  • Nginx

这些内容之后。

我越来越感觉:

IO 真的是 Linux 的核心。

所以这篇文章我想结合自己的学习过程,整理一下 Linux 基础 IO 中最重要的一些知识点,包括:

  • C 文件接口
  • 系统调用
  • 文件描述符 fd
  • 重定向
  • dup2
  • 缓冲区
  • Linux 一切皆文件
  • minishell 重定向实现

等等。


1. 理解"文件"

1.1 什么是文件

很多同学第一次接触文件时,会下意识认为:

文件 = txt、jpg、mp4

其实这只是 "狭义文件"

Linux 对文件的理解远比这个更广。

  • 文件在磁盘中
  • 磁盘属于外设
  • 对文件的操作本质是 IO
  • Linux 下一切皆文件

也就是说:

text 复制代码
文件 ≠ 只是普通文本
文件 = 操作系统对资源的一种抽象

1.1.1 狭义文件

比如:

  • hello.txt
  • music.mp3
  • movie.mp4
  • code.cpp

这些都是存储在磁盘中的数据。

磁盘属于永久存储设备,因此:文件具有持久化特征

即使程序退出,文件仍然存在。


1.1.2 广义文件

Linux 中:

  • 键盘
  • 显示器
  • 网卡
  • 管道
  • socket
  • 进程信息

全部都可以被当作"文件"处理。

这也是 Linux 最经典的一句话:Linux 下一切皆文件

💡你可以把 Linux 理解成一个"超级统一接口系统"。

比如现实世界中:

  • 遥控器控制电视
  • 鼠标控制电脑
  • 钥匙开门
  • 方向盘控制汽车

每种设备操作方式都不一样。

但 Linux 非常聪明:

它把所有资源都抽象成"文件",于是开发者只需要掌握 read/write/open 这一套接口,就能操作绝大多数系统资源。


1.2 文件的本质

文件并不只是"内容"。

实际上:
文件 = 文件属性 + 文件内容

比如:

  • 文件名
  • 文件大小
  • 创建时间
  • 权限
  • 所有者

这些都属于:元数据(metadata),而真正的数据才叫内容。

所以:

所有文件操作本质上都是:

  • 对文件内容操作
  • 对文件属性操作

2. 回顾 C 文件接口

2.1 fopen 打开文件

先来看最经典的栗子🌰:

cpp 复制代码
#include <stdio.h>

int main()
{
    FILE *fp = fopen("myfile", "w");
    if(!fp){
        printf("fopen error!\n");
    }

    while(1);

    fclose(fp);
    return 0;
}

这里最关键的问题是:myfile 到底创建在哪?

答案是:当前进程工作目录

Linux 会通过:/proc/[pid]/cwd 找到当前运行路径。

例如:

bash 复制代码
ls /proc/533463 -l

你会看到:

text 复制代码
cwd -> /home/hyb/io

说明程序当前运行目录就在:

text 复制代码
/home/hyb/io

因此 myfile 就会被创建到这里。


2.2 fwrite 写文件

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
    FILE *fp = fopen("myfile", "w");

    if(!fp){
        printf("fopen error!\n");
    }

    const char *msg = "hello bit!\n";

    int count = 5;

    while(count--){
        fwrite(msg, strlen(msg), 1, fp);
    }

    fclose(fp);
    return 0;
}

这里:

cpp 复制代码
fwrite(msg, strlen(msg), 1, fp);

参数含义:

参数 含义
msg 写入内容地址
strlen(msg) 每次写入大小
1 写入次数
fp 文件流

2.3 fread 读文件

cpp 复制代码
while(1){

    ssize_t s = fread(buf, 1, strlen(msg), fp);

    if(s > 0){
        buf[s] = 0;
        printf("%s", buf);
    }

    if(feof(fp)){
        break;
    }
}

这里有一个经典坑点:

cpp 复制代码
fread 返回值
不是 bool
而是实际读取字节数

很多初学者会写成:

cpp 复制代码
if(fread(...))

但实际上应该根据返回字节数来判断。


2.4 模拟 cat 命令

PDF 中还实现了一个简单版 cat

cpp 复制代码
FILE *fp = fopen(argv[1], "r");

然后不断读取:

cpp 复制代码
fread(buf, 1, sizeof(buf), fp);

再输出。

其实 Linux 的:

bash 复制代码
cat file.txt

底层逻辑也是类似的。

💡这就像你拿着一个水杯不断从桶里舀水,fread 每次读取一部分数据,然后 printf 再把它"倒"到显示器,一直循环直到 EOF(文件结束)。


2.5 stdin、stdout、stderr

Linux 默认会打开三个流:

cpp 复制代码
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

对应:

名称 含义
stdin 标准输入
stdout 标准输出
stderr 标准错误

对应 fd:

fd 含义
0 stdin
1 stdout
2 stderr

这三个非常重要。

后面的:

  • 重定向
  • dup2
  • shell
  • 管道

全部围绕它们展开。


3. 系统文件 IO

3.1 为什么需要系统调用

很多同学会误以为:fopen 就是操作系统提供的,其实不是。

fopen/fread/fwrite 属于 C 标准库函数。

而真正底层的是:

open/read/write/close

这些系统调用。

关系如下:

text 复制代码
fwrite
   ↓
write
   ↓
系统调用
   ↓
内核
   ↓
磁盘

3.2 open 打开文件

cpp 复制代码
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);

参数解析:

参数 含义
myfile 文件名
O_WRONLY 只写
O_CREAT 不存在则创建
0644 文件权限

这里 O_WRONLY | O_CREAT 采用的是:按位或组合标志位。

例如:

cpp 复制代码
#define ONE   0001
#define TWO   0002
#define THREE 0004

然后:

cpp 复制代码
ONE | TWO

即可同时表达多个状态。


3.3 write 写文件

cpp 复制代码
write(fd, msg, len);

参数:

参数 含义
fd 文件描述符
msg 数据地址
len 写入长度

返回值:实际写入字节数

注意:write 并不保证一次写完

这是高频面试点。

尤其网络编程中的短写问题特别重要。


3.4 read 读文件

cpp 复制代码
ssize_t s = read(fd, buf, strlen(msg));

和 write 类似:

参数 含义
fd 文件描述符
buf 缓冲区
strlen(msg) 读取长度

返回值:

返回值 含义
>0 读取成功
0 EOF
<0 出错

4. 文件描述符 fd

4.1 fd 到底是什么

文件描述符本质是一个小整数。

例如:

cpp 复制代码
int fd = open("myfile", O_RDONLY);
printf("fd:%d\n", fd);

输出:

text 复制代码
fd:3

为什么是 3?

因为:

text 复制代码
0 -> stdin
1 -> stdout
2 -> stderr

已经被占用了。

所以新的文件从3开始分配。


4.2 fd 的本质原理

Linux 内核中:

每个进程都有 files_struct,用来维护文件描述符表

本质上fd 就是数组下标

例如:

text 复制代码
fd_table

0 -> stdin
1 -> stdout
2 -> stderr
3 -> myfile

fd = 3就能找到对应文件。

💡可以把 fd 理解成"酒店房间号",真正的房间不是数字,数字只是索引。

比如:

  • 301 房间
  • 302 房间

fd 就像房间号,内核通过 fd 找到真正的 file 结构体。


4.3 fd 分配规则

运行以下代码:

cpp 复制代码
close(0);
int fd = open("myfile", O_RDONLY);

结果:

text 复制代码
fd = 0

为什么?

因为 Linux 分配 fd 的规则是:寻找当前最小可用,即:谁空闲就分配谁。


5. 重定向

5.1 什么是重定向

例如:

bash 复制代码
ls > log.txt

这就叫输出重定向

原本输出到stdout(显示器),现在输出到文件


5.2 重定向本质

举一个经典栗子🌰:

cpp 复制代码
close(1);

int fd = open("myfile", O_WRONLY|O_CREAT, 0644);

printf("hello\n");

结果:

text 复制代码
printf 内容写入文件

为什么?

因为:

text 复制代码
open 得到的 fd = 1

于是:

text 复制代码
stdout 被替换成了文件

所以:

text 复制代码
printf
↓
stdout
↓
fd=1
↓
myfile

这就是重定向本质。

5.3 dup2 实现重定向

函数原型:

cpp 复制代码
int dup2(int oldfd, int newfd);

作用:让 newfd 指向 oldfd 对应文件

例如:

cpp 复制代码
dup2(fd, 1);

即:让 stdout 指向 fd 对应文件

于是 printf 全部写入文件。


5.4 dup2 示例详解

cpp 复制代码
int fd = open("./log", O_CREAT | O_RDWR);

close(1);

dup2(fd, 1);

流程:

text 复制代码
1. 打开 log 文件
2. 得到 fd
3. 关闭 stdout
4. 让 1 指向 log

于是:

text 复制代码
printf → log

完成输出重定向。


6. shell 中的重定向

PDF 中还实现了 minishell 的重定向功能。fileciteturn0file0

这一部分非常有含金量。


6.1 识别重定向符号

cpp 复制代码
if(command_buffer[end] == '<')

< 表示输入重定向 ,而 > 表示输出重定向>> 表示追加重定向


cpp 复制代码
if(redir == InputRedir)
{
    int fd = open(filename, O_RDONLY);
    dup2(fd, 0);
}

输入重定向:文件 → stdin

输出重定向:dup2(fd, 1);,即:stdout → 文件

追加模式:O_APPEND 即可。


6.2 为什么重定向放在子进程

重定向应该由子进程完成

为什么?

因为shell 自己不能被污染

如果父进程 shell 自己 stdout 被改掉,那么:整个终端都会失控.

所以:

text 复制代码
fork 子进程
↓
子进程重定向
↓
exec 执行命令

这才是正确流程。

💡这就像公司派员工出差,你不可能让老板永久搬去外地,应该让"子员工"去执行特殊任务,子进程就是那个临时员工。


7. Linux 下一切皆文件

7.1 为什么这么设计

Linux 最伟大的设计之一:统一 IO 接口

例如:resd()write()

可以实现:

  • 读文件
  • 读键盘
  • 读 socket
  • 读 pipe
  • 读设备

的全部统一,这极大降低了开发复杂度。


7.2 file 结构体

Linux 内核 file 结构的核心字段如下:

cpp 复制代码
struct file {
    struct inode *f_inode;
    const struct file_operations *f_op;
    unsigned int f_flags;
    loff_t f_pos;
};

重点:f_op,即:文件操作函数表

里面包含:read, write, open, mmap, ioctl 等函数指针。

其中,不同设备实现不同 read/write,但用户层统一调用 read/write,这就是 Linux 抽象哲学。


8. 缓冲区

8.1 什么是缓冲区

缓冲区是内存中的一块区域,用于临时缓存 IO 数据

例如:

text 复制代码
磁盘 → 缓冲区 → 程序

而不是:

text 复制代码
磁盘 → 程序

直接读取。


8.2 为什么需要缓冲区

因为:系统调用非常昂贵

涉及:

  • 用户态
  • 内核态
  • 上下文切换

频繁调用会严重降低性能。

一次读大块数据,放入缓冲区,后续直接从内存读取,速度极快。

💡这就像去超市买矿泉水,你不可能每喝一口就跑一次超市。正确做法是一次买一箱,缓冲区就是这一整箱水。


8.3 三种缓冲区

一共有三种缓冲模式:全缓冲、行缓冲、无缓冲。


1)全缓冲

缓冲区满了才刷新,适合磁盘文件。


2)行缓冲

遇到 \n 才刷新,适合终端输出。

例如:

cpp 复制代码
printf("hello\n");

立刻显示。


3)无缓冲

直接系统调用

典型:stderr 因为错误信息必须立刻输出。


9. 系统调用与库函数关系

核心理解:库函数 ≠ 系统调用

而是:库函数 = 对系统调用的封装

例如:

text 复制代码
printf
↓
fprintf
↓
fwrite
↓
write
↓
系统调用

这也是为什么:printf 最终也会进入内核


10. 基础 IO 高频面试题

10.1 为什么 printf 比 write 慢?

text 复制代码
printf 需要格式化,并且存在缓冲区,而write 更接近底层

10.2 为什么 stderr 默认无缓冲?

text 复制代码
因为错误信息必须立即显示,否则程序崩了,错误日志还没刷新,就会非常难排查。

10.3 dup2 本质是什么?

text 复制代码
修改文件描述符映射关系,而不是复制文件内容。

10.4 Linux 为什么说一切皆文件?

text 复制代码
因为Linux 使用统一 file 抽象,开发者只需要 read/write/open 即可操作绝大多数资源。

10.5 为什么下面代码能够实现输出重定向?

cpp 复制代码
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0644);
printf("hello\n");

答:

Linux 分配文件描述符时,总是分配当前最小可用 fd,由于 close(1) 关闭了 stdout,因此 open() 返回的 fd 为1,于是 stdout 被替换为 log.txt,而 printf 底层最终仍然使用 fd=1,因此输出进入文件。


结语

基础 IO 是 Linux 最核心的知识之一。

很多同学学习 Linux 时:

  • 会写 fopen
  • 会写 printf
  • 会写 read/write

但并不真正理解:

  • fd 是什么
  • 文件如何管理
  • 重定向如何实现
  • dup2 为什么有效
  • Linux 为什么"一切皆文件"
  • 缓冲区为什么能提高性能

而这些,恰恰是:

  • 操作系统
  • Linux 后端
  • 网络编程
  • Redis/Nginx/MySQL
  • 高性能服务器

的核心基础。

真正厉害的程序员,往往不是"API 记得多",而是:理解系统底层抽象.

当你真正理解:

text 复制代码
文件描述符
↓
file结构
↓
系统调用
↓
file_operations
↓
驱动层

这一整条链路时,就会发现:Linux 世界 suddenly clear.

后续再学习:

  • epoll
  • socket
  • pipe
  • mmap
  • reactor
  • nginx
  • redis

都会轻松很多。

因为它们底层本质都是 IO

相关推荐
源图客1 小时前
Go语言goland代码编辑与调试
开发语言·后端·golang
xmlhcxr1 小时前
从 0 到 1 落地企业级 DevOps CI/CD 流水线:基于 GitLab+Jenkins+Harbor 的完整实践
运维·docker·gitlab·jenkins·harbor·devops
赋创小助手1 小时前
PCIe 8.0 要来了:1TB/s 带宽背后,AI 算力服务器正在进入“高速互联时代”
运维·服务器·人工智能
froginwe111 小时前
R 绘图 - 饼图
开发语言
xiaoye-duck1 小时前
《Linux系统编程》Linux指令 (三):从零开始理解Linux基础指令
linux
好赞科技1 小时前
深度对比2026年三款小程序商城精选推荐榜单,解决您的电商选择难题
大数据·运维·人工智能
三十六煩惱風1 小时前
2026-04/20~26技术问题整理
开发语言
hecgaoyuan1 小时前
解决使用Nvidia Jetson Nano嵌入式系统远程桌面不能操作的问题
linux
贝锐1 小时前
贝锐向日葵:商用安卓设备大规模远程运维体系搭建指南
运维·远程控制