Linux的INITRAMFS 与 INITRD

1.INITRAMFS和INITRD(INITRAMDISK)是什么?

RAMFS和RAMDISK都是内存文件系统,他们有着小巧快速的特点。INIT前缀表示其包含有效“init”可执行文件,可以作为启动的root文件系统。INITRAMDISK出现较早,在使用norflash和2.4kernel盛行的时期很流行。RAMFS出现也很早,但INITRAMFS是2.6版本kernel添加的新功能,INITRAMFS更象是INITRAMDISK的一个简化版本。如今嵌入式设备大量使用nandflash,像INITRAMFS和INITRAMDISK已经不适合用来管理nand这种大容量并且需要校验机制的存储设备。

但,在某些场合,比如:

l你需要做一个很简单快速的镜像文件

l和Kernel编译出一个镜像文件

l与板载nand文件系统完全无关的镜像文件

在这些情况下,INITRAMFS和INITRAMDISK都是不错的选择。

INITRAMFS和INITRAMDISK有着相似的制作流程:

l通过某种压缩算法,把目标root目录压缩成一个文件

l再把压缩文件存储在某个位置,或者直接链接到kernel镜像中

INITRAMFS和INITRAMDISK在Kernel的启动过程中也有着相似的加载流程:

l通过CommandLine找到压缩的文件(initramfs与CommandLine无关)

l把压缩文件解压到内存中,并以指定的文件系统类型加载(mount)

l如果加载的rootfs文件系统中有init文件,Linux启动进程就能直接跳转到init进程,然后便有了开机启动init脚本的执行,以及最终的shellconsole。

l写入内存文件系统的更改,在重启系统后都不复存在,这是内存文件系统的特点。

INITRAMFS和INITRAMDISK之间也有区别。

ramdisk文件是一个块设备文件,文件系统一般为ext2。ramdisk加载后,从文件系统中读取数据,数据流依次通过了Kernel的“块设备管理读写“->”ext2文件系统“->"VFS"->最后传到用户空间。

而ramfs没有ext2文件系统,没有块设备处理,数据的终点就是“VFSCache”(内存),同样一个读取流程,数据依次通过"VFSCache(ramfs)”->VFS->用户。

由此可以看出initramfs比initrd更简单快速,而Kernel对initramfs的内部处理也更为简单上面提到的"VFSCache"在Kernel中,通常以一个加载的rootfs文件系统来管理,此文件系统的类型为RAMFS(实际名称为ROOTFS)。INITRAMFS的处理过程把initramfs文件直接解压到此文件系统中,然后再将此rootfs文件系统直接呈给用户空间(执行用户init进程)。INITRAMDISK的解压加载过程同样要用到这个rootfs,它会把initramdisk文件解压到此文件系统中的'/dev/ram'块设备里,然后再加载'/dev/ram'块设备。

2.Linux2.6.28对INITRAMFS的处理

以Samsung6410测试平台(Kernel2.6.28)为例,来体验initramfs制作、编译、kernel解析、加载、用户init调度的整个过程。

2.1Initramfs文件的制作、编译过程

首先查看kernel/.config,这里没有看到对INITRAMFS功能的开关。这跟Kernel文档描述一致,因为Kernel处理INITRAMFS的负荷极小,所以Kernel始终包含处理INITRAMFS的功能。通过此文章后续的说明,你可以看到实际上Kernel一直包含一个RAMFS的rootfs,当此rootfs无效(为空或者没有有效的init文件),此rootfs就会被后续command-line所指定的文件系统所覆盖。

在Kernelmenuconfig中,唯一需要指定的是‘CONFIG_INITRAMFS_SOURCE’,这里需要指定用于制作initramfs文件的目标root目录的绝对路径。

然后编译(cdKernel;makeV=1),以下是相关的编译日志:

'kernel/scripts/gen_initramfs_list.sh-ousr/initramfs_data.cpio.gz-u0-g0/XXX/root

