08 | 内核初始化:生意做大了就得成立公司

本文仅作为学习记录,非商业用途,侵删,如需转载需作者同意。

上一节课,我们获得了《企业经营宝典》完成了一件大事,切换到了老板角色,从实模式切换到了保护模式,有了更强的寻址能力,接下来要按照宝典里面的指引,开始经营企业了。

内核的启动从入口函数 start_kernel() 开始,在init/main.c 文件中,start_kernel 相当于内核的main函数,里面有各种各样的xxx_init 函数。

在这里插入图片描述

一、初始化公司职能部门

首先是项目管理部,因为后续需要接各种各样的项目,项目管理体系和项目管理流程要建立起来,之前说的都是复制别的老项目,现在需要有第一个全新的项目。这个项目需要老板来打个样。

在操作系统里面,需要有个创始进程,有一行指令set_task_stack_end_magic(&init_task) ,这里面有个参数 init_task,它的定义是struct_task_struct init_task= INIT_TASK(init_task),它是系统创建的第一个进程,称为0号进程。
这是唯一一个没有通过fork 或者 kernel_thread 产生的进程,是进程列表的第一个。

所谓进程列表(Process 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) 虚拟文件系统。

这里的rootfs 还有其他的用途,下面我们会用到。
在这里插入图片描述

最后,start_kernel() 调用的是 rest_init() 用来做其他方面的初始化,这里面做了好多工作。

1.1、初始化1号进程

rest_init 的第一大工作是,用kernel_thread(kernel_init,NULL,CLONE_FS) 创建第二个进程,就是1号进程。

1号进程对于操作系统来说,有“划时代”的意义,因为它将运行一个用户进程,这意味着这个公司把一个老板独立完成的制度,变成了可以交付他人完成的制度。
这个相当于老板带了一个大徒弟,有了第一个就有第二个,后面大徒弟开枝散叶,带了很多徒弟,形成一颗进程树。

一旦有了用户进程,公司的运行模式就要发生一些变化。因为原来你是老板,所有的东西都是你控制的,没有人抢和恶意破坏。

现在有了其他人,公司里的环境和资源就需要做一些区分,哪些是核心的,哪些是非核心的。 哪些是普通人员可以访问的,哪些是核心人员访问的。

x86提供了分层的权限机制,把区域分成了四个Ring,越往里权限越高,越往外权限越低。

在这里插入图片描述

操作系统很好的利用了这个机制:
将能够访问关键资源的代码放在Ring0,我们称为内核态(Kernel Mode);
将普通程序代码放在Ring3,我们称为用户态(User Mode)。

现在系统处于保护模式,除了可以访问的空间大一些,还有"保护"的功能。
当处于用户态的代码想要执行更高权限的指令,这种行为是要被禁止的,防止他们为所欲为。

如果用户态的代码需要访问核心资源,怎么办呢?
咱们不是有提供系统调用的办事大厅吗?这里是统一的入口,用户态代码在这里请求就是了。 办事大厅后面就是内核态,用户态代码不用管后面发生了什么,做完了返回结果就行了。

当一个用户态的程序运行到一半,要访问一个核心资源,例如访问网卡发一个网络包,就需要暂停当前的运行,调用系统调用,接下来就轮到内核中的代码运行。

首先,内核将系统调用传过来的包,在网卡上排队,轮到的时候就发送,发送完了,系统调用就结束了,返回用户态,让暂停运行的程序接着运行。


这个暂停是如何实现的呢?
其实就是把程序运行到一半的情况保存下来。例如,我们知道,内存是用来保存程序运行时候的中间结果的,现在要暂停下来,这些中间结果不能丢,因为再次运行的时候还需要基于这些中间结果接着来。
另外当前代码运行到哪一行,当前的栈在哪里,这些都是在寄存器里面。

因此,当暂停的那一刻,要把当时CPU的寄存器的值全部暂存到一个地方,这个地方可以放在进程管理系统很容易获取的地方。
当系统调用完毕,返回的时候,再从这个地方将寄存器的值恢复回去,就能接着运行了。

在这里插入图片描述

这个过程是这样的:用户态-系统调用-保存寄存器-内核态执行系统调用-恢复寄存器-返回用户态,然后接着执行。

在这里插入图片描述

1.2、从内核态到用户态

