第4阶段——制作根文件系统之分析init进程(2)

本节目标:

(1) 了解busybox(init进程和命令都放在busybox中)

(2) 创建SI工程,分析busybox源码来知道init进程做了哪些事情

(3)  分析busybox中init进程 init_main()

(3.1)熟悉init进程的inittab配置文件(位于/etc/inittab)

(3.2)熟悉inittab配置文件中不同action的子进程区别

(3.3)了解init进程如何读取分析inittab,以及运行inittab文件中的各个子进程

(4) 了解制作一个最小的根文件系统的需求

1.busybox简介

内核启动成功后,建立init进程并执行了第一个应用程序后,我们就可以输入ls、cp、vi等命令了

这些命令其实都是一个应用程序,命令都放在了/bin目录中,如下图所示:

不过它们的链接地址都是放在了busybox里.比如:执行ls命令,其实就是执行 busybox ls,

如下图所示,我们在/bin目录中输入busybox ls,和ls命令一摸一样:

同样,我们在/bin目录中输入ls - l 列出详细信息,如下图所示:

发现所有命令都是放在busybox中,linux是借助busybox来实现这些命令

除了命令外,init进程同样也是放在busybox中,如下图:

 所以命令和init进程都位于busybox,制作根文件系统必须要busybox

2. 接下来创建SI工程,分析busybox源码来知道init进程做了哪些事情

busybox源码位于资料光盘中/system中,添加所有文件,并同步文件.

可以发现:

其中ls命令就位于ls.c文件中,cp命令就位于cp.c文件中,同样的init进程就位于init.c文件中

执行这些命令或者进程,最终调用它们自己的文件中xx_main()函数。

所以分析init进程就分析init.c文件中的init_main()函数

3分析busybox中init进程 init_main()

init进程:除了启动第一个应用程序(/linuxrc或者/sbin/init等),还要启动用户的应用程序(例如启动摄像,视频等),那么就需要:

(1)读取配置文件(一般放在linux中/etc目录下, /etc/inittab)

(2)解析配置文件

(3)最后执行用户的应用程序(里面的各个子进程)

其中配置文件说明在busybox-1.7.0/examples/inittab中,通过inittab分析得出:

inittab配置文件格式如下:

Format for each entry: <id>:<runlevels>:<action>:<process>

参数如下:

id id 会等于/dev/id, 用做终端(标准输入、标准输出以及标准错误) ,这个可以不需要设置,因为/etc/console已经设为标准输入输出了,如不设置就等于dev/null,则从控制台输入输出。

runlevels:可以被忽略

action: 运行时机,指应用程序何时(action)行动,它的参数有(参数必须小写):

sysinit(用来初始化时启动),

 respawn(每当相应的进程终止运行时,该进程就会重新启动),

askfirst(每次启动进程之前等待用户按下enter键),

wait(告诉init必须等到相应的进程执行完成之后才能继续执行),

once(仅执行相应的进程一次,而且不会等待它执行完成),

restart(当重新读取分析inittab配置文件时,会执行相应进程),

ctrlaltdel(当按下Ctrl+Alt+Delete组合键时,会执行相应进程),

andshutdown(该进程用于系统关机时执行)

process:应用程序或者脚本, 就是要启动的进程(如果有“-”字符,说明这个程序被称为”交互的”)。

init_main()流程图如下:

3.1先分析init_main()前部分如何读取解析配置文件

init_main()部分代码如下:

int init_main(int argc, char **argv)
{
 ... ...
 console_init();                //初始化控制台,在init_post()中只是打开了控制台设备
 ... ...
 if (argc > 1      //在init_post()中执行的”_init_process("/sbin/init");”,所以argc=1, argv=/sbin/init
        && (!strcmp(argv[1], "single") || !strcmp(argv[1], "-s") || LONE_CHAR(argv[1], '1')))
     {... ...}                   //此处省略,因为不执行
 else {       
      parse_inittab();              //argc==1,执行else,读取解析init 表(解析配置文件)
       }

.... ...                               //运行应用程序

}

通过函数名称可以猜测出,上面代码中parse_inittab()就是实现解析init表的

3.1.1接下来分析parse_inittab();函数是怎么读取解析init表:

