一、系统文件I/O
在 Linux 编程体系中,C 标准库 I/O 是上层封装,而 系统文件 I/O 是底层基石 ------ 它是操作系统内核提供的原生接口 ,是所有上层文件操作(包括 C 库
fopen/fread)的最终执行入口。
1.1 一种传递标记位的方法
Linux 系统调用中,大量接口(如 open)需要通过一个整型变量传递多个配置标记 ,这种设计的核心是利用二进制位运算(按位或 | 组合标记、按位与 & 校验标记
核心原理
- 每个标记对应一个独立的二进制位(如
0x01、0x02、0x04),互不冲突;- 用 按位或
|组合多个标记,实现 "多标记共存";- 用 按位与
&校验某个标记是否被启用,实现**"单标记检测"**。- 可以采用位图传递标记位
| 标记位 | 含义 | 适用场景 |
|---|---|---|
O_RDONLY |
只读模式 | 纯读取文件,不可写 |
O_WRONLY |
只写模式 | 纯写入文件,不可读 |
O_RDWR |
读写模式 | 可读可写 |
O_CREAT |
不存在则创建 | 需配合权限参数,如0644 |
O_TRUNC |
截断文件(清空原有内容) | 打开文件时清空内容 |
O_APPEND |
追加模式(写时移到文件末尾) | 不覆盖原有内容,仅追加 |
#include <stdio.h>
#define ONE_FLAG (1 << 0)
#define TWO_FLAG (2 << 0)
#define THREE_FLAG (3 << 0)
#define FOUR_FLAG (4 << 0)
void Print(int flags)
{
if (flags & ONE_FLAG)
{
printf("One!\n");
}
if (flags & TWO_FLAG)
{
printf("Two!\n");
}
if (flags & THREE_FLAG)
{
printf("Three!\n");
}
if (flags & FOUR_FLAG)
{
printf("four!\n");
}
}
int main()
{
Print(ONE_FLAG);
printf("\n");
Print(ONE_FLAG | TWO_FLAG);
printf("\n");
Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);
printf("\n");
Print(ONE_FLAG | TWO_FLAG | THREE_FLAG | FOUR_FLAG);
printf("\n");
Print(TWO_FLAG);
printf("\n");
return 0;
}


操作文件,除了上小节的C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问, 先来直接以系统代码的形式,实现和上面一模一样的代码:
1.2 hello.c写文件
把相关头文件加上~

1.2.1 umask掩码

编译链接完,生成文件,我们发现权限位不太对

所以我们再加上权限码 0666

受系统的权限影响,没有达到我们的预想

umask(文件创建掩码)是 Linux 系统的一个进程级属性 ,用来限制新创建文件 / 目录的默认权限。
- 系统有一个默认的
umask值(通常是0002或0022);- 当你调用
open(带O_CREAT)或mkdir时,最终权限 = 你指定的 mode & ~umask。举个例子:
- 你指定
mode = 0666(rw-rw-rw-)- 系统默认
umask = 0022(----w--w-)- 最终文件权限 =
0666 & ~0022 = 0644(rw-r--r--)umask(0)的目的是:让你在open里指定的mode权限 100% 生效,不被系统默认的umask扣减。



1.2.2 write


注意:strlen(msg) 不需要 +1


1.2.3 O_TRUNC清空
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char* argv[])
{
umask(0);
int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd:%d\n", fd);
const char* msg = "qqq!\n";
int cnt = 1;
while (cnt)
{
write(fd, msg, strlen(msg));
cnt--;
}
close(fd);
return 0;
}


1.2.4 O_APPEND追加

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char* argv[])
{
// 清空文件创建掩码,使 open 指定的 0666 权限完全生效
umask(0);
// 打开 log.txt,不存在则创建,存在则追加内容(O_APPEND)
// 权限设置为 0666 (rw-rw-rw-)
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
// 检查打开是否失败
if (fd < 0)
{
perror("open");
return 1;
}
// 打印获取到的文件描述符值,通常为 3
printf("fd:%d\n", fd);
// 定义待写入的字符串
const char* msg = "abcd";
int cnt = 1; // 循环次数,这里只为了演示一次写入
// 循环向文件写入数据
while (cnt)
{
// write 系统调用:向 fd 写入 msg 内容,长度为字符串长度
write(fd, msg, strlen(msg));
cnt--; // 循环递减
}
// 关键步骤:关闭文件描述符,释放内核资源
close(fd);
return 0;
}

