[TOC]

fs/locks.c 文件锁与租约(File Locks and Leases)

历史与背景

这项技术是为了解决什么特定问题而诞生的?

fs/locks.c 中实现的文件锁机制是为了解决在多进程、多用户环境下并发访问同一文件时可能导致的数据竞争和文件损坏问题。当多个进程同时对一个文件进行读写时,如果没有协调机制,操作的交错执行可能会导致不可预期的结果(例如,经典的银行转账问题)。文件锁为希望协作的进程提供了一种标准化的互斥机制。

具体来说,它解决了以下问题:

  • 数据一致性:确保一个进程在修改文件(或文件的一部分)时,其他进程不能同时修改,防止数据被破坏。
  • 原子操作:允许进程以原子的方式执行一系列操作,例如读取文件、修改内容、再写回文件,而不会被其他进程干扰。
  • 进程间同步:提供一种简单的同步原语,让进程可以等待其他进程完成对文件的操作后再继续执行。例如,一个进程可以等待另一个进程生成完一个报告文件后再去读取它。

它的发展经历了哪些重要的里程碑或版本迭代?

Linux中的文件锁机制是逐步演化而来的,主要融合了两种不同的Unix传统:

  • BSD锁 (flock):早期Linux内核(1.3.x系列之前)通过C库来模拟flock(2)系统调用。后来,为了提供真正的BSD语义,flock被实现为一个独立的内核系统调用。 它的特点是始终锁定整个文件。
  • POSIX记录锁 (fcntl):这是POSIX标准定义的文件锁,通过fcntl(2)系统调用实现。 它更加灵活,支持对文件的任意字节范围(记录)进行加锁。fs/locks.c最初就是为了支持fcntlF_GETLK, F_SETLK, 和 F_SETLKW命令而创建的。
  • 混合与分离:内核曾尝试让flockfcntl锁能够协作,但由于存在大量的竞争条件和死锁可能性,最终决定将两者分离开来,使它们互不影响。 这是一个重要的决定,使Linux在这方面的行为与其他商业Unix系统保持一致。
  • 强制锁 (Mandatory Locking):除了默认的“劝告式锁”,内核还实现了强制锁。但这被认为存在风险(例如可能冻结NFS服务器),因此其使用从一个全局配置项变为一个可选的、需要显式开启的文件系统挂载选项。
  • 开放文件描述锁 (Open File Description Locks):为了结合BSD锁和POSIX锁的优点,Linux在3.15内核中引入了这种新的锁类型。 它像POSIX锁一样支持字节范围,同时像BSD锁一样与文件描述符关联,解决了多线程共享文件描述符时的一些问题。

目前该技术的社区活跃度和主流应用情况如何?

文件锁是Linux和所有类Unix系统中一个非常基础且核心的功能。它被广泛应用于各种用户空间应用程序中:

  • 数据库:像SQLite这样的文件型数据库严重依赖文件锁来协调多个客户端对数据库文件的并发访问。
  • 邮件服务器:邮件后台程序(如sendmail, Postfix)使用文件锁来防止在投递邮件到用户邮箱(maildir/mbox)时发生冲突。
  • 脚本和系统管理:系统管理员和脚本开发者经常使用flock命令行工具来确保cron任务或其他脚本不会并发执行,从而避免冲突。
  • 应用软件:许多应用程序使用锁文件(lockfile)来防止用户同时启动多个实例。

这是一个非常稳定和成熟的内核子系统,其代码的改动通常是为了修复bug、进行性能优化或适应新的文件系统特性。

核心原理与设计

它的核心工作原理是什么?