由于argc=1,所以会进入到parse_inittab()中

该函数代码如下:

#define INITTAB      "/etc/inittab"           //定义INITTAB=/etc/inittab
static void parse_inittab(void)
{
file = fopen(INITTAB, "r");  //找到INITTAB定义,显然是打开 /etc/inittab 配置文件

/* 如果/etc/inittab无法打开,则调用new_init_action进行一些默认的操作配置 */
if (file == NULL) {         
     new_init_action(CTRLALTDEL, "reboot", "");
     new_init_action(SHUTDOWN, "umount -a -r", "");
   if (ENABLE_SWAPONOFF) new_init_action(SHUTDOWN, "swapoff -a", "");
     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);
     /* sysinit */
     new_init_action(SYSINIT, INIT_SCRIPT, "");
     return ;
     }

/* while一直循环解析file文件将里面的内容一行一行读出来,然后调用new_init_action进行操作*/
while (fgets(buf, INIT_BUFFS_SIZE, file) != NULL) {  
                   /* Skip leading spaces */
                   for (id = buf; *id == ' ' || *id == '\t'; id++);
                   /* Skip the line if it's a comment */
                   if (*id == '#' || *id == '\n')
                           continue;
                           ... ...
new_init_action(a->action, command, id);        //读完后调用new_init_action
     }}

显然parse_inittab()函数任务就是将配置文件内容读出来,然后调用new_init_action解析配置文件.

如果,上面函数中file == NULL,会配置出什么来?默认的配置文件里内容又是什么?

(1)首先我们分析new_init_action()函数

我们以上面的new_init_action(ASKFIRST, bb_default_login_shell, VC_2);为例来分析:

其中该函数定义为:

new_init_action(int action, const char *command, const char *cons)
{... ...}

首先搜索这3个实参 ASKFIRST, bb_default_login_shell, VC_2:

其中ASKFIRST=0X04;

bb_default_login_shell[]="-/bin/sh"

VC_2= "/dev/tty2"

其中参数定义:

0X04(action): 等于配置文件的action(运行时机,指应用程序何时(action)行动)

"-/bin/sh"(*command): 等于配置文件的process(应用程序)

"/dev/tty2"( *cons) :等于配置文件的id (终端,这里使用的tty2终端)

接下来分析new_init_action(0x04,"-/bin/sh","/dev/tty2")函数:

# define bb_dev_null "/dev/null"                 //定义bb_dev_null等于"/dev/null"
static struct init_action *init_action_list = NULL;     //定义init_action型结构体链表

static void new_init_action(int action, const char *command, const char *cons)  //函数开始
{
/*
先介绍init_action结构体,定义如下:
struct init_action {
struct init_action *next;      //指向下一个init_action结构体,用于链表
int action;                       //执行时机,用于何时执行
pid_t pid;                       //process id(进程号)
char command[INIT_BUFFS_SIZE];  //应用程序或者脚本, 就是要启动的进程。
char terminal[CONSOLE_NAME_SIZE]; //终端
};
*/

/*定义init_action型指针, *new_action:指新的结构体*/
struct init_action *new_action, *a, *last;       

/*判断cons是否 "/"开头 */
if (strcmp(cons, bb_dev_null) == 0 && (action & ASKFIRST))
           return; 

/* a和last都等于init_action_list 链表,a始终指向下一个结构体,查找是否有相同的command和termin*/
     for (a = last = init_action_list; a; a = a->next)
   {
         /*找到有相同的command和termin,则只更新action执行时机参数,并return*/
       if ((strcmp(a->command, command) == 0)&& (strcmp(a->terminal, cons) == 0))
       {a->action = action;
       return;}

     /*更新last,等于上一个init_action 结构体*/
        last = a;
   }

new_action = xzalloc(sizeof(struct init_action));  //为new_action分配内存,使它成为静态变量,不释放
if (last) {       //last!=NULL,说明init_action_list当前有内容,将链表下一个节点等于new_action
           last->next = new_action;           }
   else {      //last==NULL,说明init_action_list里面还没有内容,直接将链表等于new_action
         init_action_list = new_action;           }
strcpy(new_action->command, command);  //更新当前链表里的command
new_action->action = action;             //更新链表里的action
strcpy(new_action->terminal, cons);      //更新链表里的command
messageD(L_LOG | L_CONSOLE, "command='%s' action=%d tty='%s'\n",
new_action->command, new_action->action, new_action->terminal);
}

