内核的启动从入口函数start_kernel()开始。在init/main.c文件中,start_kernel相当于内核的main函数。打开这个函数,你会发现,里面是各种个样初始化函数XXXX_init。
初始化:
首先是项目管理部门,在操作系统中,有一个创始进程,有一行指令set_task_stack_end_magic(&init_task)。这里面有一个参数init_task,它的定义是struct task_struct init_task=INIT_TASK(init_task)。它是系统创建的第一个进程,我们称为0号进程。这是唯一一个没有通过fork或者kernel_thread产生的进程,是进程列表(Procese List)的第一个。
第二个要初始化的是办事大厅,来响应客户需求。这里面对应的函数是trap_init(),里面设置了很多中断门(Interrupt Gate),用于处理各种中断。其中有一个set_system_intr_gate(IA32_SYSCALL_VECTOR,entry_INT80_32),这是系统调用的中断门。系统调用也是通过发送中断的方式进行的。当然,64位的有另外的系统调用的方法。
接下来要初始化的是咱们的会议室管理系统。对应的,mm_init()就是用来初始化内存管理模块。项目需要项目管理进行调度,需要执行一定的调度策略。sched_init()就是用于初始化调度模块。vfs_caches_init()会用来初始化基于内存的文件系统rootfs。在这个函数里面,会调用mnt_init() -> init_rootfs()。这里面有一行代码,register_filesystem(&rootfs_fs_type)。在VFS虚拟文件系统里面注册了一种类型,我们定义为struct file_system_type rootfs_fs_type。文件系统是我们的项目资料库,为了兼容各种各样的文件系统,我们需要将文件的相关数据结构和操作抽象出来,形成一个抽象层对上提供统一的接口,这个抽象层就是VFS(Virtual FIle System),虚拟文件系统。
最后,start_kernel()调用的是reset_init(),用来做其他方面的初始化,这里面做了好多的工作。
- reset_init()的第一大工作是,用kernel_thread(kernel)init,NULL,CLONE_FS)创建第二个进程,这个是1号进程。有了多个进程就要开始对进程访问的资源进行一些限制,好在x86提供了分层的权限机制,把区域分成了四个Ring,越往里权限越高。
操作系统很好的利用了这个机制,将能够访问关键资源的代码放在Ring0,我们称为内核态;将普通的程序代码放在Ring3,我们称为用户态。
当处于用户态的代码想要执行更高权限的指令,这种行为是被禁止的,那如果用户态的代码想要访问核心资源,怎么办呢?这个过程是:用户态—系统调用—保存寄存器—内核态执行系统调用—恢复寄存器—返回用户态,然后接着执行。
从内核态到用户态
我们再回到1号进程启动的过程。当前执行kernel_thread这个函数的时候,我们还在内核态,现在我们就来跨越这道屏障,到用户态去运行一个程序。这该怎么办呢?
kernel_thread的参数是一个函数kernel_init,也就是这个进程会运行这个函数。在kernel_init里面,会调用kernel_init_freeable(),里面有这样的代码:
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
先不管ramdisk是啥,我们回到kernel_init里面。这里有这样的代码块:
if (ramdisk_e
xecute_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号进程运行的是一个文件。如果我们打开run_init_process函数,会发现它调用的是do_exccve。execve是一个系统调用,它的作用是运行一个执行文件。加一个do_的往往是内核系统调用的实现。没错,这就是一个系统调用,它会尝试运行ramdisk的"/init",或者普通文件系统上的"/sbin/init""/etc/init""/bin/init""/bin/sh"。不同版本的Linux会选择不同的文件启动,但是只要有一个起来了就可以。
static int run_init_process(const char *init_filename)
{
argv_init[0] = init_filename;
return do_execve(getname_kernel(init_filename),
(const char __user *const __user *)argv_init,
(const char __user *const __user *)envp_init);
}
如何利用执行init文件的机会,从内核态回到用户态呢?我们从系统调用的过程可以得到启发,“用户态–系统调用–保存寄存器–内核态执行系统调用–恢复寄存器–返回用户态”,然后接着运行。而咱们刚才运行init,是调用do_execve,正是上面的过程的后半部分,从内核态执行系统调用开始。
do_execve -> do_execveat_common -> exec_binprm -> search_binary_handler,这里会调用这段内容:
int search_binary_handler(struct linux_binprm *bprm)
{
......
struct linux_binfmt *fmt;
......
retval = fmt->load_binary(bprm);
......
}
我要运行一个程序,需要加载这个二进制文件,它是有一定格式的。Linux下一个常用的格式是ELF。于是我们就有了下面的这个定义:
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};
这其实就是先调用load_elf_binary,最好调用start_thread。
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);
struct pt_regs,看名字里的register,就是寄存器。这个结构就是在系统调用的时候,内核中保护用户态运行上下文的,里面将用户态的代码段CS设置为_USER_CS,将用户态的数据段DS设置为_USER_DS,以及指令指针寄存器IP、栈指针寄存器SP。这里相当于补上了原来系统调用里,保存寄存器的一个步骤。
最后的iret是干什么的呢?它是用于从系统调用中返回。这个时候会恢复寄存器。从哪里恢复呢?按说是从进入系统调用的时候,保存的寄存器里面拿出来。好在上面的函数补上了寄存器。CS和指令指针寄存器IP恢复了,指向用户态下一个要执行的语句。DS和函数栈指针SP也被恢复了,指向用户态函数栈的栈顶。所以,下一条指令,就从用户态开始运行了。
ramdisk的作用
ramdisk是根文件系统,在内核启动时,我们会配置一个参数:
initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img
这是一个基于内存的文件系统。为啥会有这个呢?
是因为刚才那个init程序是在文件系统上的,文件系统一定是在一个存储设备上的,例如硬盘。Linux访问存储设备,要有驱动才能访问。如果存储系统数目有限,那驱动可以直接放到内核里面,反正前面我们加载过内核到内存里了,现在可以直接对存储系统进行访问。
但是存储系统越来越多,如果市面上的存储系统的驱动默认都放进内核,内核就太大了。这怎么办?
我们只好先弄一个基于内存的文件系统。内存访问是不需要驱动的,这个就是ramdisk。这个时候,ramdisk是根文件系统。
然后,我们开始运行ramdisk上的/init。等它运行完了就已经在用户态了。/init这个程序会先根据存储系统的类型加载驱动,有了驱动就可以设置真正的根文件系统了。有了真正的根文件系统,ramdisk上的/init会启动文件系统上的init。
这时,我们仅仅形成了用户态的所有进程的祖先。
创建二号进程
kernel_thread(kthreadd,NULL,CLONE_FS|CLONE_FILES)又一次使用kernel_thread函数创建进程。这里需要指出一点,函数名thread可以翻译为线程,这也是操作系统很重要的一个概念。从内核态来看,无论是进程还是线程,我们都可以统称为任务(Task),都使用相同的数据结构,平放在同一个链表中。
这里的函数kthreadd,负责所有内核态的线程的调度与管理,是内核态所有线程运行的祖先。