嵌入式的瑞士军刀 - BusyBox

时间:2022-03-25 18:51:06

From: 文件系统

事物总是有N多种表现形式 - XX

Linux 内核启动到最后的时候,摇身一变,进入托管模式。内核就默默的退居幕后,应用程序变身前台主人,所有对于用户的交互都是都APPlication 来完成。
说到这里层次感就很明确。
对于操作系统而言,Application 是交互的前端Application 可以是一个单独的程序,也可以是一个库 + 程序,秉承unix的设计理念(小即是美)。对于类Unix系统而言,Application 都很小,所以它们依赖很多库(Lib)。Application又通过系统调用将任务下发到内核,内核通过调度确定任务的复杂程序以及任务周期,并选择具体的任务出口程序(驱动程序)驱动程序找到自己的伙伴(硬件出口),同样驱动程序一直在等待任务的反馈,得到反馈后立马上报,逆流程到Application。这样子一次任务的交互就完成。
由于任务的复杂性、保密性,实时性等诸多标签,内核衍生出了安全子系统,以及各种应对与标签事件的总线驱动,设备驱动。以保证任务以最佳的方式获得传递与反馈。
好了,我们创建完1号线程后,其实前端控制权利已经交由所谓的Application层了,那么对于我们Application 而言,我们不可能一个一个的去梳理,它们也有框架和表示形式,在嵌入式领域,用的最多的小系统那肯定属于Busybox,当然这只是最小系统,完整的嵌入式上层框架也有,譬如Buildroot等.

Busybox - 嵌入式的瑞士军刀

Busybox 默认带了很多系统小工具(而且大多都是被功能裁剪过的),很多人可能都是直接拿它来用。但其实在真实的应用中,往往Busybox被用初始化启动流程<设置环境变量,选择终端,以及选择波特率<如果是串口的话>等等>。其优点就在于便捷,快速。如果你想快速的搭建一个小系统,Busybox是最好的选择之一。
BusyBox init非常适合在嵌入式系统开发中使用,它可以为嵌入式系统提供只要的init功能,并且通过定制可以做得非常 精炼。inittab是帮助init完成系统配置的主要文件。
/* Line is: "id:runlevel_ignored:action:command" */
这是BusyBox-1.11.1中init.c文件中的一句注释,该注释指明了inittab文件中每行的格式。以下对各字段进行简要解析:
1.id
尽管该格式与发行版linux的Sys V init类似,但是,id在BusyBox的init中具有不同的意义。对BusyBox而言, id用来指定启动进程的控制终端 。如果所启动的进程并不是可以交互的shell,例如BusyBox的sh(ash),应该会有个控制终端,如果控制终端不存在,BusyBox的sh会报错。
2.runlevel_ignored
由该字段的名称可知, BusyBox init忽略runlevel_ignored字段 ,所以配置inittab时空着它就行了。
3.command
command字段用来指定要执行命令(含路径),包括命令行选项。
4.action
在BusyBox-1.11.1中init.c定义了以下8种action

staticconstchar actions[]=
STR_SYSINIT "sysinit/0"
STR_RESPAWN "respawn/0"
STR_ASKFIRST "askfirst/0"
STR_WAIT "wait/0"
STR_ONCE "once/0"
STR_CTRLALTDEL "ctrlaltdel/0"
STR_SHUTDOWN "shutdown/0"
STR_RESTART "restart/0"
;

其中,STR_SYSINIT、STR_RESPAWN、STR_ASKFIRST、STR_WAIT、STR_ONCE、 STR_CTRLALTDEL、STR_SHUTDOWN、STR_RESTART为action_type,即action的编码。它们各占1字节,具 体定义如下:
#define STR_SYSINIT “/x01”
#define STR_RESPAWN “/x02”
#define STR_ASKFIRST “/x04”
#define STR_WAIT “/x08”
#define STR_ONCE “/x10”
#define STR_CTRLALTDEL “/x20”
#define STR_SHUTDOWN “/x40”
#define STR_RESTART “/x80”

下表列举了这8种action的含义:

 action  含义
sysinit 为init提供初始化命令脚本的路径
respawn 每当相应的进程终止执行时,重新启动该进程
askfirst 类似respawn,主要用途是减少系统上执行的终端应用程序的数量。
它将会促使init在控制台上显示“Please press Enter to active this console”的信息,并在重新启动进程之前等待用户按下“enter”键
wait 告诉init必须等到相应的进程执行完成之后才能继续执行
once 仅执行相应的进程一次,而且不会等待它执行完成
ctratldel 当按下Ctrl+Alt+Delete组合键时,执行相应的进程
shutdown 当系统关机时,执行相应的进程
restart 当init重新启动时,执行相应的进程,通常此处所执行的进程就是init本身

