Linux 底层明明是 open/read/write,为什么日常写C语言代码只见到 fopen/fread/fwrite?
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 权限分为三类用户:
- 所有者 (user) 文件创建者
- 同组用户 (group)
- 其他用户 (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) 强制刷新缓冲区。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)