'arm-none-linux-gnueabi-gcc-c-ousr/initramfs_data.ousr/initramfs_data.S'#initramfs_data.S:Include'.gz'asaglobalsection'init.ramfs'

'arm-none-linux-gnueabi-ld-EL-r-ousr/built-in.ousr/initramfs_data.o'

'arm-none-linux-gnueabi-ld-EL-r-ovmlinux.o-Tarch/arm/kernel/vmlinux.lds...usr/built-in.o...'

以下是脚本“vmlinux.lds”中的相关内容:

.init:{/*Initcodeanddata*/

*(.init.text)*(.cpuinit.text)*(.meminit.text)

...

__initramfs_start=.;

usr/built-in.o(.init.ramfs)

__initramfs_end=.;

...

}

编译过程可以归纳成三步:

第一步,gen_initramfs_list.sh脚本以cpio格式对目标root目录压缩生成XXX.cpio.gz文件,这里为什么使用cpio而不是tar,因为简单,这一点内核文档有说明。

第二步,initramfs_data.S只做一件事情,创建一个Section,包含XXX.cpio.gz文件。

第三步,编译并链接,‘arch/arm/kernel/vmlinux.lds’中,在initsection中声明了两个全局变量“__initramfs_start”和"__initramfs_end"用于指向init.ramfs域的开始和结束。实际上,__initramfs_start就是内核空间全局的指针变量,指定XXX.cpio.gz开始地址,__initramfs_end指定结束地址。当然这两个全局变量只在init进程中有效。

编译过程完成,最终形成一个镜像文件zImage,在这个文件中,__initramfs_start和initramfs_end两个变量分别指向XXX.cpio.gz数据的起始和结束地址(结束地址+1)。

2.2Kernel加载initramfs的过程

initramfs文件生效的过程大致分为四步:

第一步:Kernel首先要注册一个RAMFS文件系统类型(实际注册的类型名称是"ROOTFS",后续我们可以看到它实际上就是"RAMFS");

第二步:然后加载(mount)一个空的rootfs文件系统,类型就是上面提到的RAMFS(ROOTFS);

第三步:寻址initramfs文件“XXX.cpio.gz”并解压到已mount的rootfs文件系统中;

第四步:寻址用户空间的init,并执行init进程;

第一步和第二步的调用堆栈如下:

init/main.c:start_kernel()->fs/dcache.c:vfs_caches_init()->

fs/namespace.c:mnt_init(){

...

//Thelast2lines

init_rootfs();

init_mount_tree();

}

在“fs/ramfs/inode.c:init_rootfs()”函数中,注册了"ROOTFS"文件系统类型。通过阅读inode.c源码,发现"ROOTFS"就是"RAMFS",几乎没有区别。

在“fs/namespace.c:init_mount_tree()”函数中,加载了一个类型为ROOTFS的空root:

mnt=do_kern_mount("rootfs",0,"rootfs",NULL);

//第一个rootfs引用文件系统类型ROOTFS,即RAMFS。第二个rootfs为mountentry。

第三步(寻址initramfs文件XXX.cpio.gz,并解压)的调用堆栈如下:

kernel/init/main.c:start_kernel(){

...

vfs_caches_init();

...

//Thelastline

rest_init();

}

kernel/init/main.c:rest_init(){

kernel_thread(kernel_init,NULL,CLONE_FS|CLONE_SIGHAND);

...

}

kernel/init/main.c:kernel_init(){

...

do_basic_setup();

...

}

kernel/init/main.c:rest_init(){

...

do_initcalls();

}

//OK

kernel/init/initramfs.c:populate_rootfs(){

//Decompressthe.gzinto'rootfs'

}

rootfs_initcall(populate_rootfs);

检查populate_rootfs做了什么,它通过__initramfs_start指针和__initramfs_end指针访问XXX.cpio.gz文件,调用函数unpack_to_rootfs函数把源文件解压到rootfs中。gunzip执行解压过程,do_name()/do_copy/do_symlink/do_XXX为解压过程回调函数,在这些do_XXX回调函数中,看到大量使用sys_open/sys_write/sys_XXX函数创建、读写、更改文件和目录及其权限。sys_open/sys_write/sys_read实际上是用户空间open/write/read函数系统调用的实现。

