内核抢占机制(preempt)

hy:之前用户态进程只能在,从内核态返回用户态时调用,或者睡眠、阻塞、中断返回时,内核抢占打开后,在内核态下,优先级更高的用户进程,也可以抢占低优先级的用户进程。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Linux2.4不支持内核抢占,即是说进程通过系统调用陷入到内核态的时候,不可以被其他的进程抢占。如果有更高优先级的进程,只有在系统调用返回用户空间的时候,才可被调度程序调度,由高优先级的进程占用cpu。这里的“不可以被其他进程抢占”当然不包括中断上下文,无论内核态还是用户态,中断上下文都可以抢占进程上下文,中断上下文是拥有最高的权限,它甚至可以抢占其他的中断上下文。

Linux2.6有一个CONFIG_PREEMPT的选项,打开该选项后,linux kernel就支持了内核代码的抢占。对于抢占式内核而言,即便是从中断上下文返回内核空间的进程上下文,只要内核代码不在临界区内,就可以发生调度,让最高优先级的任务调度执行。例如高优先级的进程可以抢占内核态的系统调用,而不必等系统调用执行完返回用户空间才抢占。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

可抢占性,对一个系统的调度延时具有重要意义。2.6 之前,一个进程进入内核态后,别的进程无法抢占,只能等其完成或退出内核态时才能抢占, 这带来严重的延时问题,2.6 开始支持内核态抢占。

禁止内核抢占的情况列出如下:

(1)内核执行中断处理例程时不允许内核抢占,中断返回时再执行内核抢占。
(2)当内核执行软中断或tasklet时,禁止内核抢占,软中断返回时再执行内核抢占。 
(3)在临界区禁止内核抢占,临界区保护函数通过抢占计数宏控制抢占,计数大于0,表示禁止内核抢占。

为保证Linux内核在以上情况下不会被抢占,抢占式内核使用了一个变量preempt_ count,称为内核抢占锁。这一变量被设置在进程的PCB结构task_struct中。每当内核要进入以上几种状态时,变量preempt_ count就加1,指示内核不允许抢占。每当内核从以上几种状态退出时,变量preempt_ count就减1,同时进行可抢占的判断与调度。抢占式Linux内核的修改主要有两点:

一是对中断的入口代码和返回代码进行修改。在中断的入口内核抢占锁preempt_count加1,以禁止内核抢占;在中断的返回处,内核抢占锁preempt_count减1,使内核有可能被抢占。另一基本修改是重新定义了自旋锁、读、写锁,在锁操作时增加了对preempt count变量的操作。在对这些锁进行加锁操作时preemptcount变量加1,以禁止内核抢占;在释放锁时preemptcount变量减1,并在内核的抢占条件满足且需要重新调度时进行抢占调度。

调度的时机

1) 中断返回内核空间:检查preempt_count是否为0和TIF_NEED_RESCHED是否为1 
2) 中断或异常返回到user space:检查TIF_NEED_RESCHED是否为1
3) 显式或者隐式调preempt_enable()函数:检查preempt_count是否为0和TIF_NEED_RESCHED是否为1
4) 使能软中断:检查preempt_count是否为0和TIF_NEED_RESCHED是否为1
5) 自己主动schedule()

spinlock不允许睡眠,原因就是他设计初衷就是如此,因此在获取spinlock时就调用禁止抢占的函数,所以正常情况下获取锁的CPU是不会被抢占的,但如果工程师代码中进行了睡眠,那么睡眠会导致让出CPU,想象一下,一个低优先级的线程先获取了spinlock,如果来一个高优先级的线程去获取spinlock,就会一直获取不到导致死锁。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

早期的Linux核心是不可抢占的。它的调度方法是:一个进程可以通过schedule()函数自愿地启动一次调度(hy:或者阻塞、睡眠时)。非自愿的强制性调度只能发生在每次从系统调用返回的前夕以及每次从中断或异常处理返回到用户空间的前夕。但是,如果在系统空间发生中断或异常是不会引起调度的。这种方式使内核实现得以简化。

{

2.6内核前,进程的调度时机:

①通过schedule()函数自愿地启动一次调度(hy:或者阻塞、睡眠时)

②每次从内核态返回到用户态时。

}