所以new_init_action()解析配置文件,就是将配置文件中的配置格式放在init_action链表中.

(2)然后通过new_init_action()函数反推出parse_inittab()函数中file==NULL情况下的默认配置文件:

其中配置文件格式: <id>:<runlevels>:<action>:<process>

idid 会等于/dev/id, 用做终端,可以忽略使用从控制台输入输出。  

runlevels:可以被忽略

action: 运行时机,指应用程序何时(action)行动,它有sysinit, respawn, askfirst, wait, once,restart, ctrlaltdel, andshutdown.这些值可选择。

process:应用程序或者脚本, 就是要启动的进程。

(2.1) 然后逐步反推代码:

if (file == NULL) {         

/*ID为空, runlevels忽略, action= ctrlaltdel, process= reboot */
new_init_action(CTRLALTDEL, "reboot", "");     

/*ID为空, runlevels忽略, action= shutdown, process= umount -a -r */
new_init_action(SHUTDOWN, "umount -a -r", ""); 

/* ENABLE_SWAPONOFF 未定义,不分析*/if (ENABLE_SWAPONOFF) new_init_action(SHUTDOWN, "swapoff -a", "");  

/*ID为空, runlevels忽略, action= restart, process= init */
new_init_action(RESTART, "init", ""); 

/*ID为空, runlevels忽略, action= askfirst, process= -/bin/sh */
/* 其中bb_default_login_shell ="-/bin/sh" */
new_init_action(ASKFIRST, bb_default_login_shell, "");
 
/*ID=/dev/tty2, runlevels忽略, action= askfirst, process=-/bin/sh */
new_init_action(ASKFIRST, bb_default_login_shell, VC_2); //VC_2= "/dev/tty2"
 
/* ID=/dev/tty3, runlevels忽略, action= askfirst, process=-/bin/sh */
new_init_action(ASKFIRST, bb_default_login_shell, VC_3);  // VC_3= "/dev/tty3"
 
/* ID=/dev/tty4,runlevels忽略, action= askfirst, process=-/bin/sh */
new_init_action(ASKFIRST, bb_default_login_shell, VC_4); // VC_4= "/dev/tty3"
 
/* sysinit */
/*ID为空, runlevels忽略, action= sysinit, process= etc/init.d/rcS */

new_init_action(SYSINIT, INIT_SCRIPT, ""); //INIT_SCRIPT="etc/init.d/rcS"

return ;
}

(2.2)根据配置文件格式<id>:<runlevels>:<action>:<process>,得出最终默认的配置文件内容如下:

:: ctrlaltdel:reboot          //当按下Ctrl+Alt+Delete组合键时,会执行reboot

:: shutdown:umount -a -r      //  告诉init,在系统关机的时候执行umount命令卸载所有文件系统,失败则以读模式安装

:: restart:init                //init重启时,指定执行init进程

:: askfirst: -/bin/sh          //启动-/bin/sh之前不显示,等待用户按enter键

/dev/tty2:: askfirst:-/bin/sh      //启动-/bin/sh之前在终端tty2上显示信息,并等待用户按enter键

/dev/tty3:: askfirst:-/bin/sh      //启动-/bin/sh之前在终端tty3上显示信息,并等待用户按enter键

/dev/tty4:: askfirst:-/bin/sh         //启动-/bin/sh之前在终端tty4上显示信息,并等待用户按enter键

:: askfirst:etc/init.d/rcS           //启动etc/init.d/rcS之前在终端tty4上显示信息,并等待用户按enter键

从上面发现init进程里分了很多个子进程,每个子进程都需要3样:

id(可以为空),action(运行时机,必须小写),process(指定要运行的应用程序位置)

parse_inittab()函数到这里就分析完毕,它主要就是将配置文件读出来解析,然后放在链表init_action_list中

3.2 接下来继续分析int_main()后面如何运行应用程序的,简写代码如下:

int init_main(int argc, char **argv)
{
 ... ...
 console_init();                //初始化控制台,在init_post()中只是打开了控制台设备
 ... ...

if (argc > 1      //在init_post()中执行的”_init_process("/sbin/init");”,所以argc=1, argv=/sbin/init
        && (!strcmp(argv[1], "single") || !strcmp(argv[1], "-s") || LONE_CHAR(argv[1], '1')))
{... ...}                   //此处省略,因为不执行
 else {       
                 parse_inittab();              //读取解析init 表(解析配置文件)
       }
....
  
/* First run the sysinit command */
run_actions(SYSINIT);    /*首先运行系统初始化的链表节点(SYSINIT:等待运行结束为止)*/
/* Next run anything that wants to block */
run_actions(WAIT);      //运行 action = WAIT的链表节点(WAIT:等待运行结束为止)
/* Next run anything to be run only once */
run_actions(ONCE);           //运行 action = ONCE的链表节点(ONCE:不会等待运行)
while (1) {
  run_actions(RESPAWN);    //运行 action = RESPAWN的链表节点(pid==0时才能运行) 
  run_actions(ASKFIRST); //运行action = ASKFIRST的链表节点(pid==0时才能运行,且还需要等待回车)

  sleep(1);                  //让CPU等待会儿
  wpid = wait(NULL);        //等待上面两个的子进程退出

  while (wpid > 0)              //退出后设置pid=0,然后while重新运行RESPAWN&& ASKFIRST
    { a->pid = 0;}           
      }

}

从上面得出run_actions()函数就是用来链表节点里的应用程序.

且 ASKFIRST和 RESPAWN会在while中一直运行.

3.3分析上面run_actions ()函数是怎么运行链表节点的,代码如下:

static void run_actions(int action)       //执行时机参数
{
  struct init_action *a, *tmp;
   for (a = init_action_list; a; a = tmp)            //从链表init_action_list中循环查找
  {tmp = a->next;                           //指向下一个链表的节点
     if (a->action == action)                    //找到相同名称的action节点了
    {        if (a->terminal[0] && access(a->terminal, R_OK | W_OK))
         { delete_init_action(a);}   //已经使用过该应用程序,从链表中删除

            /* SYSINIT|WAIT|CTRLALTDEL|SHUTDOWN|RESTART这些的应用程序都需要等待执行完毕 */
        else if (a->action & (SYSINIT | WAIT | CTRLALTDEL | SHUTDOWN | RESTART))
        {  waitfor(a, 0); //(0:ID号为空) 执行a节点的应用程序,然后等待它执行完毕      
          delete_init_action(a);  //然后从链表init_action_list中删除(delete)
        } 
        else if (a->action & ONCE) //action(运行时机)=ONCE时,不需要等待执行完毕
       {run(a);                    //创建子进程后即删除该节点
        delete_init_action(a);        }         
         //action(运行时机)= RESPAWN | ASKFIRST时,也不需要等待执行完毕
         else if (a->action & (RESPAWN | ASKFIRST))
       { if (a->pid == 0) {a->pid = run(a);}  }    //a->pid==0才run(a)创建子进程
    }
  }
}

通过上面代码分析出执行waitfor()时,需要等待应用程序执行完毕,

执行run()时,不需要等待.

3.4先分析上面waitfor(a, 0)函数是怎么实现执行应用程序然后等待的?

waitfor代码如下:

static int waitfor(const struct init_action *a, pid_t pid) //*a:链表中的一个节点
{
         int runpid;
         int status, wpid;
         /*run(a):创建<process>子进程(运行应用程序)*/
         runpid = (NULL == a)? pid : run(a);  //当a==NULL,runpid=pid=0,否则runpid=run(a).
         while (1) {
                   wpid = waitpid(runpid, &status, 0);  //等待应用程序执行完毕
                   if (wpid == runpid)
                            break;
                   if (wpid == -1 && errno == ECHILD) {
                            /* we missed its termination */
                            break;
                   }

                   /* FIXME other errors should maybe trigger an error, but allow
                    * the program to continue */
         }
         return wpid;
}

最终waitfor还是调用的run(a),所以这些所有节点都会调用run(a)来创建<process>子进程(运行应用程序).然后在while中循环运行action=(RESPAWN| ASKFIRST)的节点

