Linux 最底层的文件 IO 是 open、read、write、close 系统调用,这是操作系统原生的接口。

但我们初学 C 语言写代码时,从头到尾见到的全是 fopen、fread、fwrite、fclose 带 f 的函数,从来没见过原生的 open、read、write。

这并不是知识点冲突,而是两套完全不同、层级不同的 IO 接口,本文彻底讲透二者的关系、区别、使用场景,补齐绝大多数初学者的知识盲区。

一、先分清:两套文件 IO 接口(核心核心!)

Linux 环境下,文件读写分为上下两层,各司其职,绝对不能混淆:

1. 底层原生:无 f 系列 —— 系统调用

函数:open()、read()、write()、close()、lseek()

  • 归属:Linux 操作系统内核(系统调用)

  • 头文件:<unistd.h>、<fcntl.h>

  • 返回值:int 文件描述符 fd(纯数字令牌)

  • 特点:最底层、无封装、无缓冲、跨平台性差(仅 Unix/Linux/macOS)

2. 上层封装:带 f 系列 —— C 标准库函数

函数:fopen()、fread()、fwrite()、fclose()、fseek()

  • 归属:C 标准库 stdio.h(Glibc)

  • 头文件:<stdio.h>

  • 返回值:FILE* 结构体指针(库层封装对象)

  • 特点:封装完善、自带缓冲区、跨平台、上手简单

二、最关键的真相:f 系列函数,底层就是封装了系统调用

很多人以为这是两套独立的功能,其实不然!

所有 f 开头的标准库文件函数,内部最终都会调用 open / read / write / close。

层级链路非常清晰:

用户代码 → C标准库(f系列函数) → Linux系统调用(无f原生函数) → 操作系统内核

简单理解:f 系列函数是给原生系统调用穿了一层“外衣”,让底层复杂的内核接口,变得简单、通用、高效。

三、为什么初学只教 fopen?几乎不用原生 open?

这是最大的疑问,这里给出三个最核心的原因,也是工业界的通用准则。

1. f 系列是 C 国际标准,真正跨平台

fopen/fread 是 C89 标准规定的通用函数,Windows、Linux、MacOS 全平台通用,一份代码可以随处编译运行。

open/read/write 是 Linux 专属系统调用:

Windows 没有这套接口,写了直接编译报错,完全不兼容。

初学 C 语言主打通用性、基础性,教材和教程自然优先讲解跨平台的标准库函数。

2. f 系列自带用户缓冲区,性能碾压原生系统调用

这是二者最大的性能差距,也是标准库存在的核心意义。

系统调用(read/write)无缓冲:每调用一次,就会触发一次「用户态→内核态」切换,切换开销极大。如果频繁读写少量数据,性能极低。

标准库(fread/fwrite)自带缓冲区:C 库会在用户层开辟一块内存缓冲区,程序读写数据会先存入缓冲区,积攒足量数据后,一次性调用系统调用刷入内核,极大减少态切换次数,大幅提升 IO 效率。

简单总结:原生系统调用笨、慢、直接;标准库聪明、快、有缓冲。

3. f 系列语法简单,入门门槛极低

初学者对比一下就能直观感受到差距:

标准库 fopen(简单易懂)

FILE* fp = fopen("test.txt", "r");

只用简单的字符串模式 "r" "w" "a",零基础也能快速上手。

 原生 open 系统调用(参数复杂)

int fd = open("test.txt", O_RDONLY);

需要记忆 O_RDONLY、O_WRONLY、O_CREAT、O_TRUNC 等大量宏参数,还要手动处理文件权限,对新手极度不友好,入门教程自然不会优先讲解。

   额外扩展一下 open函数      头文件:#include <fcntl.h>

int open(const char *pathname, int flags, mode_t mode);

flags 通过按位或 | 组合多个标识。

释义 使用要点
O_RDONLY 只读打开 基础三模式之一;只能读,不可写;O_RDONLY / O_WRONLY / O_RDWR 三者互斥,只能选一个
O_WRONLY 只写打开 基础三模式之一;只能写,不可读
O_RDWR 读写打开 基础三模式之一;可读、可写
O_CREAT 文件不存在则创建新文件 使用此标志时,open必须提供第三个 mode 参数指定文件权限;文件存在时该标志无效果
O_TRUNC 文件存在并且以可写方式打开时,清空文件,长度置 0 只读模式下 O_TRUNC 失效;等价 fopen "w" 的清空行为
O_EXCL 和 O_CREAT 配套,原子创建 文件已经存在,则 open 直接返回失败;避免并发场景重复创建文件
O_APPEND 追加模式 每次 write 都会自动写到文件末尾;原子追加,多进程写入不会互相覆盖,等价 fopen "a"
O_NONBLOCK 非阻塞 IO open、read、write 不会阻塞等待;无数据 / 无法写入直接返回;多用于管道、设备、socket
O_SYNC 同步写 write 返回前,数据强制刷入磁盘,绕过缓存;数据安全性高,读写速度慢
O_ASYNC 信号驱动 IO 文件就绪(可读 / 可写)时,向进程发送信号通知;多用于网络通信
// 示例:不存在新建,存在清空,只写
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);

其中0644是文件权限 r=4可读  w=2可写  x=1可执行

Linux 权限分为三类用户:

  1. 所有者 (user) 文件创建者
  2. 同组用户 (group)
  3. 其他用户 (other)

每组权限用 3 个比特表示:r w x

  • r = 4 可读
  • w = 2 可写
  • x = 1 可执行

其中数字是八进制,代码里必须写0644,开头0代表八进制

110 100 100 权限对应为rw-r--r--

回过头来

四、什么时候必须用原生 open/read/write?

既然 f 系列这么好用,为什么还要学习底层系统调用?

因为 标准库为了通用性,封装的同时,也隐藏了内核底层能力。在系统编程、底层开发场景,必须使用原生接口:

  • 需要操作文件描述符 fd(标准输入0、输出1、错误2、重定向、管道、dup 等)

  • 需要设置非阻塞 IO、文件锁、特殊文件权限

  • 网络编程、驱动开发、服务器底层开发

  • 需要极致理解 Linux 进程、文件、内核交互原理

五、高危禁忌:绝对不要混用两套接口

这是新手极易踩坑的致命错误:不要同时用 fwrite 和 write 操作同一个文件

原因:FILE* 自带用户缓冲区,fwrite 的数据会先存在库缓存中,还没落到内核文件里。此时调用原生 write 直接操作内核文件,会导致数据乱序、覆盖、丢失

若必须混用,一定要先执行 fflush(fp) 强制刷新缓冲区。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