fs/locks.c 作为一个通用的锁管理器,为不同的文件系统提供了一个统一的锁实现框架。其核心原理是将锁与文件的inode关联起来

  1. 锁的数据结构:每个inode(struct inode)中都包含一个指向struct file_lock链表的指针(i_flock)。 这个链表存储了所有施加在该文件上的锁。
  2. 锁的类型:每个file_lock结构都记录了锁的类型(BSD flock、POSIX fcntl、Lease)、锁的模式(共享锁/读锁 F_RDLCK,排他锁/写锁 F_WRLCK)、持有锁的进程信息,以及对于POSIX锁而言的字节范围(起始和结束位置)。
  3. 加锁过程:当一个进程通过fcntlflock请求一个锁时:
    • 内核分配一个新的file_lock结构来描述这个请求。
    • 它会遍历该inode上的锁链表,检查新请求的锁是否与任何已存在的锁冲突(例如,请求一个写锁的范围与一个已存在的读锁范围重叠)。
    • 如果不冲突,就将新的file_lock结构添加到链表中,加锁成功。
    • 如果冲突,根据请求是阻塞型(F_SETLKW)还是非阻塞型(F_SETLK),进程或者被放入等待队列睡眠,或者立即收到一个错误返回。
  4. 解锁过程:当进程关闭文件或显式释放锁时,内核会从inode的锁链表中移除对应的file_lock结构,并唤醒可能正在等待该资源的进程。
  5. 死锁检测:对于POSIX锁,fs/locks.c还实现了一个死锁检测算法。当一个进程被阻塞时,内核会检查是否存在一个依赖环(A等B,B等C,C等A),如果存在,则会使加锁请求失败以避免死锁。

它的主要优势体现在哪些方面?

  • 标准化:提供了符合POSIX和传统BSD标准的API,使得应用程序具有良好的可移植性。
  • 灵活性:特别是POSIX锁,支持对文件的任意字节范围进行加锁,非常适合于需要对记录进行操作的数据库类型应用。
  • 通用性:它是一个VFS(虚拟文件系统)层的实现,这意味着它可以工作在所有支持它的Linux文件系统之上,包括像NFS这样的网络文件系统(尽管在NFS上的实现有其特殊性)。
  • 内核级仲裁:由内核来裁决锁的授予和冲突,保证了其权威性和跨进程的有效性。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 劝告式本质 (Advisory Nature):默认情况下,文件锁是劝告式的。这意味着它们只对那些同样尝试去获取锁的“合作”进程有效。 一个不遵守锁协议的进程仍然可以随意读写被锁定的文件,内核不会阻止它。 这需要所有访问共享文件的程序都遵循同一套锁定规则。
  • 性能开销:文件锁的管理,特别是冲突检测和死锁检测,会带来一定的性能开销。对于需要极高I/O吞吐量的应用,频繁地获取和释放锁可能会成为瓶颈。
  • 网络文件系统(NFS)的复杂性:在NFS上实现文件锁比在本地文件系统上要复杂得多,因为它需要客户端和服务器之间的协调。早期版本的flock在NFS上甚至无法工作。 尽管现代NFS协议已经很好地支持了锁,但其性能和语义可能与本地文件系统略有不同。
  • 强制锁的风险:虽然内核支持强制锁,但它很少被使用,因为它可能带来意想不到的副作用,比如一个持有强制锁的进程可以阻止其他进程(甚至是catls)对文件进行任何操作,容易导致系统服务被拒绝(Denial of Service)。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

  • 确保单实例应用或脚本:这是flock最常见的用途之一。一个cron脚本可以在开始执行前获取一个对特定锁文件的排他锁。如果此时该脚本的另一个实例已经运行并持有该锁,新的实例就会阻塞或退出,从而保证了任务的唯一性。
    • flock /var/run/my_cron.lock -c "my_heavy_task.sh"
  • 管理共享配置文件:多个进程可能需要读取或更新一个共享的配置文件。在读取时,它们可以获取共享锁;在写入时,则必须获取排他锁。这确保了任何进程都不会读到被部分修改的、不完整的配置。
  • 简单的文件数据库:一个简单的“键值对”数据库,如果将每个条目存储在文件的不同区域,就可以使用POSIX字节范围锁来锁定正在被访问的条目,允许多个进程同时访问数据库中不同的记录。

