Chinaunix首页 | 论坛 | 博客
  • 博客访问: 622165
  • 博文数量: 168
  • 博客积分: 1053
  • 博客等级: 少尉
  • 技术积分: 1187
  • 用 户 组: 普通用户
  • 注册时间: 2012-04-30 17:20
个人简介

公众号【嵌入式er笔记】持续记录和分享C/C++、Linux、ARM、Android、IoT等技术相关知识,以及职场、生活经验和感悟。

文章分类

全部博文(168)

分类: LINUX

2014-10-11 20:43:54

以 Linux-2.6.25 的 kernel 为例,分析一下 Linux 启动过程中 initrd 的流程。

1. 先从 Makefile说起

下面是内核代码中 init/Makefile 文件的一段内容:
obj-y   := main.o version.o mounts.o

ifneq ($(CONFIG_BLK_DEV_INITRD),y)
obj-y += noinitramfs.o
else
obj-$(CONFIG_BLK_DEV_INITRD) += initramfs.o
endif

obj-$(CONFIG_GENERIC_CALIBRATE_DELAY) += calibrate.o

mounts-y := do_mounts.o
mounts-$(CONFIG_BLK_DEV_RAM)    += do_mounts_rd.o
mounts-$(CONFIG_BLK_DEV_INITRD) += do_mounts_initrd.o
mounts-$(CONFIG_BLK_DEV_MD)     += do_mounts_md.o

内核中和 initrd 相关的代码主要放在 init 目录下,包括 main.c,noinitramfs.c ,initramfs.c ,do_mounts.c ,do_mounts_initrd.c ,do_mounts_rd.c 和 do_mounts_md.c 。
从 Makefile 中可以看出,noinitramfs.c 是在内核不支持 initrd 的情况下被编译进内核,而 initramfs.c 正好相反,它处理(cpio包类型的)的 initrd 。do_mounts.c 主要是负责挂载根文件系统的,所以总是被编译。do_mounts_initrd.c 负责调用挂载和处理(ramdisk类型的)的 initrd 。do_mounts_rd.c 是具体实现如何挂载(ramdisk类型的)的 initrd 。do_mount_md.c 处理和 RAID 有关的一些情况。

2. cpio 包类型的 initrd 的处理


内核在初始化启动的时候会先注册一个叫作 rootfs 的文件系统,然后通过 rootfs_initcall 来生成其中的内容。根据内核是否支持 initrd 和 ramdisk ,rootfs 的生成方法和内容都会有所不同。当内核不支持 initrd 时,rootfs_initcall 调用 noinitramfs.c 中的 default_rootfs() 函数。default_rootfs() 主要往 rootfs 中生成两个目录 /dev 和 /root 以及一个设备文件 /dev/console 。下面是default_rootfs() 精简过的流程:
static int __init default_rootfs(void)
{
         sys_mkdir("/dev", 0755);
         sys_mknod((const char __user *) "/dev/console",
                  S_IFCHR | S_IRUSR | S_IWUSR,
                  new_encode_dev(MKDEV(5, 1)));
         sys_mkdir("/root", 0700);
         return 0;
}
rootfs_initcall(default_rootfs);

在调用rootfs_initcall()之前已经通过下面的调用过程建立了rootfs文件系统:

kernel_entry()->vfs_caches_init()->mnt_init()->init_rootfs()->init_mount_tree()->...

似乎在rootfs的init文件必须位于根目录下,即/init,否则系统会尝试去mount其它的文件系统,例如ram0,mtdblock等。

当内核支持 initrd 时,rootfs_initcall 调用 initramfs.c 中的 populate_rootfs() 函数来填充 rootfs。下面先看一下 populate_rootfs() 精简过的主要流程:
static int __init populate_rootfs(void)
{
        
         char *err = unpack_to_rootfs(__initramfs_start, __initramfs_end - __initramfs_start, 0);
         if (initrd_start) {   
#ifdef CONFIG_BLK_DEV_RAM   
                  err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 1);   
                  if (!err) {
                           unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 0);   
                           free_initrd();   
                           return 0;
                  }
                 
                  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
                 
                  err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 0);
                  if (err)
                           panic(err);
                  free_initrd();       