下面简要介绍一下BusyBox init怎么对inittab进行分析执行的。由BusyBox-1.11.1中init.c文件可知,BusyBox init通过init_main方法对inittab文件的分析执行,大致过程如下:

1、init_main方法先通过parse_inittab分析inittab文件,将该文件中的每一行通过 new_init_action(uint8_t action_type, const char *command, const char *cons)添加到init_action_list列表中。其中cons就是每行的id字段。init_action_list的定义如下:

/* Set up a linked list of init_actions, to be read from inittab */
/* inittab文件的每一行都会保存为一个init_action节点,并且所有 init_action节点会被链接成一个叫init_action_list的列表*/
struct init_action {
struct init_action * next;
pid_t pid; /* 实际执行该command的进程ID*/
uint8_t action_type; /* action的类型 */
char terminal[ CONSOLE_NAME_SIZE]; /* 运行该command的终端 */
char command[ COMMAND_SIZE]; /* 保存command字段(含命令行选项)*/
};

/* Static variables */
staticstruct init_action * init_action_list =NULL;
若不支持ENABLE_FEATURE_USE_INITTAB或支持ENABLE_FEATURE_USE_INITTAB但inittab文件不存在,则执行一个默认的操作,如下:
if( ENABLE_FEATURE_USE_INITTAB)
file=fopen( INITTAB,"r");
else
file=NULL;

