理解进程调度时机跟踪分析进程调度与进程切换的过程

  • 理解 Linux 系统中进程调度的时机,可以在内核代码中搜索 schedule()函数,看都是哪里调用了 schedule(),判断我们课程内容中的总结是否准确;
  • 使用 gdb 跟踪分析一个 schedule()函数 ,验证您对 Linux 系统进程调度与进程切换过程的理解;推荐在实验楼 Linux 虚拟机环境下完成实验。
  • 特别关注并仔细分析 switch_to 中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系;

一、实验过程

按照之前实验的流程操作

cd LinuxeKernel  //进入LinuxKernel目录
rm menu -rf  //将原有的menu目录删除
git clone https://github.com/mengning/menu.git  //从GitHub克隆新的menu目录
cd menu  //进入menu目录
mv test_exec.c test.c  //把当前目录下的文件test_exec.c重命名为test.c
make rootfs  //运行内核
cd ..  //退回上级目录   
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S  //冻结内核

另启一个终端

cd LinuxKernel 
gdb  //进入gdb
(gdb)file linux-3.18.6/vmlinux
(gdb)target remote:1234  //将GDB连接到正在运行的远程程序
设置断点
b schedule
b context_switch
b switch_to
b pick_next_task
按c继续运行之后按n单步执行跟踪分析schedule函数
当程序执行到schedule()函数时,GDB会自动停下。

二、内容理解

(1)理解 Linux 系统中进程调度的时机,看都是哪里调用了 schedule()

在Linux系统中,进程调度的时机是由内核决定的,通常发生在以下几种情况下:

1. 进程主动放弃CPU:当一个进程需要主动放弃CPU,让其他进程执行时,它会调用`schedule()`函数。例如,当进程主动调用了`sleep()`、`wait()`或者某些阻塞型系统调用时,它将放弃CPU并进入等待状态,同时触发进程调度。

2. 时间片耗尽:每个进程在分配给它的时间片用完后,内核会进行进程切换,选择另一个就绪态的进程运行。这是由内核的定时器驱动来实现的,定时器会在时间片耗尽时触发,调用`schedule()`函数进行进程调度。

3. 中断处理程序返回:当一个中断处理程序执行完毕并返回时,内核可能会根据优先级和调度算法决定是否进行进程切换。在中断处理过程中,可以通过调用`schedule()`函数来显式地请求进程调度。

4. 阻塞态进程变为就绪态:当一个处于阻塞态的进程所等待的条件满足后,它会变为就绪态,并加入到就绪队列中等待运行。此时,内核会进行进程调度,选择一个合适的进程运行。

(2)关于switch_to函数

31  #define switch_to(prev, next, last)                    
32  do {                                 
33    /*                              
34     * Context-switching clobbers all registers, so we clobber  
35     * them explicitly, via unused output variables.     
36     * (EAX and EBP is not listed because EBP is saved/restored  
37     * explicitly for wchan access and EAX is the return value of   
38     * __switch_to())                     
39     */                                
40    unsigned long ebx, ecx, edx, esi, edi;                
41                                    
42    asm volatile("pushfl\n\t"      /* 保存当前进程的标志位 */   
43             "pushl %%ebp\n\t"        /* 保存当前进程的堆栈基址EBP   */ 
44             "movl %%esp,%[prev_sp]\n\t"  /* 保存当前栈顶ESP   */ 
45             "movl %[next_sp],%%esp\n\t"  /* 把下一个进程的栈顶放到esp寄存器中,完成了内核堆栈的切换,从此往下压栈都是在next进程的内核堆栈中。   */ 
 
46             "movl $1f,%[prev_ip]\n\t"    /* 保存当前进程的EIP   */ 
47             "pushl %[next_ip]\n\t"   /* 把下一个进程的起点EIP压入堆栈   */    
48             __switch_canary                   
49             "jmp __switch_to\n"  /* 因为是函数所以是jmp,通过寄存器传递参数,寄存器是prev-a,next-d,当函数执行结束ret时因为没有压栈当前eip,所以需要使用之前压栈的eip,就是pop出next_ip。  */ 
 
以上四行代码实际是使用next进程的进程堆栈,但是还算成prev的进程执行,内核堆栈的切换和进程切换完成并不同时间。
 
50             "1:\t"               /* 认为next进程开始执行。 */         
51             "popl %%ebp\n\t"     /* restore EBP   */    
52             "popfl\n"         /* restore flags */  
53                                    
54             /* output parameters 因为处于中断上下文,在内核中
                prev_sp是内核堆栈栈顶
                prev_ip是当前进程的eip */                
55             : [prev_sp] "=m" (prev->thread.sp),     
56               [prev_ip] "=m" (prev->thread.ip),  //[prev_ip]是标号        
57               "=a" (last),                 
58                                    
59               /* clobbered output registers: */     
60               "=b" (ebx), "=c" (ecx), "=d" (edx),      
61               "=S" (esi), "=D" (edi)             
62                                         
63               __switch_canary_oparam                
64                                    
65               /* input parameters: 
                next_sp下一个进程的内核堆栈的栈顶
                next_ip下一个进程执行的起点,一般是$1f,对于新创建的子进程是ret_from_fork*/                
66             : [next_sp]  "m" (next->thread.sp),        
67               [next_ip]  "m" (next->thread.ip),       
68                                         
69               /* regparm parameters for __switch_to(): */  
70               [prev]     "a" (prev),              
71               [next]     "d" (next)               
72                                    
73               __switch_canary_iparam                
74                                    
75             : /* reloaded segment registers */           
76            "memory");                  
77  } while (0)

三、实验总结

       在本次实验中,我学习了 Linux 内核中的进程调度机制和进程切换过程。首先,我通过查找内核代码中的 schedule() 函数,了解了进程调度的时机以及调用该函数的上下文。这使我对进程调度的基本原理有了更清晰的认识。

      接着,我使用 gdb 工具跟踪分析了 schedule() 函数,通过设置断点和单步执行代码,深入理解了进程切换的细节。特别关注 switch_to 函数中的汇编代码,我逐步理解了进程上下文的切换机制,以及与中断上下文切换的关系。这个过程中,我对内核中的数据结构和函数调用有了更深入的了解。

    总的来说,通过这次实验,我不仅学到了关于 Linux 内核中进程调度的理论知识,还通过实际的调试过程深入理解了其实现细节。这为我在操作系统领域的深入学习提供了坚实的基础。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