至此,一个完整的rootfs已在目标系统的内存中。

第四步(寻址并运行用户空间INIT进程)的调用堆栈:

init/main.c:kernel_init(){

//Atthebottomlines

if(!ramdisk_execute_command)

ramdisk_execute_command="/init";

}

kernel/init/main.c:init_post(){

if(ramdisk_execute_command){

run_init_process(ramdisk_execute_command);

}

}

kernel/init/main.c:run_init_process(){

kernel_execve(init_filename,argv_init,envp_init);

}

也就是说,只要initramfs文件“XXX.cpio.gz”的根目录下有一个有效的init,或者一个对init有效的链接,Kernel就能启动这个rootfs。那假如说initramfs的rootfs为空(CONFIG_INITRAMFS_SOURCE没有配置或者指向不存在的位置),或者initramfs的rootfs没有有效的“/init”文件。则Kernel会解析commandline,尝试从MTD/UBI之类的NAND分区启动,或者启动INITRAMDISK,或者NFS启动(如果开启了此功能),其调用堆栈如下:

kernel/init/main.c:kernel_init(){

//...

if(!ramdisk_execute_command)

ramdisk_execute_command="/init";

if(sys_access((constchar__user*)ramdisk_execute_command,0)!=0){

ramdisk_execute_command=NULL;

prepare_namespace();

}

//...

}

kernel/init/do_mounts.c:prepare_namespace(){

//...

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

gotoout;

}

ROOT_DEV=name_to_dev_t(root_device_name);

if(strncmp(root_device_name,"/dev/",5)==0)

root_device_name+=5;

}

if(initrd_load())

gotoout;

//...

mount_root(){

//...

mount_nfs_root();

//...

}

//...

}

再来总结一下INITRAMFS的特点:

lROOTFS和RAMFS文件系统在同一个源码文件“inode.c”中实现,他们基本一致,通过Kernel文档也能说明这一点。同时,这也应证了配置Kernel的时候为什么只需要配置'CONFIG_INITRAMFS_SOURCE'一个选项足也。

lROOTFS文件系统类型的注册,以及rootfs的加载,initramfs的解压加载过程,都是Kernel启动过程的默认行为,menuconfig中没有任何选项可以开关。

l实际上rootfs不仅为INITRAMFS提供服务,它还为INITRAMDISK的加载提供中转服务,这一过程后面会讲到。

linitramfs默认启动“/init”,请确保在那个位置有一个有效的init或者是链接。

linitramfs的处理总是优先于对commandLine中启动参数的处理,因此initramfs和commandline无关。当最终的zImage包含了一个有效的XXX.cpio.gz时,不管bootloader传给kernel什么样的commandline,Kernel都会从initramfs启动,比如:

bootargs=root=/dev/mtdblock5rootfstype=yaffs2init=/initconsole=ttySAC0,115200

//即使当前mtdblock5有一个完整的yaffs2文件系统并且其'/init'也有效'.当前Kernel也会尝试优先启动initramfs。

bootargs=console=ttySAC0,115200

//即使没有指定root/rootfstype/init选项,当前kernel也会尝试自带的initramfs。

l当initramfs没有有效的root时,Kernel才会根据commandline的配置,尝试从mtd/ubi等NAND分区,或者是从ramdisk设备文件,或者是从nfs服务器加载root。

3.Kernel2.4armnommu平台对INITRAMDISK的处理

在提到INITRAMFS时,提到了ROOTFS类型的文件系统,以及用此类型的文件系统加载(mount)了一个空的rootfs。此时的rootfs是VFScache的实现,不对应任何后端设备。rootfs是INITRAMFS的数据终点,但对于INITRAMDISK来说,当前rootfs只是一个中转站。Kernel会在此对INITRAMDISK的gz文件进行解压,然后再判定解压后的文件系统类型并加载,加载完成后,还要对initrd使用过的内存(VFScache)进行释放。

