1.什么是抢占(preemptive)
![dc03209c589d648c7a779077b0ae8b61.png](https://img-blog.csdnimg.cn/img_convert/dc03209c589d648c7a779077b0ae8b61.png)
当一个进程B进入到running stage的时候(还未被真正执行),调度器会去检查进程B的优先级,如果进程B的优先级比目前正在执行的进程A的优先级高的话,linux便会抢占(preemptive)进程A,进程A便被打断执行,此时进程B会得到执行。这个过程中需要注意如下几点:
- 被打断的进程A任然是running state的。
- 在linux中如果需要抢占进程A就需要设置进程A的标志位need_reshed,待进程A执行到抢占点对need_reshed进行检查,发现有高优先级进程发起了抢占,那么就调用schedule让出cpu。
- 进程A也不是任意时候都会被抢占,只有在A的可被抢占点(preemption potions)才有机会将A打断,而抢占点也发生在几个特定的时机。
总结下来的一句话,抢占就是由高优先级进程发起,低优先级进程响应并将cpu使用权交给高优先级进程的过程,下面将详细介绍以上过程。
2.为何需要抢占
早期的linux内核是不支持抢占的,这样就会存在两个问题:
- 2.6内核之前一个进程从用户态进入内核后,别的进程只有等到它退出内核态才能有机会得到执行,这样就会存在时延问题;
- 可能存在优先级反转的问题,例如一个低优先级的用户进程由于执行中断等原因是一个高优先级的任务得不到及时的响应。
简而言之,抢占就是为了让更高优先级的任务得到及时调度。
3.何时触发抢占
- 对于用户态的程序而言,被调度到的时候会检查struct thread_info中的need_resched标识符TLF_NEED_RESCHED来检查自己是否需要重新调度;
- 对于内核态抢占除了判断此标识还需要判断preempt_count抢占计数器;
struct
3.1何时设置TIF_NEED_RESCHED
TIF_NEED_RESCHED 设置的入口都是在check_preempt_curr函数中。
void
如果rq中当前执行进程的sched_class和被唤醒进程的sched_class相同,调用sched_class->check_preempt_curr决定执行顺序,该回调函数中会由不同的调度策略执行不同的结果,传进来的flags决定是否需要触发抢占。如果rq当前进程的sched_class优先级更高,第一个break被执行,退出函数,不做设置;而如果被唤醒进程的sched_class优先级更高,会调用resched_curr(rq)设置该值。
1.当前进程时间片耗尽,却迟迟未让出cpu,则由时钟中断触发抢占
void
时钟中断处理函数scheduler_tick取出当前cpu的运行队列,然后得到这个队列上当前正在运行中的进程的task_struct,调用这个task_struct的调度类的task_tick函数处理,由entity_tick->update_curr更新当前进程的vruntime,并调用check_preempt_tick
2.新唤醒的进程优先级高于当前进程的时候,在try_to_wake_up中触发抢占
内核中提供了三个接口来唤醒进程:
- wake_up_new_task:用来唤醒新进程,fork出来的进程/线程;
- wake_up_process:唤醒处于TASK_NORMAL状态的进程;
- wake_up_state:唤醒指定状态的进程;
后两个接口最终都会调用try_to_wake_up接口。
try_to_wake_up
3.2何时设置preempt_count
对于内核抢占而言,还需要检查preemt_count抢占计数器是否为0,只有在为0的情况下才会发起抢占,为此内核提供了一些列的宏来设置和检查抢占系数。
preempt_disable
preempt_enable->preempt_count_dec_and_test(判断preempt_count和TIF_NEED_RESCHED是否可以被抢占)如果可以就调用preempt_schedule-->prermpt_schedule_common-->__schedule发起调度。
内核代码里面通常直接调用peemp_able的比较少,但是调用锁的地方比较多,我们知道spin_lock是不可打断的,其实就是在lock的时候调用了preempt_disable,unlock的时候调用了preempt_enable。所以可以认为每次调用spin_lock结束时默认都会发起一次隐式抢占。
static
4.何时执行抢占
4.1用户态执行抢占
用户程序可被抢占的地方比较简单,或者说比较固定。
- 系统调用结束,返回用户态的时候会做检查 (以x86为例)
do_syscall_64
- 中断调用结束,返回用户态时
这个在arm上没看到相应的代码,有待学习~~
4.2内核态执行抢占
- 内核执行中断处理时不允许抢占,中断返回后再执行抢占;
arch/arm64/kernel/entry.S
irq_handler
- 内核执行软中断时,禁止内核抢占;
// kernel/softirq.c
- 内核在临界区执行时禁止抢占,如spinlock、RCU等加锁保护;
上面已做介绍,感兴趣的可以看下mutex lock的实现,对比spin lock就明白为什么mutex 可以被打断,而spin lock不可以被打断。
除上面提到的,原则上认为都可以被抢占。
5.举个例子
现有两个用户态的线程,高优先级的线程sleep 3 秒,低优先级的线程sleep 1 秒,然后调用open打开一个字符设备,该字符设备的内核处理函数会忙等待12秒染回返回。
![c53fd766b0da5ec576c56bb4faacbe1c.png](https://img-blog.csdnimg.cn/img_convert/c53fd766b0da5ec576c56bb4faacbe1c.png)
内核态代码
/*
用户态代码
/*
编写内核文件ko的makefile
ARCH
编译用户态文件
arm-linux-gnueabi-gcc-5 -static user_read.c -lpthread
在qemu 上编了arm eb架构进行了测试,实验环境借助了本叔叔提供的镜像环境,qemu上跑起来后插入ko文件。
![95d8e9ea040a4b554a45d71f4432f4cb.png](https://img-blog.csdnimg.cn/img_convert/95d8e9ea040a4b554a45d71f4432f4cb.png)
新建对应的设备文件,主设备为252
![aa3669af437759f7c1b07cd34210bfe9.png](https://img-blog.csdnimg.cn/img_convert/aa3669af437759f7c1b07cd34210bfe9.png)
在qemu 上编了arm eb架构进行了测试,实验环境借助了本叔叔提供的镜像环境,当开启抢占的时候,t1在3秒后就会唤醒编译,有个疑惑的地方是内核中只忙等待了12秒,但是根据打印信息,忙等待了13秒,有想法的大佬可以帮忙解答下~
![99e73bfa82c67ac5d5fb572639d69bab.png](https://img-blog.csdnimg.cn/img_convert/99e73bfa82c67ac5d5fb572639d69bab.png)
没有开启开启抢占后,需要等到t2完成后才能唤醒
![bea8343f285afe7b25a98e24a2e20d43.png](https://img-blog.csdnimg.cn/img_convert/bea8343f285afe7b25a98e24a2e20d43.png)
以上就是对内核抢占机制的一点小小总结,不对的地方,望各位大佬指正~~~
且将新火试新茶,诗酒趁年华 ——苏轼《望江南.超然台作》
6.参考
https://www.slideshare.net/jserv/making-linux-do-hard-realtimehttps://devarea.com/understanding-linux-kernel-preemptionhttps://www.yuanguohuo.com/2020/03/31/linux-preemption-mode/https://www.cnblogs.com/sky-heaven/p/5945895.html
《深入理解linux内核》
《精通linux内核:智能设备开发》
linux kernel 4.9