第 55 章 文件锁 (File Locking)
核心结论
-
两种文件锁 API:flock()(BSD 派,整文件 shared/exclusive 锁)和 fcntl()(SysV 派,字节区间记录锁);SUSv3 只标准化 fcntl。
-
flock() 全文件锁:LOCK_SH(共享,多进程可同时)、LOCK_EX(排他,独占)、LOCK_UN(解锁)、LOCK_NB(非阻塞);锁关联 open file description(dup 的 fd 共享同一锁)。
-
fcntl() 字节区间锁:F_RDLCK(读/共享)、F_WRLCK(写/排他)、F_UNLCK(解锁);F_SETLK(非阻塞)、F_SETLKW(阻塞)、F_GETLK(探测);l_whence/l_start/l_len 指定字节范围。
-
advisory vs mandatory:默认 advisory(需进程合作);mandatory 需 mount -o mand + 文件 setgid+off-exec 位——内核强制 I/O 检查(不推荐用,有死锁风险)。
-
fcntl 锁语义:不跨 fork 继承、跨 exec 保留;同进程所有 fd 共享锁集;关闭任一 fd 即释放该进程在此文件上的所有锁(与 flock 不同)。
-
死锁检测:F_SETLKW 检测到死锁返回 EDEADLK(内核选最近 fcntl 调用的进程失败);mandatory 锁的 I/O 也可能 EDEADLK。
-
/proc/locks:查看所有锁——类型(POSIX/FLOCK)+ 模式(ADVISORY/MANDATORY)+ 读写 + PID + 文件 + 字节范围;
→表示阻塞请求。 -
典型应用:守护进程唯一实例——写
/var/run/daemon.pid+ fcntl F_WRLCK 锁整个文件;运行第二个实例时 lockRegion 失败退出。
|
本章主旨
文件锁解决「多个进程同时更新文件」的竞争——核心是「读-改-写」原子性。读者需要建立四组对比:(1) flock vs fcntl——flock 简单但只能锁整文件;fcntl 灵活但语义复杂;(2) advisory vs mandatory——advisory 需合作,mandatory 强制但有风险;(3) 锁 vs 其他 IPC——锁与文件绑定,比 sem+文件方便;(4) 字节区间 vs 全文件——flock 粒度粗,fcntl 粒度细。fcntl 的关键语义陷阱是「关闭任一 fd 释放所有锁」——库函数用 fcntl 锁文件易被调用者 close 误删。 |
一、核心概念
本章围绕 6 个核心概念展开:两种锁 API、advisory/mandatory、字节范围、死锁检测、锁的继承与释放、/proc/locks。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
flock() 整文件锁 |
|
§55.2;LOCK_SH 多进程共享;LOCK_EX 独占;LOCK_NB 非阻塞;转换非原子(先解再锁) |
fcntl() 字节区间锁(记录锁) |
|
§55.3;F_RDLCK/F_WRLCK/F_UNLCK;l_len=0 表示到 EOF;F_GETLK 探测(不真锁);F_SETLKW 阻塞 |
advisory vs mandatory |
默认 advisory(进程可忽略锁直接 I/O);mandatory 需 mount -o mand + 文件 |
§55.4;mandatory 可被恶意利用死锁 DoS;不推荐;read/write 阻塞或 EAGAIN(O_NONBLOCK) |
fcntl 锁继承与释放 |
不跨 fork 继承;跨 exec 保留(除非 FD_CLOEXEC);同进程所有 fd 共享锁集;关闭任一 fd 释放该进程在此文件的所有锁 |
§55.3.5;与 flock 截然不同——flock 锁关联 open file description;fcntl 锁关联 process + i-node |
死锁检测 |
F_SETLKW 检测循环锁依赖——内核让最近 fcntl 进程失败返回 EDEADLK;可跨多文件多进程 |
§55.3.1;mandatory 锁下 read/write 也可能 EDEADLK;处理 EINTR(信号打断) |
/proc/locks 调试 |
查看所有进程持有的锁;字段:序号 + 类型(POSIX/FLOCK)+ 模式(ADVISORY/MANDATORY)+ 读写 + PID + 文件 + 起始字节 + 结束字节 |
§55.5; |
二、详细笔记
55.1 flock() 整文件锁
What:flock(fd, operation) 对整文件加 shared/exclusive 锁。
Why:比 fcntl 简单——无需 flock 结构;锁与 open file description 关联(dup 的 fd 共享)。
How:
// 摘自《The Linux Programming Interface》 第 55 章
#include <sys/file.h>
int flock(int fd, int operation);
#define LOCK_SH 1 /* shared lock */
#define LOCK_EX 2 /* exclusive lock */
#define LOCK_NB 4 /* nonblocking */
#define LOCK_UN 8 /* unlock */
/* 阻塞加排他锁 */
flock(fd, LOCK_EX);
/* 非阻塞加共享锁(已锁则失败 EWOULDBLOCK) */
if (flock(fd, LOCK_SH | LOCK_NB) == -1) {
if (errno == EWOULDBLOCK)
/* 已锁 */
}
/* 解锁 */
flock(fd, LOCK_UN);
兼容性矩阵(表 55-2):LOCK_SH + LOCK_SH = 允许;LOCK_EX + 任何 = 拒绝。
When:(1) 简单整文件锁——首选 flock(API 简洁);(2) 需字节区间——用 fcntl;(3) 多进程读同一文件——共享锁。
Example:第 55 章 t_flock tfile s 60 后台持 60 秒共享锁;前台 t_flock tfile xn 立即 EWOULDBLOCK 失败。
55.2 fcntl() 字节区间锁
What:fcntl(fd, cmd, &flock) 对 [l_whence+l_start, l_whence+l_start+l_len) 加锁。
Why:可锁任意字节范围——支持「锁一条记录」「锁一个段」;提高并发度。
How:
// 摘自《The Linux Programming Interface》 第 55 章
#include <fcntl.h>
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 表示到 EOF */
pid_t l_pid; /* F_GETLK 返回持锁 PID */
};
/* 加写锁整个文件 */
struct flock fl = { .l_type = F_WRLCK, .l_whence = SEEK_SET,
.l_start = 0, .l_len = 0 };
if (fcntl(fd, F_SETLK, &fl) == -1) errExit("fcntl");
/* 探测是否能加锁 */
if (fcntl(fd, F_GETLK, &fl) == -1) errExit("fcntl");
if (fl.l_type == F_UNLCK) printf("Lock can be placed\n");
else printf("Denied by %s lock on %lld:%lld (held by PID %ld)\n",
fl.l_type == F_RDLCK ? "READ" : "WRITE",
(long long)fl.l_start, (long long)fl.l_len, (long)fl.l_pid);
/* 阻塞加锁 */
fcntl(fd, F_SETLKW, &fl); /* 可能 EINTR 或 EDEADLK */
/* 解锁 */
fl.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &fl);
When:(1) 字节区间锁——首选 fcntl;(2) 数据库记录锁——fcntl 字节区间;(3) 守护进程单实例——fcntl 锁整个 .pid 文件。
Example:第 55 章 i_fcntl_locking 命令行交互测试——s r 0 40 读锁 0-39;s w 50 0 写锁 50 到 EOF;g w 0 0 探测能否锁整个文件。
55.3 advisory vs mandatory
What:advisory 锁(默认)需进程合作(都调 fcntl/flock);mandatory 锁内核强制检查 I/O。
Why:mandary 适合「不能信任合作」的不可控进程场景;但有死锁/性能风险。
How:
mandatory 启用(两步):
-
mount 启用:
mount -o mand /dev/sda10 /testfs -
文件启用:
chmod g+s,g-x /testfs/file(setgid + 取消 group-execute)
mandatory I/O 行为:
| 场景 | 行为 |
|---|---|
阻塞 read/write 冲突锁 |
阻塞 |
非阻塞(O_NONBLOCK)冲突锁 |
EAGAIN |
open O_TRUNC 冲突锁 |
EAGAIN |
mmap MAP_SHARED 与锁冲突 |
EAGAIN |
When:(1) 多数情况——advisory(应用层合作足够);(2) 不可控进程访问——mandatory(不推荐);(3) 高性能 I/O——避免 mandatory(每次 I/O 检查锁)。
Example:chmod g+s,g-x /tmp/x 后 ls -l 显示 -rw-r-Sr--——mandatory 启用;并发 write 会因其他进程持读锁阻塞。
55.4 fcntl 锁的继承与释放陷阱
What:fcntl 锁关联 process + i-node——关闭任一 fd 释放该进程在此文件上的所有锁。
Why:库函数用 fcntl 锁文件时,调用者 close(fd) 会意外删锁——「架构缺陷」。
How:
// 摘自《The Linux Programming Interface》 第 55 章
/* 关键陷阱:关闭 fd2 释放该进程在此文件上的所有锁 */
fd1 = open("testfile", O_RDWR);
fd2 = open("testfile", O_RDWR);
fcntl(fd1, F_SETLK, &fl); /* 加锁 */
close(fd2); /* 即使锁从 fd1 加,close(fd2) 也释放锁! */
与 flock 截然不同(表):
| 维度 | flock | fcntl |
|---|---|---|
锁关联 |
open file description |
process + i-node |
fork 后子进程 |
共享同一锁(可释放) |
不继承 |
exec |
保留(除非 FD_CLOEXEC) |
保留(除非 FD_CLOEXEC) |
关闭任一 fd |
仅当所有 dup 的 fd 都关才解 |
立即释放该进程在此文件的所有锁 |
同一进程重复锁 |
必须自己管理 |
锁集合并 |
When:(1) fcntl 锁不能用 dup/dup2 共享——dup 不影响,但 close 任何 fd 都释放;(2) 库函数用 fcntl 锁——必须警告调用者不要 close fd;(3) 复杂场景——用 flock 避免这个陷阱。
Example:第 55 章 §55.3.5 示例——fd1 加锁 + close(fd2) 释放锁;这是 fcntl 的「架构缺陷」。
55.5 死锁检测与 EDEADLK
What:F_SETLKW 检测循环锁依赖——内核让最近 fcntl 调用失败 EDEADLK。
Why:避免两个进程永远阻塞。
How:
// 处理 F_SETLKW 死锁
if (fcntl(fd, F_SETLKW, &fl) == -1) {
if (errno == EDEADLK) {
/* 内核检测到死锁,本进程被选为受害者 */
/* 释放一些锁、回滚、重试 */
} else if (errno == EINTR) {
/* 信号打断,可重试 */
}
}
mandatory 锁下的 I/O 也会 EDEADLK:两个进程各锁文件一部分,互相 write 对方锁区——内核让一个进程 write 返回 EDEADLK。
When:(1) 始终处理 EDEADLK;(2) 释放部分锁后重试;(3) 不要假设「F_SETLKW 一定成功」;(4) EINTR 可重试。
Example:第 55 章 §55.3.2 示例——两进程互相等对方的锁;内核选后调 fcntl 的进程返回 EDEADLK。
55.6 /proc/locks 调试
What:/proc/locks 显示所有进程持有的锁——类型 + 模式 + 读写 + PID + 文件 + 字节范围。
Why:调试锁竞争——定位哪个进程持哪个锁。
How:
$ cat /proc/locks
1: POSIX ADVISORY WRITE 458 03:07:133880 0 EOF
2: FLOCK ADVISORY WRITE 404 03:07:133875 0 EOF
3: POSIX ADVISORY WRITE 312 03:07:133853 0 EOF
4: FLOCK ADVISORY WRITE 274 03:07:81908 0 EOF
字段:序号 类型(POSIX/FLOCK) 模式(ADVISORY/MANDATORY) 读写 PID 文件 起始 结束
→ 表示被前一行阻塞的请求
*When*:(1) 进程卡在 fcntl 时——查 `/proc/locks` 找谁持锁;(2) 多进程协调问题——查锁列表看是否有冲突;(3) 性能分析——查锁数量判断链表查找时间。 *Example*:第 55 章示例——`atd` 持 FLOCK WRITE 锁 `/var/run/atd.pid`;可 `kill 312` 看锁是否释放。 === 55.7 守护进程单实例模式 *What*:用 fcntl 锁 `.pid` 文件——运行第二个实例时 lockRegion 失败退出。 *Why*:常见守护进程模式——syslogd、atd、cron 都用。 *How*: [source,c]
/* createPidFile 简化版 */ int createPidFile(const char *progName, const char *pidFile, int flags) { int fd = open(pidFile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); if (fd == -1) errExit("open");
if (flags & CPF_CLOEXEC) {
int fl = fcntl(fd, F_GETFD);
fcntl(fd, F_SETFD, fl | FD_CLOEXEC);
}
struct flock fl = { .l_type = F_WRLCK, .l_whence = SEEK_SET,
.l_start = 0, .l_len = 0 };
if (fcntl(fd, F_SETLK, &fl) == -1) {
if (errno == EAGAIN || errno == EACCES)
fatal("'%s' already running", progName);
else errExit("fcntl");
}
if (ftruncate(fd, 0) == -1) errExit("ftruncate");
char buf[BUF_SIZE];
snprintf(buf, sizeof(buf), "%ld\n", (long)getpid());
if (write(fd, buf, strlen(buf)) != strlen(buf))
fatal("write");
return fd; }
/* 使用 / if (createPidFile("mydaemon", "/var/run/mydaemon.pid", CPF_CLOEXEC) == -1) errExit("createPidFile"); / 进程退出前 unlink .pid 文件 */ unlink("/var/run/mydaemon.pid");
*When*:(1) 守护进程——`/var/run/daemon.pid` + fcntl F_WRLCK;(2) 自重启服务器——CPF_CLOEXEC 防止重启时锁残留;(3) 调试——`kill -0 PID` 验证 PID 是否存活。 *Example*:第 55 章 `create_pid_file.c` 完整实现——`if (lockRegion(fd, F_WRLCK, ...) == -1 && errno == EAGAIN)` 则 fatal「daemon 已运行」。 == 三、关键图表 [NOTE] .非可视化条目(API / 标志) ==== [cols="1,3", options="header"] |=== | 类别 | 内容 | flock API | `flock(fd, LOCK_SH/LOCK_EX/LOCK_UN/LOCK_NB)`;非 SUSv3;锁关联 open file description | fcntl API | `fcntl(fd, F_SETLK/F_SETLKW/F_GETLK, &flock)`;SUSv3 标准化;锁关联 process + i-node | flock 锁类型 | LOCK_SH(共享)/ LOCK_EX(排他)/ LOCK_UN(解锁)/ LOCK_NB(非阻塞) | fcntl 锁类型 | F_RDLCK(读/共享)/ F_WRLCK(写/排他)/ F_UNLCK(解锁) | fcntl 命令 | F_SETLK(非阻塞)/ F_SETLKW(阻塞,可能 EINTR/EDEADLK)/ F_GETLK(探测) | flock 结构 | l_type / l_whence / l_start / l_len(0=到 EOF)/ l_pid(GETLK 返回) | 兼容性 | 共享+共享=允许;其他组合=拒绝 | flock 语义 | fork 共享;exec 保留;dup 共享;open 独立;LOCK_NB 失败 EWOULDBLOCK | fcntl 语义 | fork 不继承;exec 保留(除非 FD_CLOEXEC);同进程所有 fd 共享;close 任一 fd 释放所有锁 | advisory vs mandatory | 默认 advisory;mandatory 需 mount -o mand + chmod g+s,g-x | mandatory 行为 | 阻塞 I/O 阻塞;非阻塞 I/O EAGAIN;open O_TRUNC EAGAIN;mmap EAGAIN | 死锁检测 | F_SETLKW 循环依赖 → EDEADLK(最近 fcntl 进程失败);mandatory I/O 也可能 EDEADLK | 性能 | 锁链表按 PID + 起始偏移排序;O(N) 查找;Linux 不限制锁数量 | /proc/locks | 序号 + 类型(POSIX/FLOCK)+ 模式(ADVISORY/MANDATORY)+ 读写 + PID + 文件 + 字节范围 | 文件 vs 字节区间 | flock 只能整文件;fcntl 任意字节范围 | 守护进程单实例 | /var/run/daemon.pid + fcntl F_WRLCK SEEK_SET 0 0 |=== ==== == 四、思维导图 [source,mermaid]
mindmap root第 55 章 文件锁 两种 API flock 整文件 BSD fcntl 字节区间 SysV SUSv3 仅 fcntl flock 非标准 flock 语义 共享 排他 解锁 锁关联 open file description dup fd 共享锁 fork 继承 exec 保留 fcntl 语义 读锁 写锁 解锁 锁关联 process i-node fork 不继承 close 任一 fd 释放所有 库函数陷阱 advisory mandatory 默认 advisory 合作 mandatory mount o mand mandatory chmod g+s g-x 内核强制检查 I/O 不推荐 有死锁风险 死锁检测 F_SETLKW 循环依赖 EDEADLK 最近 fcntl 失败 mandatory I/O 也 EDEADLK EINTR 信号打断 proc locks 调试 序号 类型 模式 读写 PID 文件 起始 结束字节 行为 阻塞请求 守护进程单实例 var run daemon pid fcntl F_WRLCK 第二个实例 EAGAIN CPF_CLOEXEC 自重启
五、重点与易错点
-
flock vs fcntl——flock 简单但只能整文件;fcntl 灵活但语义复杂;SUSv3 只标准化 fcntl;应用不要混用两者(交互未定义)。
-
flock 锁关联 open file description——dup/dup2 共享锁;open 第二次是独立锁;fork 后父子共享锁;转换非原子(先解再锁)。
-
fcntl 锁关联 process + i-node——同进程所有 fd 共享锁集;close 任一 fd 释放该进程在此文件的所有锁——库函数用 fcntl 易被调用者 close 误删。
-
fcntl 不跨 fork 继承——与 flock 不同;锁不传给子进程。
-
F_SETLKW 可能 EINTR——信号打断;不自动重启(与 read/write 不同);可设 alarm + EINTR 实现超时。
-
F_SETLKW 死锁检测——内核选最近 fcntl 进程失败 EDEADLK;其他进程继续等;必须处理 EDEADLK 重试。
-
F_GETLK 不真锁——只是探测;探测与 F_SETLK 之间可能有竞争(探测通过但 SETLK 失败);必须准备失败。
-
l_len=0 表示到 EOF——动态增长文件方便;l_len 可负(2.4.21+,锁 l_start-|l_len| 到 l_start-1)。
-
mandatory 锁启用两步——mount -o mand + 文件 chmod g+s,g-x;ls 显示 r-S 而非 r-s。
-
mandatory 锁 I/O 检查——read/write 阻塞(无 O_NONBLOCK)或 EAGAIN(有 O_NONBLOCK);open O_TRUNC EAGAIN;mmap EAGAIN。
-
mandatory 不推荐用——恶意 DoS、性能开销(每次 I/O 检查)、有内核 race conditions。
-
fcntl 锁不阻止 unlink——仅阻止 read/write;删除文件只需父目录权限。
-
/proc/locks 调试——查哪个进程持哪个锁;
→行为表示阻塞请求;file leases(Samba oplocks/NFSv4 delegations)也在这里。 -
守护进程单实例——fcntl F_WRLCK SEEK_SET 0 0 锁整个 .pid 文件;CPF_CLOEXEC 防自重启锁残留;进程退出前 unlink。
-
flock 与 NFS——Linux NFS server 自 kernel 2.6.12 支持 flock(实现为 fcntl 全文件锁);客户端看不到服务器锁,反之亦然。
-
跨章衔接:第 5 章 fcntl 基础(F_GETFD/F_SETFD 用于 FD_CLOEXEC);第 47/53 章 sem(可替代文件锁做同步);第 48/54 章 shm(共享内存不需要文件锁);第 14 章 mount -o mand 选项。