是否有不推荐使用该技术的场景?为什么?

  • 高性能、高并发数据库:对于像PostgreSQL或MySQL这样的大型数据库系统,它们通常会实现自己更精细、更高效的内部锁管理器,而不是依赖于操作系统的文件锁。因为内核文件锁的粒度和性能无法满足它们复杂的需求。
  • 线程间同步:文件锁是为进程间同步设计的。对于同一进程内的多个线程,使用互斥锁(pthread_mutex_t)或读写锁(pthread_rwlock_t)等线程同步原语通常更轻量、更高效。
  • 需要绝对强制执行的场景:由于锁的劝告式本质,如果无法保证所有访问文件的程序都会遵守锁协议,那么文件锁就无法提供可靠的保护。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 BSD锁 (flock) POSIX记录锁 (fcntl) 开放文件描述锁 (O_OFD, fcntl)
功能概述 简单,对整个文件加锁。 灵活,可对文件任意字节范围加锁。 结合了flockfcntl的优点,支持字节范围,且与文件描述符关联。
实现方式 VFS层实现,与文件打开描述(struct file)关联。 VFS层实现,与[inode, pid]对关联。 这意味着一个进程对一个文件的所有文件描述符共享同一套锁。 VFS层实现,也与文件打开描述关联,行为更像flock
锁的粒度 整个文件 字节范围 字节范围
继承与共享 锁与文件打开描述关联。fork()出的子进程会继承文件描述符,从而共享父进程的锁。 锁与进程关联。fork()出的子进程不会继承父进程的锁。 锁与文件打开描述关联,fork()行为与flock类似。
原子性 锁模式的转换(共享到排他)不是原子的,可能会有竞争条件。 锁模式的转换是原子的。 锁模式的转换是原子的。
死锁检测 不检测死锁。 会检测死锁。 会检测死锁。
NFS支持 自Linux 2.6.12起,通过在NFS上模拟为fcntl锁来支持。 原生支持良好。 原生支持良好。
典型用途 脚本、后台任务的互斥执行;保证单实例应用。 数据库等需要对文件内记录进行细粒度锁定的应用。 多线程程序中,希望线程通过独立的open()调用获取对同一文件不同区域的独立锁。

filelock_init: 初始化内核文件锁系统

此函数在内核启动的早期阶段被调用, 其核心职责是为内核的文件锁定(flock, fcntl)和文件租约(lease)机制准备好所有必需的基础数据结构和内存资源。它通过创建专用的内存缓存和初始化每CPU的锁列表, 为高效、可靠的文件锁定操作奠定基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/*
* 当有任何尝试设置新租约的动作发生时, 内核子系统可以注册到 lease_notifier_chain,
* 以便接收通知。例如, nfsd (NFS服务器守护进程) 使用这个机制来关闭它可能已经缓存的文件,
* 以避免与新设置的冲突租约发生矛盾。
*/
/*
* 定义一个静态的 srcu_notifier_head 结构体, 名为 lease_notifier_chain.
* SRCU (Sleepable Read-Copy-Update) 是一种特殊的RCU变体,
* 它所保护的通知链允许通知处理函数睡眠, 这使得它比普通RCU通知链更灵活.
*/
static struct srcu_notifier_head lease_notifier_chain;

