2023-2024-1 20232827 田紫晴《Linux内核原理与分析》第九周作业
理解进程调度时机跟踪分析进程调度与进程切换的过程
- 理解 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 内核中进程调度的理论知识,还通过实际的调试过程深入理解了其实现细节。这为我在操作系统领域的深入学习提供了坚实的基础。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐






所有评论(0)