Linux根文件系统分析

时间:2022-08-15 20:10:00

部分内容参考的博客CrazyCatJack:http://www.cnblogs.com/CrazyCatJack/p/6184564.html

整体框架:

Linux根文件系统分析
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 命令

Linux根文件系统分析

我们查看软连接

Linux根文件系统分析
Linux根文件系统分析

内核检测根文件系统并启动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 %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.");
}

内核启动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进程的启动过程

Linux根文件系统分析

其中与构建根文件系统关系密切的是控制台的初始化、对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>:<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.
/*******************************解析************************************/
从默认的new_init_action反推出默认的配置文件:
# inittab格式:
# <id>:<runlevels>:<action>:<process>

# 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配置文件中的配置命令吗?他们分别对应于<action>、<process>、<id>。

来看看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

Linux根文件系统分析

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”命令是一样的。