/*
* 一个内联函数, 用于初始化租约通知链.
* 内联可以减少函数调用的开销.
*/
static inline void
lease_notifier_chain_init(void)
{
/*
* 调用 srcu_init_notifier_head 函数来初始化 lease_notifier_chain.
* 这个函数会设置好通知链头部所需的所有内部状态.
*/
srcu_init_notifier_head(&lease_notifier_chain);
}
/*
* filelock_init: 文件锁系统的初始化函数.
* 标记为 __init, 表示它仅在内核启动期间执行, 其占用的内存之后可以被回收.
*/
static int __init filelock_init(void)
{
/*
* 定义一个整型变量 i, 用作 CPU 的循环计数器.
*/
int i;

/*
* 调用 kmem_cache_create 创建一个名为 "file_lock_ctx" 的 SLAB 缓存.
* 这个缓存专门用于快速分配和释放 'struct file_lock_context' 对象.
* sizeof(struct file_lock_context): 指定每个对象的大小.
* 0: 指定对齐方式, 0表示使用默认对齐.
* SLAB_PANIC: 这是一个重要的标志, 如果从这个缓存分配内存失败, 内核将立即宕机(panic).
* NULL: 没有特殊的构造函数.
*/
flctx_cache = kmem_cache_create("file_lock_ctx",
sizeof(struct file_lock_context), 0, SLAB_PANIC, NULL);

/*
* 创建一个名为 "file_lock_cache" 的 SLAB 缓存, 专门用于 'struct file_lock' 对象.
* 这是最常用的文件锁结构体.
*/
filelock_cache = kmem_cache_create("file_lock_cache",
sizeof(struct file_lock), 0, SLAB_PANIC, NULL);

/*
* 创建一个名为 "file_lease_cache" 的 SLAB 缓存, 专门用于 'struct file_lease' 对象.
*/
filelease_cache = kmem_cache_create("file_lease_cache",
sizeof(struct file_lease), 0, SLAB_PANIC, NULL);

/*
* for_each_possible_cpu 是一个宏, 它会遍历系统上所有可能存在的CPU.
* 在被配置为单核的 STM32H750 系统上, 这个循环只会执行一次, 此时 i 的值为 0.
*/
for_each_possible_cpu(i) {
/*
* per_cpu_ptr 是一个宏, 用于获取指向每CPU变量的指针.
* 这里, 它获取指向当前CPU(i)的 file_lock_list_struct 实例的指针.
* 在单核系统中, 这就是获取全局唯一的那个实例.
*/
struct file_lock_list_struct *fll = per_cpu_ptr(&file_lock_list, i);

/*
* 初始化这个每CPU列表的自旋锁.
* 在单核抢占式内核中, 这个锁通过禁用内核抢占来保护锁列表, 防止并发访问.
*/
spin_lock_init(&fll->lock);
/*
* 初始化这个每CPU列表的哈希链表头.
* 文件锁被组织在一个哈希表中, 以加速查找.
*/
INIT_HLIST_HEAD(&fll->hlist);
}

/*
* 调用前面定义的内联函数来初始化租约通知链.
*/
lease_notifier_chain_init();
/*
* 返回 0 表示初始化成功.
*/
return 0;
}
/*
* 使用 core_initcall() 宏将 filelock_init 函数注册为一个内核核心初始化函数.
* 这确保了文件锁系统会在内核启动的早期、在任何进程尝试使用文件锁之前被初始化.
*/
core_initcall(filelock_init);

/proc/locks 文件实现:向用户空间展示内核文件锁状态

本代码片段实现了Linux内核中/proc/locks这个虚拟文件的后端逻辑。其核心功能是遍历内核中所有活跃的文件锁(包括POSIX记录锁、Flock锁、Lease租约等),并将它们的详细信息格式化成人类可读的文本,展示给用户空间。这对于系统管理员和开发者来说,是一个至关重要的调试工具,可以用来诊断死锁问题、查看哪些进程持有哪些文件的锁,以及了解锁的类型、范围和状态。

实现原理分析