早期的uCLinux(Linux2.4.xx)支持armnommu平台,此平台没有内存管理单元,它处理initrd的方法与当前2.6的kernel有区别。以Linux-2.4.22的armnommu平台为例,它支持三种ramdisk文件的寻址方法:

方法一,就像2.6的kernel处理initramfs一样,当前kernel使用__ramdisk_data指针指向ramdisk压缩文件的开头,__ramdisk_data_end指向其结尾。

方法二,通过menuconfig配置选项'CONFIG_SD_INITRD_START'和'CONFIG_SD_INITRD_SIZE'。

方法三,以BOOTP协议启动,initramdisk文件的位置可通过Makefile的环境变量配置,比如-DINITRD_PHYS=XXX。

所有方法寻址的结果,都是以"initrd_start"变量指向源文件的起始虚拟地址,"initrd_end"变量指向源文件的结束虚拟地址。后面两种方法不再此文章范围内讨论。

3.1通过__ramdisk_data指针寻址initramdisk文件的方法

方法一的调用堆栈:

【ramdisk文件的制作过程】

genext2fs-i607-b5896-dHost_Root_Directory-q-Ddevice_table.txtXXX.ext2

//'Host_Root_Directory'为主机root目录,'device_table.txt'为设备配置文件,'XXX.ext2'为生成的目标文件

gzip-cXXX.ext2>initrd.bin

//使用gzip压缩生成initrd.bin文件

arm-elf-ld-r-oinitrd.o-bbinaryinitrd.bin

//链接生成initrd.o文件

arm-elf-ld-p-X-Tarch/armnommu/vmlinux.ldsarch/armnommu/kernel/head-armv.oarch/armnommu/kernel/init_task.oinit/main.oinit/version.oinit/do_mounts.o\

--start-group\

arch/armnommu/kernel/kernel.oarch/armnommu/mm/mm.oarch/armnommu/mach-em86xx/em86xx.okernel/kernel.ommnommu/mmnommu.ofs/fs.oipc/ipc.o\

drivers/char/char.odrivers/block/block.odrivers/misc/misc.odrivers/net/net.odrivers/scsi/scsidrv.odrivers/pci/driver.odrivers/video/video.odrivers/usb/usbdrv.odrivers/media/media.o\

net/network.o\

arch/armnommu/lib/lib.alib/lib.atoolchain/lib/gcc-lib/arm-elf/2.95.3/libgcc.a\

--end-group\

-olinux

//与Kernel一起链接生成Linux镜像文件

vmlinux.lds内容:

__ramdisk_data=.;

initrd.o

__ramdisk_data_end=.;

【Kernel启动寻址initrd文件的过程】

