应用程序位于根文件系统,内核为了使用应用程序,需要挂接根文件系统,本节分析一个最小根文件系统的组成,在下节讲解如果创建一个最小根文件系统
内核分析时,有如下调用过程
rest_init
kernel_init(通过创建线程调用)
prepare_namespace
mount_root/*挂接根文件系统*/
init_post
sys_open((const char __user *)"/dev/console", O_RDWR, 0)/*打开控制台*/
run_init_process/*调用应用程序*/
在mount_root挂接好文件系统后,系统会调用init_post,完成以下工作:
1、以读写方式打开终端,返回文件描述符为0
sys_open((const char __user *) "/dev/console", O_RDWR, 0)
2、将文件描述符为0的文件复制两份,其描述符分为别1、2,这三个文件是标准输入、输出、错误流,都指向终端/dev/console
(void) sys_dup(0);
(void) sys_dup(0);
3、若execute_command不为空,执行相应的应用程序
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
其中execute_command在下面这个函数中赋值
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);
其中str通过命令行参数init=xxx指定
4、如果execute_command为空,则执行
run_init_process("/sbin/init");
5、如果/sbin/init执行不成功,执行
run_init_process("/etc/init");
6、如果/etc/init执行不成功,执行
run_init_process("/bin/init");
7、如果/bin/sh执行不成功,执行
run_init_process("/bin/sh");
上述五个程序段至多有一个执行成功,执行成功后程序不会再返回;如果都执行不成功,则会打印错误并停止程序的运行
/sbin/init进程与ls、cp等命令都是应用程序
在虚拟机中输入命令:
ls -l /bin/ls
可以看到ls链接到busybox,/sbin/init和/bin/cp等也一样
而init进程是系统运行的第一个应用程序,它会读取解析配置文件、执行配置文件指定的应用程序
分析busybox源码,找到init/init.c的init_main函数
1、设置信号量signal,例如:设置了ctrlaltdel_signal信号,按下ctrl+alt+del会执行某些动作
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);
2、控制台初始化:
console_init();
3、解析配置
if (argc > 1
&& (!strcmp(argv[1], "single") || !strcmp(argv[1], "-s") || LONE_CHAR(argv[1], '1'))
) {
new_init_action(RESPAWN, bb_default_login_shell, "");
} else {
parse_inittab();
}
在parse_inittab中
①:以只读方式打开配置文件
file = fopen(INITTAB, "r");
②:如果没有inittab,使用默认配置
if (file == NULL) {
new_init_action(CTRLALTDEL, "reboot", "");
new_init_action(SHUTDOWN, "umount -a -r", "");
new_init_action(RESTART, "init", "");
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);
new_init_action(SYSINIT, INIT_SCRIPT, "");
return;
}
new_init_action会创建一个链表节点,用传入参数填充,并加入到init_action_list链表中,便于后面的调用
链表节点结构的定义如下:
struct init_action {
struct init_action *next;
int action;
pid_t pid;
char command[INIT_BUFFS_SIZE];
char terminal[CONSOLE_NAME_SIZE];
};
其中:
action:执行时间
pid:进程号
command:执行程序
terminal:终端
在example/inittab中有说明inittab的格式:<id>:<runlevels>:<action>:<process>
id:会以/dev/id的格式被使用于终端
runlevels:一般忽略
action:执行时间
process:可执行程序/脚本
根据上述格式可以得出默认配置文件内容如下:
::CTRLALTDEL:reboot
::SHUTDOWN:umount -a -r
::RESTART:init
tty2::ASKFIRST:-/bin/sh
tty3::ASKFIRST:-/bin/sh
tty4::ASKFIRST:-/bin/sh
::SYSINIT:/etc/init.d/rcS
③:解析配置文件,最终调用new_init_action函数执行应用程序
while (fgets(buf, INIT_BUFFS_SIZE, file) != NULL) {
for (id = buf; *id == ' ' || *id == '\t'; id++);
if (*id == '#' || *id == '\n')
continue;
eol = strrchr(id, '\n');
if (eol != NULL)
*eol = '\0';
strcpy(lineAsRead, buf);
runlev = strchr(id, ':');
if (runlev == NULL || *(runlev + 1) == '\0') {
message(L_LOG | L_CONSOLE, "Bad inittab entry: %s", lineAsRead);
continue;
} else {
*runlev = '\0';
++runlev;
}
action = strchr(runlev, ':');
if (action == NULL || *(action + 1) == '\0') {
message(L_LOG | L_CONSOLE, "Bad inittab entry: %s", lineAsRead);
continue;
} else {
*action = '\0';
++action;
}
command = strchr(action, ':');
if (command == NULL || *(command + 1) == '\0') {
message(L_LOG | L_CONSOLE, "Bad inittab entry: %s", lineAsRead);
continue;
} else {
*command = '\0';
++command;
}
for (a = actions; a->name != 0; a++) {
if (strcmp(a->name, action) == 0) {
if (*id != '\0') {
if (strncmp(id, "/dev/", 5) == 0)
id += 5;
strcpy(tmpConsole, "/dev/");
safe_strncpy(tmpConsole + 5, id,
sizeof(tmpConsole) - 5);
id = tmpConsole;
}
new_init_action(a->action, command, id);
break;
}
}
if (a->name == 0) {
message(L_LOG | L_CONSOLE, "Bad inittab entry: %s", lineAsRead);
}
}
④:处理完毕,关闭文件
fclose(file);
4、解析完配置后,根据action依次执行的应用程序
run_actions(SYSINIT);
run_actions(WAIT);
run_actions(ONCE);
while (1) {
run_actions(RESPAWN);
run_actions(ASKFIRST);
sleep(1);
wpid = wait(NULL);
while (wpid > 0) {
for (a = init_action_list; a; a = a->next) {
if (a->pid == wpid) {
a->pid = 0;
message(L_LOG, "process '%s' (pid %d) exited. "
"Scheduling it for restart.",
a->command, wpid);
}
}
wpid = waitpid(-1, NULL, WNOHANG);
}
}
执行应用程序的函数是run_actions,分析run_actions:
如果传入命令的action是SYSINIT、WAIT、CTRLALTDEL、SHUTDOWN、RESTART之一,创建进程执行命令,等命令执行完毕,删除命令节点并返回
如果传入命令的action是ONCE,创建进程执行命令,不等待执行完毕,直接删除命令节点并返回
如果传入命令的action是RESPAWN、ASKFIRST之一,;;对于action是ASKFIRST的命令,会先在标准输出打印一段字符串然后等待用户输入回车键后才执行命令
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);
}
}
}
}
总结:
1、挂接根文件系统后,内核会打开/dev/console,执行init进程
2、init进程中会执行如下工作:
①:设置信号量
②:控制台初始化
③:解析配置文件
④:执行配置文件所规定的应用程序
故最小根文件系统必须有/dev/console,init进程,配置文件inittab,配置文件所执行的应用程序
为了运行printf等应用程序,还需要一些必要的库
为了应对配置文件中id没有设置的情况,还需要空设备/dev/null
为了使用ls、cp等应用程序,可以移植busybox,busybox内也提供了init进程
也就是说一个最小根文件系统可以由以下内容组成:
①:/dev/console、/dev/null
②:busybox
③:必要的库
④:配置文件inittab
⑤:配置文件所规定的应用程序