我们可以把内核想象成一个服务器,专门响应各种请求。这些请求可以是CPU上正在运行的进程发起的请求,也可以是外部的设备发起的中断请求。所以说,内核并不是串行运行,而是交错执行。既然是交错执行,就会产生竞态条件
,我们可以采用同步技术消除这种竞态条件。
我们首先了解一下如何向内核请求服务。然后,看一下这些请求如何实现同步。Linux内核又是采用了哪些同步技术。
为了更好地理解内核是如何工作的,我们把内核比喻成一个酒吧服务员,他响应两种请求服务:一种是来自顾客,另外一种来自多个老板。这个服务员采用的策略是:
上面的服务员就非常类似于处于内核态的代码执行。如果CPU被用户态程序占用,服务员被认为是空闲的。老板的请求就类似于中断请求,而顾客请求就对应于用户进程发出的系统调用或异常。后面描述中,异常处理程序指的是系统调用和常规异常的处理程序。
仔细研究,就会发现,前3条规则其实与内核中的异常和中断嵌套执行的规则是一样的。第4条规则就对应于内核抢占。
给内核抢占下一个完美定义很难。在这儿,我们只是尝试着给其下一个定义:如果一个进程正运行在内核态,此时,发生了进程切换我们就称其为抢占式内核。当然了,Linux内核不可能这么简单:
switch_to
宏完成。不论是抢占式还是非抢占式,当进程完成内核活动的某个线程并调用调度器时就会发生进程切换。但是,在非抢占式内核中,除非即将切换到用户态时,否则不会发生进程替换。因此,抢占式内核主要的特性就是运行在内核态的进程可以被其它进程打断而发生替换。让我们举例说明抢占式内核和非抢占式内核的区别:
假设进程A正在执行异常处理程序(内核态),这时候中断请求IRQ发生,相应的处理程序唤醒高优先级的进程B。如果内核是可抢占式的,就会发生进程A到进程B的替换。异常处理程序还没有执行完,只有当调度器再一次选择进程A执行的时候才会继续。相反,如果内核是非抢占式的,除非进程A完成异常处理或者自愿放弃CPU的使用权,否则不会发生进程切换。
再比如,考虑正在执行异常处理程序的进程,它的CPU使用时间已经超时。如果内核是抢占式的,进程被立即切换;但是,如果内核是非抢占式的,进程会继续执行,知道进程完成异常处理或自动放弃CPU的使用权。
实施内核抢占的动机就是减少用户态进程的调度延时,也就是减少可运行状态
到真正运行时
的延时。需要实时调度的任务(比如外部的硬件控制器等)需要内核具有抢占性,因为减少了被其它进程延时的风险。
Linux内核是从2.6版本开始的,相比那些旧版本的非抢占性内核而言,没有什么显著的变化。当thread_info
描述符中的preempt_count
成员的值大于0,内核抢占就被禁止。这个值分为3部分,也就是说可能有3种情况导致该值大于0:
通过上面的规则可以看出,内核只有在执行异常处理程序(尤其是系统调用)的时候才能够被抢占,而且内核抢占也没有被禁止。所以,CPU必须使能中断,内核抢占才能被执行。
下表是操作prempt_count
数据成员的一些宏:
选择
preempt_count
preempt_enable()
使能抢占,还会检查TIF_NEED_RESCHED
标志是否设置。如果设置,说明需要进行进程切换,就会调用函数preempt_schedule()
,其代码片段如下所示:
if (!current_thread_info->preempt_count && !irqs_disabled()) {
current_thread_info->preempt_count = PREEMPT_ACTIVE;
schedule();
current_thread_info->preempt_count = 0;
}
可以看出,这个函数首先检查中断是否使能,以及抢占计数是否为0。如果条件为真,调用schedule()
切换到其它进程运行。因此,内核抢占既可以发生在中断处理程序结束时,也可以发生在异常处理程序重新使能内核抢占时(调用preempt_enable()
。也就是说,对于抢占式内核来说,进程切换发生的时机有,中断、系统调用、异常处理,还有一种特殊情况就是内核线程,它们直接调用schedule()进行主动进程切换。
内核抢占不可避免地引入了更多的开销。基于这个原因,Linux2.6内核允许用户在编译内核代码的时候,通过配置,可以使能和禁止内核抢占。
我们先了解一下内核进程的竞态条件和临界区的概念。当计算结果依赖于两个嵌套的内核控制路径时就会发生竞态条件。而临界区就是每次只能一个内核控制路径可以进入的代码段。
内核控制路径的交错执行给内核开发者带来很大的麻烦:必须小心地在异常处理程序、中断处理程序、可延时处理函数和内核线程中确定临界区。一旦确定了哪些代码是临界区,就需要为这个临界区代码提供合适的保护,确保至多有一个内核控制路径可以访问它。
假设两个不同的中断处理程序需要访问相同的数据结构。所有影响数据结构的语句都必须放到一个临界区中。如果是单核处理系统,临界区的保护只需要关闭中断即可,因为内核控制路径的嵌套只有在中断使能的情况下会发生。
另一方面,如果不同的系统调用服务程序访问相同的数据,系统也是单核处理系统,临界区的保护只需要禁止内核抢占即可。
但是,在多核系统中事情就比较复杂了。因为除了内核抢占,中断、异常或软中断之外,多个CPU也可能会同时访问某个相同的数据。
后面我们会看一下内核提供了哪些内核同步手段?每种同步手段最合适的使用场景是什么?通过这些问题,我们掌握内核同步技术,为自己的内核程序设计最好的同步方法。
表5-2,列举了Linux内核使用的一些同步技术。范围一栏表明同步技术应用到所有的CPU还是单个CPU。比如局部中断禁止就是针对一个CPU(系统中的其它CPU不受影响);相反,原子操作影响所有的CPU。
表5-2 Linux内核使用的一些同步技术
技术 | 描述 | 范围 |
---|---|---|
Per-CPU变量 | 用于在CPU之间拷贝数据 | 所有CPU |
原子操作 | 针对计数器的原子RMW指令 | 所有CPU |
内存屏障 | 避免指令乱序 | 本地CPU或所有CPU |
自旋锁 | 忙等待 | 所有CPU |
信号量 | 阻塞等待(休眠) | 所有CPU |
Seqlock | 根据计数器进行加锁 | 所有CPU |
中断禁止 | 禁止响应中断 | 本地CPU |
软中断禁止 | 禁止处理可延时函数 | 本地CPU |
读-拷贝-更新(RCU) | 通过指针实现无锁访问共享资源 | 所有CPU |
后面我们会针对每种同步技术进行详细阐述。
本文分享自 嵌入式ARM和Linux 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!