5.3.3 创建进程1
在内核初始化的最后,将调用kernel_thread创建进程1,代码如下:
根据kernel_thread代码可见,进程1是通过复制进程0而来的。在复制了进程后,将执行kernel_init,相关代码如下:
根据代码可见,我们已经看清楚了,第一个进程的创建过程与我们在用户空间创建一个进程并无本质区别,就是我们惯用的套路:fork+exec。
创建进程1后,内核调用函数sechedule让进程1投入运行。在讨论进程1的投入运行前,我们先来了解一下内核的基本调度原理,如图5-20所示。
图 5-20 内核调度机制示意图
内核采用模块化的方法,将任务分成四类,优先级从高到低分别是停止类(stop_sched_class)、实时类(rt_sched_class)、公平类(fair_sched_class)和空闲类(idle_sched_class)。从名字我们就可以判断出了这几个类中归属的任务类型了。实时类中记录的是实时任务,一般的任务都归类在公平类中,而停止类和空闲类中记录的是两个特殊的任务。
在没有其他任务就绪时,CPU将运行空闲类中的任务,该任务将CPU置于停机状态,直到有中断将其唤醒。而停止类中的任务是用于负载均衡或者进行CPU热插拔时使用的任务,顾名思义,其目的是为了停止正在运行的CPU,以进行任务迁移或者插拔CPU。每个CPU分别只有一个停止任务和空闲任务。
实时类和公平类分别有一个就绪队列rt_rq和cfs_rq,维护着可以投入运行的任务。每个就绪队列有自己的排队算法,比如公平类采用红黑树对就绪的任务进行排队。
这几个类组成了一个链表,其中最高优先级的停止类作为表头。每个CPU有一个就绪队列(run queue),通过该队列,可以访问实时队列、公平队列以及停止任务和空闲任务。
每当调度发生时,调度函数schedule调用函数pick_next_task按照优先级依次遍历各个类,找出下一个投入运行的任务,代码如下:
先看函数pick_next_task中后面的for循环,显然这是在遍历调度类。pick_next_task从优先级最高的停止类开始查找,每个类提供了各自的函数pick_next_task,从就绪队列中选择需要投入运行的任务。
除非用在特定的领域,否则大部分任务应该属于公平类,所以内核开发人员对调度算法进行了一个小小的优化:如果目前系统就绪的任务都属于公平类,则直接从公平类中挑选下一个任务。这就是for循环前面的代码片段的作用。
那么进程0和进程1分别都是属于哪个调度类呢?看下面的代码:
在内核初始化时,在调度相关的初始化函数sched_init中,进程0的调度类被设置为公平类,因此,在从进程0复制后,进程1也是公平类。而在复制完成进程1后,内核将进程0的调度类设置为空闲类,代码如下:
在调用init_idle_bootup_task设置了进程0的调度类后,内核调用函数schedule_preempt_disabled进行调度,代码如下:
作为公平类中的进程1显然要排在属于空闲类的进程0的前面,因此,在这次调度后,进程1将被调度函数选中,作为下一个投入运行的任务。而当系统没有其他就绪任务时,将返回到函数schedule_preempt_disabled中,继续执行进入函数cpu_idle。cpu_idle就是一个无限的循环,循环主体就是CPU停机,等待下一次被唤醒执行任务。可见,进程0的主体最后就退化为一个无限的while循环。