Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2984324
  • 博文数量: 401
  • 博客积分: 12926
  • 博客等级: 上将
  • 技术积分: 4588
  • 用 户 组: 普通用户
  • 注册时间: 2009-02-22 14:51
文章分类

全部博文(401)

文章存档

2015年(16)

2014年(4)

2013年(12)

2012年(82)

2011年(98)

2010年(112)

2009年(77)

分类: LINUX

2011-03-14 15:44:42

为了更好地理解内核代码是如何执行的,我们借用ULK-3中的思想,把内核看作必须满足两种请求的侍者:一种请求来自顾客,另一种请求来自数量有限的几个不同的老板。对不同的请求,侍者采用如下的策略:

1. 老板提出请求时,如果侍者正空闲,则侍者开始为老板服务,这是空闲情况。

2. 如果老板提出请求时侍者正在为顾客服务,那么侍者停止为顾客服务,开始为老板服务,这是高优先级抢占低优先级情况。

3. 如果一个老板提出请求时侍者正在为另一个老板服务,那么侍者停止为第一个老板提供服务,而开始为第二个老板服务,服务完毕再继续为第一个老板服务,这是同优先级情况。

4. 一个老板可能命令侍者停止正在为顾客提供的服务。侍者在完成对老板最近请求的服务之后,可能会暂时不理会原来的顾客而去为新选中的顾客服务。

千万要注意,侍者提供的服务只是针对CPU处于内核态时所执行的代码。如果CPU在用户态执行,则侍者被认为处于空闲状态。 

老板的请求相当于中断,而顾客的请求相当于用户态进程发出的系统调用或系统产生的异常。正如我们将在中断专题详细描述的,请求内核服务的用户态进程必须发出适当的指令(在80x86上是int $0x80 或 sysenter指令)。这些指令引起一个异常,它迫使CPU从用户态切换到内核态(如何切换我们在预备知识里讲得很清楚)。

细心的读者已经把前三条原则和中断专题博文“中断和异常处理程序的嵌套执行....... ”所描述的内核控制路径的嵌套联系起来了。但是,第四条原则我第一次看到的时候很茫然,看了半天看不懂,不明白为什么用户态发出系统调用,或系统产生的异常在中断完成后却可以又被新的系统调用或异常所取代。

要理解这个思想,我们必须先学习一个Linux 2.6内核中最有趣的新特点,即内核抢占(kernel preemption)。

1 内核抢占


内核抢占是Linux 2.6中一个重要的概念。我们说:如果进程正执行内核函数时,即它在内核态运行时,允许发生内核切换(被替换的进程是正执行内核函数的进程 ),这个内核就是抢占的。遗憾的是,在Linux中(在所有其他的操作系统中也一样),情况要复杂得多:

第一:无论在抢占内核还是非抢占内核中,运行在内核态的进程都可以自动放弃CPU,比如,其原因可能是,进程由于等待资源而不得不转入睡眠状态。ULK-3中把这种进程切换称为计划性进程切换。但是,抢占式内核在响应引起进程切换的异步事件(例如唤醒高优先权进程的中断处理程序)的方式上与非抢占的内核是有差别的,我们将把这种进程切换称做强制性进程切换。

第二:所有的进程切换都由宏switch_to所代表的汇编代码段来完成,这一点在进程管理专题也描述得很清楚。在抢占内核和非抢占内核中,当进程执行完某些具有内核功能的线程,而且调度程序被调用后,就发生进程切换。不过,在非抢占内核中,当前进程是不可能被替换的,除非它打算切换到用户态,即从系统调用或中断中返回。

所以,抢占内核的主要特点是:一个在内核态运行的进程,当且仅当在执行内核函数期间被另外一个进程取代。 