该文件的实现依赖于内核的seq_file框架,这是一个为在/proc中高效、安全地输出大量序列化数据而设计的标准接口。

  1. Seq_file 迭代器模型: seq_file通过.start, .next, .show, .stop这四个回调函数构成的迭代器模型来工作。

    • locks_start: 当用户首次读取/proc/locks时调用。它负责获取必要的锁(file_rwsemblocked_lock_lock)来冻结全局锁链表的状态,然后使用seq_hlist_start_percpu找到第一个要显示的锁对象。
    • locks_next: 每次seq_file的缓冲区填满并需要下一个条目时调用。它使用seq_hlist_next_percpu在全局的、per-CPU的file_lock_list哈希链表中找到下一个锁。
    • locks_show: 这是核心的显示函数。对于locks_startlocks_next返回的每一个锁对象,该函数被调用来将其信息格式化输出到seq_file的缓冲区。
    • locks_stop: 当读取结束(无论正常完成还是被中断),该函数被调用来释放locks_start中获取的所有锁。
  2. 锁信息的格式化 (lock_get_status):

    • 这个函数负责将一个file_lock_core结构体中的信息转换成一行文本。它会提取并格式化以下关键信息:
      • 锁ID: 一个递增的序号。
      • 锁类型: POSIX, OFDLCK, FLOCK, LEASE等。
      • 锁属性: ADVISORY(建议锁),以及Lease的ACTIVE/BREAKING等状态。
      • 读写模式: WRITE, READ, UNLCK。
      • 持有者PID: 经过PID命名空间转换的进程ID。
      • 文件标识: 设备的主:次设备号和inode号,唯一标识被锁定的文件。
      • 锁范围: 对于POSIX锁,显示其在文件中的起始和结束偏移量。
  3. 阻塞关系的可视化 (locks_show):

    • locks_show函数有一个非常巧妙的设计。一个持有锁的进程可能会阻塞其他多个请求该锁的进程,而被阻塞的进程自身也可能持有其他锁,从而阻塞别的进程。这种复杂的等待关系形成了一个树状/图状结构。
    • locks_show将这个等待关系视为一个二叉树来遍历。它将一个锁的第一个被阻塞者视为“左子节点”,将被阻塞队列中的下一个成员视为“右兄弟节点”。通过一个精巧的深度优先遍历算法,它可以在输出中用->前缀和缩进来清晰地展示出锁的阻塞链,例如:
      1
      2
      1: POSIX  ADVISORY  WRITE 1234 08:01:12345 0 EOF
      2: -> POSIX ADVISORY WRITE 5678 08:01:12345 0 EOF
      这表示ID为1的锁(由进程1234持有)正在阻塞ID为2的锁(由进程5678请求)。
  4. 文件描述符锁显示 (show_fd_locks):

    • 这是一个辅助功能,未直接用于/proc/locks,但常用于/proc/[pid]/fdinfo/[fd],用于显示与某个特定文件描述符相关的锁。它会遍历指定inode上的所有锁链表,并只打印出与目标filefiles_struct匹配的锁。
  5. 初始化 (proc_locks_init):

    • 使用proc_create_seq_private/proc文件系统中创建名为locks的文件,并将locks_seq_operations注册为其处理函数。sizeof(struct locks_iterator)用于为每个打开/proc/locks的实例分配一个私有数据区,用于存储迭代器状态。

代码分析

/proc/locks 显示逻辑:将锁等待图可视化为树状结构

本代码片段是 /proc/locks 功能的核心,负责将内核中复杂的、网状的文件锁等待关系,以一种结构清晰、人类可读的树状形式展现出来。其主要功能是通过一个精巧的遍历算法,将一个锁及其所有直接和间接的等待者(被阻塞的锁请求)进行深度优先遍历,并用缩进和前缀来可视化这种阻塞依赖链。

实现原理分析

