Linux内核启动过程分析(3)

为什么不直接把真实的文件系统配置为根文件系统?答案很简单,内核中没有真实根文件系统设备(如硬盘,USB)的驱动,而且即便你将根文件系统的设备驱动编译到内核中,此时它们还尚未加载,实际上所有内核中的驱动是由后面的kernel_init线程进行加载。另外,我们的root设备都是以设备文件的方式指定的,如果没有根文件系统,设备文件怎么可能存在呢?
    注意根据调用链do_kern_mount()--->vfs_kern_mount(type)--->type->get_sb()--->fs/ramfs/inode.c:rootfs_get_sb()--->ramfs_fill_super()--->fs/dcache.c:d_alloc_root(),函数d_alloc_root分配最终的根结点,代码如下:

struct dentry * d_alloc_root(struct inode * root_inode)
{
 struct dentry *res = NULL;

if (root_inode) {
  static const struct qstr name = { .name = "/", .len = 1 };

res = d_alloc(NULL, &name);
  if (res) {
  res->d_sb = root_inode->i_sb;
  res->d_parent = res;
  d_instantiate(res, root_inode);
  }
 }
 return res;
}

从上面的代码中的可以看出,这个rootfs的dentry对象的名字为"/",这就是我们看到的根目录"/"。
    start_kernel()在最后会调用rest_init(),这个函数会启动一个内核线程来运行kernel_init(),自己则调用cpu_idle()进入空闲循环,让调度器接管控制权。抢占式的调度器就可以周期性地接管控制权,从而提供多任务处理能力。
    kernel_init()用于完成初始化rootfs、加载内核模块、挂载真正的根文件系统。根据Documentation/early-userspace/README的描述,目前2.6的kernel支持三方式来挂载最终的根文件系统:   
    (1)所有需要的设备和文件系统驱动被编译进内核,没有initrd。通过“root="参数指定的根设备,init/main.c:kernel_init()将调用prepare_namespace()直接在指定的根设备上挂载最终的根文件系统。通过可选的"init="选项,还可以运行用户指定的init程序。
    (2)一些设备和文件驱动作为模块来构建并存放的initrd中。initrd被称为ramdisk,是一个独立的小型文件系统。它需要包含/linuxrc程序(或脚本),用于加载这些驱动模块,并挂载最终的根文件系统(结合使用pivot_root系统调用),然后initrd被卸载。initrd由prepare_namespace()挂载和运行。内核必须要使用CONFIG_BLK_DEV_RAM(支持ramdisk)和CONFIG_BLK_DEV_INITRD(支持initrd)选项进行编译才能支持initrd。
    initrd文件通过在grub引导时用initrd命令指定。它有两种格式,一种是类似于linux2.4内核使用的传统格式的文件系统镜像,称之为image-initrd,它的制作方法同Linux2.4内核的initrd一样,其核心文件就是 /linuxrc。另外一种格式的initrd是cpio格式的,这种格式的initrd从linux 2.5起开始引入,使用cpio工具生成,其核心文件不再是/linuxrc,而是/init,这种 initrd称为cpio-initrd。为了向后兼容,linux2.6内核对cpio-initrd和image-initrd这两种格式的initrd 均支持,但对其处理流程有着显著的区别。cpio-initrd的处理与initramfs类似,会直接跳过prepare_namespace(),image-initrd的处理则由prepare_namespace()进行。
    (3)使用initramfs。prepare_namespace()调用会被跳过。这意味着必须有一个程序来完成这些工作。这个程序是通过修改usr/gen_init_cpio.c的方式,或通过新的initrd格式(一个cpio归档文件)存放在initramfs中的,它必须是"/init"。这个程序负责prepare_namespace()所做的所有工作。为了保持向后兼容,在现在的内核中,/init程序只有是来自cpio归档的情况才会被运行。如果不是来自cpio归档,init/main.c:kernel_init()将运行prepare_namespace()来挂载最终的根文件系统,并运行一个预先定义的init程序(或者是用户通过init=指定的,或者是/sbin/init,/etc/init,/bin/init)。
    initramfs是从2.5 kernel开始引入的一种新的实现机制。顾名思义,initramfs只是一种RAM filesystem而不是disk。initramfs实际是一个包含在内核映像内部的cpio归档,启动所需的用户程序和驱动模块被归档成一个文件。因此,不需要cache,也不需要文件系统。 编译2.6版本的linux内核时,编译系统总会创建initramfs,然后通过连接脚本arch\x86\kernel\vmlinux.lds.S把它与编译好的内核连接成一个文件,它被链接到地址__initramfs_start~__initramfs_end处。内核源代码树中的usr目录就是专门用于构建内核中的initramfs的。缺省情况下,initramfs是空的,X86架构下的文件大小是134个字节。实际上它的含义就是:在内核镜像中附加一个cpio包,这个cpio包中包含了一个小型的文件系统,当内核启动时,内核将这个cpio包解开,并且将其中包含的文件系统释放到rootfs中,内核中的一部分初始化代码会放到这个文件系统中,作为用户层进程来执行。这样带来的明显的好处是精简了内核的初始化代码,而且使得内核的初始化过程更容易定制。
    注意initramfs和initrd都可以是cpio包,可以压缩也可以不压缩。但initramfs是包含在内核映像中的,作为内核的一部分存在,因此它不会由bootloader(如grub)单独地加载,而initrd是另外单独编译生成的,是一个独立的文件,会由bootloader单独加载到RAM中内核空间以外的地址处。目前initramfs只支持cpio包格式,它会被populate_rootfs--->unpack_to_rootfs(&__initramfs_start, &__initramfs_end - &__initramfs_start, 0)函数解压、解析并拷贝到根目录。initramfs被解析处理后原始的cpio包(压缩或非压缩)所占的空间(&__initramfs_start - &__initramfs_end)是作为系统的一部分直接保留在系统中,不会被释放掉。而对于initrd镜像文件,如果没有在命令行中设置"keepinitd"命令,那么initrd镜像文件被处理后其原始文件所占的空间(initrd_end - initrd_start)将被释放掉。   
    下面看kernel_init的代码:

static int __init kernel_init(void * unused)
{
 /* ......(省略) */

do_basic_setup();

/* Open the /dev/console on the rootfs, this should never fail */
 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);
 /*
  * check if there is an early userspace init.  If yes, let it do all
  * the work
  */

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();
 }

/*
  * Ok, we have completed the initial bootup, and
  * we're essentially up and running. Get rid of the
  * initmem segments and start the user-mode stuff..
  */

init_post();
 return 0;
}

kernel_init会先调用do_basic_setup,这是一个很关键的函数。在此之前CPU子系统运行起来了,内存管理和进程管理也启动了,到do_basic_setup才开始做真正实际的工作。所有直接编译在kernel中的模块都是由它启动的。代码如下:

static void __init do_basic_setup(void)
{
    init_workqueues();
    cpuset_init_smp();
    usermodehelper_init();
    init_tmpfs();
    driver_init();
    init_irq_proc();
    do_ctors();
    do_initcalls();
}
    do_initcalls()用来启动所有在__initcall_start和__initcall_end段之间的函数,而静态编译进内核的模块会将其初始化函数放置在这段区间里。其中与rootfs相关的初始化函数都会由rootfs_initcall()所引用。在init/initramfs.c中就有rootfs_initcall(populate_rootfs)的引用,这是用来初始化rootfs的,因此do_initcall()最终会调用到populate_rootfs()。需要特别指出的是initramfs.c模块的入口函数populate_rootfs()是否执行取决于Kernel的编译选项,参考init/Makefile,内核编译时必须配置CONFIG_BLK_DEV_INITRD选项才会执行这个函数。代码如下:

static int __init populate_rootfs(void)
{
 char *err = unpack_to_rootfs(__initramfs_start,
    __initramfs_end - __initramfs_start);
 if (err)
  panic(err); /* Failed to decompress INTERNAL initramfs */
 if (initrd_start) {
#ifdef CONFIG_BLK_DEV_RAM
  int fd;
  printk(KERN_INFO "Trying to unpack rootfs image as initramfs...\n");
  err = unpack_to_rootfs((char *)initrd_start,
  initrd_end - initrd_start);
  if (!err) {
  free_initrd();
  return 0;
  } else {
  clean_rootfs();
  unpack_to_rootfs(__initramfs_start,
    __initramfs_end - __initramfs_start);
  }
  printk(KERN_INFO "rootfs image is not initramfs (%s)"
    "; looks like an initrd\n", err);
  fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 0700);
  if (fd >= 0) {
  sys_write(fd, (char *)initrd_start,
    initrd_end - initrd_start);
  sys_close(fd);
  free_initrd();
  }
#else
  printk(KERN_INFO "Unpacking initramfs...\n");
  err = unpack_to_rootfs((char *)initrd_start,
  initrd_end - initrd_start);
  if (err)
  printk(KERN_EMERG "Initramfs unpacking failed: %s\n", err);
  free_initrd();
#endif
 }
 return 0;
}