我们再回到1号j进程启动的过程,当前执行kernel_thread 这个函数的时候,我们还在内核态,现在我们就来跨越这道屏障,到用户态去运行一个程序,这该怎么操作? 很少听过 “先内核态再用户态的”。

kernel_thread 的参数是一个函数 kernel_init 也就是这个进程会运行这个参数,在kernel_init 里面,会调用kernel_init_freeable() 里面有这样的代码:


if (!ramdisk_execute_command)
    ramdisk_execute_command = "/init";

先不管ramdisk 是啥,我们回到 kernel_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 会选择不同的文件启动,但是只要有一个起来就可以了。

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 (Executable and Linkable Format 可执行与可链接格式) ,于是我们就有了下面这个定义:

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 也被恢复了,指向用户态函数栈的栈顶,所以下一条指令,就从用户态开始运行了。

1.3、ramdisk的作用

init 终于从内核到用户态了,一开始到用户态的是ramdisk 的 init,后来会启动真正根文件系统上的 init ,成为所有用户态进程的祖先。

为什么会有ramdisk 这个东西呢,还记得之前内核启动的时候配置过这个参数:

initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img

就是这个东西,这是一个基于内存的文件系统,为啥会有这个呢?

是因为刚才那个init 程序是在文件系统上,文件系统一定是在一个存储设备上的,例如硬盘。 Linux 访问存储设备,要有驱动才能访问。 如果存储系统数目有限,那驱动可以直接放到内核里面,反正前面已经加载过内核到内存里了,现在可以直接对存储系统进行访问。

但是存储系统越来越多了,如果所有市面上存储系统的驱动都默认放在内核里,那么内核就太大了,该怎么办??

解决办法:弄个基于内存的文件系统,内存访问是不需要驱动的,这个就是ramdisk,这个时候ramdisk 就是根文件系统。

然后我们开始运行ramdisk 上的 /init ,等它运行完了就已经在用户态了。 /init 这个程序会先根据存储系统的类型加载驱动,有了驱动就可以设置真正的根文件系统了,有了真正的根文件系统,ramdisk 上的/init 会启动文件系统上的 init 。

接下来就是各种系统的初始化,启动系统的服务,启动控制台,用户就可以登录进来了。

先别忙着高兴,rest_init 的第一个大事情才完成,我们仅仅形成了用户态所有进程的祖先。

二、创建2号进程

用户态的所有进程都有大师兄了,那内核态的进程有没有一个人统一管理起来呢?
有的,rest_init 第二大事情就是第三个进程,2号进程。

kernel_thread(kthreadd,NULL,CLONE_FS|CLONE_FILES) 又一次使用kernel_thread 函数创建进程。 这里需要指出一点,函数名 thread 可以翻译成线程,这也是操作系统很重要的一个概念。
它和进程有什么区别呢?为啥这里创建的是进程,函数名是线程?


从用户态来看,创建进程其实就是立项,也就是启动一个项目。这个项目包含很多资源,例如会议室,资料库等。
这些都属于这个项目,但是这个项目需要人去执行,有多个人并行执行不同的部分,这就是多线程(Multithreading),如果只有一个人,那他就是这个项目的主线程。

但是从内核态来看,无论是进程还是线程,我们都可以统称为任务(Task),都使用相同的数据结构,平放在同一个链表中。

这里的函数 kthreadd,负责所有的内核态的线程的调度和管理,是内核所有线程运行的祖先。

这下好了,用户态和内核态都有人管了,开始接项目了。

三、总结时刻

这一节,作者讲了内核初始化过程,主要做了以下几件事情:

  • 各个职能部门的创建
  • 用户态祖先进程的创建
  • 内核态祖先进程的创建在这里插入图片描述

四、课后讨论

  • 不用纠结细节,知道内核初始化都干了啥就行了

  • 0生1,1生众生,2是内核态的办公室主任

  • 0号进程不运行

  • 问:ps aux 为什么看不到0号?
    答:0号是启动过程,完成自己的使命就可以退隐了。

  • Process List 里面有其他进程,那么 0 进程就不会运行了,当就绪队列中再没有其他进程时,闲逛进程就会被调度程序选中,以此来省电,减少热量的产生。

  • 问:为什么先创建用户态进程管理再创建内核进程管理,为什么不是反过来的呢?
    答:初始化的时候嘛,这个时候不会有真的客户进程要运行,所以都行。代码是紧挨着的。

