diff --git a/study/kernel/01-process/05-schedule/06-main_scheduler/README.md b/study/kernel/01-process/05-schedule/06-main_scheduler/README.md index bab9231..bed3f42 100644 --- a/study/kernel/01-process/05-schedule/06-main_scheduler/README.md +++ b/study/kernel/01-process/05-schedule/06-main_scheduler/README.md @@ -558,6 +558,7 @@ extern const struct sched_class idle_sched_class; **问题1 : 为什么要多次一举判断所有的进程是否全是cfs调度的普通非实时进程?** + 加快经常性事件, 是程序开发中一个优化的准则, 那么linux系统中最普遍的进程是什么呢? 肯定是非实时进程啊, 其调度器必然是cfs, 因此 ```c @@ -582,7 +583,12 @@ rev->sched_class == class && rq->nr_running == rq->cfs.h_nr_running ##context_switch进程上下文切换 ------- -**进程上下文切换** +>进程上下文的切换其实是一个很复杂的过程, 我们在这里不能详述, 但是我会尽可能说明白 +> +>具体的内容请参照 + +###进程上下文切换 +------- **上下文切换**(有时也称做**进程切换**或**任务切换**)是指CPU从一个进程或线程切换到另一个进程或线程 @@ -596,25 +602,88 @@ rev->sched_class == class && rq->nr_running == rq->cfs.h_nr_running 3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程 -上下文是指某一时间点CPU寄存器和程序计数器的内容, 广义上还包括内存中进程的弟子映射信息 +因此上下文是指某一时间点CPU寄存器和程序计数器的内容, 广义上还包括内存中进程的虚拟地址映射信息. 上下文切换只能发生在内核态中, 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 Linux相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少. +###context_switch流程 +------- + context_switch函数完成了进程上下文的切换, 其定义在[kernel/sched/core.c#L2715](http://lxr.free-electrons.com/source/kernel/sched/core.c#L2715), http://abcdxyzk.github.io/blog/2014/05/22/kernel-sched-tick/ -执行如下操作 - -* 调用switch_mm(), 把虚拟内存从一个进程映射切换到新进程中 - -* 调用switch_to(),从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息 - context_switch( )函数建立next进程的地址空间。进程描述符的active_mm字段指向进程所使用的内存描述符,而mm字段指向进程所拥有的内存描述符。对于一般的进程,这两个字段有相同的地址,但是,内核线程没有它自己的地址空间而且它的 mm字段总是被设置为 NULL context_switch( )函数保证:如果next是一个内核线程, 它使用prev所使用的地址空间 -进程切换并不是我们今天的重点, 我们将在后面的章节中着重讲解 +它主要执行如下操作 + +* 调用switch_mm(), 把虚拟内存从一个进程映射切换到新进程中 + +* 调用switch_to(),从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息 + + +由于不同架构下地址映射的机制有所区别, 而寄存器等信息弊病也是依赖于架构的, 因此switch_mm和switch_to两个函数均是体系结构相关的 + + +###switch_mm切换进程虚拟地址空间 +------- + +switch_mm主要完成了进程prev到next虚拟地址空间的映射, 由于内核虚拟地址空间是不许呀切换的, 因此切换的主要是用户态的虚拟地址空间 + +这个是一个体系结构相关的函数, 其实现在对应体系结构下的[arch/对应体系结构/include/asm/mmu_context.h](http://lxr.free-electrons.com/ident?v=4.6;i=switch_mm)文件中, 我们下面列出了几个常见体系结构的实现 + +| 体系结构 | switch_mm实现 | +| ------- |:-------:| +| x86 | [arch/x86/include/asm/mmu_context.h, line 118](http://lxr.free-electrons.com/source/arch/x86/include/asm/mmu_context.h?v=4.6#L118) | +| arm | [arch/arm/include/asm/mmu_context.h, line 126](http://lxr.free-electrons.com/source/arch/arm/include/asm/mmu_context.h?v=4.6#L126) | +| arm64 | [arch/arm64/include/asm/mmu_context.h, line 183](http://lxr.free-electrons.com/source/arch/arm64/include/asm/mmu_context.h?v=4.6#L183) + +其主要工作就是切换了进程的CR3 + +>控制寄存器(CR0~CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性 +> +>CR0中含有控制处理器操作模式和状态的系统控制标志; +> +>CR1保留不用; +> +>CR2含有导致页错误的线性地址; +> +>CR3中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base address Register)。 + + +###switch_to切换进程堆栈和寄存器 +------- + +执行环境的切换是在switch_to()中完成的 + +调度过程可能选择了一个新的进程, 而清理工作则是针对此前的活动进程, 请注意, 这不是发起上下文切换的那个进程, 而是系统中随机的某个其他进程, 内核必须想办法使得进程能够与context_switch例程通信, 这就可以通过switch_to宏实现. 因此switch_to函数通过3个参数提供2个变量, + +在新进程被选中时, 底层的进程切换冽程必须将此前执行的进程提供给context_switch, 由于控制流会回到陔函数的中间, 这无法用普通的函数返回值来做到, 因此提供了3个参数的宏 + +```c +/* + * Saving eflags is important. It switches not only IOPL between tasks, + * it also protects other tasks from NT leaking through sysenter etc. +*/ +#define switch_to(prev, next, last) +``` + + +| 体系结构 | switch_to实现 | +| ------- |:-------:| +| x86 | arch/x86/include/asm/switch_to.h中两种实现

