☰
【Linux系统】14 基础IO(下)
2026/10/29 2:07:50 网站建设 项目流程

一、dup2 系统调用实现重定向

前面我们了解重定向的底层原理:关闭旧的文件描述符,再 open 新文件,利用文件描述符分配规则(优先分配最小的未被使用的 fd),让新文件占据 1 号(标准输出)、0 号(标准输入)、2 号(标准错误),完成重定向。

但是手动close再open的写法比较麻烦,Linux 提供dup2系统调用专门用来做文件描述符拷贝,实现重定向。

函数原型

#include <unistd.h> int dup2(int oldfd, int newfd);
  • oldfd:已经打开的有效文件描述符
  • newfd:想要让 oldfd 拷贝到的目标文件描述符
  • 逻辑:如果newfd原本是打开状态,dup2 会自动先关闭newfd;然后让fd_array[newfd]指向oldfd对应的内核file对象。
  • 返回值:成功返回 newfd;失败返回‑1。

注意:dup2 不会复制磁盘文件,只是复制进程内文件描述符表的指针,内核file结构体引用计数f_count会 + 1。

示例代码,实现输出重定向,把原本打印到显示器的内容输出到 log 文件:

#include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("./log", O_CREAT | O_RDWR, 0644); if (fd < 0) { perror("open"); return 1; } // 把fd拷贝到1号描述符,等价输出重定向 > log dup2(fd, 1); char buf[1024] = {0}; ssize_t read_size = read(0, buf, sizeof(buf)-1); if(read_size > 0) { printf("%s", buf); fflush(stdout); } close(fd); return 0; }

这里printf是 C 库函数,底层访问依旧去找 fd=1,现在 fd=1 指向 log 文件,输出全部写入文件,完成重定向。

二、给 minishell 增加重定向功能

shell 中常见三种重定向:

  1. <输入重定向:把文件内容当作程序标准输入
  2. >输出重定向:截断式输出,清空原有文件再写入
  3. >>追加重定向:在文件末尾追加内容,不删除原有内容

实现思路

  1. 解析命令行:扫描命令字符串,识别</>/>>,分割出真正执行的命令和重定向文件名;标记重定向类型。
  2. 重定向逻辑放在子进程:fork()创建子进程之后,程序替换exec*之前执行重定向。

重点:程序替换只会替换进程代码和数据,不会修改进程的文件描述符表,所以子进程做 dup2 重定向之后,exec 执行外部命令,重定向依旧生效。

  1. 父进程等待子进程退出。

核心逻辑片段:

// 重定向类型 #define NoneRedir 0 #define InputRedir 1 // < #define OutputRedir 2 // > #define AppRedir 3 // >> int redir = NoneRedir; char *filename = nullptr; // 执行重定向,子进程调用 void DoRedir() { if(redir == InputRedir) { int fd = open(filename, O_RDONLY); dup2(fd,0); close(fd); } else if(redir== OutputRedir) { // O_TRUNC:截断清空文件 int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666); dup2(fd,1); close(fd); } else if(redir == AppRedir) { // O_APPEND:追加写 int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666); dup2(fd,1); close(fd); } }

执行流程:

pid_t id = fork(); if(id == 0) { DoRedir(); //子进程完成重定向 execvpe(gargv[0], gargv, genv); //程序替换 exit(7); } waitpid(id, &status, 0);

小知识点:cd、export属于 shell内建命令,不能 fork 子进程执行。子进程修改工作目录、环境变量只会影响子进程,父 shell 环境不会变化,必须 shell 进程自己调用chdir、修改环境变量。

三、Linux “一切皆文件” 真正含义

狭义文件:磁盘上普通文件。 广义:Linux 下键盘、显示器、网卡、磁盘、管道、socket 套接字全部抽象成文件。

内核层面原理

每个打开的资源在内核中都有一个struct file结构体,结构体内部有一个struct file_operations *f_op指针。file_operations是一个巨大的函数指针表,里面包含read、write、llseek、open、release等函数指针。

不同硬件 / 设备,驱动实现自己的 read、write 方法:

  • 磁盘驱动的 read:从磁盘扇区读取数据;
  • 键盘驱动 read:等待键盘输入;
  • 显示器驱动 write:把数据输出到屏幕;
  • socket 套接字 read/write:收发网络数据包。

对上层用户来说,不管操作什么设备,只需要调用统一的系统调用read / write。操作系统根据 fd 找到struct file,调用file->f_op->read/write,底层就会走到对应设备驱动的函数。

这就是一切皆文件的核心:对外提供同一套系统调用接口,底层驱动各自实现差异化操作。

四、IO 缓冲区详解

缓冲区本质是内存空间,用来缓存数据,减少系统调用次数。

一次系统调用会发生用户态↔内核态切换,开销大;缓冲区攒一批数据再调用一次系统调用,大幅提升 IO 效率。

三种缓冲模式(C 标准库,用户态缓冲区)

  1. 全缓冲:缓冲区写满才触发系统调用。普通磁盘文件默认全缓冲。
  2. 行缓冲:遇到换行符\n,或者缓冲区写满,刷新缓冲区。标准输出 stdout 指向终端(显示器)默认行缓冲。
  3. 无缓冲:不缓存,调用库函数立刻执行系统调用。stderr标准错误,无缓冲,保证错误信息立刻输出。

触发缓冲区刷新的条件:

  1. 缓冲区存满;
  2. fflush()手动强制刷新;
  3. 程序进程正常退出,会自动刷新用户缓冲区。

⚠️坑点:重定向现象 当 stdout 原本指向显示器(行缓冲),一旦重定向到普通磁盘文件,stdout 自动切换为全缓冲模式。 此时 printf 打印的内容带有\n,也不会立刻刷新,只有缓冲区满、fflush、进程退出才会写入。

示例坑代码:

int main() { close(1); int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); printf("hello world\n", fd); close(fd); return 0; }