#endif
         }
         return 0;
}

rootfs_initcall(populate_rootfs);

关于 initramfs (也就是内核中自带的 cpio 包)需要解释的是:如果内核支持 initrd,但并没有配置 CONFIG_INITRAMFS_SOURCE 选项的话,内核在编译的时候会自动生成一个最小的 cpio 包附在内核中。这个内核自带的 cpio 包的内容与由 default_rootfs() 生成的一样。具体可参见编译后的内核源码树中的 usr/initramfs_data.cpio.gz 文件。
至此,内核对(cpio包格式的)initrd 的处理流程就结束了。如果在 populate_rootfs() 中成功地 unpack_to_rootfs() 的话,之后内核就不会再对 initrd 作任何操作,也不会去挂载根文件系统,所有的工作都留给 cpio 包(也就是rootfs)中的 /init 去完成了。关于这一点,在后面分析 main.c 中的流程中会看到。

3. 如果没有使用 initrd

下面先考虑一下内核在没有使用 initrd 的情况下挂载根文件系统的流程。这又分内核没有编译支持 initrd 或 内核支持 initrd 但系统引导时没有提供 initrd 文件两种情况。但这两种情况其结果其实是一样的,根据前面的分析,两种情况间的差别只是在生成 rootfs 的方式不一样,一个是通过 default_rootfs() ,另一个是调用 unpack_to_rootfs() ,而且其产生内容是一样的(只要没有配置 CONFIG_INITRAMFS_SOURCE)。
如果没有使用到 cpio 类型的 initrd,内核会执行 prepare_namespace() 函数(关于这个函数在内核启动过程中的位置,后面会有讲到)。prepare_namespace() 在 do_mounts.c 中定义,它主要负责挂载根文件系统和 ramdisk 类型的 initrd (如果需要的话)。下面看一下它精简过的大致流程:
void __init prepare_namespace(void)
{
         if (saved_root_name[0]) {   
                  root_device_name = saved_root_name;
                  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;

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

变量 saved_root_name 的值是内核启动的参数 "root=" 后的值,这个是由 __setup() 宏提取的。

下面看一下 mount_root() 函数精简过的流程:
void __init mount_root(void)
{
#ifdef CONFIG_BLOCK
         create_dev("/dev/root", ROOT_DEV);     
         mount_block_root("/dev/root", root_mountflags);
#endif
}

这里在 rootfs 中新建了一个 /dev/root 设备文件,这个设备文件一般就是指内核启动参数指定的包含根文件系统的设备。在 rootfs 中,这个设备文件被命名为 /dev/root 。

再往下看 mount_block_root() 的精简流程:
void __init mount_block_root(char *name, int flags)
{
         get_fs_names(fs_names);       
retry:
         for (p = fs_names; *p; p += strlen(p)+1) {   
                  int err = do_mount_root(name, p, flags, root_mount_data);
                  switch (err) {
                  ... ...
                  }

                 
                  ... ...
         }
         ... ...
}

所以在这个函数中,最主要的是调用了 do_mount_root() 来挂载根文件系统。

最后来看一下 do_mount_root() 函数的实现:
static int __init do_mount_root(char *name, char *fs, int flags, void *data)
{
         int err = sys_mount(name, "/root", fs, flags, data);
         if (err)
                  return err;

         sys_chdir("/root");
         ROOT_DEV = current->fs->pwd.mnt->mnt_sb->s_dev;
         ... ...   
         return 0;
}

do_mount_root() 尝试把参数 name 指定的设备文件挂载到 /root 目录中去,并 cd 到新的根文件系统的根目录中去。

至此,如果一切顺利的话,我们已经成功地把包含根文件系统的设备挂载到了 rootfs 中的 /root 目录上去了。我们的调用流程是 prepare_namespace() --> mount_root() --> mount_block_root() --> do_mount_root() 。

回到 prepare_namespace() 中去,在顺利 mount_root() 完之后,还有两步要做:
out:
         sys_mount(".", "/", NULL, MS_MOVE, NULL);   
         sys_chroot(".");
}

由于之前已经切换到了新的根文件系统的根目录中去,所以这两步的作用是用新的根文件系统的根目录替换 rootfs ,使其成为 Linux VFS 的根目录。

至此,我们已经走完了 prepare_namespace() 在不使用 initrd 情况下的流程,根文件系统设备也已经挂载上了,且替代了 rootfs 成为了 VFS 的根,剩下的任务就留给了根文件系统中的 /sbin/init 程序了。

4. ramdisk 类型的 initrd 的处理

从前面的分析可以看出,不管内核是否使用了 initrd ,只要进入了 prepare_namespace() , 都会去调用 initrd_load() 函数。下面看一下 initrd_load() 流程:
int __init initrd_load(void)
{
       if (mount_initrd) {
                 create_dev("/dev/ram", Root_RAM0);

                  if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
                          sys_unlink("/initrd.image");
                           handle_initrd();
                        return 1;
                 }
       }
      sys_unlink("/initrd.image");
       return 0;
}