此功能的核心在于 locks_show 函数及其将锁等待关系抽象为二叉树的遍历算法。

  1. 数据结构抽象:

    • 内核中的文件锁等待关系实际上是一个有向图:锁A阻塞锁B,锁B可能又阻塞锁C,同时锁A还可能阻塞锁D。
    • locks_show 函数巧妙地将这个图(在单个锁的等待队列这个局部视图中)视为一个二叉树
      • 父节点: cur,当前正在被分析的锁。
      • 左子节点: curflc_blocked_requests 链表的第一个成员。代表第一个被cur阻塞的锁请求。
      • 右兄弟节点: cur 在其父节点的 flc_blocked_requests 链表中的下一个成员get_next_blocked_member函数就是用来获取这个节点的。
      • 回溯到父节点: cur->flc_blocker 指针可以直接找到阻塞cur的锁,即父节点。
  2. 深度优先遍历 (Depth-First Traversal):

    • locks_showwhile 循环实现了一个非递归的深度优先遍历算法:
    • 访问当前节点: 循环开始时,首先调用 lock_get_status 打印当前锁 cur 的信息。level 变量控制着缩进的深度。
    • 转向左子节点 (Go Left): 检查当前锁 cur 是否有阻塞其他锁(!list_empty(&cur->flc_blocked_requests))。如果有,就将 cur 更新为其阻塞队列的第一个成员(左子节点),并增加深度 level
    • 转向右兄弟节点 (Go Right): 如果当前锁没有阻塞其他锁(即没有左子节点),算法尝试转向其右兄弟节点。它调用 get_next_blocked_member(cur) 来获取。
    • 回溯 (Backtrack): 如果既没有左子节点,也没有右兄弟节点,算法就需要回溯。它会通过 cur->flc_blocker 移动到父节点,减少深度 level,然后再次尝试从父节点位置寻找右兄弟节点。这个 while (tmp == NULL && ...) 循环实现了连续的回溯,直到找到一个可以向右走的分支,或者回到根节点。
    • 遍历结束: 当 cur 最终变为NULL时(通常是在回溯到根节点之上后,再也找不到右兄弟节点),整个遍历结束。
  3. 信息格式化 (lock_get_status):

    • 此函数是遍历算法中每个节点的“访问”操作。它负责将 file_lock_core 结构体中的二进制信息翻译成易于理解的文本。
    • PID 命名空间: 它会调用 locks_translate_pid,将内核内部的pid结构体转换为当前查看/proc/locks的进程所属PID命名空间中的PID值。这对于在容器化环境中调试尤为重要。
    • 锁类型与状态: 通过检查 flc_flags,它可以区分 POSIX 锁、Flock 锁、Lease 租约等多种类型的锁,并打印出它们各自的状态(如 ADVISORY, BREAKING, ACTIVE)。
    • 文件标识: 它输出设备的主/次设备号和 inode 号,这是在整个系统中唯一标识一个文件的方式。

特定场景分析:单核、无MMU的STM32H750平台

硬件交互与MMU

这部分代码是纯粹的数据结构遍历和字符串格式化,与硬件或MMU完全无关。

单核环境影响

遍历算法本身是单线程执行的(在locks_start获取锁之后)。因此,单核或多核对其逻辑没有影响。其正确性依赖于locks_startlocks_stop中锁机制的正确实现,以保证在遍历期间锁链表不会被并发修改。

实际意义

在STM32H750平台上,多任务或多线程应用可能会因为不正确地使用文件锁而导致死锁或性能问题。例如,一个高优先级的实时任务可能在等待一个被低优先级日志任务持有的文件锁。

  • locks_show 中精巧的树状遍历和可视化输出,使得开发者可以一目了然地看到阻塞链。在上述例子中,输出会清晰地显示日志任务的锁阻塞了实时任务的锁请求。
  • 这种直观的展示对于诊断嵌入式系统中的复杂并发问题非常有价值,因为它将内核内部抽象的指针关系转换成了易于理解的依赖关系图。

