【Linux】基础IO(二):系统文件IO


✨道路是曲折的,前途是光明的!

📝 专注C/C++、Linux编程与人工智能领域,分享学习笔记!

🌟 感谢各位小伙伴的长期陪伴与支持,欢迎文末添加好友一起交流!

  • 一、IO操作的层级调用关系
  • 二、open
    • [2.1 第一个参数](#2.1 第一个参数)
    • [2.2 第二个参数](#2.2 第二个参数)
      • [2.2.1 核心原理:把整数当成"32 个开关的面板"](#2.2.1 核心原理:把整数当成“32 个开关的面板”)
      • [2.2.2 第一步:定义开关(宏定义与左移 `<<`)](#2.2.2 第一步:定义开关(宏定义与左移 <<))
      • [2.2.3 第二步:按下开关(传参与按位或 `|`)](#2.2.3 第二步:按下开关(传参与按位或 |))
      • [2.2.4 第三步:检查开关(解析与按位与 `&`)](#2.2.4 第三步:检查开关(解析与按位与 &))
      • [2.2.5 常见的选项如下](#2.2.5 常见的选项如下)
    • [2.3 第三个参数](#2.3 第三个参数)
      • [2.3.1 基础用法示例](#2.3.1 基础用法示例)
      • [2.3.2 umask(文件默认掩码)的影响](#2.3.2 umask(文件默认掩码)的影响)
      • [2.3.3 取消umask影响的方法](#2.3.3 取消umask影响的方法)
      • 注意事项
    • [2.4 实例测试](#2.4 实例测试)
  • 三、close
  • 四、write
  • 五、read

一、IO操作的层级调用关系

简单来说:C/C++程序(标准库) → 调用 → 系统调用 → 调用 → 操作系统 → 调用 → 硬件驱动 → 操作 → 硬件

latex 复制代码
      应用程序 (App)
           ↓
    C/C++ 标准库 (Libc)
           ↓
    系统调用接口 (Syscall)
           ↓
    操作系统内核 (Kernel)
           ↓
    硬件驱动程序 (Driver)
           ↓
        硬件 (Hardware)
  • 操作系统为保证安全,仅通过系统调用对外开放硬件访问接口,任何程序(包括C标准库)都需通过系统调用才能自上而下访问操作系统→硬件驱动→硬件;
  • printf/fprintf/fscanf/fwrite/fread/fgets/gets等文件操作库函数,本质是对文件类系统调用的封装,其底层均依赖系统调用实现对硬件的读写。

二、open

系统接口中使用open函数打开文件,open函数的函数原型如下:

c 复制代码
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

latex 复制代码
  O_RDWR:    000...0000 0010
| O_CREAT:   000...0100 0000
----------------------------
  结果:      000...0100 0010

看!结果整数中,第 1 位和第 6 位都变成了 1。我们成功地把两个指令"打包"进了一个整数里,传给了内核。


2.2.4 第三步:检查开关(解析与按位与 &

open 函数的内核源码收到这个整数后,怎么知道你按下了哪些开关呢?

它使用 按位与 & 运算符。它的规则是:两个都是 1,结果才是 1

内核逻辑模拟:

  1. 检查是否要创建文件?
    传入的整数 & O_CREAT
    • 如果结果不为 0,说明第 6 位是 1 → 执行创建逻辑
    • 如果结果为 0,说明第 6 位是 0 → 跳过创建逻辑
  2. 检查是否要追加写入?
    传入的整数 & O_APPEND
    • 同理,判断第 10 位是否为 1。

通过这种方式,内核就能精准地解析出我们想要的所有操作模式。


这种设计模式不仅存在于 open 函数,在 socketfcntl 等系统调用中无处不在。掌握了"比特位传递标志位",你就掌握了阅读 Linux 源码的一把金钥匙。


2.2.5 常见的选项如下

参数选项 含义 对应数值(1<<n) 二进制(简化)
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 (默认掩码)约束,计算公式为:

plain 复制代码
实际权限 = mode & (~umask)

默认场景示例

  • 系统默认 umask0002(二进制:000 000 010
  • 设置 mode = 0666(二进制:110 110 110
  • 计算过程:0666 & (~0002) = 0664
  • 最终权限:-rw-rw-r--(所有者/组可读可写,其他用户仅可读)

2.3.3 取消umask影响的方法

若希望文件权限完全按 mode 设置,不受 umask 干扰,可在调用 open 前通过 umask 函数将掩码置0:

c 复制代码
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 实例测试

我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符。

c 复制代码
#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函数打开文件失败。

c 复制代码
#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。

总结

  1. 文件描述符(File Descriptor,简称 fd)是 Linux 系统操作文件的核心标识,它的本质并非随机数字,而是进程内一个指针数组的下标。Linux 进程会维护一个专门的指针数组,数组中每个元素(指针)都指向一个"已打开文件的信息结构体",这个结构体包含了文件路径、读写位置、权限等所有文件相关信息,通过文件描述符这个下标,就能精准找到对应的文件信息。
  2. 当使用 open 函数成功打开文件时,系统会在这个指针数组中新增一个指向该文件信息的指针,随后将这个指针在数组中的下标作为文件描述符返回;若文件打开失败,则直接返回 -1。正因为数组下标是连续分配的,所以成功打开多个文件时,获得的文件描述符会呈现连续且递增的特点。
  3. Linux 进程在默认情况下会预先打开 3 个缺省的文件描述符,分别是代表标准输入的 0、代表标准输出的 1、代表标准错误的 2,这三个下标会被系统占用,这也是为什么我们手动调用 open 函数成功打开文件时,得到的文件描述符总是从 3 开始分配的原因。

三、close

原函数如下:

c 复制代码
int close(int fd);

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。


四、write

原函数如下:

c 复制代码
ssize_t write(int fd, const void *buf, size_t count);

系统接口中使用write函数向文件写入信息。

我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。

  • 如果数据写入成功,实际写入数据的字节个数被返回。
  • 如果数据写入失败,-1被返回。

实例测试:

c 复制代码
#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函数的函数原型如下:

c 复制代码
ssize_t read(int fd, void *buf, size_t count);

我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。

  • 如果数据读取成功,实际读取数据的字节个数被返回。
  • 如果数据读取失败,-1被返回。

实例测试:

c 复制代码
#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;
}

✍️ 坚持用 清晰易懂的图解 + 可落地的代码,让每个知识点都 简单直观!

💡 座右铭 :"道路是曲折的,前途是光明的!"

相关推荐
大树8810 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠10 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质11 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush411 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52011 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz11 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工12 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智12 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩12 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_12 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化