/* No inittab file -- set up some default behavior */
if(file==NULL){
/* 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 QUIT 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,"");

return;
}

通过代码中的英文注释,应该都可以看懂该代码。需要解释可能只有INIT_SCRIPT。INIT_SCRIPT的定义如下:
#define INIT_SCRIPT "/etc/init.d/rcS" /* Default sysinit script. */
即, BusyBox init默认的初始化脚本是/etc/init.d/rcS
当支持ENABLE_FEATURE_USE_INITTAB且inittab文件存在时,BusyBox init会对inittab文件进行如下分析:

//循环获取inittab文件中的每一行到buf中
while(fgets( buf, COMMAND_SIZE,file)!=NULL){
//定义action的种类
staticconstchar actions[]=
STR_SYSINIT "sysinit/0"
STR_RESPAWN "respawn/0"
STR_ASKFIRST "askfirst/0"
STR_WAIT "wait/0"
STR_ONCE "once/0"
STR_CTRLALTDEL "ctrlaltdel/0"
STR_SHUTDOWN "shutdown/0"
STR_RESTART "restart/0"
;
char tmpConsole[ CONSOLE_NAME_SIZE];
char* id,* runlev,* action,* command;
constchar* a;
/*通过跳过空格,并截取到换行/n为止,来获取本行的有效内容,并保存于id中*/
/* Skip leading spaces */
id = skip_whitespace( buf);
/* Trim the trailing '/n' */
* strchrnul( id,'/n')='/0';
//若为注释,跳出本次循环
/* Skip the line if it is a comment */
if(* id =='#'||* id =='/0')
continue;

/* Line is: "id:runlevel_ignored:action:command" */
//获取runlev字段
runlev =strchr( id,':');
if( runlev ==NULL/*|| runlev[1] == '/0' - not needed */)
goto bad_entry;
//获取action字段
action =strchr( runlev + 1,':');
if( action ==NULL/*|| action[1] == '/0' - not needed */)
goto bad_entry;
//获取command字段
command =strchr( action + 1,':');
if( command ==NULL|| command[ 1]=='/0')
goto bad_entry;
/*循环遍历actions数组,查找数组中与action字段相同的元素。找到后,通过new_init_action方法,
将该元素的第一个字符(即action_type编码)和id及command字段作为一init_action节点添加到init_action_list列表中。
接着跳到下一行进行处理*/
* command ='/0';/* action => ":action/0" now */
for( a = actions; a[ 0]; a +=strlen( a)+ 1){
//查到数组actions中与action字段相同的元素
if(strcmp( a + 1, action + 1)== 0){
//截取id字段,格式为"id/0"
* runlev ='/0';
//若id字段非空
if(* id !='/0'){
//若id字段带前缀/dev/,先截掉该前缀
if(strncmp( id,"/dev/", 5)== 0)
id += 5;
//复制字符串/dev/到tmpConsole临时缓存区中
strcpy( tmpConsole,"/dev/");
/*再将id字段复制到tmpConsole第5个字符之后,即/dev/之后。这样tmpConsole就成为了某一设备文件名(含路径)。
对于BusyBox init来说,tmpConsole是终端设备*/
safe_strncpy( tmpConsole + 5, id,
sizeof( tmpConsole)- 5);
/*来到这里,应该就明白为什么BusyBox init的id字段是控制终端*/
id = tmpConsole;
}
//将action_type、command和控制终端id作为一init_action节点,添加到init_action_list列表中。
从这里可以看出BusyBox init忽略了runlevel字段
new_init_action((uint8_t) a[ 0], command + 1, id);
goto next_line;
}
}
* command =':';
/* Choke on an unknown action */
bad_entry:
message( L_LOG | L_CONSOLE,"Bad inittab entry: %s", id);
next_line:;
}

2、BusyBox init分析完inittab之后,就是执行各command了。BusyBox init通过run_actions(int action_type)方法,查找init_action_list列表中action类型为action_type的所有init_action,并 为符合条件的init_action节点调用run(const struct init_action *a)。而实际上,run方法中,是通过init_exec(a->command)来执行具体的command。run_actions的源码如 下:

/* Run all commands of a particular type */
staticvoid run_actions(int action_type)
{
struct init_action * a,* tmp;
// 遍历init_action_list列表,查找类型为action_type的节点
for( a = init_action_list; a; a = tmp){
tmp = a-> next;
//查到类型为action_type的节点
if( a-> action_type & action_type){
// Pointless: run() will error out if open of device fails.

///* a->terminal of "" means "init's console" */

//if (a->terminal[0] && access(a->terminal, R_OK | W_OK)) {

// //message(L_LOG | L_CONSOLE, "Device %s cannot be opened in RW mode", a->terminal /*, strerror(errno)*/);

// delete_init_action(a);

//} else

if( a-> action_type &( SYSINIT | WAIT | CTRLALTDEL |SHUTDOWN| RESTART)){/*若该节点类型为SYSINIT、WAIT、CTRLALTDEL、SHUTDOWN和RESTART的init_action,init会等待它的command执行完,再继续执行。并且command执行完后,删除该节点*/
waitfor( run( a));
delete_init_action( a);
}elseif( a-> action_type & ONCE){
/*action_type为ONCE的init_action,init则不会等待它执行完,并且将该节点从init_action_list中删除*/
run( a);
delete_init_action( a);
}elseif( a-> action_type &( RESPAWN | ASKFIRST)){
/* Only run stuff with pid==0. If they have
* a pid, that means it is still running */
/*当action_type为RESOAWN或ASKFIRST的init_action,且执行该init_action的command的进程已死
(通过a->pid == 0判断,已死RESOAWN或ASKFIRST的command进程,其init_action的pid域都会在init_main方法被置为0,
具体见 本文最后一段源码)时,调用run方法fork一子进程(用于执行command),并将run返回的子进程ID保存于init_action的pid 域*/
if( a-> pid == 0){
a-> pid = run( a);
}
}
}
}
}

由run_actions源码可知:
action_type为SYSINIT、WAIT、CTRLALTDEL、SHUTDOWN、 RESTART和ONCE的init_action,执行其command后,都会通过delete_init_action(struct init_action *action)将它从init_action_list中删除,这样做的原因可能是:这些init_action节点只会被BusyBox init执行一次,并且删除它们可提高对init_action_list其它类型的init_action节点的查找效率并节省空间。结合以下 BusyBox init对各类init_action的执行顺序,可能会更好的理解这一点。
由init_main方法可知,BusyBox init对各类init_action的执行顺序如下:

/* Now run everything that needs to be run */

/* First run the sysinit command */
run_actions( SYSINIT);

/* Next run anything that wants to block */
run_actions( WAIT);

/* Next run anything to be run only once */
run_actions( ONCE);

/* Now run the looping stuff for the rest of forever */
while( 1){
/* run the respawn/askfirst stuff */
run_actions( RESPAWN | ASKFIRST);

/* Don't consume all CPU time -- sleep a bit */
sleep( 1);

/* Wait for any child process to exit */
wpid = wait(NULL);
while( wpid > 0){
/* Find out who died and clean up their corpse */
for( a = init_action_list; a; a = a-> next){
if( a-> pid == wpid){
/* Set the pid to 0 so that the process gets
* restarted by run_actions() */
a-> pid = 0;
message( L_LOG,"process '%s' (pid %d) exited. "
"Scheduling for restart.",
a-> command, wpid);
}
}
/* see if anyone else is waiting to be reaped */
wpid = wait_any_nohang(NULL);
}
}
}

By: Keven - 点滴记录