理解进程调度时机跟踪分析进程调度与进程切换的过程
一、相关知识
schedule函数
schedule() 是 linux 调度器中最重要的一个函数,就像 fork 函数一样优雅,它没有参数,没有返回值,却实现了内核中最重要的功能,当需要执行实际的调度时,直接调用 shedule(),进程就这样神奇地停止了,而另一个新的进程占据了 CPU
内核真正执行调度的时机有:
1、系统调用返回到用户空间
2、中断返回到用户空间
3、中断返回到内核空间,需要内核支持内核抢占,不过内核抢占基本上已经是目前 linux 的默认配置
4、重新使能内核抢占时
调度的处理过程
1、schedule()接口
首先需要关闭抢占,防止调度重入,然后调用__schedule,进行current相关的处理,比如有待决信号,则继续标记状态为TASK_RUNNING,或者如果需要睡眠则调用deactivate_task将从运行队列移除后加入对应的等待队列,通过pick_next_task选择下一个需要执行的进程,进行context_switch进入新进程运行。
2、pick_next_task
首先判断当前进程调度类sched_class是否为fair_sched_calss,也就是CFS,如果是且当前cpu的调度队列下所有调度实体数目与其下面所有CFS调度类的下属群组内的调度实体数目总数相同,即无RT等其他调度类中有调度实体存在(rq->nr_running == rq->cfs.h_nr_running),则直接返回当前调度类fair_sched_class的pick_next_task选择结果,否则需要遍历所有调度类for_each_class(class),返回class->pick_next_task的非空结果。
3、context_switch完成进程上下文切换
进程的抢占或者切换工作是由context_switch完成的。操作系统在进行切换进程的过程中必须记录重启进程和启动新进程使之活动所需要的所有信息。这些信息被称作上下文, 它描述了进程的现有状态
进程的上下文信息包括, 指向可执行文件的指针, 栈, 内存(数据段和堆), 进程状态, 优先级, 程序I/O的状态, 授予权限, 调度信息, 审计信息, 有关资源的信息(文件描述符和读/写指针), 关事件和信号的信息, 寄存器组(栈指针, 指令计数器)等等
linux中进程调度时, 内核在选择新进程之后进行抢占时, 通过context_switch完成进程上下文切换
二、使用 gdb 跟踪分析一个 schedule()函数
(1)更新menu
将test.c替换成test_exec.c
(2)冻结MenuOS并设置断点调节
//code in shell 1
cd ..
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -S -s
//code in shell 2
gdb
file linux-3.18.6/vmlinux
target remote:1234
break schedule
break context_switch
break pick_next_task
(3)进入gdb分布调试
schedule函数对应的代码
pick_next_task函数对应的代码
context_switch函数对应的代码
switch_to宏定义函数
#define switch_to(prev, next, last)
do {
/*
* Context-switching clobbers all registers, so we clobber
* them explicitly, via unused output variables.
* (EAX and EBP is not listed because EBP is saved/restored
* explicitly for wchan access and EAX is the return value of
* __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)
总结
经过这次实验深刻的了解了进程调度、上下文切换的过程以及背后的原理,通过schedule函数来实现进程的调度,换句话说调用schedule函数一次就是进程调度一次。pick_next_task是负责根据调度策略和调度算法选择下一个进程,context_switch函数schedule函数中实现进程切换的函数。switch_to是context_switch函数进行进程关键上下文切换的函数。