💡 核心观点
Linux 文件锁是进程同步的基石,通过 flock()、fcntl() 和 lockf() 三大函数实现。它们分别提供建议性锁与强制性锁,确保多进程环境下对共享文件的读写安全。理解它们的差异与适用场景,是构建稳定系统的关键。
在多进程并发访问同一文件的场景中,不加锁可能导致数据混乱、写入冲突等问题。Linux 系统提供了三种文件锁机制:flock()、fcntl() 和 lockf()。它们各有特点,适用于不同需求。
一、flock 函数:轻量级的建议性锁
flock() 是 Linux 中最简单的文件锁函数,它提供建议性锁(advisory locking),即锁仅对遵守相同锁协议的进程有效,不强制其他进程遵守。
📌 函数原型
#include<sys/file.h>intflock(int fd, int operation);
参数 fd 为文件描述符,operation 指定操作方式:
🔑 核心要点
1LOCK_SH:共享锁,多个进程可同时持有,适合读操作。
2LOCK_EX:排它锁(互斥锁),仅一个进程可持有,适合写操作。
3LOCK_UN:解锁。
4LOCK_NB:非阻塞模式,与 LOCK_SH 或 LOCK_EX 组合使用,避免阻塞等待。
⚠️ 注意:flock() 仅支持建议性锁,不强制其他进程遵守。这意味着如果进程未检查锁状态,仍可读写文件,可能导致数据不一致。
排它锁(写锁)使用示例
进程 1 先获取排它锁并保持,进程 2 尝试获取锁但失败,但进程 2 仍可读写文件(建议性锁的弊端)。
进程1(加锁并等待信号):#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/file.h>#include<signal.h>...flock(fd, LOCK_EX | LOCK_NB); // 非阻塞加排它锁...进程2(尝试加锁但失败):flock(fd, LOCK_EX | LOCK_NB); // 返回错误,但可正常读写write(fd, buf, len); // 仍可写入lseek(fd, 0, SEEK_SET);read(fd, buf, len); // 仍可读取
"建议性锁如同君子协定,只约束守规矩的进程。"
—— Linux 编程实践
共享锁使用示例
多个进程可同时持有共享锁,但排它锁会阻塞共享锁的获取。
📋 操作步骤
1.进程1 获取共享锁(LOCK_SH)
2.进程2 也获取共享锁(成功)
3.进程3 尝试获取排它锁(LOCK_EX),但被阻塞,直到进程1和2释放锁
flock 规则总结
📌 关键规则
• 同一进程多次加锁不会死锁,新锁替换旧锁
• 文件关闭时自动解锁,进程终止时全部释放
• 一个进程不能解锁另一个进程的锁
• fork() 创建的子进程会继承父进程的锁
• 复制文件描述符(dup/dup2/fcntl)后,所有副本共享同一锁,关闭所有副本才释放
二、fcntl 函数:强大的多功能锁
fcntl() 不仅支持建议性锁,还可配置为强制性锁(mandatory locking),且支持对文件区域的精细锁定。
📌 函数原型与结构体
#include<unistd.h>#include<fcntl.h>intfcntl(int fd, int cmd, struct flock *flockptr);struct 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; /* 持有锁的进程ID(F_GETLK时填充) */};
🔑 核心命令
1F_GETLK:测试锁,检查指定锁是否会被阻塞,不实际加锁。
2F_SETLK:加锁或解锁,非阻塞,失败返回 EACCES 或 EAGAIN。
3F_SETLKW:阻塞版 F_SETLK,等待锁可用。
使用示例:锁定整个文件
intlock_file(int fd) { struct flock fl; memset(&fl, 0, sizeof(fl)); fl.l_type = F_WRLCK; // 写锁 fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 0; // 0 表示到文件尾 return fcntl(fd, F_SETLK, &fl);}voidunlock_file(int fd) { struct flock fl; memset(&fl, 0, sizeof(fl)); fl.l_type = F_UNLCK; fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 0; fcntl(fd, F_SETLK, &fl);}
⚠️ 注意:fcntl() 默认也是建议性锁。要启用强制性锁,需要:
1. 文件系统挂载时使用 -o mand 选项
2. 对目标文件执行 chmod g+s,g-x /path/to/file
强制性锁会强制所有进程遵守,包括未显式加锁的进程。
三、lockf 函数:简化版排它锁
lockf() 是 fcntl() 的简化封装,仅支持排它锁,更适合简单场景。
📌 函数原型
#include<unistd.h>intlockf(int fd, int cmd, off_t len);
cmd 参数:
• F_LOCK:阻塞模式加锁
• F_TLOCK:非阻塞模式加锁
• F_ULOCK:解锁
• F_TEST:测试锁(不实际加锁)
len 表示锁定区域长度,从当前文件偏移量开始,若 len=0 表示扩展到文件末尾。
⚠️ 注意:fd 必须以可写模式(O_WRONLY 或 O_RDWR)打开,否则 lockf() 会失败。
四、三大函数对比
特性 | flock() | fcntl() | lockf() |
锁类型 | 共享锁 + 排它锁 | 读锁 + 写锁 | 仅排它锁 |
锁定粒度 | 整个文件 | 文件区域 | 文件区域 |
强制性锁 | 不支持 | 支持(需配置) | 同 fcntl |
fork 继承 | 子进程继承 | 子进程不继承 | 同 fcntl |
复杂度 | 低 | 高 | 中 |
✅ 总结与行动建议
• 简单场景(整个文件锁定)优先使用 flock(),代码简洁高效。
• 需要区域锁定或强制性锁时,选择 fcntl(),功能最强大。
• 仅需排它锁且希望接口简洁时,lockf() 是很好的选择。
• 记住:建议性锁需要所有进程协作;强制性锁虽强,但会带来性能开销。
• 生产环境中,推荐使用 fcntl() 的区域锁,配合非阻塞模式,避免死锁和性能瓶颈。