介绍背景

本文简要描述systemd进程在linux下的启动过程,内核源码参考2.6.34。

启动流程

  • 在架构有关的汇编代码完成特定初始化后,调用x86_64_start_kernel/i386_start_kernel函数,该函数又调用start_kernel开始内核的初始化工作;
  • start_kernel函数中有关根文件系统挂载的简要执行流程概述如下:
// 函数调用简要流程
start_kernel()
    ->vfs_caches_init()
        ->mnt_init()
            ->init_rootfs()          // 初始化rootfs文件系统
            ->init_mount_tree()
                ->mount_fs()         // 挂载rootfs文件系统
    ->rest_init()
        ->kernel_init()
            ->do_basic_setup()
                -> ...
                ->populate_rootfs()  // 解压处理initrd/initramfs根文件系统
            ->prepare_namespace()
                ->initrd_load()      // 处理initrd根文件系统
            ->init_post()
                ->run_init_process() // 执行init用户空间进程

// 代码位于/init/main.c
asmlinkage void __init start_kernel(void)
{
    ...
    vfs_caches_init(totalram_pages);
    ...
    rest_init();
    ...
}

static void rest_init(void)
{
    int pid;

    // 创建pid为1的init进程,所有用户空间进程的父进程
    rcu_scheduler_starting();
    kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
    numa_default_policy();

    // 创建用于内核调度的内核空间进程
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    rcu_read_lock();
    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
    rcu_read_unlock();
    unlock_kernel();

    // 创建pid为0的idle进程,用于在cpu空闲时执行
    cpu_idle();
}
  • vfs_caches_init函数创建VFS根目录以及初始化rootfs文件系统并挂载;
  • rest_init函数中依次创建了三个进程,idle进程,init进程以及kthreadd进程;
// 代码位于/init/main.c
static int kernel_init(void * unused)
{
    do_basic_setup();

    // 打开控制台设备,再复制两次,得到0,1,2三个描述符,即标准输入,标准输出,标准错误
    // 之后的所有子进程都继承这三个描述符

    if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
        printk(KERN_WARNING "Warning: unable to open an initial console.\n");

    (void) sys_dup(0);
    (void) sys_dup(0);

    // 启动initramfs根文件系统中的/init文件,systemd就是把init软链接到自身而取代init的
    if (!ramdisk_execute_command)
        ramdisk_execute_command = "/init";

    if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
        ramdisk_execute_command = NULL;

        prepare_namespace();
    }

    // 启动init进程,即执行上面的ramdisk_execute_command
    init_post();
}
  • kernel_init函数中调用do_basic_setup函数,如果内核支持initrd文件系统,则populate_rootfs函数将会编译进内核并被调用;
  • populate_rootfs函数调用unpack_to_rootfs解压释放initramfs类型文件系统到rootfs的根目录,其中就必须包括/init可执行文件;
  • 如果是ramdisk类型的initrd,则是通过bootloader将外部的initrd文件系统读到内存的指定地址,然后在populate_rootfs函数中解压到rootfs文件系统的/initrd.image中;
  • kernel_init函数中使用sys_access判断/init是否存在于当前挂载的文件系统,如果不存在则认为是ramdisk类型的initrd,执行prepare_namespace函数;
// 代码位于/init/do_mounts.c
void __init prepare_namespace(void)
{
    ...
    // 将设备名称转换为设备号并保存
    ROOT_DEV = name_to_dev_t(root_device_name);
    ...

    if (initrd_load())
        goto out;
    
out:
    // 用新的根文件系统的根目录替换 rootfs ,使其成为Linux VFS的根目录
    sys_mount(".", "/", NULL, MS_MOVE, NULL);
    sys_chroot(".");
}

