Linux文件IO

Linux文件IO

UNIX系统中的大多数文件I/O只需用到五个函数:openreadwritelseek 以及 close。这些函数通常被称为不带缓冲的I/O,它们直接通过系统调用与内核交互,是文件操作最底层的接口。


文件描述符

文件描述符是一个非负整数,它代表进程与已打开文件之间的连接。每个进程启动时,系统会自动分配三个标准描述符:

  • 0 ------ 标准输入
  • 1 ------ 标准输出
  • 2 ------ 标准错误

当进程调用 opencreat 打开新文件时,内核会返回当前未被使用的最小描述符编号。每个进程能够打开的文件描述符数量有限,可通过 ulimit -n 查看,并可以通过 setrlimit 调整上限。

在内核中,文件描述符是进程文件描述符表(files_struct)的索引,该表项指向内核中的文件对象(struct file)。文件对象中保存了当前文件偏移量、文件状态标志(如只读/只写)、指向inode的指针以及一组文件操作函数。多个描述符可以指向同一个文件对象(例如通过 dup 复制),此时它们共享相同的文件偏移量。


open 与 openat

open 函数

c 复制代码
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);

flags 参数必须包含以下三种之一:O_RDONLYO_WRONLYO_RDWR。其他常用标志包括:

  • O_CREAT ------ 文件不存在则创建
  • O_EXCL ------ 与 O_CREAT 配合,若文件已存在则报错
  • O_TRUNC ------ 打开时清空文件内容
  • O_APPEND ------ 每次写入追加到文件末尾
  • O_NONBLOCK ------ 非阻塞模式

mode 参数仅在创建文件时生效,用于指定文件权限(如 0644),实际权限会受到进程 umask 的影响(最终权限 = mode & ~umask)。

openat 函数

c 复制代码
int openat(int dirfd, const char *pathname, int flags, mode_t mode);

openatopen 的区别在于多了一个 dirfd 参数。当 pathname 为相对路径时,系统会根据 dirfd 指向的目录来解析路径;若 pathname 为绝对路径,则 dirfd 被忽略。如果希望使用当前工作目录,可以将 dirfd 设为 AT_FDCWD

openat 主要带来两个好处:

  • 线程安全 :在多线程程序中,若一个线程调用 chdir 改变工作目录,其他线程的 open 可能受到影响。openat 通过目录描述符固定父目录,避免这种竞态。
  • 避免 TOCTTOU 攻击 :在检查文件属性后再打开文件时,路径可能被恶意篡改(如符号链接攻击)。openat 基于已打开的目录句柄操作,降低了此类风险。

creat

c 复制代码
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);

creat 等价于 open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode)。它只能以只写方式创建文件,并且如果文件已存在,会将其内容截断为空。

由于 open 可以组合更多标志(如同时指定读权限、使用 O_EXCL 等),creat 在现代编程中已较少使用,但出于历史兼容性,POSIX 仍然保留该函数。


read

c 复制代码
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

read 从当前文件偏移量开始读取数据,读取后偏移量向后移动实际读到的字节数。返回值表示实际读取的字节数:

  • 返回 0 表示到达文件末尾(EOF)
  • 返回正数表示读取的字节数,可能小于 count(例如剩余数据不足)
  • 返回 -1 表示出错,可通过 errno 获取具体错误

注意事项

  • 如果调用被信号中断,read 可能返回部分已读数据,此时 errnoEINTR,需要自行处理重试。
  • 不能假设一次 read 能读满 count 个字节,应当循环读取直到读完所需数据或遇到 EOF/错误。

性能建议 :单次 readcount 值不宜过小(否则系统调用频繁),也不宜过大(浪费内存)。通常选择 4KB 或 8KB,与文件系统块大小对齐效果更佳。


write

c 复制代码
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

write 从当前文件偏移量开始写入数据,写入后偏移量相应增加。返回值表示实际写入的字节数,正常情况下等于 count,但可能因为磁盘空间不足、信号中断等原因导致部分写入。

重要特性

  • 对于普通文件,若写入的字节数小于 PIPE_BUFwrite 是原子操作,不会与其他进程或线程的写入交错。
  • 数据通常先被拷贝到内核的页缓存(page cache)中,随后由内核异步刷入磁盘,因此 write 返回并不代表数据已安全落盘。

处理部分写入:虽然普通文件很少出现部分写入,但在套接字或管道上可能发生。可靠的代码应使用循环写入:

