系统 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。
函数
c
#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
示例:创建并打开文件
c
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int main() {
// 创建a.txt,初始权限0644
int 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 复用。
函数
c
#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);
示例:关闭文件
c
#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 对应的文件中,读取指定字节数的数据到用户提供的缓冲区。
函数
c
#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);
示例:读取文件内容
c
#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 对应的文件中。
函数
c
#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);
示例:向文件写入数据
c
#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 对应的文件的 "读写位置(偏移量)",仅改变位置,不实际读写数据。偏移量是从文件开头计算的字节数。
函数
c
#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);
示例:指定偏移量读取
c
#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)
c
#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)
c
#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);
示例:重定向标准输出到文件
c
#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=1
dup2(log_fd, 1);
// 此时printf会写入log.txt(而非终端)
printf("这是一条日志,时间:%s\n", __TIME__);
close(log_fd);
return 0;
}
fcntl:文件控制(灵活操作 fd)
功能
对已打开的 fd 进行多样化控制,如获取 / 设置文件状态、设置非阻塞、复制 fd 等(功能最灵活的系统 IO 函数)。
函数
c
#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,进程不等待;
- 适用场景:网络编程(如服务器同时处理多个客户端)、设备操作(如读取传感器数据)。
c
#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:获取文件打开模式
c
#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>
)。
示例:错误处理实战
c
#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 的写权限被屏蔽)。