diff --git a/study/kernel/01-process/05-schedule/04-periodic_scheduler/README.md b/study/kernel/01-process/05-schedule/04-periodic_scheduler/README.md index 520a6c8..bfbb557 100644 --- a/study/kernel/01-process/05-schedule/04-periodic_scheduler/README.md +++ b/study/kernel/01-process/05-schedule/04-periodic_scheduler/README.md @@ -3,11 +3,9 @@ Linux进程核心调度器之周期性调度器 - - | 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | | ------- |:-------:|:-------:|:-------:|:-------:|:-------:| -| 2016-06-14 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | +| 2016-06-24 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | @@ -27,13 +25,14 @@ Linux进程核心调度器之周期性调度器 而我们的周期性调度器以固定的频率激活负责当前进程调度类的周期性调度方法, 以保证系统的并发性 -首先还是让我们简单回顾一下子之前的的内容 -#前景回顾 + +#1 前景回顾 ------- +首先还是让我们简单回顾一下子之前的的内容 ##进程调度 ------- @@ -52,7 +51,7 @@ Linux进程核心调度器之周期性调度器 -##进程的分类 +##1.1 进程的分类 ------- linux把进程区分为**实时进程**和**非实时进程**, 其中非实时进程进一步划分为交互式进程和批处理进程 @@ -61,20 +60,14 @@ linux把进程区分为**实时进程**和**非实时进程**, 其中非实时 对于实时进程,采用FIFO, Round Robin或者Earliest Deadline First (EDF)最早截止期限优先调度算法|的调度策略. -但是普通进程的调度策略就比较麻烦了, 因为普通进程不能简单的只看优先级, 必须公平的占有CPU, 否则很容易出现进程饥饿, 这种情况下用户会感觉操作系统很卡, 响应总是很慢,因此在linux调度器的发展历程中经过了多次重大变动, linux总是希望寻找一个最接近于完美的调度策略来公平快速的调度进程. +对于普通进程则采用CFS完全公平调度器进行调度 -##linux调度器的演变 +##1.2 linux调度器的演变 ------- -一开始的调度器是复杂度为**$O(n)$的始调度算法**(实际上每次会遍历所有任务,所以复杂度为O(n)), 这个算法的缺点是当内核中有很多任务时,调度器本身就会耗费不少时间,所以,从linux2.5开始引入赫赫有名的**$O(1)$调度器** - -然而,linux是集全球很多程序员的聪明才智而发展起来的超级内核,没有最好,只有更好,在$O(1)$调度器风光了没几天就又被另一个更优秀的调度器取代了,它就是**CFS调度器Completely Fair Scheduler**. - - - | 字段 | 版本 | | ------------- |:-------------:| | O(n)的始调度算法 | linux-0.11~2.4 | @@ -82,7 +75,7 @@ linux把进程区分为**实时进程**和**非实时进程**, 其中非实时 | CFS调度器 | linux-2.6~至今 | -##Linux的调度器组成 +##1.3 Linux的调度器组成 ------- @@ -136,43 +129,21 @@ linux中针对当前可调度的实时和非实时进程, 定义了类型为sech * sched_entity 采用CFS算法调度的普通非实时进程的调度实体 - - -**调度器类的就绪队列** - -另外,对于调度框架及调度器类,它们都有自己管理的运行队列,调度框架只识别rq(其实它也不能算是运行队列),而对于cfs调度器类它的运行队列则是cfs_rq(内部使用红黑树组织调度实体),实时rt的运行队列则为rt_rq(内部使用优先级bitmap+双向链表组织调度实体), 此外内核对新增的dl实时调度策略也提供了运行队列dl_rq - - -**调度器整体框架** - -每个进程都属于某个调度器类(由字段task_struct->sched_class标识), 由调度器类采用进程对应的调度策略调度(由task_struct->policy )进行调度, task_struct也存储了其对应的调度实体标识 - -linux实现了6种调度策略, 依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程. - - -| 调度器类 | 调度策略 | 调度策略对应的调度算法 | 调度实体 | 调度实体对应的调度对象 | -| ------- |:-------:|:-------:|:-------:||:-------:| -| stop_sched_class | 无 | 无 | 无 | 特殊情况, 发生在cpu_stop_cpu_callback 进行cpu之间任务迁移migration或者HOTPLUG_CPU的情况下关闭任务 | -| dl_sched_class | SCHED_DEADLINE | Earliest-Deadline-First最早截至时间有限算法 | sched_dl_entity | 采用DEF最早截至时间有限算法调度实时进程 | -| rt_sched_class | SCHED_RR

SCHED_FIFO | Roound-Robin时间片轮转算法

FIFO先进先出算法 | sched_rt_entity | 采用Roound-Robin或者FIFO算法调度的实时调度实体 | -| fair_sched_class | SCHED_NORMAL

