【问题标题】:Deciding the critical section of kernel code确定内核代码的关键部分
【发布时间】:2016-02-16 11:33:54
【问题描述】:

您好,我正在编写旨在进行进程调度和多线程执行的内核代码。我研究了锁定机制及其功能。关于关键部分中的哪种数据结构应该通过锁定(互斥锁/信号量/自旋锁)来保护,是否有经验法则?

我知道,只要部分代码有并发的机会,我们就需要锁定。但是我们如何决定,如果我们错过并且测试用例没有抓住它们怎么办。早些时候,我为系统调用和文件系统编写了代码,我从不关心获取锁。

【问题讨论】:

    标签: multithreading concurrency linux-kernel kernel


    【解决方案1】:

    关于临界区中的哪种数据结构应该通过锁定来保护,是否有一个经验法则?

    任何对象(全局变量、结构对象的字段等),并发访问一个访问是写访问需要一些锁定规则以进行访问。

    但是我们如何决定,如果我们错过并且测试用例没有捕捉到它们怎么办?

    良好的做法是对变量、结构或结构字段的每个声明进行适当的注释,这需要锁定规则才能访问。使用此变量的任何人都可以阅读此评论并编写相应的代码以供访问。内核和模块倾向于遵循这种策略。

    对于测试,常见的测试很少会发现并发问题,因为它们的概率很低。在测试内核模块时,我建议使用Kernel Strider,它试图证明并发内存访问的正确性或RaceHound,它增加并发问题的可能性并检查它们。

    【讨论】:

      【解决方案2】:

      在访问任何共享数据的任何代码期间获取锁是始终安全的,但这很慢,因为这意味着一次只有一个线程可以运行大量代码。

      但根据相关数据,可能存在安全快速的快捷方式。如果它是一个简单的整数(整数是指 CPU 的本机字长,即不是 32 位 cpu 上的 64 位),那么您可能不需要做任何锁定:如果一个线程尝试写入整数,而另一个同时读取它,读取器将获得旧值或新值,绝不是两者的混合。如果读者不关心他得到了旧值,那么就不需要锁。

      但是,如果您同时更新两个整数,而读取一个整数的新值和另一个整数的旧值对读者来说是不利的,那么您需要一个锁。另一个例子是如果线程正在递增整数。这通常涉及读取、添加和写入。如果一个读取旧值,然后另一个设法读取、添加和写入新值,然后第一个线程添加和写入新值,两者都认为他们已经增加了变量,但不是被增加两次,而是只增加一次。这需要一个锁,或者使用原子增量原语来确保读/修改/写周期不会被中断。还有原子测试和设置原语,因此您可以读取一个值,对其进行一些数学运算,然后 try 将其写回,但只有在它仍然保持原始值时写入才会成功。也就是说,如果另一个线程在您读取它之后对其进行了更改,那么测试和设置将失败,那么您可以丢弃您的新值并重新开始读取另一个线程设置的值并尝试测试和设置- 重新设置。

      指针实际上只是整数,所以如果你设置了一个数据结构,然后将一个指向它的指针存储在另一个线程可以找到它的地方,你不需要锁,只要你完全设置了结构之前 您将其地址存储在指针中。另一个读取指针的线程(它需要确保只读取一次指针,即将它存储在一个局部变量中,然后从那时起只使用它来引用结构)将看到新结构或旧结构一个,但绝不是中间状态。如果大多数线程只通过指针读取结构,并且任何想要写入的线程要么使用锁,要么使用指针的原子测试和设置,这就足够了。但是,无论何时您想修改结构的任何成员,都必须将其复制到新成员,更改新成员,然后更新指针。这本质上就是内核的 RCU(读取、复制、更新)机制的工作原理。

      【讨论】:

        【解决方案3】:

        理想情况下,您必须在设计期间枚举系统中所有可用的资源、相关线程和通信、共享机制。为每个资源确定以下内容并在进行更改时维护适当的检查清单会很有帮助:

        1. 资源繁忙的持续时间(资源利用率)和锁定类型
        2. 在该特定资源(负载)和优先级上排队的任务数量
        3. 与资源相关的通信类型、共享机制
        4. 与资源相关的错误情况

        如果可能,最好有一个流程图来描述资源、利用率、锁、负载、通信/共享机制和错误。

        此过程可以帮助您确定缺少的场景/未知数、关键部分以及识别瓶颈。

        除了上述过程之外,您可能还需要某些工具来帮助您进行测试/进一步分析,以排除隐藏的问题(如果有):

        1. Helgrind - 用于检测同步错误的 Valgrind 工具。 这有助于识别由于数据竞争/同步问题 对于不正确的锁定,可能导致死锁的锁定顺序和 POSIX 线程 API 使用不当也会产生以后的影响。 参考:http://valgrind.org/docs/manual/hg-manual.html
        2. Locksmith - 用于确定期间可能出现的常见锁定错误 运行时或可能导致死锁。
        3. ThreadSanitizer - 用于检测比赛条件。应显示所有访问涉及的所有访问和锁定。
        4. Sparse 可以帮助列出函数获取和释放的锁,以及识别用户地址空间指针和内核地址空间指针混合等问题。
        5. Lockdep - 用于调试锁
        6. iotop - 用于通过监控内核输出的 I/O 使用信息来确定系统上进程或线程的当前 I/O 使用情况。
        7. LTTng - 用于跟踪竞争条件和可能的中断级联。 (LTT 的继任者 - kprobes、tracepoint 和 perf 功能的组合)
        8. Ftrace - 用于分析/调试延迟和性能相关问题的 Linux 内核内部跟踪器。
        9. lsoffuser 可以方便地确定具有锁的进程和锁的类型。

        分析可以帮助确定内核花费时间的确切位置。这可以使用perfOprofile 等工具来完成。 strace 可以拦截/记录进程调用的系统调用以及进程接收到的信号。它应该显示事件的顺序和调用的所有返回/恢复路径。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-10-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多