O_APPEND的作用
- 这是你图中标注的重点。它表示追加模式。
- 每次调用
write向文件写数据时,数据都会被写到文件的末尾 ,而不会覆盖文件原本的内容。- 如果不加
O_APPEND,再次打开文件写,默认会从文件开头开始写,导致内容被覆盖。
umask(0)的必要性
- 虽然你指定了权限
0666,但系统默认的umask通常是0022。umask(0)是为了屏蔽系统默认的权限限制,确保最终生成的log.txt确实是0666权限,而不是被系统截成0644。
write函数
- 第一个参数是
fd(文件句柄)。- 第三个参数
strlen(msg)是不含\0结束符的字节数,这是系统级 IO 的标准写法。

1.2.5 写入数据类型


- 不管你写的是:
- 字符串
"123456"- 整数
123456- 图片、音频、可执行文件
- 在 OS 眼里,全都是一串二进制字节,它只负责把这些字节从进程缓冲区搬到文件 / 设备缓冲区,完全不关心这些字节代表什么 "类型"。
- 没有 "文本模式" 和 "二进制模式" 的区别,这是上层语言(比如 C 标准库)给你封装的概念。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char* argv[])
{
umask(0);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd:%d\n", fd);
int a = 12345;
//const char* msg = "abcd";
int cnt = 1;
while (cnt)
{
// 当成字符来写 ---
char buffer[16];
snprintf(buffer, sizeof(buffer), "%d", a);
write(fd, buffer, strlen(buffer));
cnt--;
}
close(fd);
return 0;
}
- 直接
write(&a, sizeof(a))会把整数的二进制内存布局写入文件,人类无法阅读- 通过
snprintf先将整数转成字符串,再写入文件,就能得到人类可读的文本内容- 这正是
printf等格式化输出函数的底层实现思路:先把数值类型转成字符串,再调用系统write输出
1.3 hello.c读文件
读文件的核心是用
read系统调用,从文件中读取数据到缓冲区,同样需要处理文件描述符和读写位置。
read读取成功返回实际读取的字节数 ,到达文件末尾返回0;- 缓冲区
buf需预留\0位置,避免打印时出现乱码;- 读取完成后必须
close,否则会造成文件描述符泄漏。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
umask(0);
//int fd = open("log.txt", O_CREAT|O_WRONLY|O_TRUNC, 0666);
int fd = open("log.txt", O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
printf("fd:%d\n", fd);
while(1)
{
char buffer[64];
int n = read(fd, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = 0;
printf("%s", buffer);
}
else if(n == 0)
{
break;
}
}
close(fd);
return 0;
}

1.4 接口介绍

当 open 带 O_CREAT 却不传 mode 时,内核会用栈上的随机垃圾值当文件权限,导致权限异常(比如不可读),后续读文件时要么权限不足打开失败,要么读到未初始化的缓冲区垃圾数据,最终表现为 "乱码"。
| 接口函数 | 功能 | 核心参数 | 返回值 |
|---|---|---|---|
open(const char *path, int flags, ...) |
打开 / 创建文件 | path:文件路径;flags:标记位组合;...:可选权限(仅创建时) | 成功返回 fd,失败返回 - 1 |
write(int fd, const void *buf, size_t count) |
向文件写数据 | fd:文件描述符;buf:待写数据缓冲区;count:待写字节数 | 成功返回实际写字节数,失败返回 - 1 |
read(int fd, void *buf, size_t count) |
从文件读数据 | fd:文件描述符;buf:存储数据缓冲区;count:最大读字节数 | 成功返回实际读字节数,到达末尾返回 0,失败返回 - 1 |
lseek(int fd, off_t offset, int whence) |
移动文件读写位置 | fd:文件描述符;offset:偏移量;whence:基准位置(SEEK_SET/SET_CUR/SET_END) | 成功返回新的读写位置,失败返回 - 1 |
close(int fd) |
关闭文件描述符 | fd:待关闭的文件描述符 | 成功返回 0,失败返回 - 1 |
1.5 open函数返回值
open 函数的返回值是文件描述符(fd),其核心规则如下:
- 成功返回 :非负整数,最小从 0 开始(后续会详细讲 fd 分配规则);
- 失败返回 :固定为
-1,并设置全局错误码errno,可通过perror打印错误原因; - fd 的本质:进程打开文件的唯一标识,后续所有读写操作都依赖 fd 定位文件。
系统调用 & 库函数


所以,可以认为 ,f#系列的函数,都是对系统调用的封装,方便二次开发
二、 文件描述符fd
2.1 0&1&2



