Linux系统编程笔记(六):文件I/O详解
在 Linux 下一切皆文件,文件 I/O 是我们与内核交互最基本的方式。本篇笔记将从 man 手册的使用开始,逐步拆解系统调用、文件描述符、读写函数以及参数传递等核心概念,希望能帮助刚入门的同学理清思路。
一、man 手册:你的第一参考书
在 Linux 下遇到不认识的函数,第一反应应该是 man。man man 可以查看 man 手册本身的说明,它共分为 9 个章节,实际使用中我们最常关注前 3 章:
- 1 可执行程序或 shell 指令
- 2 系统调用(内核提供的函数)
- 3 库调用
- 4 特殊文件
- 5 文件格式和规范
- 6 游戏
- 7 杂项
- 8 系统管理指令
- 9 内核例程
经验之谈:当你想查
open、read这类函数时,记得用man 2 open,否则可能看到的是 shell 命令open的解释。
二、系统调用的本质
系统调用是内核提供给外部访问 Linux 操作系统的一系列函数接口。应用程序通过系统调用陷入内核态,由内核完成实际的操作后返回结果。

理解系统调用,有助于我们区分"库函数"和"系统调用":库函数在用户态实现,可能封装了多个系统调用;而系统调用直接与内核交互,代价更高。
三、文件操作函数
1. 打开文件
c
int open(const char* pathname,int flags);
//参数1 :打开文件的路径名
//参数2:打开文件的方式
//O_RDONLY|O_WRONLY|O_RDWR --- <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
//参数3使用的前提是参数二指定了 O_CREAT
//mode意思为权限 参数包括rwx 也可以传8进制0664
c
注意 open 函数的第二个参数 flags 必须包含 O_RDONLY、O_WRONLY 或 O_RDWR 三者之一,这是"访问模式"。第三个参数 mode 只有在 flags 中指定了 O_CREAT 时才有效,用于指定新建文件的权限。
2. 文件创建权限与 umask
Linux 系统中,创建文件的最终权限并不是 open 的 mode 参数直接决定的,它还会受到进程的文件屏蔽码 umask 影响。
计算公式为:
text
最终文件权限 = mode & ~umask
(原笔记写作 mode&-umask,其实隐藏了按位取反的逻辑,实际使用中请务必用 ~umask)
小技巧:在 shell 里执行
umask可以查看当前进程的屏蔽码,常见的0022表示屏蔽掉组和其他用户的写权限。
3. 文件读写
- 读文件
c
ssize_t read(int fd,void* buf,size_t count);
//按字节读
fd:文件描述符
buf存储缓冲区
count:缓冲区大小
返回值
成功
未读到文件末尾》0
读到:0
失败:-1
- 写文件
c
ssize_t write(int fd,const void *buf,size_t count);
//按字节写入
fd:文件描述符
buf存储待写出缓冲区
count:数据大小
返回值
成功
>0实际写出的字节数
=0没写出数据
失败:-1
read 和 write 都是按字节操作的,非常底层。返回值要特别注意:read 返回 0 表示读到文件末尾,返回 -1 表示出错;write 返回 0 表示没写出数据(通常不会发生),返回 -1 表示出错。
4. 错误处理
Linux 系统调用失败时通常会返回 -1,并设置全局错误号 errno。我们可以用以下两个函数来获取错误描述:
c
# include<string.h>
char* strerror(int errnum);
参数为错误号
返回值为错误号对应的错误描述
c
# include<stdio.h>
void perror(const char* s);
参数为用户自定义的错误信息
strerror 需要传入错误号,返回对应的字符串描述;perror 则会把 errno 当前的错误描述打印出来,并自动加上用户传入的前缀。
5. 预读入缓输出
Linux 对文件 I/O 做了内核缓冲处理,读操作会预读入数据到内核缓冲区,写操作则先缓存在缓冲区,再由内核统一写出,这样能显著提升性能。

这里所说的"预读入"和"缓输出"是内核层面的机制,和我们用户态的 stdio 缓冲不是一回事。不过 stdio 也有自己的缓冲区,比如 fread/fwrite,使用时注意区分。
6. 文件描述符
- PCB(进程控制块)本质上是一个结构体,其中包含一个文件描述符表。
- 文件描述符范围:0/1/2/3/4/5/6/.../1023
- 默认最大上限可以用
ulimit -a查看
- 默认最大上限可以用
- 文件描述符表的使用规则:使用表中未被占用的最小文件描述符
三个特殊文件:
stdin:标准输入 - 0 -STDIN_FILENOstdout:标准输出 - 1 -STDOUT_FILENOstderr:标准错误 - 2 -STDERR_FILENO

以后写程序时,如果不小心关闭了
stdout,那么再打开的第一个文件就会占用文件描述符 1。这种细节会直接影响输入输出行为,调试时务必留意。
7. 阻塞与非阻塞
阻塞、非阻塞是设备文件、网络文件具备的属性(不是 read/write 的属性)。
产生阻塞的场景主要有:
- 读设备文件
- 读网络文件
读常规文件则没有阻塞的概念。比如终端文件 /dev/tty,默认是阻塞的,如果用户不输入,read 会一直卡住。我们也可以把它设置为非阻塞:
c
open("/dev/tty",O_RDWE|O_NONBLOCK);
注意:示例中的
O_RDWE疑似笔误,正确应为O_RDWR或O_RDONLY等访问模式标志,实际使用时请参照fcntl.h中定义。
8. lseek 函数
c
off_t lseek(int fd,offJ_t offset,int whence);
fd:文件描述
offset:偏移量 正数向后 负数向前
whence:起始偏移位置,SEEK_SET/SEEK_CUR/SEEKs_END
返回值:
成功返回起始位置的偏移
(代码示例中的 offJ_t 应理解为 off_t,请勿照抄)
相关知识:
- 文件的"读""写"使用的偏移位置是同一个位置
- 可以使用
lseek获取文件大小 - 可以使用
lseek拓展文件大小,但要使文件大小真正发生拓展,必须要有 I/O 操作介入- 直接拓展文件,也可以使用
truncate()函数来完成
- 直接拓展文件,也可以使用
四、传入参数、传出参数与传入传出参数
函数参数是指针时,在 C 语言中非常常见。根据数据的流动方向,我们可以把参数分为三类:
1. 传入参数
- 例子:
write的第 2 参、strcpy的第 2 参 - 特点:
- 指针做函数参数
- 通常有
const修饰 - 指针指向有效区域,在函数内做读操作
2. 传出参数
- 例子:
strcpy的第 1 参 - 特点:
- 指针做函数参数
- 在函数调用前,指针指向的空间可以无意义但必须有效
- 在函数内部做写操作
- 函数调用结束后充当函数返回值
3. 传入传出参数
- 例子:
strtok的第 3 参 - 特点:
- 指针做函数参数
- 在函数调用之前指针指向的空间有效且有实际意义
- 在函数内部,先做读操作,后做写操作
- 函数调用结束后,充当函数返回值
理解这三类参数,对阅读源代码和设计接口都非常有帮助。尤其是"传出参数",经常用来替代多个返回值的情况,这是 C 语言的一种常见编程手法。
总结
本篇笔记梳理了 Linux 文件 I/O 的若干核心概念:man 手册、系统调用、open/read/write、错误处理、文件描述符、阻塞/非阻塞、lseek 以及参数传递方向。掌握这些基础,后续学习重定向、管道、socket 等高级 I/O 会轻松很多。希望这篇笔记对你有所帮助,欢迎在评论区交流讨论!