前段时间遇到一个小问题,后来发现这是个挺常见的坑,顺手整理一篇笔记。
🔥个人主页:爱和冰阔乐 📚专栏传送门:《数据结构与算法》 、C++ 🐶学习方向:C++方向学习爱好者 ⭐人生格言:得知坦然 ,失之淡然
🏠博主简介
文章目录
- 前言
- 一、重谈重定向:stdout 和 stderr 为什么可以分离?
- 1.1 标准输出和标准错误不是一回事
- 1.2 为什么要区分 stdout 和 stderr?
- 1.3
2>&1到底是什么意思? - 1.4
2>1和2>&1不是一回事
前言
前面大家已经知道,Linux 文件 I/O 里有一条很重要的主线:
进程通过文件描述符 fd,在自己的文件描述符表中找到对应的 struct file,然后由操作系统完成真正的读写操作。
理解了这句话以后,再看 fopen、open、read、write、close 这些接口,其实就不会觉得它们是分散的知识点了。
这篇文章继续往下看几个更容易混的内容:
- 标准输出和标准错误为什么可以分开重定向?
2>&1到底是把谁复制给谁?struct file里面的f_pos、f_count、f_op分别有什么用?- Linux 为什么说“一切皆文件”?
printf和write都能输出,为什么重定向后表现不一样?- glibc 里的
FILE*到底封装了什么?
一、重谈重定向:stdout 和 stderr 为什么可以分离?
大家平时写输出重定向时,最常见的是:
./a.out > log.txt
其实完整写法应该是:
./a.out 1> log.txt
这里的 1 指的是标准输出,也就是 stdout 对应的文件描述符。
正常情况下可以省略 1,因为 > 默认就是重定向标准输出。
先看一段代码:
#include
#include
int main()
{
std::cout stdout 最终对应的是文件描述符 1,只要把 1 的指向改掉,标准输出的位置就会跟着改变。
---
### 1.1 标准输出和标准错误不是一回事
再看下面这段代码:
include
include
int main()
{
// 标准输出:stdout / cout -> fd 1
std::cout 显示器
这就是为什么标准输出进了文件,而标准错误还在屏幕上。
---
### 1.2 为什么要区分 stdout 和 stderr?
一开始我也觉得奇怪:反正都是往显示器上打印,为什么还要分成 `stdout` 和 `stderr`?
其实这样设计是为了把**普通结果**和**错误信息**分开。
比如程序正常运行时输出结果,用 `stdout`;程序出现错误、调试信息、异常提示,用 `stderr`。
这样我们就可以在命令行中分别保存:
./a.out 1> log.normal 2> log.error
含义是:
- `1> log.normal`:普通输出写入 `log.normal`
- `2> log.error`:错误输出写入 `log.error`

这样做的好处是很明显的:
> 正常结果和错误日志不会混在一起,排查问题时更方便。
---
### 1.3 2>&1 到底是什么意思?
如果想让标准输出和标准错误都进入同一个文件,有两种写法。
第一种:
./a.out 1> log.txt 2>> log.txt
这里标准错误使用 `>>` 是为了追加,否则两次 `>` 可能会出现覆盖问题。
第二种更常见:
./a.out 1> log.txt 2>&1

这条命令不要死记,要拆开看。
第一步:
1> log.txt
底层类似:
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
close(fd);
执行完后:
fd 1 -> log.txt
fd 2 -> 显示器
第二步:
2>&1
这里的 `&1` 表示“文件描述符 1”,不是普通文件名。
所以 `2>&1` 的含义是:
> 让 fd = 2 复制当前 fd = 1 的指向。
底层可以理解成:
dup2(1, 2);
注意,复制的是 **当前 1 号文件描述符背后指向的文件对象**。由于第一步已经让 `1` 指向了 `log.txt`,所以第二步执行完以后,`2` 也指向 `log.txt`。
最终结果:
fd 1 -> log.txt
fd 2 -> log.txt
这就是 `2>&1` 的真正含义。
---
### 1.4 2>1 和 2>&1 不是一回事
这个地方很容易写错。
./a.out 2>1
它的意思是:
> 把标准错误重定向到一个名字叫 1 的普通文件里。
而:
./a.out 2>&1
才是:
> 把标准错误重定向到标准输出当前指向的位置。
所以 `&` 不能省略。
---
---
## 二、补充:内建命令的重定向为什么要恢复现场?
上面讲的重定向,大多数情况都是针对普通外部命令来说的。
比如:
ls -l > log.txt
这种命令通常可以 `fork` 出子进程,然后在子进程中完成:
dup2(fd, 1);
execvp(...);
因为子进程的文件描述符表被改了,并不会污染父 Shell。
但是 Shell 里还有一种特殊命令,叫做**内建命令**,比如:
cd
export
echo
这些命令有些不能简单交给子进程执行。比如 `cd` 如果在子进程里执行,只会改变子进程的工作目录,父 Shell 的目录并不会变,所以 `cd` 这类命令必须由父 Shell 自己执行。
这时候问题就来了:
echo "hello" > log.txt
如果 `echo` 被当成内建命令,由父 Shell 自己执行,那么父进程就必须临时完成重定向。
但父进程不能永久改掉自己的 `fd = 1`,否则命令执行完之后,Shell 后续的提示符和输出都会继续写进 `log.txt`,终端就被污染了。
所以内建命令的重定向需要一个额外步骤:
> 先备份现场,执行完命令后再恢复现场。
---
### 2.1 dup:先把标准输出备份起来
可以先调用:
int backup = dup(1);
`dup(1)` 的作用是:在当前进程的文件描述符表里找一个最小的空闲位置,把 `fd = 1` 当前指向的文件对象复制过去。
比如此时 `fd = 1` 指向显示器,`dup(1)` 可能返回 `3`:
fd 1 -> 显示器
fd 3 -> 显示器
这一步相当于先把“显示器的指向”保存到 `backup` 里。
---
### 2.2 dup2:临时把标准输出切到文件
接着打开目标文件:
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
然后执行:
dup2(fd, 1);
close(fd);
此时:
fd 1 -> log.txt
backup 3 -> 显示器
接下来父 Shell 执行内建命令时,输出就会进入 `log.txt`。
---
### 2.3 命令执行完后恢复标准输出
内建命令执行完成后,必须马上恢复:
dup2(backup, 1);
close(backup);
恢复后:
fd 1 -> 显示器
这样父 Shell 的标准输出就回到了终端,后续命令提示符也不会被写进文件。
完整流程可以整理成:
int backup = dup(1); // 1. 备份标准输出
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1); // 2. 临时重定向到文件
close(fd);
// 3. 执行内建命令,比如 echo/export/cd 等
RunBuiltinCommand();
dup2(backup, 1); // 4. 恢复标准输出
close(backup); // 5. 关闭备份 fd
这就是内建命令重定向和普通外部命令重定向最大的区别:
场景重定向位置是否需要恢复外部命令子进程中重定向,然后 `exec`一般不需要,子进程退出即可内建命令父 Shell 自己重定向必须恢复,否则污染 Shell
所以 Shell 实现重定向时,不能只会 `dup2(fd, 1)`,还要看命令到底是在子进程执行,还是父进程自己执行。
## 三、从源码角度看 fd 表和 struct file
前面一直在说:重定向的本质是修改文件描述符表中某个下标背后的指向。
那文件描述符表到底长什么样?
在内核中,进程由 `task_struct` 描述。每个进程都有自己的文件相关信息,其中有一个很重要的指针:
struct task_struct {
// ...
struct files_struct files;
// ...
};
也就是说:
> 进程通过 files 指针找到自己的文件描述符表。
---
### 2.1 files_struct:文件描述符表
`files_struct` 中有一个非常重要的数组:
struct files_struct {
atomic_t count; // 引用计数
struct fdtable fdt;
struct fdtable fdtab;
spinlock_t file_lock;
int next_fd;
struct embedded_fd_set close_on_exec_init;
struct embedded_fd_set open_fds_init;
struct file fd_array[NR_OPEN_DEFAULT]; // 文件描述符表数组
};
这里最关键的是:
struct file fd_array[NR_OPEN_DEFAULT];
它是一个指针数组,每个元素存放一个 `struct file*`。
用户层拿到的 `fd`,本质上就是这个数组的下标。
比如:
fd_array[0] -> 标准输入对应的 struct file
fd_array[1] -> 标准输出对应的 struct file
fd_array[2] -> 标准错误对应的 struct file
fd_array[3] -> 新打开文件对应的 struct file
所以:
> fd 不是文件本身,而是当前进程文件描述符表中的数组下标。
---
### 2.2 struct file:内核中的打开文件对象
当一个文件被进程打开时,内核会创建一个 `struct file` 对象,用来描述“本次打开”。
简化后可以看成这样:
struct file {
atomic_t f_count; // 引用计数
unsigned int f_flags; // 打开方式,如 O_APPEND、O_TRUNC
mode_t f_mode; // 访问模式,如读、写
loff_t f_pos; // 当前读写位置
const struct file_operations f_op; // 操作函数集合
void
private_data; // 私有数据,常给设备驱动使用};
几个成员要重点理解:
成员含义`f_count`引用计数,表示有多少地方正在使用这个打开文件对象`f_flags`打开文件时的标志位,比如 `O_APPEND``f_mode`当前文件的读写模式`f_pos`当前读写位置`f_op`文件操作函数表,里面是一组函数指针
---

### 2.3 为什么刚写完不能马上读?
`struct file` 中只有一个 `f_pos`,它表示当前读写位置。
不管是读还是写,都会影响这个位置。
比如用读写方式打开文件后,先写入内容:
write(fd, "hello", 5);
写完后,`f_pos` 已经移动到了文件末尾。
如果这时候马上:
read(fd, buf, sizeof(buf));
就会从文件末尾开始读,自然读不到刚才写入的内容。
所以如果想重新从头读,需要使用:
lseek(fd, 0, SEEK_SET);
或者在 C 库层使用:
rewind(fp);
这就是为什么之前学习 `w+` 时,写完想读必须调整读写位置。
---
### 2.4 进程和文件是解耦的
进程有自己的 `task_struct`,打开文件有自己的 `struct file`。
它们不是直接绑死在一起,而是通过文件描述符表建立联系:
task_struct
|
v
files_struct
|
v
fd_array[fd]
|
v
struct file
也就是说:
> 进程通过 fd 找到文件对象,文件对象再记录本次打开的状态。
这个设计让进程管理和文件管理解耦了。
---
## 四、一切皆文件:VFS 到底做了什么?
Linux 下常说一句话:
> 一切皆文件。
这句话不能只背下来,要结合 `struct file` 和 `file_operations` 理解。
在 Windows 中,普通文件是文件;在 Linux 中,不只是普通文件,键盘、显示器、磁盘、管道、socket 等很多资源也会被抽象成文件来管理。
这样做的好处是:
> 用户层可以使用统一的 read/write/open/close 接口访问不同资源。
比如:
- 读普通文件:`read(fd, buf, size)`
- 从键盘读:`read(0, buf, size)`
- 向屏幕写:`write(1, buf, size)`
- 向管道写:`write(pipefd[1], buf, size)`
上层接口看起来一样,但底层设备完全不同。
---

### 3.1 不同设备的读写方式肯定不一样
磁盘、键盘、显示器、网卡,这些设备硬件结构完全不同。
它们都可以“读写”,但是具体实现肯定不一样。
比如:
- 键盘主要负责输入
- 显示器主要负责输出
- 磁盘既可以读也可以写
- 网卡有自己的网络收发逻辑
那么 Linux 怎么能用统一的 `read/write` 管理这些不同设备?
答案就在 `struct file` 里的 `f_op`。
---
### 3.2 file_operations:函数指针表
`struct file` 中有一个成员:
const struct file_operations f_op;
`file_operations` 结构体里面存放的是一组函数指针:
struct file_operations {
ssize_t (read) (struct file , char __user , size_t, loff_t );
ssize_t (write) (struct file , const char __user , size_t, loff_t );
int (open) (struct inode , struct file );
int (release) (struct inode , struct file );
// ...
};
可以这样理解:
> file_operations 就是一张操作函数表,里面规定了这个文件对象应该怎么读、怎么写、怎么打开、怎么关闭。
如果打开的是普通磁盘文件,`f_op->read/write` 指向磁盘文件系统对应的读写方法。
如果打开的是显示器设备,`f_op->write` 就会指向显示器驱动提供的写方法。
如果打开的是键盘设备,`f_op->read` 就会指向键盘驱动提供的读方法。
所以当用户层调用:
write(fd, buf, size);
内核大致会走这样的路线:
当前进程
-> fd_array[fd]
-> struct file
-> file->f_op->write(...)
-> 具体设备驱动
这就是“一切皆文件”的关键。
---

### 3.3 VFS:屏蔽底层差异的一层
我们可以把 `struct file` 和 `file_operations` 这一层理解成 VFS 的一部分。
VFS 全称是:
> Virtual File System,虚拟文件系统。
它的作用不是直接操作某个具体硬件,而是做一层统一抽象。
上层只需要调用统一接口:
read(fd, ...);
write(fd, ...);
至于底层到底是磁盘、显示器、键盘、管道还是 socket,就交给 `f_op` 中的函数指针去分发。
这有点像 C 语言版本的“多态”:
层次作用用户层统一调用 `read/write`VFS 层通过 `struct file` 和 `f_op` 做抽象驱动层不同设备实现自己的读写方法硬件层真正的物理设备
所以,“一切皆文件”不是说所有东西真的都变成了普通磁盘文件,而是说:
> Linux 在访问这些资源时,尽量把它们抽象成统一的文件接口。
---
## 五、缓冲区:为什么 printf 和 write 表现不一样?
接下来进入另一个容易混的点:缓冲区。
我们先看一个实验。
include
include
include
include
include
include
include
int main()
{
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
// C 库函数
printf("fd: %d\n", fd);
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
// 系统调用
const char
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
现象是:
- `write` 的内容可以写入文件。
- `printf` 的内容可能没有写进去。
为什么?
因为:
> printf 是 C 标准库函数,数据通常先进入用户态缓冲区;write 是系统调用,直接进入内核。
---
### 4.1 用户态缓冲区和内核缓冲区
这里要区分两种缓冲区:
缓冲区所在位置由谁管理常见接口用户态缓冲区用户空间C 标准库 glibc`printf/fprintf/fwrite`内核缓冲区内核空间操作系统`write/read`
`printf` 的数据不会一定马上进入内核,而是先写到 C 库维护的用户态缓冲区中。
满足某些条件后,C 库再调用底层的 `write`,把用户态缓冲区中的数据刷新到内核。
刷新条件常见有:
1. 调用 `fflush`
2. 缓冲区满了
3. 遇到换行并且是行缓冲
4. 进程正常退出时刷新
而 `write` 本身已经是系统调用,它不经过 C 库的 `FILE*` 缓冲区,所以表现和 `printf` 不一样。
---
### 4.2 close(fd) 为什么可能导致 printf 内容丢失?
回到上面的代码。
`printf` 的内容先进入 `stdout` 的用户态缓冲区。
`stdout` 内部记录的底层 fd 是 `1`。
因为我们先执行了:
close(1);
又打开了 `log.txt`,使得新文件占用了 `fd = 1`。
所以此时 `stdout` 的底层 fd 仍然是 1,只不过 1 背后已经指向了 `log.txt`。
但是问题在于,后面代码里又执行了:
close(fd);
而这个 `fd` 很可能就是 1。
也就是说,文件描述符 1 被关闭了。
等程序退出时,C 库想把 `stdout` 用户态缓冲区中的内容刷新出去,就需要通过 `stdout` 内部记录的 fd,也就是 1 去调用 `write`。
但这时候 fd 1 已经被关闭了,刷新失败,所以 `printf` 的内容就可能看不到。
如果想让 `printf` 的内容写进去,可以在关闭 fd 前强制刷新:
fflush(stdout);
close(fd);
这就是 `fflush` 的作用:
> 把 C 库用户态缓冲区中的数据,尽快刷新到内核。
---
### 4.3 三种缓冲策略
C 标准库常见有三种缓冲策略:
缓冲类型触发刷新条件常见场景无缓冲立即刷新`stderr` 常见行缓冲遇到 `\n` 刷新终端显示器常见全缓冲缓冲区满或主动刷新普通磁盘文件常见
这也解释了一个现象:
同样是 `printf("hello\n")`,输出到显示器时看起来马上出现;但重定向到普通文件时,可能要等程序结束或者缓冲区满才真正写入文件。
---

### 4.4 fflush、fsync、sync 的区别
这几个接口也容易混。
接口作用范围简单理解`fflush(FILE*)`C 库用户态缓冲区 -> 内核把 `FILE*` 缓冲区刷给操作系统`fsync(fd)`内核缓冲区 -> 磁盘设备要求内核把 fd 对应文件的数据同步到磁盘`sync` 命令系统级缓存 -> 外设要求系统把缓存数据统一刷回外设
可以这样记:
printf/fwrite
-> 用户态缓冲区
-> fflush
-> 内核缓冲区
-> fsync/sync
-> 磁盘/外设
所以:
> fflush 解决的是 C 库层面的刷新问题;fsync 解决的是内核到磁盘的同步问题。
---
## 六、fork 后为什么 printf 可能重复输出?
再看一个经典实验:
include
include
include
include
int main()
{
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char s = "hello fwrite\n";
fwrite(s, strlen(s), 1, stdout);
const char ss = "hello write\n";
write(1, ss, strlen(ss));
fork();
return 0;
}
如果直接输出到显示器,通常看到 4 条消息。
如果重定向到文件:
./a.out > log.txt
可能看到 7 条消息:
- `write` 输出 1 次
- `printf/fprintf/fwrite` 各输出 2 次


---
### 5.1 为什么输出到显示器时没有重复?
当输出目标是显示器时,C 库通常采用行缓冲。
也就是说:
printf("hello printf\n");
遇到 `\n` 后,数据就被刷新出去了。
等执行到 `fork()` 时,用户态缓冲区里基本已经没有这些内容,所以父子进程退出时不会再次重复刷新。
---
### 5.2 为什么重定向到文件后会重复?
当输出目标是普通文件时,C 库通常采用全缓冲。
这时候:
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(...);
这些内容可能还留在用户态缓冲区里,没有马上写入文件。
执行 `fork()` 后,子进程会继承父进程的用户态内存状态,其中就包括这份尚未刷新的缓冲区内容。
之后父进程和子进程都会退出。进程正常退出时,C 库会刷新自己的标准 I/O 缓冲区。
这里可以再细一点理解:父进程或子进程在退出时刷新缓冲区,本质上要把用户态缓冲区中的数据拷贝到内核,同时还会调整或清理这块用户态缓冲区的状态。这个过程会涉及对用户态缓冲区内存的修改,因此可能触发写时拷贝。
触发写时拷贝后,父子进程各自拥有一份缓冲区数据。于是:
- 父进程退出时刷新一次
- 子进程退出时也刷新一次
所以同一份 `printf/fprintf/fwrite` 内容就可能被写入两次。
---
### 5.3 为什么 write 只输出一次?
因为 `write` 是系统调用。
它在 `fork()` 之前就已经把数据交给了内核,不经过 C 库的用户态缓冲区。
所以子进程不会再继承一份 `write` 的用户态缓冲数据,自然也不会重复输出。
这说明:
> C 库函数和系统调用虽然都能完成输出,但它们中间隔着一层用户态缓冲区,所以在 fork 和重定向场景下表现会不一样。
---
## 七、glibc 中 FILE 的真身:_IO_FILE
我们平时用的 `FILE*` 到底是什么?
在 C 标准库里,`FILE` 不是内核的 `struct file`,而是用户态 glibc 提供的一个结构体。
内核中的 `struct file` 和 glibc 中的 `FILE` 不是一回事:
结构所在位置作用`struct file`内核态描述内核中一个被打开的文件对象`FILE / _IO_FILE`用户态 glibc描述 C 标准库中的文件流,包含用户态缓冲区和 fd
---
### 6.1 为什么有了 struct file,还需要 FILE?
因为系统调用有成本。
如果每输出几个字节都调用一次 `write`,用户态和内核态之间会频繁切换,效率很低。
所以 C 标准库在用户态又封装了一层 `FILE*`:
fprintf
-> 写入 FILE 用户态缓冲区
-> 缓冲区满足刷新条件
-> 通过 _fileno 拿到 fd
-> 调用 write(fd, ...)
-> 内核 struct file
-> 磁盘/设备
这样就能减少系统调用次数,提高效率。
---
### 6.2 FILE 中封装了什么?
简化理解,`FILE` 里面至少要封装:
- 对应的文件描述符 fd
- 用户态缓冲区
- 当前缓冲区读写位置
- 刷新策略
- 和其他 FILE 对象组织起来的指针
比如 glibc 中的 `_IO_FILE` 会包含类似字段:
struct _IO_FILE {
int _flags;
char _IO_read_ptr;
char _IO_read_end;
char _IO_read_base;
char _IO_write_base;
char _IO_write_ptr;
char _IO_write_end;
int _fileno; // 底层文件描述符 fd
struct _IO_FILE _chain; // 用来把多个 FILE 对象串起来
};
其中最重要的是:
字段含义`_fileno`C 库文件流绑定的系统层 fd`_IO_write_base / _IO_write_ptr / _IO_write_end`写缓冲区相关指针`_chain`将多个 `FILE` 对象连接起来,方便统一管理
---

### 6.3 进程退出时为什么文件会自动刷新?
我们经常会发现,即使没有手动 `fflush`,程序正常结束时,文件内容也可能被写进去。
这是因为 C 库会维护自己打开的 `FILE` 对象。
当进程正常退出时,C 库会尝试刷新并关闭这些已经打开的标准 I/O 流。
所以:
FILE fp = fopen("log.txt", "w");
fprintf(fp, "hello world\n");
return 0;
即使没有手动 `fclose(fp)`,正常情况下也可能看到内容写入文件。
但是这并不代表可以不写 `fclose`。
更好的习惯还是:
fclose(fp);
因为 `fclose` 不只是关闭文件,也会触发刷新,并释放相关 C 库资源。
---
## 八、模拟理解:为什么缓冲区能减少系统调用?
可以写一个简单实验来观察。
假设我们每秒查看一次文件内容:
while :; do
cat log.txt
sleep 1
echo "#########"
done

如果程序使用的是 C 库全缓冲,可能会发现:程序运行过程中,文件内容迟迟不出现;等程序结束或调用 `fclose/fflush` 后,内容才突然出现。
如果想在程序没有结束时也看到内容,可以手动刷新:
fflush(fp);

如果想进一步要求内核把数据同步到磁盘,可以使用:
fsync(fd);
```
这里要要注意层次:
fflush 是 C 库层的刷新,fsync 是系统调用层的同步。
它们不是一回事。
九、全文总结
本文主要把 Linux 文件 I/O 中几个容易分散的知识点串在了一起:重定向、2>&1、VFS、一切皆文件、缓冲区和 glibc 的 FILE*。
重定向的本质不是 printf 变了,而是进程文件描述符表中对应 fd 背后的指向变了。普通的 > 默认只改 fd = 1,所以标准错误 fd = 2 仍然可以留在屏幕上;而 2>&1 的核心就是让 2 复制当前 1 的指向。
一切皆文件的关键在于 VFS。上层统一调用 read/write,内核通过 struct file 中的 file_operations 函数指针,分发到不同设备自己的读写方法,从而屏蔽底层硬件差异。
缓冲区部分要分清用户态和内核态。printf/fprintf/fwrite 会先经过 glibc 的用户态缓冲区,而 write 是系统调用。正因为这层区别,重定向到文件后再 fork,C 库函数的输出可能重复刷新,而 write 不会。
最终,FILE* 并不是内核的 struct file,而是 glibc 在用户态封装出来的文件流结构。它内部保存了 fd、缓冲区和管理指针,最终还是要通过系统调用进入内核。
所以这篇最核心的一句话就是:
系统层用 fd 找到内核 struct file,C 库层用 FILE* 封装 fd 和用户态缓冲区,VFS 再通过 file_operations 屏蔽不同设备的差异。
抓住这条线,重定向、缓冲区、glibc 和“一切皆文件”就能连起来理解了。
资源分享
【Linux基础IO】printf 为什么会写进文件?一文搞懂重定向和 dup2 底层原理
【Linux】基础 I/O 详解:从 fopen 到 open,终于搞懂 fd 为什么从 3 开始
【Linux进程控制】从exec程序替换到手写简易Shell:fork、execvp、环境变量与内建命令
暂时整理到这里。以上都是个人理解,可能有疏漏,欢迎指正。
评论 (0)
暂无评论