67

Linux -- 内核初始化

 5 years ago
source link: http://zhongmingmao.me/2019/04/13/linux-kernel-init/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

NzeyIzA.jpg!web

// init/main.c
asmlinkage __visible void __init start_kernel(void)
{
    // 0号进程(创始进程)
    set_task_stack_end_magic(&init_task);
    // 设置中断门(系统调用是通过中断的方式触发)
    trap_init();
    // 初始化内存管理模块
    mm_init();
    // 初始化调度模块
    sched_init();
    // 初始化基于内存的文件系统rootfs
    vfs_caches_init();
    // 创建1号进程(用户态总管)和2号进程(内核态总管)
    arch_call_rest_init();
    ...
}
  1. 内核的启动是从入口函数start_kernel()开始,相当于内核的main函数
  2. start_kernel()函数里面有各种各样的初始化函数(XXX_init)

0号进程

  1. set_task_stack_end_magic(&init_task) ,这是系统创建的第一个进程,称为 0号进程
  2. 0号进程是 唯一 一个没有通过 fork 或者 kernel_thread 产生的进程,是进程列表的第一个进程

trap_init()

  1. trap_init()里面设置了很多 中断门 (Interrupt Gate),用于处理各种中断
  2. 系统调用的也是通过 发送中断 的方式进行的,系统调用的中断门
    SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32)
    

mm_init() + sched_init()

  1. mm_init():初始化 内存管理模块
  2. sched_init():初始化 调度模块

vfs_caches_init()

// fs/dcache.c
void __init vfs_caches_init(void)
{
    mnt_init();
}

// init/do_mounts.c
void __init mnt_init(void)
{
    init_rootfs();
}

// init/do_mounts.c
int __init init_rootfs(void)
{
    // 在VFS虚拟文件系统里面注册一种类型
    int err = register_filesystem(&rootfs_fs_type);
}
  1. vfs_caches_init():初始化 基于内存的文件系统 rootfs
  2. 在vfs_caches_init()中会依次调用:mnt_init() -> init_rootfs() -> register_filesystem(&rootfs_fs_type)

VFS

  1. VFS: Virtual File System ,虚拟文件系统
  2. 为了兼容各种各样的文件系统,需要将文件的相关数据结构和操作 抽象 出来,形成一个抽象层并对上提供统一的接口

rest_init()

start_kernel()最后调用的是rest_init(),用来进行其他方面的初始化

// init/main.c
noinline void __ref rest_init(void)
{
    // 初始化1号进程,用户态总管,systemd
    pid = kernel_thread(kernel_init, NULL, CLONE_FS);
    ...
    // 初始化2号进程,内核态总管,kthreadd
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
}
$ top
PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
103587 root      20   0  162004   2284   1612 R  0.3  0.2   0:00.42 top
103801 root      20   0       0      0      0 S  0.3  0.0   0:00.08 kworker/0:1
     1 root      20   0  127976   6544   4136 S  0.0  0.7   0:25.99 systemd
     2 root      20   0       0      0      0 S  0.0  0.0   0:00.03 kthreadd

1号进程

  1. 通过 kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个线程,即 1号进程
  2. 1号进程对于操作系统来说,具有划时代的意义,因为1号进程将运行一个 用户进程

权限

NFNvym3.jpg!web

  1. 原来只有0号进程,所有资源都可以使用,没有竞争关系,也无须担心被恶意破坏
  2. 现在有了1号进程,需要区分核心资源和非核心资源,而x86提供了 分层的权限机制
    • 把区域分成了4个Ring,越往里权限越高
  3. 操作系统很好地利用了x86的分层权限机制
    • 将能够访问 关键资源 的代码放在 Ring0 ,称为 内核态 (Kernel Mode)
    • 将普通的程序代码放在Ring3,称为 用户态 (User Mode)
  4. 回忆Linux的启动过程,易知此时系统是处于 保护模式
    • 保护模式除了 寻址空间变大 以外,还有另外一个重要功能就是 保护
    • 保护: 禁止处于用户态的代码执行更高权限的指令
  5. 如果用户态的代码需要访问核心资源,需要通过 系统调用

状态切换

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

iiIjEj7.jpg!web

发送网络包

  1. 场景
    • 当一个用户态程序运行到一半时,要访问一个核心资源,例如访问网卡发送一个网络包
    • 此时需要暂停当前运行的用户态程序,调用 系统调用 ,切换到 内核态 ,接下来就是运行内核中的代码了
    • 内核将从系统调用传过来的包,在网卡上排队,等待发送
    • 发送完了,系统调用就结束,返回 用户态 ,让暂停运行的用户态程序继续运行
  2. 如何实现暂停?
    • 在暂停的那一刻,需要把当时CPU 寄存器 的值 全部 暂存到一个地方(进程管理系统很容易获取)
    • 当系统调用执行完毕,准备返回的时候,再从这个地方将寄存器的值恢复回去,就能接着运行了

a2AZrq2.jpg!web

内核态 -> 用户态

