Linux之系统启动流程(3)—内核初始化

date
slug
linux-kernel-init
author
status
Public
tags
Linux
summary
type
Post
thumbnail
Linux-Kernel_blog-1000x500.png
category
updatedAt
Mar 29, 2023 07:14 AM

入口函数start_kernel()

 
在init/main.c中,start_kernel相当于内核的main函数,大致流程如下
notion image
我们一个一个看:
 
第一个INIT_TASK(init_task),这一步是创建进程结构,并建立0号进程,这是唯一一个没有通过 fork 或者 kernel_thread 产生的进程,是进程列表的第一个。
 
第二个trap_init(),里面设置了很多中断门(Interrupt Gate),用于处理各种中断。
 
第三个mm_init() 用来初始化内存管理模块。
 
第四个sched_init() 就是用于初始化调度模块。
 
在rest_init()之前还有一些步骤:
 
vfs_caches_init() 会用来初始化基于内存的文件系统 rootfs。在这个函数里面,会调用 mnt_init()->init_rootfs()。这里面有一行代码,register_filesystem(&rootfs_fs_type)。在 VFS 虚拟文件系统里面注册了一种类型,我们定义为 struct file_system_type rootfs_fs_type。
 
最后,start_kernel() 调用的是 rest_init(),用来做其他方面的初始化,这里面做了好多的工作。
 
rest_init 的第一大工作是,用 kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个进程,这个是 1 号进程。
 

权限机制

 
x86 提供了分层的权限机制,把区域分成了四个 Ring,越往里权限越高,越往外权限越低。操作系统很好地利用了这个机制,将能够访问关键资源的代码放在 Ring0,我们称为内核态(Kernel Mode);将普通的程序代码放在 Ring3,我们称为用户态(User Mode)
 
如果用户态的代码想要访问核心资源,怎么办呢?
 
当一个用户态的程序运行到一半,要访问一个核心资源,例如访问网卡发一个网络包,就需要暂停当前的运行,调用系统调用,接下来就轮到内核中的代码运行了。
 
首先,内核将从系统调用传过来的包,在网卡上排队,轮到的时候就发送。发送完了,系统调用就结束了,返回用户态,让暂停运行的程序接着运行。
 
这个暂停怎么实现呢?其实就是把程序运行到一半的情况保存下来。例如,我们知道,内存是用来保存程序运行时候的中间结果的,现在要暂时停下来,这些中间结果不能丢,因为再次运行的时候,还要基于这些中间结果接着来。另外就是,当前运行到代码的哪一行了,当前的栈在哪里,这些都是在寄存器里面的。
 
notion image
 

从内核态到用户态

 
上图中我们看到了一个用户进程完整的执行系统调用的过程:用户态 - 系统调用 - 保存寄存器 - 内核态执行系统调用 - 恢复寄存器 - 返回用户态,然后接着运行。
 
回到系统初始化,执行 kernel_thread 这个函数的时候,我们还在内核态。那假如要进入到用户态,现在这个半路出家的状态需要怎么做呢?
 
首先看kernel_thread这个函数,他的参数是一个函数 kernel_init,也就是这个进程会运行这个函数。在 kernel_init 里面,会调用 kernel_init_freeable(),里面有这样的代码:
if (!ramdisk_execute_command)
    ramdisk_execute_command = "/init";
 
这里有必要说一下ramdisk这个东西,还记得上一节咱们内核启动的时候,配置过这个参数:
initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img
这是一个基于内存的文件系统,涉及到一个先有鸡还是先有蛋的问题。init 程序是在文件系统上的,文件系统一定是在一个存储设备上的,这个时候,内核处于一个还没有完全启动的状态,甚至连访问存储设备的驱动都没有。必须等到内核完全启动完成后驱动才会加载完成。
 
那这样一来不就死循环了?要访问设备就要有驱动,要有驱动就要先访问设备。所以只好先弄一个基于内存的文件系统。内存访问是不需要驱动的,这个就是 ramdisk。这个时候,ramdisk 是根文件系统。
 
然后,我们开始运行 ramdisk 上的 /init。等它运行完了就已经在用户态了。/init 这个程序会先根据存储系统的类型加载驱动,有了驱动就可以设置真正的根文件系统了。有了真正的根文件系统,ramdisk 上的 /init 会启动文件系统上的 init。
 

启动1号进程

 
现在回到ramdisk的“/init”中,里面有这样的代码块:
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 号进程运行的是一个文件。如果我们打开 run_init_process 函数,会发现它调用的是 do_execve。
 
execve 是一个系统调用,它的作用是运行一个执行文件。加一个 do_ 的往往是内核系统调用的实现。没错,这就是一个系统调用,它会尝试运行 ramdisk 的“/init”,或者普通文件系统上的“/sbin/init”“/etc/init”“/bin/init”“/bin/sh”。不同版本的 Linux 会选择不同的文件启动,但是只要有一个起来了就可以。
 
现在处于从“内核态执行系统调用”开始。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);
  ......
}
load_binary就是加载二进制文件,一般的二进制文件的格式是ELF(Executable and Linkable Format,可执行与可链接格式,
 
加载完二进制文件之后,调用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);
 
这个结构就是在系统调用的时候,内核中保存用户态运行上下文的,里面将用户态的代码段 CS 设置为 __USER_CS,将用户态的数据段 DS 设置为 __USER_DS,以及指令指针寄存器 IP、栈指针寄存器 SP。这里相当于补上了原来系统调用里,保存寄存器的一个步骤。
 
force_iret()用于从系统调用中返回。这个时候会恢复寄存器。从哪里恢复呢?按说是从进入系统调用的时候,保存的寄存器里面拿出。好在上面的函数补上了寄存器。CS 和指令指针寄存器 IP 恢复了,指向用户态下一个要执行的语句。DS 和函数栈指针 SP 也被恢复了,指向用户态函数栈的栈顶。所以,下一条指令,就从用户态开始运行了。