linux操作系统:内核初始化,按照宝典成立公司

按照步骤,你完成了从个体户(实模式)到老板角色(保护模式)的变化,获得了一本《企业经营宝典》,有了更强的寻址能力。接下来,我们就要按照宝典里面的角色,开始经营企业了

内核的启动从入口函数start_kernel()开始。在init/main.c文件中,start_kernel相当于内核的main函数。打开这个函数,你会发现,里面是各种各样的初始化函数xxxx_init。
在这里插入图片描述

初始化公司职能部门

于是,公司要开始建立各种职能部门了。

首先是项目管理部门

  • 在操作系统里面,先要有个创始进程(作为项目模板,之后创建项目就可以复制老项目)
    • 内核初始化有一行指令set_task_stack_end_magic(&init_task)。这里面的参数init_task,它的定义是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(),是用来初始化内存管理模式

项目需要项目管理进行调度,需要执行一定的调度策略。shed_init()就是用于初始化调度模块

文件系统是我们的项目资料库,为了兼容各种各样的文件系统,我们需要将文件的相关数据结构和操作抽象出来,形成一个抽象层对上提供统一的接口,这个抽象层就是VFS(Virtual File System),虚拟文件系统

  • 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(),用来做其他方面的初始化。

初始化1号进程

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

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

  • 一旦有了用户进程,公司的运行模式就要发生一定的变化。因为原来只有你是老板,没有雇佣其他人,所有东西都是你的,无论多么关键的资源,第一,不会有人跟你抢,第二,不会有人恶意破坏,恶意使用

  • 但是现在有了其他人,就要开始做一定的区分,哪些是核心资源,哪些是非核心资源;办公区也有分开,有普通的项目人员都能访问的项目工作区,还有职业核心人员能够访问的核心保密区。

  • 为此x86提供了分层的机制,把区域分成了4个Ring,越往里权限越高,越往外权限越低。
    在这里插入图片描述

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

现在,我们的系统已经处于保护模式了,保护模式除了可访问空间大一些,还有另一个重要功能,就是“保护”

  • 也就是说,当处于用户态的代码想要执行更高权限的指令,这种行为是被禁止的,要防止它们为所欲为。

  • 那用户态的代码想要访问核心资源,该怎么做呢?可以通过提供系统调用的办事大厅申请。

  • 办事大厅是统一的入门,其后面就是内核态,用户态代码在这里请求之后,就只需要等待返回结果就可以了,不用管后面(内核态)做了什么。

  • 当一个用户态的程序运行到一半,要访问一个核心资源,比如访问网卡发一个网络包,就需要暂停当前的运行,调用系统调用。调用系统调用之后,就会转为内核态,就是轮到内核中的代码运行了。

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

那,这个暂停怎么实现呢?其实就是把程序运行到一半的情况保存下来。比如:

  • 我们知道,内存是用来保存程序运行时候的中间结果的,现在要暂停,这些中间结果也不能丢,因为再次运行的时候,还要基于这些中间结果接着来。另外就是,当前运行到代码的哪一行了,当前的栈在哪里,这些都是在寄存器里面的
  • 所以,暂停的那一刻,要把当时CPU的寄存器的值全部暂存到某个地方,当系统调用完毕,返回的时候,再从这个地方将寄存器的值恢复回去,就能接着运行了

在这里插入图片描述

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

在这里插入图片描述

从内核态到用户态

我们再回到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_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_的往往是内核系统调用的实现。
  • 可以推断,run_init_process会尝试运行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。这里相当于补上了原来系统调用里,保存寄存器的一个步骤。
  • 最后的force_iret是干什么的呢?它是用于从系统调用中返回。这个时候会恢复寄存器。按说是从进入系统调用的时候,保存的寄存器里面拿出。好在上面的函数补上了寄存器。CS 和指令指针寄存器 IP 恢复了,指向用户态下一个要执行的语句。DS 和函数栈指针 SP 也被恢复了,指向用户态函数栈的栈顶。所以,下一条指令,就从用户态开始运行了

ramdisk的作用

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

ramdisk是什么?

  • 在安装操作系统启动内核时,会配置一个参数:
 initrd16 /boot/initramfs-3.10.0-862.el7.x86_64.img
  • 就是这个东西。这是一个基于内存的文件系统

为什么会有ramdisk这个东西呢?

  • 是因为刚刚那个init程序是在文件系统上的,文件系统一定是在一个存储设备上的
  • Linux访问存储设备,要有驱动才能访问。如果存储系统数目很有限,那驱动可以直接放在内核里面。
  • 但是存储系统越来越多了,如果所有市面上的存储系统的驱动都默认放进内核,内核就太大了。那怎么办呢?
  • 我们可以先做一个基于内存的文件系统。内存访问时不需要驱动的,这个就是ramdisk。这个时候,ramdisk是根文件系统。
  • 然后,我们开始运行ramdisk上的/init。等它运行完了就已经在用户态了。/init这个程序会先根据存储系统的类型加载驱动,有了驱动就可以设置真正的根文件系统了。有了真正的根文件系统,ramdis上的/init会启动文件系统上的init

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

限制,rest_init第一个里程碑就完成了:创建了用户态所有进程的祖先

创建2号进程

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

  • kernel_thread(
  • , NULL, CLONE_FS | CLONE_FILES)使用kernel_thread创建第二号进程
  • 问题:函数名thread可以翻译成“线程”,那为什么这里创建的是进程,函数名确是线程呢?
    • 从用户态来看,创建进程其实就是立项,也就是启动一个项目。
      • 这个项目需要包含很多资源,比如会议室、资料室等。
      • 有了项目就必须有人去执行才有价值,如果这个项目需要很多人去执行不同的部分,这就叫做多线程。如果只有一个人,就是单线程,这个人是这个项目的主线程
    • 而从用户态来看,无论是进程,还是线程,我们都可以统称为任务(Task),都使用相同的数据结构,放在同一个链表中
    • 也就说,用户态并不区分线程和进程
  • 函数里面的kthreadd,负责所有内核态的线程的调度和管理,是内核态所有线程运行的祖先

这些,用户态和内核态都有人管理,可以开始接项目了

总结

内核初始化过程中,主要做了下面几件事:

  • 各个职能部门的创建
  • 用户态祖先进程的创建
  • 内核态祖先进程的创建

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值