结论locks_show的实现不仅仅是简单地罗列所有锁,而是通过一种巧妙的算法,将锁之间的等待关系进行了可视化。这种设计对于在任何平台(包括资源受限的STM32H750)上诊断和理解复杂的并发锁问题,都是一个极其强大且设计优良的工具。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
// lock_get_status: 格式化并打印单个文件锁的状态信息。
static void lock_get_status(struct seq_file *f, struct file_lock_core *flc,
loff_t id, char *pfx, int repeat)
{
// 提取PID、锁类型、锁模式、文件inode信息、锁范围等,并使用seq_printf输出。
struct inode *inode = NULL;
unsigned int pid;
struct pid_namespace *proc_pidns = proc_pid_ns(file_inode(f->file)->i_sb);
int type = flc->flc_type;
struct file_lock *fl = file_lock(flc);

pid = locks_translate_pid(flc, proc_pidns);

/*
* If lock owner is dead (and pid is freed) or not visible in current
* pidns, zero is shown as a pid value. Check lock info from
* init_pid_ns to get saved lock pid value.
*/
if (flc->flc_file != NULL)
inode = file_inode(flc->flc_file);

seq_printf(f, "%lld: ", id);

if (repeat)
seq_printf(f, "%*s", repeat - 1 + (int)strlen(pfx), pfx);

if (flc->flc_flags & FL_POSIX) {
if (flc->flc_flags & FL_ACCESS)
seq_puts(f, "ACCESS");
else if (flc->flc_flags & FL_OFDLCK)
seq_puts(f, "OFDLCK");
else
seq_puts(f, "POSIX ");

seq_printf(f, " %s ",
(inode == NULL) ? "*NOINODE*" : "ADVISORY ");
} else if (flc->flc_flags & FL_FLOCK) {
seq_puts(f, "FLOCK ADVISORY ");
} else if (flc->flc_flags & (FL_LEASE|FL_DELEG|FL_LAYOUT)) {
struct file_lease *lease = file_lease(flc);

type = target_leasetype(lease);

if (flc->flc_flags & FL_DELEG)
seq_puts(f, "DELEG ");
else
seq_puts(f, "LEASE ");

if (lease_breaking(lease))
seq_puts(f, "BREAKING ");
else if (flc->flc_file)
seq_puts(f, "ACTIVE ");
else
seq_puts(f, "BREAKER ");
} else {
seq_puts(f, "UNKNOWN UNKNOWN ");
}

seq_printf(f, "%s ", (type == F_WRLCK) ? "WRITE" :
(type == F_RDLCK) ? "READ" : "UNLCK");
if (inode) {
/* userspace relies on this representation of dev_t */
seq_printf(f, "%d %02x:%02x:%lu ", pid,
MAJOR(inode->i_sb->s_dev),
MINOR(inode->i_sb->s_dev), inode->i_ino);
} else {
seq_printf(f, "%d <none>:0 ", pid);
}
if (flc->flc_flags & FL_POSIX) {
if (fl->fl_end == OFFSET_MAX)
seq_printf(f, "%Ld EOF\n", fl->fl_start);
else
seq_printf(f, "%Ld %Ld\n", fl->fl_start, fl->fl_end);
} else {
seq_puts(f, "0 EOF\n");
}
}

// get_next_blocked_member: 获取一个锁在其父节点的阻塞队列中的下一个成员(“右兄弟”)。
static struct file_lock_core *get_next_blocked_member(struct file_lock_core *node)
{
struct file_lock_core *tmp;

// 如果节点是NULL或者它没有阻塞者(即它是一个根节点),则它没有兄弟。
if (node == NULL || node->flc_blocker == NULL)
return NULL;

// 获取阻塞队列链表中的下一个元素。
tmp = list_next_entry(node, flc_blocked_member);
// 如果下一个元素是链表头(即回到了父节点)或就是节点自身,说明没有右兄弟。
if (list_entry_is_head(tmp, &node->flc_blocker->flc_blocked_requests,
flc_blocked_member)
|| tmp == node) {
return NULL;
}

return tmp;
}

// locks_show: seq_file的核心show回调,用于显示一个锁及其阻塞的锁。
static int locks_show(struct seq_file *f, void *v)
{
// ... (变量定义) ...
struct locks_iterator *iter = f->private;
struct file_lock_core *cur, *tmp;
struct pid_namespace *proc_pidns = proc_pid_ns(file_inode(f->file)->i_sb);
int level = 0; // level用于控制缩进,表示树的深度。

cur = hlist_entry(v, struct file_lock_core, flc_link);

// 如果锁的持有者PID在当前命名空间不可见,则跳过。
if (locks_translate_pid(cur, proc_pidns) == 0)
return 0;

// 将等待关系视为二叉树进行深度优先遍历的循环。
while (cur != NULL) {
// 访问当前节点:打印锁信息。如果level>0,则添加"-> "前缀和缩进。
if (level)
lock_get_status(f, cur, iter->li_pos, "-> ", level);
else
lock_get_status(f, cur, iter->li_pos, "", level);

// 检查是否有左子节点(即当前锁是否阻塞了其他锁)。
if (!list_empty(&cur->flc_blocked_requests)) {
// 如果有,则向左走:cur更新为第一个被阻塞的锁。
cur = list_first_entry_or_null(&cur->flc_blocked_requests,
struct file_lock_core,
flc_blocked_member);
level++; // 深度增加。
} else {
// 如果没有左子节点,则尝试向右走(寻找右兄弟)。
tmp = get_next_blocked_member(cur);
// 如果没有右兄弟,则回溯到父节点,直到找到有右兄弟的祖先节点。
while (tmp == NULL && cur->flc_blocker != NULL) {
cur = cur->flc_blocker; // 回溯到父节点
level--; // 深度减少
tmp = get_next_blocked_member(cur); // 再次尝试寻找右兄弟
}
// 更新cur为找到的右兄弟(如果找不到,则为NULL,循环结束)。
cur = tmp;
}
}

return 0;
}

