Ⅰ.理解进程调度时机跟踪分析进程调度与进程切换的过程
理解 Linux 系统中进程调度的时机,可以在内核代码中搜索 schedule()函数,看都是哪里调用了 schedule(),判断我们课程内容中的总结是否准确;
使用 gdb 跟踪分析一个 schedule()函数 ,验证您对 Linux 系统进程调度与进程切换过程的理解;
特别关注并仔细分析 switch_to 中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系。
Linux进程调度时机主要有:
1、进程状态转换的时刻:进程终止、进程睡眠;
2、当前进程的时间片用完时(current->counter=0);
3、设备驱动程序
4、进程从中断、异常及系统调用返回到用户态时;
时机1,进程要调用sleep()或exit()等函数进行状态转换,这些函数会主动调用调度程序进行进程调度;
时机2,由于进程的时间片是由时钟中断来更新的,因此,这种情况和时机4是一样的。
时机3,当设备驱动程序执行长而重复的任务时,直接调用调度程序。在每次反复循环中,驱动程序都检查need_resched的值,如果必要,则调用调度程序schedule()主动放弃CPU。
时机4,如前所述,不管是从中断、异常还是系统调用返回,最终都调用ret_from_sys_call(),由这个函数进行调度标志的检测,如果必要,则调用调用调度程序。那么,为什么从系统调用返回时要调用调度程序呢?这当然是从效率考虑。从系统调用返回意味着要离开内核态而返回到用户态,而状态的转换要花费一定的时间,因此,在返回到用户态前,系统把在内核态该处理的事全部做完。
每个时钟中断(timer interrupt)发生时,由三个函数协同工作,共同完成进程的选择和切换,它们是:schedule()、do_timer()及ret_form_sys_call()。我们先来解释一下这三个函数:
schedule():进程调度函数,由它来完成进程的选择(调度);
do_timer():暂且称之为时钟函数,该函数在时钟中断服务程序中被调用,是时钟中断服务程序的主要组成部分,该函数被调用的频率就是时钟中断的频率即每秒钟100次(简称100赫兹或100Hz);
ret_from_sys_call():系统调用返回函数。当一个系统调用或中断完成时,该函数被调用,用于处理一些收尾工作,例如信号处理、核心任务等等。
在内核代码中搜索 schedule()函数:
结合ChatGPT了解:
Ⅱ.实验
和前面的实验过程相同,先搭建好基础环境和设置:
cd LinuxeKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs
冻结内核:
cd LinuxKernel
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S //冻结内核的启动
进入新的Terminal,开启gdb调试,建立链接:
cd LinuxKernel
gdb
(gdb)file linux-3.18.6/vmlinux
(gdb)target remote:1234
分别在schedule(进程调度的主体函数)context_switch(实现进程切换的函数)pick_next_task(负责根据调度策略和调度算法选择下一个进程)三处设置断点。
在switch_to设置断点时失败,因为在 Linux 内核中,switch_to
是一个用于进行进程切换的宏。它通常用于将控制权从一个进程切换到另一个进程。不能设置断点。
接下来c运行:
switch_to的汇编代码:
// 定义 switch_to 宏,用于在内核中进行进程切换
#define switch_to(prev, next, last) \
do { \
asm volatile ( \
// 保存当前进程的寄存器状态,包括 esi、edi、ebp 寄存器
"pushl %%esi\n\t" \
"pushl %%edi\n\t" \
"pushl %%ebp\n\t" \
// 将当前栈指针 esp 的值保存到 prev->thread.esp 中
"movl %%esp,%0\n\t" \
// 将 next 进程的栈指针 esp 的值加载到 esp 寄存器
"movl %3,%%esp\n\t" \
// 将标签 1f 的地址保存到 %1 中
"movl $1f,%1\n\t" \
// 将 next 进程的指令指针 eip 的值推送到栈上
"pushl %4\n\t" \
// 跳转到 __switch_to 标签,开始进行实际的上下文切换
"jmp __switch_to\n" \
// 标签 1,用于在切换完成后返回到这里
"1:\t" \
// 恢复先前保存的寄存器状态,包括 ebp、edi、esi
"popl %%ebp\n\t" \
"popl %%edi\n\t" \
"popl %%esi" \
// 输出部分,指定了宏的输出操作数
: "=m" (prev->thread.esp), "=m" (prev->thread.eip) \
// 输入部分,指定了宏的输入操作数
: "m" (prev->thread.ebp), "m" (next->thread.esp), "m" (next->thread.eip) \
// clobber 部分,指定了汇编代码可能修改的寄存器
: "memory"); \
} while (0)
这个宏使用了内联汇编 (asm volatile
) 来执行一些汇编指令,完成从当前进程到下一个进程的上下文切换。
Ⅲ.总结
Linux进程调度基于分时和优先级,包括用户态和内核态的进程。内核线程是只有内核态的特殊进程,代表中断处理和其他内核任务。整体执行过程可描述为从运行的用户态进程X切换到用户态进程Y。内核线程可以主动调度而无需中断上下文切换,通过schedule()
函数和context_switch
宏实现上下文切换,其中switch_to
用于关键上下文切换。这一结构允许灵活而高效的进程管理和调度。