Linux内核并发控制
1,内核抢占和用户抢占
抢占分为两种情况,用户抢占和内核抢占
用户抢占:内核在即将返回用户空间时检查进程是否设置了TIF_NEED_RESCHED标志,如果设置了,就会发生用户抢占。用户抢占发生的时机:从系统调用或中断处理程序返回用户空间的时候。
在 2.4 和更早的内核中,仅仅用户模式的进程可以被其他进程抢占(发生在内核态返回到用户态时),内核模式代码可以一直独占 CPU,内核模式的代码可以无限制地使用完整的 CPU 指令集并访问所有的内核和 I/O 空间,除非发生以下情况
- 它自愿放弃 CPU
- 发生中断或异常
linux2.6的内核引入了内核抢占,允许进程在任何时刻都可能停下来以便更高优先级的进程执行,thread_info结构中的preempt_count字段为0,表示可以抢占内核。抢占伴随着schedule()的执行。内核提供了一个TIF_NEED_RESCHED标志来表明是否要用schedule()调度一次。根据抢占发生的时机分为用户抢占和内核抢占。用户抢占发生在内核即将返回到用户空间的时候。内核抢占发生在返回内核空间的时候。
内核抢占:在不支持内核抢占的内核中,内核进程如果自己不主动停止,就会一直的运行下去。无法响应实时进程。抢占内核虽然牺牲了上下文切换的开销,但获得了更大的吞吐量和响应时间。2.6的内核添加了内核抢占,同时为了某些地方不被抢占,又添加了自旋锁。thread_info结构中添加了preempt_count该数值为0,当进程使用一个自旋锁时就加1,释放一个自旋锁时就减1.为0时表示内核可以抢占。
内核抢占是指一个在内核态运行的任务,可以在内核态执行期间被另一个进程取代,不是在内核的任何一个地方都可以发生内核抢占的,内核不能被抢占的情况如下:
- 中断处理或者中断的下半部分处理
- 内核持有spinlock自旋锁(spinlock禁止抢占标记)
- 内核正在执行schedule()调度程序
- 内核正在对Per-CPU的数据进行操作
为什么操作Per-CPU的数据不能抢占,实际上Per-CPU是根据CPU的个数创建的一个变量数组,每个CPU访问自己的数组成员,所有不需用锁,单是有一个问题需要考虑,那就是内核抢占问题,一个访问Per-CPU的任务,可能会被调度到不同的CPU上运行,此时对应的Per-CPU数组成员就不相同,再次,一个任务抢占了当前访问Per-CPU变量的任务,如果这两个问题都访问同一个Per-CPU变量就会发生问题,由于以上原因,在每次操作Per-CPU数据时,需要禁用抢占,内核提供了两个API实现该功能,get_cpu(),put_cpu()
内核可能被抢占的地方如下:
- 当再一次有可抢占的时候,如果spin_unlock,local_bh_enable, preempt_enable
- 调用schedule()的时候
- 内核中的任务阻塞
内核模式的代码可以无限制地使用完整的 CPU 指令集并访问所有的内核和 I/O 空间。但是,如果用户模式的进程要享有此特权,并必须通过系统调用向设备驱动或其他内核模式的代码请求服务。
2 进程上下文和中断上下文
内核可以处于两种上下文:进程上下文和中断上下文。在系统调用之后,用户应用程序进入内核空间,此后内核空间针对用户空间相应进程的代表就运行于进程上下文。异步发生的中断会引发中断处理程序被调用,中断处理程序就运行于中断上下文。中断上下文和进程上下文在同一个CPU上不可能同时发生。
运行于进程上下文的内核代码是可抢占的,而中断上下文则不会被抢占。因此,内核会限制中断上下文的工作,不允许其执行如下操作:
- 进入睡眠状态或主动放弃 CPU
- 占用 mutex
- 执行耗时的任务
- 访问用户空间虚拟内存(因为可能导致睡眠
硬中断,软中断,异常之间的抢占关系
- 硬中断就是外设等硬件的中断
- 软中断可以被硬中断“中断”,但是不能被另一个软中断“中断”,在一个CPU上,软中断是串行执行的,所以在单处理器上,在只在软中断中访问的共享资源不用添加任何同步原语。
- 硬中断和软中断都能够抢占异常
3 自旋锁和互斥体
访问共享资源的代码区域称作临界区。自选锁( spinlock )和互斥体( mutex )是保护内核临界区的 2 种基本机制。我们一个一个分析。自选锁可以确保在同时只有一个线程进入临界区。其他想进入临界区的线程必须不停地原地打转,直到第1个线程释放自选锁。
与自选锁不同的是,互斥体在进入一个被占用的临界区之前,不会原地打转而是使当前线程进入睡眠状态。如果要等待的时间较长,互斥体比自选锁会更合适,因为自选锁会消耗CPU 资源。在使用互斥体的场合,多于2 次进程切换时间都可被认为是长时间,因此一个互斥体会引起本线程睡眠,而当其被唤醒时,它需要被切换回来。
因此,在很多情况下,决定使用自选锁还是互斥体相对来说很容易:
- 如果临界区需要睡眠,只能使用互斥体,因为在获得自选锁后进行调度、抢占以及在等待队列上睡眠都是非法的;
- 由于互斥体会在面临竞争的情况下将当前线程置于睡眠状态,因此,在中断处理函数中,只能使用自选锁。
为了论证并发保护的用法,我们首先以一个仅存在于进程上下文的临界区开始,并以下面的顺序逐步引入复杂性:
- 非抢占内核,单CPU 情况下存在于进程上下文的临界区;
- 非抢占内核,单CPU 情况下存在于进程和中断上下文的临界区;
- 可抢占内核,单CPU 情况下存在于进程和中断上下文的临界区;
- 可抢占内核,SMP 情况下存在于进程和中断上下文的临界区。
如下图执行单元A,B为进程上下文,执行单元C为中断上下文
案例 1 :非抢占内核,单CPU,进程上下文
这种情况最为简单,不需要加锁,因此不再赘述。
案例 2 :非抢占内核,单CPU,进程和中断上下文
在这种情况下,为了保护临界区,仅仅需要禁止中断。如图 ,假定进程上下文的执行单元A 、B 以及中断上下文的执行单元C 都企图进入相同的临界区。由于执行单元C 总是在中断上下文执行,它会优先于执行单元A 和B ,因此,它不要担心保护的问题。执行单元A 和B 也不必关心彼此会被互相打断,因为内核是非抢占的。因此,执行单元A和B仅仅需要担心C会在它们进入临界区的时候横行进入。为了实现此目的,它们会在进入临界区之前禁止中断,local_irq_disable()/local_irq_enable。
案例 3 :可抢占内核,单CPU,进程和中断上下文
如果内核使能了抢占,仅仅禁止中断将不再能确保对临界区的保护,因为另一个处于进程上下文的执行单元可能会进入临界区。重新看上图 ,现在,除了C以外,执行单元A和B 必须提防彼此。显而易见,解决该问题的方法是在进入临界区之前禁止内核抢占和中断,并在退出临界区的时候恢复内核抢占和中断, preempt_disable()/preempt_enable()。
案例 4 :可抢占内核,多CPU,进程和中断上下文
现在临界区执行于SMP机器上,而且你的内核配置了CONFIG_SMP和CONFIG_PREEMPT。我们除了要执行local_irq_disable和preempt_disable外,执行单元A可能在CPU0上,B可能在CPU1,为了防止这种情况,内核使用了spin_lock来防止不通CPU上的执行路径访问同一个资源,spin_lock内核会调用preempt_disable,内核也提供了spin_lock和local_irq_disable共同使用的接口spin_lock_irq,在SMP系统上,掉用spin_lock_irq时,仅仅本CPU上的中断被禁止。因此,一个进程上下文的执行单元(上图的执行单元A )在一个CPU上运行的同时,一个中断处理函数(上图的执行单元C)可能运行在另一个CPU 上。非本CPU上的中断处理函数必须自旋等待本CPU上的进程上下文代码退出临界区。中断上下文需要调用spin_lock()/spin_unlock() 。
为什么执行单元A和B需要调用spin_lock_irq,只调用spin_lock不行吗,答案是不行,我们试想这样的一个场景,执行单元A执行了spin_lock后进入临界区,此时由于某种原因发生了中断C,中断C可能在任何一个CPU执行,如果恰好中断了A,由于A已经获取了spin_lock,此时中断程序再次spin_lock,由于在同一个CPU上,这个锁不会被unlock,从而产生死锁。
自旋锁也有底半部(BH )变体。在锁被获取的时候,spin_lock_bh() 会禁止底半部,而spin_unlock_bh() 则会在锁被释放时重新使能底半部。
问题:
spin_lock_irqsave关中断后,为什么要再禁止抢占
假设有这么个情况:
1、CPU-1在进程A的上下文调用了spin_lock_irqsave;
2、CPU-2调用wake_up_process唤醒了CPU-1上的进程B,由于进程B的优先级高于进程A,进程A的TIF_NEED_RESCHED标记被设置。(CPU-2还会用IPI通知CPU-1进行resched,但是CPU-1禁用了中断而不会响应);
3、CPU-1调用了某某函数,这个函数包含了preempt_disable和preempt_enable(没有规定关中断的情况下不能调用这样的函数吧~); 那么,如果spin_lock_irqsave没有preempt_disable,第3步中的preempt_enable将触发preempt_check_resched,从而让进程B抢占掉进程A。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律