理解进程调度时机跟踪分析进程调度与进程切换的过程
2016-04-12 10:29 20135128 阅读(455) 评论(0) 编辑 收藏 举报理解进程调度时机跟踪分析进程调度与进程切换的过程
符钰婧 原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、首先来理解一下Linux系统中进程调度的时机
(1)中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();//对于用户态进程 【注:用户态进程只能被动调度。】
(2)内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程(时钟中断、I/O中断等)中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;//内核线程是只有内核态没有用户态的特殊进程
(3)用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。
schedule()是一个内核函数,不是系统调用,所以不能直接调用。
在内核的任何一个位置都能调用schedule():
二、接下来使用gdb跟踪分析一个schedule()函数
2870行处为_schedule(),其中包含一个关键的信息:
next = pick_next_task(rq, prev);//进程调度算法都封装这个函数内部,是一个调度策略
context_switch(rq, prev, next);//进程上下文切换
三、分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系
switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度的进程
注:进程切换的时候堆栈要切换,eip的位置要切换,还有其他的一些寄存器也需要切换
------switch_to的汇编代码-------
42 asm volatile("pushfl\n\t"
43 "pushl %�p\n\t"
44 "movl %%esp,%[prev_sp]\n\t"
//%[]为用字符串代替%0来标记参数,可读性更强
45 "movl %[next_sp],%%esp\n\t"//44和45两行代码完成了内核堆栈的切换
//之后所有的压栈动作都是在next进程的内核堆栈中进行
46 "movl $1f,%[prev_ip]\n\t"
47 "pushl %[next_ip]\n\t"
48 __switch_canary \
49 "jmp __switch_to\n"
//当函数__switch_to执行结束之后,return的时候会pop %[next_ip],其实就是$1f
注:next ip一般是$1f,对于新创建的子进程是ret_from_fork
50 "1:\t"//就是从这个位置开始,此时next进程开始执行
//从内核堆栈的角度来看,45行时进程切换已完成;但实际的代码执行是从50行开始
51 "popl %�p\n\t"
52 "popfl\n"
53 \
54 \
55 : [prev_sp] "=m"(prev->thread.sp), //sp为内核堆栈的栈底
56 [prev_ip] "=m"(prev->thread.ip), //thread.ip为当前进程的eip
57 "=a"(last), \
58 \
59
60 "=b"(ebx), "=c"(ecx), "=d"(edx), \
61 "=S"(esi), "=D"(edi) \
62 \
63 __switch_canary_oparam \
64 \
65 \
66 : [next_sp] "m"(next->thread.sp), //sp为下一个进程的内核堆栈的栈底
67 [next_ip] "m"(next->thread.ip), //下一个进程执行的起点
68 \
69 \
70 [prev] "a"(prev), \
71 [next] "d"(next) \
72 \
四、Linux中进程调度与进程切换过程
1、一般执行过程:正在运行的用户态进程X切换到运行用户态进程Y的过程
(1)正在运行的用户态进程X
(2)发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).//把当前的CPU上下文压到当前用户态进程X的内核堆栈中,然后加载当前进程的内核堆栈的相关信息(当前中断对应的服务例程的起点、ss:esp等)。这些动作都是由CPU自动完成的
(3)SAVE_ALL //保存现场
(4)中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换//在中断处理过程中总有一个发生调度的时机,有可能会发生调度
(5)标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行,即这个next进程曾经做过prev)//这里是用户态进程Y的内核部分
(6)restore_all//恢复现场
(7)iret - pop cs:eip/ss:esp/eflags from kernel stack//pop 出Y进程在发生中断时保存到内核堆栈中的信息
(8)继续运行用户态进程Y
关键点小结:
中断和中断返回有一个CPU上下文的切换;
在进程调度的过程中有一个进程上下文的切换,这是从一个进程的内核堆栈切换到另一个进程的内核堆栈。
2、几个特殊情况
(1)通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;//内核线程没有内核态
(2)内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
(3)创建子进程的系统调用在子进程中的执行起点(ret_from_fork)及返回用户态,如fork;//如果next是一个新创建的子进程,next_ip=ret_from_fork;不是返回到标号1执行,而是直接到ret_from_fork执行
(4)加载一个新的可执行程序后返回到用户态的情况,如execve;//在execve内部会把中断上下文修改
3、内核与舞女
(1)在32位x86的系统下,一个进程的地址空间有4G;0到3G是用户态的,3G以上的部分仅仅是内核态可以访问的。
(2)对所有的进程来说,3G以上的部分是完全共享的。
(3)陷入内核态之后,进程X切换到了进程Y,但是地址空间还是在3G以上的部分;只是把进程标识符、进程上下文等切换了;
(4)回到用户态的时候才会有不同,在内核态中代码段和内核堆栈等等是完全相同的。
(5)内核实际上也可以比喻成出租车,哪个进程招手都可以陷入进内核态,做一些工作之后再返回到用户态;接下来发生中断又可以进入内核态;
(6)当出租车没有客人的时候,就进入CPU idle(0号进程)。
小结:内核是各种中断处理过程和内核线程的集合。
五、最后来个大总结
从CPU的角度看Linux系统的执行
进程的地址空间:下面是0到3G的部分,0xc000000以上是剩下的部分。
在main函数中有一个gets(),即从控制态获取一个字符串(在shell下打了一个命令)。
这时系统调用gets需要陷入内核态,即从用户态的堆栈进入到内核;会把esp等信息压栈。
(箭头指向为进程管理)
进入系统调用等待键盘的输入;等待的过程CPU会调度到其他进程来执行;在执行其他进程的过程中,实际上同时在等待着键盘的输入;输入键盘的话会发生I/O中断,还会再调度回来。
进程X之后可能会idle,也可能会执行其他的进程。
这时在键盘上敲击了ls,就发生了一个I/O中断给CPU,然后CPU就执行中断处理程序;
中断处理程序过程中可能接收了一个键盘的输入;
进程X刚陷入内核中若没有任务执行,就会变为阻塞态,一直在等待键盘的输入;
此时出现了键盘的输入,中断处理程序就把X进程设为就绪态(wakeup progress)了。
中断处理过程会出现进程调度的时机,它可能会把进程X作为next进程来执行;
这时进程管理就会切换到进程X;
然后gets系统调用就获得了需要从键盘中读取的数据,然后返回到用户态。
然后继续执行下面的指令;堆栈再次变为用户态堆栈。