76、内核同步 - 第二部分

内核同步 - 第二部分

1. RCU详细文档

要获取关于RCU及其在内核中的实现和使用的更深入细节,可以参考官方内核文档:
- RCU概念文档 :6.1内核系列的RCU文档“根”文件(index.rst)可在 此处 找到。其中列出了一个包含一些文档的Design目录,以及其他几个RCU文档(通常为.rst格式)。
- 经典RCU介绍 What is RCU? 是一个很好的起点,它是著名的六篇LWN系列文章的集合。
- RCU设计文档 :深入了解RCU设计结构,可查看 RCU Design ,其中的 Requirements部分 尤其有用。
- RCU特定数据结构使用文档
- 保护读多写少的链表 Using RCU to Protect Read-Mostly Linked Lists 提供了在内核中如何使用RCU的详细逐步示例。
- RCU列表API总结 The RCU API tables, 2019 edition 以表格形式总结了RCU列表API。
- 保护读多写少的数组 Using RCU to Protect Read-Mostly Arrays
- TREE_RCU数据结构 A Tour through TREE_RCU’s Data Structures [LWN.net]

此外,还有更多关于RCU的进一步阅读材料,可在 这里 查看。

理解基本的RCU概念后,查看核心RCU API(或原语)是个不错的选择,可参考 此处

2. 调试RCU使用情况

当需要调试内核(或模块)中RCU的使用情况时,可以通过以下步骤查看相关配置选项:
1. 执行 make menuconfig
2. 导航到 Kernel hacking > RCU Debugging 子菜单。
3. 查看其中的各种RCU调试配置选项。相关的Kconfig文件是 kernel/rcu/Kconfig.debug 点击查看 。在调试/开发内核中启用这些选项非常有用,有助于捕获与RCU相关的错误。

设置 CONFIG_RCU_EXPERT=y 可以启用更多RCU调试选项(如 CONFIG_PROVE_RCU_LIST ),该选项可在 General setup > RCU Subsystem 子菜单中找到。

3. 内核中RCU的使用情况

Paul E. McKenney提供了 RCU Linux Usage 页面,其中包含各种图表展示了RCU在Linux内核中的使用情况。以下是一个简单的示例表格:
| 日期 | RCU调用次数 | 常规锁调用次数 |
| ---------- | ----------- | -------------- |
| 31-Oct-2002 | 5 | 28,248 |
| 29-Nov-2006 | 1,147 | 53,463 |
| 08-Dec-2014 | 10,040 | 122,705 |
| 24-Dec-2018 | 15,167 | 143,428 |
| 30-Oct-2023 | 20,355 | 165,860 |
| 08-Jan-2024 | 20,761 | 165,602 |

常规锁原语(如自旋锁、互斥锁和信号量)的使用更为频繁,因为RCU是一种专门的同步原语,适用于特定条件:
- 适用场景 :读多写少的场景,更新(写入)操作较少,并且读者可以接受稍微陈旧或不一致的数据。
- 不适用场景 :可参考 Paul的“RCU Area of Applicability”幻灯片

4. 实时(RT)与无锁技术

实时环境通常与无锁编程冲突,因为大多数无锁技术涉及内核空间中的非抢占式代码路径,这会极大增加实时线程的调度延迟。对于RCU,其读端关键部分必须是非抢占式的,这与实时环境冲突。因此,对于实时Linux(RTL)内核,默认应用了RCU优先级提升功能,详细信息可查看 Priority-Boosting RCU Read-Side Critical Sections

启用 CONFIG_PREEMPTION 可以解决实时Linux中使用RCU的问题,此外, General setup/RCU Subsystem/Offload RCU callback processing from boot-selected CPUs (CONFIG_RCU_NOCB_CPU=y) 等选项也有助于实时场景。

5. 内核中的锁调试

内核中的锁和同步设计与实现可能会变得复杂,增加了潜在错误的可能性。内核有多种方法来帮助调试这些问题,尤其是死锁问题。

调试可能发生在软件开发生命周期(SDLC)的不同阶段:
- 开发期间
- 开发后但发布前(测试、质量保证等)
- 内部发布后
- 发布后在实际环境中

越早发现并修复错误,成本越低。以下是一些在开发内核/模块时调试锁问题的工具和技术。

6. 配置调试内核进行锁调试

参考Linux内核补丁提交检查清单文档中的相关要点,配置调试内核进行锁调试:
- 确保已阅读同步、锁和死锁指南的基础知识。
- 运行专门为开发/调试目的配置的调试内核。

以下是一些需要同时启用的配置选项:
- CONFIG_PREEMPT
- CONFIG_DEBUG_PREEMPT
- CONFIG_DEBUG_SLAB
- CONFIG_DEBUG_PAGEALLOC
- CONFIG_DEBUG_MUTEXES
- CONFIG_DEBUG_SPINLOCK
- CONFIG_DEBUG_ATOMIC_SLEEP
- CONFIG_PROVE_RCU
- CONFIG_DEBUG_OBJECTS_RCU_HEAD

同时,还需要在有和没有 CONFIG_SMP CONFIG_PREEMPT 的情况下进行构建和运行时测试,并在启用所有lockdep功能的情况下执行所有代码路径。