3.2.3 , 除了没分析run(a)以外,RESPAWN和ASKFIRST还是没懂什么不同.

RESPAWN和ASKFIRST到底有什么不同,就需要分析run(a)了

代码如下:

static pid_t run(const struct init_action *a) //*a:链表中的一个节点   
{
 .. ...
     if (a->action & (SYSINIT | WAIT | CTRLALTDEL | SHUTDOWN | RESTART)) 
          {... ...}        //只分析RESPAWN和ASKFIRST有什么不同,所以此处省略
     if (a->action & ASKFIRST)  //action==ASKFIRST的时候
      {
         /*打印\nPlease press Enter to activate this console.(请按回车键启动控制台.)*/
       tatic const char press_enter[] ALIGN1 ="\nPlease press Enter to activate this console. ";
       char c;
       ... ...
       while (read(0, &c, 1) == 1 && c != '\n');  //一直等待用户回车
       }

BB_EXECVP(cmdpath, cmd);          //创建子进程

}

从上面分析出,当执行action=RESPAWN时,只创建子进程,而action=ASKFIRST时,需要一直等待用户回车才创建子进程

4.通过前面的分析,制作一个最小的根文件系统至少需要:

(1)/dev/console(终端控制台, 提供标准输入、标准输出以及标准错误)

/dev/null  (为空的话就是/dev/null, 所有写到这个文件中的数据都会被丢弃掉。)

 (2) init进程的程序(也就是busybox,因为init程序位于busybox中)

(3)/etc/inittab(用于init进程读取配置, 然后执行inittab里的指定应用程序)

(4)应用程序(被inittab配置文件调用的应用程序)

(5)C库(被应用程序调用的C库函数,比如:printf,strcmp,fopen等)

init进程分析完毕,接下来开始通过上面的需要来制作一个最小文件系统.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏飞雪无情的博客

Go语言实战笔记(一)| Go包管理

这本是In Action系列的书籍,这个系列做研发的都知道,在研发届评价很多,很多新的技术、语言等都会有一本实战的书籍。既然是实战,那么这本书假设了他的读者有了...

673
来自专栏Fundebug

20个Chrome DevTools调试技巧

为了保证可读性,本文采用意译而非直译。另外,本文版权归原作者所有,翻译仅用于学习。

773
来自专栏知晓程序

开发 | 小程序跨页传值的的问题,这个 JS 库就能解决(内附项目地址)

最近微信小程序越来越火,不少公司都在开发微信小程序,将其他平台的代码转为小程序的代码。

592
来自专栏编程直播室

使用Ionic2开发Todo应用0 开始之前1 创建新的Ionic 2工程2. 设置主页(Home page)3 持久化数据保存4 总结

2015
来自专栏PHP实战技术

PHP模拟栈

如果说栈这个词,大家可能不是很清楚,但是说先进先出,后进先出大家可能就会反映出队列和栈

693
来自专栏施炯的IoT开发专栏

《101 Windows Phone 7 Apps》读书笔记-NOTEPAD

课程内容 Ø 读写文件     Notepad提供了快速、高效的记录手段,它具有以下的特点: Ø 自动保存,使得速记变得快速而简单。 Ø 提供每条笔记的快...

18410
来自专栏落影的专栏

编译与链接过程的思考

前言 最近遇到一个错误,如下 ? 在解决过程中,回顾了很多知识,于是有了这篇文章。 关键词:预处理、编译、汇编、链接、动态链接库、静态链接库、真机调...

4359
来自专栏Greenplum

Linux 常用命令(三)

curl 命令支持在线下载功能,使用方便,它支持,S,等协议,还支持PUT,POST,COOKIES,认证授权等操作。

130
来自专栏帘卷西风的专栏

修改lua的文件加载器,自定义lua文件加载

require使用的路径和普通我们看到的路径还有些区别,我们一般见到的路径都是一个目录列表。require的路径是一个模式列表,每一个模式指明一种由虚文件名(r...

683
来自专栏haley的分享

内核timer crash debug思路

思路:内核timer有回调函数timer->function,可以由function查到crash的内核模块,找到bug的蛛丝马迹。

995

扫码关注云+社区