如果还没看明白,让我们举一对实例来说明抢占内核和非抢占内核的区别:进程A执行异常处理程序时(肯定是在内核态),一个具有较高优先级的进程变为可执行状态。这种情况是可能出现的,因为,有可能某个设备,如键盘发生了中断请求而且相应的处理程序唤醒了进程B。如果内核是抢占的,就会发生强制性进程切换,让进程B取代进程A。异常处理程序的执行被暂停,直到调度程序再次选择进程A时才恢复它的执行(B进程执行完毕后不一定马上恢复到A,要看调度程序的选择,这就是前边提到的第四种情况我们所产生的疑问的答案)。相反,如果内核是非抢占的,在进程A完成异常处理程序的执行之前是不会发生进程切换的,除非进程A回到用户态,或者自动放弃CPU。

再看另外一个例子,我们考虑一个执行异常处理程序的进程已经用完了它的时间配额(参见“scheduler_tick()函数 ”博文)的情况。如果内核是抢占的,进程可能会立即被取代,但如果内核是非抢占的,进程继续运行直到它执行完异常处理程序或自动放弃CPU。

说了这么多,那么Linux 2.6为啥要设置内核抢占这么个机制呢?

使内核可抢占的目的在于减少用户态进程的分派延迟(dispatch latency),即减少从进程变为可执行状态到它实际开始运行之间的时间间隔 。内核抢占对执行及时被调度的任务(如:硬件控制器、环境监视器、电影播放器等等)的进程确实是有好处的,因为它降低了这种进程被另一个运行在内核态的进程拖延的风险。

使Linux 2.6内核具有可抢占的特性并不用对支持非抢占的旧内核在设计上做太大的改变,当被current_thread_info()宏所引用的thread_info描述符的preempt_count字段大于0时,就禁止内核抢占 ,即对应的进程必须回到用户态或主动放弃CPU才能被其他内核态进程抢占。Linux对该字段的编码对应三个不同的计数器,对应以下三种情况发生时,取值都大于0:
1. 内核正在执行中断服务例程。
2. 可延迟函数被禁止(当内核正在执行软中断或tasklet时经常如此)。
3. 通过把抢占计数器设置为正数而显式地禁用内核抢占。

上面的原则告诉我们:只有当内核正在执行异常处理程序(尤其是系统调用),而且内核抢占没有被显式地禁用时,才可能抢占内核。此外,别忘了本地CPU打开本地中断,否则无法完成内核抢占 。

内核提供一些专门的简单宏,来处理preempt_count字段的抢占计数器:

preempt_count —— 在thread_info描述符中选择preempt_count字段
preempt_disable —— 使抢占计数加1
preempt_enable_no_resched —— 使抢占计数减1
preempt_enable —— 使抢占计数器的值减1,并在thread _info描述符的TIF_NEED_RESCHED标志被置为1的情况下,调用preempt_schedule()
get_cpu —— 与preempt_disable相似,但要返回本地CPU的数量
put_cpu —— 与preempt_enable相同
put_cpu_no_resched —— 与preempt_enable_no_resched相同

这里我们只是重点介绍一下preempt_enable()宏,它递减抢占计数器,然后检查当前thread_info的flags字段中的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;
    }

该函数检查是否允许本地中断,以及当前进程的preempt_count字段是否为0,如果两个条件都为真,它就调用schedule()选择另外一个进程来运行。因此,内核抢占可能在结束内核控制路径(通常是一个中断处理程序)时发生,也可能在异常处理程序调用preempt_enable()重新允许内核抢占时发生。
-
最后,我郑重提醒大家:内核抢占会引起不容忽视的开销。因此,Linux 2.6独具特色地允许用户在编译内核时通过设置选项来禁用或启用内核抢占——#define CONFIG_PREEMPT

2 什么时候同步是必须的


内核抢占是同步与互斥的必要预备知识,希望大家掌握。那么回到同步概念上,学过操作系统的人都知道,同步与互斥主要针对的是产生竞争条件与临界区的。这些定义同样适用于内核控制路径:在执行内核态进程时,当计算的结果依赖于两个或两个以上的交叉内核控制路径的嵌套方式时,可能出现竞争条件;临界区是一段代码,在其他的内核控制路径能够进入临界区前,进入临界区的内核控制路径必须全部执行完这段代码。

交叉内核控制路径使内核开发者的工作变得复杂了:我们必须特别小心地识别出异常处理程序、中断处理程序、可延迟函数和内核线程中的b临界区b。一旦临界区被确定,就必须对其采用适当的保护措施,以确保在任意时刻只有一个内核控制路径处于临界区。

