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

张家骥 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

1. 进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并回复以前挂起的某个进程的执行。这种行为被称为进程切换。

1.1 硬件上下文

尽管每个进程可以拥有自己的地址空间,但是所有进程必须共享CPU的寄存器。因此,在恢复一个进程之前,内核必须确保每个寄存器装入了挂起进程时的值。
进程恢复执行前必须装入寄存器的一组数据成为硬件上下文。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。在linux中,进程硬件上下文的一部分存放在TSS段,而剩余部分存放在内核态堆栈中。
在下面的描述中,假定用prev局部变量表示切换出的进程的描述符,用next表示切换进的进程的描述符。因此,我们把进程切换定义为这样的行为:保存prev硬件上下文,用next硬件上下文代替prev。因为进程切换经常发生,因此减少保存和装入硬件上下文所花费的时间是非常重要的。进程切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈上,这也包括ss和esp这对寄存器的内容(存储用户态堆栈指针的地址)。

1.2 thread字段

在每次进程切换时,被替换进程的硬件上下文必须保存在别处。不能像Intel原始设计那样把它保存在TSS段中,因为Linux为每个处理器而不是为每个进程使用TSS。
因此,每个进程描述符包含一个类型为thread_struct的thread字段,只要进程被切换出去,内核就把其硬件上下文保存在这个结构中。但这个结构不包括eax,ebx等通用寄存器,它们的值保留在内核堆栈中。

1.3 执行进程切换

进程切换可能只发生在精心定义的点:schedule()函数。从本质上说,每个进程的切换由两步组成:
1. 切换页全局目录以安装一个新的地址空间;
2. 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器等。
(说明:prev和next是schedule函数的局部变量。)

switch_to宏

进程切换的第二步由switch_to宏执行。它是内核中与硬件关系最密切的例程之一。
首先,该宏有三个参数,它们是prev,next和last。prev和next是局部变量prev和next的占位符,即它们是输入参数,分别表示被替换进程和新进程描述的地址在内存中的位置。
那第三个参数last呢?在任何进程切换中,涉及到三个进程而不是两个。假设决定暂停进程A而激活进程B。在schedule()函数中,prev指向A的描述符而next指向B的描述符。swtich_to宏一旦使A暂停,A的执行流就冻结。
随后,当内核想再次激活A,就必须暂停另一个进程C(通常不是B),于是就要用prev指向C,next指向A,来执行另一个switch_to宏。当A恢复它的执行流时,就会找到它原来的内核栈,于是prev局部变量恢复成了指向A,next恢复成了指向B,此时进程A执行的内核就失去了对C的任何引用(忘了是从哪里切换过来的)。事实表明,这个引用对于完成进程的切换时很有用的。
switch_to宏的最后一个参数是输出参数,它表示把进程C的描述符地址写在内存的什么位置了(当然,这是在A恢复执行之后完成的)。在进程切换之前,宏把第一个输入参数prev表示的变量的内容存入CPU的eax寄存器。在完成进程切换,A已经恢复执行时,宏把CPU的eax寄存器的内容写入由第三个输出参数——last所指示的A在内存中的位置。因为CPU寄存器不会再切换点发生变化,所以C的描述符地址也存在内存的这个位置。在schedule()执行过程中,参数last指向A的局部变量prev,所以prev被C的地址覆盖。
下图显示了进程A,B,C内核堆栈的内容以及eax寄存器的内容。必须注意的是:图中显示的是在被eax寄存器的内容覆盖以前的prev局部变量的值。
这里写图片描述
由于switch_to宏采用扩展的内联汇编语言编码,可读性差,所以接下来用标准汇编语言来描述switch_to宏在x86处理器上所完成的典型工作。
1. 在eax和edx寄存器中分别保存prev和next的值。

movl prev,%eax
movl next,%edx

2 把eflags和ebp寄存器的内容保存在prev内核栈中。必须保存它们的原因是编译器认为在switch_to结束之前它们的值应当保持不变。

pushfl
pushl %ebp

3 把esp的内容保存到prev->thread.esp中以使该字段指向prev内核栈的栈顶:

movl %esp,484(%eax)

4 把next->thread.esp装入esp。此时,内核开始在next的内核栈上操作,因此这条指令完成了从prev到next的切换。由于进程描述符的地址和内核栈的地址紧挨着,所以改变内核栈意味着改变当前进程。

movl 484(%eax),%esp

5 把标记为1的地址存入prev->thread.eip。当被替换的进程重新恢复执行时,进程执行被标记为1的那条指令:

movl $1f, 480(%eax)

6 宏把next->thread.eip的值(绝大多数情况是一个被标记为1的地址)压入next的内核栈:

pushl 480(%eax)

7 跳到__switch_to()函数:

jmp __switch_to

8 这里被进程B替换的进程A再次获得CPU:它执行一些恢复eflags和ebp寄存器内容的指令,这两条指令的第一条指令被标记为1。

1:
    popl %ebp
    popfl

注意这些pop指令时怎样引用prev进程的内核栈的。当进程调度程序选择了prev作为新进程在CPU运行时,将执行这些指令。于是,以prev作为第二个参数调用switch_to。因此,esp寄存器指向prev的内核栈。
9 拷贝eax寄存器的内容到switch_to宏的第三个参数last标识的内存区域中:

movl %eax, last

正如先前讨论的,eax寄存器指向刚被替换的进程的描述符。

2. 使用gdb跟踪分析一个schedule()函数

2.1 实验过程

下载最新的menu系统。
这里写图片描述
重新编译生成根文件系统,然后使用-s -S启动调试内核。(具体参加前几次作业)
这里写图片描述
启动gdb,调试menu,在schedule处设置断点1。在context_switch处设置断点2。然后运行。可以看到,首先停在了断点1:schedule处。
这里写图片描述
按n,单步执行,发现停在了断点2:context_switch处。
这里写图片描述
继续按n单步执行:
这里写图片描述
这里写图片描述
之后又回到断点2:context_switch处。

2.2 实验分析

schedule()选择一个新的进程来运行,并调用context_switch进行上下文的切换,context_switch调用switch_to宏来进行关键上下文的切换。

next=pick_next_task(rq,prev);//进程调度算法被封装在内
context_switch(rq,prev,next);//进程上下文的切换
jmp 函数名//使用寄存器来传递参数,而不是压栈

switch_to利用了prev和next两个参数:prev指向当前进程,next指向被调度进程。

进程调度时机:

1.中断处理过程,直接调用schedule()或根据need_reached标记调用schedule();(用户态进程被动被调度)
2. 内核线程可以直接调用schedule()进行切换,也可以在中断过程中进行调度。

进程上下文:

  1. 用户地址空间:程序代码,数据,用户态堆栈等。
  2. 控制信息:进程描述符,内核堆栈等。
  3. 硬件上下文(中断也要保存硬件上下文,只是方法不同)

3. 对“Linux系统一般执行过程”的理解

最一般的情况:正在运行的用户态进程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).
  3. SAVE_ALL //保存现场
  4. 中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
  5. 标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
  6. restore_all //恢复现场
  7. iret - pop cs:eip/ss:esp/eflags from kernel stack
  8. 继续运行用户态进程Y

Linux系统执行过程中的几种特殊情况

  1. 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
  2. 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
  3. 创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;
  4. 加载一个新的可执行程序后返回到用户态的情况,如execve;

参考资料:深入理解Linux内核(中文第三版)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值