本文的写作过程主要参考《Linux内核源代码情景分析》的内容,结合自己的理解,重点讨论Linux内核如何进行过程转换。参考的代码是linux-2.4.16,与书中使用的代码版本略有不同,但不影响对问题的理解。

我们在考虑流程调度问题时会涉及以下三个问题。

调度的时机:什么情况,什么时候调度;调度的战略:选择要运行的下一个进程的标准:调度方法:对正在运行的进程是否采取“强制剥夺”或“自愿返回”执行权。第一,调度时间。

自愿的日程可以随时进行。在用户空间中,可以通过系统调用属于应用层库函数的pause()或sleep函数来实现此目的。这个函数的基础最终是通过系统调用nanosleep()来实现的。在内核空间中,可以通过schedule()启动一次调度。当然,在开始之前,您可以将程序的状态设定为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE,暂时放弃执行并睡觉。

另外,日程可能是非自愿的。也就是说,强制进程(系统调用、中断或异常处理)即将返回到用户空间。也就是说,当CPU在内核上运行时,不考虑强制调度的可能性,系统空间中发生的中断或异常也是可能的,但这不会导致调度。主要是早期内核依赖这个原则来简化设计和实现。

注:用户进程从内核空间返回到用户空间只是调度发生的必要条件,不是充分条件。可以参考Arch/i386/kernel中的以下代码:

系统调用返回处理宏

仅当当前进程的task_struct结构中的need_resched字段不为零时,才转到reschedule并调用schedule()。只有内核需要唤醒进程时,才会设置此字段。

二、调度方法

Linux内核的调度方式可以说是“有条件的可剥夺”。进程在用户空间中运行时,无论是否自愿(例如,运行时间足够长),内核都可以暂时剥夺进程的运行。但是,进入内核空间后,将进入超级管理程序模式。我知道内核需要调度,但在进程返回用户空间之前,这种情况实际上不会发生。

三、调度策略

调度策略是基于优先级的调度,内核计算反映每个进程的执行“权利”的权重,然后选择权重最高的进程来执行。在运行过程中,流程的资格会随着时间的推移而减少,下一个时间表中原始资格较低的可能会优先运行。

但是,为了满足多种应用程序的需求,实施了三种战略:SCHED_FIFO、SCHED_RR和SCHED_OTHER。每个进程都有自己的策略,可以通过系统调用sched_setscheduler()来设置自己的调度策略。

其中SCHED_FIFO适用于时间要求相对较强,但每次运行所需时间相对较短的进程。SCHED_RR是轮询策略,非常适合于相对较大的进程,即每次运行需要很长时间的进程。此外,SCHED_OTHER是适用于交互式分时应用程序的现有策略。

四、深入明细表函数

1、分析此函数之前,必须了解task_struct的宏定义,该定义从下一个内核获取当前进程。

获取当前的task_struct

在内核空间中,进程描述符存储在8k大小的进程堆栈顶部(低地址,英特尔系统的堆栈从高地址扩展到低地址),而esp寄存器保留当前进程的堆栈基础地址,因此,将esp上指针的低13位(8k)清空为0,就可以获得当前进程描述符的地址。

2.进程的虚拟地址空间分为两部分:用户状态程序可以访问的虚拟地址空间和内核可以访问的内核空间。每当内核执行上下文切换时,虚拟地址空间的用户层部分都会切换,使当前运行的进程相匹配,内核空间不会切换。Task_struct流程描述符包含与流程地址空间相关的两个字段mm:active _ mm。对于最终用户进程,mm是指虚拟地址空间的用户空间部分;对于内核线程,mm是NULL。

这为遵循所谓的惰性TLB处理(lazy TLB handing)进行优化提供了余地。Active_mm主要用于优化。内核线程与特定的用户层进程无关,因此内核不需要切换虚拟地址空间的用户层部分,因此不需要保留以前的设置。内核线程以前可能正在运行所有用户层进程,因此用户空间部分的内容本质上是随机的,内核线程不应修改内容,因此如果将mm设置为NULL并切换到用户进程,内核会将原始进程的mm存储在新的内核线程中。

的active_mm中,因为某些时候内核必须知道用户空间当前包含了什么。

3、这就是刚开始环境检查中,active_mm不能为空的原因,否则,报出异常。

#define BUG() __asm__ __volatile__(".byte 0x0f,0x0b")

异常的产生是让CPU去执行两个字节的非法指令,这会产生一次“invalid_op”异常,使得CPU捕获并执行do_invalid_op()。

4、继续往下看,这里的counter表示当前进程的运行时间配额,其数值在每次时钟中断的时候都要递减,这一操作是在update_process_times()中进行的。当该值递减到0,就要从runqueue队列中移动到末尾,同时恢复最初的时间配额。

到期移动到队尾