static void __show_fd_locks(struct seq_file *f,
struct list_head *head, int *id,
struct file *filp, struct files_struct *files)
{
struct file_lock_core *fl;

list_for_each_entry(fl, head, flc_list) {

if (filp != fl->flc_file)
continue;
if (fl->flc_owner != files && fl->flc_owner != filp)
continue;

(*id)++;
seq_puts(f, "lock:\t");
lock_get_status(f, fl, *id, "", 0);
}
}


// show_fd_locks: 显示与特定文件描述符相关的锁。
void show_fd_locks(struct seq_file *f,
struct file *filp, struct files_struct *files)
{
// ... 实现细节如上文分析 ...
// 遍历inode上的所有锁,只打印与目标filp和files匹配的锁。
struct inode *inode = file_inode(filp);
struct file_lock_context *ctx;
int id = 0;

ctx = locks_inode_context(inode);
if (!ctx)
return;

spin_lock(&ctx->flc_lock);
__show_fd_locks(f, &ctx->flc_flock, &id, filp, files);
__show_fd_locks(f, &ctx->flc_posix, &id, filp, files);
__show_fd_locks(f, &ctx->flc_lease, &id, filp, files);
spin_unlock(&ctx->flc_lock);
}

// locks_start: seq_file的start回调,初始化迭代器并获取锁。
static void *locks_start(struct seq_file *f, loff_t *pos)
__acquires(&blocked_lock_lock) // 静态分析注解,表明此函数获取了锁。
{
struct locks_iterator *iter = f->private;

iter->li_pos = *pos + 1;
// 获取per-cpu读写信号量,防止其他任务修改全局锁列表。
percpu_down_write(&file_rwsem);
// 获取自旋锁,保护阻塞锁列表。
spin_lock(&blocked_lock_lock);
// 在per-cpu哈希链表中找到迭代的起始点。
return seq_hlist_start_percpu(&file_lock_list.hlist, &iter->li_cpu, *pos);
}

// locks_next: seq_file的next回调,获取下一个锁对象。
static void *locks_next(struct seq_file *f, void *v, loff_t *pos)
{
struct locks_iterator *iter = f->private;

++iter->li_pos;
// 在per-cpu哈希链表中获取下一个节点。
return seq_hlist_next_percpu(v, &file_lock_list.hlist, &iter->li_cpu, pos);
}

// locks_stop: seq_file的stop回调,释放锁。
static void locks_stop(struct seq_file *f, void *v)
__releases(&blocked_lock_lock) // 静态分析注解,表明此函数释放了锁。
{
spin_unlock(&blocked_lock_lock);
percpu_up_write(&file_rwsem);
}

// locks_seq_operations: 将上述回调函数组织成seq_operations结构体。
static const struct seq_operations locks_seq_operations = {
.start = locks_start,
.next = locks_next,
.stop = locks_stop,
.show = locks_show,
};

// proc_locks_init: 内核模块初始化函数。
static int __init proc_locks_init(void)
{
// 在/proc下创建名为"locks"的seq_file文件,并关联其操作函数。
proc_create_seq_private("locks", 0, NULL, &locks_seq_operations,
sizeof(struct locks_iterator), NULL);
return 0;
}
// 将初始化函数注册为文件系统初始化阶段的回调。
fs_initcall(proc_locks_init);