kernel/arch/armnommu/kernel/setup.c:setup_arch(){

#ifdefCONFIG_BLK_DEV_INITRD

//letthekernelknowwheretheinitrdexists

#ifdefCONFIG_SD_INITRD_EMBED

{

externint__ramdisk_data,__ramdisk_data_end;

initrd_start=(unsignedlong)&__ramdisk_data;

initrd_end=(unsignedlong)&__ramdisk_data_end-(unsignedlong)&__ramdisk_data;

}

#else

initrd_start=CONFIG_SD_INITRD_START;

initrd_end=CONFIG_SD_INITRD_START+CONFIG_SD_INITRD_SIZE;

#endif

#endif

寻址的结果就是变量“initrd_start”和变量“initrd_end”,他们指向实际initrd文件的起始和结束地址,后面的加载过程将会用到这两个变量。

3.2Kernel加载initramdisk的过程

在前面讲到Kernel处理INITRAMFS时,首先加载的文件系统rootfs要为INITRAMDISK提供中转服务。数据中转服务要用到rootfs下的两个RAMDISK设备文件。

/dev/ram->Ramdisk文件解压的目标设备,可以被rootfs或者上层直接mount的ramdisk设备。

/dev/initrd->加载ramdisk文件的源设备,封装对ramdisk文件的读操作,所有对此文件的读操作,都会定位到对initrd_start指针的读操作。

[解压initrd的调用堆栈]

init/main.c:init()->init/do_mounts.c:prepare_namespace()->initrd_load()->handle_initrd()

init/do_mounts.c:initrd_load(){

#ifdefCONFIG_BLK_DEV_RAM

//...

create_dev("/dev/ram",MKDEV(RAMDISK_MAJOR,n),NULL);

//这里"/dev/ram"是initrd加载的目标设备

#endif

returnrd_load_image("/dev/initrd");

//这里"/dev/initrd"是initrd加载的源设备

}

init/do_mounts.c:rd_load_image(){

crd_load();//meanscompressedramdiskload

}

[为什么对设备“/dev/initrd”的读操作,就能定位到对initrd_start的访问]

drivers/block/rd.c:

rd_init(){

//...

#ifdefCONFIG_BLK_DEV_INITRD

/*Weoughttoseparateinitrdoperationshere*/

register_disk(NULL,MKDEV(MAJOR_NR,INITRD_MINOR),1,&rd_bd_op,rd_size<<1);

devfs_register(devfs_handle,"initrd",DEVFS_FL_DEFAULT,MAJOR_NR,

INITRD_MINOR,S_IFBLK|S_IRUSR,&rd_bd_op,NULL);

#endif

//...

}

//在随后的open函数中,会重定位read函数到以下函数:

initrd_read(){

//...

copy_to_user(buf,(char*)initrd_start+*ppos,count);

//...

}

[加载(mount)"/dev/ram"的过程]

前面提到的initrd_load()函数把initrd文件内容读到块设备"/dev/ram"中,此时的块设备“/dev/ram”内容,实际上就是制作过程中提到的XXX.ext2文件内容。

下面要做的就是mount块设备“/dev/ram”。

init/do_mounts.c:prepare_namespace(){

//...

sys_mkdir("/root",0700);

//当前文件系统是'rootfs',目录'/root'是后续mount的Entry

//...

//然后要判断commandline的‘root=’选项。

//如果'root=/dev/ramN'(这是正确的启动选项,N取值0-15,或者直接取值'/dev/ram'),

//则,直接加载root

mount_root();

//如果'root!=/dev/ramN'(commandline给了其他启动选项)

handle_initrd();

}

handle_initrd(){

//1.尝试加载initrd到rootfs的/old目录下,并执行其启动脚本'/linuxrc'

//2.尝试加载commandline指定的root设备文件

mount_root();

//3.在把initrd从/old移动到已经加载的文件系统的'/initrd'目录下。

}

mount_root(){

//...

create_dev("/dev/root",ROOT_DEV,root_device_name);

//在当前rootfs下创建设备文件root,指向当前的启动文件

mount_block_root("/dev/root",root_mountflags);

//把'/dev/root'Mount到'/root'

}

4.Linux2.6.28对INITRAMDISK的处理

2.6与2.4的Kernel处理INITRAMDISK有一些区别,表现在:

l寻址initramdisk文件的方式已变,2.6的kernel不再支持init域变量“__ramdisk_data”,转而采用commandLine传递ramdisk文件的物理地址和大小信息。

l加载initramdisk文件的方式已变,2.6的kernel把ramdisk文件内容直接写入rootfs中的文件“/initrd.image”,再由此文件写入块设备“/dev/ram”。2.6的Kernel不再使用块设备/dev/initrd封装对initramdisk文件的读取操作。

l2.6的Kernel加入了对initramfs的支持,Kernel在寻址initramdisk地址时,会自动判断当前地址究竟存放的是initramfs还是initramdisk,这种自适应可能会纠正用户的错误使用行为。

其他未提及的部分,比如ramdisk文件的假牙过程,用户INIT寻址过程,与2.4的Kernel一致。

4.1Kernel2.6寻址initramdisk的过程

Kernel2.6通bootloade传递的commandline参数寻址initramdisk内容,这需要bootloader事先读取initramdisk内容到物理地址中,然后在把物理地址和数据长度传递给kernel。

通常的commandline如下:

"console=ttySAC0,115200mem=16M@0xc0000000root=/dev/raminitrd=0xc0400000,4M"

其中选项有以下意义:

l"initrd=phy_addr,size",这是向kernel传递initrd内容的物理地址及长度。

l"mem=size@phy_addr",这是在向kernel传递bootmem保留区域的配置,bootmem是Kernel启动初期的内存分配机制,内存区域被保留即意味着告诉bootmem此区域已分配,勿做它用。这里要求当前配置区域完全覆盖initrd所配置区域,commandline中可同时存在多个"mem="选项以适应不同的应用。

l"root=/dev/ram",指定ramdisk设备,跟2.4Kernel一样。

[Kernel端寻址的堆栈调用]:

1.phys_initrd_start变量和phys_initrd_size变量的赋值:

arch/arm/mm/init.c:early_initrd(){

//解析commandline的"initrd="选项,并复制phys_initrd_start(_size)变量。

}

2.bootmem的配置:

arch/arm/kernel/setup.c:early_mem(){

//解析commandline的mem选项

//把当前mem装入meminfo全局数组中

arm_add_memory();

}

arch/arm/kernel/setup.c:setup_arch()->arch/arm/mm/mmu.c:paging_init()->bootmem_init()

arch/arm/mm/init.c:bootmem_init(){

//check_initrd(){

if(bank_phys_start(bank)<=phys_initrd_start&&

end<=bank_phys_end(bank))

initrd_node=bank->node;

//这里也说明了,为什么"mem="选项配置的内存区域,要完全覆盖"initrd="选项所配置的内存区域

//...

returninitrd_node;

}

for_each_node(node){

if(node==initrd_node){

bootmem_reserve_initrd(){

res=reserve_bootmem_node(pgdat,phys_initrd_start,

phys_initrd_size,BOOTMEM_EXCLUSIVE);

if(res==0){

initrd_start=__phys_to_virt(phys_initrd_start);

initrd_end=initrd_start+phys_initrd_size;

}

}

}

}

}

