Read/Write 基础¶
是什么 / 解决什么问题¶
open/read/write/close 是文件 I/O 的基础系统调用。理解它们与 page cache 的交互关系是理解 Linux 文件性能的关键。
使用模式¶
基本文件读写¶
#include <fcntl.h>
#include <unistd.h>
// 打开文件
int fd = open("/path/to/file", O_RDONLY);
int fd = open("/path/to/file", O_WRONLY | O_CREAT | O_TRUNC, 0644);
int fd = open("/path/to/file", O_RDWR | O_APPEND);
// openat: 相对目录 fd 打开(更安全,避免 TOCTOU)
int fd = openat(dirfd, "filename", O_RDONLY);
// 读
char buf[4096];
ssize_t n = read(fd, buf, sizeof(buf));
// n > 0: 实际读取字节数
// n == 0: 到达文件末尾(EOF)
// n == -1: 错误
// 写
ssize_t n = write(fd, data, data_len);
// n >= 0: 实际写入字节数(可能 < data_len)
// n == -1: 错误
// 定位
off_t pos = lseek(fd, 0, SEEK_SET); // 文件开头
off_t pos = lseek(fd, 0, SEEK_CUR); // 当前位置(查询)
off_t pos = lseek(fd, 0, SEEK_END); // 文件末尾
off_t pos = lseek(fd, -100, SEEK_END); // 从末尾倒退 100 字节
// 关闭
close(fd);
pread/pwrite(原子定位+读写)¶
// 在指定偏移量读写,不改变 fd 的当前位置
// 多线程共享 fd 时安全
ssize_t n = pread(fd, buf, size, offset);
ssize_t n = pwrite(fd, data, size, offset);
// 等价于 lseek + read/write,但是原子操作
readv/writev(散布/聚集 I/O)¶
struct iovec iov[3] = {
{ .iov_base = header, .iov_len = 16 },
{ .iov_base = body, .iov_len = body_len },
{ .iov_base = footer, .iov_len = 4 },
};
// 一次 syscall 写入多个不连续缓冲区
ssize_t n = writev(fd, iov, 3);
// 同样支持 preadv/pwritev(指定偏移)
ssize_t n = preadv(fd, iov, 3, offset);
行为与语义¶
Page Cache 模型¶
应用程序
│ read(fd, buf, 4096)
▼
┌──────────┐
│ Page Cache│ ← 内核维护的文件数据缓存(内存中)
│ (内存页) │
└──────────┘
│ 缺页时触发磁盘读取
▼
┌──────────┐
│ 块设备层 │ ← 实际磁盘 I/O
└──────────┘
read 的实际路径: 1. 检查目标数据是否在 page cache 中 2. Cache hit:内存拷贝到用户缓冲区,微秒级返回 3. Cache miss:发起磁盘 I/O,阻塞等待数据到达 page cache,再拷贝
write 的实际路径: 1. 数据拷贝到 page cache 中对应的页 2. 标记该页为 dirty(脏页) 3. 立即返回(不等磁盘写入) 4. 后台线程(pdflush/writeback)异步将脏页写回磁盘
Short read / Short write¶
read 和 write 可能返回比请求更少的字节数:
// 请求读 1MB,但只读到 4KB(文件剩余不足、信号中断等)
ssize_t n = read(fd, buf, 1048576);
// n 可能是 4096
// 正确做法:循环读完
size_t total = 0;
while (total < expected) {
ssize_t n = read(fd, buf + total, expected - total);
if (n <= 0) break;
total += n;
}
普通文件的 write 几乎不会短写(除非磁盘满或被信号中断),但 socket/pipe 会。养成检查返回值的习惯。
O_APPEND 的原子性¶
int fd = open("log.txt", O_WRONLY | O_APPEND);
write(fd, msg, msg_len);
// 内核保证:lseek 到末尾 + write 是原子的
// 多进程并发 append 写同一文件不会交错
文件描述符与 file description 的区别¶
int fd1 = open("file", O_RDONLY);
int fd2 = dup(fd1);
// fd1 和 fd2 是不同的 fd(文件描述符)
// 但共享同一个 file description(内核 struct file)
// 共享偏移量!fd1 的 read 会影响 fd2 的位置
int fd3 = open("file", O_RDONLY);
// fd3 有独立的 file description,独立的偏移量
性能考量¶
读取性能¶
// 1. 对齐读取:按 4KB (page size) 对齐
// 避免跨页读取导致多次 page fault
char buf[4096] __attribute__((aligned(4096)));
pread(fd, buf, 4096, offset & ~4095);
// 2. 预读 (readahead):提前加载后续数据到 page cache
posix_fadvise(fd, offset, length, POSIX_FADV_WILLNEED);
// 或
readahead(fd, offset, count);
// 3. 顺序读提示
posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
// 内核会增大预读窗口(默认 128KB,顺序可到 256KB+)
// 4. 随机读提示
posix_fadvise(fd, 0, 0, POSIX_FADV_RANDOM);
// 内核关闭预读(避免浪费 I/O)
写入性能¶
// 1. 批量写:减少 syscall 次数
// 差:每行一个 write
// 好:缓冲多行后一次 write
// 2. 预分配空间:避免文件系统碎片和元数据更新
fallocate(fd, 0, 0, file_size);
// 3. 控制脏页回写
// 系统级:
// /proc/sys/vm/dirty_ratio = 20 (内存 20% 为脏页时阻塞写入)
// /proc/sys/vm/dirty_background_ratio = 10 (10% 时开始后台回写)
读取后不再需要的数据¶
// 告诉内核:这段数据不用缓存了
posix_fadvise(fd, offset, length, POSIX_FADV_DONTNEED);
// 内核尝试释放对应 page cache
// 适合:大文件顺序扫描,读完一段就释放
常见陷阱与 FAQ¶
1. write 返回成功 ≠ 持久化¶
write(fd, data, len); // 只到 page cache
fsync(fd); // 等待数据和元数据落盘
fdatasync(fd); // 只等数据落盘(不含 mtime 等元数据)
数据库必须 fsync。日志文件通常不需要(可以容忍少量丢失)。
2. O_CREAT 忘记指定权限¶
// 错误:权限未定义(取决于栈上的垃圾值)
int fd = open("file", O_WRONLY | O_CREAT);
// 正确:第三个参数指定权限
int fd = open("file", O_WRONLY | O_CREAT | O_TRUNC, 0644);
3. 读到的内容与文件大小不匹配¶
文件可以有"空洞"(hole):
int fd = open("sparse", O_WRONLY | O_CREAT | O_TRUNC, 0644);
lseek(fd, 1000000, SEEK_SET);
write(fd, "x", 1);
// 文件逻辑大小 1000001 字节,但磁盘只占一个块
// read 空洞区域返回 \0
4. EINTR 处理¶
ssize_t safe_read(int fd, void *buf, size_t count) {
ssize_t n;
do {
n = read(fd, buf, count);
} while (n == -1 && errno == EINTR);
return n;
}
观测与调试¶
# 查看 page cache 使用情况
free -h # buffers/cache 列
# 查看文件是否在 page cache 中
vmtouch /path/to/file
# 输出:Pages in cache: 1024/1024 (100%)
# 查看进程的文件 I/O 统计
cat /proc/<pid>/io
# rchar: 总读取字节
# wchar: 总写入字节
# syscr: read syscall 次数
# syscw: write syscall 次数
# read_bytes: 实际磁盘读取
# write_bytes: 实际磁盘写入
# strace 看 I/O 模式
strace -e trace=read,write,openat -T ./app
# 观察 page cache 命中率
perf stat -e cache-references,cache-misses ./app
延伸阅读¶
man 2 open/man 2 read/man 2 writeman 2 pread/man 2 readvman 2 posix_fadvise— 预读控制man 2 fallocate— 预分配