78、内核同步 - 第二部分

内核同步 - 第二部分

在多线程和多核环境中,内核同步是确保系统稳定运行的关键。本文将深入探讨内核同步的几个重要方面,包括锁依赖报告、锁依赖注解、锁依赖已知问题、内核锁统计以及内存屏障。

锁依赖报告

锁依赖(lockdep)报告是一个强大的工具,它能够检测并报告内核中的锁使用问题。当检测到“可能的循环锁依赖”时,报告的初始部分会提示我们。接着,它会展示运行时实际发生的情况。

例如,线程 0(表示为 thrd_0/0/3578,包含线程名和 PID)尝试获取锁(64 位哈希值 ffffffffc06c80b8,符号名为 lockB),但该线程已经持有了另一个锁。报告还会显示锁在同一模块的同一函数中被持有,这清晰地表明了一个循环(AB - BA)死锁场景。此外,报告还会展示导致死锁的内核调用栈,帮助我们明确是哪些代码路径引发了死锁。

报告还会通过一个典型场景展示死锁的根本原因,例如在 CPU 核心 0 上的内核线程先获取了 lockA,而在核心 1 上的另一个内核线程先获取了 lockB,然后两个线程又分别尝试获取对方持有的锁,从而导致了 AB - BA 循环死锁。这种情况可能会导致软锁定(soft lockup)错误,由内核看门狗检测到。

需要注意的是,锁依赖报告只会在首次违反任何内核锁规则时进行报告。

锁依赖注解

在用户空间,我们熟悉使用 assert() 宏来检查运行时条件。类似地,锁依赖提供了 lockdep_assert_held() 宏,允许内核开发者断言在特定点持有某个锁,这被称为锁依赖注解。

以下是相关宏的定义:

// include/linux/lockdep.h
#define lockdep_assert(cond)            \
        do { WARN_ON(debug_locks && !(cond)); } while (0)
[ … ]
#define lockdep_assert_held(l)          \
        lockdep_assert(lockdep_is_held(l) != LOCK_STATE_NOT_HELD)

当断言失败时,会通过 WARN_ON() 发出警告,这意味着本应持有该锁,但实际上并没有。这些断言仅在启用锁调试时生效,内核代码库中广泛使用了锁依赖注解。

对于 RCU 读侧锁, rcu_read_lock_held() 等 API 可以用于检查是否处于 RCU 读侧临界区。

锁依赖已知问题

在使用锁依赖时,可能会遇到以下几个已知问题:
1. 重复加载和卸载模块 :重复加载和卸载内核模块可能会导致锁依赖的内部锁类限制被超出。因为加载模块会为其所有锁创建新的锁类,而卸载模块时这些锁类不会被移除,而是被重用。因此,应避免重复加载和卸载模块,否则可能需要重置系统。
2. 锁初始化问题 :当数据结构包含大量锁时,如果未能正确初始化每个锁,可能会导致锁依赖的锁类溢出。
3. 误报问题 :锁依赖通过开发者编写的注解来定义“规则”,当检测到规则未被遵循时会生成警告。但这种方法有时会产生误报。不过,产生误报总比没有任何提示而导致系统真正死锁要好。解决方法是“修复锁的使用,而不是修复锁依赖的误报”。
4. 锁调试禁用 :当锁调试被禁用时, debug_locks 整数会被设置为 0,可能会出现“ WARNING lock debugging disabled!!”的警告。这可能是由于之前锁依赖发出的警告导致的,此时需要重启系统并重新尝试。

内核锁统计

锁可能会被竞争,即一个上下文想要获取锁,但锁已经被占用,需要等待解锁。严重的锁竞争会导致性能瓶颈,内核提供了详细的锁统计信息,帮助我们识别竞争激烈的锁,这在性能分析时非常有用。

要启用锁统计,需要开启 CONFIG_LOCK_STAT 内核配置选项。如果未选择该选项, /proc/lock_stat 伪文件将不存在。

锁统计代码利用了锁依赖在锁代码路径中插入的钩子( __contended __acquired __released )来收集关键数据。相关的内核文档通过状态图详细介绍了这些信息。

以下是查看和操作内核锁统计的命令:
| 操作 | 命令(以 root 权限运行) |
| ---- | ---- |
| 清除锁统计 | echo 0 > /proc/lock_stat |
| 启用锁统计收集 | echo 1 > /proc/sys/kernel/lock_stat |
| 禁用锁统计收集 | echo 0 > /proc/sys/kernel/lock_stat |
| 查看当前锁统计 | cat /proc/lock_stat |

