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 index 7a60f45..8167d72 100644 --- a/study/kernel/01-process/05-schedule/07-context_switch/README.md +++ b/study/kernel/01-process/05-schedule/07-context_switch/README.md @@ -367,6 +367,9 @@ qizhongenter_lazy_tlb通知底层体系结构不需要切换虚拟地址空间 ##3.4 switch_mm切换进程虚拟地址空间 ------- +###3.4.1 switch_mm函数 +------- + switch_mm主要完成了进程prev到next虚拟地址空间的映射, 由于内核虚拟地址空间是不许呀切换的, 因此切换的主要是用户态的虚拟地址空间 这个是一个体系结构相关的函数, 其实现在对应体系结构下的[arch/对应体系结构/include/asm/mmu_context.h](http://lxr.free-electrons.com/ident?v=4.6;i=switch_mm)文件中, 我们下面列出了几个常见体系结构的实现 @@ -379,16 +382,62 @@ switch_mm主要完成了进程prev到next虚拟地址空间的映射, 由于内 其主要工作就是切换了进程的CR3 ->控制寄存器(CR0~CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性 -> ->CR0中含有控制处理器操作模式和状态的系统控制标志; -> ->CR1保留不用; -> ->CR2含有导致页错误的线性地址; -> ->CR3中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base address Register)。 +###3.4.2 CPU-CR0~CR4寄存器 +------- +控制寄存器(CR0~CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性 + + +| 控制寄存器 | 描述 | +| ------- |:-------:| +| CR0 | 含有控制处理器操作模式和状态的系统控制标志 | +| CR1 | 保留不用, 未定义的控制寄存器,供将来的处理器使用 | +| CR3 | 含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base address Register), 保存页目录表的物理地址,页目录表总是放在以4K字节为单位的存储器边界上,因此,它的地址的低12位总为0,不起作用,即使写上内容,也不会被理会 | +| CR4 | 在Pentium系列(包括486的后期版本)处理器中才实现,它处理的事务包括诸如何时启用虚拟8086模式等 | + +### 3.4.3 保护模式下的GDT、LDT和IDT +------- + +保护模式下三个重要的系统表——GDT、LDT和IDT + +这三个表是在内存中由操作系统或系统程序员所建,并不是固化在哪里,所以从理论上是可以被读写的。 + +这三个表都是描述符表. 描述符表是由若干个描述符组成, 每个描述符占用8个字节的内存空间, 每个描述符表内最多可以有8129个描述符. 描述符是描述一个段的大小,地址及各种状态的。 + +描述符表有三种,分别为**全局描述符表GDT**、**局部描述符表LDT**和**中断描述符表IDT** + + +| 描述符表 | 描述 | +| ------- |:-------:| +| 全局描述符表GDT | 全局描述符表在系统中只能有一个,且可以被每一个任务所共享. +任何描述符都可以放在GDT中,但中断门和陷阱门放在GDT中是不会起作用的. 能被多个任务共享的内存区就是通过GDT完成的 | +| 局部描述符表LDT | 局部描述符表在系统中可以有多个,通常情况下是与任务的数量保持对等,但任务可以没有局部描述符表.

任务间不相干的部分也是通过LDT实现的.这里涉及到地址映射的问题.

和GDT一样,中断门和陷阱门放在LDT中是不会起作用的. | +| 中断描述符表IDT | 和GDT一样,中断描述符表在系统最多只能有一个,中断描述符表内可以存放256个描述符,分别对应256个中断.因为每个描述符占用8个字节,所以IDT的长度可达2K.

