Linux基础IO------重定向原理剖析
本文基于Linux系统IO,讲解重定向是什么、底层原理、
dup2系统调用,以及shell中重定向的实现思路。
一、前置知识
1. 文件描述符fd
Linux进程默认打开3个文件描述符:
| fd | 名称 | 对应硬件 |
|---|---|---|
| 0 | stdin 标准输入 | 键盘 |
| 1 | stdout 标准输出 | 显示器 |
| 2 | stderr 标准错误 | 显示器 |
文件描述符本质:files_struct结构体内部fd_array指针数组的下标。
task_struct(进程PCB)里面有*files指针,指向files_struct;files_struct维护file* fd_array[],数组每一个元素是内核file结构体的指针;- open打开文件,内核创建
file对象,把对象地址填入数组的空闲下标,返回这个下标,就是fd。
fd分配规则 :从fd_array中找最小的未被使用的下标作为新的文件描述符。
比如关闭fd=1,再open新文件,新文件fd就会等于1。
2. 库函数与系统调用
- C库函数:
fopen/fclose/fread/fwrite/printf,属于用户层库,封装系统调用,自带用户缓冲区。 - 系统调用:
open/close/read/write/dup2,操作系统提供接口,没有用户缓冲区。>
FILE*结构体内部封装了文件描述符_fileno,库函数最终还是通过fd调用系统调用。
二、什么是重定向
重定向:不改变用户代码,改变数据流向。本该输出到显示器的数据,输出到文件;本该从键盘读数据,改成从文件读取。
shell常见重定向符号:
>:输出重定向,截断式,command > file,清空文件,把命令输出写入文件。>>:追加重定向,command >> file,把输出追加到文件末尾,不覆盖原有内容。<:输入重定向,command < file,命令不再从键盘读取,从文件读取数据。
重定向底层本质
进程的fd_array[1]原本保存显示器对应的file*指针。
如果我们close(1)关闭标准输出,再open目标文件 ,根据fd分配规则,新打开文件就拿到fd=1。
此时,所有往fd=1写入的数据(printf、fprintf、write(1,...)),都会写入这个新打开的文件,而不再输出显示器。
cpp
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(1); //关闭标准输出,释放fd=1下标
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
//新文件占用fd=1
printf("hello 重定向\n"); //printf底层操作fd=1,输出到test.txt
fflush(stdout); //库函数带缓冲区,必须刷新
close(fd);
return 0;
}
注意:C标准库有用户缓冲区 ,重定向到文件时stdout变成全缓冲,如果不fflush,进程退出前数据不会落到文件,出现文件为空。
上面手动close+open实现重定向有缺陷:原来打开的文件fd会被浪费,于是有专门的系统调用dup2。
三、dup2系统调用
函数原型
cpp
#include <unistd.h>
int dup2(int oldfd, int newfd);
功能:
- 如果
newfd已经打开,dup2会自动关闭newfd指向的文件; - 将
fd_array[oldfd]里面的file*拷贝赋值给fd_array[newfd]; - oldfd和newfd,两个下标同时指向同一个内核
file对象,引用计数f_count++; - 成功返回newfd,失败返回‑1。
dup2(fd,1):让fd指向的文件,占用标准输出1号描述符,完成输出重定向。不需要手动
close(1),dup2内部帮我们处理。
示例代码:
cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC,0644);
dup2(fd,1); //把fd拷贝到1,1现在指向log.txt
printf("hello dup2重定向"); //输出到log.txt
fflush(stdout);
close(fd); //关闭fd,但是fd=1仍然指向该file对象,引用计数没有到0,文件不会关闭
return 0;
}
注意:
close(fd)只是关闭我们自己的fd,dup2之后fd=1仍然指向文件,内核file对象引用计数没有归零,文件不会关闭。
四、Shell(minishell)如何实现重定向
整体思路:解析命令字符串识别重定向符号,在子进程执行程序替换之前完成重定向操作。
步骤拆解
- 解析命令行字符串
遍历命令buffer识别符号<、>、>>:
<:标记输入重定向,截取后面的文件名,把该符号位置置\0截断命令,命令只保留重定向符号之前的部分;>后面还有一个>就是>>追加重定向;否则是普通输出重定向。
- fork创建子进程
重定向操作必须在子进程中执行!
如果父进程shell做dup2,shell本身的输入输出就被重定向,shell本身无法正常交互。
- 子进程中执行
DoRedir()函数
根据解析出来的重定向类型,打开文件,调用dup2完成描述符拷贝:
- 输入重定向
<:open(filename,O_RDONLY),dup2(fd,0); - 输出重定向
>:open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666),dup2(fd,1); - 追加重定向
>>:open(filename,O_WRONLY|O_CREAT|O_APPEND,0666),dup2(fd,1)。
- 子进程执行程序替换execvpe
重点:exec程序替换只会替换进程的代码、数据,不会修改进程的文件描述符表!
所以dup2设置好的重定向,在exec之后依然生效。
- 父进程waitpid等待子进程退出。
核心伪代码
//子进程内
DoRedir();
execvpe(argv[0],argv,env);
exit(1);
区分:内建命令(cd、export)不创建子进程,直接shell进程内部执行,所以内建命令一般不做重定向。
五、常见坑点总结
- 缓冲区坑(printf库函数重定向)
- 输出到终端:stdout行缓冲,遇到
\n自动刷新; - 重定向到普通文件:stdout切换成全缓冲。没有换行、缓冲区没满,程序直接close,数据留在用户缓冲区,文件看不到内容。
解决:fflush(stdout)强制刷新用户缓冲区。>
注意:
write系统调用没有用户缓冲区,不受这个问题影响。stderr默认无缓冲。
- dup2之后不要忘记:oldfd可以close,但是newfd还在生效。内核
file对象靠引用计数管理,所有指向它fd全部close才会真正释放。 - exec不会修改fd表,重定向放在fork之后、exec之前。顺序不能颠倒,如果先exec再dup2,已经晚了。
- fd分配规则:最小未占用下标,理解为什么
close(1)再open拿到fd=1。
六、拓展:Linux一切皆文件和重定向
键盘、显示器、磁盘、socket套接字在内核都抽象成struct file,提供统一的read/write函数指针接口。
重定向本质就是改变fd下标指向哪一个file对象,所以可以把数据重定向到普通文件、管道、socket,实现丰富功能。