Linux 系统中,每个进程启动时会默认打开 3 个文件描述符 ,对应 C 标准库的
stdin/stdout/stderr,这是操作系统为进程预设的基础 I/O 通道。
| fd 值 | 名称 | 宏定义 | 对应设备 | 缓冲区类型 | 用途 |
|---|---|---|---|---|---|
| 0 | 标准输入 | STDIN_FILENO |
键盘 | 行缓冲 | 接收用户输入(如 scanf 底层) |
| 1 | 标准输出 | STDOUT_FILENO |
显示器 | 行缓冲 | 输出正常结果(如 printf 底层) |
| 2 | 标准错误 | STDERR_FILENO |
显示器 | 无缓冲 | 输出错误信息(如 perror 底层) |

2.1.1 FILE里封装文件描述符





2.2 文件描述符的分配规则
Linux 为每个进程维护一个文件描述符表(
fd_array),这是一个指针数组,数组下标就是 fd,数组元素指向内核的file结构体(代表打开的文件实例)。
2.2.1 分配核心规则
从当前未被使用的 最小下标分配新的 fd。
- 进程启动时,0/1/2 已被占用,因此首次打开普通文件时,fd 默认为 3;
- 如果手动关闭 0/1/2 中的某个 fd,后续打开文件会优先复用该下标(这是重定向的核心原理)。




2.2.2 理解



- 内核不会直接读写磁盘,而是先把文件内容加载到内核文件缓冲区 :
- 读:先从缓冲区读,缓冲区没数据才从磁盘加载。
- 写:先写到缓冲区,由内核定期刷回磁盘(提升性能)。
read本质是:内核缓冲区 → 用户 buffer 的数据拷贝。
完整链路总结
- 用户调用
open→ 内核创建struct file→ 分配最小可用 fd → 把fd_array[fd]指向这个struct file。- 用户调用
read(fd, buffer, ...)→ 内核通过 fd 找到struct file→ 从文件缓冲区拷贝数据到用户 buffer。- 用户调用 close(fd) → 减少 struct file 引用计数 → 计数为 0 时释放内核资源。
- 多个进程可以打开同一个文件 →各自有独立的 struct file(独立读写位置),共享磁盘文件数据。
Q:一个文件可以被打开多次吗?
✅ 可以!每次
open都会创建一个新的struct file,有独立的读写位置和状态,互不干扰。Q:fd 是什么?为什么是整数?
fd 就是
fd_array[]的下标,用整数是为了高效索引,让内核快速找到对应的struct file。Q:为什么不直接读写磁盘?
磁盘太慢!内核缓冲区是为了性能:批量读写、减少磁盘 IO 次数,同时让上层操作更流畅。
fd 是数组下标,struct file 是内核里的「打开文件实例」,文件 IO 本质是内核缓冲区和用户 buffer 之间的拷贝。
2.3 重定向
重定向是 Linux 系统 I/O 的核心高级功能 ,本质是修改文件描述符表中某个下标对应的 file 指针指向,让原本输出到显示器 / 键盘的内容,转而输出到文件 / 其他设备。
| 类型 | 命令示例 | 本质 |
|---|---|---|
| 输出重定向 | ./a.out > output.txt |
将 fd=1(stdout)的指向改为 output.txt |
| 追加重定向 | ./a.out >> output.txt |
将 fd=1 的指向改为 output.txt,且以追加模式打开 |
| 输入重定向 | ./a.out < input.txt |
将 fd=0(stdin)的指向改为 input.txt |
| 错误重定向 | ./a.out 2> error.txt |
将 fd=2(stderr)的指向改为 error.txt |


核心原理(以输出重定向为例)
- 关闭 fd=1(断开与显示器的关联);
- 打开目标文件(如 log1.txt),操作系统会分配最小可用 fd(此时为 1);
- 后续所有向 fd=1 写入的数据,都会写入 log1.txt,而非显示器。
我们在这里看一个现象:

2.4 使用dup2系统调用
手动关闭 fd 再打开文件的方式虽然能实现重定向,但不够灵活。Linux 提供
dup2系统调用,可直接复制文件描述符的指向,实现更高效、更可控的重定向。

- 功能 :将
oldfd的文件指向复制 到newfd;- 参数 :
oldfd:已打开的文件描述符(源指向);newfd:目标文件描述符(被覆盖的指向);- 返回值 :成功返回
newfd,失败返回-1;- 核心逻辑 :如果
newfd已被占用,dup2会先自动关闭newfd,再完成复制。


输出重定向
关闭stdout , 选项选择 只写 + 打开文件 + 清空文件


追加重定向
关闭stdout , 选项选择 只写 + 打开文件 + 追加


输入重定向
关闭stdin,选项选择 只读