c 复制代码
size_t total = 0;
while (total < count) {
    ssize_t ret = write(fd, buf + total, count - total);
    if (ret == -1) {
        if (errno == EINTR) continue;
        else break; // 真正的错误
    }
    total += ret;
}

lseek

c 复制代码
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

lseek 用于显式移动文件偏移量,它本身不产生任何 I/O 操作。whence 参数指定偏移的基准位置:

  • SEEK_SET ------ 从文件开头偏移 offset 字节
  • SEEK_CUR ------ 从当前偏移量增加 offset(可正可负)
  • SEEK_END ------ 从文件末尾偏移 offset(通常为正数表示向后扩展)

常见用法

  • 获取当前偏移量:off_t cur = lseek(fd, 0, SEEK_CUR);
  • 获取文件大小:off_t size = lseek(fd, 0, SEEK_END);(更推荐使用 fstat
  • 创建空洞文件:将偏移量移到超出文件末尾的位置,然后写入少量数据,文件会变大但实际占用的磁盘块可能很少(稀疏文件)。

需要注意的是,管道、套接字等特殊文件不支持随机访问,调用 lseek 会返回 -1 并设置 errnoESPIPE


缓存机制

页缓存

Linux 内核通过页缓存(Page Cache)来提升文件 I/O 性能。当进程调用 read 时,内核会先检查所需数据是否已在缓存中,若在则直接拷贝到用户空间,否则从磁盘读取并缓存。write 操作先将数据写入页缓存,并标记该页为"脏页",之后由内核线程(如 flush)异步将脏页写回磁盘。

这种机制大幅减少了磁盘访问次数,但也带来了数据丢失的风险------如果系统崩溃,尚未刷盘的缓存数据会永久丢失。

同步缓存的方法

  • fsync(int fd) ------ 同步文件数据和元数据(如修改时间、文件大小)到磁盘,阻塞直到完成。
  • fdatasync(int fd) ------ 仅同步数据,不同步元数据(除非元数据影响后续数据的读取),性能略优于 fsync
  • sync(void) ------ 将所有脏缓存同步到磁盘,属于全局操作,不针对特定文件。

打开标志对缓存的影响

  • O_SYNC ------ 每次 write 都会阻塞直到数据真正写入磁盘(相当于自动调用 fsync),性能较差,但保证了持久性。
  • O_DSYNC ------ 类似 O_SYNC,但只同步数据(类似 fdatasync)。
  • O_DIRECT ------ 绕过页缓存,直接进行磁盘 I/O(要求数据对齐),常用于数据库等自行管理缓存的应用,但使用门槛较高。

用户态缓冲 vs 内核缓冲

需要区分两类缓冲:

  • 不带缓冲的 I/O(如本文所述)------ 系统调用直接操作内核页缓存,用户态没有额外缓冲。
  • 标准 I/O 库 (如 fopenfread)------ 在用户态维护缓冲区,攒够一定数据后再调用 write,从而减少系统调用次数,提高效率。

因此,write 并不意味着"立即写磁盘",而是"写入内核缓存";而 fwrite 则是"写入用户缓存" → 再通过 write 送入内核缓存 → 最后异步刷盘。

相关推荐
小此方1 小时前
Linux加餐(一):藏在Linux中的设计模式(一)策略模式与日志
linux·设计模式·策略模式
邪修king1 小时前
Re:Linux系统篇(十):从零上手 Git + GitHub(Ubuntu 环境实操完整版|个人代码归档必备)
linux·git·github
wuminyu1 小时前
JDK21中FFM api的upcall回调机制解析
java·linux·c语言·jvm·c++
蜀道山老天师2 小时前
Zabbix监控MySQL与Redis应用实践完整指南
linux·运维·redis·mysql·zabbix
OsDepK3 小时前
在安卓Termux终端中部署openssh
linux·服务器
动力 continue10 小时前
Linux 基础篇 · 压缩打包与用户管理
linux·服务器·基础命令linux
NJCloud12 小时前
Ansible(三)——Zabbix 监控系统部署与敏感信息加密
linux·运维·chrome·信息可视化·ansible·zabbix
ltl12 小时前
Cgroups v2:为什么内存限制设了,宿主机数据库还是被 OOM
linux
NJCloud12 小时前
TiDB 集群部署与 MySQL 兼容性适配实战
linux·运维·数据库·mysql·tidb