HOOOS

进程崩溃后,Linux内核是如何自动释放 flock 文件锁的?

0 27 Linux内功修炼 Linux内核文件锁系统编程
Apple

在 Linux 系统中,如果一个进程在持有 flock 锁的情况下意外崩溃(例如收到 SIGSEGV 段错误信号而终止),内核并不会让这个文件锁一直悬空。内核拥有一套极其严密的资源回收机制,能够确保在进程退出时,自动释放其持有的所有 flock 锁。

要理解这一过程,需要深入到 Linux 内核的文件描述符管理以及进程退出(do_exit)的底层源码逻辑。


1. 核心结论:锁的生命周期绑定

flock 锁的本质,是绑定在“打开的文件描述”(Open File Description,即内核中的 struct file)上,而不是直接绑定在进程(struct task_struct)或文件路径上。

只要该文件描述符的引用计数归零,内核就会自动触发清理函数,释放对应的 flock 锁。进程崩溃时,内核回收该进程的所有文件描述符,从而间接触发了锁的释放。


2. 崩溃回收的底层调用链

当进程崩溃时,操作系统会接管并强制执行进程退出流程。其内核调用链如下:

[进程崩溃] 
   │
   ▼
do_exit()                      <-- 进程退出入口
   │
   ▼
exit_files(tsk)                <-- 回收进程打开的文件资源
   │
   ▼
put_files_struct() 
   │
   ▼
close_files()                  <-- 遍历并关闭所有文件描述符 (fd)
   │
   ▼
filp_close()                   <-- 减少 file 结构体的引用计数
   │
   ▼
fput() / __fput()              <-- 引用计数降为 0,触发真正释放
   │
   ▼
locks_remove_file(file)        <-- 内核在此处彻底清除该 file 上的所有 flock 锁

关键步骤解析:

  1. do_exit() 阶段
    不论进程是正常 exit() 退出,还是因为崩溃被信号(如 SIGKILLSIGSEGV)强行终止,内核都会无条件调用 kernel/exit.c 中的 do_exit() 函数。

  2. exit_files() 阶段
    内核通过 exit_files(tsk) 释放进程持有的文件资源。它会找到该进程的 files_struct,并在 close_files() 中遍历该进程打开的所有文件描述符(fd),对每一个 fd 调用 filp_close()

  3. __fput()locks_remove_file()
    当某个 struct file 的引用计数(f_count)因为 fd 的关闭而降为 0 时,内核会调用 fs/file_table.c 中的 __fput()

    __fput() 的执行过程中,有一行关键的代码:

    locks_remove_file(file);
    

    这个函数会遍历内核锁链表,将所有与该 struct file 关联的 flock 锁(以及文件租借锁 lease)全部移除。至此,锁被安全释放,其他等待该锁的进程会被唤醒。


3. 经典陷阱:flock 与 POSIX 锁 (fcntl) 的本质区别

很多开发者会混淆 flockfcntl(POSIX 记录锁)。它们在进程崩溃时的释放逻辑虽然结果相似,但底层机制有着决定性的不同。

关系对比图:

【flock 关联关系】
Process A (fd 3) ──┐
                   ├─► [Open File Description] (struct file) ──► [flock 锁绑定在此处]
Process B (fd 4) ──┘

【POSIX fcntl 关联关系】
Process A ──► [POSIX 锁] ──► (绑定在 Process + Inode 组成的二元组上)

区别 1:锁的所有者不同

  • flock:锁属于 Open File Description。如果一个进程 fork() 出子进程,子进程会共享相同的 struct file
  • fcntl:锁属于 进程(PID)

区别 2:多进程共享 fd 时的释放行为

这种设计差异导致了一个致命的「锁不释放」漏洞:

  • flock 的潜在陷阱(Fork 问题)
    如果进程 A 获取了 flock 锁,随后调用 fork() 创建了子进程 B。此时,子进程 B 继承了父进程的文件描述符,它们指向同一个 struct file该文件的引用计数变为 2

    如果此时父进程 A 崩溃了

    1. 内核关闭进程 A 的 fd,该 struct file 的引用计数从 2 降为 1。
    2. 因为引用计数不为 0,内核不会调用 __fput(),因此 flock 锁不会被释放
    3. 只有等子进程 B 也关闭了该 fd 或子进程 B 也退出时,锁才会真正释放。
  • fcntl 的表现
    如果进程 A 持有 fcntl 锁后崩溃,由于 fcntl 锁直接绑定在 Process A 上,内核在 filp_close() 阶段就会通过 locks_remove_posix() 直接将锁清除,即使子进程还拿着那个 fd,锁也会立刻释放。


4. 如果是整机断电/内核崩溃呢?

上述的自动释放逻辑建立在**“内核仍然正常运转”**的前提下。

如果发生的是整机断电、硬件死机或 Linux 内核自身 Panic,内核无法执行 do_exit() 扫尾工作:

  • 本地文件系统(Ext4, XFS 等):由于锁只存在于内存中,整机重启后,随着内存清空,锁自然消失。
  • 网络文件系统(NFS, CIFS 等)
    网络文件锁(NLM/NFSv4)依赖客户端与服务端的租约(Lease)机制。如果客户端整机崩溃,服务端在租约期内(通常为 90 秒)仍会保留该锁。只有等到租约过期(Lease Expiration)且没有收到心跳续期时,网络文件系统服务端才会强制释放该锁。

总结

Linux 内核通过将 flock 锁挂载在 struct file 上,并利用 do_exit() 强行闭环垃圾回收,完美解决了进程崩溃带来的锁残留问题。

但在编写多进程或带有 fork 的架构时,必须警惕子进程继承文件描述符导致引用计数不归零、从而使 flock 锁失效的经典坑点。在这种场景下,使用 fcntl 锁或在 fork 后立即在子进程中关闭无关 fd 是更安全的做法。

点评评价

captcha
健康