异常 中断上下文 内核抢占

来源:互联网 发布:硬盘安装深度linux 编辑:程序博客网 时间:2024/05/23 23:11

1.异常和中断上下文

一个中断处理程序既可以抢占其他中断处理程序,也可以抢占异常处理程序,相反,异常处理程序从不抢占中断处理程序。在内核态能触发的唯一异常就是缺页异常。但是,中断处理程序从不执行可以导致缺页(因此意味着进程切换)的操作。

补充:此处的异常是指除中断irq以外的异常,它们处于进程上下文,而不是中断上下文。


2.关于内核抢占

内核抢占是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



内核抢占会发生在:
1.当从中断处理程序正在执行,且返回内核空间之前(此时可抢占标志premptcount须为0,need_resched被置位);
2.当内核代码再一次具有可抢占性的时候,如解锁及使能软中断等;
3.如果内核中的任务显式地调用schedule();
4.如果内内核中的任务阻塞(这同样也会导致调用schedule())

原创粉丝点击