例如,假设两个不同的中断处理程序要访问同一个数据结构,如一个缓冲区和一个表示缓冲区大小的整型变量,而所有影响该数据结构的语句都必须放入一个单独的临界区。如果是单CPU的系统,内核开发者可以采取访问共享数据结构时关闭中断的方式来实现临界区,因为只有在开中断的情况下,才可能发生内核控制路径的嵌套。 这样,就实现了一个简单的同步与互斥访问。

另外,如果相同的数据结构仅被系统调用服务例程所访问,而且系统中只有一个CPU,就可以非常简单地通过在访问共享数据结构时禁用内核抢占功能来实现临界区。

在多处理器系统中,情况要复杂得多。由于许多CPU可能同时执行内核路径,因此内核开发者不能假设只要关闭中断,或禁用内核抢占功能,就能保证这个数据结构能够安全地被访问。

我们将在本专题中看到内核提供了各种不同的同步技术。内核设计者通过选择最有效的技术解决了所有的同步难题。

3 什么时候同步是不必要的


中断专题中所讨论的一些Linux设计上的特点在某种程度上简化了内核控制路径的同步。让我们简单地回忆一下:

(1)所有的中断处理程序响应来自PIC的中断并禁用对应的IRQ线。此外,在中断处理程序结束之前,不允许产生相同的中断事件。
(2)中断处理程序(上半部)、软中断和tasklet(下半部)既不可以被抢占也不能被阻塞,所以它们不可能长时间处于挂起状态 。在最坏的情况下,它们的执行将有轻微的延迟,因为在其执行的过程中可能发生其他的中断(内核控制路径的嵌套执行)。
(3)执行中断处理的内核控制路径不能被执行可延迟函数或系统调用服务例程的内核控制路径中断 。即中断响应的过程一直被保护,直到开始执行对应的中断服务程序。所谓可延迟函数包括软中断与tasklets,就是中断的下半部分。
(4)软中断和tasklet不能在一个给定的CPU上交错执行。
(5)同一个tasklet不可能同时在几个CPU上执行。

以上的每一种设计选择都可以被看做是一种约束,也就是说用不着对他们进行同步,它能使一些内核函数的编码变得更容易。下面是一些经常用到的例子:

  • 中断处理程序和tasklet不必编写成可重入的函数,即不必考虑被抢占或阻塞。
  • 仅被软中断和tasklet访问的每CPU变量不需要同步。
  • 仅被一种tasklet访问的数据结构不需要同步。


我们本专题主要描述在需要同步的时候应该做些什么,也就是:如何避免由于对共享数据的不安全访问导致的数据崩溃。

4 同步技术


现在,我们就考察一下在避免共享数据之间的竞争条件时,内核控制路径是如何交错执行的。下表列出了Linux内核使用的同步技术。“适用范围”一栏表示同步技术是适用于系统中的所有CPU还是单个CPU。例如,本地中断的禁止只适用于一个CPU(系统中的其他CPU不受影响);相反,原子操作影响系统中的所有CPU(当访问同一个数据结构时,几个CPU上的原子操作不能交错)。

技术

说明

适用范围

CPU 变量

CPU 之间复制数据结构

所有CPU

原子操作

对一个计数器原子地“读- 修改- 写”的指令

所有CPU

内存屏障

避免指令重新排序

本地CPU 或所有CPU

自旋锁

加锁时忙等

所有CPU

信号量

加锁时阻塞等待(睡眠)

所有CPU

顺序锁

基于访问计数器的锁

所有CPU

本地中断的禁止

禁止单个CPU 上的中断处理

本地CPU

本地软中断的禁止

禁止单个CPU 上的可延迟函数处理

本地CPU

- 拷贝- 更新(RCU )

通过指针而不是锁来访问共享数据结构

所有CPU

阅读(1813) | 评论(0) | 转发(1) |
0

上一篇:Linux 进程创建

下一篇:Linux 内核中断内幕

给主人留下些什么吧!~~