运行后 log.txt 是空文件! 原因:重定向之后 stdout 变成全缓冲,字符串不足以填满缓冲区;没有 fflush,在进程退出之前我们就close(fd)关闭文件,缓冲区数据还留在用户内存,没有落地到文件。 修复:fflush(stdout);在 close 之前强制刷新缓冲区。

区分:用户缓冲区 VS 内核缓冲区

  • C 库 FILE 里面的缓冲区:用户态缓冲区,由 libc 提供,printf/fwrite使用;系统调用read/write没有用户缓冲区。
  • 调用 write 之后,数据进入内核缓冲区(操作系统维护),由操作系统决定什么时候刷到磁盘硬件;可以调用fsync/fdatasync强制把内核缓冲区落地磁盘。

五、FILE 结构体 和 文件描述符 fd

  1. fd:int 类型的小整数,内核层面标识打开文件,系统调用 open/read/write/close 使用。
  2. FILE*:C 标准库的结构体指针,用户层库函数封装,fopen/fprintf/fwrite 使用。

FILE结构体内部:

  • 内部成员_fileno保存封装的文件描述符 fd;
  • 一整套缓冲区指针:_IO_write_base、_IO_write_ptr、_IO_write_end等,维护用户缓冲区。

经典 fork 缓冲区面试题

#include <stdio.h> #include <unistd.h> int main() { printf("hello printf\n"); fwrite("hello fwrite\n", 13,1,stdout); write(1,"hello write\n",12); fork(); return 0; }
  • 直接运行输出到显示器:三行各输出一次。stdout 是行缓冲,遇到\n,fork 之前缓冲区已经刷新完毕。
  • 使用重定向./a.out > file输出到普通文件:printf、fwrite输出两次,write只输出一次。

原因:

  1. 重定向到文件,stdout 变成全缓冲。\n不会触发刷新;printf、fwrite 的数据还留在 C 库的用户缓冲区。
  2. fork()创建子进程,发生写时拷贝。父进程用户空间缓冲区数据拷贝一份给子进程。
  3. 父子进程各自退出,都会刷新自己的用户缓冲区,所以 printf/fwrite 打印两遍。
  4. write是系统调用,没有用户缓冲区,数据直接交给内核,fork 不会拷贝这份数据,只会输出一次。

总结:用户缓冲区是 C 库提供,存在用户空间;fd 属于内核。库函数 = 用户缓冲区逻辑 + 封装系统调用。

六、系统调用与库函数的关系

  • open / close / read / write / dup2:系统调用,操作系统内核提供接口,用户态切换内核态完成文件操作。
  • fopen / fclose / fread / fwrite / printf:C 标准库函数,运行在用户空间。

调用链路:

应用程序 → C 库函数(处理用户缓冲区) → 系统调用 → 内核 → 硬件驱动

库函数不是替代系统调用,是在系统调用之上封装缓冲区逻辑,给程序员更友好的接口。我们甚至可以自己模拟简易 C 库,封装 fd、自己实现缓冲区,模拟fopen/fwrite/fflush。

本篇小结

  1. dup2(oldfd, newfd):拷贝文件描述符指针,实现重定向,子进程中使用,exec 程序替换不影响 fd 表。
  2. shell 重定向解析:识别< > >>,子进程做 dup2 重定向;cd/export 等属于内建命令不能 fork。
  3. Linux 一切皆文件:依靠struct file和file_operations函数指针表,统一 read/write 系统调用,底层设备驱动各自实现。
  4. 缓冲区分为全缓冲、行缓冲、无缓冲;重定向会改变 stdout 缓冲模式;缓冲区刷新时机。
  5. fd 是内核文件描述符;FILE 结构体是 C 库封装,内部包含 fd + 用户缓冲区;fork 会拷贝用户缓冲区造成重复输出。
  6. 库函数封装系统调用,用户缓冲区属于 libc,write 系统调用没有用户缓冲区。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询