中断描述符表中可以有任务门、中断门、陷阱门三个门描述符,其它的描述符在中断描述符表中无意义 | + +**段选择子** + +在保护模式下,段寄存器的内容已不是段值,而称其为选择子. +该选择子指示描述符在上面这三个表中的位置,所以说选择子即是索引值。 +当我们把段选择子装入寄存器时不仅使该寄存器值,同时CPU将该选择子所对应的GDT或LDT中的描述符装入了不可见部分。 +这样只要我们不进行代码切换(不重新装入新的选择子)CPU就不会对不可见部分存储的描述符进行更新,可以直接进行访问,加快了访问速度。 +一旦寄存器被重新赋值,不可见部分也将被重新赋值。 + +**关于选择子的值是否连续** + +关于选择子的值,我认为不一定要连续。 +但是每个描述符的起始地址相对于第一个描述符(即空描述符)的首地址的偏移必须是8的倍数,即二进制最后三位为0。这样通过全局描述符表寄存器GDTR找到全局描述符表的首地址后,使用段选择子的高13位索引到正确的描述符表项(段选择子的高13位左移3位加上GDTR的值即为段选择子指定的段描述符的逻辑首地址) + +也就是说在两个段选择符之间可以填充能被8整除个字节值。当然,如果有选择子指向了这些填充的字节,一般会出错,除非你有意填充一些恰当的数值,呵呵。 + +**关于为什么LDT要放在GDT中 -LDT中的描述符和GDT中的描述符** + +除了选择子的bit3一个为0一个为1用于区分该描述符是在GDT中还是在LDT中外,描述符本身的结构完全一样。 +开始我考虑既然是这样,为什么要将LDT放在GDT中而不是像GDT那样找一个GDTR寄存器呢? + +后来终于明白了原因——很简单, +GDT表只有一个,是固定的;而LDT表每个任务就可以有一个,因此有多个,并且由于任务的个数在不断变化其数量也在不断变化。 + +如果只有一个LDTR寄存器显然不能满足多个LDT的要求。因此INTEL的做法是把它放在放在GDT中。 ##3.5 prev是内核线程时的处理 ------- @@ -410,17 +459,175 @@ if (!prev->mm) } ``` +###3.4.4 switch_mm函数注释 +------- +下面我们提取了x86架构下的switch_mm函数, 其定义在[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) + +```c +// http://lxr.free-electrons.com/source/arch/x86/include/asm/mmu_context.h?v=4.6#L118 +static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, + struct task_struct *tsk) +{ + unsigned cpu = smp_processor_id(); + + + /* 确保prev和next不是同一进程 */ + if (likely(prev != next)) + { +#ifdef CONFIG_SMP + /* 刷新cpu地址转换后备缓冲器TLB */ + this_cpu_write(cpu_tlbstate.state, TLBSTATE_OK); + this_cpu_write(cpu_tlbstate.active_mm, next); +#endif + /* 设置当前进程的mm->cpu_vm_mask表示其占用cpu */ + cpumask_set_cpu(cpu, mm_cpumask(next)); + + /* + * Re-load page tables. + * + * This logic has an ordering constraint: + * + * CPU 0: Write to a PTE for 'next' + * CPU 0: load bit 1 in mm_cpumask. if nonzero, send IPI. + * CPU 1: set bit 1 in next's mm_cpumask + * CPU 1: load from the PTE that CPU 0 writes (implicit) + * + * We need to prevent an outcome in which CPU 1 observes + * the new PTE value and CPU 0 observes bit 1 clear in + * mm_cpumask. (If that occurs, then the IPI will never + * be sent, and CPU 0's TLB will contain a stale entry.) + * + * The bad outcome can occur if either CPU's load is + * reordered before that CPU's store, so both CPUs must + * execute full barriers to prevent this from happening. + * + * Thus, switch_mm needs a full barrier between the + * store to mm_cpumask and any operation that could load + * from next->pgd. TLB fills are special and can happen + * due to instruction fetches or for no reason at all, + * and neither LOCK nor MFENCE orders them. + * Fortunately, load_cr3() is serializing and gives the + * ordering guarantee we need. + * + * 将新进程的pgd页目录表填写到cpu的cr3寄存器中 + */ + load_cr3(next->pgd); + + trace_tlb_flush(TLB_FLUSH_ON_TASK_SWITCH, TLB_FLUSH_ALL); + + /* Stop flush ipis for the previous mm + * 除prev的cpu_vm_mask,表示prev放弃使用cpu */ + cpumask_clear_cpu(cpu, mm_cpumask(prev)); + + /* Load per-mm CR4 state + */ + load_mm_cr4(next); + +#ifdef CONFIG_MODIFY_LDT_SYSCALL + /* + * Load the LDT, if the LDT is different. + * + * It's possible that prev->context.ldt doesn't match + * the LDT register. This can happen if leave_mm(prev) + * was called and then modify_ldt changed + * prev->context.ldt but suppressed an IPI to this CPU. + * In this case, prev->context.ldt != NULL, because we + * never set context.ldt to NULL while the mm still + * exists. That means that next->context.ldt != + * prev->context.ldt, because mms never share an LDT. + * + * + */ + if (unlikely(prev->context.ldt != next->context.ldt)) + load_mm_ldt(next); +#endif + } +#ifdef CONFIG_SMP + else + { + this_cpu_write(cpu_tlbstate.state, TLBSTATE_OK); + BUG_ON(this_cpu_read(cpu_tlbstate.active_mm) != next); + + if (!cpumask_test_cpu(cpu, mm_cpumask(next))) + { + /* + * On established mms, the mm_cpumask is only changed + * from irq context, from ptep_clear_flush() while in + * lazy tlb mode, and here. Irqs are blocked during + * schedule, protecting us from simultaneous changes. + */ + cpumask_set_cpu(cpu, mm_cpumask(next)); + + /* + * We were in lazy tlb mode and leave_mm disabled + * tlb flush IPI delivery. We must reload CR3 + * to make sure to use no freed page tables. + * + * As above, load_cr3() is serializing and orders TLB + * fills with respect to the mm_cpumask write. + */ + load_cr3(next->pgd); + trace_tlb_flush(TLB_FLUSH_ON_TASK_SWITCH, TLB_FLUSH_ALL); + load_mm_cr4(next); + load_mm_ldt(next); + } + } +#endif +} +``` + ##3.6 switch_to完成进程切换 ------- +###3.6.1 switch_to函数 +------- 最后用switch_to完成了进程的切换, 该函数切换了寄存器状态和栈, 新进程在该调用后开始执行, 而switch_to之后的代码只有在当前进程下一次被选择运行时才会执行 执行环境的切换是在switch_to()中完成的, switch_to完成最终的进程切换,它保存原进程的所有寄存器信息,恢复新进程的所有寄存器信息,并执行新的进程 -**为什么switch_to需要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是栈底指针, 它确定了当前用户空间属于哪个进程 + + +__switch_to函数 + +| 体系结构 | __switch_to实现 | +| ------- |:-------:| +| x86 | [arch/x86/kernel/process_32.c, line 242](http://lxr.free-electrons.com/source/arch/x86/kernel/process_32.c?v=4.6#L242) | +| x86_64 | [arch/x86/kernel/process_64.c, line 277](http://lxr.free-electrons.com/source/arch/x86/kernel/process_64.c?v=4.6#L277) | +| arm64 | [arch/arm64/kernel/process.c, line 329](http://lxr.free-electrons.com/source/arch/arm64/kernel/process.c?v=4.6#L329) + + + + + + +###3.6.2 为什么switch_to需要3个参数 +------- 调度过程可能选择了一个新的进程, 而清理工作则是针对此前的活动进程, 请注意, 这不是发起上下文切换的那个进程, 而是系统中随机的某个其他进程, 内核必须想办法使得进程能够与context_switch例程通信, 这就可以通过switch_to宏实现. 因此switch_to函数通过3个参数提供2个变量. @@ -446,37 +653,78 @@ if (!prev->mm) 内核实现该行为特性的方式依赖于底层的体系结构, 但内核显然可以通过考虑两个进程的内核栈来重建所需要的信息 +###3.6.3 switch_to函数注释 +------- +switch_mm()进行用户空间的切换, 更确切地说, 是切换地址转换表(pgd), 由于pgd包括内核虚拟地址空间和用户虚拟地址空间地址映射, linux内核把进程的整个虚拟地址空间分成两个部分, 一部分是内核虚拟地址空间, 另外一部分是内核虚拟地址空间, 各个进程的虚拟地址空间各不相同, 但是却共用了同样的内核地址空间, 这样在进程切换的时候, 就只需要切换虚拟地址空间的用户空间部分. + +每个进程都有其自身的页目录表pgd + +进程本身尚未切换, 而存储管理机制的页目录指针cr3却已经切换了,这样不会造成问题吗?不会的,因为这个时候CPU在系统空间运行,而所有进程的页目录表中与系统空间对应的目录项都指向相同的页表,所以,不管切换到哪一个进程的页目录表都一样,受影响的只是用户空间,系统空间的映射则永远不变 + +我们下面来分析一下子, x86_32位下的switch_to函数, 其定义在[arch/x86/include/asm/switch_to.h, line 27](http://lxr.free-electrons.com/source/arch/x86/include/asm/switch_to.h?v=4.6#L27) + +先对flags寄存器和ebp压入旧进程内核栈,并将确定旧进程恢复执行的下一跳地址,并将旧进程ip,esp保存到task_struct->thread_info中,这样旧进程保存完毕;然后用新进程的thread_info->esp恢复新进程的内核堆栈,用thread->info的ip恢复新进程地址执行。 +关键点:内核寄存器[eflags、ebp保存到内核栈;内核栈esp地址、ip地址保存到thread_info中,task_struct在生命期中始终是全局的,所以肯定能根据该结构恢复出其所有执行场景来] ```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) + */ +#define switch_to(prev, next, last) \ +do { \ + /* \ + * Context-switching clobbers all registers, so we clobber \ + * them explicitly, via unused output variables. \ + * (EAX and EBP is not listed because EBP is saved/restored \ + * explicitly for wchan access and EAX is the return value of \ + * __switch_to()) \ + */ \ + unsigned long ebx, ecx, edx, esi, edi; \ + \ + asm volatile("pushfl\n\t" /* save flags 保存就的ebp、和flags寄存器到旧进程的内核栈中*/ \ + "pushl %%ebp\n\t" /* save EBP */ \ + "movl %%esp,%[prev_sp]\n\t" /* save ESP 将旧进程esp保存到thread_info结构中 */ \ + "movl %[next_sp],%%esp\n\t" /* restore ESP 用新进程esp填写esp寄存器,此时内核栈已切换 */ \ + "movl $1f,%[prev_ip]\n\t" /* save EIP 将该进程恢复执行时的下条地址保存到旧进程的thread中*/ \ + "pushl %[next_ip]\n\t" /* restore EIP 将新进程的ip值压入到新进程的内核栈中 */ \ + __switch_canary \ + "jmp __switch_to\n" /* regparm call */ \ + "1:\t" \ + "popl %%ebp\n\t" /* restore EBP 该进程执行,恢复ebp寄存器*/ \ + "popfl\n" /* restore flags 恢复flags寄存器*/ \ + \ + /* output parameters */ \ + : [prev_sp] "=m" (prev->thread.sp), \ + [prev_ip] "=m" (prev->thread.ip), \ + "=a" (last), \ + \ + /* clobbered output registers: */ \ + "=b" (ebx), "=c" (ecx), "=d" (edx), \ + "=S" (esi), "=D" (edi) \ + \ + __switch_canary_oparam \ + \ + /* input parameters: */ \ + : [next_sp] "m" (next->thread.sp), \ + [next_ip] "m" (next->thread.ip), \ + \ + /* regparm parameters for __switch_to(): */ \ + [prev] "a" (prev), \ + [next] "d" (next) \ + \ + __switch_canary_iparam \ + \ + : /* reloaded segment registers */ \ + "memory"); \ +} while (0) ``` - -| 体系结构 | 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是栈底指针, 它确定了当前用户空间属于哪个进程 - - - - - -##3.7 finish_task_switch完成清理工作 +## 3.7 barrier路障同步 ------- + + witch_to完成了进程的切换, 新进程在该调用后开始执行, 而switch_to之后的代码只有在当前进程下一次被选择运行时才会执行. ```c @@ -520,21 +768,103 @@ barrier往往通过编译器指令来实现, 内核中多处都实现了barrier, 关于内存屏障的详细信息, 可以参见 [Linux内核同步机制之(三):memory barrier](http://www.wowotech.net/kernel_synchronization/memory-barrier.html) + + +##3.8 finish_task_switch完成清理工作 +------- + finish_task_switch完成一些清理工作, 使得能够正确的释放锁, 但我们不会详细讨论这些. 他会向各个体系结构提供了另一个挂钩上下切换过程的可能性, 当然这只在少数计算机上需要. +前面我们谅解switch_to函数的3个参数时, 讲到 +注:A进程切换到B, A被切换, 而当A再次被选择执行, C再次切换到A,此时A执行,但是系统为了告知调度器A再次执行前的进程是C, 通过switch_to的last参数返回的prev指向C,在A调度时候需要把调用A的进程的信息清除掉 -#4 switch_mm切换虚拟地址空间详细分析 -------- +由于从C切换到A时候, A内核栈中保存的实际上是A切换出时的状态信息, 即prev=A, next=B,但是在A执行时, 其位于context_switch上下文中, 该函数的last参数返回的prev应该是切换到A的进程C, A负责对C进程信息进行切换后处理,比如,如果切换到A后,A发现C进程已经处于TASK_DEAD状态,则将释放C进程的TASK_STRUCT结构 -switch_mm()进行用户空间的切换, 更确切地说, 是切换地址转换表(pgd), 由于pgd包括内核虚拟地址空间和用户虚拟地址空间地址映射, linux内核把进程的整个虚拟地址空间分成两个部分, 一部分是内核虚拟地址空间, 另外一部分是内核虚拟地址空间, 各个进程的虚拟地址空间各不相同, 但是却共用了同样的内核地址空间, 这样在进程切换的时候, 就只需要切换虚拟地址空间的用户空间部分. +函数定义在[kernel/sched/core.c, line 2715](http://lxr.free-electrons.com/source/kernel/sched/core.c#?v=4.6L2715)中, 如下所示 -每个进程都有其自身的页目录表pgd +```c +/** + * finish_task_switch - clean up after a task-switch + * @prev: the thread we just switched away from. + * + * finish_task_switch must be called after the context switch, paired + * with a prepare_task_switch call before the context switch. + * finish_task_switch will reconcile locking set up by prepare_task_switch, + * and do any other architecture-specific cleanup actions. + * + * Note that we may have delayed dropping an mm in context_switch(). If + * so, we finish that here outside of the runqueue lock. (Doing it + * with the lock held can cause deadlocks; see schedule() for + * details.) + * + * The context switch have flipped the stack from under us and restored the + * local variables which were saved when this task called schedule() in the + * past. prev == current is still correct but we need to recalculate this_rq + * because prev may have moved to another CPU. + */ +static struct rq *finish_task_switch(struct task_struct *prev) + __releases(rq->lock) +{ + struct rq *rq = this_rq(); + struct mm_struct *mm = rq->prev_mm; + long prev_state; -进程本身尚未切换, 而存储管理机制的页目录指针cr3却已经切换了,这样不会造成问题吗?不会的,因为这个时候CPU在系统空间运行,而所有进程的页目录表中与系统空间对应的目录项都指向相同的页表,所以,不管切换到哪一个进程的页目录表都一样,受影响的只是用户空间,系统空间的映射则永远不变 + /* + * The previous task will have left us with a preempt_count of 2 + * because it left us after: + * + * schedule() + * preempt_disable(); // 1 + * __schedule() + * raw_spin_lock_irq(&rq->lock) // 2 + * + * Also, see FORK_PREEMPT_COUNT. + */ + if (WARN_ONCE(preempt_count() != 2*PREEMPT_DISABLE_OFFSET, + "corrupted preempt_count: %s/%d/0x%x\n", + current->comm, current->pid, preempt_count())) + preempt_count_set(FORK_PREEMPT_COUNT); -##5 switch_to切换寄存器状态和内核栈详细分析 -------- + rq->prev_mm = NULL; + + /* + * A task struct has one reference for the use as "current". + * If a task dies, then it sets TASK_DEAD in tsk->state and calls + * schedule one last time. The schedule call will never return, and + * the scheduled task must drop that reference. + * + * We must observe prev->state before clearing prev->on_cpu (in + * finish_lock_switch), otherwise a concurrent wakeup can get prev + * running on another CPU and we could rave with its RUNNING -> DEAD + * transition, resulting in a double drop. + */ + prev_state = prev->state; + vtime_task_switch(prev); + perf_event_task_sched_in(prev, current); + finish_lock_switch(rq, prev); + finish_arch_post_lock_switch(); + + fire_sched_in_preempt_notifiers(current); + if (mm) + mmdrop(mm); + if (unlikely(prev_state == TASK_DEAD)) /* 如果上一个进程已经终止,释放其task_struct 结构 */ + { + if (prev->sched_class->task_dead) + prev->sched_class->task_dead(prev); + + /* + * Remove function-return probe instances associated with this + * task and put them back on the free list. + */ + kprobe_flush_task(prev); + put_task_struct(prev); + } + + tick_nohz_task_switch(); + return rq; +} +``` diff --git a/study/kernel/01-process/05-schedule/07-context_switch/finish_task_switch.c b/study/kernel/01-process/05-schedule/07-context_switch/finish_task_switch.c new file mode 100644 index 0000000..5cd1621 --- /dev/null +++ b/study/kernel/01-process/05-schedule/07-context_switch/finish_task_switch.c @@ -0,0 +1,80 @@ +/** + * finish_task_switch - clean up after a task-switch + * @prev: the thread we just switched away from. + * + * finish_task_switch must be called after the context switch, paired + * with a prepare_task_switch call before the context switch. + * finish_task_switch will reconcile locking set up by prepare_task_switch, + * and do any other architecture-specific cleanup actions. + * + * Note that we may have delayed dropping an mm in context_switch(). If + * so, we finish that here outside of the runqueue lock. (Doing it + * with the lock held can cause deadlocks; see schedule() for + * details.) + * + * The context switch have flipped the stack from under us and restored the + * local variables which were saved when this task called schedule() in the + * past. prev == current is still correct but we need to recalculate this_rq + * because prev may have moved to another CPU. + */ +static struct rq *finish_task_switch(struct task_struct *prev) + __releases(rq->lock) +{ + struct rq *rq = this_rq(); + struct mm_struct *mm = rq->prev_mm; + long prev_state; + + /* + * The previous task will have left us with a preempt_count of 2 + * because it left us after: + * + * schedule() + * preempt_disable(); // 1 + * __schedule() + * raw_spin_lock_irq(&rq->lock) // 2 + * + * Also, see FORK_PREEMPT_COUNT. + */ + if (WARN_ONCE(preempt_count() != 2*PREEMPT_DISABLE_OFFSET, + "corrupted preempt_count: %s/%d/0x%x\n", + current->comm, current->pid, preempt_count())) + preempt_count_set(FORK_PREEMPT_COUNT); + + rq->prev_mm = NULL; + + /* + * A task struct has one reference for the use as "current". + * If a task dies, then it sets TASK_DEAD in tsk->state and calls + * schedule one last time. The schedule call will never return, and + * the scheduled task must drop that reference. + * + * We must observe prev->state before clearing prev->on_cpu (in + * finish_lock_switch), otherwise a concurrent wakeup can get prev + * running on another CPU and we could rave with its RUNNING -> DEAD + * transition, resulting in a double drop. + */ + prev_state = prev->state; + vtime_task_switch(prev); + perf_event_task_sched_in(prev, current); + finish_lock_switch(rq, prev); + finish_arch_post_lock_switch(); + + fire_sched_in_preempt_notifiers(current); + if (mm) + mmdrop(mm); + if (unlikely(prev_state == TASK_DEAD)) /* 如果上一个进程已经终止,释放其task_struct 结构 */ + { + if (prev->sched_class->task_dead) + prev->sched_class->task_dead(prev); + + /* + * Remove function-return probe instances associated with this + * task and put them back on the free list. + */ + kprobe_flush_task(prev); + put_task_struct(prev); + } + + tick_nohz_task_switch(); + return rq; +} \ No newline at end of file diff --git a/study/kernel/01-process/05-schedule/07-context_switch/switch_mm.c b/study/kernel/01-process/05-schedule/07-context_switch/switch_mm.c index 6e0e4b8..33570f2 100644 --- a/study/kernel/01-process/05-schedule/07-context_switch/switch_mm.c +++ b/study/kernel/01-process/05-schedule/07-context_switch/switch_mm.c @@ -4,13 +4,16 @@ static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, { unsigned cpu = smp_processor_id(); + + /* 确保prev和next不是同一进程 */ if (likely(prev != next)) { #ifdef CONFIG_SMP + /* 刷新cpu地址转换后备缓冲器TLB */ this_cpu_write(cpu_tlbstate.state, TLBSTATE_OK); this_cpu_write(cpu_tlbstate.active_mm, next); #endif - // + /* 设置当前进程的mm->cpu_vm_mask表示其占用cpu */ cpumask_set_cpu(cpu, mm_cpumask(next)); /* @@ -39,16 +42,20 @@ static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, * and neither LOCK nor MFENCE orders them. * Fortunately, load_cr3() is serializing and gives the * ordering guarantee we need. - * + * + * 将新进程的pgd页目录表填写到cpu的cr3寄存器中 */ load_cr3(next->pgd); trace_tlb_flush(TLB_FLUSH_ON_TASK_SWITCH, TLB_FLUSH_ALL); - /* Stop flush ipis for the previous mm */ + /* Stop flush ipis for the previous mm + * 除prev的cpu_vm_mask,表示prev放弃使用cpu */ cpumask_clear_cpu(cpu, mm_cpumask(prev)); - /* Load per-mm CR4 state */ + /* Load per-mm CR4 state + * 加载 + */ load_mm_cr4(next); #ifdef CONFIG_MODIFY_LDT_SYSCALL @@ -63,17 +70,21 @@ static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, * never set context.ldt to NULL while the mm still * exists. That means that next->context.ldt != * prev->context.ldt, because mms never share an LDT. + * + * */ if (unlikely(prev->context.ldt != next->context.ldt)) load_mm_ldt(next); #endif } #ifdef CONFIG_SMP - else { + else + { this_cpu_write(cpu_tlbstate.state, TLBSTATE_OK); BUG_ON(this_cpu_read(cpu_tlbstate.active_mm) != next); - if (!cpumask_test_cpu(cpu, mm_cpumask(next))) { + if (!cpumask_test_cpu(cpu, mm_cpumask(next))) + { /* * On established mms, the mm_cpumask is only changed * from irq context, from ptep_clear_flush() while in diff --git a/study/kernel/01-process/05-schedule/07-context_switch/switch_to.c b/study/kernel/01-process/05-schedule/07-context_switch/switch_to.c index 5f03ecb..2926b60 100644 --- a/study/kernel/01-process/05-schedule/07-context_switch/switch_to.c +++ b/study/kernel/01-process/05-schedule/07-context_switch/switch_to.c @@ -33,16 +33,16 @@ do { \ */ \ unsigned long ebx, ecx, edx, esi, edi; \ \ - asm volatile("pushfl\n\t" /* save flags */ \ + asm volatile("pushfl\n\t" /* save flags 保存就的ebp、和flags寄存器到旧进程的内核栈中*/ \ "pushl %%ebp\n\t" /* save EBP */ \ - "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ - "movl %[next_sp],%%esp\n\t" /* restore ESP */ \ - "movl $1f,%[prev_ip]\n\t" /* save EIP */ \ - "pushl %[next_ip]\n\t" /* restore EIP */ \ + "movl %%esp,%[prev_sp]\n\t" /* save ESP 将旧进程esp保存到thread_info结构中 */ \ + "movl %[next_sp],%%esp\n\t" /* restore ESP 用新进程esp填写esp寄存器,此时内核栈已切换 */ \ + "movl $1f,%[prev_ip]\n\t" /* save EIP 将该进程恢复执行时的下条地址保存到旧进程的thread中*/ \ + "pushl %[next_ip]\n\t" /* restore EIP 将新进程的ip值压入到新进程的内核栈中 */ \ __switch_canary \ "jmp __switch_to\n" /* regparm call */ \ "1:\t" \ - "popl %%ebp\n\t" /* restore EBP */ \ + "popl %%ebp\n\t" /* restore EBP 该进程执行,恢复ebp寄存器*/ \ "popfl\n" /* restore flags */ \ \ /* output parameters */ \