[定义CONFIG_X86_32宏](http://lxr.free-electrons.com/source/arch/x86/include/asm/switch_to.h?v=4.6#L27)

[未定义CONFIG_X86_32宏](http://lxr.free-electrons.com/source/arch/x86/include/asm/switch_to.h?v=4.6#L103) | +| arm | [arch/arm/include/asm/switch_to.h, line 25](http://lxr.free-electrons.com/source/arch/arm/include/asm/switch_to.h?v=4.6#L18) | +| 通用 | [include/asm-generic/switch_to.h, line 25](http://lxr.free-electrons.com/source/include/asm-generic/switch_to.h?v=4.6#L25) | + +内核在switch_to中执行如下操作 + +1. 进程切换, 即esp的切换, 由于从esp可以找到进程的描述符 + +2. 硬件上下文切换, 设置ip寄存器的值, 并jmp到__switch_to函数 + +3. 堆栈的切换, 即ebp的切换, ebp是栈底指针,它确定了当前变量空间属于哪个进程 ##need_resched与TIF_NEED_RESCHED标识 diff --git a/study/kernel/01-process/05-schedule/06-main_scheduler/context_switch.c b/study/kernel/01-process/05-schedule/06-main_scheduler/context_switch.c index 04c3ea2..ccc26ff 100644 --- a/study/kernel/01-process/05-schedule/06-main_scheduler/context_switch.c +++ b/study/kernel/01-process/05-schedule/06-main_scheduler/context_switch.c @@ -18,15 +18,37 @@ context_switch(struct rq *rq, struct task_struct *prev, */ arch_start_context_switch(prev); - if (!mm) { - next->active_mm = oldmm; + /* 如果next是内核线程,则线程使用prev所使用的地址空间 + * schedule( )函数把该线程设置为懒惰TLB模式 + * 内核线程并不拥有自己的页表集(task_struct->mm = NULL) + * 它使用一个普通进程的页表集 + * 不过,没有必要使一个用户态线性地址对应的TLB表项无效 + * 因为内核线程不访问用户态地址空间。 + */ + if (!mm) /* 内核线程无虚拟地址空间, mm = NULL*/ + { + /* 内核线程的active_mm为上一个进程的mm + * 注意此时如果prev也是内核线程, + * 则oldmm为NULL, 即next->active_mm也为NULL */ + next->active_mm = oldmm; + /* 增加mm的引用计数 */ atomic_inc(&oldmm->mm_count); + /* 通知底层体系结构不需要切换虚拟地址空间的用户部分 + * 这种加速上下文切换的技术称为惰性TBL */ enter_lazy_tlb(oldmm, next); - } else + } + else /* 不是内核线程, 则需要切切换虚拟地址空间 */ switch_mm(oldmm, mm, next); - if (!prev->mm) { + /* 如果prev是内核线程或正在退出的进程 + * 就重新设置prev->active_mm + * 然后把指向prev内存描述符的指针保存到运行队列的prev_mm字段中 + */ + if (!prev->mm) + { + /* 将prev的active_mm赋值和为空 */ prev->active_mm = NULL; + /* 更新运行队列的prev_mm成员 */ rq->prev_mm = oldmm; } /* @@ -38,25 +60,25 @@ context_switch(struct rq *rq, struct task_struct *prev, lockdep_unpin_lock(&rq->lock); spin_release(&rq->lock.dep_map, 1, _THIS_IP_); - /* Here we just switch the register state and the stack. */ + /* Here we just switch the register state and the stack. + * 切换进程的执行环境, 包括堆栈和寄存器 + * 同时返回上一个执行的程序 + * 相当于prev = witch_to(prev, next) */ switch_to(prev, next, prev); - barrier(); + + /* switch_to之后的代码只有在 + * 当前进程再次被选择运行(恢复执行)时才会运行 + * 而此时当前进程恢复执行时的上一个进程可能跟参数传入时的prev不同 + * 甚至可能是系统中任意一个随机的进程 + * 因此switch_to通过第三个参数将此进程返回 + */ + + /* 路障同步, 一般用编译器指令实现 + * 确保了switch_to和finish_task_switch的执行顺序 + * 不会因为任何可能的优化而改变 */ + barrier(); + + /* 进程切换之后的处理工作 */ return finish_task_switch(prev); } - -/* - * nr_running and nr_context_switches: - * - * externally visible scheduler statistics: current number of runnable - * threads, total number of context switches performed since bootup. - */ -unsigned long nr_running(void) -{ - unsigned long i, sum = 0; - - for_each_online_cpu(i) - sum += cpu_rq(i)->nr_running; - - return sum; -} \ No newline at end of file