1号是用户态,2号是内核态,进程CMD都被 [ ] 包裹这
在这里插入图片描述

  • 0号进程,链表头
  • 内核不要当作一个进程看,更不容易晕。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目录 1 引子 2 1.1 上电 2 1.2 BIOS时代 3 1.3 内核引导程序 5 2 内核映像的形成 8 2.1 MakeFile预备知识 9 2.1.1 Makefile书写规则 9 2.1.2 Makefile变量 10 2.1.3 条件判断 14 2.1.4 函数 17 2.1.5 隐含规则 17 2.1.6 定义模式规则 19 2.1 KBuild体系 23 2.1.1 内核目标 24 2.1.2 主机程序 26 2.1.3 编译标志 27 2.2 内核编译分析 28 2.2.1 编译配置 29 2.2.2 寻找第一个目标 32 2.2.3 prepare和scripts目标 38 2.2.4 递归编译各对象 41 2.2.5 链接vmlinux 44 2.2.6 制作bzImage 50 3 实模式下的内核代码 57 3.1 内核映像内存布局 58 3.2 实模式汇编代码header.S 60 3.2.1 无用的bootsect代码 60 3.2.2 初始化头变量hdr 63 3.2.3 准备实模式下C语言环境 64 3.3 实模式代码main函数 69 3.3.1 复制初始化头变量 71 3.3.2 初始化堆 74 3.3.3 确保支持当前运行的CPU 75 3.3.4 设置BIOS的x86模式 76 3.3.5 内存的检测 78 3.3.6 设置键盘属性 81 3.3.7 填充系统环境配置表 82 3.3.8 填充IST信息 83 3.3.9 设置Video模式 83 3.4 实模式代码go_to_proteced_mode函数 91 3.4.1 禁止可屏蔽和不可屏蔽中断 92 3.4.2 打开A20地址线 93 3.4.3 安装临时全局描述符表 99 3.4.4 第一次启动保护模式 101 4 保护模式下的内核代码 107 4.1 32位x86保护模式代码 107 4.1.1 内核解压缩的前期工作 108 4.1.2 解压缩内核 111 4.1.3 第二次启动保护模式 121 4.1.4 第一次启动分页管理 124 4.1.5 初始化0号进程 128 4.2 向start_kernel进发 131 4.2.1 初始化中断描述符表 132 4.2.2 第三次启动保护模式 137 4.2.3 启动x86虚拟机 141 5 走向现代:start_kernel函数 144 5.1 初始化同步与互斥环境 148 5.1.1 屏蔽中断 148 5.1.2 启动大内核锁 152 5.1.3 注册时钟通知链 153 5.1.4 激活第一个CPU 155 5.1.5 初始化地址散列表 160 5.1.6 打印版本信息 161 5.2 执行setup_arch()函数 166 5.2.1 拷贝可用内存区信息 171 5.2.2 获得总页面数 175 5.2.3 着手建立永久内核页表 177 5.2.4 第二次启动分页管理 181 5.2.5 建立内存管理架构 186 5.2.6 添砖加瓦 192 5.3 设置每CPU环境 206 5.4 初始化内存管理区列表 211 5.5 利用early_res分配内存 214 5.6 触碰虚拟文件系统 223 5.7 初始化异常服务 224 5.8 初始化内存管理 230 5.8.1 启用伙伴算法 230 5.8.2 初始化slab分配器 241 5.8.3 初始化非连续内存区 250 5.9 初始化调度程序 251 5.10 初始化中断处理系统 256 5.10.1 设置APIC中断服务 256 5.10.2 初始化本地软时钟 264 5.10.3 软中断初始化 268 5.10.4 初始化定时器中断 271 5.11 走进start_kernel尾声 273 5.11.1 初始化slab的后续工作 273 5.11.2 启动console 275 5.11.3 一些简单的函数 276 5.11.4 校准CPU时钟速度 279 5.11.5 创建一些slab缓存 282 5.12 安装根文件系统 287 5.12.1 创建VFS相关slab缓存 288 5.12.2 安装rootfs 291 5.12.3 安装proc文件系统 296 6 后start_kernel时代 298 6.1 创建1号进程 298 6.2 子系统的初始化 306 6.3 启动shell环境 309

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值