In continuation of the previous text:第二章:模块的编译与运行-3, let's GO ahead .
Concurrency in the Kernel
One way in which kernel programming differs greatly from conventional application programming is the issue of concurrency. Most applications, with the notable excep-tion of multithreading applications, typically run sequentially, from the beginning to the end, without any need to worry about what else might be happening to changetheir environment. Kernel code does not run in such a simple world, and even the simplest kernel modules must be written with the idea that many things can be hap-pening at once.
内核编程与传统应用程序编程存在一个极大差异,那就是并发(concurrency)问题。
大多数应用程序(多线程应用程序是明显例外)通常是从开始到结束按顺序执行的,无需担心其他操作会干扰并改变自身的运行环境。但内核代码的运行环境远非如此简单 —— 即便最简单的内核模块,在编写时也必须考虑到:多个操作可能会同时发生。
There are a few sources of concurrency in kernel programming. Naturally, Linux systems run multiple processes, more than one of which can be trying to use your driver at the same time. Most devices are capable of interrupting the processor; interrupt handlers run asynchronously and can be invoked at the same time that your driver is trying to do something else. Several software abstractions (such as kernel timers, introduced in Chapter 7) run asynchronously as well. Moreover, of course, Linux can run on symmetric multiprocessor (SMP) systems, with the result that your driver could be executing concurrently on more than one CPU. Finally, in 2.6, kernel code has been made preemptible; this change causes even uniprocessor systems to have
many of the same concurrency issues as multiprocessor systems.
内核编程中的并发存在若干来源,具体如下:
首先,Linux 系统会运行多个进程,其中可能有多个进程同时尝试使用你的驱动程序。其次,大多数设备都具备中断处理器的能力 —— 中断处理程序会异步运行,且可能在你的驱动程序正试图执行其他操作时被调用。此外,一些软件抽象(如第 7 章将介绍的内核定时器)同样会异步运行。
当然,Linux 还可运行在对称多处理器(SMP) 系统上,这意味着你的驱动程序可能会在多个 CPU 上并发执行。最后,在 2.6 版本的内核中,代码被设计为可抢占的(preemptible);这一改动使得即便是单处理器系统,也会面临许多与多处理器系统相同的并发问题。
补充说明:
-
内核并发的核心来源内核需要同时处理多个任务,这些任务会导致代码 “并发执行”,主要来源包括:
-
多处理器(SMP)架构:多个 CPU 核心可同时执行内核代码,若模块未做并发控制,不同核心可能同时操作同一份数据;
-
进程调度:内核代码(如系统调用处理逻辑)可能在执行过程中被调度器暂停,转而执行其他进程的内核代码,后续恢复执行时,数据或环境可能已被修改;
-
中断与软中断:内核在执行模块代码时,可能被硬件中断(如网卡接收数据、磁盘 IO 完成)或软中断(如内核定时器)打断,中断处理程序会优先执行,若中断处理逻辑与模块代码操作同一份资源,就会引发并发冲突。
-
-
并发的潜在风险若模块未处理并发问题,可能导致 “竞态条件”(race condition)—— 即代码执行结果依赖于多个并发操作的执行顺序,进而引发数据错乱、内存泄漏、系统崩溃等严重问题。例如:模块中的全局变量若被多个并发执行的代码路径修改,可能出现 “值覆盖”(一个操作写入的值被另一个操作覆盖),导致逻辑错误。
-
与应用程序并发的区别应用程序的多线程并发可通过编程语言提供的工具(如 Java 的
synchronized、C++ 的std::mutex)控制,且线程运行在用户空间,权限较低,即便出现问题也通常局限于应用程序自身;而内核并发需依赖内核提供的专用机制(如自旋锁spinlock、互斥锁mutex),且内核代码直接操作系统资源,并发控制不当会影响整个系统的稳定性。
As a result, Linux kernel code, including driver code, must be reentrant—it must be capable of running in more than one context at the same time. Data structures must be carefully designed to keep multiple threads of execution separate, and the code must take care to access shared data in ways that prevent corruption of the data. Writing code that handles concurrency and avoids race conditions (situations in which an unfortunate order of execution causes undesirable behavior) requires thought and can be tricky. Proper management of concurrency is required to write correct kernel code; for that reason, every sample driver in this book has been written with concurrency in mind. The techniques used are explained as we come to them; Chapter 5 has also been dedicated to this issue and the kernel primitives avail-able for concurrency management.
因此,Linux 内核代码(包括驱动程序代码)必须是可重入的(reentrant)—— 即能够同时在多个上下文中运行。数据结构必须经过精心设计,以分隔多个执行线程,并且代码必须注意以防止数据损坏的方式访问共享数据。
编写能够处理并发并避免竞态条件(即因执行顺序不当而导致不良行为的情况)的代码需要深思熟虑,而且颇具挑战性。正确管理并发是编写正确内核代码的必要条件;因此,本书中的每个示例驱动程序在编写时都考虑了并发问题。我们会在涉及相关技术时进行解释;第 5 章还专门讨论了这个问题以及用于并发管理的内核原语。
补充说明:
-
可重入性(reentrancy)的核心含义指同一段代码可被多个执行路径(如不同进程的系统调用、中断处理程序)同时调用,且不会因共享资源访问导致错误。例如,驱动的
read函数若被两个进程同时调用,需保证两者的执行不会相互干扰(如通过参数区分不同进程的请求,或对共享数据加锁)。 -
竞态条件(race condition)的危害当多个执行路径无序访问共享资源时,可能导致数据不一致。例如:驱动中记录 “设备已打开次数” 的变量
open_count,若两个进程同时执行open_count++,可能因指令交错(先读取旧值,再各自加 1 后写入)导致最终结果少加 1,造成设备状态管理错误。 -
内核并发管理原语内核提供了多种工具来处理并发,如:
-
自旋锁(spinlock):适用于短时间锁定,获取不到锁时会循环等待(不睡眠);
-
互斥锁(mutex):适用于长时间锁定,获取不到锁时会睡眠等待;
-
原子操作(atomic operations):用于对整数等简单数据类型进行无锁的原子修改;
-
信号量(semaphore):用于控制多个进程对共享资源的访问数量。
-
A common mistake made by driver programmers is to assume that concurrency is not a problem as long as a particular segment of code does not go to sleep (or “block”). Even in previous kernels (which were not preemptive), this assumption was not valid on multiprocessor systems. In 2.6, kernel code can (almost) never assume that it can hold the processor over a given stretch of code. If you do not write your code with concurrency in mind, it will be subject to catastrophic failures that can be exceedingly difficult to debug.
驱动程序程序员常犯的一个错误是认为:只要某段代码不会进入睡眠(或 “阻塞”)状态,并发就不会成为问题。即便在早期(非抢占式)内核中,这种假设在多处理器系统上也不成立。在 2.6 版本的内核中,内核代码(几乎)永远不能假设自己能在某段代码执行期间一直占用处理器。
如果编写代码时不考虑并发问题,程序就可能出现灾难性故障,而且这类故障极难调试。
补充说明:
-
“不睡眠≠无并发” 的原因即使代码不主动睡眠(如不调用可能阻塞的函数),在多处理器系统中,同一段代码仍可能被多个 CPU 核心同时执行;在支持抢占的内核中,高优先级任务还可能打断当前执行的内核代码。例如:驱动中一段 “无睡眠” 的设备状态检查代码,可能在单 CPU 上看似安全,但在双 CPU 系统中,两个核心可能同时执行这段代码并修改同一份状态变量,导致数据错乱。
-
2.6 内核抢占特性的影响抢占机制允许内核在代码执行的 “安全点”(如未持有自旋锁时)暂停当前任务,调度其他任务运行。这意味着即便是单处理器系统,内核代码也可能在执行过程中被 “打断”,后续恢复执行时,之前访问的数据可能已被其他任务修改。因此,“不睡眠” 不再能保证代码执行的连续性,必须通过同步机制保护共享资源。
-
并发相关故障的调试难点竞态条件导致的错误往往具有 “偶发性”—— 依赖特定执行顺序才会触发,且难以通过常规调试工具(如打印日志)复现。例如:某驱动在 99% 的情况下正常工作,但在特定 CPU 负载、中断 timing 下才会崩溃,这种问题定位往往需要借助内核专用调试工具(如
lockdep检测锁竞争,kprobes动态跟踪代码执行)。
1016

被折叠的 条评论
为什么被折叠?