另外,有两个强大的工具:
- KASAN :内核地址消毒剂(Kernel Address SANitizer),使用编译时插桩的动态分析来捕获常见的内存相关错误,自4.0内核开始支持x86_64,4.4内核开始支持AArch64(ARM64)。
- KCSAN :内核并发消毒剂(Kernel Concurrency Sanitizer),是一个用于Linux内核的数据竞争检测器,通过编译时插桩工作。但KASAN与KCSAN冲突,需要相互独立使用,详细的启用和使用说明可参考《Linux Kernel Debugging》一书。

在用户空间,也有一些工具可用于捕获锁错误和死锁,如helgrind、TSan、lockdep(可作为库在用户空间工作),以及现代[e]BPF框架提供的 deadlock-bpfcc(8) 前端,用于查找给定运行进程(或线程)中的潜在死锁。

7. 内核锁调试配置选项

在6.1.25内核源代码树的根目录下,执行 make menuconfig ,导航到 Kernel hacking > Lock Debugging (spinlocks, mutexes, etc...) 菜单。也可以查看 lib/Kconfig.debug 文件来获取帮助信息, 点击查看

以下是一些常见的内核锁(和RCU)调试配置选项及其作用:
| 锁调试菜单标题 | 作用 |
| — | — |
| Lock debugging: prove locking correctness (CONFIG_PROVE_LOCKING), aka lockdep! | 这是lockdep内核选项,开启后可随时获得“锁正确性的滚动证明”,能在实际发生之前报告任何与锁相关的死锁可能性。 |
| Lock usage statistics (CONFIG_LOCK_STAT) | 跟踪锁争用点。 |
| RT mutex debugging, deadlock detection (CONFIG_DEBUG_RT_MUTEXES) | 允许自动检测和报告rt互斥锁语义违规和rt互斥锁相关的死锁(锁定)。 |
| Spinlock and rw-lock debugging: basic checks (CONFIG_DEBUG_SPINLOCK) | 开启此选项(连同CONFIG_SMP)有助于捕获缺失的自旋锁初始化和其他常见的自旋锁错误,并开启不可屏蔽中断(NMI)看门狗。 |
| Mutex debugging: basic checks (CONFIG_DEBUG_MUTEXES) | 允许检测和报告互斥锁语义违规。 |
| RW semaphore debugging: basic checks (CONFIG_DEBUG_RWSEMS) | 允许检测和报告不匹配的读写信号量锁定和解锁操作。 |
| Lock debugging: detect incorrect freeing of live locks (CONFIG_DEBUG_LOCK_ALLOC) | 检查内核是否通过任何内存释放例程(kfree()、kmem_cache_free()、free_pages()、vfree()等)错误地释放了任何持有锁(自旋锁、读写锁、互斥锁或读写信号量),是否通过spin_lock_init()/mutex_init()等错误地重新初始化了活动锁,或者在任务退出期间是否持有任何锁。 |
| Sleep inside atomic section checking (CONFIG_DEBUG_ATOMIC_SLEEP) | 如果选择Y,当在原子部分(如持有自旋锁、在rcu读端关键部分、在抢占禁用部分、在中断内等)调用可能睡眠的各种例程时,会产生大量警告信息。 |
| Torture tests for locking (CONFIG_LOCK_TORTURE_TEST) | 提供一个内核模块,对内核锁原语进行折磨测试。该内核模块可以根据需要在要测试的运行内核上事后构建,可选择内联构建(Y)或作为外部模块构建(M)。 |
| RCU list lockdep debugging (CONFIG_PROVE_RCU_LIST) | 启用RCU锁依赖检查用于列表使用。默认情况下该选项关闭,因为有几个列表RCU用户仍需要转换以通过锁依赖表达式。为防止误报,默认禁用该选项,但一旦所有用户都转换完成,就可以移除该配置选项。 |
| Debug RCU callbacks objects (CONFIG_DEBUG_OBJECTS_RCU_HEAD) | 开启此选项以对RCU列表头(call_rcu()使用)进行调试。 |

在开发和测试期间,在调试内核中开启所有或大多数这些锁调试选项是个不错的主意,但启用内核调试配置可能会显著减慢执行速度并使用更多内存,需要在速度和错误检测之间进行权衡。

8. 锁验证器lockdep - 早期捕获锁问题

Linux内核的lockdep是一个运行时锁正确性或锁依赖验证器。其基本原理是,只要内核中发生任何锁操作(获取或释放任何内核级锁,或涉及多个锁的锁序列),lockdep就会跟踪或映射这些操作。

lockdep的优点:
- 数学证明 :能够实现100%的数学证明,确定锁序列是否正确。对于内核生命周期内至少发生一次的每个简单、独立的单任务锁序列,验证器可以100%确定这些锁序列的任何组合和时间安排都不会导致任何类别的锁相关死锁。但需要注意一些已知问题,参考 相关文档
- 错误警告 :通过 WARN*() 宏警告违反以下类别的锁错误:死锁/锁反转场景、循环锁依赖以及硬IRQ/软IRQ安全/不安全锁错误。