变量 mount_initrd 是是否要加载 initrd 的标志,默认为1,当内核启动参数中包含 noinitrd 字符串时,mount_initrd 会被设为0 。接着为了把 initrd 释放到内存盘中,先需要创建设备文件,然后通过 rd_load_image 把之前保存的 /initrd.image 加载到内存盘中。 之后判断如果内核启动参数中指定的最终的根文件系统不是内存盘的话,那就先要执行 initrd 中的 linuxrc;如果最终的根文件系统就是刚加载到内存盘的 initrd 的话,那就先不处理它,留到之后当真正的根文件系统处理。
需要补充的是,只要没有用到 cpio 类型的 initrd,那内核都会执行到 rd_load_image("/initrd.image"),无论是否真的提供了 initrd 。如果没有提供 initrd,那 /initrd.image 自然不会存在,rd_load_image() 也会提早结束。另外 /dev/ram 这个设备节点文件在 rd_load_image() 中用完之后总会被删除(但相应的内存盘中的内容还在)。

下面看一下 handle_initrd() 的主要流程:
static void __init handle_initrd(void)
{
        int error;
        int pid;

         real_root_dev = new_encode_dev(ROOT_DEV);

        
        create_dev("/dev/root.old", Root_RAM0);
         mount_block_root("/dev/root.old", root_mountflags & ~MS_RDONLY);

        
         sys_mkdir("/old", 0700);
         root_fd = sys_open("/", 0, 0);
         old_fd = sys_open("/old", 0, 0);


        
         sys_chdir("/root");
         sys_mount(".", "/", NULL, MS_MOVE, NULL);
         sys_chroot(".");

        
         pid = kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD);
         if (pid > 0)
                  while (pid != sys_wait4(-1, NULL, 0, NULL))
                  yield();

        
         sys_fchdir(old_fd);
         sys_mount("/", ".", NULL, MS_MOVE, NULL);

        
         sys_fchdir(root_fd);
         sys_chroot(".");

         sys_close(old_fd);
         sys_close(root_fd);

        
         if (new_decode_dev(real_root_dev) == Root_RAM0) {
                 sys_chdir("/old");
                  return;
        }

        
         ROOT_DEV = new_decode_dev(real_root_dev);

        
         mount_root();

         printk(KERN_NOTICE "Trying to move old root to /initrd ... ");
        
         error = sys_mount("/old", "/root/initrd", NULL, MS_MOVE, NULL);
         if (!error)
                  printk("okay\n");
         else {
                  int fd = sys_open("/dev/root.old", O_RDWR, 0);
                  if (error == -ENOENT)
                           printk("/initrd does not exist. Ignored.\n");
                  else
                           printk("failed\n");

                 
                  printk(KERN_NOTICE "Unmounting old root\n");
                  sys_umount("/old", MNT_DETACH);
                  printk(KERN_NOTICE "Trying to free ramdisk memory ... ");
                  if (fd < 0) {
                           error = fd;
                  } else {
                           error = sys_ioctl(fd, BLKFLSBUF, 0);
                           sys_close(fd);
                  }
                  printk(!error ? "okay\n" : "failed\n");
          }
}

