Unix 系统调用 I/O 与标准 I/O 对比
本文档整理自学习笔记,涵盖概念定义、核心区别、详细接口列表及混用注意事项。
一、概念定义
系统调用 I/O(又称"无缓冲 I/O")
- 由内核直接提供的接口:
open、close、read、write、lseek、pread、pwrite 等 - 操作对象是文件描述符(fd)
- 由 POSIX.1 标准定义
标准 I/O
- 建立在系统调用之上、由 C 库封装的一层:
fopen、fclose、fread、fwrite、fgets、fputs、printf、scanf、fflush、fseek 等 - 操作对象是 FILE 流(
FILE *) - 由 ANSI C 标准定义
二、核心区别
| 对比项 | 系统调用 I/O | 标准 I/O |
|---|
| 缓冲 | 无用户级缓冲,每次调用直接陷入内核(一次 read/write = 一次系统调用) | 有用户级缓冲(全缓冲、行缓冲、无缓冲三种模式),减少系统调用次数 |
| 操作对象 | 文件描述符 fd | 流指针 FILE * |
| 标准 | POSIX.1 | ANSI C(跨平台移植性好) |
| 性能 | 小数据量频繁读写时开销大 | 利用缓冲,批量陷入内核,通常更快 |
| 能力 | 底层、完整,支持 lseek、fcntl、select、O_NONBLOCK 等 | 提供格式化、字符/行读写、复制等高层能力 |
| 并发访问 | 无 | fork 后父子进程共享 FILE 结构时需注意 |
三、底层关系
// 系统调用:每次 write 都是一次内核陷阱
write(fd, buf, n); // -> 内核
// 标准 I/O:先写入用户态缓冲区
fwrite(buf, 1, n, fp); // -> 缓冲区 -> (满时) -> 内核
fread/fwrite 内部最终调用 read/write;标准 I/O 只是在内核之上加了一层用户态缓冲,并封装了格式化等功能。
四、系统调用 I/O 接口详解
定义在 <unistd.h>、<fcntl.h>、<sys/stat.h>、<sys/types.h> 等头文件中。
1. 打开 / 关闭
| 函数 | 说明 |
|---|
open(path, flags[, mode]) | 打开或创建文件,返回 fd;flags 如 O_RDONLY、O_WRONLY、O_RDWR、O_CREAT、O_APPEND、O_TRUNC、O_NONBLOCK |
openat(dirfd, path, flags[, mode]) | 相对目录 fd 打开(POSIX.1-2008),避免竞态条件 |
creat(path, mode) | 等价于 `open(path, O_CREAT\ | O_WRONLY\ | O_TRUNC, mode)` |
close(fd) | 关闭 fd |
2. 读 / 写
| 函数 | 说明 |
|---|
read(fd, buf, nbytes) | 读 nbytes 到 buf,返回实际读到的字节数(可能 < nbytes) |
write(fd, buf, nbytes) | 写 nbytes,返回实际写入字节数 |
pread(fd, buf, n, offset) | 定位 + 读的原子操作,不改变文件偏移量 |
pwrite(fd, buf, n, offset) | 定位 + 写的原子操作 |
readv / writev | 分散读 / 聚集写(scatter-gather) |
recv / send / sendto / recvfrom | 套接字专用读写 |
3. 文件定位与截断
| 函数 | 说明 |
|---|
lseek(fd, offset, whence) | 设置偏移量;whence 为 SEEK_SET/SEEK_CUR/SEEK_END |
truncate(path, len) / ftruncate(fd, len) | 按路径 / 按 fd 截断文件 |
4. 描述符复制与重定向
| 函数 | 说明 |
|---|
dup(fd) | 复制 fd,返回最小的可用 fd |
dup2(fd, fd2) / dup3(fd, fd2, flags) | 复制到指定 fd(常用于 shell 重定向 >、2>) |
5. 文件控制与状态
| 函数 | 说明 |
|---|
fcntl(fd, cmd, ...) | 万能控制:复制 fd、设置 O_NONBLOCK、获取/设置记录锁(F_SETLK 等) |
ioctl(fd, request, ...) | 设备控制(终端、网卡等) |
stat / fstat / lstat | 获取文件元信息(inode、权限、大小等) |
fstatat | 相对目录 fd 的 stat |
access(path, mode) / faccessat | 检查访问权限 |
chmod / fchmod / fchmodat | 修改权限 |
chown / fchown / lchown | 修改属主/属组 |
umask(mask) | 设置默认权限掩码 |
6. 同步与缓存控制
| 函数 | 说明 |
|---|
fsync(fd) | 将数据 + 元数据刷到磁盘 |
fdatasync(fd) | 只刷数据(不刷元数据,更快) |
sync() | 刷所有内核缓冲 |
mmap / munmap / msync | 内存映射 I/O 及其同步 |
7. 目录与链接
| 函数 | 说明 |
|---|
mkdir / rmdir | 建/删目录 |
opendir / readdir / closedir / rewinddir | 读目录项 |
chdir / fchdir / getcwd | 切换/获取工作目录 |
link / unlink / symlink / readlink / rename | 硬链接、删除、软链接、重命名 |
8. 多路复用(配合非阻塞 I/O)
| 函数 | 说明 |
|---|
select / pselect | 跨平台的多路复用 |
poll / ppoll | 同上 |
epoll_create / epoll_ctl / epoll_wait | Linux 高性能多路复用 |
9. 文件锁
| 函数 | 说明 |
|---|
flock(fd, op) | 整文件咨询锁 |
fcntl 记录锁(F_RDLCK/F_WRLCK/F_UNLCK) | 字节范围锁,POSIX 标准 |
五、标准 I/O 接口详解
定义在 <stdio.h> 中,核心是 FILE * 流。
1. 打开 / 关闭 / 转换
| 函数 | 说明 |
|---|
fopen(path, mode) | 打开流;mode 如 "r"、"w"、"a"、"r+"、"wb" |
freopen(path, mode, stream) | 重定向已有流(常用于 freopen("/dev/null","w",stdout)) |
fdopen(fd, mode) | 将 fd 包装成流(系统调用 ↔ 标准 I/O 的桥梁) |
fclose(fp) | 关闭流(自动刷新缓冲) |
fileno(fp) | 从流取出底层 fd(反过来用) |
2. 字符级读写(无缓冲调用,但经过流缓冲)
| 函数 | 说明 |
|---|
getc(fp) / fgetc(fp) / getchar() | 读单个字符 |
putc(c, fp) / fputc(c, fp) / putchar(c) | 写单个字符 |
3. 行级读写
| 函数 | 说明 |
|---|
fgets(buf, n, fp) | 读一行(含换行符,安全,推荐) |
fputs(s, fp) | 写一行(不自动加换行) |
gets(s) | 读一行(已废弃,无法防止缓冲区溢出) |
puts(s) | 写一行(自动加换行) |
4. 二进制 / 块读写
| 函数 | 说明 |
|---|
fread(ptr, size, nmemb, fp) | 读 size*nmemb 字节,返回完整读入的元素数 |
fwrite(ptr, size, nmemb, fp) | 写二进制块 |
5. 格式化 I/O(标准 I/O 独有,系统调用没有)
| 函数 | 说明 |
|---|
printf(fmt, ...) | 格式化到 stdout |
fprintf(fp, fmt, ...) | 格式化到任意流 |
sprintf / snprintf(buf, n, fmt, ...) | 格式化到字符串(snprintf 安全) |
dprintf(fd, fmt, ...) | 格式化到 fd(POSIX) |
scanf(fmt, ...) / fscanf(fp, ...) / sscanf(str, ...) | 格式化输入 |
vfprintf / vprintf / vsnprintf | 可变参数版本(供自定义 printf 使用) |
6. 定位
| 函数 | 说明 |
|---|
fseek(fp, offset, whence) / ftell(fp) | 定位 / 获取位置 |
rewind(fp) | 回到文件头(等价 fseek(0, SEEK_SET)) |
fgetpos / fsetpos | 大文件安全定位 |
7. 缓冲控制
| 函数 | 说明 |
|---|
fflush(fp) | 把缓冲内容刷给内核(或立即输出到终端) |
setvbuf(fp, buf, mode, size) | 设置缓冲模式:_IOFBF 全缓冲、_IOLBF 行缓冲、_IONBF 无缓冲 |
setbuf(fp, buf) | 简化版 setvbuf |
8. 错误与 EOF 检测
| 函数 | 说明 |
|---|
feof(fp) | 是否到达文件尾 |
ferror(fp) | 是否有 I/O 错误 |
clearerr(fp) | 清除 EOF/错误标志 |
perror(s) / strerror(errno) | 输出/获取错误描述 |
9. 文件操作与临时文件
| 函数 | 说明 |
|---|
remove(path) | 删除文件或空目录 |
rename(old, new) | 重命名 |
tmpfile() | 创建自动删除的临时文件 |
tmpnam / tempnam | 生成临时文件名(有竞态,不推荐) |
mkstemp / mkdtemp | 安全创建临时文件/目录 |
六、混用的关键桥梁
int fd = open("f", O_RDWR); // 系统调用
FILE *fp = fdopen(fd, "r+"); // 包装成流
fflush(fp); // 混用前先刷缓冲
| 转换方向 | 函数 |
|---|
| fd → FILE * | fdopen(fd, mode) |
| FILE * → fd | fileno(fp) |
混用时的注意事项
- 同步问题:如果同文件同时用 fd 和 FILE 流操作,缓冲内容可能丢失。需用
fflush 刷空缓冲,或用 fdopen / dup 让两者共享偏移量。 必须用系统调用的场景:
- 需要控制缓冲的设备行为(如套接字设
O_NONBLOCK、配合 select/poll) read/write 可能被信号中断,需要自行处理 EINTR- 需要原子性保证(
pread/pwrite、O_APPEND)
七、选择建议
- 优先用标准 I/O:普通文本/二进制文件、需要格式化、追求可移植性
- 必须用系统调用:非阻塞 I/O、多路复用(select/poll/epoll)、信号中断处理(EINTR)、精确控制缓冲、直接操作设备、需要原子定位读写(pread/pwrite)、文件锁
八、一句话总结
系统调用 I/O 是内核接口,灵活底层但每次调用开销大;标准 I/O 是 C 库封装,带用户态缓冲、方便安全,适合大多数普通文件读写场景。标准 I/O 解决的是效率与易用性,系统调用解决的是控制力。