20242828-《Linux内核原理与分析》第九周作业
Linux 系统中进程调度的时机
在 Linux 系统中,进程调度是由内核完成的,用于管理进程的执行顺序和资源分配。调度的时机是指内核决定从当前进程切换到另一个进程的时刻,主要包括以下几种场景:
-
时间片用尽
**触发机制:**定时器中断(clock interrupt)。
Linux 使用时间片轮转(time slicing)调度策略,每个进程被分配一定的 CPU 时间片。当时间片用尽时,内核触发调度器选择下一个合适的进程运行。
**典型场景:**抢占式多任务系统中,确保所有进程都能公平使用 CPU。 -
进程主动让出 CPU
**触发机制:**进程调用特定系统调用。
如果一个进程因某种原因需要等待(如 I/O 操作、信号或获取锁),它会主动进入睡眠(阻塞)状态。此时,内核会触发调度器选择其他进程运行。
常见系统调用:
sleep():进程主动休眠。
wait():等待子进程结束。
阻塞式 I/O 调用:如 read()、write()。 -
高优先级任务唤醒
**触发机制:**有更高优先级的进程从睡眠状态被唤醒。
如果某个优先级较高的进程从阻塞状态被唤醒,内核可能会立即中断当前的低优先级进程,切换到高优先级进程。
**典型场景:**实时任务、I/O 完成通知。 -
系统调用返回或中断处理结束
**触发机制:**进程从内核态返回用户态时。
每当进程完成系统调用或中断处理时,内核会检查是否需要进行进程切换。
典型场景:
某进程完成 I/O 操作。
中断处理完成时触发调度器。 -
新进程创建或进程终止
触发机制:
调用 fork() 或其他进程创建函数后。
某进程调用 exit() 终止后。
新进程创建后,调度器会决定是否切换到新进程执行。
如果当前运行的进程终止,调度器会选择其他进程运行。 -
总结
Linux 的进程调度是多任务操作系统的核心功能,其调度时机由内核态的各种事件驱动,主要包括时间片用尽、进程状态变化以及系统资源条件变化等。Linux 通常采用 CFS(完全公平调度器) 和特定场景下的实时调度策略来实现这些调度行为。
实验:使用 gdb 跟踪分析一个 schedule()函数
1、启动MenuOS系统
cd LinuxKernel
rm -rf menu # 无法克隆,可使用侧边栏上传代码将代码压缩包传入实验楼环境中
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs

2、配置gdb,远程调试并设置断点;
cd ..
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
gdb # 打开另一个终端,使用gdb调试
file linux-3.18.6/vmlinux
target remote:1234
b schedule
b context_switch
b switch_to
b pick_next_task


按c继续执行,停在schedule函数断点处,使用list查看函数的上下文代码,发现调用了schedule(),即发生了进程调度

按c继续执行到pick_next_task断点处

按c继续执行到context_switch断点处:context_switch用来实现进程的切换

单步调试进入switch_to内部

schedule() 是内核调度的核心函数,当当前任务需要让出 CPU 时被调用。pick_next_task()选择下一个要运行的任务。context_switch()完成从当前任务到新任务的上下文切换。switch_to()执行具体的寄存器切换操作。
当前任务调用 schedule() 请求调度。schedule() 调用 pick_next_task() 选择新的任务。如果任务不同,调用 context_switch() 完成上下文切换:保存当前任务状态。切换到新任务的内核栈。更新 current 指针。context_switch() 调用 switch_to(),最终切换寄存器并启动新任务。
分析 switch_to 中的汇编代码
汇编代码:
#define switch_to(prev, next, last)
do {
/*
1. Context-switching clobbers all registers, so we clobber
2. them explicitly, via unused output variables.
3. (EAX and EBP is not listed because EBP is saved/restored
4. explicitly for wchan access and EAX is the return value of
5. __switch_to())
*/
unsigned long ebx, ecx, edx, esi, edi;
asm volatile("pushfl\n\t" /* 保存当前进程flags */
"pushl %%ebp\n\t" /* 当前进程堆栈基址压栈*/
"movl %%esp,%[prev_sp]\n\t" /*保存ESP,将当前堆栈栈顶保存起来*/
"movl %[next_sp],%%esp\n\t" /*更新ESP,将下一栈顶保存到ESP中*/
//完成内核堆栈的切换
"movl $1f,%[prev_ip]\n\t" /*保存当前进程EIP*/
"pushl %[next_ip]\n\t" /*将next进程起点压入堆栈,即next进程的栈顶为起点*/
//完成EIP的切换
__switch_canary
//next_ip一般是$1f,对于新创建的子进程时ret_from_fork
"jmp __switch_to\n" /*prev进程中,设置next进程堆栈*/
//jmp不同于call是通过寄存器传递参数
"1:\t" //next进程开始执行
"popl %%ebp\n\t"
"popfl\n"
/*输出变量定义*/
: [prev_sp] "=m" (prev->thread.sp), //[prev_sp]定义内核堆栈栈顶
[prev_ip] "=m" (prev->thread.ip), //[prev_ip]当前进程EIP
"=a" (last),
/* 要破坏的寄存器: */
"=b" (ebx), "=c" (ecx), "=d" (edx),
"=S" (esi), "=D" (edi)
__switch_canary_oparam
/* 输入变量: */
: [next_sp] "m" (next->thread.sp), //[next_sp]下一个内核堆栈栈顶
[next_ip] "m" (next->thread.ip),
//[next_ip]下一个进程执行起点,,一般是$1f,对于新创建的子进程是ret_from_fork
/* regparm parameters for __switch_to(): */
[prev] "a" (prev),
[next] "d" (next)
__switch_canary_iparam
: /* 重新加载段寄存器 */
"memory");
} while (0)
1、 定义需要保存的寄存器变量unsigned long ebx, ecx, edx, esi, edi;这些变量用于存储上下文切换过程中需要保存的寄存器值,避免在切换时被破坏。
2、使用 asm volatile 指令定义一个内联汇编块,volatile 指定编译器不优化或重排这段代码。
3、pushfl:将当前进程的标志寄存器(EFLAGS)压入栈中,保存标志位。目的: 在切换回来时能恢复原任务的中断状态和标志位。
4、pushl %%ebp:将当前进程的 EBP(栈基址寄存器)压入栈中,保存堆栈基址。目的:便于恢复时正确调整栈基址。
5、movl %%esp, %[prev_sp]: 将当前任务的 ESP(栈指针)值保存到 prev->thread.sp。目的:保存当前任务的内核栈顶位置,供恢复时使用。
6、movl %[next_sp], %%esp:将 next->thread.sp(下一任务的栈指针)加载到 ESP。目的:完成内核栈的切换,切换到目标任务的内核栈。
7、movl $1f, %[prev_ip]:将当前任务的下一条指令地址(标号 1f)保存到 prev->thread.ip。目的:当恢复 prev 任务时,从保存的地址继续执行。
8、pushl %[next_ip]:将目标任务的指令地址(next->thread.ip)压入堆栈。目的:确保下一任务的执行从 next_ip 开始。
9、jmp switch_to:跳转到 switch_to 函数。目的:完成底层寄存器切换,包括通用寄存器和段寄存器。
10、popl %%ebp
popfl
恢复新的 EBP 和 EFLAGS 值。目的: 确保目标任务的堆栈基址和标志寄存器被正确恢复。
switch_to 的核心是保存当前任务的状态(堆栈、寄存器、程序计数器等),切换到目标任务的上下文,并从目标任务的执行点继续运行。
862

被折叠的 条评论
为什么被折叠?