但常存在下面两个问题:

  • 如果这样的中断发生在内核中,本次中断返回是不会引起调度的,而要到最初使CPU从用户空间进入内核空间的那次系统调用或中断(异常)返回时才会发生调度。
  • 另外一个问题是优先级反转。在Linux中,在核心态运行的任何操作都要优先于用户态进程,这就有可能导致优先级反转问题的出现。例如,一个低优先级的用户进程由于执行软/硬中断等原因而导致一个高优先级的任务得不到及时响应。

当前的Linux内核加入了内核抢占(preempt)机制。内核抢占指用户程序在执行系统调用期间可以被抢占,该进程暂时挂起,使新唤醒的高优先级进程能够运行。这种抢占并非可以在内核中任意位置都能安全进行,比如在临界区中的代码就不能发生抢占。临界区是指同一时间内不可以有超过一个进程在其中执行的指令序列。在Linux内核中这些部分需要用自旋锁保护。

内核抢占要求内核中所有可能为一个以上进程共享的变量和数据结构就都要通过互斥机制加以保护,或者说都要放在临界区中。在抢占式内核中,认为如果内核不是在一个中断处理程序中,并且不在被 spinlock等互斥机制保护的临界代码中,就认为可以"安全"地进行进程切换。

Linux内核将临界代码都加了互斥机制进行保护,同时,还在运行时间过长的代码路径上插入调度检查点,打断过长的执行路径,这样,任务可快速切换进程状态,也为内核抢占做好了准备。

Linux内核抢占只有在内核正在执行例外处理程序(通常指系统调用)并且允许内核抢占时,才能进行抢占内核。禁止内核抢占的情况列出如下:

(1)内核执行中断处理例程时不允许内核抢占,中断返回时再执行内核抢占。

(2)当内核执行软中断或tasklet时,禁止内核抢占,软中断返回时再执行内核抢占。

(3)在临界区禁止内核抢占,临界区保护函数通过抢占计数宏控制抢占,计数大于0,表示禁止内核抢占。

抢占式内核实现的原理是在释放自旋锁时或从中断返回时,如果当前执行进程的 need_resched 被标记,则进行抢占式调度。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 内核抢占在一定程度上减少了对某种事件的响应延迟,这也是内核抢占被引入的目的。之前的内核中,除了显示调用系统调度器的某些点,内核其他地方是不允许中重新调度的,如果内核在做一些比较复杂的工作,就会造成某些急于处理的事得不到及时响应。针对内核抢占其实本质上也是对当前进程而言(不知道这么描述是否合适),因为内核是为用户程序提供服务,换言之,其本身不会主动的去执行某个动作。这里内核抢占,重点在于用户程序请求内核服务时,CPU切换到内核态在执行某个系统服务期间,被抢占。

  当然,即使支持内核抢占,也不是什么时候都可以的,还是要考虑对临界区的保护。类似于多处理器架构,如果进程A陷入到内核模式访问某个临界资源,而在访问期间,进程B也要访问临界区,如果这种抢占被允许,那么就发生了临界区被重入。所以,在访问临界资源时需要禁止内核抢占,在出临界区则要开启内核抢占。

...............

前面提到,在不支持内核抢占的内核中,内核态程序会一直执行,直到返回用户空间进程时,才会检查调度。在内核中执行期间,能打断当前执行的,只有中断,在硬件中断来了之后,处理器会根据情况着手处理硬件中断,在硬件中断处理完毕需要恢复现场时,若检查之前的状态是内核态,则不触发调度,只有之前状态是用户态时才会触发调度。而在支持内核抢占的内核中,在从硬件中断返回时,不管是返回用户态和内核态都会检查调度,若是返回内核态,检查当前线程调度标识和抢占标识,若都允许,则可进程调度。这是内核抢占下由增加的一个调度点。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

抢占式任务调度和非抢占式(轮询任务调度)的区别,以及任务调度算法的用途。