下面是一个简单的 Bash 脚本示例,用于演示如何使用锁统计:

clear_lock_stats() {
  echo 0 > /proc/lock_stat
}
enable_lock_stats() {
  echo 1 > /proc/sys/kernel/lock_stat
}
disable_lock_stats() {
  echo 0 > /proc/sys/kernel/lock_stat
}
view_lock_stats() {
  cat /proc/lock_stat
}

脚本首先禁用锁统计,然后运行一个测试用例,即加载之前的 deadlock_eg_AB - BA 模块(但不会导致循环死锁)。在插入模块之前,脚本会清除并启用锁统计,完成后禁用锁统计并将结果写入文件 lockstats.txt

解释锁统计结果时,需要注意以下几点:
1. 大多数锁统计列仅适用于锁名后面带有冒号 : 的行,没有冒号的行通常表示锁竞争点。
2. 显示的时间单位是微秒,第一列的类名是锁类,即锁的符号名。
3. contentions :需要等待的锁竞争次数。
4. con - bounces :涉及跨 CPU 数据的锁竞争次数,这是一种开销,实际的锁获取次数可能会更少。
5. 锁竞争点出现在两条短分隔线 ----- 之间,其行内容与列标题不匹配,格式为锁名、 con - bounces 和调用点。
6. 接下来是各种锁的等待时间( waittime* )。
7. acquisitions :锁被获取的总次数,为正数。
8. 最后四列是累积的锁持有时间统计( holdtime - {min|max|total|avg} )。

需要注意的是,锁统计可能会因锁调试被禁用而失效,此时可能会看到警告信息,需要重启系统。

内存屏障

有时,微处理器、内存控制器和编译器可能会对内存读写进行重新排序,这在大多数情况下可以优化性能,但在某些情况下,需要确保内存加载和存储序列按照程序员的意图执行,这时就需要使用内存屏障。

内存屏障通常通过 *mb*() 宏实现,用于抑制这种重新排序,强制 CPU、内存控制器和编译器按照期望的顺序处理指令和数据。

可以使用以下宏在代码路径中插入内存屏障,需要包含 <asm/barrier.h> 头文件:
- rmb() :在指令流中插入读(或加载)内存屏障。
- wmb() :在指令流中插入写(或存储)内存屏障。
- mb() :通用内存屏障,确保在屏障之前指定的所有加载和存储操作在系统的其他组件看来,会在屏障之后的所有加载和存储操作之前发生。

在某些情况下,如使用 DMA 时,驱动作者可能会使用内存屏障。例如,在 Raspberry Pi 上,参考 Broadcom BCM2835 系列的外设手册可以帮助确定何时使用内存屏障。

设备驱动中使用内存屏障的示例

以 Realtek 8139 “快速以太网”网络驱动为例,在通过 DMA 传输网络数据包时,需要设置 DMA 传输描述符对象。

以下是 DMA 描述符对象的定义:

// drivers/net/ethernet/realtek/8139cp.c
struct cp_desc {
    __le32 opts1;
    __le32 opts2;
    __le64 addr;
};

为了确保 DMA 控制器正确解释描述符,需要使用内存屏障来保证写入操作的顺序。例如,设置 DMA 描述符时:

desc->word0 = address;
wmb();
desc->word1 = DESC_VALID;

在实际的驱动代码中,也使用了 wmb() 写内存屏障来保证写入顺序:

