一、利用系统调用进行读写文件操作
open 是最核心的文件打开/创建系统调用,用于获取文件描述符(fd),后续的 write、read、close 等系统调用都依赖这个文件描述符。它直接与内核交互,支持灵活的文件操作模式和权限控制,是底层文件编程的基础。
函数原型:
#include <fcntl.h> // 包含 O_RDONLY、O_CREAT 等标志
#include <unistd.h> // 包含文件描述符相关定义
// 原型 1:打开已存在的文件(或创建文件,需配合 O_CREAT)
int open(const char *pathname, int flags);
// 原型 2:打开并指定文件权限(仅当 flags 包含 O_CREAT 时才需要第三个参数)
int open(const char *pathname, int flags, mode_t mode);
参数:
- pathname: 文件路径(绝对/相对)。绝对路径如 /home/user/test.c;相对路径如 test.c(相对于当前终端的工作目录)。进程知道自己在哪,即便文件不带路径,OS 也能知道要创建的文件放在哪里。
- flags (标记位):打开文件时传入的选项常量,可用'或'运算组合。常用常量包括:
- O_RDONLY: 只读打开
- O_WRONLY: 只写打开
- O_RDWR: 读,写打开
- O_CREAT: 若文件不存在,则创建它。需要使用 mode 选项指明新文件的访问权限。
- O_APPEND: 追加写,不清楚文件的内容。
- O_TRUNC: 清除文件的内容。
- mode: 文件权限(仅 flags 含 O_CREAT 时有效)。类型 mode_t,需用八进制数指定(如 0644、0755)。最终权限 = mode & ~umask。
- 返回值:成功返回新打开的文件描述符(fd);失败返回 -1。
在 C 语言层面,fopen 打开文件的方式有 r、r+、w、w+、a、a+,它们底层都会去调用 open 系统调用。上面使用的 fopen、fclose、fwrite 都是 C 标准库中的函数,称为库函数(libc)。这些函数是对系统调用的封装,方便二次开发,增加程序的可移植性。
write 文件写入系统调用
write 是 Linux 系统中最基础的文件写入系统调用,用于向文件描述符(文件、管道、socket 等)写入数据。它是底层 I/O 操作的核心,C 标准库的 fwrite 本质上也是对 write 的封装。
- 函数原型:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
- 参数说明: | 参数 | 含义 | | --- | --- | | fd | 文件描述符(非负整数):0 为 stdin,1 为 stdout,2 为 stderr,大于 2 为打开文件/管道/socket 等 | | buf | 指向要写入数据的缓冲区 | | count | 期望写入的字节数 |
- 返回值:成功返回实际写入的字节数;失败返回 -1。 注意:write 是底层系统调用,无用户态缓冲,调用后直接触发内核 I/O 操作。
系统调用写文件示例
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
umask(0); // 设置系统权限掩码为 0
// 调用系统调用 open 打开文件
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0)
{
perror("打开文件失败:");
exit(1);
}
// 写文件
const char *msg = "hello linux!\n";
int cnt = 5;
size_t len = strlen(msg);
while (cnt--)
{
write(fd, msg, len); // 向文件描述符中写入 msg 缓冲区中的数据
}
close(fd);
return 0;
}
read 文件读取系统调用
read 是 Linux/Unix 系统中最基础的文件读取系统调用,用于从文件描述符读取数据到用户态缓冲区。C 标准库的 fread 本质是对 read 的封装。
- 函数原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
- 参数说明: | 参数 | 含义 | | --- | --- | | fd | 文件描述符(非负整数):0 为 stdin,1 为 stdout,2 为 stderr,大于 2 为打开文件/管道/socket 等 | | buf | 指向用户态缓冲区,必须是可写内存,且需提前分配足够空间 | | count | 期望读取的最大字节数 |
- 返回值:成功返回实际读取的字节数;失败返回 -1。 注意:read 是底层系统调用,无用户态缓冲,调用后直接从内核态读取数据到用户缓冲区。
系统调用读文件示例
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
// 打开文件
int fd = open("log.txt", O_RDONLY);
if (fd < 0)
{
perror("打开文件失败:");
exit(1);
}
// 读取文件
char buf[1024]; // 定义缓冲区
while (1)
{
ssize_t s = read(fd, buf, sizeof(buf)); // 从文件描述符中读取数据到 buf 缓冲区
if (s > 0)
{
printf("%s", buf);
}
else
break;
}
// 关闭文件
close(fd);
return 0;
}
open 函数的返回值
前面提到 open 函数的返回值是一个文件描述符(File Descriptor, fd),本质是一个非负整数。
- open 成功返回时:返回一个大于等于 3 的非负整数。Linux 下,进程默认情况下会有 3 个文件描述符,分别是标准输入 0,标准输出 1,标准错误 2。这个整数是内核分配给该文件的唯一标识,后续对文件的读写、关闭等操作都需要通过这个文件描述符来完成。
- 失败时的返回值:固定返回 -1,同时内核会设置全局变量 errno 来标识具体的错误原因。
文件描述符(fd)的原理
我们要读取一个文件的内容,首先得打开它。操作系统打开文件时,会在内核创建 struct_file 数据结构,包含文件的权限、读写位置、读写方法、缓冲区等属性,还会用链表将所有 struct file 连接起来。每个进程都有一个指针 *files,指向一张表 files_struct,该表最重要的部分包含一个指针数组,每个元素都是指向打开文件的指针。所以,本质上,文件描述符就是该数组的下标。只要拿着文件描述符,就可以找到对应的文件。
文件读写操作的原理
- open 调用:用户层进程执行 open 调用时,操作系统创建 struct_file,在当前进程的文件描述符表中找当前未使用的最小下标,填入 struct_file 地址,关联进程和文件。
- 文件描述符的分配原则:最小的,没有被使用,作为新的 fd 给用户。
- read 调用:read 调用时,操作系统根据文件描述符索引数组找到对应文件,将磁盘文件内容预加载到缓冲区,再将缓冲区数据拷贝到用户层的 buffer。
- write 调用:write 时,先根据文件描述符找到对应文件,将用户空间数据拷贝到文件缓冲区,操作系统定期将缓冲区数据刷到磁盘。
- 文件修改操作:对文件内容做任何修改,都要先将文件加载到内核缓冲区,在内存修改后再写回磁盘,本质是磁盘到内存的拷贝。
二、重定向的原理
先来看一个现象:
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
if (fd < 0)
{
perror("打开文件失败");
exit(1);
}
printf("hello linux\nfd:%d\n", fd);
fflush(stdout); // 强制刷新缓冲区
close(fd);
return 0;
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 log.txt 当中,其中 fd=1。这种现象叫做输出重定向。
常见的重定向有:
| 符号 | 作用 | 底层原理 |
|---|---|---|
| > | 覆盖式输出重定向 | 打开文件时使用 O_TRUNC(截断文件,清空原有内容) |
| >> | 追加式输出重定向 | 打开文件时使用 O_APPEND(追加模式,写入到文件末尾) |
| < | 输入重定向 | 打开文件时使用 O_RDONLY |
、>>、<是命令解释器(Shell)定义的语法规则,作用是告诉 Shell:'在执行后续程序前,先修改它的 IO 指向';它们的底层是通过系统调用 open 和 dup2 实现的。
重定向的核心操作:修改文件描述符的指向。
- 关闭默认的 1 号文件描述符(断开与终端的连接);
- 打开目标文件(比如 log.txt),操作系统会为这个文件分配一个新的文件描述符(通常刚好是刚关闭的 1);
- 后续程序向 1 号描述符写入的所有数据,都会被操作系统转发到新打开的文件中。
重定向的本质:更改文件描述符表的指向。
思考: 上图中 1 号文件描述符指向了 log.txt 文件,那标准输出文件的 struct file 结构体是否会被释放?在操作系统内核中,一个文件可以被多个进程打开,struct file 中存在一个 ref_cnt 变量,记录指向该文件的进程数量,当该文件被重定向时,ref_cnt 变量会 -1,当有新的新的文件描述符指向 struct file 时,ref_cnt 会 +1,当 struct file 中的 ref_cnt 减到 0 时,struct file 结构体才会被释放。
标准错误重定向:
#include <iostream>
#include <stdio.h>
int main()
{
std::cout << "hello cout" << std::endl;
printf("hello printf\n");
std::cerr << "hello cerr" << std::endl;
fprintf(stderr, "hello stderr\n");
return 0;
}
g++ stream.cc 编译代码,然后将运行后的内容重定向输出到 log.txt 文件,即 ./a.out > log.txt,结果应该是运行的结果都输出到 log.txt 文件。但是标准错误没有输出到 log.txt 文件,这是因为 ./a.out > log.txt 等价于 ./a.out 1 > log.txt,也就是 1 号文件描述符重定向到 log.txt,但是标准错误还是对应的 2 号文件描述符,标准错误对应的物理硬件还是显示器,所以标准错误最终打印到屏幕上。
既然标准输出和标准错误对应的物理硬件都是显示器,那为什么不让标准输出和标准输入合二为一呢?这是因为我们可以通过重定向能力,把常规消息和错误消息进行分离。
如果 stderr 和 stdout 打印到同一个文件呢?
./a.out 1>log.txt 2>&1 是一个将标准输出和标准错误都重定向到同一个文件的经典写法,1>log.txt:将标准输出(文件描述符 1)重定向到 log.txt 文件;2>&1:将标准错误(文件描述符 2)重定向到与标准输出(1)相同的目标(也就是 log.txt)。
dup2 系统调用
它是实现重定向的核心工具,dup2(fd1, fd2) 的作用是让文件描述符 fd2 完全继承 fd1 的指向,最终让两个文件描述符指向同一个文件/设备。
- 函数原型:
#include <unistd.h>
int dup2(int oldfd, int newfd);
- 参数:
- oldfd:已有、指向目标文件/设备的文件描述符;
- newfd:要被'覆盖'的文件描述符(比如 1 号 stdout、0 号 stdin)。
- 返回值:成功返回 newfd,失败返回 -1 并设置 errno。
示例
实现从键盘获取内容,标准输出(stdout)重定向到 log.txt 文件
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0)
{
perror("open fail");
exit(1);
}
dup2(fd, 1); // 将 1 号 FD(stdout)重定向到 log.txt
close(fd); // 关闭多余的 fd,fd 已被 1 号复用,无需保留。
while (1)
{
char buffer[1024];
ssize_t s = read(0, buffer, sizeof(buffer) - 1); // 留 1 个字节给'\0'
if (s < 0)
{
perror("read fail");
exit(2);
}
if (s == 0) // 处理输入结束(Ctrl+D)
{
();
fflush();
;
}
buffer[s] = ;
(, buffer);
fflush();
}
;
}
在自定义 minishell 中添加重定向功能
在原有 minishell 的基础上添加重定向功能。
思路:
- 重定向处理思路: 获取到用户输入的命令行信息后,在命令行解析前,先进行重定向分析。即将命令行 "ls -a -l > myfile.txt" 拆分成:"ls -a -l" 和 "myfile.txt",再判定重定向方式。
- 重定向分析接口编写:RedirCheck(实现命令行的拆分)
- 定义重定向方式的类型,包括无重定向(NONE_REDIR,值为 0)、输入重定向(INPUT_REDIR,值为 1)、输出重定向(OUTPUT_REDIR,值为 2)、输追加重定向(APPEND_REDIR,值为 3);默认重定向类型为无重定向,默认文件名为空。每次操作前清空重定向类型和文件名。
- 使用 while 循环从命令行字符串末尾向前查找重定向符号(>、>>、<),若找到重定向符号,将其置为 '\0',以此分隔左右两部分,左侧为要执行的命令,右侧为文件名。
- 编写 Trimspace 函数,去除文件名前的空格,函数第一个参数为要处理的字符串,第二个参数为引用的下标,用于定位文件名起始位置。包含头文件 ctype.h,使用 isspace 函数判断字符是否为空格,若为空格则将下标后移,直到不是空格为止。
- 子进程做重定向处理: 使用打开文件方式 +dup2 修改文件描述符所对应的指针指向位置,实现重定向的底层处理。