1、说说轮巡任务调度与抢占式任务调度的区别?
答:轮询任务调度与抢占式任务调度的区别在于抢占式调度可以因为优先级高的任务抢占cpu,而轮询的不能。 
2、当软件线程个数超过硬件线程个数的时候,支持抢占式多任务处理的操作系统一般会采用时间片轮转调度的方案。
3 、对于RTOS中,理解这两个概念是很重要的。实时系统对于响应时间是有非常严格的要求,尤其是在硬实时系统中,没有满足响应时间的上限将视为系统失败。影响RTOS响应时间的一个重要方面就是任务调度算法。在 RTOS中,主要的调度算法是基于任务优先级的抢占式调度。在这种调度算法中,系统总是选择优先级别的最高的算法进行调度,并且 一旦高优先级别的任务准备就绪之后,它就会马上被调度而不等待低优先级的任务主动放弃CPU。这和通用OS的时间片轮转调度算法是 不一样的,在时间片轮转调度算法中,只有等任务主动放弃CPU,高优先级的任务才有调度的优先权。在基于任务优先级抢占式调度算法中,会产生一个优先级反转问题。解决这个问题的方式主要包括继承优先级和天花板策略。继承优先级策略是一旦高优先级的任务所需要的竞争资源被低优先级的任务使用,就提高低优先级的任务的优先级别,从而使得能使竞争资源尽快释放。天花板策略是在创建信号量的时候,根据可能使用该信号量所有的任务的最高优先级别来设置当前任务的优先级,这个优先级是由创建资源的用户来决定。
4、很多编程人员都认为,使用多线程能够提升程序的性能,如果少量的线程能够提升程序的性能,他们就会认为更多的线程能够更好。但实际上,多线程只是为不同的程序比较合理地安排运行时间,更加充分的利用系统资源,这当中存在着一个线程数和程序性能的平衡,过多的线程可能会严重影响程序的性能。这种影响主要有以下两个方面:首先,将给定的工作量划分给过多的线程会造成每个线程的工作量过少,因此可能导致线程启动和终止时的开销比程序实际工作的开销还要多;其次,过多并发线程的存在将导致共享有限硬件资源的开销增大。 

       操作系统使用进程将它们正在执行的不同应用程序分开。线程是操作系统分配处理器时间的基本单元,并且进程中可以有多个线程同时执行代码。每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。线程上下文包括为使线程在线程的宿主进程地址空间中无缝地继续执行所需的所有信息,包括线程的 CPU 寄存器组和堆栈。 
      当软件线程个数超过硬件线程个数的时候,支持抢占式多任务处理的操作系统一般会采用时间片轮转调度的方案。它通过以下方式实现这一点:在需要硬件线程时间的软件线程之间分割可用硬件线程时间,并轮流为每个线程分配硬件线程时间片(time slice)。当前执行的软件线程在其时间片结束时被挂起,而另一个软件线程继续运行。当系统从一个软件线程切换到另一个软件线程时,它将保存被抢占的软件线程的线程上下文,并重新加载线程队列中下一个软件线程的已保存线程上下文。 
      时间片的长度取决于操作系统和处理器。由于每个时间片都很小,因此即使只有一个处理器,多个线程看起来似乎也是在同时执行。这实际上就是多处理器系统中发生的情形,在此类系统中,可执行线程分布在多个可用处理器中。 
      时间片机制保证了所有的软件线程都能够被执行,这样也就避免了某些软件线程长期占据硬件线程,而导致其他的软件线程出现饥饿(starvation)的情况。但是,操作系统的时间片轮转调度方案也将引入额外的开销,随着软件线程的增多,这种开销将会急剧增加,进而降低系统性能。这种开销主要有以下几种: 
     首先是线程间切换时保存和恢复进程寄存器的开销。在系统要进行线程间切换的时候,需要保存当前线程的寄存器状态,以便在下次回调到当前线程的时候,能够恢复线程寄存器继续运行。当存在少量线程的时候,进程调度程序会给每一个线程分配足够长的时间片,相比较之下,保存和恢复线程寄存器的开销变得不是很显著。但是随着线程数目的增加,线程调度程序分给每个线程的时间片也会相应减少,而保存和恢复线程寄存器的开销不变,这样,在每个时间片当中,系统将更多的时间用于保存和恢复线程寄存器,就会显著地降低系统性能。 
       另一方面,在使用时间片机制的时候,保存和恢复线程使用的cache的开销则是更敏感的一种开销。现代体系结构中,cache通常比主存快10到100倍,CPU访问cache并命中对于系统性能的提高具有非常重要的作用。而当存在线程切换的时候,新的线程要访问的数据尚未装入cache,CPU需要从主存中读取信息的同时,cache替换策略把该地址所在的那块存储内容从主存拷贝到cache中。一般情况下,cache替换策略是使用最近最少使用策略(LRU)选择的,LRU策略是把当前近期cache中使用次数最少的那块数据块替换出去,而这些数据块中的数据很可能就是上几个时间片的线程使用的。这样,各个线程就在不断地竞争cache,相互淘汰彼此使用的cache数据,不断造成cache扑空,最终降低了系统性能。 
