代码改变世界

理解进程调度时机跟踪分析进程调度与进程切换的过程

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利用了prevnext两个参数: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"//4445两行代码完成了内核堆栈的切换

//之后所有的压栈动作都是在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自动完成的

3SAVE_ALL //保存现场

4)中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换//在中断处理过程中总有一个发生调度的时机,有可能会发生调度

5)标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行,即这个next进程曾经做过prev)//这里是用户态进程Y的内核部分

(6)restore_all//恢复现场

7iret - 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)在32x86的系统下,一个进程的地址空间有4G03G是用户态的,3G以上的部分仅仅是内核态可以访问的。

2)对所有的进程来说,3G以上的部分是完全共享的。

3)陷入内核态之后,进程X切换到了进程Y,但是地址空间还是在3G以上的部分;只是把进程标识符、进程上下文等切换了;

4)回到用户态的时候才会有不同,在内核态中代码段和内核堆栈等等是完全相同的。

5)内核实际上也可以比喻成出租车,哪个进程招手都可以陷入进内核态,做一些工作之后再返回到用户态;接下来发生中断又可以进入内核态;

6)当出租车没有客人的时候,就进入CPU idle0号进程)。

小结:内核是各种中断处理过程和内核线程的集合。

五、最后来个大总结

CPU的角度看Linux系统的执行

 

进程的地址空间:下面是03G的部分,0xc000000以上是剩下的部分。

main函数中有一个gets(),即从控制态获取一个字符串(在shell下打了一个命令)。

 

这时系统调用gets需要陷入内核态,即从用户态的堆栈进入到内核;会把esp等信息压栈。

 

(箭头指向为进程管理)

进入系统调用等待键盘的输入;等待的过程CPU会调度到其他进程来执行;在执行其他进程的过程中,实际上同时在等待着键盘的输入;输入键盘的话会发生I/O中断,还会再调度回来。

 

进程X之后可能会idle,也可能会执行其他的进程。

 

这时在键盘上敲击了ls,就发生了一个I/O中断给CPU,然后CPU就执行中断处理程序;

中断处理程序过程中可能接收了一个键盘的输入;

进程X刚陷入内核中若没有任务执行,就会变为阻塞态,一直在等待键盘的输入;

此时出现了键盘的输入,中断处理程序就把X进程设为就绪态(wakeup progress)了。

 

中断处理过程会出现进程调度的时机,它可能会把进程X作为next进程来执行;

这时进程管理就会切换到进程X

然后gets系统调用就获得了需要从键盘中读取的数据,然后返回到用户态。

 

然后继续执行下面的指令;堆栈再次变为用户态堆栈。