init/main.c中的start_kernel函数完成了所有的全局特性初始化,这些全局特性包括内核运转所需要的基础设施,比如虚拟内存设施,进程调度设施,中断设施,缓存设施,VFS设施等,接下来启动1号进程的内核部分,在start_kernel的最后rest_init函数中启动之:
static void noinline rest_init(void)
{
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
unlock_kernel();
cpu_idle();
}
因此init/main.c中的init内核线程函数即是1号进程的内核部分,它完成内核的另一部分初始化之后即exec到1号进程的用户态,从此一直到关机或者重启,不再返回内核态,实际上exec本质上替换了进程地址空间,也就无从返回了。
init函数主要进行另一部分的初始化,涉及驱动,网络协议栈,以及为1号进程用户态即init进程准备环境,其中最为重要的就是populate_rootfs函数,在启动initrd的情况下,最为重要的是它将initrd的内存写到了一个文件当中或者直接将initrd的内容写到整个rootfs:
1.将内容写到文件:
fd = sys_open(“/initrd.image”, O_WRONLY|O_CREAT, 700);
if (fd >= 0) {
sys_write(fd, (char *)initrd_start, initrd_end – initrd_start); //将initrd的内容写入文件
sys_close(fd);
free_initrd_mem(initrd_start, initrd_end); //释放initrd原始内容所占用的内存
}
2.将内容直接放到rootfs:根据rootfs和initrd的内存地址信息直接写。
可见sys_open调用创建了一个文件,即/initrd.image,它的内容就是initrd内存盘的内容,可是它在/下被创建,在linux中,所有的文件都要有一个“文件系统”作为载体,这个/目录所在的文件系统是什么呢?其实是一个内存盘,在start_kernel中的vfs_caches_init负责初始化文件系统,也就是VFS,这是一个虚拟文件系统的框架,其实现如下:
void __init vfs_caches_init(unsigned long mempages)
{
unsigned long reserve;
/* Base hash sizes on available memory, with a reserve equal to
150% of current kernel size */
reserve = min((mempages – nr_free_pages()) * 3/2, mempages – 1);
mempages -= reserve;
names_cachep = kmem_cache_create(“names_cache”, PATH_MAX, 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL, NULL);
filp_cachep = kmem_cache_create(“filp”, sizeof(struct file), 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC, filp_ctor, filp_dtor);
dcache_init(mempages);
inode_init(mempages);
files_init(mempages);
mnt_init(mempages);
bdev_cache_init();
chrdev_init();
}
在mnt_init中初始化了一个rootfs:
int __init init_rootfs(void)
{
return register_filesystem(&rootfs_fs_type);
}
然后在init_mount_tree中调用do_kern_mount挂载了这个rootfs作为“根”,接下来在init函数中创建/initrd.image就有所依托了,它就是在rootfs中创建了一个文件:/initrd.image,rootfs本质上是一个内存文件系统,因为此时还没有加载任何驱动,更别说磁盘,磁带,网卡驱动了,因此文件系统也只能是内存式的。linux的vfs功能十分强大,以文件作为接口使初始化过程如此简洁!
接下来就要加载驱动了,do_basic_setup初始化了所有硬编译进内核的驱动,然而驱动并不一定要编译进内核,而这些驱动可能对于加载基于磁盘的根文件系统还至关重要,比如ide驱动,比如scsi驱动等,这些就由initrd来加载了,initrd的本质是一只鸡,也就是在有磁盘根文件系统这个“蛋”之前先要有一只“鸡”。
接下来prepare_namespace登场,我们可以看到在populate_rootfs结束后分开了两条线索:
if (sys_access((const char __user *) “/init”, 0) == 0)
execute_command = “/init”;
else
prepare_namespace();
void __init prepare_namespace(void)
{
…
if (saved_root_name[0]) { //设置root文件系统的命令行参数,比如root=/dev/hda1
root_device_name = saved_root_name;
ROOT_DEV = name_to_dev_t(root_device_name); //设置ROOT_DEV
if (strncmp(root_device_name, “/dev/”, 5) == 0)
root_device_name += 5;
}
…
if (initrd_load())
goto out;
…
}
在initrd_load中会将/initrd.image挂载到/dev/ram0,然后将之挂载为“临时根”,chroot到这个临时根,fork出一个内核线程执行/linuxrc程序,这就是image格式的initrd的处理过程,在等待内核子线程执行完/linuxrc之后,1号进程继续在内核执行,由于/linuxrc可能已经加载了需要的磁盘驱动以及其它妨碍挂载真正根文件系统的驱动,内核此时就可以switch root到ROOT_DEV了,按照linux发行版的习惯,一般init程序在sbin下,也可能在bin下或者其它的什么地方,于是内核在这几个地方搜索:
run_init_process(“/sbin/init”);
run_init_process(“/etc/init”);
run_init_process(“/bin/init”);
如果实在没有找到,那么执行一个shell也行:
run_init_process(“/bin/sh”);
否则只有panic了。
可见,image格式和cpio格式的initrd采用了截然不同的两种机制来支持initrd,哪一种更好呢?如果从机制和策略分离的角度当然是cpio的,cpio的initrd直接接手所有的工作-加载驱动和挂载根以及switch到根,这些显然都是用户要做的事,而image格式的initrd则需要内核帮忙把这几个步骤完成:
1.内核执行/linuxrc,加载驱动,再准备一些别的;
2.内核挂载根;
3.内核switch到根。
然而如果不从机制和策略角度分析,image格式的initrd也是一种很好的方式。另外还有一个问题,那就是/linuxrc一定要加载驱动吗?既然它是一个用户态的程序,内核能限制它里面怎么做吗?Debian 3的initrd就是一个现成的好例子,在Debian 3中,其initrd是image格式的,其linuxrc如下:
#!/bin/sh
# $Id: linuxrc,v 1.11 2004/04/26 12:04:46 herbert Exp $
export PATH=/sbin:/bin
mount -nt proc proc proc
root=$(cat proc/sys/kernel/real-root-dev)
echo 256 > proc/sys/kernel/real-root-dev
mount -nt tmpfs tmpfs bin || mount -nt ramfs ramfs bin
echo $root > bin/root
并没有加载驱动,它首先挂载proc文件系统,然后往proc/sys/kernel/real-root-dev中写了一个256,基本这就完了!proc/sys/kernel/real-root-dev中保留的是一个kdev_t类型的数字:real_root_dev,它是一个设备号,记录着根设备的主次设备号,本来在handle_initrd中已经为该变量赋了值:
real_root_dev = new_encode_dev(ROOT_DEV);
而ROOT_DEV则是在prepare_namespace的最开始已经被赋值的,就是grub启动命令行中的root=XXX参数,可能是/dev/hda1之类的,在赋值的时候从/sys/block(需要sysfs的支持并且需要挂载sysfs系统)或者直接从参数本身得到信息,将一个字符串转化成成一个kdev_t的数字。然而/linuxrc将这个数字改成了256,也就是(1,0)这个设备号对。这就意味着,linuxrc调用完毕之后,内核将会加载(1,0)这个设备作为根,这是设备其实就是initrd本身!这发生在handle_initrd:
pid = kernel_thread(do_linuxrc, “/linuxrc”, SIGCHLD);
if (pid > 0) {
while (pid != sys_wait4(-1, &i, 0, NULL))
yield();
}
…
ROOT_DEV = new_decode_dev(real_root_dev);
mount_root();可见ROOT_DEV根据real_root_dev被重新赋了值,real_root_dev已经被linuxrc改了,因此mount_root的时候仍然mount的是initrd,也就是/dev/ram0,因此在init函数的最后,执行的仍然是initrd中的/sbin/init函数,当我们挂载这个initrd的时候,发现其中有一个sbin/init,该init是一个脚本,负责加载驱动,然后挂载真正的根文件系统,procfs中的根也就是变量real_root_dev由于已经被linuxrc重写了,因此在initrd的sbin/init中就必须从/proc/cmdline中重新解析出真正的根了。
我不明白为何Debian 3如此设计initrd,是为了兼容还是别的什么原因,希望有人能告诉我。直接用cpio不更好吗?或者说,即使使用了image格式的initrd,为何不在linuxrc中将该做的都做完呢,而如此拐来拐去的又意义何在呢?这些都是问题!
附:
1.内核启动参数
在grub中,我们可以在kernel后面跟上启动参数,比如console=x,root=x,init=x之类的,这些参数是如何被initrd接收的呢?实际上grub会把这些参数放到一个内存区段中,然后linux内核起来的时候,从该区段中将参数读到一个全局变量中:
movl $boot_params,%edi
movl $(PARAM_SIZE/4),%ecx
cld
rep
并且procfs中有这个变量的访问接口/proc/cmdline,initrd只需要挂载procfs就可以得到内核的启动参数了。
2.内核模块参数
两种方式:
之一:内核模块参数的名称和设置值的函数被编译到一个特定的“段”中,然后sys_init_module在调用模块的init函数之前会parse_args,这个过程和内核启动时在start_kernel中解析启动参数时所做的一样;或者在parse参数的时候仅仅将参数放到一个位置,模块的init函数中自己解析参数;
之二:内核模块将参数名称和设置函数编译到内核参数一样的内存段中,完全使用内核参数解析的方式解析模块参数。
为了节省内存,一般编译进内核的且在启动过程中至关重要不可缺失的参数采用__setup的形式将参数搞到init.setup段,在parse_early_param的时候解析,内核启动完毕后即释放,而动态加载的模块或者编译到内核但是并不是不可缺失的参数编译到__param段,这可以看链接脚本(所述的是2.6.32版本):/arch/x86/kernel/vmlinux.lds
3.定制内核启动参数
如果你在内核启动参数中写一个aaa=bbb也没什么,当系统起来以后aaa=bbb可以从/proc/cmdline被取出,你自己解释就可以了,另外你也可以设置aaa=ccc,然后在initrd的linuxrc或者sbin/init或者/init中解释它为root=/dev/hda1或者别的什么!
4.initrd的做法
之一:image格式的initrd做法
此处略,很多资料都演示了这种做法
之二:cpio格式的initrd做法
需要说明的是,很多资料都直接引用内核Documentation目录下的initrd.txt中的做法,然而却不知何故修改了cpio命令的参数,原始的做法是:find . | cpio –quiet -H newc -o | gzip -9 -n > /boot/imagefile.img
而被修改过的却是:
bash# find . | cpio -c -o > ../initrd.img
bash# gzip ../initrd.img
不知何故,用此法做出的initrd无法使用。我推荐的是使用内核源码下的usr/gen_init_cpio工具来制作,也仅仅需要两步骤:
一:先生成文件列表,这个需要首先准备好initrd的目录,该目录下存在init程序,lib/modules目录,etc目录等运行时目录,然后使用内核源码的scripts/gen_initramfs_list.sh脚本生成文件列表;
二:使用gen_init_cpio工具针对生成的文件列表进行制作,一步即可。