2021-2022-1 20212802《Linux内核原理与分析》第九周作业

进程的切换和系统的一般执行过程

1.知识总结

(1)进程调度的时机:

  • 中断处理过程直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()。
  • 内核线程是一个特殊的进程,只有内核态没有用户态,可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度(内核线程可以直接访问内核函数,所以不会发生系统调用)。内核线程作为一类的特殊的进程可以主动调度,也可以被动调度。
  • 用户态进程无法实现主动调度,仅能在中断处理过程中进行调度(schedule是一个内核函数,不是一个系统调用)。

(2)挂起正在CPU上执行的进程,与中断时保存现场不同。中断前后是在同一个进程上下文中,只是由用户态转向内核态执行。进程上下文包含了进程执行需要的所有信息:

  • 用户地址空间:包括程序代码,数据,用户堆栈等
  • 控制信息:进程描述符,内核堆栈等
  • 硬件上下文

(3)schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,context_switch中的一个关键宏switch_to来进行关键上下文切换。

(4)0到3G用户可以访问,3G以上只有内核态可以访问。所有进程3G以上都是完全共享的,比如进程X切换到进程Y,但是地址空间仍然是3G以上的部分,只是把进程描述符和其他的进程上下文切换了,只有在返回的时候才不同。哪一个进程都可以“招手”进入内核态,走了一段以后便可以返回到用户态,空车的时候就进入idle进程空转。

2.关键代码分析

(1)schedule

asmlinkage__visible void __sched schedule(void)  
{  
         struct task_struct *tsk = current;  
   
         sched_submit_work(tsk);  
         __schedule();  
}  

schedule()的尾部调用了__schedule(),__schedule()的关键代码next = pick_next_task(rq, prev);封装了进程调度算法,使用某种进程调度策略选择下一个进程。得到调度策略后用context_switch(rq, prev, next);实现进程上下文的切换。其中最关键的switch_to(prev,next, prev); 切换堆栈和寄存器的状态。

(2)switch_to

#define switch_to(prev, next, last) //prev指向当前进程,next指向被调度的进程                                   
do {                                                                              
                                                        
         unsigned long ebx, ecx, edx, esi, edi;
                                  
         asm volatile("pushfl\n\t"  //把prev进程的flag保存到prev进程的内核堆栈中
                      "pushl %%ebp\n\t" //把prev进程的基址ebp保存到prev进程的内核堆栈中
           
                      "movl %%esp,%[prev_sp]\n\t"//把prev进程的内核栈esp保存到prev->thread.sp中
                      "movl %[next_sp],%%esp\n\t"//esp指向next进程的内核堆栈栈顶(next->thread.sp) 
                      
                      "movl $1f,%[prev_ip]\n\t"//把"1:\t"地址赋给prev->thread.ip,当prev进程下次被switch_to切回来时,从"1:\t"处执行,即往后执行"popl %%ebp\n\t"和"popfl\n"     
                      "pushl %[next_ip]\n\t"//把next->thread.ip压入next进程的内核堆栈栈顶 
                      __switch_canary                                        
                      "jmp __switch_to\n"//执行__switch_to()函数,完成硬件上下文切换  
                      "1:\t"                                                    
                      "popl %%ebp\n\t"   
                      "popfl\n"                         
                                                                                
                      /* 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():*/  
                      //jmp通过eax寄存器和edx寄存器传递参数
                        [prev]     "a" (prev),                                   
                        [next]     "d" (next)                                    
                                                                                     
                        __switch_canary_iparam                             
                                                                                    
                      : /* reloaded segment registers */                          
                     "memory");                                           
} while (0)  

[prev_sp] "=m"(prev->thread.sp),之前分析汇编的时候,看到的是使用标号(%0、%1、%2等)标记参数,为了更好的可读性,这里用字符串([prev_sp])来标记参数(prev->thread.sp)。

首先保存prev进程的flags,ebp,用"movl %%esp,%[prev_sp]""movl %[next_sp],%%esp"完成内核堆栈的切换,使esp指向next进程的内核堆栈栈顶,然后把prev进程的thread.ip设置为"1:\t"地址(等到prev进程下次被switch_to切回来执行时,从"1:\t"处执行)。将next->thread.ip保存到next进程的内核堆栈栈顶,接下来执行jmp __switch_to(注意这里用的是jmp而不是call)完成硬件上下文切换,执行结束返回时弹出next进程内核堆栈的栈顶保存的next->thread.ip,eip指向此位置。分两种情况讨论一下:

  • 如果next进程之前被switch_to切出去过(可以理解为它之前也做过prev进程),next进程的内核堆栈上有被切出去是保存的的ebp和flags。由于执行过movl $1f,%[prev_ip],所以next->thread.ip是"1:\t"地址,即__switch_to函数执行结束返回时弹出的是"1:\t",eip指向"1:\t",执行"popl %%ebp""popfl" 恢复next进程的ebp和flag,next进程就可以执行了。
  • 如果next进程之前没有被switch_to出去过,那么next->thread.ip是ret_from_fork。__switch_to函数返回后执行的就是ret_from_fork。

所以,如果使用call,会把call __switch_to的下一条1:\t压栈,执行结束后eip指向"1:\t",这只对第一种情况适用,无法满足第二种情况的需要去执行ret_from_fork。

进程调度相关源代码跟踪和分析

1. 配置运行MenuOS系统

重新克隆一个menu,然后重新编译内核。

2. 配置gdb远程调试和设置断点并跟踪分析schedule()函数

打开调试模式,另打开一个窗口进行gdb远程调试,配置gdb远程调试并设置断点,按c执行,停在schedule函数断点处。

总结

要点:

  • 中断处理过程直接调用schedule(),或者当内核返回用户态时根据need_resched标志调用schedule()。
  • 内核线程是一个特殊的进程,只有内核态没有用户态,可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度(内核线程可以直接访问内核函数,所以不会发生系统调用)。
  • 内核线程作为一类的特殊的进程可以主动调用schedule函数让出CPU,也可以被动调度。
    用户态进程无法实现主动调度,仅能在中断处理过程中进行调度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值