锁序列 :指在某个代码路径上,先获取锁A,然后获取锁B,这就是一个锁序列或锁链。lockdep会跟踪所有锁及其锁链,可通过 /proc/lockdep_chains 查看。

性能优化 :由于内核中存在大量锁实例,验证每个锁序列会非常缓慢(时间复杂度为O(N^2)),因此lockdep只在锁序列首次出现时进行验证,通过维护一个64位哈希来识别。

即使在运行时没有实际发生死锁,lockdep也会发出关于数学上不正确锁操作的警告,提示可能存在的问题。当然,也需要注意lockdep本身可能存在的错误,可通过 CONFIG_DEBUG_LOCKDEP 配置选项进行调试。

在用户空间,有一些原始的方法来检测可能的死锁场景:
- 使用GNU ps(1)命令: ps -LA -o state,pid,cmd | grep "^D" 打印处于D(不可中断睡眠,TASK_UNINTERRUPTIBLE)状态的线程,长时间处于该状态可能表示存在死锁,但不保证。
- 使用strace(1)和ltrace(1):分别提供进程(或线程)发出的每个系统调用和库调用的详细跟踪信息,可用于捕获挂起的进程/线程并查看其卡住的位置,例如 strace -p <PID> 对挂起的进程可能特别有用。

内核同步 - 第二部分

9. 锁调试工具的使用流程

为了更清晰地展示在开发过程中如何使用上述提到的锁调试工具和技术,下面给出一个简单的流程图:

graph LR
    A[开始] --> B[配置调试内核]
    B --> C{选择调试工具}
    C -->|KASAN| D[启用KASAN配置]
    C -->|KCSAN| E[启用KCSAN配置]
    C -->|lockdep| F[启用lockdep相关配置]
    D --> G[运行内核并测试]
    E --> G
    F --> G
    G --> H{是否发现问题}
    H -->|是| I[分析问题并修复]
    H -->|否| J[继续开发测试]
    I --> G

具体操作步骤如下:
1. 配置调试内核 :按照前面提到的要求,启用一系列相关的配置选项,如 CONFIG_PREEMPT CONFIG_DEBUG_PREEMPT 等。
2. 选择调试工具 :根据具体需求选择KASAN、KCSAN或lockdep等工具。
3. 启用相应配置 :如果选择KASAN,启用KASAN相关配置;选择KCSAN,启用KCSAN相关配置;选择lockdep,启用lockdep相关配置。
4. 运行内核并测试 :在配置好的调试内核上运行内核并进行相关测试。
5. 检查是否发现问题 :观察测试过程中是否出现与锁相关的问题。
6. 分析问题并修复 :如果发现问题,使用相应工具提供的信息分析问题所在,并进行修复。修复后再次运行内核进行测试,直到问题解决。
7. 继续开发测试 :如果没有发现问题,继续进行开发和测试工作。

10. 不同调试工具的特点对比

为了帮助开发者更好地选择合适的调试工具,下面对KASAN、KCSAN和lockdep这三种常见的调试工具进行特点对比:
| 工具名称 | 主要功能 | 适用场景 | 优缺点 |
| — | — | — | — |
| KASAN | 使用编译时插桩的动态分析来捕获常见的内存相关错误 | 主要用于检测内存访问越界、使用已释放内存等内存相关问题 | 优点:能有效检测多种内存错误;缺点:与KCSAN冲突,不能同时使用 |
| KCSAN | 是一个用于Linux内核的数据竞争检测器,通过编译时插桩工作 | 主要用于检测内核中的数据竞争问题 | 优点:专门针对数据竞争问题进行检测;缺点:与KASAN冲突,不能同时使用 |
| lockdep | 运行时锁正确性或锁依赖验证器,能检测锁相关的死锁、锁反转等问题 | 主要用于检测锁使用过程中的正确性问题,如死锁、循环锁依赖等 | 优点:能提供数学证明,提前发现潜在的锁问题;缺点:可能存在误报情况,需要人工进一步判断 |

11. 总结与建议

在处理内核同步和锁调试问题时,需要综合运用各种工具和技术。以下是一些总结和建议:
- RCU的使用 :RCU是一种专门的同步原语,适用于读多写少的场景。在使用RCU时,要确保理解其适用条件和核心API,同时可以利用内核提供的调试选项来排查问题。
- 锁调试工具的选择 :根据具体的问题类型和场景,选择合适的调试工具。KASAN适用于内存相关问题,KCSAN适用于数据竞争问题,lockdep适用于锁正确性问题。在使用过程中,要注意不同工具之间的冲突问题。
- 早期发现问题 :尽早发现并修复锁相关的问题可以大大降低开发成本。使用lockdep等工具可以在开发阶段就发现潜在的死锁和锁错误,避免在后期出现难以调试的问题。
- 性能权衡 :启用内核调试配置可能会显著减慢执行速度并使用更多内存,因此需要在速度和错误检测之间进行权衡。在开发和测试阶段,可以适当牺牲一些性能来确保代码的正确性。

通过合理运用这些工具和技术,开发者可以更高效地解决内核同步和锁调试问题,提高内核代码的稳定性和可靠性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值