至此,同2.4的kernel一样,后续的加载initramdisk过程将使用虚拟地址initrd_start和initrd_end访问initramdisk数据。

4.2Kernel2.6通过initrd_start加载initramdisk的过程

Initramfs的解压过程:initrd_start->rootfs:/initrd.image

init/initramfs.c:populate_rootfs(){

if(initrd_start){

#ifdefCONFIG_BLK_DEV_RAM

//首先判断initrd_start指向的内容是否是initramfs

err=unpack_to_rootfs((char*)initrd_start,

initrd_end-initrd_start,1);

if(!err){

//是,则解压,过程与initramdisk无关。

unpack_to_rootfs((char*)initrd_start,

initrd_end-initrd_start,0);

free_initrd();

return0;

}

//否,把initramdisk内容写入rootfs中的文件"/initrd.image"

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

//...

#endif

}

}

initramfs的加载过程:rootfs:/initrd.image->rootfs:/dev/ram->mount

init/do_mounts.c:prepare_namespace(){

//...

initrd_load();

//...

}

init/do_mounts_initrd.c:initrd_load(){

//解压的目标块设备

create_dev("/dev/ram",Root_RAM0);

//解压的源文件,2.4Kernel是“/dev/initrd”块设备

rd_load_image("/initrd.image");

//如果ROOT_DEV不是"/dev/ramN"

handle_initrd(){

//内容与2.4Kernel一致,加载别的root并对initrd进行转储

}

}

init/do_mounts.c:mount_root(){

//...

//以下过程与2.4Kernel完全一致。

#ifdefCONFIG_BLOCK

//在rootfs下创建设备root,指向有效root设备。

create_dev("/dev/root",ROOT_DEV);

//加载(mount)

mount_block_root("/dev/root",root_mountflags);

#endif

}