5、当前进程就是正在运行的进程,可是当进入到schedule()时其状态不一定是TASK_RUNNING,比如当前进程在do_exit()中将状态改成TASK_ZOMBLE,又比如当前进程在sys_wait4()中设置为TASK_INTERRUPTIBLE。可以理解当前进程的意愿不是继续运行,则需要从队列中撤下来。

进程撤出运行等待队列

这里也可以看出,可中断进程和不可中断进程在睡眠状态的区别,前者有信号等待处理的时候,还是运行状态,要让其处理完这些信号后再说,而后者则不受信号的影响。

6、这时,就要挑选最佳的候选进程了,c是这个进程的综合权值。挑选从idle进程即0号进程开始,然后遍历runqueue队列,通过goodness计算每个进程的权值,和最高的c进行比较,至于选择的计算规则这里就不深入去看了。

选择下个进程

从这里我们可以理解为什么当系统任务较少,负载较低的时候,基本上都是运行的idle进程,也就是top命令中idle指数偏高的原因。


7、到这里,略过其他的一些细节,实际上只剩下两件事情,其一是对用户虚拟空间的处理,其二就是进程的切换switch_to()。

a、先来看对用户空间的处理,这里补充了两个变量mm和oldmm,内核的设计和实现不允许active_mm指针为0,因为执行页面映射的目录指针就在这个数据结构中。所以, 如果为0,就需要在进入运行时想被切换出去的进程借用一个mm_struct,可行的原因是所需的只是系统空间的映射,而所有进程的系统空间的映射都是相同的。在下次调度其他进程运行的时候,这个内核线程被切换出去的时候归还,这里的mmdrop()只是将共享计数减一,而不是真正的释放。

进行切换的最后处理

由于新的进程有自己的用户空间,所以就要通过switch_mm()进行用户空间的切换。这个函数的关键语句就一行,它将进程的页面起始物理地址装入到CR3寄存器中。

asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));
b、现在就是最后的切换进程的处理了,由宏操作switch_to()完成, 代码在include/asm-i386中:

进程切换操作

这个汇编还是有点难理解的,我们梳理一下:

方向

标号

寄存器

结合值

Out

%0

m

prev->

Out

%1

m

prev->

Out

%2

ebx

last

In

%3

m

next->

In

%4

m

next->

In

%5

eax

prev

In

%6

edx

next

In

%7

ebx

prev

先来看开头三条push指令和结尾的三条pop指令,看起来很一般,却暗藏玄机。且看第20行,先将prev的esp保存,第21行将next的esp置入ESP寄存器,这两条完成了栈的切换。也就是说,从21行开始当前进程已经是next了,为什么呢,我们获取task_struct在文章刚开始讲过了,是宏定义从当前的esp中计算得出的, 此时引用current宏已经是next的
task_struct数据了。

但是要执行起来,还需要指令寄存器的支持,第22行就是将当前进程下次的执行点给保存,这个点就是讲事先当前入栈的数据恢复出来;然后第23行将next的eip给置位,就可以通过jmp调用__switch_to(), 这里为什么不使用call指令,因为call调用会产生压栈破坏了当前设置的寄存器,直接跳转过去,最后返回的时候就是刚刚压栈的next的eip,也就是标号“1f”所在的地址。

如果读者不是十分清楚这个过程,最好自己画一下堆栈的变化,注意,这里有两个堆栈,在这个过程中,有一个时期esp和ebp并不在同一个堆栈上,要格外注意这个时期里所有涉及堆栈的操作分别是在哪个堆栈上进行的。记住一个简单的原则即可,pop/push这样的操作,都是对esp所指向的堆栈进行的,这些操作同时也会改变esp本身,除此之外,其它关于变量的引用,都是对ebp所指向的堆栈进行的。
8、__switch_to的逻辑

这里的主要是TSS, 核心是将TSS中的内核空间(0级)堆栈指针换成next->esp()。

其次是将段寄存器的fs和gs的内容也做了相应的切换。至于CPU为debug而设置的一些寄存器,以及说明I/O操作权限的位图,暂时不是这篇文章关心的了。

总之,新旧进程的交接点就在switch_to()这段代码中。switch_to只是个普通的宏,但是却能实现进程的切换,很多人对此比较费解。为了正确的理解,大家需要注意:这些代码是所有进程共用的,代码本身不属于某一个特定的进程,所以判定当前在哪一个进程不是通过看执行的代码是哪个进程的,而是通过esp指向哪个进程的堆栈来判定的。所以,对于上面图中的切换点也可以这样理解,在这一点处,esp指向了其它进程的堆栈,当前进程即被挂起,等待若干时间,当esp指针再次指回这个进程的堆栈时,这个进程又重新开始运行。

1.《【重新随机进程】源代码分析Linux内核进程切换》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《【重新随机进程】源代码分析Linux内核进程切换》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/gl/2557931.html