xv6 IO重定向原理
概要:以 shell 执行 cat < input.txt 为例,讲解 IO 重定向的实现原理:子进程先关闭标准输入,再打开目标文件使其占用最小文件描述符 0,从而让 cat 从文件读取。
1 代码示例
- shell 执行
cat<input.txt的简化代码
char *argv[2];
argv[0] = "cat";
### argv[1] = 0;
if(fork() == 0) {
close(0);
open("input.txt", O_RDONLY);
exec("cat", argv);
}
shell 的实际行为是:
shell
└── fork()
└── child
close(0)
open("input.txt")
exec("cat")
1.1 执行过程:
- step 1:fork
child:
FD0 -> terminal
FD1 -> terminal
FD2 -> terminal
- step 2:close(0)
child:
FD0 -> 空
FD1 -> terminal
FD2 -> terminal
- step 3:open
open("input.txt") -> 返回 0
现在:
FD0 -> input.txt
FD1 -> terminal
- step 4:exec
exec("cat")
cat 启动时看到:
stdin = input.txt
stdout = terminal
所以 cat 自动从文件读取,而无需任何额外代码。
2 文件描述符的分配规则
在 Unix 中:
| FD | 含义 |
|---|---|
| 0 | stdin |
| 1 | stdout |
| 2 | stderr |
系统保证:
新分配的文件描述符一定是当前进程中最小的未使用整数
例如:
当前FD: 0 1 2 3 4
close(0)
现在空闲: 0
open() -> 返回 0
这就是代码里:
close(0);
open("input.txt", O_RDONLY);
的核心原理: 通过关闭 0,让 open 占用它,从而替换 stdin。
3 fork 会复制文件描述符表
执行:
pid = fork();
之后:
父进程FD表: 0 1 2 -> tty
子进程FD表: 0 1 2 -> tty (完全一样)
也就是说:
fork 会复制文件描述符表,但不会重新打开文件
父子进程:
- 共享同一个打开文件(内核对象)
- 但各自拥有独立的 FD 表
4 exec 不会清空文件描述符
exec 的行为是:
| 内容 | 是否保留 |
|---|---|
| 内存 | ❌ 替换 |
| 代码 | ❌ 替换 |
| 文件描述符 | ✅ 保留 |
因此:
fork -> 修改FD -> exec
是 shell 重定向的标准流程。
5 xv6实际代码解析
7930
case REDIR:
rcmd = (struct redircmd*)cmd;
close(rcmd->fd);
if(open(rcmd->file, rcmd->mode) < 0){
printf(2, "open %s failed\n", rcmd->file);
exit();
}
runcmd(rcmd->cmd);
break;
- 在 xv6 中,命令被解析成一棵结构体树,例如:
struct redircmd {
int type;
struct cmd *cmd; // 实际要执行的命令
char *file; // 重定向文件
int mode; // 打开方式
int fd; // 要替换的文件描述符
};
- 当前命令被解析为 **重定向命令**,强制转换为 `redircmd` 结构。
```c
rcmd = (struct redircmd*)cmd;
- 关闭要被替换的文件描述符:
close(rcmd->fd);
| 重定向类型 | fd |
|---|---|
< |
0 |
> |
1 |
2> |
2 |
例如:
cat < input.txt
这里:
rcmd->fd = 0
- 打开文件,返回 最小可用 fd
open(rcmd->file, rcmd->mode)
因为 fd 已经被关闭,所以:
open 会返回 最小可用 fd,也就是刚刚关闭的 fd。
这就完成了:
stdin -> input.txt
- 递归执行真正的命令
runcmd(rcmd->cmd);
xv6 shell 是递归执行命令树的,例如:
grep x < in.txt | sort > out.txt
解析结构:
pipe
/ \
redir redir
| |
grep sort
留下评论