Linux 系统编程 02:文件高级操作

📅 2026/6/30 4:17:13
Linux 系统编程 02:文件高级操作
前言承接上一篇基础文件 IO 的核心系统调用本篇深入讲解 Linux 文件操作的四大高级特性文件重定向、文件锁、文件空洞与写入原子性。这些特性是 Shell 重定向、日志系统、多进程文件互斥、大文件预分配等场景的底层支撑也是嵌入式、后端开发岗位笔试面试的高频考点掌握后才能写出稳定、高效的多进程文件操作代码。一、文件重定向dup/dup2 底层原理1. 重定向的本质文件重定向的核心是修改进程文件描述符表的指向让原本指向终端的标准输入 / 输出 / 错误转而指向我们打开的目标文件。后续所有通过该文件描述符的读写操作都会作用到新文件上。Linux 提供了两个系统调用实现文件描述符的复制与重定向dup和dup2两者本质都是让多个文件描述符指向同一个文件表项共享文件偏移量、文件状态。2. dup 函数函数原型#include unistd.h int dup(int oldfd);功能复制oldfd对应的文件描述符返回一个新的文件描述符两者指向同一个文件表项返回值成功返回当前可用的最小编号 fd失败返回 - 1 并设置 errno分配规则遵循最小可用原则自动分配当前未使用的最小编号3. dup2 函数函数原型#include unistd.h int dup2(int oldfd, int newfd);功能强制将newfd重定向指向oldfd对应的文件。如果newfd已经打开会先自动关闭再重定向核心特性整个关闭 重定向的操作是原子的中间不会被打断这是它和手动 closedup 的本质区别返回值成功返回新的文件描述符即 newfd失败返回 - 14. 实战实现标准输出重定向#include stdio.h #include unistd.h #include fcntl.h #include errno.h #include string.h int main(void) { int fd open(log.txt, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd -1) { perror(open file failed); return 1; } // 将1号标准输出重定向到log.txt if (dup2(fd, STDOUT_FILENO) -1) { perror(dup2 failed); close(fd); return 1; } close(fd); // 重定向完成后原fd可关闭不影响输出 // 后续所有标准输出都会写入文件 printf(这句话会写入log.txt文件\n); printf(标准输出已被重定向\n); return 0; }底层原理内核中每个文件表项都有引用计数。多个 fd 指向同一个文件时引用计数累加。关闭其中一个 fd 只是让引用计数减一不会真正关闭文件因此关闭原 fd 后标准输出依然正常工作。5. dup 与 dup2 的核心区别对比维度dupdup2fd 分配方式自动分配最小可用 fd手动指定目标 fd 编号原子性仅复制操作是原子的关闭旧 fd 重定向整体是原子的适用场景复制 fd 保存备份实现标准输入输出重定向竞态风险手动配合 close 使用时有竞态原子操作无竞态风险二、文件锁多进程文件互斥方案1. 为什么需要文件锁当多个进程同时读写同一个文件时会出现数据竞争比如两个进程同时向文件末尾追加数据可能出现数据互相覆盖、内容错乱的问题。文件锁就是用来实现多进程间文件操作的互斥与同步保证同一时间只有一个进程执行写操作。2. 劝告锁与强制锁Linux 的文件锁分为两类默认使用劝告锁劝告锁建议性锁内核只负责记录锁的状态不强制阻止写操作。所有进程必须主动申请锁、遵守锁的约定才能生效相当于 “君子协定”。强制锁内核会强制检查持有写锁时其他进程的读写操作都会被阻塞。需要文件系统挂载时开启强制锁支持实际工程中很少使用。3. fcntl 实现文件锁fcntl是文件控制的多功能系统调用最核心的用途之一就是实现文件锁支持字节范围的读写锁。函数原型#include fcntl.h int fcntl(int fd, int cmd, ... /* struct flock *arg */ );锁相关的 cmd 参数F_SETLK非阻塞加锁 / 解锁加锁失败立刻返回 - 1F_SETLKW阻塞加锁加锁失败则挂起等待直到锁可用F_GETLK查询文件上的锁信息判断是否能加锁锁结构体 flockstruct flock { short l_type; // 锁类型F_RDLCK读锁、F_WRLCK写锁、F_UNLCK解锁 short l_whence; // 偏移基准SEEK_SET/SEEK_CUR/SEEK_END off_t l_start; // 加锁区域起始偏移 off_t l_len; // 加锁区域长度0表示到文件末尾 pid_t l_pid; // 持有锁的进程IDF_GETLK时返回 };读写锁特性读锁共享锁多个进程可以同时持有读锁互不阻塞适用于多读场景写锁独占锁同一时间只能有一个进程持有写锁写锁和读锁、其他写锁都互斥4. 实战加写锁保护文件写入#include stdio.h #include unistd.h #include fcntl.h #include string.h // 阻塞加写锁锁定整个文件 int file_wrlock(int fd) { struct flock lock; lock.l_type F_WRLCK; lock.l_whence SEEK_SET; lock.l_start 0; lock.l_len 0; // 0表示锁定整个文件 return fcntl(fd, F_SETLKW, lock); } // 解锁 int file_unlock(int fd) { struct flock lock; lock.l_type F_UNLCK; lock.l_whence SEEK_SET; lock.l_start 0; lock.l_len 0; return fcntl(fd, F_SETLKW, lock); } int main(void) { int fd open(data.txt, O_WRONLY | O_CREAT | O_APPEND, 0644); if (fd -1) { perror(open failed); return 1; } // 加写锁多进程下同一时间只有一个进程能进入 if (file_wrlock(fd) -1) { perror(lock failed); close(fd); return 1; } // 临界区写入文件 const char *msg 进程安全写入的数据\n; write(fd, msg, strlen(msg)); // 解锁 file_unlock(fd); close(fd); return 0; }注意事项文件锁是和进程绑定的不是和文件描述符绑定。同一个进程的多个 fd 指向同一个文件锁是共享的进程退出或关闭所有关联 fd 后锁会自动释放。三、文件空洞稀疏文件与 lseek 进阶1. 什么是文件空洞文件空洞也叫稀疏文件指的是文件的逻辑大小远大于实际占用的磁盘空间文件中全为 0 的空洞区域不占用实际的磁盘块读取空洞区域会返回 0。它的本质是文件系统只为实际写入数据的区域分配磁盘块跳过的空白区域只记录在元数据中不占用真实存储空间。2. 文件空洞的生成方式通过lseek将文件指针移动到文件末尾之后的位置再执行write写入数据中间跳过的区域就会形成文件空洞。3. 实战创建带空洞的稀疏文件#include stdio.h #include unistd.h #include fcntl.h #include string.h int main(void) { int fd open(sparse_file.bin, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd -1) { perror(open failed); return 1; } // 写入开头的1KB数据 const char *head head data; write(fd, head, strlen(head)); // 指针向后偏移100MB跳过中间区域 lseek(fd, 100 * 1024 * 1024, SEEK_CUR); // 写入末尾的1KB数据 const char *tail tail data; write(fd, tail, strlen(tail)); close(fd); printf(稀疏文件创建完成逻辑大小约100MB实际占用极小\n); return 0; }执行后可以通过命令验证ls -l sparse_file.bin查看逻辑大小约 100MBdu -h sparse_file.bin查看实际磁盘占用只有几 KB4. 典型应用场景虚拟机磁盘镜像虚拟机磁盘标称 100G实际系统只占用 10G镜像文件就只占 10G 空间数据库大文件预分配大文件空间避免运行时扩容的性能波动下载大文件边下边存先创建完整大小的稀疏文件再逐步填充数据日志预分配提前预留日志文件空间避免磁盘满时无法写入注意普通的文件拷贝会把空洞区域填满 0导致稀疏文件变成普通文件占用全部磁盘空间。需要使用支持稀疏拷贝的工具如cp --sparsealways才能保留空洞。四、写入原子性O_APPEND 与原子读写1. 什么是原子操作原子操作指的是一个操作要么完整执行要么完全不执行执行过程中不会被其他进程 / 线程打断中间状态对外不可见。文件操作中原子性是保证多进程并发安全的核心基础。2. 普通写入的竞态问题如果不用O_APPEND追加写入需要两步lseek定位到文件末尾write写入数据这两步是两个独立的系统调用中间可能被其他进程打断。比如两个进程同时追加写进程 A 定位到末尾还没写入进程 B 定位到同一个位置写入数据进程 A 再写入就会覆盖进程 B 刚写入的内容导致数据丢失3. O_APPEND 的原子追加机制打开文件时加上O_APPEND标志后每次执行write之前内核会自动将文件指针定位到文件末尾定位 写入是一个原子操作中间不会被其他进程打断。这是多进程日志系统最常用的方案所有进程都以追加模式打开日志文件各自写入不会互相覆盖保证日志内容完整。注意O_APPEND保证的是追加位置的原子性不会覆盖已有内容但不保证单次 write 的内容是连续的。写入数据量超过PIPE_BUF时数据可能被拆分多次写入只是不会覆盖其他进程的内容。4. pread/pwrite原子偏移读写除了追加写Linux 还提供了原子性的指定位置读写函数读写操作不改变文件指针的位置。函数原型#include unistd.h ssize_t pread(int fd, void *buf, size_t count, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);功能在指定的offset偏移处读写数据不修改文件原有的读写指针位置原子性相当于lseek read/write合并为一个原子操作中间不会被打断适用场景多线程操作同一个文件每个线程读写不同位置不需要互斥保护文件指针五、fcntl 函数文件属性动态修改fcntl是文件操作的 “瑞士军刀”可以动态修改已打开文件的各种属性不需要重新 open 文件除了文件锁之外还有两个高频用法1. 获取 / 设置文件状态标志通过F_GETFL和F_SETFL可以读取、修改文件的打开标志最常用于动态切换非阻塞模式。// 获取文件状态标志 int flags fcntl(fd, F_GETFL); if (flags -1) { perror(fcntl get failed); } // 添加非阻塞标志不改变原有标志 flags | O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) -1) { perror(fcntl set failed); }注意F_SETFL只能修改O_APPEND、O_NONBLOCK等少数标志O_RDONLY、O_CREAT等基础标志无法动态修改。2. 设置执行时关闭标志FD_CLOEXEC标志表示进程执行 exec 替换程序时自动关闭该文件描述符避免泄露给新程序。这是安全编程的规范写法。六、面试高频考点与易错坑点1. 经典面试问答Q1dup 和 dup2 有什么核心区别为什么推荐用 dup2 实现重定向答分配方式不同dup 自动分配最小可用的新 fddup2 可以手动指定目标 fd 的编号。原子性不同dup2 的关闭旧 fd 重定向整体是原子操作中间不会被打断如果手动 closedup 实现重定向中间可能被信号或其他线程抢占出现竞态问题。 实现重定向需要指定目标 fd如 1 号标准输出且必须保证原子性因此优先用 dup2。Q2什么是文件空洞它是怎么生成的有什么应用价值答文件空洞是逻辑大小大于实际磁盘占用的稀疏文件空洞区域全为 0不占用真实磁盘块。生成方式用 lseek 将文件指针移到文件末尾之后再执行 write 写入中间跳过的区域就形成空洞。应用价值节省磁盘空间适合大文件预分配、虚拟机镜像、数据库文件等场景。Q3O_APPEND 为什么能保证多进程追加写不覆盖答 开启 O_APPEND 后每次 write 执行前内核会自动将文件指针定位到文件末尾定位和写入是一个原子操作中间不会被其他进程打断。 而普通的 lseekwrite 是两步独立操作中间可能被其他进程抢占导致写入位置错误覆盖已有数据。Q4文件锁有哪几种劝告锁和强制锁有什么区别答分为劝告锁和强制锁Linux 默认是劝告锁。劝告锁是建议性的内核只记录锁状态不强制阻止读写需要所有进程主动遵守锁约定才能生效。强制锁由内核强制检查持有写锁时其他进程的读写都会被阻塞但需要文件系统特殊挂载支持实际使用很少。Q5pread/pwrite 和普通的 lseekread/write 有什么区别答原子性pread/pwrite 是原子操作定位 读写中间不会被打断lseekread 是两步操作有竞态风险。文件指针pread/pwrite 在指定偏移处操作不会改变文件原有的读写指针位置普通读写会移动指针。适用场景pread/pwrite 更适合多线程操作同一个文件的不同位置不需要额外互斥。2. 常见易错坑点误以为 dup2 会关闭原 fd实际关闭的是目标 newfdoldfd 保持不变需要手动关闭手动 closedup 实现重定向认为和 dup2 等价忽略了原子性多进程 / 多线程下存在竞态漏洞以为文件锁能强制阻止其他进程写文件忽略了 Linux 默认是劝告锁必须所有进程都主动加锁才有效认为 O_APPEND 能保证单次 write 的内容完全连续实际上超过 PIPE_BUF 的写入依然可能拆分只是不会覆盖其他内容普通方式拷贝稀疏文件导致空洞被 0 填满磁盘占用从几 KB 变成上百 MB混淆文件锁的绑定对象以为锁和 fd 绑定实际和进程绑定同一个进程多个 fd 共享同一把锁用 F_SETFL 修改所有文件标志实际上只读 / 只写等基础标志无法动态修改设置无效以上就是 Linux 文件高级操作的全部核心内容掌握这些特性就能处理绝大多数多进程、高性能的文件操作场景。下一篇我们将讲解目录与文件属性操作深入 inode、软硬链接、目录遍历等核心知识点。制作不易如果对你有用希望能点赞收藏支持一下。