Linux深入学习:如何分析根文件系统
扫描二维码
随时随地手机看文章
整体框架:
1)在Linux kernel的源代码中,对如何启动应用程序有着明确的定义。首先我们需要挂载根文件系统,只有正确挂载了根文件系统,才能够从根文件系统中读出应用程序。我们启动的第一个程序就是init程序。init进程完成了对应用程序的各项配置(进程ID、执行时机、命令、终端、下一个执行的进程等),并最终依据配置执行了应用程序。 2)要执行应用程序,首先进行配置。配置文件inittab里有着对应用程序的详细配置,这些都是C文件。init进程读出配置、分析配置并配置应用程序、配置C库(用到很多C库里的函数)。最后执行程序。 3)Busybox是一个遵循GPL v2协议的开源项目。Busybox将众多的UNIX命令集合进一个很小的可执行程序中,可以用来替换GNU fileutils、shellutils等工具集。Busybox中各种命令与相应的GNU工具相比,所能提供的选项较少,但是能够满足一般应用。Busybox为各种小型的或者嵌入式系统提供了一个比较完全的工具集。更多详细介绍参考README。
我们执行
命令
的时候实际是执行busybox 命令
我们查看软连接
内核检测根文件系统并启动init
内核启动的最后一步就是启动init进程,代码在init/main.c/init_post函数
static int noinline init_post(void) { free_initmem(); unlock_kernel(); mark_rodata_ro(); system_state = SYSTEM_RUNNING; numa_default_policy(); 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 %sn", ramdisk_execute_command); } /* * 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) { 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."); }
内核启动init进程的过程如下:
(1)打开标准输入、标准输出、标准错误设备open("/dev/console")
尝试打开/dev/console设备文件,如果成功即为init进程标准输入设备。(void) sys_dup(0); (void) sys_dup(0);
将文件描述符0复制给文件描述符1、2,所以标准输入、输出、错误都对应同一个文件(设备)
(2)如果execute_command
变量指定了要运行的程序,启动它。
if (execute_command) { run_init_process(execute_command); }
其中execute_command为命令行参数,在我们uboot传给内核的参数中,init设置了init=/linuxrc,所以这里的execute_command就等于/linuxrc。
如果传值成功则执行run_init_process,否则打印printk(KERN_WARNING “Failed to execute %s. Attempting “”defaults…n”, execute_command);
并接着往下执行,接着检测其他位置的init进程,若成功则执行,失败则接着往下检测,直到找到合适的init进程或者没找到则打印panic(“No init found. Try passing init= option to kernel.”);
那么这里我们可以先使用nand erase root
擦除root分区,也就是说擦除根文件系统,然后启动只有bootloader和kernel的系统。在结果是否和代码中说明的一致,结果Linux kernel在启动过程中,打印出了如下的信息:
VFS: Mounted root (yaffs filesystem). Freeing init memory: 140K Warning: unable to open an initial console. Failed to execute /linuxrc. Attempting defaults... Kernel panic - not syncing: No init found. Try passing init= option to kernel.
首先是VFS:挂载了根文件系统,可能大家会问,不是刚刚已经擦除了根文件系统,为什么说这里挂载了?
这是因为当我们擦除了根文件系统的root分区后,Linux kernel认为它是任意格式的根文件系统(其实分区里面什么都没有),而默认的又是yaffs格式,所以这里说挂载了yaffs格式的根文件系统。
这里的warning难道不是和我们init_post函数中的printk(KERN_WARNING “Warning: unable to open an initial console.n”);相对应吗?
同理,Failed to execute /linuxrc. Attempting defaults…和printk(KERN_WARNING “Failed to execute %s. Attempting “”defaults…n”, execute_command);相对应。
Kernel panic - not syncing: No init found. Try passing init= option to kernel.和panic(“No init found. Try passing init= option to kernel.”);相对应。
so=>这证明我们的分析是正确的。
Busybox init进程的启动过程
其中与构建根文件系统关系密切的是控制台的初始化、对inittab文件的解释执行。
内核启动init进程时已经打开“/dev/console”设备作为控制台,一般情况下Busybox init程序就使用/dev/console。
但是如果内核启动init进程的同时设置了环境变量CONSOLE或console,则使用环境变量所指定的设备。
在Busybox init程序中,还会检查这个设备是否可以打开,如果不能打开则使用”/dev/null”。
/etc/inittab文件的相关文档和示例代码都在Busybox的examples/inittab文件中,我们来一探究竟
查看inittab文件得知inittab格式: Format for each entry: #:::#id: The id field is used by BusyBox init to specify the controlling tty for the specified process to run on. #runlevels: The runlevels field is completely ignored. #action: Valid actions include: sysinit, respawn, askfirst, wait, once, # restart, ctrlaltdel, and shutdown. #process: Specifies the process to be executed and it's command line. /*******************************解析************************************/ 从默认的new_init_action反推出默认的配置文件: # inittab格式: #:::# id => /dev/id,用作终端:stdin,stdout,stderr:printf, scanf, err(即标准输入、输出、错误设 # 备),如果省略,则使用与Init进程一样的控制台。 # runlevels : 忽略 # action :执行时机 sysinit, respawn, askfirst, wait, once, # restart, ctrlaltdel, and shutdown. # process :应用程序或脚本,如果前有“-”字符,这个程序被称为“交互的”。
在init_main函数中,调用了parse_inittab函数来读取配置文件inittab。如果根文件系统中没有/etc/inittab文件,Busybox init程序将使用默认的inittab条目。这里我们可以通过默认的配置语句,倒推出默认的配置文件内容。
DIR: init.c-parse_inittab函数 /* Reboot on Ctrl-Alt-Del */ new_init_action(CTRLALTDEL, "reboot", ""); /* Umount all filesystems on halt/reboot */ new_init_action(SHUTDOWN, "umount -a -r", ""); /* Swapoff on halt/reboot */ if (ENABLE_SWAPONOFF) new_init_action(SHUTDOWN, "swapoff -a", ""); /* Prepare to restart init when a HUP is received */ new_init_action(RESTART, "init", ""); /* Askfirst shell on tty1-4 */ new_init_action(ASKFIRST, bb_default_login_shell, ""); new_init_action(ASKFIRST, bb_default_login_shell, VC_2); new_init_action(ASKFIRST, bb_default_login_shell, VC_3); new_init_action(ASKFIRST, bb_default_login_shell, VC_4); /* sysinit */ new_init_action(SYSINIT, INIT_SCRIPT, ""); /*******************************解析************************************/ ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r ::shutdown:/sbin/swapoff -a ::restart:/sbin/init ::askfirst:-/bin/sh tty2::askfirst:-/bin/sh tty3::askfirst:-/bin/sh tty4::askfirst:-/bin/sh ::sysinit:/etc/init.d/rcS
这里涉及到了一个函数 new_init_action 。 它实际上的工作就是把各个程序的执行时机、命令行、控制台参数分别赋值给结构体,并把这些结构体组成一个单链表。这也就是我们所说的配置。 它的声明是:static void new_init_action(int action, const char *command, const char *cons);这三个参数不正是inittab配置文件中的配置命令吗?他们分别对应于来看看new_init_action函数:
DIR:init.c-new_init_action函数 static void new_init_action(int action, const char *command, const char *cons) { struct init_action *new_action, *a, *last; if (strcmp(cons, bb_dev_null) == 0 && (action & ASKFIRST)) return; /* Append to the end of the list */ for (a = last = init_action_list; a; a = a->next) { /* don't enter action if it's already in the list, * but do overwrite existing actions */ if ((strcmp(a->command, command) == 0) && (strcmp(a->terminal, cons) == 0) ) { a->action = action; return; } last = a; } new_action = xzalloc(sizeof(struct init_action)); if (last) { last->next = new_action; } else { init_action_list = new_action; } strcpy(new_action->command, command); new_action->action = action; strcpy(new_action->terminal, cons); messageD(L_LOG | L_CONSOLE, "command='%s' action=%d tty='%s'n", new_action->command, new_action->action, new_action->terminal); } /* Set up a linked list of init_actions, to be read from inittab */ struct init_action { struct init_action *next; int action; pid_t pid; char command[INIT_BUFFS_SIZE]; char terminal[CONSOLE_NAME_SIZE]; };
new_init_action函数用于配置,参数为执行时机、命令行、控制台。
结构体指针new_action开始指向上一个配置过的程序(其存储在结构体,参数是上一个程序的执行时机、命令行、控制台),这里首先进行了一个If判断,如果控制台等于bb_dev_null(宏定义等于 /dev/null)且action为ASKFIRST那么直接返回,不进行任何配置。
接着这个for循环算是这个函数的一个重点吧,首先令结构体指针init_action_list赋值给a和last。
这里的init_action_list(宏定义为NULL)开始为NULL,后来指向第一个配置的程序。
也就是说,遍历所有配置过的程序,如果这个程序之前被配置过(命令行和控制台同时等于当前遍历的程序),那么执行时机action被重新赋值为当前值。
通俗的说,这个for为了避免程序重复配置,查找之前配置过的程序有没有当前要配置的程序,如果有,则只改变其执行时机action。命令行和控制台不变。
如果没有,接下来为new_action重新分配内存,并且给它赋值,令它的各项信息等于当前的程序。在上面的if语句中,last->next=new_action,也就是说,将所有程序的配置结构体连成一个单链表。
new_init_action函数讲解完毕。
经过上面的讲解,我们明白了Linux根文件系统中,对于程序的配置是在parse_inittab函数完成的,它打开配置文件inittab,将程序信息一一填入结构体init_action,并将它们连接成单链表。现在配置已经完成,下一步是执行了。接着看init_main中的代码是怎样执行应用程序的:
busybox-> init_main parse_inittab file = fopen(INITTAB, "r"); //打开配置文件/etc/inittab new_init_action // ① 创建一个init_action结构,填充 // ② 把这个结构放入init_action_list链表 run_actions(SYSINIT); waitfor(a, 0); // 执行应用程序,等待它执行完毕 run(a) // 创建process子进程 waitpid(runpid, &status, 0); // 等待它结束 delete_init_action(a);// 在init_action_list链表里删除 run_actions(WAIT); waitfor(a, 0); // 执行应用程序,等待它执行完毕 run(a) // 创建process子进程 waitpid(runpid, &status, 0); // 等待它结束 delete_init_action(a);// 在init_action_list链表里删除 run_actions(ONCE); run(a); delete_init_action(a); while(1) { run_actions(RESPAWN); if (a->pid == 0) { a->pid = run(a); } run_actions(ASKFIRST); if (a->pid == 0) { a->pid = run(a); 打印:Please press Enter to activate this console. 等待回车 创建子进程 } wpid = wait(NULL); // 等待子进程退出 while (wpid > 0) { a->pid = 0; // 退出后,就设置pid=0 } }
在/etc/inittab文件的控制下,init进程的行为总结如下:
① 在系统启动前期,init进程首先启动为sysinit、wait、once的3类子进程。
② 在系统正常运行期间,init进程首先启动为respawn、askfirst的两类子进程。
③ 在系统退出时,执行为shutdown、restart、ctrlaltdel的三类子进程(之一或全部)
从而我们可以总结出来最小的根文件系统由5部分组成:
1./dev/console or
/dev/null
2.init => busybox
3./etc/inittab
4.配置文件指定的程序
5.C库
busybox的配置、编译和安装
打开busybox自带的INSTALL文件查看我们该怎样配置、编译和安装busybox。
Building: ========= The BusyBox build process is similar to the Linux kernel build: make menuconfig # This creates a file called ".config" make # This creates the "busybox" executable make install # or make CONFIG_PREFIX=/path/from/root install The full list of configuration and install options is available by typing: make help
1.配置
进入busybox文件夹make menuconfig
生成配置文件.config
2.编译
由于我们文件系统是给嵌入式板子用的,先修改Busybox的Makefile,使用交叉编译器。
修改前
ARCH ?= $(SUBARCH) CROSS_COMPILE ?=
修改后
ARCH ?= $(SUBARCH) CROSS_COMPILE ?= arm-linux-
然后make
3.安装
注意:如果你是在虚拟机上安装busybox,安装不可直接执行make INSTALL,必须在虚拟机下自己创建一个文件夹,将安装路径指向这个文件夹的路径。再执行make CONFIG_PREFIX=dir_path install
否则会破坏系统。
注:除bin/busybox外,其他文件都是到bin/busybox的符号连接。busybox是所有命令的集合体,这些符号连接文件可以直接运行。比如在开发板上,运行“ls”命令和”busybox ls”命令是一样的。