linux内核启动分析 三,Linux内核启动过程分析

为什么不直接把真实的文件系统配置为根文件系统?答案很简单,内核中没有真实根文件系统设备(如硬盘,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;

}0b1331709591d260c1c78e86d0c51c18.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值