
✨道路是曲折的,前途是光明的!
📝 专注C/C++、Linux编程与人工智能领域,分享学习笔记!
🌟 感谢各位小伙伴的长期陪伴与支持,欢迎文末添加好友一起交流!

- 一、IO操作的层级调用关系
- 二、open
-
- 2.1 第一个参数
- 2.2 第二个参数
-
- 2.2.1 核心原理:把整数当成“32 个开关的面板”
- 2.2.2 第一步:定义开关(宏定义与左移 `<<`)
- 2.2.3 第二步:按下开关(传参与按位或 `|`)
- 2.2.4 第三步:检查开关(解析与按位与 `&`)
- 2.2.5 常见的选项如下
- 2.3 第三个参数
-
- 2.3.1 基础用法示例
- 2.3.2 umask(文件默认掩码)的影响
- 2.3.3 取消umask影响的方法
- 注意事项
- 2.4 实例测试
-
- 总结
- 三、close
- 四、write
- 五、read
一、IO操作的层级调用关系
简单来说:C/C++程序(标准库) → 调用 → 系统调用 → 调用 → 操作系统 → 调用 → 硬件驱动 → 操作 → 硬件。
应用程序 (App)
↓
C/C++ 标准库 (Libc)
↓
系统调用接口 (Syscall)
↓
操作系统内核 (Kernel)
↓
硬件驱动程序 (Driver)
↓
硬件 (Hardware)
- 操作系统为保证安全,仅通过系统调用对外开放硬件访问接口,任何程序(包括C标准库)都需通过系统调用才能自上而下访问操作系统→硬件驱动→硬件;
- printf/fprintf/fscanf/fwrite/fread/fgets/gets等文件操作库函数,本质是对文件类系统调用的封装,其底层均依赖系统调用实现对硬件的读写。
二、open
系统接口中使用open函数打开文件,open函数的函数原型如下:
int open(const char *pathname, int flags, mode_t mode);

