Linux根文件系统分析之init和busybox
Hi,大家好!我是CrazyCatJack。今天给大家讲解Linux根文件系统的init进程和busybox的配置及编译。
先简单介绍一下,作为一个嵌入式系统,要想在硬件上正常使用的话。它的软件组成大概有这三部分:1)bootloader 2)嵌入式系统kernel 3)根文件系统 。这其实非常好理解,类比于PC上的操作系统,首先我们需要类似BIOS的东东,来控制系统的启动项,决定从哪里启动,怎样启动,启动什么。在嵌入式系统里bootloader就起着这样的作用。再者,我们需要一个已经配置、编译、链接好的内核。当然,如果自己有源码的话,完全可以自己修改源码,生成自己想要的内核。最后,我们需要根文件系统,在windows下,我们的系统有很多分区,那么在嵌入式系统下,我们就需要根文件系统来完成这项工作。PC上的应用程序都是从硬盘读取,我们嵌入式系统上的应用程序都是从根文件系统上读取。因此,制作或修改出符合自己要求的根文件系统就变成了一件必须要做的事。今天CrazyCatJack给大家带来的是根文件系统的分析,我们只有分析出,在嵌入式系统下,根文件系统是怎样配置、怎样启动应用程序。才能修改或制作出自己的根文件系统。也就是说:要理解才行。这次博客CrazyCatJack给大家分析,那么下一篇blog就给大家演示自己构建根文件系统了,我会尽量用简洁明了的语言把自己会的知识分享给大家,让大家最终也能做出自己的根文件系统^_^
1.原理分析
1.内容简介
之前有博友给CrazyCatJack提建议,原理和代码分开写。最好还配上图片和流程图,因此,今天CCJ给大家配图,相信更容易理解吧!今天讲的内容如下面的流程图:
1)在Linux kernel的源代码中,对如何启动应用程序有着明确的定义。首先我们需要挂载根文件系统,只有正确挂载了根文件系统,才能够从根文件系统中读出应用程序。我们启动的第一个程序就是init程序。init进程完成了对应用程序的各项配置(进程ID、执行时机、命令、终端、下一个执行的进程等),并最终依据配置执行了应用程序。
2)要执行应用程序,首先进行配置。配置文件inittab里有着对应用程序的详细配置,这些都是C文件。init进程读出配置、分析配置并配置应用程序、配置C库(用到很多C库里的函数)。最后执行程序。
3)busybox的话,其实是一个方便移植的模块。它主要的功能在其README自述文档中有着精炼的定义:BusyBox combines tiny versions of many common UNIX utilities into a single small executable.它的确是很方便的工具,而且本身模块化、可配置且极具移植性。它的这一特点充分体现了busybox的存在意义和潜在价值。而且大家完全不必担心它的执行效率。在它的自述文档中,明白的写着BusyBox has been written with size-optimization and limited resources in mind, both to produce small binaries and to reduce run-time memory usage.也就是说,不但将这些实用程序集结到了一个小的可执行文件中,而且它本身执行速度快,体积小,占用运行内存少。(怎么感觉博主是在为busybox做广告 -_-|| ,但谁叫它这么给力呢)。有很多的程序都是指向busybox的。如下图:
大家可以看到这两个程序都指向了busybox。也就是说,执行这个命令本身和执行busybox加上这个命令的效果是一样的。如下图:
可能有人会问:这有什么好的?这样写命令岂不是很麻烦。每次都要多加一个busybox。那请问ls你是什么时候编译安装的呢?也就是说,如果不用busybox,那么你需要自己从bin目录下和sbin目录下找到你要安装的程序源代码,手动一个一个的编译生成文件,再安装。而UNIX的常用工具上百个,你能一个一个的这样做吗?如果你可以,那在下佩服!^_^
busybox有一整套的工具,将这些你需要的工具统一编译安装,瞬间生成大量实用工具。
2.代码讲解
1.检测根文件系统并启动init
首先,在kernel源文件中的Main.c文件中,init_post函数完成了对根文件系统和控制台的检测,并启动init进程。代码如下:
DIR: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 %s\n", 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."); }
这里我们看到,它首先做了一项工作:打开console文件。然后复制、复制。对应于代码:
open("/dev/console")
(void) sys_dup(0);
(void) sys_dup(0);
也就是说打开控制台,这个控制台会对应标准输入、标准输出、标准错误。它可能是显示器、键盘鼠标等等。接着往下看:
if (execute_command) {
run_init_process(execute_command);
}
其中execute_command为命令行参数,在我们uboot传给内核的参数中,init设置了init=/linuxrc(关于uboot的源码讲解见我之前的博客),所以这里的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.");
这里我们可以做一个实验,检测实际的根文件系统检测是否和这个init_post函数定义的一样,如果和我们分析的一致,则证明我们的分析是正确的。那么这里我们可以先擦除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.");相对应。
这证明我们的分析是正确的。
2.init进程分析
在上面的内容简介中,CCJ提到了busybox这个实用工具。这里,我们的init程序也是由busybox编译生成的。如果你有busybox,打开它的文件,你会发现每个小程序都有一个文件夹,里面放的是它的编译文件,一定会有XX.c。比如init.c、ls.c。在每个XX.c中一定有XX_main函数,定义了这个小程序将如何执行。这里我们就打开init.c看init_main函数。init_main很长,CCJ不会把它直接拷贝过来让大家看得眼晕,我会一点一点帮助大家分析。首先,init进程的执行顺序大概是这样的:读取配置文件inittab(若读取失败则用默认配置),解析配置文件,根据配置执行应用程序。那么这里我们先瞧瞧inittab配置文件是怎样定义的:
查看inittab文件得知inittab格式:
Format for each entry: # <id>:<runlevels>:<action>:<process> #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.
这是我们配置相关的格式要求。CCJ帮大家整理出来了。通俗的讲,这里的<id>就是终端,标准输入输出和错误。这里的<runlevels>如同注释,是完全忽略掉的。<action>是程序执行的时机,可取的值有sysinit, respawn, askfirst, wait, once,restart, ctrlaltdel, and shutdown.<process>表示将要执行的应用程序或脚本。
在init_main函数中,调用了parse_inittab函数来读取配置文件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, "");
这是在读取配置文件失效时的默认配置。这里涉及到了一个函数 new_init_action 。它实际上的工作就是把各个程序的执行时机、命令行、控制台参数分别赋值给结构体,并把这些结构体组成一个单链表。这也就是我们所说的配置。它的声明是:static void new_init_action(int action, const char *command, const char *cons);这三个参数不正是inittab配置文件中的配置命令吗?他们分别对应于<action>、<process>、<id>。按照<id>:<runlevels>:<action>:<process>的顺序将参数填充进去不就是我们需要的默认配置文件吗^_^
从默认的new_init_action反推出默认的配置文件
/* Reboot on Ctrl-Alt-Del */ new_init_action(CTRLALTDEL, "reboot", ""); ::ctrlaltdel:reboot /* Umount all filesystems on halt/reboot */ new_init_action(SHUTDOWN, "umount -a -r", ""); ::shutdown:umount -a -r /* Swapoff on halt/reboot */ if (ENABLE_SWAPONOFF) new_init_action(SHUTDOWN, "swapoff -a", ""); //PC机上当内存不够,就把应用程序移动到硬盘,然后让新的应用程序读入内存。在嵌入式Linux中不长用 /* Prepare to restart init when a HUP is received */ new_init_action(RESTART, "init", ""); ::restart:init /* Askfirst shell on tty1-4 */ new_init_action(ASKFIRST, bb_default_login_shell, ""); ::askfirst:-/bin/sh new_init_action(ASKFIRST, bb_default_login_shell, VC_2); tty2::askfirst:-/bin/sh new_init_action(ASKFIRST, bb_default_login_shell, VC_3); tty3::askfirst:-/bin/sh new_init_action(ASKFIRST, bb_default_login_shell, VC_4); tty4::askfirst:-/bin/sh /* sysinit */ new_init_action(SYSINIT, INIT_SCRIPT, ""); ::sysinit:/etc/init.d/rcS
其中,粉色部分的内容就是我们根据inittab配置文件格式,将C语句中new_init_action函数中的参数一一组合成默认配置文件语句,合起来就是默认配置文件的内容。这里,CCJ认为有必要深入了解一下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];
};
说实话,刚开始感觉这个函数蛮绕的,CCJ根本看不懂它要干嘛,但是我们可以写在纸上,将它要做的工作,一步一步写下来就明白了。不管是循环,还是链表,还是结构体。它禁不住推敲的。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中的代码是怎样执行应用程序的:
init_main程序简要结构: init.c->init_main-> parse_inittab run_actions(SYSINIT); run_actions(WAIT); run_actions(ONCE); while (1) { run_actions(RESPAWN); run_actions(ASKFIRST); wpid = wait(NULL); while (wpid > 0) { a->pid = 0; } }
上面是简化的init_main的程序结构,上面只有比较主要的几个函数。第一个函数parse_inittab完成了配置。那么下一步开始执行时机类型为sysinit, respawn, askfirst, wait, once, restart, ctrlaltdel, and shutdown类型的应用程序。那么正如大家看到的,执行应用程序主要涉及到的是run_actions函数。这里我们打开它:
DIR:init.c-run_actions函数 static void run_actions(int action) { struct init_action *a, *tmp; for (a = init_action_list; a; a = tmp) { tmp = a->next; if (a->action == action) { /* a->terminal of "" means "init's console" */ if (a->terminal[0] && access(a->terminal, R_OK | W_OK)) { delete_init_action(a); } else if (a->action & (SYSINIT | WAIT | CTRLALTDEL | SHUTDOWN | RESTART)) { waitfor(a, 0); delete_init_action(a); } else if (a->action & ONCE) { run(a); delete_init_action(a); } else if (a->action & (RESPAWN | ASKFIRST)) { /* Only run stuff with pid==0. If they have * a pid, that means it is still running */ if (a->pid == 0) { a->pid = run(a); } } } } }
这里我们以SYSINIT类型的程序为例,可以看到,在if的分支语句中,如果发现我们传入的参数是SYSINIT,即此刻运行SYSINIT类型的应用程序。那么waitfor(a,0),就是执行应用程序,并等待它执行完毕。具体对应于waitfor函数的run(a);(创建process子进程)和waitpid(runpid, &status, 0);(等待它结束)。执行完waitfor后,会执行delete_init_action(a);这个函数的作用是这个SYSINIT类型的程序执行完一次,就在init_action_list链表里删除。那么其他执行时机类型的程序依此类推,前三个执行详细如下:
SYSINIT、WAIT、ONCE时机的程序运行机制
run_actions(SYSINIT); waitfor(a, 0); //执行应用程序,等待他执行完毕 run(a); //创建process子进程 BB_EXECVP(cmdpath, cmd); waitpid(runpid, &status, 0);//等待它结束 delete_init_action(a); //执行完一次,就在init_action_list链表里删除 run_actions(WAIT); waitfor(a, 0); //执行应用程序,等待他执行完毕 run(a); //创建process子进程 BB_EXECVP(cmdpath, cmd); waitpid(runpid, &status, 0);//等待它结束 delete_init_action(a); //执行完一次,就在init_action_list链表里删除 run_actions(ONCE); run(a); //创建process子进程 delete_init_action(a); //执行完一次,就在init_action_list链表里删除 //可见init进程不会等待ONCE子进程执行完毕。
接下来的RESPAWN和ASKFIRST和上面的三个执行时机略有不同,它们处于while(1)循环里,并且,只有当进程的PID号等于0时才重新执行该进程。其运行机制如下:
RESPAWN和ASKFIRST运行机制 while (1) { run_actions(RESPAWN); if (a->pid == 0) { a->pid = run(a); } run_actions(ASKFIRST); if (a->pid == 0) { a->pid = run(a); } wpid = wait(NULL); //等待子进程X退出 while (wpid > 0) { a->pid = 0; //子进程X退出后,就设置X的pid等于0.然后重新进入死循环,再次执行这个已经退出的子进程X。也就是说哪个子进程退出,再重新执行哪个,而不是全部再执行。 } }
也就是说,只有当该进程执行完毕退出时,设置其PID为0,然后再次执行该进程。RESPAWN和ASKFIRST执行时机的程序是要重复执行已经执行完毕的程序。对于其他程序则不重复执行。可能大家会问,那么RESPAWN和ASKFIRST执行时机的程序有什么不同?大致有两点:1)RESPAWN和ASKFIRST的执行顺序不同。2)在run函数中,对于ASKFIRST类型的程序会先打印:"\nPlease press Enter to activate this console. "并且等待回车后才会继续执行。
现在我们已经讲解了SYSINIT、WAIT、ONCE、RESPAWN、ASKFIRST类型的程序。至于CTRLALTDEL类型的程序则是在init_main函数的起始部分就做了信号量的定义。也就是说,同时按下CTRL+ALT+DEL键,就会向内核发送信号量,并执行run_actions(CTRLALTDEL);其定义如下:
DIR:init.c-init_main函数 signal(SIGHUP, exec_signal); signal(SIGQUIT, exec_signal); signal(SIGUSR1, shutdown_signal); signal(SIGUSR2, shutdown_signal); signal(SIGINT, ctrlaltdel_signal); signal(SIGTERM, shutdown_signal); signal(SIGCONT, cont_handler); signal(SIGSTOP, stop_handler); signal(SIGTSTP, stop_handler);
shutdown类型的程序则比较复杂,因为它完成的是要关闭系统的动作。在init.c中有一个函数shutdown_system就是完成关闭系统的工作,如下:
static void shutdown_system(void) { sigset_t block_signals; /* run everything to be run at "shutdown". This is done _prior_ * to killing everything, in case people wish to use scripts to * shut things down gracefully... */ run_actions(SHUTDOWN); /* first disable all our signals */ sigemptyset(&block_signals); sigaddset(&block_signals, SIGHUP); sigaddset(&block_signals, SIGQUIT); sigaddset(&block_signals, SIGCHLD); sigaddset(&block_signals, SIGUSR1); sigaddset(&block_signals, SIGUSR2); sigaddset(&block_signals, SIGINT); sigaddset(&block_signals, SIGTERM); sigaddset(&block_signals, SIGCONT); sigaddset(&block_signals, SIGSTOP); sigaddset(&block_signals, SIGTSTP); sigprocmask(SIG_BLOCK, &block_signals, NULL); message(L_CONSOLE | L_LOG, "The system is going down NOW!"); /* Allow Ctrl-Alt-Del to reboot system. */ init_reboot(RB_ENABLE_CAD); /* Send signals to every process _except_ pid 1 */ message(L_CONSOLE | L_LOG, "Sending SIG%s to all processes", "TERM"); kill(-1, SIGTERM); sync(); sleep(1); message(L_CONSOLE | L_LOG, "Sending SIG%s to all processes", "KILL"); kill(-1, SIGKILL); sync(); sleep(1); }
大家可以看到,它先照例执行了run_actions(SHUTDOWN)。然后禁止了所有的信号传送。打印"The system is going down NOW!" 然后关闭所有的进程,最后关闭系统。init进程讲解完毕。
3.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
文件中写的很明确,编译busybox和编译linux kernel差不多。如果大家看了我之前有关linux内核的配置、编译和连接的博客就不会对这三条命令感到陌生了。那么首先要make menuconfig生成配置文件.config。然后make生成busybox可执行文件。最后make install安装busybox。首先执行 make menuconfig:
执行过后会出现图形界面,这方便了我们对busybox的配置,这里我们只需要手动选择需要编译安装的项目,其中有很多实用的工具,最后进行保存就可以了。大家可以看到有很多的选项提供给我们。选择后,最后退出,save即可。然后执行make生成可执行busybox文件。下一步就是安装了。
注意:如果你是在虚拟机上安装busybox,安装不可直接执行make INSTALL,必须在虚拟机下自己创建一个文件夹,将安装路径指向这个文件夹的路径。再执行 make CONFIG_PREFIX=/path/from/root install 否则会破坏系统。
经过简单的配置编译和安装,我们最终就能使用busybox这个方便的工具了。
敬告: 本文原创,欢迎大家学习转载^_^ 但请尊重博主CrazyCatJack的版权。 转载请在显著位置注明: 博主ID:CrazyCatJack |
题外话:
这大概是博主写过最长的博客了吧。。。苦笑~写之前没有想到会写这么长,但是因为已经做出了流程图,三个方面的内容就必须得写下去了,自己挖的坑自己填了 T_T 。但是还是觉得很值得的。刚开始写博客就是为了梳理自己的知识,将学过的内容巩固,并不在乎大家是否能看懂。(实际上我怀疑是否有人会看我的博客)所以对自己写的内容要求不是很高。但是当博主发现居然真的有人会看,那么再这样写就有些对不起大家了。也有博友给博主提建议,改进博客的表现形式。博主悉心听取了,正一步一步完善博文,方便更好的分享给大家。
博主是一路自学过来的,从51、stm32、Freescale K系列、再到现在的ARM和嵌入式Linux。看视频,看PDF,买板子,买传感器,做项目,走到今天。相信很多博友也是和博主一样吧。我之所以坚持到现在,就是希望创造更美好的事物。比起毁灭的力量,我更想得到创造的力量。毁灭一个事物很容易,但创造一个事物很难。不管是机器人也好、智能穿戴设备也好,创造这些insteresting、exciting、creative的东西,这是我的理想。
但是我知道一个人的力量是渺小的,虽然我做了一部分的工作,但这还远远不够。我希望将知识分享给大家。集体的力量是不容小觑的。懂得分享才会有更多思维的火花碰撞,不同领域的人们彼此分享,帮助。这样的力量是会改变时代的。就像人类大脑里的神经元,一条神经元可能只会传达极其简单的讯息,比如0和1。但是当这种讯息在大量的神经元间彼此传递,越来越多,越来越快。就会形成非常高级的事物:思想。这是一条神经元远远无法做到和想象得到的。
CCJ
2016-12-16 22:02:20