此外,在更底层的存储器结构上也存在相似的问题,那就是线程对主存的争夺。现代大部分操作系统都实现了虚拟内存。一台机器上同时运行着很多程序,这些程序所需的存储空间可能比存储器的实际容量要大得多,但是在每个时间点,存储器只有一部分被激活。主存只需存放众多程序中的活跃部分,就像cache中只存放一个程序的活跃部分一样,而其他的部分则存储到磁盘上。由于每个线程都有自己的栈空间和私有数据结构。类似于对cache的争夺,随着线程数的增加,线程之间就会争夺主存,导致性能上的损失。 
        以上几种问题是由于线程对共享资源的争夺产生的,另外还存在一个性质不同,但是后果可能更加严重的问题,称为护航(convoying),它是指线程聚集在一起,等待获取某一个锁。当某个线程持有一个锁的时候,用完了自己的时间片,这时所有等待锁的线程都必须等待这个线程被唤醒并且释放锁。 
       最好的解决方法是依据实际情况,使用尽可能少的线程,这样可以最大限度地减少操作系统资源的使用,并可提高性能。千万不要硬性规定线程的个数,而是将其作为一个可调节的参数。 
       操作系统中存在两种类型的线程,I/O阻塞线程和计算非阻塞线程。许多应用程序,例如终端机模拟程序,都需要在同一时间处理对一个以上的文件的读写操作,我们不可能依次地等待每个请求完成才继续处理下一个请求,而是让所有这些I/O操作并行处理,并且当任何一个I/O完成时,应用程序会收到一个事件通告,这种处理I/O的线程称为I/O阻塞线程。而另外一些进程,例如进行大量计算,处理图形的线程则称为计算非阻塞线程。I/O阻塞线程不会引起时间片切换开销,而计算非阻塞线程则会引起时间片切换的开销。所以,将I/O阻塞线程和计算非阻塞线程分离开是一种非常好的组织方法。计算非阻塞线程在大多数时间内都是被调度函数调度到的,应该和处理器资源相匹配,而I/O阻塞线程在大多数时间内都在等待外部事件。 
     由于构造良好的多线程应用程序要多方面地考虑资源要求和潜在冲突,需要相当的专业技术,所以一般最好采用现成的软件来完成,下面的一些经验是很有帮助的: 
        建议您尽量使用OpenMP。OpenMP提供了一种简单的方法,供程序员描述要并行的循环迭代,而不用创建具体数目的线程,OpenMP可以根据目标系统尽量使用最优数量的线程个数。 
        建议您使用线程池。每个传入的请求都将分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理。一旦池中的某个线程完成任务,它将返回到等待线程队列中,等待被再次使用,这种重用使应用程序可以避免为每个线程创建新进程的开销。线程池通常具有最大线程数限制,如果所有线程都繁忙,而额外的任务将放入队列中,直到有线程可用时才能够得到处理。当您不需要给一个任务设定特定的优先级,当不会有可能会运行很长时间的任务(并因此阻塞了其他任务),我们建议您使用线程池,技术高超的软件设计专家可能希望编写自己的任务调度程序,一般都采用任务窃取(work stealing)的方法,它是指每个线程都有自己私有的任务集合,当一个线程执行完自己的任务以后,它就从其他线程的任务集合中窃取任务来执行。任务窃取实现了较好的cache利用率和负载平衡。当一个线程运行自己的任务时,它往往会重用自己cache中的数据,当它完成自己的任务,就需要窃取任务来运行,这实际上就是一种负载平衡。高效的任务策略在于窃取目标的选择,选择较大的任务能够使窃取者忙碌相当长一段时间。早期的Clik调度程序(Blumofe,1995年)就是一个关于如何编写高效的任务窃取调度程序的范例。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值