// init/main.c
static int __ref kernel_init(void *unused)
{
    // 将ramdisk_execute_command设置为/init
    kernel_init_freeable();

    // 执行ramdisk的/init
    if (ramdisk_execute_command) {
    	ret = run_init_process(ramdisk_execute_command);
    }

    // 执行根文件系统的/sbin/init、/etc/init、/bin/init、/bin/sh
    if (execute_command) {
    	ret = run_init_process(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;
}

// init/main.c
static noinline void __init kernel_init_freeable(void)
{
    if (!ramdisk_execute_command)
    	ramdisk_execute_command = "/init";
}

// init/main.c
static int run_init_process(const char *init_filename)
{
    // 系统调用,运行一个可执行文件,do_xxx往往是内核系统调用的实现
    return do_execve(getname_kernel(init_filename)...);
}

// init/main.c
static int try_to_run_init_process(const char *init_filename)
{
    ret = run_init_process(init_filename);
}

// fs/exec.c
int do_execve(struct filename *filename...)
{
    return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}

// fs/exec.c
static int do_execveat_common(int fd, struct filename *filename,...)
{
    return __do_execve_file(fd, filename, argv, envp, flags, NULL);
}

// fs/exec.c
static int __do_execve_file(int fd, struct filename *filename,...)
{
    retval = exec_binprm(bprm);
}

// fs/exec.c
static int exec_binprm(struct linux_binprm *bprm)
{
    ret = search_binary_handler(bprm);
}

// fs/exec.c
int search_binary_handler(struct linux_binprm *bprm)
{
    struct linux_binfmt *fmt;
    retval = fmt->load_binary(bprm);
}

// fs/binfmt_elf.c
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,
};

// fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
    start_thread(regs, elf_entry, bprm->p);
}

// arch/x86/kernel/process_32.c
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();
}
  1. 在1号进程的启动过程中,当执行kernel_thread函数时,还处于 内核态 ,需要切换到 用户态 去运行程序
  2. kernel_thread的第一个参数是一个函数kernel_init,kernel_init函数会调用kernel_init_freeable()
  3. 1号进程运行的是一个 文件 ,在run_init_process函数中,可见它实际调用的是do_execve
    • execve是一个系统调用,作用是运行一个 可执行文件 ,do_xxx往往是内核系统调用的实现
  4. 尝试运行ramdisk上的/init,或者普通文件系统上的/sbin/init、/etc/init、/bin/init、/bin/sh
    • 不同版本的Linux会选择不同的文件启动,只要有一个能起来即可
  5. 利用执行init文件的机会,从内核态回到用户态
    • 调用do_execve,恰好是上面系统调用过程的后半部分: 内核态执行系统调用 -> 恢复寄存器 -> 返回用户态
    • load_binary
      • 运行一个程序,需要加载 二进制文件
      • 而二进制文件也是有一定的格式,Linux下的常用格式为 ELF (Executable and Linkable Format)
    • .load_binary = load_elf_binary :先调用load_elf_binary,最后调用start_thread
      • start_thread的第一个参数 struct pt_regs寄存器
      • 这个结构是在 系统调用 时, 内核中用于保存 用户态运行上下文
      • 将用户态代码段CS设置为 __USER_CS ,将用户态的数据段DS设置为 __USER_DS
        • 以及指令指针寄存器IP和栈指针寄存器SP
      • force_iret()用于从系统调用中返回,此时会 恢复寄存器
      • CS和指令指针寄存器IP恢复了,指向用户态下一个要执行的指令
      • DS和函数栈指针SP也恢复了,指向用户态函数栈的栈顶

ramdisk

  1. init终于从内核态到用户态了,一开始到用户态的是ramdisk的init
    • 后来会启动 真正根文件系统 上的init,成为 所有用户态进程的祖先
  2. Grub启动配置
    • initrd16 /initramfs-3.10.0-957.el7.x86_64.img – 基于 内存 的文件系统
  3. 刚才的init程序是在 文件系统 上的,而文件系统一定是在 存储设备 上的,例如硬盘
  4. Linux需要 驱动 才能访问存储设备
    • 如果存储系统的数量很有限,那么驱动可以直接放到内核里面,内核会被加载到内存里,进而对存储系统进行访问
    • 但现在存储系统很多,如果把所有存储系统的驱动都放进内核,那内核会很大
  5. 因此可以先弄一个基于 内存 的文件系统, 内存访问是不需要驱动的 ,这就是ramdisk,此时的 ramdisk是根文件系统
  6. 然后开始运行ramdisk上的/init,运行完毕后就已经是用户态的
  7. ramdisk上的/init程序会 根据存储系统类型加载驱动 ,加载驱动后就可以 设置真正的根文件系统
  8. 有了真正的根文件系统,ramdisk上的/init会启动文件系统上的init
  9. 接下来就是各种系统的初始化,如启动系统服务,控制台等,用户就可以登录进来了

2号进程

  1. 通过 kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES) 创建第三个进程,即2号进程
  2. 进程与线程
    • 用户态 来看,创建的是 进程
    • 内核态 来看,无论进程还是线程,都可以统称为 任务 ,使用 相同的数据结构平放在同一个链表中
  3. kthreadd负责所有内核态线程的调度和管理,是 内核态所有线程的祖先

转载请注明出处:http://zhongmingmao.me/2019/04/13/linux-kernel-init/

访问原文「Linux -- 内核初始化」获取最佳阅读体验并参与讨论


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK