linux 内核同步互斥技术之禁止内核抢占

禁止内核抢占

内核抢占是指当进程在内核模式下运行的时候可以被其他进程抢占,编译内核时需要打开配置宏 CONFIG_PREEMPT。
支持抢占的内核称为抢占式内核,不支持抢占的内核称为非抢占式内核。个人计算机的桌面操作系统要求响应速度快,适合使用抢占式内核;服务器要求业务的吞吐率高,适合使用非抢占式内核。
如果变量只会被本处理器上的进程访问,比如每处理器变量,可以使用禁止内核抢占的方法来保护,代价很低。如果变量可能被其他处理器上的进程访问,应该使用锁保护。
每个进程的 thread_info 结构体有一个抢占计数器:“int preempt_count”,其中第 0~7 位是抢占计数,第 8~15 位是软中断计数,第 16~19 位是硬中断计数,第 20 位是不可屏蔽中断计数。
禁止内核抢占的时候把当前进程的抢占计数加 1,开启内核抢占的时候把当前进程的抢占计数减 1。

preempt_disable(); //增加抢占计数值,禁止内核抢占
....
preempt_enable(); //减少抢占计数值,并在值为0并且没有关闭中断时进行进程调度
preempt_enable_no_resched(); //激活内核抢占但不再检查任何被挂起的需调度任务
preempt_count() //返回抢占计数

申请自旋锁的函数包含了禁止内核抢占,其代码如下:
spin_lock() -> raw_spin_lock() -> _raw_spin_lock() -> __raw_spin_lock()
include/linux/spinlock_api_smp.h
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    preempt_disable();
    …
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

释放自旋锁的函数包含了开启内核抢占,其代码如下:

spin_unlock() -> raw_spin_unlock() -> _raw_spin_unlock() -> __raw_spin_unlock()
include/linux/spinlock_api_smp.h
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
    …
    do_raw_spin_unlock(lock);
    preempt_enable();
}
 

Linux内核抢占和进程调度

  用户抢占


内核即将返回用户空间的时候,如果need resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。在内核返回用户空间的时候,它知道自己是安全的。所以,内核无论是在从中断处理程序还是在系统调用后返回,都会检查need resched标志。如果它被设置了,那么,内核会选择一个其他(更合适的)进程投入运行。
简而言之,用户抢占在以下情况时产生:
1) 从系统调用返回用户空间。
2) 从中断处理程序返回用户空间。

  不可抢占内核的特点


在不支持内核抢占的内核中,内核代码可以一直执行,到它完成为止。也就是说,调度程序没有办法在一个内核级的任务正在执行的时候重新调度—内核中的各任务是协作方式调度的,不具备抢占性。内核代码一直要执行到完成(返回用户空间)或明显的阻塞为止。


 为什么需要内核抢占


实现内核的可抢占对Linux具有重要意义。首先,这是将Linux应用于实时系统所必需的。实时系统对响应时间有严格的限定,当一个实时进程被实时设备的硬件中断唤醒后,它应在限定的时间内被调度执行。而Linux不能满足这一要求,因为Linux的内核是不可抢占的,不能确定系统在内核中的停留时间。事实上当内核执行长的系统调用时,实时进程要等到内核中运行的进程退出内核才能被调度,由此产生的响应延迟,在如今的硬件条件下,会长达100ms级。

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

 如何支持抢占内核


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

抢占式Linux内核的修改主要有两点:

一是对中断的入口代码和返回代码进行修改。在中断的入口内核抢占锁preempt_count加1,以禁止内核抢占;在中断的返回处,内核抢占锁preempt_count减1,使内核有可能被抢占。

另一基本修改是重新定义了自旋锁、读、写锁,在锁操作时增加了对preempt count变量的操作。在对这些锁进行加锁操作时preemptcount变量加1,以禁止内核抢占;在释放锁时preempt count变量减1,并在内核的抢占条件满足且需要重新调度时进行抢占调度。

设置调度的时机


内核必须知道在什么时候调用schedule()。如果仅靠用户程序代码显式地调用schedule(),它们可能就会永远地执行下去。相反,内核提供了一个need_resched标志来表明是否需要重新执行一次调度。

1).当前进程用完了它的CPU时间片,update_process_times()重新进行计算时钟中断触发schduler_tick()的主要作用就是每一个tick 进程陷入内核后, 他的时间片就递减1 , 当变为0的时候, 会设置struct thread_info ->flag的第TIF_NEED_RESCHED位置为1 。

2) .当一个进程被唤醒,而且它的优先级比当前进程高 try_to_wake_up(),会设置TIF_NEED_RESCHED为1 。

调度的时机


1) 中断(包括处理软中断的内核线程ksoftirqd)返回内核空间:检查preempt_count是否为0和struct thread_info ->flag是否设置了TIF_NEED_RESCHED。
2)中断或异常返回到user space:检查struct thread_info ->flag的TIF_NEED_RESCHED位置是否为1?。
3). 显式或者隐式调preempt_enable()函数:检查preempt_count是否为0和struct thread_info ->flag的TIF_NEED_RESCHED位置是否为1。
4)使能软中断:检查preempt_count是否为0和struct thread_info ->flag的TIF_NEED_RESCHED位置是否为1。
5)自己主动schedule()。

你可能感兴趣的:(linux,linux,运维,服务器,网络,c语言)