实验目的
1、理解 Linux 系统中进程调度的时机,可以在内核代码中搜索 schedule()函数,看都是哪里调用了 schedule(),判断我们课程内容中的总结是否准确;
2、使用 gdb 跟踪分析一个 schedule()函数 ,验证您对 Linux 系统进程调度与进程切换过程的理解;推荐在实验楼 Linux 虚拟机环境下完成实验。
3、特别关注并仔细分析 switch_to 中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系;
一、知识总结
(1)进程调度的时机:中断处理过程直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()。
内核线程是一个特殊的进程,只有内核态没有用户态,可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度(内核线程可以直接访问内核函数,所以不会发生系统调用)。内核线程作为一类的特殊的进程可以主动调度,也可以被动调度。
用户态进程无法实现主动调度,仅能在中断处理过程中进行调度(schedule是一个内核函数,不是一个系统调用)。
(2)挂起正在CPU上执行的进程,与中断时保存现场不同。中断前后是在同一个进程上下文中,只是由用户态转向内核态执行。进程上下文包含了进程执行需要的所有信息:用户地址空间:包括程序代码,数据,用户堆栈等
控制信息:进程描述符,内核堆栈等。
硬件上下文
(3)schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,context_switch中的一个关键宏switch_to来进行关键上下文切换。
(4)0到3G用户可以访问,3G以上只有内核态可以访问。所有进程3G以上都是完全共享的,比如进程X切换到进程Y,但是地址空间仍然是3G以上的部分,只是把进程描述符和其他的进程上下文切换了,只有在返回的时候才不同。哪一个进程都可以“招手”进入内核态,走了一段以后便可以返回到用户态,空车的时候就进入idle进程空转。
二、实验步骤
1.删除并更新menuOS
代码如下:
cd LinuxKernel
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs
连接可能不稳定,多试几次
2.挂起qemu准备之后进行gdb调试
代码如下:
cd ..
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -S -s
3.另开一个终端,进行gdb追踪
cd LinuxKernel
gdb
file linux-3.18.6/vmlinux
target remote:1234
设置断点:
b schedule
b context_switch
b switch_to
b pick_next_task
下图可看到switch_to为宏定义,不能设置断点
开始运行
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)
linux调度的核心函数为schedule,schedule函数封装了内核调度的框架。细节实现上调用具体的调度类中的函数实现。schedule函数主要流程为:将当前进程从相应的运行队列中删除,计算和更新调度实体和进程的相关调度信息;将当前进重新插入到调度运行队列中,根据具体的运行时间进行插入而对于实时调度插入到对应优先级队列的队尾,从运行队列中选择运行的下一个进程,进程调度信息和上下文切换。当进程上下文切换后,调度就基本上完成了,当前运行的进程就是切换过来的进程了。
总结
通过这次实验,我认识到schedule函数的作用非常重要,是进程调度的主题函数。其中pick_next_task函数是schedule函数中重要的函数,负责根据调度策略和调度算法选择下一个进程。context_switch函数是schedule函数中实现进程切换的函数。switch_to是context_switch函数中进行进程关键上下文切换的函数。
中断处理过程直接调用schedule(),或者当内核返回用户态时根据need_resched标志调用schedule()。
内核线程是一个特殊的进程,只有内核态没有用户态,可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度(内核线程可以直接访问内核函数,所以不会发生系统调用)。
内核线程作为一类的特殊的进程可以主动调用schedule函数让出CPU,也可以被动调度。
用户态进程无法实现主动调度,仅能在中断处理过程中进行调度。