Linux文件IO

Linux文件IO

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


文件描述符

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

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

当进程调用 open 或 creat 打开新文件时,内核会返回当前未被使用的最小描述符编号。每个进程能够打开的文件描述符数量有限,可通过 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_RDONLY、O_WRONLY 或 O_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);

openat 与 open 的区别在于多了一个 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 可能返回部分已读数据,此时 errno 为 EINTR,需要自行处理重试。
  • 不能假设一次 read 能读满 count 个字节,应当循环读取直到读完所需数据或遇到 EOF/错误。

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


write

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

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

重要特性:

  • 对于普通文件,若写入的字节数小于 PIPE_BUF,write 是原子操作,不会与其他进程或线程的写入交错。
  • 数据通常先被拷贝到内核的页缓存(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 并设置 errno 为 ESPIPE。


缓存机制

页缓存

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 库 (如 fopen、fread)------ 在用户态维护缓冲区,攒够一定数据后再调用 write,从而减少系统调用次数,提高效率。

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

相关推荐
夜听莺儿鸣7 小时前
502-003_Linux 中断与异常(一):从Linux角度理解中断与异常
linux·中断与异常·linux中断
杨云龙UP8 小时前
TDengine 3.4.2.8 Community 三节点三副本生产集群部署实战(DNode/MNode/taosAdapter/Explorer)
大数据·linux·运维·数据库·tdengine·时序库
向成科技9 小时前
XC3576H工控主板|深度适配Ubuntu 26.04 LTS,释放边缘AI与工业开发新潜能
linux·人工智能·ubuntu·机器人·硬件·主板·边缘ai
Fcy6489 小时前
Linux下 进程间关系与守护进程
linux·运维·服务器·守护进程
码农小韩10 小时前
Linux驱动理论(二)——Linux字符设备驱动
linux·嵌入式软件开发·linux操作系统·linux应用开发·linux驱动理论
well061210 小时前
Linux粘滞位与Makefile机制深度解析
linux·运维·服务器
再写一行代码就下班10 小时前
linux sh脚本在windows修改导致无法使用解决方式
java·linux·centos
额额额对了10 小时前
SPI通信
linux·c语言·汇编·单片机·嵌入式硬件·arm
子木HAPPY阳VIP11 小时前
Ubuntu 关闭防火墙操作步骤
linux·运维·ubuntu
王振超wzc11 小时前
嵌入式开发环境搭建--WM软件安装,Ubuntu操作系统安装
linux·运维·ubuntu