第 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() 整文件锁

      flock(fd, LOCK_SH/LOCK_EX/LOCK_UN/LOCK_NB);锁关联 open file description(dup 的 fd 共享);BSD 派;非 SUSv3

      §55.2;LOCK_SH 多进程共享;LOCK_EX 独占;LOCK_NB 非阻塞;转换非原子(先解再锁)

      fcntl() 字节区间锁(记录锁)

      fcntl(fd, F_SETLK/F_SETLKW/F_GETLK, &flock);flock 含 l_type/l_whence/l_start/l_len/l_pid;SUSv3 标准化;可锁任意字节范围

      §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 + 文件 chmod g+s,g-x,内核强制检查

      §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() 整文件锁

      Whatflock(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() 字节区间锁

      Whatfcntl(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 启用(两步):

      1. mount 启用mount -o mand /dev/sda10 /testfs

      2. 文件启用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 检查锁)。

      Examplechmod g+s,g-x /tmp/xls -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 自重启

      五、重点与易错点

      1. flock vs fcntl——flock 简单但只能整文件;fcntl 灵活但语义复杂;SUSv3 只标准化 fcntl;应用不要混用两者(交互未定义)。

      2. flock 锁关联 open file description——dup/dup2 共享锁;open 第二次是独立锁;fork 后父子共享锁;转换非原子(先解再锁)。

      3. fcntl 锁关联 process + i-node——同进程所有 fd 共享锁集;close 任一 fd 释放该进程在此文件的所有锁——库函数用 fcntl 易被调用者 close 误删。

      4. fcntl 不跨 fork 继承——与 flock 不同;锁不传给子进程。

      5. F_SETLKW 可能 EINTR——信号打断;不自动重启(与 read/write 不同);可设 alarm + EINTR 实现超时。

      6. F_SETLKW 死锁检测——内核选最近 fcntl 进程失败 EDEADLK;其他进程继续等;必须处理 EDEADLK 重试。

      7. F_GETLK 不真锁——只是探测;探测与 F_SETLK 之间可能有竞争(探测通过但 SETLK 失败);必须准备失败。

      8. l_len=0 表示到 EOF——动态增长文件方便;l_len 可负(2.4.21+,锁 l_start-|l_len| 到 l_start-1)。

      9. mandatory 锁启用两步——mount -o mand + 文件 chmod g+s,g-x;ls 显示 r-S 而非 r-s。

      10. mandatory 锁 I/O 检查——read/write 阻塞(无 O_NONBLOCK)或 EAGAIN(有 O_NONBLOCK);open O_TRUNC EAGAIN;mmap EAGAIN。

      11. mandatory 不推荐用——恶意 DoS、性能开销(每次 I/O 检查)、有内核 race conditions。

      12. fcntl 锁不阻止 unlink——仅阻止 read/write;删除文件只需父目录权限。

      13. /proc/locks 调试——查哪个进程持哪个锁; 行为表示阻塞请求;file leases(Samba oplocks/NFSv4 delegations)也在这里。

      14. 守护进程单实例——fcntl F_WRLCK SEEK_SET 0 0 锁整个 .pid 文件;CPF_CLOEXEC 防自重启锁残留;进程退出前 unlink。

      15. flock 与 NFS——Linux NFS server 自 kernel 2.6.12 支持 flock(实现为 fcntl 全文件锁);客户端看不到服务器锁,反之亦然。

      16. 跨章衔接:第 5 章 fcntl 基础(F_GETFD/F_SETFD 用于 FD_CLOEXEC);第 47/53 章 sem(可替代文件锁做同步);第 48/54 章 shm(共享内存不需要文件锁);第 14 章 mount -o mand 选项。