本文首先介绍一下Non-PreemptiveKernel和PreemptiveKernel的区别。然后分析Linux下有两种抢占方式:UserPreemption和KernelPreemption。然后在内核态分析:如何判断内核是否可以被抢占(什么是抢占条件);何时触发重调度(何时设置抢占条件);抢占发生时(何时检查可抢占条件);当内核不能被抢占时。***分析了2.6kernel如何支持内核抢占。1、不可抢占内核和可抢占内核的区别为了简化问题,我以嵌入式实时系统uC/OS为例。首先要指出的是,uC/OS只有内核态,没有用户态,这一点与Linux不同。在多任务系统中,内核负责管理各个任务,或者说为各个任务分配CPU时间,并负责任务之间的通信。内核提供的基本服务是任务切换。调度(Scheduler),英文还有一个词叫dispatcher,也是调度的意思。这是内核的主要职责之一,即决定轮到哪个任务运行。大多数实时内核都是基于优先级调度的。每个任务根据其重要性被赋予一定的优先级。基于优先级的调度方法是指CPU总是让处于就绪状态的优先级最高的任务先运行。但是,允许高优先级任务使用CPU有两种不同的情况,这取决于使用的内核类型,是非抢占式内核还是可抢占式内核。非抢占式内核非抢占式内核是一种主动放弃CPU使用权的任务。非抢占式调度方式也称为协作式多任务处理,各个任务相互协作,共享一个CPU。异步事件仍然由中断服务处理。中断服务可以使高优先级任务从挂起状态变为就绪状态。但是,服务中断后,控制权又回到被中断的任务上。直到任务主动放弃CPU使用权,高优先级任务才能获得CPU使用权。非抢占式内核如下图所示。非抢占式内核的优点是:中断响应快(与抢占式内核相比);允许使用不可重入函数;几乎不需要使用信号量来保护共享数据。正在运行的任务占用CPU,不用担心被其他任务抢占。这不是绝对的,在打印机的使用中还是需要满足互斥条件的。非抢占式内核的缺点是:任务响应时间慢。高优先级任务已经进入就绪状态,但还不能运行,必须等到当前运行的任务释放CPU。非抢占式内核的任务级响应时间是不确定的。不知道什么时候优先级最高的任务能拿到CPU的控制权。这完全取决于应用程序何时释放CPU。抢占式内核使用抢占式内核可以保证系统响应时间。一旦优先级最高的任务就绪,它总能获得CPU的使用权。当一个正在运行的任务使一个比它优先级高的任务进入就绪状态时,当前任务的CPU使用权就会被剥夺,或者挂起,高优先级的任务会立即获得CPU的控制权。正确的。如果中断服务子程序使高优先级任务进入就绪状态,当中断完成后,被中断的任务挂起,高优先级任务开始运行。抢占式内核如下图所示。抢占式内核的优点是:有了抢占式内核,就知道什么时候可以执行优先级最高的任务,并且可以获得CPU的使用权。使用抢占式内核可以最大化任务级响应时间。抢占式内核的缺点是:不能直接使用不可重入函数。在调用不可重入函数时,必须满足互斥条件,可以使用互斥信号量来实现。如果调用不可重入函数,低优先级任务的CPU使用权被高优先级任务剥夺,不可重入函数中的数据可能被破坏。2、Linux下的用户态抢占和内核态抢占除了内核态,Linux还有用户态。用户程序的上下文属于用户态,系统调用和中断处理例程的上下文属于内核态。在2.6内核之前,Linux内核只支持用户态抢占。2.1用户抢占当内核回到用户空间,need_resched标志位为1时,调用调度器,即用户空间抢占。当内核回到用户态时,系统可以安全地执行当前任务,或者切换到另一个任务。当中断处理例程或系统调用完成并且内核返回用户模式时,将检查need_resched标志的值。如果为1,调度程序将选择一个新任务并执行它。中断和系统调用的返回路径(returnpath)的实现在entry.S中(entry.S不仅包括内核入口代码,还包括内核出口代码)。2.2内核抢占(KernelPreemption)在2.6内核之前,内核代码(中断和系统调用都属于内核代码)会一直运行,直到代码完成或阻塞(系统调用可以被阻塞)。在2.6内核中,Linux内核变得可抢占。当从中断处理程序返回到内核空间时,内核检查它是否可以被抢占以及是否需要重新安排。内核可以在任何时间点抢占一个任务(因为中断可以在任何时间点发生),只要内核在这个时间点的状态是安全的和可重新调度的。3.内核态抢占的设计3.1内核抢占一个任务的内核态必须满足什么条件?没有锁定。锁用于保护临界区,不能被抢占。内核代码可以是可重入的(reentrant)。因为内核是SMP安全的,所以它满足可重入性。如何判断当前上下文(中断处理例程、系统调用、内核线程等)没有持有锁?Linux在每个任务的thread_info结构体中添加preempt_count变量作为抢占的计数器。这个变量初始为0,锁上时计数器加1,解锁时计数器减1。3.2内核态需要抢占的触发条件内核提供了一个need_resched标志(这个标志在task结构体thread_info中)来表示是否需要重新执行调度。3.3何时触发重调度set_tsk_need_resched():设置指定进程中的need_resched标志clear_tskneed_resched():清除指定进程中的need_resched标志need_resched():检查need_resched标志的值;设置则返回true,否则返回false需要重新调度时:时钟中断处理例程检查当前任务的时间片。当任务的时间片用完后,scheduler_tick()函数会设置need_resched标志;信号量,等待队列、完成等机制唤醒。基于waitqueue,waitqueue的唤醒函数是default_wake_function,调用try_to_wake_up将被唤醒的任务变为就绪状态,并设置need_resched标志。当设置用户进程的nice值时,可能会使高优先级的任务进入就绪状态;当改变任务的优先级时,可能会使高优先级的任务进入就绪状态;创建新任务时,可能会使高优先级任务进入就绪状态;任务进入就绪状态;当CPU(SMP)负载均衡时,当前任务可能需要放到另外一个CPU上运行;3.4当抢占发生时(whentocheckthepreemptiblecondition)当中断处理例程退出时,当返回到内核模式(kernel-space)。这是对schedule()函数的隐式调用。当前任务不是主动放弃CPU使用权,而是被剥夺了CPU使用权。当内核代码从不可抢占状态变为可抢占状态(再次可抢占)时。即preempt_count从正整数变为0时。这也是对schedule()函数的隐式调用。任务在内核模式下显式调用schedule()函数。任务主动放弃CPU使用权。一个任务在内核态被阻塞,导致需要调用schedule()函数。任务主动放弃CPU使用权。3.5禁止/允许抢占条件操作preempt_count操作的函数包括add_preempt_count()、sub_preempt_count()、inc_preempt_count()和dec_preempt_count()。开启可抢占条件的操作是preempt_enable(),调用dec_preempt_count()函数,然后调用preempt_check_resched()函数检查是否需要重调度。禁用可抢占条件的操作是preempt_disable(),调用了inc_preempt_count()函数。内核中有很多函数会调用preempt_enable()和preempt_disable()。例如,spin_lock()函数调用preempt_disable()函数,spin_unlock()函数调用preempt_enable()函数。3.6什么时候不允许抢占?preempt_count()函数用于获取preempt_count的值,preemptible()用于判断内核是否可以被抢占。有几种情况Linux内核不应该被抢占,除此之外,Linux内核可以在任何时候被抢占。这些情况是:内核正在处理中断。在Linux内核中,进程不能抢占中断(中断只能被其他中断终止和抢占,进程不能被中断终止或抢占),中断例程中不允许进程调度。进程调度函数schedule()会对此进行判断,如果在中断中调用,会打印错误信息。内核正在对中断上下文进行BottomHalf(中断的下半部分)处理。软中断会在硬件中断返回之前执行,并且还在中断上下文中。内核的代码段持有spinlock自旋锁、writelock/readlock读写锁等锁,并处于这些锁的保护状态。内核中的这些锁是为了保证SMP系统中运行在不同CPU上的进程在短时间内并发执行的正确性。在持有这些锁的时候,内核不能被抢占,否则其他CPU会因为抢占而长时间抢不到锁而死等。内核正在执行调度器Scheduler。抢占的原因是为了进行新的调度。没有理由抢占调度器然后运行调度器。内核在每个CPU的“私有”数据结构(每个CPU日期结构)上运行。在SMP中,per-CPU数据结构不受自旋锁保护,因为这些数据结构是隐式保护的(不同的CPU具有不同的per-CPU数据,运行在其他CPU上的进程不会使用另一个CPUper-CPU数据)。但是,如果允许抢占,但是一个进程被抢占后重新调度,则有可能被调度到其他CPU上。这时候定义的Per-CPU变量就会有问题,此时应该禁止抢占。4.Linux内核模式抢占的实现4.1数据结构[cpp]查看plaincopystructthread_info{structtask_struct*task;/*maintaskstructure*/structexec_domain*exec_domain;/*executiondomain*//***如果有TIF_NEED_RESCHED标志,调度器必须叫做。*/unsignedlongflags;/*lowlevelflags*//***线程标志:*TS_USEDFPU:表示进程在当前执行过程中是否使用过FPU、MMX和XMM寄存器。*/unsignedlongstatus;/*thread-synchronousflags*//***可运行进程所在运行队列的CPU逻辑号。*/__u32cpu;/*currentCPU*/__s32preempt_count;/*0=>可抢占,<0=>BUG*/mm_segment_taddr_limit;/*线程地址空间:0-0xBFFFFFFFFforuser-thead0-0xFFFFFFFFforkernel-thread*/structrestart_blockrestart_block;unsignedlong*ESinvious_ofestedp(IRQ)stacks*/__u8supervisor_stack[0];};4.2禁用/启用可以抢占条件的函数的代码流=(val);}while(0)#definesub_preempt_count(val)do{preempt_count()-=(val);}while(0)#endif#defininenc_preempt_count()add_preempt_count(1)#definedec_preempt_count()sub_preempt_count(1)/***选择thread_info描述符中的preempt_count字段*/#definepreempt_count()(current_thread_info()->preempt_count)#ifdefCONFIG_PREEMPTasmlinkagevoidpreempt_schedule(void);/***抢占计数加1*/#definepreempt_disable()\do{\inc_preempt_count();\barrier();\}while(0)/***将抢占计数减1*/#definepreempt_enable_no_resched()\do{\barrier();\dec_preempt_count();\}while(0)#definepreempt_check_resched()\do{\if(不太可能(test_thread_flag(TIF_NEED_RESCHED)))\preempt_schedule();\}while(0)/***将抢占计数减1,当thread_info描述符的TIF_NEED_RESCHED标志设置为1时调用preempt_schedule()*/#definepreempt_enable()\do{\preempt_enable_no_resched();\preempt_check_resched();\}while(0)#else#definepreempt_disable()do{}while(0)#definepreempt_enable_no_resched()do{}while(0)#definepreempt_enable()do{}while(0)#definepreempt_check_resched()do{}while(0)#endif函数设置need_resched标志[cpp]查看普通复制staticinlinevoidset_tsk_need_resched(structtask_struct*tsk){set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);}staticinlinevoidclear_tsk_need_resched(structtask_struct_struct*tsk){skclear_tsk_th)
