60、利用 Lockdep 检测内核死锁问题

利用 Lockdep 检测内核死锁问题

1. Lockdep 基础

Lockdep 基于锁类(lock class)工作,锁类是一种“逻辑”锁,与锁的“物理”实例相对。例如,内核的打开文件数据结构 struct file 有两个锁(一个互斥锁和一个自旋锁),Lockdep 将它们都视为锁类。即使运行时内存中存在数千个 struct file 实例,Lockdep 也只会将其作为一个类进行跟踪。更多关于 Lockdep 内部设计的详细信息,可参考 官方内核文档

2. 验证 Lockdep 是否启用

在使用 Lockdep 之前,需要确保其已在调试内核中启用。可以通过以下命令进行验证:

$ uname -r
5.4.0-llkd-dbg
$ grep PROVE_LOCKING /boot/config-5.4.0-llkd-dbg
CONFIG_PROVE_LOCKING=y
3. 示例 1:使用 Lockdep 捕获自死锁 bug

以下是一个示例,展示了如何使用 Lockdep 捕获自死锁问题。

3.1 原始代码

最初的代码通过直接访问 t->comm 来获取线程名称:

// ch6/foreach/thrd_showall/thrd_showall.c
static int showthrds(void)
{
    struct task_struct *g = NULL, *t = NULL; /* 'g' : process ptr; 't': thread ptr */
    [ ... ]
    do_each_thread(g, t) { /* 'g' : process ptr; 't': thread ptr */
        task_lock(t);

        [ ... ]
        if (!g->mm) {    // kernel thread
            snprintf(tmp, TMPMAX-1, " [%16s]", t->comm);
        } else {
            snprintf(tmp, TMPMAX-1, " %16s ", t->comm);
        }
        snprintf(buf, BUFMAX-1, "%s%s", buf, tmp);
        [ ... ]
3.2 修改后的代码

为了改进代码,使用了 get_task_comm() 辅助宏:

// ch13/3_lockdep/buggy_lockdep/thrd_showall_buggy.c
static int showthrds_buggy(void)
{
    struct task_struct *g, *t; /* 'g' : process ptr; 't': thread ptr */
    [ ... ]
    char buf[BUFMAX], tmp[TMPMAX], tasknm[TASK_COMM_LEN];
    [ ... ]
    do_each_thread(g, t) { /* 'g' : process ptr; 't': thread ptr */
        task_lock(t);
        [ ... ]
        get_task_comm(tasknm, t);
        if (!g->mm) // kernel thread
            snprintf(tmp, sizeof(tasknm)+3, " [%16s]", tasknm);
        else
            snprintf(tmp, sizeof(tasknm)+3, " %16s ", tasknm);
        [ ... ]

当编译并将此模块插入内核时,系统可能会出现异常甚至挂起。

3.3 问题分析

通过查看内核日志,发现 Lockdep 捕获到了自死锁问题。 get_task_comm() 宏内部调用了 __get_task_comm() 函数,其代码如下:

// fs/exec.c
char *__get_task_comm(char *buf, size_t buf_size, struct task_struct *tsk)
{
    task_lock(tsk);
    strncpy(buf, tsk->comm, buf_size);
    task_unlock(tsk);
    return buf; 
}
EXPORT_SYMBOL_GPL(__get_task_comm);

在调用 get_task_comm() 之前已经调用了 task_lock(t) ,而 __get_task_comm() 函数内部又尝试重新获取同一个锁,导致自死锁。 task_lock() 函数的代码如下:

// include/linux/sched/task.h */
static inline void task_lock(struct task_struct *p)
{
    spin_lock(&p->alloc_lock);
}

可以看出,这是任务结构中名为 alloc_lock 的自旋锁。

3.4 Lockdep 报告解读

Lockdep 的报告中有一些令人困惑的符号,例如:

[ 1021.449384] insmod/2367 is trying to acquire lock:
[ 1021.451361] ffff88805de73f08 (&(&p->alloc_lock)->rlock){+.+.}, at: __get_task_comm+0x28/0x50
[ 1021.453676]
               but task is already holding lock:
[ 1021.457365] ffff88805de73f08 (&(&p->alloc_lock)->rlock){+.+.}, at: showthrds_buggy+0x13e/0x6d1 

忽略时间戳,第二行最左边列的数字是用于标识特定锁序列的 64 位轻量级哈希值。 {+.+.} 是 Lockdep 用于表示锁获取状态的符号,具体含义可参考 内核文档

3.5 修复方法

修复方法很简单,在调用 get_task_comm() 之前解锁,执行完后再重新加锁。

4. 示例 2:使用 Lockdep 捕获 AB - BA 死锁

以下是一个故意创建循环依赖以导致死锁的示例。

4.1 代码逻辑

创建两个内核线程,分别运行在不同的 CPU 核心上,并使用两个自旋锁 lockA lockB 。规定的锁顺序是先获取 lockA ,再获取 lockB 。但如果违反这个顺序,就会导致经典的 AB - BA 死锁:

kthread 0 on CPU #0                kthread 1 on CPU #1
  Take lockA                           Take lockB
     <perform work>                       <perform work>
                                          (Try and) take lockA
                                          < ... spins forever :
                                                DEADLOCK ... >
(Try and) take lockB
< ... spins forever : 
      DEADLOCK ... >
4.2 代码示例
// ch13/3_lockdep/deadlock_eg_AB-BA/deadlock_eg_AB-BA.c
[ ... ]
/* Our kernel thread worker routine */
static int thrd_work(void *arg)
{
    [ ... ]
   if (thrd == 0) { /* our kthread #0 runs on CPU 0 */
        pr_info(" Thread #%ld: locking: we do:"
            " lockA --> lockB\n", thrd);
        for (i = 0; i < THRD0_ITERS; i ++) {
            /* In this thread, perform the locking per the lock ordering 'rule';
             * first take lockA, then lockB */
            pr_info(" iteration #%d on cpu #%ld\n", i, thrd);
            spin_lock(&lockA);
            DELAY_LOOP('A', 3); 
            spin_lock(&lockB);
            DELAY_LOOP('B', 2); 
            spin_unlock(&lockB);
            spin_unlock(&lockA);
        }
   } else if (thrd == 1) { /* our kthread #1 runs on CPU 1 */
        for (i = 0; i < THRD1_ITERS; i ++) {
            /* In this thread, if the parameter lock_ooo is 1, *violate* the
             * lock ordering 'rule'; first (attempt to) take lockB, then lockA */
            pr_info(" iteration #%d on cpu #%ld\n", i, thrd);
            if (lock_ooo == 1) {        // violate the rule, naughty boy!
                pr_info(" Thread #%ld: locking: we do: lockB --> lockA\n",thrd);
                spin_lock(&lockB);
                DELAY_LOOP('B', 2);
                spin_lock(&lockA);
                DELAY_LOOP('A', 3);
                spin_unlock(&lockA);
                spin_unlock(&lockB);
            } else if (lock_ooo == 0) { // follow the rule, good boy!
                pr_info(" Thread #%ld: locking: we do: lockA --> lockB\n",thrd);
                spin_lock(&lockA);
                DELAY_LOOP('B', 2);
                spin_lock(&lockB);
                DELAY_LOOP('A', 3);
                spin_unlock(&lockB);
                spin_unlock(&lockA);
            }
    [ ... ]
4.3 运行结果

lock_ooo 参数设置为 0 时,程序正常运行;当设置为 1 时,系统会出现死锁。通过 journalctl --since="10 min ago" 可以获取 Lockdep 的报告:

======================================================
WARNING: possible circular locking dependency detected
5.4.0-llkd-dbg #2 Tainted: G OE
------------------------------------------------------
thrd_0/0/6734 is trying to acquire lock:
ffffffffc0fb2518 (lockB){+.+.}, at: thrd_work.cold+0x188/0x24c [deadlock_eg_AB_BA]
but task is already holding lock:
ffffffffc0fb2598 (lockA){+.+.}, at: thrd_work.cold+0x149/0x24c [deadlock_eg_AB_BA]
which lock already depends on the new lock.
[ ... ]
other info that might help us debug this:
 Possible unsafe locking scenario:
       CPU0                    CPU1
       ----                    ----
  lock(lockA);
                               lock(lockB);
                               lock(lockA);
  lock(lockB);
 *** DEADLOCK ***
[ ... lots more output follows ... ]
5. Lockdep 注解和问题
5.1 Lockdep 注解

Lockdep 允许内核开发者使用 lockdep_assert_held() 宏来断言在特定点持有锁,这被称为 Lockdep 注解:

// include/linux/lockdep.h
#define lockdep_assert_held(l) do { \
        WARN_ON(debug_locks && !lockdep_is_held(l)); \
    } while (0)

当断言失败时,会通过 WARN_ON() 发出警告。

5.2 Lockdep 问题

使用 Lockdep 时可能会出现以下问题:
- 重复加载和卸载模块可能会导致 Lockdep 的内部锁类限制被超出。解决方法是避免重复加载/卸载模块或重置系统。
- 当数据结构中有大量锁时,未能正确初始化每个锁可能会导致 Lockdep 锁类溢出。
- 当调试锁被禁用时, debug_locks 整数会被设置为 0,可能会出现 *WARNING* lock debugging disabled!! 消息。此时需要重启系统并重试。

6. 总结

通过以上示例可以看出,Lockdep 是一个强大的工具,可以帮助开发者捕获内核中的死锁问题。同时,了解 Lockdep 的工作原理和可能出现的问题,有助于更有效地使用它进行内核调试。

以下是一个简单的流程图,展示了使用 Lockdep 检测死锁的基本流程:

graph TD;
    A[验证 Lockdep 是否启用] --> B[编写可能存在死锁的代码];
    B --> C[编译并插入内核模块];
    C --> D{系统是否异常};
    D -- 是 --> E[获取内核日志];
    D -- 否 --> F[继续测试其他情况];
    E --> G[分析 Lockdep 报告];
    G --> H[修复死锁问题];
    H --> C;

表格总结 Lockdep 可能出现的问题及解决方法:
| 问题 | 原因 | 解决方法 |
| ---- | ---- | ---- |
| 锁类限制超出 | 重复加载和卸载模块 | 避免重复加载/卸载模块或重置系统 |
| 锁类溢出 | 数据结构中有大量锁且未正确初始化 | 确保正确初始化所有锁 |
| 调试锁禁用 | 出现 Lockdep 警告或其他错误 | 重启系统并重试 |

利用 Lockdep 检测内核死锁问题

7. 操作步骤总结

为了更清晰地展示使用 Lockdep 检测内核死锁的操作流程,以下是详细的步骤列表:
1. 启用并验证 Lockdep
- 确保在调试内核中启用 Lockdep。
- 使用以下命令验证:

$ uname -r
5.4.0-llkd-dbg
$ grep PROVE_LOCKING /boot/config-5.4.0-llkd-dbg
CONFIG_PROVE_LOCKING=y
  1. 编写测试代码
    • 编写可能存在死锁问题的内核模块代码,如自死锁或 AB - BA 死锁的示例代码。
  2. 编译并插入内核模块
    • 编译内核模块代码。
    • 使用 insmod 命令将模块插入内核。
  3. 观察系统状态
    • 检查系统是否出现异常,如挂起等。
  4. 获取内核日志
    • 如果系统异常,可尝试使用 dmesg journalctl 命令获取内核日志。
    • 例如: journalctl --since="10 min ago"
  5. 分析 Lockdep 报告
    • 查看 Lockdep 报告,找出死锁的原因和相关信息。
  6. 修复死锁问题
    • 根据报告分析结果,修改代码以解决死锁问题。
  7. 重复测试
    • 再次编译并插入修改后的内核模块,重复上述步骤进行测试,直到问题解决。
8. 常见问题解答

以下是关于 Lockdep 使用过程中常见问题的解答:
| 问题 | 解答 |
| ---- | ---- |
| 如何解读 Lockdep 报告中的锁状态符号,如 {+.+.} ? | {+.+.} 是 Lockdep 用于表示锁获取状态的符号, + 表示锁在 IRQ 启用时获取, . 表示锁在 IRQ 禁用且不在 IRQ 上下文时获取。具体含义可参考 内核文档 。 |
| 为什么重复加载和卸载模块会导致 Lockdep 锁类限制超出? | 加载内核模块会为其所有锁创建新的锁类,而卸载模块时这些锁类不会被移除,而是被重用。因此,重复加载和卸载会使锁类数量不断增加,最终超出限制。 |
| 当系统出现 *WARNING* lock debugging disabled!! 消息时该怎么办? | 这可能是由于 Lockdep 发出警告或其他内核锁定基础设施出现错误导致调试锁被禁用。此时需要重启系统并重试。 |

9. 深入理解 Lockdep

为了更好地使用 Lockdep,我们可以进一步深入理解其工作原理和内部机制。Lockdep 通过跟踪锁的获取和释放操作,构建锁的依赖关系图。当检测到锁的获取顺序违反了已建立的规则时,就会发出警告,提示可能存在死锁问题。

以下是一个简单的示意图,展示了 Lockdep 跟踪锁操作的基本原理:

graph LR;
    A[线程 1 获取锁 A] --> B[线程 1 获取锁 B];
    C[线程 2 获取锁 B] --> D[线程 2 尝试获取锁 A];
    B & D --> E{检测到死锁风险};
    E --> F[Lockdep 发出警告];
10. 拓展应用

除了上述示例中的自死锁和 AB - BA 死锁,Lockdep 还可以用于检测其他复杂的死锁场景。例如,多个线程之间的循环依赖死锁、递归锁导致的死锁等。在实际开发中,开发者可以根据具体需求编写不同的测试代码,利用 Lockdep 进行全面的死锁检测。

同时,结合其他内核调试工具,如 kdump crash ,可以在系统崩溃后进行事后分析,进一步定位和解决死锁问题。具体操作步骤如下:
1. 启用 kdump :在系统中启用 kdump 功能,确保在系统崩溃时能够生成内核转储文件。
2. 系统崩溃后 :使用 crash 工具对内核转储文件进行分析,获取更多关于死锁的详细信息。

11. 总结与展望

Lockdep 作为一个强大的内核调试工具,为开发者提供了有效的手段来检测和解决内核中的死锁问题。通过深入理解其工作原理、掌握使用方法以及注意可能出现的问题,开发者可以更高效地进行内核开发和调试。

随着内核技术的不断发展,未来可能会有更多的工具和技术出现,进一步提升内核调试的效率和准确性。例如,Kernel Concurrency Sanitizer (KCSAN) 是一个数据竞争检测器,通过编译时插桩技术来检测 Linux 内核中的数据竞争问题。开发者可以关注这些新技术的发展,并结合使用,以提高内核代码的质量和稳定性。

总之,掌握 Lockdep 等调试工具的使用,对于内核开发者来说是非常重要的。希望本文能够帮助读者更好地理解和应用 Lockdep,解决内核开发中的死锁难题。

基于可靠性评估序贯蒙特卡洛模拟法的配电网可靠性评估研究(Matlab代码实现)内容概要:本文围绕“基于可靠性评估序贯蒙特卡洛模拟法的配电网可靠性评估研究”,介绍了利用Matlab代码实现配电网可靠性的仿真分析方法。重点采用序贯蒙特卡洛模拟法对配电网进行长时间段的状态抽样与统计,通过模拟系统元件的故障与修复过程,评估配电网的关键可靠性指标,如系统停电频率、停电持续时间、负荷点可靠性等。该方法能够有效处理复杂网络结构与设备时序特性,提升评估精度,适用于含分布式电源、电动汽车等新型负荷接入的现代配电网。文中提供了完整的Matlab实现代码与案例分析,便于复现和扩展应用。; 适合人群:具备电力系统基础知识和Matlab编程能力的高校研究生、科研人员及电力行业技术人员,尤其适合从事配电网规划、运行与可靠性分析相关工作的人员; 使用场景及目标:①掌握序贯蒙特卡洛模拟法在电力系统可靠性评估中的基本原理与实现流程;②学习如何通过Matlab构建配电网仿真模型并进行状态转移模拟;③应用于含新能源接入的复杂配电网可靠性定量评估与优化设计; 阅读建议:建议结合文中提供的Matlab代码逐段调试运行,理解状态抽样、故障判断、修复逻辑及指标统计的具体实现方式,同时可扩展至不同网络结构或加入更多不确定性因素进行深化研究。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值