Linux代码版本:linux4.4
导读:前些天拿到供应商的一块arm64开发板,需要对其新CPU进行测试评估。需要将公司自己的系统移植上去测试一些参数。在挂载公司的cpio包的时候,出现解压失败。之前对于根文件系统的挂载都是一些零零散散的学习,按部就班也没出过啥问题,所以突然遇到问题不知道从哪下手,也刚好趁此机会系统的整理一次。最好的老师还是源代码,只有对源代码熟悉,才能知道该如何定位。由于公司用的是嵌入式设备,使用的是基于ram的cpio格式的文件系统,所以重点分析initrd和initramfs两种跟文件系统的挂载过程,顺便也带上其他类型的根文件系统挂载。
一、基本概念小结
文件系统:文件系统是操作系统用于明确存储设备(常见的是磁盘,也有基于NAND Flash的固态硬盘)或分区上的文件的方法和数据结构;即在存储设备上组织文件的方法(引自百度百科)。这句话已经解释的很清楚了,在存储设备上组织文件的方法。你在电脑硬盘上看到的文件夹、文件、目录层次都是文件系统的功劳,就是一种特定的组织文件的方法,然后提供一组标准的接口让开发者可轻易读写,增删和改变权限。假如没有文件系统,对于一个有1000个block的存储器,最开始开发者可以规划,A文件放在前100个block,其他文件依次排序,如果存储器换台计算机换个开发者呢,鬼知道你是如何划分的,也就是说毫无通用性可言,对于任意删除一个文件后产生的磁盘碎片,更是无法利用。这个时候你可能会想,干脆在存储器的头部留一定空间,里面写好有哪些文件,分别放在哪个位置。对于磁盘碎片,干脆用像内存管理一样,用链表串起来,这样就可以再利用部分零碎空间。这个时候你又发现,有些文件不能随便往里写,还需要加上一些标志,标明哪些可写哪些只读。然后呢,发现为了管理好文件,面临的问题原来越多,最后你花了大量时间终于解决了所有问题,自己完成了一个文件系统。windows上我们最常见的两种文件系统NTFS和FAT,Linux常用的Ext4。甚至对于不同的存储介质,也有不同的文件系统与其对应,以更好的利用硬件特性达到最好的性能。
linux 根文件系统:从“根”这个字上就可以看出,这是linux系统最根本,最底层的文件系统,其他文件系统只能在跟文件系统之下挂载,在Linux下的目录就是 "/",“cd /” 就是切换倒了根文件系统下,ls 就就可以看到所有的文件夹和文件都在 "/" 下面,没有比“根”更靠前的了。就像一棵大树,所有的树干、枝叶和花果都建立在树根的基础上。
initramfs/initrd:全称是 initial RAM filesystem and RAM disk,百度这两个概念的话,其实是两种机制,多看几篇博客的话甚至越看越迷糊。在写这两个概念的时候,自己内心也是比较纠结的,也许是理解有误,暂时先将现在的理解记录下来,将来发现现在理解的是错误再纠正。在linux代码下make menuconfig-->general setup-->initial RAM filesystem and RAM disk,即是支持这两种方式的。在我们PC上面装linux系统,是比较灵活的,根文件系统可能在硬盘上,也可能在USB设备上,也可能在光驱设备上,在Linux挂载跟文件系统时,往往没有这个设备驱动,这时就无法挂载根文件系统,启动自然就会失败。俗话说,没有加个中间层解决不了的问题,initrd机制就是这加的这个中间层。先做一个临时的根文件系统,启动过程中先挂载这个临时的跟文件系统,然后执行里面的一些脚本,加载驱动等等工作(具体没研究都做哪些工作),然后再挂载真正的根文件系统。在嵌入式设备上,通常都是作为真正的根文件系统使用,毕竟嵌入式设备是定制的,不需要考虑太多通用的情况。
initrd支持两种格式,image-initrd和cpio-initrd。image-initrd是将一块内存当作硬盘,然后在上面载入文件系统。cpio-initrd和下面的initramfs一块说。
initramfs是一个基于ram的文件系统,只支持cpio格式。image-initrd 是将一块内存当作硬盘,然后需要写入文件系统,挂载到linux的时候还需要创建cache、filelist等等,浪费内存,并且大小固定,不灵活。initramfs则直接是基于RAM的文件系统,直接解压就行,各种cache、filelist已经在cpio包里的了。initramfs和cpio-initrd同样都是cpio格式,initramfs是填上根文件路径编译到内核内部的,跟内核链接到一起的,链接后在__initramfs_start,__initramfs_size,代码里找不到这两个变量,因为是在vmlinux.lds链接脚本里定义的,通过调用scripts/gen_initramfs_list.sh 生成cpio包。这样每次修改根文件系统里的文件都需要重新编译内核。
如果选择支持initramfs并且没有填写initramfs source file(s) 选项,会执行gen_initramfs_list.sh中的default_initramfs生成一个默认的cpio包。
而cpio-initrd则是单独制作cpio包,通过设备树dts里的chosen节点的bootargs中的linux,initrd-start和linux,initrd-end指定其地址范围。也就是说需要boot loader拷贝到内存里,然后通过参数传递给内核,这样根文件系统的修改不用每次编译内核了。
二、根文件系统挂载过程
1. 下面开始分析代码,只挑出挂载根文件系统相关的部分。linux内核启动的第一阶段主要是解压内核,初始化环境等等,真正的启动是从start_kernel开始的,下面只列出挂载跟文件系统的部分
setup_machine_fdt(__fdt_pointer);
{
/*转换成虚拟地址,并做合法性检查,如地址对齐等*/
void *dt_virt = fixmap_remap_fdt(dt_phys);
if (!dt_virt || !
early_init_dt_scan(dt_virt)) {
{
bool status;
/*device tree合法性检查,并计算crc32*/
status = early_init_dt_verify(params);
if (!status)
return false;
/*扫描节点*/
early_init_dt_scan_nodes();
{
/* Retrieve various information from the /chosen node */
/*检索chosen节点,原来要传递给内核的参数
现在变成device tree了 ,可数据还是要传啊,就放在了chosen节点*/
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
{
int l;
const char *p;
pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname);
if (depth != 1 || !data ||
(strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
return 0;
early_init_dt_check_for_initrd(node);
{
u64 start, end;
int len;
const __be32 *prop;
pr_debug("Looking for initrd properties... ");
/*查找initrd的 start地址*/
prop = of_get_flat_dt_prop(node, "linux,initrd-start", &len);
if (!prop)
return;
start = of_read_number(prop, len/4);
/*查找initrd的 end 地址*/
prop = of_get_flat_dt_prop(node, "linux,initrd-end", &len);
if (!prop)
return;
end = of_read_number(prop, len/4);
/*从device tree中解析出initrd的start和end地址后,转换为虚拟地址分别
赋值给相应的变量,后面会用到*/
initrd_start = (unsigned long)__va(start);
initrd_end = (unsigned long)__va(end);
initrd_below_start_ok = 1;
pr_debug("initrd_start=0x%llx initrd_end=0x%llx\n",
(unsigned long long)start, (unsigned long long)end);
}
/* Retrieve command line */
/*查找bootargs并赋值给 boot_command_line ,后面还要从bootargs解析传递的init进程*/
p = of_get_flat_dt_prop(node, "bootargs", &l);
if (p != NULL && l > 0)
strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));
/*
* CONFIG_CMDLINE is meant to be a default in case nothing else
* managed to set the command line, unless CONFIG_CMDLINE_FORCE
* is set in which case we override whatever was found earlier.
*/
#ifdef CONFIG_CMDLINE
#ifndef CONFIG_CMDLINE_FORCE
if (!((char *)data)[0])
#endif
strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
#endif /* CONFIG_CMDLINE */
pr_debug("Command line is: %s\n", (char*)data);
/* break now */
return 1;
}
/* Initialize {size,address}-cells info */
of_scan_flat_dt(early_init_dt_scan_root, NULL);
/* Setup memory, calling early_init_dt_add_memory_arch */
of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}
return true;
}
pr_crit("\n"
"Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n"
"The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
"\nPlease check your bootloader.",
&dt_phys, dt_virt);
while (true)
cpu_relax();
}
dump_stack_set_arch_desc("%s (DT)", of_flat_dt_get_machine_name());
}
上面的分析setup_machine_fdt中得到两个重点:
a. 从chosen节点中解析到 initrd_start 和 initrd_end ,后面解压initrd的时候会用到。
b. 从chosen节点中解析到 bootargs ,后面还会解析 bootargs 参数。
下面就是关注解析bootargs中的 三个参数,看着是不是很熟悉,就是bootloader传递给kernel的参数,解析后放在一个全局变量里。
static int __init init_setup(char *str)
{
unsigned int i;
execute_command = str;
/*
* In case LILO is going to boot us with default command line,
* it prepends "auto" before the whole cmdline which makes
* the shell think it should execute a script with such name.
* So we ignore all arguments entered _before_ init=... [MJ]
*/
for (i = 1; i < MAX_INIT_ARGS; i++)
argv_init[i] = NULL;
return 1;
}
__setup("init=", init_setup);
static int __init rdinit_setup(char *str)
{
unsigned int i;
ramdisk_execute_command = str;
/* See "auto" comment in init_setup */
for (i = 1; i < MAX_INIT_ARGS; i++)
argv_init[i] = NULL;
return 1;
}
__setup("rdinit=", rdinit_setup);
static int __init root_dev_setup(char *line)
{
strlcpy(saved_root_name, line, sizeof(saved_root_name));
return 1;
}
__setup("root=", root_dev_setup);
linux源代码里面大量类似于__setup这类宏定义的写法,将一类函数放到一个具体的代码段里面,然后再执行该代码段里函数。上面的函数通过 start_kernel-->parse_args-->unknown_bootoption-->obsolete_checksetup 调用到。
2. 上面的分析主要是对传递给kernel参数的解析,下面就要开始根文件系统挂载相关的分析,上面列出来的参数会被用到。
start_kernel-->vfs_caches_init-->mnt_init
void __init mnt_init(void)
{
unsigned u;
int err;
.....
/*初始化sysfs文件系统,最终会挂载到/sys/目录下*/
err = sysfs_init();
if (err)
printk(KERN_WARNING "%s: sysfs_init error: %d\n",
__func__, err);
/*在/sys/下创建fs目录*/
fs_kobj = kobject_create_and_add("fs", NULL);
if (!fs_kobj)
printk(KERN_WARNING "%s: kobj create error\n", __func__);
init_rootfs();
{
/*注册rootfs类型的文件系统,注意rootfs是一种文件系统的名字*/
int err = register_filesystem(&rootfs_fs_type);
if (err)
return err;
/*注册ramfs/tmpfs文件系统类型,都是基于ram的文件系统,tmpfs是ramfs的一个变种,
rootfs也是 ramfs/tmpfs的一个特殊例子*/
if (IS_ENABLED(CONFIG_TMPFS) && !saved_root_name[0] &&
(!root_fs_names || strstr(root_fs_names, "tmpfs"))) {
err = shmem_init();
is_tmpfs = true;
} else {
err = init_ramfs_fs();
}
if (err)
unregister_filesystem(&rootfs_fs_type);
return err;
}
init_mount_tree();
{
struct vfsmount *mnt;
struct mnt_namespace *ns;
struct path root;
struct file_system_type *type;
/*查找rootfs的类型定义结构体*/
type = get_fs_type("rootfs");
if (!type)
panic("Can't find rootfs type");
/*将roots挂载到vfs中,这个第一个挂载到vfs的文件系统,
所以也是根文件系统,其他文件系统只能挂载到rootfs下面*/
mnt = vfs_kern_mount(type, 0, "rootfs", NULL);
put_filesystem(type);
if (IS_ERR(mnt))
panic("Can't create rootfs");
/*创建namespace即命名空间*/
ns = create_mnt_ns(mnt);
if (IS_ERR(ns))
panic("Can't allocate initial namespace");
init_task.nsproxy->mnt_ns = ns;
get_mnt_ns(ns);
/*初始化跟文件系统的路径*/
root.mnt = mnt;
root.dentry = mnt->mnt_root;
mnt->mnt_flags |= MNT_LOCKED;
/*将当前进程工作路径和根文件系统的路径设置为刚挂载的rootfs*/
set_fs_pwd(current->fs, &root);
set_fs_root(current->fs, &root);
}
}
从上面的代码可以看出,rootfs是作为根文件系统挂载的,并将进程的当前路径和根路径 切换到rootfs,后面其他文件系统只能挂载到rootfs下面。
3. 通过上面的代码,“/”已经有了,可是什么里面什么东西都没有啊,挂载“/”才是第一步,下面继续分析
start_kernel-->rest_init-->kernel_init-->kernel_init_freeable-->do_basic_setup-->do_initcalls-->populate_rootfs
static int __init populate_rootfs(void)
{
/*解压initramfs到rootfs,就是链接到内核的cpio,解压失败则尝试initd方式*/
char *err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
if (err)
panic("%s", err); /* Failed to decompress INTERNAL initramfs */
if (initrd_start) {
#ifdef CONFIG_BLK_DEV_RAM
int fd;
/*从下面的printk可以看出,cpio-initrd和initramfs是一回事,只是传递方式不一样,
initramfs是链接到内核,而cpio-initrd是通过uboot传递*/
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();
goto done;
} else {
clean_rootfs();
unpack_to_rootfs(__initramfs_start, __initramfs_size);
}
/*如果不是initramfs的格式,则按照image-initrd的方式处理,对image-initrd完全没有了解,
嵌入式设备上也没见过,所以也不分析*/
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) {
ssize_t written = xwrite(fd, (char *)initrd_start,
initrd_end - initrd_start);
if (written != initrd_end - initrd_start)
pr_err("/initrd.image: incomplete write (%zd != %ld)\n",
written, initrd_end - initrd_start);
sys_close(fd);
free_initrd();
}
done:
#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
/*
* Try loading default modules from initramfs. This gives
* us a chance to load before device_initcalls.
*/
/*加载默认的模块,也就是bootargs中的"elevator="部分*/
load_default_modules();
}
return 0;
}
从上上面的代码可以看出,cpio-initrd和initramfs其实是一回事,只是传递给内核的方式不同,initramfs是通过链接到内核方式,而cpio-initrd是通过Uboot的bootargs传递给内核,解压函数都是 unpack_to_rootfs 。可直接解压到rootfs,猜测(未具体研究)cpio包本身就是基于rootfs类型,就像将基于ext4的文件目录制作成imge格式烧写到磁盘上。同时也发现,解压成功后,initramfs_start和initramfs_end 之间的空间并没有被释放,而initrd_start和init_end之间的地址空间会被释放(如果配置了retain_initrd 就不会被释放了)。
4. 上面解压完 initrd/initramfs 后就该执行剩余的部分,调用init进程
start_kernel-->rest_init-->kernel_init-->kernel_init_freeable
static noinline void __init kernel_init_freeable(void)
{
......
do_basic_setup();
/* Open the /dev/console on the rootfs, this should never fail */
/*打开/dev/console ,还不能失败,还记得上面 gen_initramfs_list.sh 里面的default_initramfs,会创建该文件,
如果没有配置initramfs,真正的根文件系统还没挂载怎么办,系统会调用 default_rootfs 函数,创建该文件*/
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
pr_err("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
*/
/*如果bootargs没有指定rdinit=,默认启动进程为/init*/
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
/*判断启动文件是否存在,如果使用initrd/initramfs最为最终根文件系统,肯定存在启动进程的,如果不存在
则会继续执行,下面也进行分析*/
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
{
int is_floppy;
/*如过配置的延时,则等待*/
if (root_delay) {
printk(KERN_INFO "Waiting %d sec 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.
*/
/*等待设备probe过程完成*/
wait_for_device_probe();
md_run_setup();
/*判断指定的root=前3个自己是否是mtd或ubi,如果是则挂载到根文件系统*/
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;
}
/*比较前5个字节是否为"/dev/",嵌入式设备也常将flash上的 文件系统挂载为根文件系统,
通常形如"root=/dev/mtdblock2、"root=/dev/mmcblk0p2"*/
ROOT_DEV = name_to_dev_t(root_device_name);
if (strncmp(root_device_name, "/dev/", 5) == 0)
root_device_name += 5;
}
/*image-initrd的 情况,不做分析*/
if (initrd_load())
goto out;
/* wait for any asynchronous scanning to complete */
/*等待异步的scan完成*/
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();
{
/*网络文件系统nfs的情况*/
#ifdef CONFIG_ROOT_NFS
if (ROOT_DEV == Root_NFS) {
if (mount_nfs_root())
return;
printk(KERN_ERR "VFS: Unable to mount root fs via NFS, trying floppy.\n");
ROOT_DEV = Root_FD0;
}
#endif
#ifdef CONFIG_BLK_DEV_FD
if (MAJOR(ROOT_DEV) == FLOPPY_MAJOR) {
/* rd_doload is 2 for a dual initrd/ramload setup */
if (rd_doload==2) {
if (rd_load_disk(1)) {
ROOT_DEV = Root_RAM1;
root_device_name = NULL;
}
} else
change_floppy("root floppy");
}
#endif
/*跟文件 系统在块设备(flash、mmc等)的情况*/
#ifdef CONFIG_BLOCK
{
/*创建"/dev/root"设备,上面有 ROOT_DEV 赋值*/
int err = create_dev("/dev/root", ROOT_DEV);
/*将"/dev/root"挂载到/root目录*/
if (err < 0)
pr_emerg("Failed to create /dev/root: %d\n", err);
mount_block_root("/dev/root", root_mountflags);
{
struct page *page = alloc_page(GFP_KERNEL |
__GFP_NOTRACK_FALSE_POSITIVE);
char *fs_names = page_address(page);
char *p;
#ifdef CONFIG_BLOCK
char b[BDEVNAME_SIZE];
#else
const char *b = name;
#endif
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);
{
struct super_block *s;
/*将/dev/root/挂载到/root目录*/
int err = sys_mount(name, "/root", fs, flags, data);
if (err)
return err;
/*将当前目录切换到/root*/
sys_chdir("/root");
s = current->fs->pwd.dentry->d_sb;
ROOT_DEV = s->s_dev;
printk(KERN_INFO
"VFS: Mounted root (%s filesystem)%s on device %u:%u.\n",
s->s_type->name,
s->s_flags & MS_RDONLY ? " readonly" : "",
MAJOR(ROOT_DEV), MINOR(ROOT_DEV));
return 0;
}
switch (err) {
case 0:
goto out;
case -EACCES:
case -EINVAL:
continue;
}
/*
......
}
}
#endif
}
/*挂载devtmpfs到/dev/目录*/
out:
devtmpfs_mount("dev");
/*将当前目录(上面设置的/root)挂载到 / 目录 */
sys_mount(".", "/", NULL, MS_MOVE, NULL);
/*将当前进程的根目录切换成当前目录*/
sys_chroot(".");
}
}
/*
* 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..
*
* rootfs is available now, try loading the public keys
* and default modules
*/
integrity_load_keys();
load_default_modules();
}
总结上面的流程,在 initrd/intramfs 解压后,准备启动init进程,如果init进程不存在,就认为真正的根文件系统还未挂载,继续尝试挂载根文件系统,对于嵌入式常用的"root=/dev/mtdblock2、"root=/dev/mmcblk0p2",先创建/dev/root,然后挂载到/root目录,再然后挂载/目录下。这下,不管用的是initrd/initramfs还是nfs或者flash设备上文件 系统,都已经完成了挂载,下一步就是执行init进程了
static int __ref kernel_init(void *unused)
{
int ret;
kernel_init_freeable();
/* need to finish all async __init code before freeing the memory */
async_synchronize_full();
free_initmem();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
flush_delayed_fput();
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
上面的代码很明显,先执行rdinit=xx,如果不存在,则按照/sbin/init、/etc/init、/bin/init和/bin/sh的过程执行,都不存在那肯定是启动不了。
三、总结
linux对根文件系统的挂载,还是比较灵活的,考虑了各种应用场景,对于要求性能的设备,可以选择initrd/initramfs,毕竟ram的读写性能不是其他存储介质能比的;调试过程中也可以使用nfs来提高效率;也可以将initrd/initramfs作为临时的根文件系统做一些必要的动作后再挂载最终的根文件系统。