int __init initrd_load(void)
{
    // 是否要加载 initrd 的标志,默认为1,当内核启动参数中包含 noinitrd 字符串时,mount_initrd 会被设为0
    if (mount_initrd) {
        // 创建设备文件,把ramdisk类型的initrd文件系统释放到内存中,然后通过下面函数加载/initrd.image,这个是在populate_rootfs阶段创建的
        create_dev("/dev/ram", Root_RAM0);
        // 如果内核启动参数中指定的最终根文件系统是这个刚创建的内存文件系统的话,就不处理
        // 留到之后当真正的文件系统处理,否则,先执行initrd中的linuxrc
        if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
            sys_unlink("/initrd.image");
            handle_initrd();
            return 1;
        }
    }
    sys_unlink("/initrd.image");
    return 0;
}
  • prepare_namespace函数首先解析root=参数,将指定的挂载设置转换为设备号保存到ROOT_DEV变量;
  • initrd_load函数中先在rootfs中创建一个设备号为Root_RAM0的设备/dev/ram0
  • rd_load_image将之前在populate_rootfs函数中解压到/initrd.image的文件写入到rootfs文件系统中的/dev/ram0
  • 如果root=参数指定的挂载设备和Root_RAM0不相同就执行handle_initrd,该函数主要启动内核线程执行/linuxrc
  • 按照上述步骤就完成根文件系统的挂载,然后调用init_post函数;
// 代码位于/init/main.c
static int init_post(void)
{
    ...

    if (ramdisk_execute_command) {
        run_init_process(ramdisk_execute_command);
        printk(KERN_WARNING "Failed to execute %s\n", ramdisk_execute_command);
    }

    if (execute_command) {
        run_init_process(execute_command);
        printk(KERN_WARNING "Failed to execute %s.  Attempting "
                    "defaults...\n", execute_command);
    }
    run_init_process("/sbin/init");
    run_init_process("/etc/init");
    run_init_process("/bin/init");
    run_init_process("/bin/sh");

    panic("No init found.  Try passing init= option to kernel. "
          "See Linux Documentation/init.txt for guidance.");
}

static void run_init_process(char *init_filename)
{
    argv_init[0] = init_filename;

    // 内核空间调用用户空间的程序,启动一个用户空间的进程,猜测是阻塞的
    kernel_execve(init_filename, argv_init, envp_init);
}
  • execute_command参数为从内核参数列表中解析的init参数;
  • 如果根文件系统非initramfs类型,则ramdisk_execute_command为NULL,所以首先执行的是init参数指定的可执行文件或者/sbin/init
  • 否则,则直接执行rootfs文件系统中的/init可执行程序;
  • 按照以上步骤逐步寻找要启动的用户进程,如果都找不到内核就启动失败,panic异常;

启动systemd

  • 查看systemd进程PID实际为上1,即为上述的init进程,系统/boot/initrd.img一般即为cpio格式的initramfs类型文件系统;
  • 通过cpio工具解压后可以看到:
drwxrwxr-x 12 alan  alan  4096 3月  12 09:57 ./
drwxrwxrwt 19 root  root  4096 3月  12 14:22 ../
lrwxrwxrwx  1 alan  alan     7 3月  12 09:57 bin -> usr/bin/
drwxr-xr-x  2 alan  alan  4096 3月  12 09:55 dev/
drwxr-xr-x 12 alan  alan  4096 3月  12 09:57 etc/
lrwxrwxrwx  1 alan  alan    23 3月  12 09:57 init -> usr/lib/systemd/systemd*
lrwxrwxrwx  1 alan  alan     7 3月  12 09:57 lib -> usr/lib/
lrwxrwxrwx  1 alan  alan     9 3月  12 09:57 lib64 -> usr/lib64/
  • 可以看到实际上/init可执行文件是systemd的符号链接,所以在init_post函数中执行的用户进程即为systemd;
  • 在制作initramfs根文件系统时,将systemd程序也打包进去,创建/init到systemd的软链接,即可启动systemd;
  • systemd启动后,完成后续的linux操作系统加载过程。