多进程共享文件时需要互斥。Linux 提供三代文件锁:flock(BSD)、fcntl POSIX 锁、OFD 锁。前两代各有语义地雷。
一、先看图
flowchart TD
FLOCK[flock 锁<br/>per-file 粒度] --> BSD[BSD 语义<br/>fork 后子进程<br/>不继承]
POSIX[POSIX fcntl 锁<br/>byte-range] --> POSIXSEM[per-process 语义<br/>close 任一 fd<br/>释放所有锁!]
OFD[OFD 锁<br/>F_OFD_SETLK<br/>byte-range] --> OFDSEM[per-open-file 语义<br/>安全]
classDef safe fill:#3fb95022,stroke:#3fb950,color:#adbac7;
classDef danger fill:#f8514922,stroke:#f85149,color:#adbac7;
classDef ok fill:#388bfd22,stroke:#388bfd,color:#adbac7;
class FLOCK ok
class POSIX,POSIXSEM danger
class OFD,OFDSEM safe
二、flock(BSD 锁)
int fd = open("/tmp/data", O_RDWR);
flock(fd, LOCK_EX); // 独占锁
// ... 操作文件
flock(fd, LOCK_UN); // 解锁特点:
- 粒度:整个文件(无 byte-range)
- 关联:per-open-file-description(dup 后同一把锁,fork 后不继承)
- 简单、语义清晰
缺点:无 byte-range → 只能锁整个文件。
NFS:早期 flock 在 NFS 上是 noop!现代 NFS v4 已修复。
三、POSIX fcntl 锁
struct flock fl = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 1024, // 锁前 1024 字节
};
fcntl(fd, F_SETLK, &fl);特点:
- 粒度:byte-range
- 关联:per-process(!)
致命陷阱
close 任何一个指向同一文件的 fd 都会释放该进程持有的所有锁。
int fd1 = open("/tmp/data", O_RDWR);
fcntl(fd1, F_SETLK, &lock); // 拿锁
int fd2 = open("/tmp/data", O_RDONLY);
close(fd2); // 锁被释放了!这违反直觉,但是 POSIX 标准规定。
fork 后子进程不继承锁(因为是 per-process)。
四、OFD 锁(Open File Description Lock)
3.15+ 引入,修复 POSIX 锁的问题:
struct flock fl = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 1024,
.l_pid = 0, // OFD 锁忽略 pid
};
fcntl(fd, F_OFD_SETLK, &fl);特点:
- 粒度:byte-range ✓
- 关联:per-open-file-description(像 flock)
- close 另一个 fd 不影响 ✓
- fork 后子进程继承(因为共享 struct file)
推荐:新代码应该用 OFD 锁,不用 POSIX fcntl 锁。
五、lease(租约)
fcntl(fd, F_SETLEASE, F_WRLCK);含义:当其他进程打开这个文件时,内核给 lease 持有者发 SIGIO → 有时间窗做清理(如 Samba 的 oplock)。
用途:
- Samba/NFS 的 oplock / delegation
- 防止竞争打开
六、mandatory lock(已退出)
mount -o mand /dev/sda1 /mnt
chmod g+s,g-x /mnt/file // 设置 mandatory lock 标志强制锁——没拿锁的进程 read/write 会被阻塞。
Linux 5.15 已删除 mandatory lock 支持:
- 性能差
- 语义怪异
- 安全隐患(DoS)
七、NFS 上的文件锁
NFS v3:lockd + rpc.statd
管理锁状态。客户端重启后需要 reclaim。
NFS v4:锁集成到协议里。lease-based。
NFSv4 的 delegation 类似 lease——server 委托客户端独占 cache。
八、死锁检测
POSIX 锁支持死锁检测:
fcntl(fd, F_SETLKW, &fl); // 等待模式 + 死锁检测
// 如果检测到死锁返回 EDEADLKflock 和 OFD 锁不做死锁检测。
九、选择指南
| 需求 | 推荐 |
|---|---|
| 简单互斥 | flock |
| byte-range、多线程 | OFD 锁 |
| NFS 兼容 | fcntl POSIX(NFS 原生支持) |
| 遗留代码 | fcntl POSIX(注意陷阱) |
| 新代码 | OFD 锁 |
十、观察
# 查看进程持有的锁
cat /proc/locks
# 1: POSIX ADVISORY WRITE 1234 08:01:12345 0 1023
# 2: FLOCK ADVISORY WRITE 5678 08:01:12345 0 EOF
# lslocks 友好版
lslocks十一、小结
- flock 简单但只能锁整文件
- POSIX fcntl 锁有 close 陷阱——close 任一同文件 fd 释放所有锁
- OFD 锁修复了 POSIX 的设计缺陷,推荐新代码使用
- mandatory lock 已删除
- NFS 锁有专门机制(lockd / NFSv4 delegation)
参考文献
Documentation/filesystems/locking.rst- Jeff Layton, “File locking: the current state.” LWN.net 2014
man 2 flock、man 2 fcntl(F_OFD_SETLK 段落)fs/locks.c
工具
/proc/lockslslocksstrace -e flock,fcntlflock(1)shell 工具
上一篇:fd 表与 struct file 下一篇:xattr/ACL/capabilities
文章导航
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
CPU 调度:CFS 的虚拟运行时间与 EEVDF 的虚拟截止期
从比例份额调度的论文谱系出发,对照 Linux v6.6 与 v6.12 fair.c 源码,解释 CFS 的 vruntime、EEVDF 的 lag/eligible/deadline,并用确定性模拟复现延迟与 lag 的差异。
I/O 调度:在寻道、队列深度与公平性之间取舍
从电梯算法到 blk-mq,解释 Linux I/O 调度器为何在 HDD 上排序、在共享设备上保公平、在 NVMe 上常选择 none,并用确定性模拟展示寻道距离与队列尾延迟的取舍。
用户态内存分配器:size class、线程缓存与碎片边界
从 Wilson 分配器综述到 jemalloc、gperftools tcmalloc 与 mimalloc 的源码路径,解释 size class、线程缓存、跨线程释放和 RSS 碎片;用可复现实验说明不能只凭 allocator 名字下结论。
伙伴系统与 SLUB:Linux 物理页和小对象分配的边界
以 Linux v6.6 源码和确定性模拟为准,解释伙伴系统的 split/coalesce、migratetype 反碎片策略,以及 SLUB 的 per-CPU freelist、partial list 与安全加固。