1. 程式人生 > >第4階段——制作根文件系統之分析init_post()如何啟動第1個程序(2)

第4階段——制作根文件系統之分析init_post()如何啟動第1個程序(2)

函數 mage cpu 腳本 init block ebo pat images

本節目標:

(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>

id: id 會等於/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進程分析完畢,接下來開始通過上面的需要來制作一個最小文件系統.

第4階段——制作根文件系統之分析init_post()如何啟動第1個程序(2)