内核的初始化

内核初始化,就是我们操作系统的内核初始化。那么操作系统初始化需要初始化哪些东西?是按照什么步骤进行的?带着孜孜不倦的态度去探索了…

先来个大概的印象:

从中可以知道初始化的内容包含进程初始化(0、1、2)、中断处理初始化、内存管理初始化、调度管理初始化。

进程初始化

首先,OS的目的是为了处理各种进程,但是OS本身也可以看做是一个跟CPU等硬件打交道的进程,因此在操作系统中必须先有一个进程:0号进程。这是OS创建的第一个进程,也是进程列表中的第一个进程。通过指令set_task_end_magic(&init_task)来创建。OS中我们知道创建进程都是通过fork或者kernel_thread产生进程,但这个进程却不是,它也是唯一一个不是的。而进程列表(Process List)可以看做一个管理进程的工具,里面存放着所有的进程。

中断初始化

在有了进程之后,我们如果有进程进行了中断或者其他处理,那么就需要进程系统调用,而系统调用也是通过发送中断的方式进行的。这就需要进行中断的初始化,通过函数trap_init()进行初始化,里面设置好了很多中断门

内存初始化

同样,进程的运行离不开内存(各种代码啊数据啊等等等),所以需要内存的初始化。通过函数mm_init()来初始化内存管理。

调度初始化

进程在运行中可能会需要切换,这时候就需要对进程进行调度了,通过函数sched_init()来初始化调度管理模块。

文件系统初始化

我们需要存取的数据太多的时候,不可能全部都放入在内存中,这时候就需要一个存放这些数据的地方:文件。通过vfs_caches_init()来初始化基于内存的文件系统rootfs

最后就是OS调用rest_init()初始化其他方面了。

1号进程初始化

在OS调用rest_init()的时候,首先第一个需要干的大事就是调用kernel_thread(kernel_init, NULL, CLONE_FS)创建系统第二个进程,这个进程就是1号进程

我们都知道OS是分为用户态与内核态的(不知道的强行知道),这个1号进程就是运行的第一个用户进程。如果我们将0号进程比作师父,1号进程就是大师兄,而大师兄也会开枝散叶,就会形成一个门派(这个门派我就叫他进程树派)。

但是俗话说教会徒弟饿死师父,OS不能把所有的东西都完全交给1号进程去管理,只有把非核心的一些东西交给1号进程(核心资源自己保留,核心内存自己保留),也就是类似古代皇帝不能将所有权限都给了你的属下,不然它们会造反王朝就会分崩离析(蓝屏警告)。因此x86提供了一种分层的权限机制,将区域分成了四个Ring,越往里权限越高。

OS很机灵的用了这个机制,将关键资源的代码放在Ring0,这就是我们耳熟能详、常常被八股提到的内核态;而讲普通的程序代码就放在Ring3,这也是我们常常听到的用户态。我们一般的程序也就是跑在用户态的。

这里有个问题,就是大徒弟解决不了的问题必须要师父解决,也就是说用户态的资源没办法去解决问题了,需要用到核心的内核态资源去运行,这时候怎么去访问呢?比如发送一个网络包,需要去内核中访问。而这时候就需要暂停当前的运行,调用系统调用来运行内核中的代码了。首先,内核将从系统调用传过来的包在网卡上排队,一直等待轮到的时候就发送,发送完了之后代表系统调用结束,再返回用户态,让暂停的程序继续跑。

那么,上面提到了这个程序要暂停,那么是怎么实现的呢?我们都知道程序跑的就是数据与数据的计算,所以暂停是不是就需要将这些跑了的数据给记录下来?再次运行的时候接着这些数据继续运行就可以了。再有就是程序跑到哪儿了,代码段执行到哪一个指令了,这也需要保存起来,这个都是保存在寄存器里面的。所以,在暂停的那一刻,我们需要将CPU中寄存器的值全部存在一个进程管理容易获取的地方,当进程恢复的时候将这些值恢复回去继续运行。所以整个过程可以总结为:

用户态->系统调用->保存寄存器->内核态执行系统调用->恢复寄存器->返回用户态

内核态到用户态

上面在初始化1号进程的时候,也就是在执行kernel_thread这个函数的时候,我们还是在内核态的(这时候还没用户态呢),那么如何从内核态切换到用户态呢?

在调用kernel_thread这个函数的时候有一个参数kernel_init,这也是个函数,在kernel_init中有一段代码:

1
2
3
4
5
6
7
8
9
10
if (ramdisk_execute_command) { 
ret = run_init_process(ramdisk_execute_command);
......
}
......
if ( !try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;

可以发现1号进程运行的是一个文件(linux一切皆文件是不是又符合了),而在run_init_process中发现都会调用do_execve。需要注意的是,execve是一个系统调用,其作用就是执行一个文件,前面加一个do一般就是指内核系统调用实现。而在调用do_execve的时候,会执行“内核态系统调用->恢复寄存器->返回用户态”这个过程中的部分,从内核态执行系统调用开始。在经过一系列的调用之后最终会调用一个函数start_thread函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
set_user_gs(regs, 0);
regs->fs = 0;
regs->ds = __USER_DS;
regs->es = __USER_DS;
regs->ss = __USER_DS;
regs->cs = __USER_CS;
regs->ip = new_ip;
regs->sp = new_sp;
regs->flags = X86_EFLAGS_IF;
force_iret();
}
EXPORT_SYMBOL_GPL(start_thread);

其中register代表的是寄存器,pt_regs这个机构体保存的就是用户态的上下文,里面将用户态的代码段CS设置为__USER_CS,将用户太的数据段DS设置为__USER_DS,以及指令指针寄存器IP、栈寄存器SP。这就是保存寄存器的步骤。

最后的 iret 是干什么的呢?它是用于从系统调用中返回。这个时候会恢复寄存器。从哪里恢复呢?按说是从进入系统调用的时候,保存的寄存器里面拿出。好在上面的函数补上了寄存器。CS 和指令指针寄存器 IP 恢复了,指向用户态下一个要执行的语句。DS 和函数栈指针 SP 也被恢复了,指向用户态函数栈的栈顶。所以,下一条指令,就从用户态开始运行了。

2号进程初始化

从上可知用户态进程已经有人管理了,那么内核态的进程有没有一个类似的大师兄来进程管理呢?没有就说不过去了,所以rest_init的第二个大事情就是创建第三个进程,2号进程

通过函数kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES)来创建进程。注意这里创建进程使用的是thread,可以翻译成线程。明明是创建的进程为什么会用线程的字来创建呢?

我们从用户态来看,创建一个进程就是分配资源(内存啊、文件啊等等),若只有一个线程来执行就是这个进程的主线程,若是有多个线程来并行执行就是多线程。但是从内核来看,无论是进程还是线程,都被统一称之为任务(task),使用的都是相同的数据结构,平放在同一个链表中。这里创建内核进程的函数kthreadd,其负责所有内核的线程的调度和管理,是内核态所有线程(或者说任务)运行的祖先。

现在用户态和内核态的进程都有人管了,可以去真正的运行一些进程了。