SCHED_BATCH | CFS完全公平懂调度算法 |sched_entity | 采用CFS算法普通非实时进程 | -| idle_sched_class | SCHED_IDLE | 无 | 无 |特殊进程, 用于cpu空闲时调度空闲进程idle | - 它们的关系如下图 ![调度器的组成](../images/level.jpg) -#周期性调度器 +#2 周期性调度器 ------- - - - 周期性调度器在scheduler_tick中实现. 如果系统正在活动中, 内核会按照频率HZ自动调用该函数. 如果没有近曾在等待调度, 那么在计算机电力供应不足的情况下, 内核将关闭该调度器以减少能耗. 这对于我们的嵌入式设备或者手机终端设备的电源管理是很重要的. +##2.1 周期性调度器主流程 +------- + scheduler_tick函数定义在[kernel/sched/core.c, L2910](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L2910)中, 它有两个主要任务 @@ -188,10 +159,7 @@ scheduler_tick函数定义在[kernel/sched/core.c, L2910](http://lxr.free-electr - - ```c - /* * This function gets called by the timer code, with HZ frequency. * We call it with interrupts disabled. @@ -210,15 +178,9 @@ void scheduler_tick(void) /* 1.3 获取就绪队列上正在运行的进程curr */ struct task_struct *curr = rq->curr; - - - - sched_clock_tick(); - - - /* 2 更新rq上的统计信息, 并执行进程对应调度类的周期性的调度 */ + /* 2 更新rq上的统计信息, 并执行进程对应调度类的周期性的调度 */ /* 加锁 */ raw_spin_lock(&rq->lock); @@ -262,7 +224,7 @@ void scheduler_tick(void) -##更新统计量 +##2.2 更新统计量 ------- @@ -273,7 +235,7 @@ void scheduler_tick(void) | calc_global_load_tick | 跟新cpu的活动计数, 主要是更新全局cpu就绪队列的calc_load_update | [kernel/sched/loadavg.c, L382](http://lxr.free-electrons.com/source/kernel/sched/loadavg.c?v=4.6#L378) | -##激活进程所属调度类的周期性调度器 +##2.3 激活进程所属调度类的周期性调度器 ------- @@ -304,5 +266,107 @@ task_tick的实现方法取决于底层的调度器类, 例如完全公平调度 | fail_sched_class | | [kernel/sched/fair.c, line 8116, task_tick_fail](http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L8116) | | idle_sched_class | | [kernel/sched/idle_task.c, line 53, task_tick_idle](http://lxr.free-electrons.com/source/kernel/sched/idle_task.c?v=4.6#L53) | +* 如果当前进程是**完全公平队列**中的进程, 则首先根据当前就绪队列中的进程数算出一个延迟时间间隔,大概每个进程分配2ms时间,然后按照该进程在队列中的总权重中占得比例,算出它该执行的时间X,如果该进程执行物理时间超过了X,则激发延迟调度;如果没有超过X,但是红黑树就绪队列中下一个进程优先级更高,即curr->vruntime-leftmost->vruntime > X,也将延迟调度 -如果当前进程希望被重新调度, 那么调度类方法会在task_struct中设置TIF_NEED_RESCHED标志, 以表示该请求, 而内核将会在接下来的适当实际完成此请求. \ No newline at end of file + +>**延迟调度**的真正调度过程在:schedule中实现,会按照调度类顺序和优先级挑选出一个最高优先级的进程执行 + + +* 如果当前进程是实时调度类中的进程:则如果该进程是SCHED_RR,则递减时间片[为HZ/10],到期,插入到队列尾部,并激发延迟调度,如果是SCHED_FIFO,则什么也不做,直到该进程执行完成 + + +如果当前进程希望被重新调度, 那么调度类方法会在task_struct中设置TIF_NEED_RESCHED标志, 以表示该请求, 而内核将会在接下来的适当实际完成此请求. + + +# 3 周期性调度器的激活 +------- + +## 3.1 定时器周期性的激活调度器 +------- + +定时器是Linux提供的一种定时服务的机制. 它在某个特定的时间唤醒某个进程,来做一些工作. + +在低分辨率定时器的每次时钟中断完成全局统计量更新后, 每个cpu在软中断中执行一下操作 + +* 更新该cpu上当前进程内核态、用户态使用时间xtime_update +* 调用该cpu上的定时器函数 +* 启动周期性定时器(scheduler_tick)完成该cpu上任务的周期性调度工作; + +在支持动态定时器的系统中,可以关闭该调度器,从而进入深度睡眠过程;scheduler_tick查看当前进程是否运行太长时间,如果是,将进程的TIF_NEED_RESCHED置位,然后再中断返回时,调用schedule,进行进程切换操作 + + +```c +// http://lxr.free-electrons.com/source/arch/arm/kernel/time.c?v=4.6#L74 +/* +* Kernel system timer support. +*/ +void timer_tick(void) +{ + profile_tick(CPU_PROFILING); + xtime_update(1); +#ifndef CONFIG_SMP + update_process_times(user_mode(get_irq_regs())); +#endif +} + +// http://lxr.free-electrons.com/source/kernel/time/timer.c?v=4.6#L1409 +/* + * Called from the timer interrupt handler to charge one tick to the current + * process. user_tick is 1 if the tick is user time, 0 for system. + */ +void update_process_times(int user_tick) +{ + struct task_struct *p = current; + + /* Note: this timer irq context must be accounted for as well. */ + account_process_tick(p, user_tick); + run_local_timers(); + rcu_check_callbacks(user_tick); +#ifdef CONFIG_IRQ_WORK + if (in_irq()) + irq_work_tick(); +#endif + scheduler_tick(); + run_posix_cpu_timers(p); +} +``` + +##早期实现 +------- + +Linux初始化时, init_IRQ()函数设定8253的定时周期为10ms(一个tick值). 同样,在初始化时, time_init()用setup_irq()设置时间中断向量irq0, 中断服务程序为timer_interrupt. + +在2.4版内核及较早的版本当中, 定时器的中断处理采用底半机制, 底半处理函数的注册在start_kernel()函数中调用sechd_init(), 在这个函数中又调用init_bh(TIMER_BH, timer_bh)注册了定时器的底半处理函数. 然后系统才调用time_init( )来注册定时器的中断向量和中断处理函数. + +在中断处理函数timer_interrupt()中,主要是调用do_timer()函数完成工作。do_timer()函数的主要功能就是调用mark_bh()产生软中断,随后处理器会在合适的时候调用定时器底半处理函数timer_bh()。在timer_bh()中, 实现了更新定时器的功能. 2.4.23版的do_timer()函数代码如下(经过简略): + +```c +void do_timer(struct pt_regs *regs) +{ + (*(unsigned long *)&jiffies)++; + update_process_times(user_mode(regs)); + mark_bh(TIMER_BH); +} +``` + +而在内核2.6版本以后,定时器中断处理采用了软中断机制而不是底半机制。时钟中断处理函数仍然为timer_interrup()-> do_timer_interrupt()-> do_timer_interrupt_hook()-> do_timer()。不过do_timer()函数的实现有所不同 +```c +void do_timer(struct pt_regs *regs) +{ + jiffies_64++; + update_process_times(user_mode(regs)); + update_times(); +} +``` + +>更详细的实现linux-2.6 +> +[Linux中断处理之时钟中断(一)](http://www.bianceng.cn/OS/Linux/201111/31272_4.htm) +> +>[(原创)linux内核进程调度以及定时器实现机制](http://blog.csdn.net/joshua_yu/article/details/591038) + + + +>参考 +> +>[进程管理与调度5 -- 进程调度、进程切换原理详解](http://wanderer-zjhit.blogbus.com/logs/156738683.html) \ No newline at end of file diff --git a/study/kernel/01-process/05-schedule/06-preempt/1.c b/study/kernel/01-process/05-schedule/06-preempt/1.c deleted file mode 100644 index 5dc4a71..0000000 --- a/study/kernel/01-process/05-schedule/06-preempt/1.c +++ /dev/null @@ -1,48 +0,0 @@ -#include -#include - - - -struct ThreadInfo -{ - int preempt_count; -}tiv; - -struct ThreadInfo *ti = &tiv; -/* - * Increment/decrement the preempt count. - */ -#ifdef CONFIG_PREEMPT_COUNT - .macro inc_preempt_count, ti, tmp - ldr \tmp, [\ti, #TI_PREEMPT] @ get preempt count - add \tmp, \tmp, #1 @ increment it - str \tmp, [\ti, #TI_PREEMPT] - .endm - - .macro dec_preempt_count, ti, tmp - ldr \tmp, [\ti, #TI_PREEMPT] @ get preempt count - sub \tmp, \tmp, #1 @ decrement it - str \tmp, [\ti, #TI_PREEMPT] - .endm - - .macro dec_preempt_count_ti, ti, tmp - get_thread_info \ti - dec_preempt_count \ti, \tmp - .endm -#else - .macro inc_preempt_count, ti, tmp - .endm - - .macro dec_preempt_count, ti, tmp - .endm - - .macro dec_preempt_count_ti, ti, tmp - .endm -#endif - -int main(void) -{ - ti->preempt_count = 0; - inc_preempt_count(ti); - -} \ No newline at end of file diff --git a/study/kernel/01-process/05-schedule/07-context_switch/README.md b/study/kernel/01-process/05-schedule/07-context_switch/README.md new file mode 100644 index 0000000..821b566 --- /dev/null +++ b/study/kernel/01-process/05-schedule/07-context_switch/README.md @@ -0,0 +1,556 @@ +Linux用户抢占和内核抢占详解(概念, 实现和触发时机) +======= + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------- |:-------:|:-------:|:-------:|:-------:|:-------:| +| 2016-06-14 | [Linux-4.6](http://lxr.free-electrons.com/source/?v=4.6) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | + + +前面我们了解了linux进程调度器的设计思路和注意框架 + +周期调度器scheduler_tick通过linux定时器周期性的被激活, 进行程序调度 + +进程主动放弃CPU或者发生阻塞时, 则会调用主调度器schedule进行程序调度 + +在分析的过程中, 我们提到了内核抢占和用户抢占的概念, 但是并没有详细讲, 因此我们在这里详细分析一下子 + + + +CPU抢占分两种情况, **用户抢占**, **内核抢占** + +其中内核抢占是在Linux2.5.4版本发布时加入, 同SMP(Symmetrical Multi-Processing, 对称多处理器), 作为内核的可选配置。 + + + +#1 前景回顾 +------- + +##1.1 Linux的调度器组成 +------- + + +**2个调度器** + +可以用两种方法来激活调度 + +* 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU + +* 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测是否有必要 + +因此当前linux的调度程序由两个调度器组成:**主调度器**,**周期性调度器**(两者又统称为**通用调度器(generic scheduler)**或**核心调度器(core scheduler)**) + +并且每个调度器包括两个内容:**调度框架**(其实质就是两个函数框架)及**调度器类** + + + +**6种调度策略** + +linux内核目前实现了6中调度策略(即调度算法), 用于对不同类型的进程进行调度, 或者支持某些特殊的功能 + +* SCHED_NORMAL和SCHED_BATCH调度普通的非实时进程 + +* SCHED_FIFO和SCHED_RR和SCHED_DEADLINE则采用不同的调度策略调度实时进程 + +* SCHED_IDLE则在系统空闲时调用idle进程. + + + +**5个调度器类** + +而依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程. + + +其所属进程的优先级顺序为 +```c +stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class +``` + + +**3个调度实体** + +调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度. + +这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组. + +linux中针对当前可调度的实时和非实时进程, 定义了类型为seched_entity的3个调度实体 + +* sched_dl_entity 采用EDF算法调度的实时调度实体 + +* sched_rt_entity 采用Roound-Robin或者FIFO算法调度的实时调度实体 + +* sched_entity 采用CFS算法调度的普通非实时进程的调度实体 + + +##1.2 主调度器与内核/用户抢占 +------- + +###1.2.1 调度过程中关闭内核抢占 +------- + +我们在上一篇linux内核主调度器schedule(文章链接, [CSDN](未填写网址), [Github](未填写网址))中在分析主调度器的时候, 我们会发现内核在进行调度之前都会通过preempt_disable关闭内核抢占, 而在完成调度工作后, 又会重新开启**内核抢占** + +参见[主调度器函数schedule](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L3243) +```c + do { + preempt_disable(); /* 关闭内核抢占 */ + __schedule(false); /* 完成调度 */ + sched_preempt_enable_no_resched(); /* 开启内核抢占 */ + + } while (need_resched()); /* 如果该进程被其他进程设置了TIF_NEED_RESCHED标志,则函数重新执行进行调度 */ +``` + +这个很容易理解, 我们在内核完成调度器过程中, 这时候如果发生了内核抢占, 我们的调度会被中断, 而调度却还没有完成, 这样会丢失我们调度的信息. + +###1.2.2 调度完成检查need_resched看是否需要重新调度 + +而同样我们可以看到, 在调度完成后, 内核会去判断need_resched条件, 如果这个时候为真, 内核会重新进程一次调度. + +这个的原因, 我们在前一篇博客中, 也已经说的很明白了, + +内核在thread_info的flag中设置了一个标识来标志进程是否需要重新调度, 即重新调度need_resched标识TIF_NEED_RESCHED, 内核在即将返回用户空间时会检查标识TIF_NEED_RESCHED标志进程是否需要重新调度,如果设置了,就会发生调度, 这被称为**用户抢占** + + + + + +#2 非抢占式和可抢占式内核 +------- + +为了简化问题,我使用嵌入式实时系统uC/OS作为例子 + +首先要指出的是,uC/OS只有内核态,没有用户态,这和Linux不一样 + +多任务系统中, 内核负责管理各个任务, 或者说为每个任务分配CPU时间, 并且负责任务之间的通讯. + +内核提供的基本服务是任务切换. 调度(Scheduler),英文还有一词叫dispatcher, 也是调度的意思. + +这是内核的主要职责之一, 就是要决定该轮到哪个任务运行了. 多数实时内核是基于优先级调度法的, 每个任务根据其重要程度的不同被赋予一定的优先级. 基于优先级的调度法指,CPU总是让处在就绪态的优先级最高的任务先运行. 然而, 究竟何时让高优先级任务掌握CPU的使用权, 有两种不同的情况, 这要看用的是什么类型的内核, 是**不可剥夺型**的还是**可剥夺型内核** + +##2.1 非抢占式内核 +------- + +**非抢占式内核**是由任务主动放弃CPU的使用权 + +非抢占式调度法也称作合作型多任务, 各个任务彼此合作共享一个CPU. 异步事件还是由中断服务来处理. 中断服务可以使一个高优先级的任务由挂起状态变为就绪状态. + +但中断服务以后控制权还是回到原来被中断了的那个任务, 直到该任务主动放弃CPU的使用权时,那个高优先级的任务才能获得CPU的使用权。非抢占式内核如下图所示. + +![非抢占式内核](./images/NonPreemptiveKernel.png) + +非抢占式内核的优点有 + +* 中断响应快(与抢占式内核比较); + +* 允许使用不可重入函数; + +* 几乎不需要使用信号量保护共享数据, 运行的任务占有CPU,不必担心被别的任务抢占。这不是绝对的,在打印机的使用上,仍需要满足互斥条件。 + +非抢占式内核的缺点有 + +* 任务响应时间慢。高优先级的任务已经进入就绪态,但还不能运行,要等到当前运行着的任务释放CPU。 +* 非抢占式内核的任务级响应时间是不确定的,不知道什么时候最高优先级的任务才能拿到CPU的控制权,完全取决于应用程序什么时候释放CPU。 + +##2.2 抢占式内核 +------- + +使用抢占式内核可以保证系统响应时间. 最高优先级的任务一旦就绪, 总能得到CPU的使用权。当一个运行着的任务使一个比它优先级高的任务进入了就绪态, 当前任务的CPU使用权就会被剥夺,或者说被挂起了,那个高优先级的任务立刻得到了CPU的控制权。如果是中断服务子程序使一个高优先级的任务进入就绪态,中断完成时,中断了的任务被挂起,优先级高的那个任务开始运行。 + +抢占式内核如下图所示 + +![抢占式内核](./images/PreemptiveKernel.png) + +抢占式内核的优点有 + +* 使用抢占式内核,最高优先级的任务什么时候可以执行,可以得到CPU的使用权是可知的。使用抢占式内核使得任务级响应时间得以最优化。 +抢占式内核的缺点有: + +* 不能直接使用不可重入型函数。调用不可重入函数时,要满足互斥条件,这点可以使用互斥型信号量来实现。如果调用不可重入型函数时,低优先级的任务CPU的使用权被高优先级任务剥夺,不可重入型函数中的数据有可能被破坏。 + + + +#3 linux用户抢占 +------- + +#3.1 #linux用户抢占 +------- + +当内核即将返回用户空间时, 内核会检查need_resched是否设置, 如果设置, 则调用schedule(),此时,发生用户抢占. + + +##3.2 need_resched标识 +------- + +内核如何检查一个进程是否需要被调度呢? + +内核在即将返回用户空间时检查进程是否需要重新调度,如果设置了,就会发生调度, 这被称为**用户抢占**, 因此**内核在thread_info的flag中设置了一个标识来标志进程是否需要重新调度, 即重新调度need_resched标识TIF_NEED_RESCHED** + +并提供了一些设置可检测的函数 + + +| 函数 | 描述 | 定义 | +| ------- |:-------:|:-------:| +| set_tsk_need_resched | 设置指定进程中的need_resched标志 | [include/linux/sched.h, L2920](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L2920) | +| clear_tsk_need_resched | 清除指定进程中的need_resched标志 | [include/linux/sched.h, L2926](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L2931) | +| test_tsk_need_resched | 检查指定进程need_resched标志 | [include/linux/sched.h, L2931](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L2931) | + +而我们内核中调度时常用的need_resched()函数检查进程是否需要被重新调度其实就是通过test_tsk_need_resched实现的, 其定义如下所示 + +```c +// http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L3093 +static __always_inline bool need_resched(void) +{ + return unlikely(tif_need_resched()); +} + +// http://lxr.free-electrons.com/source/include/linux/thread_info.h?v=4.6#L106 +#define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED) +``` + +##3.3 用户抢占的发生时机(什么时候需要重新调度need_resched) + +一般来说,用户抢占发生几下情况: + +* 从系统调用返回用户空间; + +* 从中断(异常)处理程序返回用户空间 + +从这里我们可以看到, 用户抢占是发生在用户空间的抢占现象. + +更详细的触发条件如下所示, 其实不外乎就是前面所说的两种情况: 从系统调用或者中断返回用户空间 + +1. 时钟中断处理例程检查当前任务的时间片,当任务的时间片消耗完时,scheduler_tick()函数就会设置need_resched标志; + +2. 信号量、等到队列、completion等机制唤醒时都是基于waitqueue的,而waitqueue的唤醒函数为default_wake_function,其调用try_to_wake_up将被唤醒的任务更改为就绪状态并设置need_resched标志。 + +3. 设置用户进程的nice值时,可能会使高优先级的任务进入就绪状态; + +4. 改变任务的优先级时,可能会使高优先级的任务进入就绪状态; + +5. 新建一个任务时,可能会使高优先级的任务进入就绪状态; + +6. 对CPU(SMP)进行负载均衡时,当前任务可能需要放到另外一个CPU上运行 + + +#4 linux内核抢占 +------- + +##4.1 内核抢占的概念 +------- + +对比用户抢占, 顾名思义, 内核抢占就是指一个在内核态运行的进程, 可能在执行内核函数期间被另一个进程取代. + + +##4.2 为什么linux需要内核抢占 +------- + +linux系统中, 进程在系统调用后返回用户态之前, 或者是内核中某些特定的点上, 都会调用调度器. 这确保除了一些明确指定的情况之外, 内核是无法中断的, 这不同于用户进程. + +如果内核处于相对耗时的操作中, 比如文件系统或者内存管理相关的任务, 这种行为可能会带来问题. 这种情况下, 内核代替特定的进程执行相当长的时间, 而其他进程无法执行, 无法调度, 这就造成了系统的延迟增加, 用户体验到"缓慢"的响应. 比如如果多媒体应用长时间无法得到CPU, 则可能发生视频和音频漏失现象. + +在编译内核时如果启用了对**内核抢占**的支持, 则可以解决这些问题. 如果高优先级进程有事情需要完成, 那么在启用了内核抢占的情况下, 不仅用户空间应用程序可以被中断, 内核也可以被中断, + + +linux内核抢占是在Linux2.5.4版本发布时加入的, 尽管使内核可抢占需要的改动特别少, 但是该机制不像抢占用户空间进程那样容易实现. 如果内核无法一次性完成某些操作(例如, 对数据结构的操作), 那么可能出现静态条件而使得系统不一致. + +内核抢占和用户层进程被其他进程抢占是两个不同的概念, 内核抢占主要是从实时系统中引入的, 在非实时系统中的确也能提高系统的响应速度, 但也不是在所有情况下都是最优的,因为抢占也需要调度和同步开销,在某些情况下甚至要关闭内核抢占, 比如前面我们将主调度器的时候, linux内核在完成调度的过程中是关闭了内核抢占的. + +内核不能再任意点被中断, 幸运的是, 大多数不能中断的点已经被SMP实现标识出来了. 并且在实现内核抢占时可以重用这些信息. 如果内核可以被抢占, 那么单处理器系统也会像是一个SMP系统 + +##4.3 内核抢占的发生时机 +------- + +要满足什么条件,kernel才可以抢占一个任务的内核态呢? + +* 没持有锁。锁是用于保护临界区的,不能被抢占。 + +* Kernel code可重入(reentrant)。因为kernel是SMP-safe的,所以满足可重入性。 + +内核抢占发生的时机,一般发生在: + +1. 当从中断处理程序正在执行,且返回内核空间之前。当一个中断处理例程退出,在返回到内核态时(kernel-space)。这是隐式的调用schedule()函数,当前任务没有主动放弃CPU使用权,而是被剥夺了CPU使用权。 + +2. 当内核代码再一次具有可抢占性的时候,如解锁(spin_unlock_bh)及使能软中断(local_bh_enable)等, 此时当kernel code从不可抢占状态变为可抢占状态时(preemptible again)。也就是preempt_count从正整数变为0时。这也是隐式的调用schedule()函数 + +3. 如果内核中的任务显式的调用schedule(), 任务主动放弃CPU使用权 + +4. 如果内核中的任务阻塞(这同样也会导致调用schedule()), 导致需要调用schedule()函数。任务主动放弃CPU使用权 + +内核抢占,并不是在任何一个地方都可以发生,以下情况不能发生 + +1. 内核正进行中断处理。在Linux内核中进程不能抢占中断(中断只能被其他中断中止、抢占,进程不能中止、抢占中断),在中断例程中不允许进行进程调度。进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错信息。 + +2. 内核正在进行中断上下文的Bottom Half(中断下半部,即软中断)处理。硬件中断返回前会执行软中断,此时仍然处于中断上下文中。如果此时正在执行其它软中断,则不再执行该软中断。 + +3. 内核的代码段正持有spinlock自旋锁、writelock/readlock读写锁等锁,处干这些锁的保护状态中。内核中的这些锁是为了在SMP系统中短时间内保证不同CPU上运行的进程并发执行的正确性。当持有这些锁时,内核不应该被抢占。 + +4. 内核正在执行调度程序Scheduler。抢占的原因就是为了进行新的调度,没有理由将调度程序抢占掉再运行调度程序。 + +5. 内核正在对每个CPU“私有”的数据结构操作(Per-CPU date structures)。在SMP中,对于per-CPU数据结构未用spinlocks保护,因为这些数据结构隐含地被保护了(不同的CPU有不一样的per-CPU数据,其他CPU上运行的进程不会用到另一个CPU的per-CPU数据)。但是如果允许抢占,但一个进程被抢占后重新调度,有可能调度到其他的CPU上去,这时定义的Per-CPU变量就会有问题,这时应禁抢占。 + + + + +#5 内核抢占的实现 +------- + +##5.1 内核如何跟踪它能否被抢占? +------- + + + +前面我们提到了, 系统中每个进程都有一个特定于体系结构的struct thread_info结构, 用户层程序被调度的时候会检查struct thread_info中的need_resched标识TLF_NEED_RESCHED标识来检查自己是否需要被重新调度. + +自然内核抢占·也可以应用同样的方法被实现, linux内核在thread_info结构中添加了一个自旋锁标识preempt_count, 称为**抢占计数器(preemption counter)**. + +```c +struct thread_info +{ + /* ...... */ + int preempt_count; /* 0 => preemptable, <0 => BUG */ + /* ...... */ +} +``` + + +| preempt_count值 | 描述 | +| ------- |:-------:| +| >0 | 禁止内核抢占, 其值标记了使用preempt_count的临界区的数目 | +| 0 | 开启内核抢占 | +| <0 | 锁为负值, 内核出现错误 | + +内核自然也提供了一些函数或者宏, 用来开启, 关闭以及检测抢占计数器preempt_coun的值, 这些通用的函数定义在[include/asm-generic/preempt.h](http://lxr.free-electrons.com/source/include/asm-generic/preempt.h?v=4.6#L8), 而某些架构也定义了自己的接口, 比如x86架构[/arch/x86/include/asm/preempt.h](http://lxr.free-electrons.com/source/arch/x86/include/asm/preempt.h?v=4.6) + +| 函数 | 描述 | 定义 | +| ------- |:-------:|:-------:| +| preempt_count | 获取当前current进程抢占计数器的值 | [include/asm-generic/preempt.h, line 8](http://lxr.free-electrons.com/source/include/asm-generic/preempt.h?v=4.6#L8) | +| preempt_count_ptr | 返回指向当前current进程的抢占计数器的指针 | [include/asm-generic/preempt.h, line 13](http://lxr.free-electrons.com/source/include/asm-generic/preempt.h?v=4.6#L13) | +| preempt_count_set | 重设当前current进程的抢占计数器 | [include/asm-generic/preempt.h, line 18](http://lxr.free-electrons.com/source/include/asm-generic/preempt.h?v=4.6#L18) | +| init_task_preempt_count | 初始化task的抢占计数器为FORK_PREEMPT_COUNT | [include/asm-generic/preempt.h, line 26](http://lxr.free-electrons.com/source/include/asm-generic/preempt.h?v=4.6#L26) | +| init_idle_preempt_count | 初始化task的抢占计数器为PREEMPT_ENABLED | [include/asm-generic/preempt.h, line 30](http://lxr.free-electrons.com/source/include/asm-generic/preempt.h?v=4.6#L30) | +| preempt_count_add | 将增加current的抢占计数器增加val | [include/linux/preempt.h, line 132](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L32) | +| preempt_count_sub | 将增加current的抢占计数器减少val | [include/linux/preempt.h, line 133](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L133) | +| preempt_count_dec_and_test | 将current的抢占计数器减少1, 然后看是否可以进程内核抢占, 即检查抢占计数器是否为0(允许抢占), 同时检查tif_need_resched标识是否为真 | [include/linux/preempt.h, line 134, 61](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L134) | +| preempt_count_inc | current的抢占计数器增加1 | [include/linux/preempt.h, line 140](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L140) | +| preempt_count_dec | current的抢占计数器减少1 | [include/linux/preempt.h, line 141](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L141) | + + + +还有其他函数可用于开启和关闭内核抢占 + +| 函数 | 描述 | 定义 | +| ------- |:-------:|:-------:| +| preempt_disable | 通过preempt_count_inc来停用内核抢占, 并且通过路障barrier同步来避免编译器的优化 | [include/linux/preempt.h, line 145](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L145) | +| preempt_enable | preempt_count_dec_and_test启用内核抢占, 然后通过__preempt_schedule检测是够有必要进行调度 | [include/linux/preempt.h, line 162](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L162) | +| preempt_enable_no_resched | 开启抢占, 但是不进行重调度 | [include/linuxc/preempt.h, line 151](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L151) | +| preempt_check_resched | 调用__preempt_schedule检测是够有必要进行调度 | [include/linux/preempt.h, line 176](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L176) | +| should_resched | 检查current的抢占计数器是否为参数preempt_offset的值, 同时检查 tif_need_resched是否为真 | [include/linux/preempt.h, line 74](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L74) | +| preemptible | 检查是否可以内核抢占, 检查抢占计数器是否为0, 以及是否停用了中断 | [/include/linux/preempt.h, line159](http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L159) | + +##5.2 内核如何知道是否需要抢占? +------- + +首先必须设置了TLF_NEED_RESCHED标识来通知内核有进程在等待得到CPU时间, 然后会在判断抢占计数器preempt_count是否为0, 这个工作往往通过preempt_check_resched或者其相关来实现 + + +###5.2.1 重新启用内核抢占时使用preempt_schedule检查抢占 +------- + +在内核停用抢占后重新启用时, 检测是否有进程打算抢占当前执行的内核代码, 是一个比较好的时机, 如果是这样, 应该尽快完成, 则无需等待下一次对调度器的例行调用. + + + +抢占机制中主要的函数是preempt_schedule, 设置了TIF_NEED_RESCHED标志并不能保证可以抢占内核, 内核可能处于临界区, 不能被干扰 + +```c +// http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L3307 + +/* + * this is the entry point to schedule() from in-kernel preemption + * off of preempt_enable. Kernel preemptions off return from interrupt + * occur there and call schedule directly. + */ +asmlinkage __visible void __sched notrace preempt_schedule(void) +{ + /* + * If there is a non-zero preempt_count or interrupts are disabled, + * we do not want to preempt the current task. Just return.. + */ + /* !preemptible() => preempt_count() != 0 || irqs_disabled() + * 如果抢占计数器大于0, 那么抢占被停用, 该函数立即返回 + * 如果 + */ + if (likely(!preemptible())) + return; + + preempt_schedule_common(); +} +NOKPROBE_SYMBOL(preempt_schedule); +EXPORT_SYMBOL(preempt_schedule); + +// http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L159 + #define preemptible() (preempt_count() == 0 && !irqs_disabled()) +``` + + +>!preemptible => preempt_count() != 0 || irqs_disabled()表明 + +* 如果抢占计数器大于0, 那么抢占仍然是被停用的, 因此内核不能被打断, 该函数立即结束. + +* 如果在某些重要的点上内核停用了硬件中断, 以保证一次性完成相关的处理, 那么抢占也是不可能的.irqs_disabled会检测是否停用了中断. 如果已经停用, 则内核不能被抢占 + +接着如果可以被抢占, 则执行如下步骤 + +```c + +static void __sched notrace preempt_schedule_common(void) +{ + do { + /* + preempt_disable_notrace定义在 + http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L198 等待于__preempt_count_inc(); + */ + preempt_disable_notrace(); + /* 完成一次调度 */ + __schedule(true); + + /* + preempt_enable_no_resched_notrace + http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.6#L204 + 等价于__preempt_count_dec + */ + preempt_enable_no_resched_notrace(); + + /* + * Check again in case we missed a preemption opportunity + * between schedule and now. + * 再次检查, 以免在__scheudle和当前点之间错过了抢占的时机 + */ + } while (need_resched()); +} +``` + +我们可以看到, 内核在增加了抢占计数器的计数后, 用__schedule进行了一次调度, 参数传入preempt = true, 表明调度不是以普通的方式引发的, 而是由于内核抢占. 在内核重调度之后, 代码流程回到当前进程, 那么就井抢占计数器减少1. + + + +###5.2.2 中断之后返回内核态时通过preempt_schedule_irq触发 + +上面preempt_schedule只是触发内核抢占的一种方法, 另一种激活抢占的方式是在处理了一个硬件中断请求之后. 如果处理器在处理中断请求后返回内核态(返回用户态则没有影响), 特定体系结构的汇编例程会检查抢占计数器是否为0, 即是否允许抢占, 以及是否设置了重调度标识, 类似于preempt_schedule的处理. 如果两个条件都满足则通过preempt_schedule_irq调用调度器, 此时表明抢占请求发自中断上下文 + + +该函数与preempt_schedule的本质区别在于: preempt_schedule_irq调用时停用了中断, 防止终端造成的递归调用, 其定义在[kernel/sched/core.c, line3360](http://lxr.free-electrons.com/source/kernel/sched/core.c?v=4.6#L3360) + + +```c +/* + * this is the entry point to schedule() from kernel preemption + * off of irq context. + * Note, that this is called and return with irqs disabled. This will + * protect us against recursive calling from irq. + */ +asmlinkage __visible void __sched preempt_schedule_irq(void) +{ + enum ctx_state prev_state; + + /* Catch callers which need to be fixed */ + BUG_ON(preempt_count() || !irqs_disabled()); + + prev_state = exception_enter(); + + do { + preempt_disable(); + local_irq_enable(); + __schedule(true); + local_irq_disable(); + sched_preempt_enable_no_resched(); + } while (need_resched()); + + exception_exit(prev_state); +} +``` + + +###5.2.3 PREEMPT_ACTIVE标识位和PREEMPT_DISABLE_OFFSET +------- + +之前的内核版本中, 抢占计数器中于一个标识位PREEMPT_ACTIVE, 这个位设置后即标识了可以进行内核抢占, 使得preempt_count有一个很大的值, 这样就不受普通的抢占计数器加1操作的影响了 + +>PREEMPT_ACTIVE的引入, 参见[PREEMPT_ACTIVE: add default defines](https://lkml.org/lkml/2009/7/20/19) + +```c +// http://lxr.free-electrons.com/source/include/linux/preempt.h?v=4.3#L58 +#define PREEMPT_ACTIVE_BITS 1 +#define PREEMPT_ACTIVE_SHIFT (NMI_SHIFT + NMI_BITS) +#define PREEMPT_ACTIVE (__IRQ_MASK(PREEMPT_ACTIVE_BITS) << PREEMPT_ACTIVE_SHIFT) +``` + +然后也为其提供了一些置位的函数,其实就是将preempt_count加上/减去一个很大的数, 参见[preempt: Disable preemption from preempt_schedule*() callers](https://lkml.org/lkml/2015/5/11/519) + +但是在linux-4.4版本之后移除了这个标志, 取而代之的是在linux-4.2时引入的PREEMPT_DISABLE_OFFSET + +>参见 +> +>[Rename PREEMPT_CHECK_OFFSET to PREEMPT_DISABLE_OFFSET](https://lkml.org/lkml/2015/5/11/517) +>[ preempt: Rename PREEMPT_CHECK_OFFSET to PREEMPT_DISABLE_OFFSET](http://marc.info/?l=linux-kernel&m=143144178020323) +> +>[preempt: Remove PREEMPT_ACTIVE unmasking off in_atomic()](https://lkml.org/lkml/2015/5/11/521) +> +>[sched: Kill PREEMPT_ACTIVE](https://lkml.org/lkml/2015/9/30/108) +> +>[sched: Stop setting PREEMPT_ACTIVE](https://lkml.org/lkml/2015/9/29/224) + + + +>参考 +> +>[内核随记(二)——内核抢占与中断返回](http://www.cnblogs.com/hustcat/archive/2009/08/31/1557507.html) +> +>[PREEMPT_ACTIVE](http://www.cnblogs.com/openix/archive/2013/03/09/2952041.html) + + + +#6 总结 +------- + +一般来说,CPU在任何时刻都处于以下三种情况之一: + +1. 运行于用户空间,执行用户进程 + +2. 运行于内核空间,处于进程上下文 + +3. 运行于内核空间,处于中断上下文 + + +##6.1 用户抢占 +------- + +一般来说, 当进程从系统调用或者从中断(异常)处理程序返回用户空间时会触发主调度器进行用户抢占 + +* 从系统调用返回用户空间 + +* 从中断(异常)处理程序返回用户空间 + +为了对一个进程需要被调度进行标记, 内核在thread_info的flag中设置了一个标识来标志进程是否需要重新调度, 即重新调度need_resched标识TIF_NEED_RESCHED, 内核在即将返回用户空间时会检查标识TIF_NEED_RESCHED标志进程是否需要重新调度,如果设置了,就会发生调度, 这被称为**用户抢占** + + +##6.2 内核抢占 +------- + +如果内核处于相对耗时的操作中, 比如文件系统或者内存管理相关的任务, 这种行为可能会带来问题. 这种情况下, 内核代替特定的进程执行相当长的时间, 而其他进程无法执行, 无法调度, 这就造成了系统的延迟增加, 用户体验到"缓慢"的响应. 因此linux内核引入了内核抢占. + + linux内核通过在thread_info结构中添加了一个自旋锁标识preempt_count, 称为**抢占计数器(preemption counter)**来作为内核抢占的标记, + +内核抢占的触发大致也是两类, 内核抢占关闭后重新开启时, 中断返回内核态时 + +* 内核重新开启内核抢占时使用preempt_schedule检查内核抢占 + +* 中断之后返回内核态时通过preempt_schedule_irq触发内核抢占 + + +而内核抢占时, 通过调用__schedule(true)传入的preempt=true来通知内核, 这是一个内核抢占 + + + + +