一、实验内容
- 理解 Linux 系统中进程调度的时机,可以在内核代码中搜索 schedule()函数,看都是哪里调用了 schedule(),判断我们课程内容中的总结是否准确;
- 使用 gdb 跟踪分析一个 schedule()函数 ,验证您对 Linux 系统进程调度与进程切换过程的理解;推荐在实验楼 Linux 虚拟机环境下完成实验。
- 特别关注并仔细分析 switch_to 中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系;
二 、实验过程
- 和之前的实验过程相同,先以Stopped的方式启动内核,运行代码如下:
cd LinuxeKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv 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)file linux-3.18.6/vmlinux
(gdb)target remote:1234 //将GDB连接到正在运行的远程程序
b schedule
b context_switch
b switch_to
b pick_next_task
- 使用c命令进行调试,跟踪分析schedule函数
三、内容理解
- 理解 Linux 系统中进程调度的时机,看都是哪里调用了 schedule()
- 主动调用:当进程调用某些会导致阻塞的系统调用(如 sleep()、wait() 等)或明确调用 sched_yield() 时,内核会通过 schedule() 尝试选择其他可运行的进程。
- 被动让出:(1)进程时间片耗尽:当当前进程的时间片用尽时,调度器会被触发,检查是否需要切换到其他优先级更高的进程。(2)优先级变更:如果有一个高优先级的进程被唤醒,可能触发抢占调度,调用 schedule()。
- 进程阻塞:当进程因等待资源(如 I/O 或锁)而进入阻塞状态时,会通过调度器让出 CPU。
- 高优先级进程唤醒:如果一个高优先级进程被唤醒,可能触发抢占式调度,调用 schedule()。
- 中断处理后:在中断(如定时器中断)处理结束后,可能触发调度器检查是否需要切换到其他进程。
- 关于switch_to函数
由于switch_to不是函数,无法设置断点,我们进入内核源码查看context_switch()函数内部,源码如下:
asm volatile(
"pushfl\n\t" /* 保存当前进程的 EFLAGS 寄存器内容到栈中,保留当前的标志状态 */
"pushl %%ebp\n\t" /* 将当前进程的基址指针 (ebp) 压入栈,保存当前栈帧的基址 */
"movl %%esp,%[prev_sp]\n\t" /* 将当前栈指针 (esp) 保存到 prev->thread.sp,记录当前进程的栈顶 */
"movl %[next_sp],%%esp\n\t" /* 将目标进程的栈指针 (next->thread.sp) 赋值给 esp,切换到目标进程的栈 */
"movl $1f,%[prev_ip]\n\t" /* 将下一条指令的地址 (1: 标签对应的位置) 保存到 prev->thread.ip */
"pushl %[next_ip]\n\t" /* 将目标进程的下一条指令地址 (next->thread.ip) 压入栈,准备执行目标进程 */
"jmp __switch_to\n" /* 跳转到内核的 __switch_to 函数,完成进一步的上下文切换 */
"1:\t" /* 标签 1,标识当前代码的位置,供返回时使用 */
"popl %%ebp\n\t" /* 恢复目标进程的基址指针 (ebp),还原目标进程的栈帧基址 */
"popfl\n" /* 恢复目标进程的 EFLAGS 状态,完成标志寄存器的切换 */
/* output parameters: 定义输出的变量及其对应的存储位置 */
: [prev_sp] "=m" (prev->thread.sp), /* 将当前进程的栈指针保存到 prev->thread.sp */
[prev_ip] "=m" (prev->thread.ip), /* 将当前进程的下一条指令地址保存到 prev->thread.ip */
"=a" (last), /* 将最后一个运行的任务地址存储到 eax 寄存器 */
/* clobbered output registers: 声明会被覆盖的寄存器 */
"=b" (ebx), "=c" (ecx), "=d" (edx), /* ebx, ecx, edx 被汇编代码修改 */
"=S" (esi), "=D" (edi) /* esi, edi 被汇编代码修改 */
/* input parameters: 定义输入的变量及其来源 */
: [next_sp] "m" (next->thread.sp), /* 提供目标进程的栈指针 (next->thread.sp) */
[next_ip] "m" (next->thread.ip), /* 提供目标进程的下一条指令地址 (next->thread.ip) */
[prev] "a" (prev), /* eax 寄存器中保存当前进程的任务描述符地址 */
[next] "d" (next) /* edx 寄存器中保存目标进程的任务描述符地址 */
/* clobbered segment registers: 声明可能被修改的内存 */
: "memory"
);
四、实验总结
在操作系统中,进程切换和中断处理是核心功能。schedule()
函数负责调度,它通过调用pick_next_task()
函数来执行进程调度算法,从而选择下一个即将运行的进程。随后,schedule()
会利用context_switch()
函数来完成进程的上下文切换,确保进程能够顺利交接控制权。
在context_switch()
函数的实现中,switch_to()
宏扮演着至关重要的角色。它负责保存当前进程的上下文信息至其栈中,并从目标进程的栈中恢复上下文,这涉及到处理器寄存器、堆栈指针和程序计数器等关键状态的保存与恢复。
switch_to()
的实现细节会根据不同的硬件架构和编译选项进行调整。它通常需要汇编语言来精确控制上下文切换的具体操作,包括寄存器状态的保存和恢复。通过传入的参数,switch_to()
将控制权转移到目标进程的堆栈,并从该堆栈中恢复进程的上下文,使得新选择的进程能够继续执行。
综上,schedule()
、pick_next_task()
、context_switch()
以及switch_to()
共同协作,实现了进程的调度和上下文的保存与恢复。这些组件在操作系统内核中发挥着至关重要的作用,它们确保了系统能够高效地管理CPU资源,并实现进程间的平滑切换。
“20242817李臻 原创作品转载请注明出处 《Linux内核分析》《Linux内核分析》MOOC课程Linux内核分析MOOC课程”