解码Linux文件IO之系统IO
系统 IO 与标准 IO 基础
核心思想:Linux “一切皆文件”
Linux 中所有资源(普通文件、目录、设备、套接字等)都以 “文件” 形式抽象,内核通过统一的 “文件描述符” 管理这些资源,系统 IO 就是内核提供的、直接操作这些 “文件” 的函数接口。
系统 IO 与标准 IO 的区别
对比维度 | 系统 IO(内核提供) | 标准 IO(ANSI C 标准库提供) |
---|---|---|
缓冲区 | 无内置缓冲区 | 有缓冲区(全缓冲、行缓冲、无缓冲) |
调用层级 | 直接调用内核接口,属于 “系统调用” | 基于系统 IO 封装,属于 “库函数” |
效率 | 频繁调用时效率低(每次切换内核态) | 减少系统调用次数,效率更高 |
支持文件类型 | 所有文件(普通文件、设备、套接字) | 仅支持普通文件 |
适用场景 | 实时性要求高的场景(如 LCD、触摸屏) | 普通文件读写(如文本、配置文件) |
通俗理解:标准 IO 像 “快递代收点”—— 先把用户要写的内容存到代收点(缓冲区),攒够一批再一次性交给内核(系统 IO),减少跑内核的次数;系统 IO 像 “直接送快递”—— 每次有内容都直接跑内核,实时但麻烦(切换内核态耗时)。
如:写 100 字节到文件
- 标准 IO(fwrite):调用 1 次 fwrite,缓冲区存满 100 字节后,仅调用 1 次 write(系统 IO),快;
- 系统 IO(write):若循环 100 次写 1 字节,需调用 100 次 write,慢。
文件描述符(fd):打开文件的 “身份证号”
本质
文件描述符(file descriptor,简称 fd)是进程内一个名为fd_array
的数组的下标。内核为每个进程维护一个file_struct
结构体,其中的fd_array
数组存储指向 “打开文件结构体(file
)” 的指针,fd 就是通过这个下标找到对应的文件信息。
关键特性
-
类型:非负整数(>=0);
-
分配规则:每次打开文件,分配当前进程中 “最小未使用” 的 fd;
-
默认 fd:进程启动时默认打开 3 个 fd,不可手动关闭(除非重定向):
- 0:
STDIN_FILENO
(标准输入,如键盘); - 1:
STDOUT_FILENO
(标准输出,如终端); - 2:
STDERR_FILENO
(标准错误,如终端);
- 0:
-
独立性:同一文件可多次打开,每次生成不同 fd,各自对应独立的
file
结构体(如读写偏移量独立)。
fd 的最大限制
- 默认限制:每个进程最多打开 1024 个 fd(可通过
ulimit -n
查看); - 临时修改:
ulimit -n 4096
(仅当前终端有效); - 永久修改:修改
/etc/security/limits.conf
,添加soft nofile 4096
和hard nofile 8192
。
核心系统 IO 函数详解
open:打开或创建文件
功能
打开已存在的文件,或创建新文件,返回操作该文件的 fd。
函数
#include <fcntl.h>
/*** 打开已存在文件,或创建新文件(需配合O_CREAT)* @param pathname目标文件的路径+文件名(如"./a.txt",绝对/相对路径均可)* @param flags 打开文件的选项(必选1个访问模式,可选多个创建/状态标志,用|连接)* 必选访问模式(三选一):* O_RDONLY:只读模式;O_WRONLY:只写模式;O_RDWR:可读可写模式* 可选创建/状态标志:* O_CREAT:文件不存在则创建(需配合第三个参数mode);* O_EXCL:与O_CREAT连用,若文件已存在则报错(避免覆盖);* O_TRUNC:打开普通文件时,清空文件原有内容;* O_APPEND:写操作前,自动将偏移量移到文件末尾(追加模式);* O_NONBLOCK:非阻塞模式(读写不等待,无数据时立即返回);* O_CLOEXEC:进程执行exec系列函数时,自动关闭该fd(避免泄露)* O_TMPFILE:创建一个无文件名的临时文件* @param mode 仅当flags含O_CREAT/O_TMPFILE时有效,指定新文件的初始权限(八进制)* 权限规则:owner(u)、group(g)、other(o)各有r(4)、w(2)、x(1),如:* 0644:owner读+写(6=4+2),group读(4),other读(4);* 0755:owner读+写+执行(7=4+2+1),group读+执行(5),other读+执行(5)* 注意:实际权限 = mode & ~umask(umask是系统默认权限掩码,默认0022)* @return 成功:返回非负整数(文件描述符fd);失败:返回-1,设置errno* @note O_CREAT必须配合mode,否则新文件权限不确定;* 不可同时指定O_RDONLY和O_WRONLY,需三选一访问模式;* 设备文件(如/dev/tty)不支持O_TRUNC(清空无意义)*/
int open(const char *pathname, int flags); // 打开已存在文件,无mode
int open(const char *pathname, int flags, mode_t mode); // 创建新文件,需mode
示例:创建并打开文件
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int main() {// 创建a.txt,初始权限0644int fd = open("a.txt", O_RDWR | O_CREAT | O_EXCL, 0644);if (fd == -1) {perror("open failed"); // 打印错误:open failed: File exists(若文件已存在)return -1;}printf("成功打开文件,fd=%d\n", fd); // 首次运行fd=3(默认0/1/2已占用)return 0;
}
close:关闭文件
功能
释放 fd 对应的内核资源(file
结构体等),fd 变为 “未使用”,可被后续 open 复用。
函数
#include <unistd.h>
/*** 关闭文件描述符,释放内核资源* @param fd 要关闭的文件描述符(必须是当前进程已打开的有效fd)* @return 成功:返回0;失败:返回-1,设置errno(如fd无效时errno=EBADF)* @note 多次关闭同一fd:仅第一次成功,后续关闭返回-1(fd已无效);* 关闭fd=0/1/2(标准输入/输出/错误)后,若再open,新fd可能为0/1/2;* 进程退出时,内核会自动关闭所有未关闭的fd,但建议手动关闭(避免资源泄漏)*/
int close(int fd);
示例:关闭文件
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main() {int fd = open("a.txt", O_RDWR);if (fd == -1) {perror("open failed");return -1;}// 第一次关闭:成功int ret = close(fd);if (ret == 0) {printf("第一次关闭成功\n");}// 第二次关闭:失败(fd已无效)ret = close(fd);if (ret == -1) {perror("第二次关闭失败"); // 输出:第二次关闭失败: Bad file descriptor}return 0;
}
read:从文件读取数据
功能
从 fd 对应的文件中,读取指定字节数的数据到用户提供的缓冲区。
函数
#include <unistd.h>
/*** 从文件描述符读取数据到用户缓冲区* @param fd 已打开的文件描述符(需有读权限,如O_RDONLY/O_RDWR)* @param buf 用户缓冲区地址(需可写,存储读取到的数据)* @param count 期望读取的字节数(不能超过buf的实际大小,避免缓冲区溢出)* @return 成功:返回实际读取的字节数(可能小于count);* 0:到达文件末尾(EOF),无数据可读;* -1:失败,设置errno(如fd无读权限时errno=EBADF)* @note 1. 实际读取字节数<count的场景:* - 接近文件末尾(如文件剩50字节,count=100,返回50);* - 读取管道/终端(如终端输入仅30字节,count=100,返回30);* - 被信号中断(读取部分数据后,返回已读字节数);* 2. buf需提前分配空间(如char buf[1024];),不可为NULL*/
ssize_t read(int fd, void *buf, size_t count);
示例:读取文件内容
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main() {int fd = open("a.txt", O_RDONLY);if (fd == -1) {perror("open failed");return -1;}char buf[1024] = {0}; // 初始化缓冲区(避免脏数据)// 读取最多1023字节(留1字节存'\0',方便打印)ssize_t n = read(fd, buf, sizeof(buf) - 1);if (n == -1) {perror("read failed");close(fd);return -1;} else if (n == 0) {printf("文件为空\n");} else {printf("读取到%d字节:%s\n", (int)n, buf);}close(fd);return 0;
}
write:向文件写入数据
功能
将用户缓冲区中的数据,写入到 fd 对应的文件中。
函数
#include <unistd.h>
/*** 从用户缓冲区向文件描述符写入数据* @param fd 已打开的文件描述符(需有写权限,如O_WRONLY/O_RDWR)* @param buf 存储待写入数据的缓冲区(const修饰,避免被修改)* @param count 期望写入的字节数(通常是strlen(buf),若buf是字符串)* @return 成功:返回实际写入的字节数(可能小于count);* -1:失败,设置errno(如磁盘满时errno=ENOSPC)* @note 实际写入字节数<count的场景:* - 磁盘空间不足(无法写入全部数据);* - 超过文件大小限制(RLIMIT_FSIZE,需用setrlimit修改);* - 被信号中断(写入部分数据后返回);* O_APPEND模式:每次write前,内核自动将偏移量移到文件末尾(原子操作,避免多进程追加冲突);* 普通文件写入后,文件偏移量会自动增加“实际写入字节数”*/
ssize_t write(int fd, const void *buf, size_t count);
示例:向文件写入数据
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main() {// 打开文件,若存在则追加,不存在则创建(权限0644)int fd = open("a.txt", O_WRONLY | O_APPEND | O_CREAT, 0644);if (fd == -1) {perror("open failed");return -1;}const char *msg = "Hello, System IO!\n";// 写入msg的全部字节(strlen(msg)计算长度)ssize_t n = write(fd, msg, strlen(msg));if (n == -1) {perror("write failed");close(fd);return -1;}printf("成功写入%d字节\n", (int)n);close(fd);return 0;
}
lseek:设置文件读写偏移量
功能
调整 fd 对应的文件的 “读写位置(偏移量)”,仅改变位置,不实际读写数据。偏移量是从文件开头计算的字节数。
函数
#include <sys/types.h>
#include <unistd.h>
/*** 设置文件描述符的读写偏移量* @param fd 已打开的文件描述符(需支持seek,普通文件/设备支持,管道/套接字不支持)* @param offset 偏移量(可正可负,具体含义由whence决定)* @param whence 偏移量的参考基准:* SEEK_SET:以文件开头为基准,offset=0表示文件开头;* SEEK_CUR:以当前偏移量为基准,offset=5表示向后移5字节,offset=-3表示向前移3字节;* SEEK_END:以文件末尾为基准,offset=0表示文件末尾,offset=-10表示末尾前10字节* @return 成功:返回调整后的偏移量(从文件开头计算的字节数);* -1:失败,设置errno(如管道不支持seek时errno=ESPIPE)* @note 文件空洞:若offset超过文件当前大小,后续write会在“空洞部分”填'\0',但空洞不占用磁盘空间;* 文本文件建议用SEEK_SET(避免换行符导致的偏移计算错误);* off_t是长整型(32/64位),不是int,需包含<sys/types.h>*/
off_t lseek(int fd, off_t offset, int whence);
示例:指定偏移量读取
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main() {int fd = open("a.txt", O_RDWR);if (fd == -1) {perror("open failed");return -1;}// 定位到文件开头(偏移量0)off_t pos = lseek(fd, 0, SEEK_SET);printf("当前偏移量(开头):%ld\n", pos);// 定位到文件末尾,获取文件大小(偏移量=文件大小)pos = lseek(fd, 0, SEEK_END);printf("文件大小:%ld字节\n", pos);// 定位到末尾前10字节,读取这10字节pos = lseek(fd, -10, SEEK_END);char buf[11] = {0};ssize_t n = read(fd, buf, 10);if (n > 0) {printf("末尾前10字节:%s\n", buf);}close(fd);return 0;
}
dup/dup2:复制文件描述符
功能
复制已有的 fd,新 fd 与原 fd 共享同一个file
结构体(即共享读写偏移量、文件状态等)。
函数(dup)
#include <unistd.h>
/*** 复制文件描述符(自动分配最小未用fd)* @param oldfd 已打开的源文件描述符(必须有效)* @return 成功:返回新的文件描述符(当前进程最小未用fd);* -1:失败,设置errno(如oldfd无效时errno=EBADF)* @note 新fd与oldfd共享:读写偏移量、文件权限、状态标志(如O_APPEND);* 关闭新fd不影响oldfd,反之亦然,但关闭任意一个,另一个仍可操作文件*/
int dup(int oldfd);
函数(dup2)
#include <unistd.h>
/*** 复制文件描述符(指定新fd的数值)* @param oldfd 已打开的源文件描述符(必须有效)* @param newfd 期望的新文件描述符数值* @return 成功:返回newfd(若newfd已打开,先自动关闭newfd再复制);* -1:失败,设置errno(如oldfd无效或newfd=oldfd时无操作)* @note 若newfd已打开且≠oldfd,dup2会先关闭newfd(即使关闭失败,仍会继续复制);* 常用场景:重定向(如将stdout(fd=1)重定向到文件);* 若newfd=oldfd,直接返回newfd(无复制操作)*/
int dup2(int oldfd, int newfd);
示例:重定向标准输出到文件
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main() {// 打开日志文件(追加模式)int log_fd = open("log.txt", O_WRONLY | O_APPEND | O_CREAT, 0644);if (log_fd == -1) {perror("open log failed");return -1;}// 将stdout(fd=1)重定向到log_fd:关闭fd=1,再复制log_fd为fd=1dup2(log_fd, 1);// 此时printf会写入log.txt(而非终端)printf("这是一条日志,时间:%s\n", __TIME__);close(log_fd);return 0;
}
fcntl:文件控制(灵活操作 fd)
功能
对已打开的 fd 进行多样化控制,如获取 / 设置文件状态、设置非阻塞、复制 fd 等(功能最灵活的系统 IO 函数)。
函数
#include <fcntl.h>
/*** 文件描述符控制函数(变参函数,参数由cmd决定)* @param fd 已打开的文件描述符* @param cmd 控制命令(决定函数功能),常用命令:* F_GETFL:获取文件状态标志(如O_APPEND、O_NONBLOCK);* F_SETFL:设置文件状态标志(仅支持O_APPEND、O_NONBLOCK等部分标志);* F_DUPFD:复制fd,类似dup,参数为“最小允许的新fd”;* F_GETFD:获取fd的标志(如FD_CLOEXEC);* F_SETFD:设置fd的标志(如FD_CLOEXEC)* @param ... 可选参数,由cmd决定(如F_SETFL需传“新状态标志”)* @return 成功:返回值由cmd决定(F_GETFL返回状态标志,F_DUPFD返回新fd);* -1:失败,设置errno* @note 变参函数:cmd不同,后续参数不同,需严格匹配;* F_SETFL不能修改所有标志(如O_RDONLY/O_WRONLY/O_RDWR不能改,需重新open);* 常用场景:设置非阻塞IO、获取文件打开模式*/
int fcntl(int fd, int cmd, ... /* 可选参数 */);
示例 1:设置非阻塞模式
阻塞与非阻塞 IO
- 阻塞 IO:默认模式,读写时若无数据 / 资源,进程会 “等待”(阻塞),直到有数据 / 资源;
- 非阻塞 IO:读写时若无数据 / 资源,立即返回 - 1,errno=EAGAIN/EWOULDBLOCK,进程不等待;
- 适用场景:网络编程(如服务器同时处理多个客户端)、设备操作(如读取传感器数据)。
#include <fcntl.h>
#include <stdio.h>
int main() {// 打开标准输入(fd=0,键盘)int fd = 0;// 获取当前状态标志int flags = fcntl(fd, F_GETFL);if (flags == -1) {perror("fcntl F_GETFL failed");return -1;}// 添加非阻塞标志(O_NONBLOCK)flags |= O_NONBLOCK;// 设置新状态标志if (fcntl(fd, F_SETFL, flags) == -1) {perror("fcntl F_SETFL failed");return -1;}printf("标准输入已设置为非阻塞模式\n");return 0;
}
示例 2:获取文件打开模式
#include <fcntl.h>
#include <stdio.h>
int main() {int fd = open("a.txt", O_RDWR | O_APPEND);if (fd == -1) {perror("open failed");return -1;}// 获取状态标志int flags = fcntl(fd, F_GETFL);if (flags == -1) {perror("fcntl failed");close(fd);return -1;}// 判断打开模式if (flags & O_RDWR) {printf("文件打开模式:可读可写\n");}if (flags & O_APPEND) {printf("文件状态:追加模式\n");}close(fd);return 0;
}/*
结果:
文件打开模式:可读可写
文件状态:追加模式
*/
原子操作
- 定义:操作要么完全执行,要么完全不执行,无中间状态;
- 示例:O_APPEND 模式的 write(偏移到末尾 + 写入一步完成,避免多进程追加冲突);
- 非原子操作:手动 lseek 到末尾再 write(多进程时可能出现数据覆盖)。
错误处理:定位系统 IO 的 “问题”
核心工具:errno 与错误函数
- errno:全局变量(定义在
<errno.h>
),系统调用失败时自动设置(成功时值不确定,不可用); - strerror:将 errno 转为人类可读的错误信息字符串(
<string.h>
); - perror:直接打印错误信息(格式:“自定义信息:错误信息”,
<stdio.h>
)。
示例:错误处理实战
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
int main() {int fd = open("nonexist.txt", O_RDONLY);if (fd == -1) {// 方法1:用perror打印perror("open nonexist.txt failed");// 方法2:用strerror打印fprintf(stderr,"open failed: errno=%d, info=%s\n", errno, strerror(errno));return -1;}close(fd);return 0;
}
输出:
open nonexist.txt failed: No such file or directory
open failed: errno=2, info=No such file or directory
文件权限与 umask:控制谁能操作文件
权限组成(r/w/x)
文件权限分为三类用户,每类用户有 3 种权限:
用户类型 | 含义 | 权限符号 | 权限数值 |
---|---|---|---|
owner(u) | 文件所有者(创建者) | r/w/x | 4/2/1 |
group(g) | 文件所属组的用户 | r/w/x | 4/2/1 |
other(o) | 其他用户 | r/w/x | 4/2/1 |
示例:
- 0644:owner(6=4+2,读 + 写),group(4,读),other(4,读);
- 0755:owner(7=4+2+1,读 + 写 + 执行),group(5=4+1,读 + 执行),other(5,读 + 执行)。
umask:系统权限掩码
- 作用:限制新文件的默认权限(避免权限过宽);
- 计算方式:实际权限 = mode(open 的第三个参数) & ~umask;
- 默认值:Linux 默认 umask=0022(八进制),即屏蔽 group 和 other 的 “写权限”;
- 修改方式:
- shell 临时修改:
umask 0002
(仅当前终端有效); - 程序中修改:
umask(0002)
(需包含<sys/stat.h>
)。
- shell 临时修改:
示例:open 时 mode=0666,umask=0022:实际权限 = 0666 & ~0022 = 0644(group 和 other 的写权限被屏蔽)。