// drivers/net/ethernet/realtek/8139cp.c
[ ... ]
static netdev_tx_t cp_start_xmit([...])
{
    [ ... ]
    len = skb->len;
    mapping = dma_map_single(&cp->pdev->dev, skb->data, len, PCI_DMA_TODEVICE);
    [ ... ]
    struct cp_desc *txd;
    [ ... ]
    txd->opts2 = opts2;
    txd->addr = cpu_to_le64(mapping);
    wmb();
    opts1 |= eor | len | FirstFrag | LastFrag;
    txd->opts1 = cpu_to_le32(opts1);
    wmb();
    [...]

综上所述,内核同步涉及多个方面,包括锁依赖、锁统计和内存屏障等,正确使用这些技术可以提高系统的稳定性和性能。

内核同步 - 第二部分

内存屏障的应用场景分析

内存屏障在多种场景下发挥着重要作用,下面通过流程图进一步展示其常见应用场景。

graph LR
    A[内存屏障应用场景] --> B[跨硬件边界]
    A --> C[原子操作]
    A --> D[外设访问]
    A --> E[硬件中断]
    B --> B1[多核系统CPU核心间]
    D --> D1[CPU与外设DMA通信]
  1. 跨硬件边界 :在多核系统中,不同的 CPU 核心可能会并行执行任务,对共享内存的访问可能会出现乱序。例如,一个核心正在写入数据,而另一个核心可能会在数据未完全写入时就尝试读取。内存屏障可以确保每个核心看到的内存操作顺序是一致的,避免数据不一致的问题。
  2. 原子操作 :原子操作通常需要保证操作的完整性,即操作要么全部完成,要么完全不执行。内存屏障可以防止编译器和处理器对原子操作进行重排序,确保原子操作的正确性。
  3. 外设访问 :当 CPU 与外设进行通信时,特别是通过 DMA 进行数据传输时,内存屏障非常重要。如前面提到的 Realtek 8139 网络驱动,正确使用内存屏障可以确保 DMA 控制器正确解释描述符,保证数据的正确传输。
  4. 硬件中断 :在处理硬件中断时,内存屏障可以确保中断处理程序看到的内存状态是一致的。例如,在中断处理程序中访问共享数据时,使用内存屏障可以防止数据的不一致性。
锁统计与性能优化

通过内核锁统计,我们可以深入了解系统中锁的使用情况,从而进行性能优化。以下是一个简单的性能优化流程:

graph LR
    A[开始] --> B[启用锁统计]
    B --> C[运行测试用例]
    C --> D[查看锁统计结果]
    D --> E{是否存在高竞争锁}
    E -- 是 --> F[分析高竞争锁代码]
    E -- 否 --> G[结束]
    F --> H[优化锁使用]
    H --> C
  1. 启用锁统计 :开启 CONFIG_LOCK_STAT 内核配置选项,并使用 echo 1 > /proc/sys/kernel/lock_stat 命令启用锁统计收集。
  2. 运行测试用例 :选择具有代表性的测试用例,让系统在正常运行过程中产生锁竞争。例如,可以运行之前提到的 deadlock_eg_AB - BA 模块。
  3. 查看锁统计结果 :使用 cat /proc/lock_stat 命令查看锁统计信息,并分析哪些锁的竞争比较激烈。
  4. 分析高竞争锁代码 :对于竞争激烈的锁,深入分析其所在的代码,找出可能导致竞争的原因。例如,是否存在不必要的锁持有时间过长,或者是否可以使用更细粒度的锁。
  5. 优化锁使用 :根据分析结果,对锁的使用进行优化。例如,可以减少锁的持有时间,或者将大锁拆分为多个小锁。
  6. 重复测试 :优化后再次运行测试用例,查看锁统计结果,验证优化效果。
锁依赖与代码审查

锁依赖不仅可以帮助我们发现死锁和其他锁相关的问题,还可以作为代码审查的重要工具。在代码审查过程中,可以关注以下几个方面:
1. 锁依赖注解的使用 :检查代码中是否正确使用了锁依赖注解,如 lockdep_assert_held() 等。确保注解能够准确反映锁的使用规则。
2. 锁的初始化 :检查所有锁是否都被正确初始化,特别是在数据结构包含大量锁的情况下。避免因锁初始化问题导致锁依赖的锁类溢出。
3. 锁的顺序 :确保锁的获取顺序是一致的,避免出现循环死锁的情况。例如,在多个线程中,都应该按照相同的顺序获取锁。
4. RCU 读侧锁 :如果代码中使用了 RCU 读侧锁,检查是否正确使用了 rcu_read_lock_held() 等 API 来检查是否处于 RCU 读侧临界区。

总结

内核同步是操作系统开发中的一个重要领域,涉及到锁依赖、锁统计和内存屏障等多个方面。通过合理使用这些技术,可以提高系统的稳定性和性能。

锁依赖可以帮助我们检测死锁和其他锁相关的问题,但在使用过程中需要注意可能出现的已知问题。锁统计可以让我们了解系统中锁的竞争情况,从而进行性能优化。内存屏障则可以确保内存操作的顺序,避免因内存重排序导致的问题。

在实际开发中,我们应该充分利用这些技术,进行代码审查和性能优化,确保系统的可靠性和高效性。同时,对于不同的硬件平台和应用场景,需要根据具体情况选择合适的同步机制和优化策略。

总之,内核同步是一个复杂而重要的领域,需要我们不断学习和实践,以应对各种挑战。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值