(1)第一行的upack_to_rootfs()用来把内核映像中的initramfs释放到rootfs。它实际上有两个功能,一个是检测是否是属于cpio包,另外一个就是解压并释放cpio包。注意如果__initramfs_start和__initramfs_end的值相等,则initramfs长度为零,unpack_to_rootfs()不会做任何处理,直接返回。
    (2)if(initrd_start)判断是否加载了initrd。无论哪种格式的initrd,都会被boot loader加载到地址initrd_start处。当然,如果是initramfs的情况下,该值肯定为空了。
    (3)第二个unpack_to_rootfs()把cpio-initrd镜像释放到rootfs,以此作为initramfs。这其中有/init脚本程序。
    (4)如果不是cpio-initrd,则认为是一个image-initrd,将其内容保存到/initrd.image中。image-initrd由prepare_namespace()函数来处理。传统的image-initrd中使用/linuxrc脚本程序进行初始化。
    回到kernel_init,接下来的工作是打开控制台设备/dev/console并设为标准输入,有���这个设备,启动信息才能显示到终端上。后续的两个sys_dup(0)是复制标准输入为标准输出和标准错误输出。然后,如果rootfs中存在init文件(用户通过rdinit=指定,或者默认的/init,保存在ramdisk_execute_command中),说明是加载了initramfs(包括cpio-initrd的情形),直接跳过prepare_namespace(),转向init_post(),它会调用run_init_process(ramdisk_execute_command)运行这个/init文件,替换当前进程,这样内核的工作全部结束,后续的初始化和挂载真正根文件系统的工作都交给/init程序。读者可能会问如果加载了cpio-initrd, 那么真实文件系统中的init进程不是没有机会运行了吗?确实,如果加载了cpio-initrd,那么内核就不负责执行用户空间的init进程了,而是将这个执行任务交给了cpio-initrd的init进程。
    如果rootfs中没有init文件,说明是image-initrd的情形,就会转入到prepare_namespace(),这个函数加载image-initrd,并运行它的/linuxrc文件。prepare_namespace()的代码如下:

void __init prepare_namespace(void)
{
 int is_floppy;

if (root_delay) {
  printk(KERN_INFO "Waiting %dsec before mounting root device...\n",
        root_delay);
  ssleep(root_delay);
 }

/*
  * wait for the known devices to complete their probing
  *
  * Note: this is a potential source of long boot delays.
  * For example, it is not atypical to wait 5 seconds here
  * for the touchpad of a laptop to initialize.
  */
 wait_for_device_probe();

md_run_setup();

if (saved_root_name[0]) {
  root_device_name = saved_root_name;
  if (!strncmp(root_device_name, "mtd", 3) ||
      !strncmp(root_device_name, "ubi", 3)) {
  mount_block_root(root_device_name, root_mountflags);
  goto out;
  }
  ROOT_DEV = name_to_dev_t(root_device_name);
  if (strncmp(root_device_name, "/dev/", 5) == 0)
  root_device_name += 5;
 }

if (initrd_load())
  goto out;

/* wait for any asynchronous scanning to complete */
 if ((ROOT_DEV == 0) && root_wait) {
  printk(KERN_INFO "Waiting for root device %s...\n",
  saved_root_name);
  while (driver_probe_done() != 0 ||
  (ROOT_DEV = name_to_dev_t(saved_root_name)) == 0)
  msleep(100);
  async_synchronize_full();
 }

is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;

if (is_floppy && rd_doload && rd_load_disk(0))
  ROOT_DEV = Root_RAM0;

mount_root();
out:
 devtmpfs_mount("dev");
 sys_mount(".", "/", NULL, MS_MOVE, NULL);
 sys_chroot(".");
}

(1)对于将根文件系统存放到USB或者SCSI设备上的情况,Kernel需要等待这些耗费时间比较久的设备驱动加载完毕,所以这里存在一个Delay。
    (2)wait_for_device_probe(),从字面的意思来看,这里也是来等待根文件系统所在的设备探测函数的完成。
    (3)用户通过“root=”指定的根设备名会被保存在saved_root_name中,如果用户指定了以mtd开始的字串做为它的根设备。就会直接调用mount_block_root()去挂载它并goto到out。这个文件是mtdblock的设备文件。否则将设备结点文件转换为ROOT_DEV即设备节点号。然后,转向initrd_load(),去加载image-initrd,执行其中的/linuxrc,挂载最终和根文件系统。
    (4)initrd_load()会把/dev/ram0作为默认的根设备并把image-initrd加载到这里。如果用户通过root=指定了实际根设备(不是/dev/ram0),则说明image-initrd只是作为临时的文件系统而存在,转向handle_initrd(),对image-initrd进行具体的处理。它执行其中的/linuxrc,挂载最终的根文件系统。
    (5)如果用户没有指定根设备(或指定为默认的/dev/ram0),说明直接把image-initrd作为最终的真实文件系统(在无盘工作站和很多嵌入式Linux系统中,initrd通常作为永久的根文件系统而存在),prepare_namespace()会设置好ROOT_DEV为/dev/ram0,并调用mount_root()挂载这个image-initrd,作为最终的文件系统而存在。
    (6)挂载完真正的根文件系统后,goto到out,将挂载点从当前目录移到"/",并把"/"作为系统的根目录,至此虚拟文件系统切换到了实际的根文件系统。
    initrd_load()的代码如下:

int __init initrd_load(void)
{
 if (mount_initrd) {
  create_dev("/dev/ram", Root_RAM0);
  /*
  * Load the initrd data into /dev/ram0. Execute it as initrd
  * unless /dev/ram0 is supposed to be our actual root device,
  * in that case the ram disk is just set up here, and gets
  * mounted in the normal path.
  */
  if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
  sys_unlink("/initrd.image");
  handle_initrd();
  return 1;
  }
 }
 sys_unlink("/initrd.image");
 return 0;
}

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/17407.html