Linux内核分析——第八周学习笔记
实验作业:进程调度时机跟踪分析进程调度与进程切换的过程
20135313吴子怡.北京电子科技学院
【第一部分】理解Linux系统中进程调度的时机
1.Linux的调度程序是一个叫schedule()
的函数,这个函数被调用的频率很高,由它来决定是否要进行进程的切换,如果要切换的话,切换到哪个进程等等。
2.Linux调度时机主要有:
1 2 3 | 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule() 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度; 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。 |
【第二部分】使用gdb跟踪分析一个schedule()函数 ,验证对Linux系统进程调度与进程切换过程的理解
使用MenuOS进行调试,并设置合适的断点。
①首先在schedule处停下来:
![process1.png](http://upload-images.jianshu.io/upload_images/307862-9ad4ced5facd6fa0.png)
②查看当前进程tsk
,观察到该进程pid=1,stack=0xC7858000
![process2.png](http://upload-images.jianshu.io/upload_images/307862-99ae564e9592eb5d.png)
③继续执行,到__schedule中的关键函数pick_next_task停下
![process3.png](http://upload-images.jianshu.io/upload_images/307862-3b9f76c8a18ba166.png)
④查看队列rq
![process4.png](http://upload-images.jianshu.io/upload_images/307862-baa93d2f4ac08c1e.png)
⑤context_switch
![process5.png](http://upload-images.jianshu.io/upload_images/307862-34558045fa12f61a.png)
⑥switch_to宏&__switch_to函数
![process6.png](http://upload-images.jianshu.io/upload_images/307862-6b27d56ed11aaa2a.png)
⑦在这里查看切换的进程prev&next,prev就是最开始tsk
![process7.png](http://upload-images.jianshu.io/upload_images/307862-46b87aee1bc2803d.png)
![process8.png](http://upload-images.jianshu.io/upload_images/307862-4d3a7ed3f3d98768.png)
【第三部分】分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系
1.关键函数的调用关系:
1 | schedule() --> context_switch() --> switch_to --> __switch_to() |
- schedule()
这里调用__schedule(),tsk为当前进程。
1 2 3 4 5 6 | asmlinkage __visible void __sched schedule( void ) { struct task_struct *tsk = current; sched_submit_work(tsk); __schedule(); } |
- __schedule()
该函数包含了一些:
1 2 3 4 5 6 | 针对抢占的处理 自旋锁(raw_spin_lock_irq(&rq-> lock );) 检查prev的状态,并且重设state的状态 进程调度算法(next = pick_next_task(rq, prev);) 更新就绪队列的时钟 进程上下文切换(context_switch(rq, prev, next);) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | static void __sched __schedule( void ) { struct task_struct *prev, *next; unsigned long *switch_count; struct rq *rq; int cpu; ... //调度算法 next = pick_next_task(rq, prev); clear_tsk_need_resched(prev); clear_preempt_need_resched(); rq->skip_clock_update = 0; if (likely(prev != next)) { rq->nr_switches++; rq->curr = next; ++*switch_count; //进程上下文切换 context_switch(rq, prev, next); cpu = smp_processor_id(); rq = cpu_rq(cpu); } else raw_spin_unlock_irq(&rq-> lock ); post_schedule(rq); sched_preempt_enable_no_resched(); if (need_resched()) goto need_resched; } |
- context_switch
在挑选得到了下一个即将被调度进来的进程之后,如果被选中的进程不是当前正在运行的进程,那么需要进行上下文切换以执行被选中的进程即context_switch
.
context_switch中包含了:
①判断是否为内核线程,即是否需要上下文切换
1 2 | 如果next是一个普通进程,schedule( )函数用next的地址空间替换prev的地址空间 如果prev是内核线程或正在退出的进程,context_switch()函数就把指向prev内存描述符的指针保存到运行队列的prev_mm字段中,然后重新设置prev->active_mm |
②切换堆栈和寄存器(switch_to(prev, next, prev);
)
注意:宏switch_to用来进行关键上下文切换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | static inline void context_switch( struct rq *rq, struct task_struct *prev, struct task_struct *next) { struct mm_struct *mm, *oldmm; prepare_task_switch(rq, prev, next); mm = next->mm; oldmm = prev->active_mm; arch_start_context_switch(prev); if (!mm) { next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); enter_lazy_tlb(oldmm, next); } else switch_mm(oldmm, mm, next); if (!prev->mm) { prev->active_mm = NULL; rq->prev_mm = oldmm; } spin_release(&rq-> lock .dep_map, 1, _THIS_IP_); context_tracking_task_switch(prev, next); /* Here we just switch the register state and the stack. */ switch_to(prev, next, prev); barrier(); finish_task_switch(this_rq(), prev); } |
宏switch_to
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | #define switch_to(prev, next, last) do { unsigned long ebx, ecx, edx, esi, edi; asm volatile ( "pushfl\n\t" /* save 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 */ __switch_canary "jmp __switch_to\n" /* regparm call */ "1:\t" "popl %%ebp\n\t" /* restore EBP */ "popfl\n" /* restore 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) |
分析:这个宏实现了进程之间的真正切换:
1 2 3 4 5 6 | 首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。 然后将prev的内核堆栈指针ebp存入prev->thread.esp中。 把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中 将popl指令所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度 通过jmp指令(而不是call指令)转入一个函数__switch_to() 恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行。 |
__switch_to函数
在宏switch_to中,用jmp
跳转到该函数运行。该函数主要进行一些针对TSS的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | __visible __notrace_funcgraph struct task_struct * __switch_to( struct task_struct *prev_p, struct task_struct *next_p) { struct thread_struct *prev = &prev_p->thread, *next = &next_p->thread; int cpu = smp_processor_id(); struct tss_struct *tss = &per_cpu(init_tss, cpu); fpu_switch_t fpu; fpu = switch_fpu_prepare(prev_p, next_p, cpu); load_sp0(tss, next); lazy_save_gs(prev->gs); load_TLS(next, cpu); if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl)) set_iopl_mask(next->iopl); task_thread_info(prev_p)->saved_preempt_count = this_cpu_read(__preempt_count); this_cpu_write(__preempt_count, task_thread_info(next_p)->saved_preempt_count); if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV || task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT)) __switch_to_xtra(prev_p, next_p, tss); arch_end_context_switch(next_p); this_cpu_write(kernel_stack, (unsigned long )task_stack_page(next_p) + THREAD_SIZE - KERNEL_STACK_OFFSET); if (prev->gs | next->gs) lazy_load_gs(next->gs); switch_fpu_finish(next_p, fpu); this_cpu_write(current_task, next_p); return prev_p; } |
【第四部分】堆栈状态、CPU寄存器状态分析
1.内核堆栈情况:
![stack1.png](http://upload-images.jianshu.io/upload_images/307862-80a4d949338087fe.png)
![stack2.png](http://upload-images.jianshu.io/upload_images/307862-c56c5beda32c930e.png)
![stack3.png](http://upload-images.jianshu.io/upload_images/307862-e95fa2b94d54bd96.png)
【第五部分】总结
对“Linux系统一般执行过程”的理解
1.在调度时机方面,内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度。
2.schedule()函数实现进程调度,context_ switch完成进程上下文切换,switch_ to完成寄存器的切换。
3.用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
【第六部分】附录
作者:吴子怡
学号:20135313
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步