2.1 第一个参数
open函数的第一个参数是pathname,表示要打开或创建的目标文件。
- 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
- 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)
2.2 第二个参数
open函数的第二个参数是flags,表明打开文件的方式。
我们要告诉操作系统:“我要读写模式打开”、“如果文件不存在就创建”、“每次写都追加到末尾”。
- 如果按照常规思维,这需要 3 个布尔类型的参数(isReadWrite, isCreate, isAppend)。如果有 10 种操作模式,难道要写 10 个参数吗?
显然不是。Linux 大神们只用了一个 int 类型(32位)就搞定了。这背后的核心魔法,就是比特位传递标志位。
2.2.1 核心原理:把整数当成“32 个开关的面板”
我们可以把一个 int 类型的变量,想象成一个拥有 32 个独立开关 的控制面板。
- 一个开关(比特位):只有两种状态,0(关)或 1(开)。
- 一个整数:就是这 32 个开关的集合。
通过操作这些开关,我们就能用这一个整数,同时传递 32 个“是/否”的指令。
2.2.2 第一步:定义开关(宏定义与左移 <<)
操作系统需要先定义好,哪个开关代表什么意思。这就是 <fcntl.h> 头文件中那些宏定义的由来。
为了保证每个开关互不干扰,我们使用 1 << n(1 左移 n 位)的方式来定义:
- O_RDWR(读写):定义在第 1 位 → 1 << 1 → 二进制 000…0010
- O_CREAT(创建):定义在第 6 位 → 1 << 6 → 二进制 000…1000000
- O_APPEND(追加):定义在第 10 位 → 1 << 10 → 二进制 000…10000000000
为什么要这么做?
因为左移操作保证了每一个宏对应的二进制数中,只有某一位是 1,其他位全是 0。这就像给每个开关贴上了唯一的标签,按下 O_CREAT 绝对不会误触 O_RDWR。
2.2.3 第二步:按下开关(传参与按位或 |)
当我们在代码中调用 open 时,我们需要告诉系统:“我要同时按下 读写 和 创建 这两个开关”。
这时候我们使用 按位或 | 运算符。它的规则是:只要有一个是 1,结果就是 1。
场景模拟: 我们要传递 O_RDWR | O_CREAT:
O_RDWR: 000…0000 0010
| O_CREAT: 000…0100 0000
—————————-
结果: 000…0100 0010
看!结果整数中,第 1 位和第 6 位都变成了 1。我们成功地把两个指令“打包”进了一个整数里,传给了内核。
2.2.4 第三步:检查开关(解析与按位与 &)
open 函数的内核源码收到这个整数后,怎么知道你按下了哪些开关呢?
它使用 按位与 & 运算符。它的规则是:两个都是 1,结果才是 1。
内核逻辑模拟:
- 如果结果不为 0,说明第 6 位是 1 → 执行创建逻辑。
- 如果结果为 0,说明第 6 位是 0 → 跳过创建逻辑。
- 同理,判断第 10 位是否为 1。
通过这种方式,内核就能精准地解析出我们想要的所有操作模式。
这种设计模式不仅存在于 open 函数,在 socket、fcntl 等系统调用中无处不在。掌握了“比特位传递标志位”,你就掌握了阅读 Linux 源码的一把金钥匙。
2.2.5 常见的选项如下
| O_RDONLY | 以只读的方式打开文件 | 0 | 00000000 |
| O_WRONLY | 以只写的方式打开文件 | 1(1<<0) | 00000001 |
| O_APPEND | 以追加的方式打开文件 | 1024(1<<10) | 10000000000 |
| O_RDWR | 以读写的方式打开文件 | 2(1<<1) | 00000010 |
| O_CREAT | 当目标文件不存在时,创建文件 | 64(1<<6) | 01000000 |
2.3 第三个参数
mode 参数仅在使用 O_CREAT 标志创建文件时生效,用于指定文件的默认权限;若无需创建文件,该参数可省略。
2.3.1 基础用法示例
当将 mode 设置为 0666 时,期望创建的文件权限为:
- 所有者(user):读、写(6 → rw-)
- 所属组(group):读、写(6 → rw-)
- 其他用户(other):读、写(6 → rw-)
- 权限表示:-rw-rw-rw-
2.3.2 umask(文件默认掩码)的影响
文件实际创建的权限并非直接等于 mode,而是受系统 umask (默认掩码)约束,计算公式为:
实际权限 = mode & (~umask)
默认场景示例:
- 系统默认 umask 为 0002(二进制:000 000 010)
- 设置 mode = 0666(二进制:110 110 110)
- 计算过程:0666 & (~0002) = 0664
- 最终权限:-rw-rw-r–(所有者/组可读可写,其他用户仅可读)
2.3.3 取消umask影响的方法
若希望文件权限完全按 mode 设置,不受 umask 干扰,可在调用 open 前通过 umask 函数将掩码置0:
umask(0); // 将文件默认掩码设置为0,后续创建文件权限完全遵循mode
int fd = open("test.txt", O_CREAT | O_RDWR, 0666); // 实际权限为0666
注意事项
- mode 的值需以 0 开头(八进制),如 0666 而非 666(十进制);
- 即使设置 mode = 0777,若 umask = 0022,实际权限仍为 0755;
- 无需创建文件时(未使用 O_CREAT),open 无需传入第三个参数。
open函数的返回值是新打开文件的文件描述符。
2.4 实例测试
我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符。
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
printf("fd1:%d\\n", fd1);
printf("fd2:%d\\n", fd2);
printf("fd3:%d\\n", fd3);
printf("fd4:%d\\n", fd4);
printf("fd5:%d\\n", fd5);
return 0;
}
运行程序后可以看到,打开文件的文件描述符是从3开始连续且递增的

我们再尝试打开一个根本不存在的文件,也就是open函数打开文件失败。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("test.txt", O_RDONLY);
printf("%d\\n", fd);
return 0;
}
运行程序后可以看到,打开文件失败时获取到的文件描述符是-1。

总结
三、close
原函数如下:
int close(int fd);

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
四、write
原函数如下:
ssize_t write(int fd, const void *buf, size_t count);

系统接口中使用write函数向文件写入信息。
我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。
- 如果数据写入成功,实际写入数据的字节个数被返回。
- 如果数据写入失败,-1被返回。
实例测试:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("ceshi.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
const char* message = "hello linux!\\n";
for (int i = 0; i < 5; i++){
write(fd, message, strlen(message));
}
close(fd);
return 0;
}

五、read
系统接口中使用read函数从文件读取信息,read函数的函数原型如下:
ssize_t read(int fd, void *buf, size_t count);

我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。
- 如果数据读取成功,实际读取数据的字节个数被返回。
- 如果数据读取失败,-1被返回。
实例测试:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("ceshi.txt", O_RDONLY);
if (fd < 0){
perror("open");
return 1;
}
char ch;
while (1){
ssize_t s = read(fd, &ch, 1);
if (s <= 0){
break;
}
write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据
}
close(fd);
return 0;
}

✍️ 坚持用 清晰易懂的图解 + 可落地的代码,让每个知识点都 简单直观!
💡 座右铭:“道路是曲折的,前途是光明的!”