在这个函数中,用到了一个 real_root_dev 变量,它是一个 int 型的全局变量,它的值从变量 ROOT_DEV 转换过来。变量 real_root_dev 是和文件 /proc/sys/kernel/real-root-dev 相关联的(参见 kernel/sysctl.c 第405行左右),所以如果在执行 initrd 中的 /linuxrc 时改了 /proc/sys/kernel/real-root-dev 的话,就相当于又重新指定了真正的根文件系统。之所以要新弄一个 real_root_dev 变量,使因为 procfs 不支持改 kdev_t 型的 ROOT_DEV 变量。
另外,在 rootfs 中会建有两个设备文件节点:/dev/root 是真正的根文件系统的设备节点,其设备号是 ROOT_DEV 的值;/dev/root.old 是 ramdisk 型的 initrd 的设备节点,其设备号总是 Root_RAM0 。


下面看一下 do_linuxrc() 的实现:
static int __init do_linuxrc(void * shell)
{
          static char *argv[] = { "linuxrc", NULL, };
          extern char * envp_init[];

          sys_close(old_fd);sys_close(root_fd);
          sys_close(0);sys_close(1);sys_close(2);
          sys_setsid();
          (void) sys_open("/dev/console",O_RDWR,0);
          (void) sys_dup(0);
          (void) sys_dup(0);
          return kernel_execve(shell, argv, envp_init);
}

这样我们就走完了整个 ramdisk 类型的 initrd 的处理流程了,如果执行了其中的 /linuxrc 文件的话,当我们退回到 prepare_namespace() 函数时就会直接跳到 out: 标签处,剩下还有两行代码要执行:
out:
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
}

这两步前一节已经分析过了,就是把当前目录替换 rootfs ,使其成为 Linux VFS 的根目录。由于之前无论是用内存盘还是指定的新设备作为我们的真正的根文件系统,我们都会 cd 进去,所以这里就很好理解了。

 

另外,补充一点的是,通过比较 cpio 包和 ramdisk 类型的initrd,我们可以发现 cpio 包里的东西是直接解压到 rootfs 中的,成为 rootfs 的一部分,而 ramdisk 的 initrd 是以 ramdisk 形式存在的,需要额外挂载到 rootfs 上才能使用,两者有很大的区别。

5. 总的流程

最后我们看一下内核启动过程中和 initrd 有关的一个总的流程顺序。
内核初始化从 start_kernel() 函数开始,start_kernel() 最后会调 rest_init() 函数。在 rest_init() 函数中,第一步就会通过调用 kernel_thread(kernel_init, ...) 生成一个内核线程来执行 kernel_init() 函数。注意,这个 kernel_init() 线程就是后来的 init 进程(1号进程)的前身,而原来的 rest_init() 函数在最后调用 cpu_idle() 后就变成 swap 进程(0号进程)了。


接着,我们从 kernel_init() 函数线程继续研究,下面列出了 kernel_init() 函数中和 initrd 相关的的主要流程:
static int __init kernel_init(void * unused)
{
   ... ...
   do_basic_setup();

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

在 kernel_init() 中会调用 do_basic_setup(),而 do_basic_setup() 又会调用 do_initcalls(),所以 cpio 包类型的 initrd (如果有的话)就会在此时被填充到 rootfs 中去。接下来初始化 ramdisk_execute_command 变量,这个变量表示在 cpio 包中要被执行的第一个程序,可通过在内核启动参数中给 rdinit= 赋值来指定。接下来检查在 rootfs 中是否存在变量 ramdisk_execute_command 所指的文件。如果有,就说明 cpio 包类型的 initrd 成功加载了,那就不需要内核再调用 prepare_namespace() 来挂载根文件系统了,这些都留给 cpio 包里的 ramdisk_execute_command 所指的程序去完成了;如果没有,就说明内核并没有成功用上 cpio 包类型的 initrd,还需要调用 prepare_namespace() 来继续准备加载根文件系统,并清空变量ramdisk_execute_command。无论怎样,内核都会继续执行 init_post()。


init_post() 是内核初始化的终点了:把 kernel_init() 内核线程 execve 成用户态的 init 进程。下面是它的主要流程:
static int noinline init_post(void)
{
    ... ...
    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);

   
    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.");
}

阅读(1559) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~