1. 程式人生 > >Android6.0系統啟動流程分析一:init程序

Android6.0系統啟動流程分析一:init程序

到了Android6.0,Init程序使用c++來寫了,不過沒有關係,它和c寫的init沒有太大的區別。
Init程序的入口程式碼是:system\core\init\init.cpp
main函式:


int main(int argc, char** argv) {
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }

    if (!strcmp(basename(argv[0]), "watchdogd")) {
        return
watchdogd_main(argc, argv); } // Clear the umask. umask(0); add_environment("PATH", _PATH_DEFPATH); bool is_first_stage = (argc == 1) || (strcmp(argv[1], "--second-stage") != 0); // Get the basic filesystem setup we need put together in the initramdisk // on / and then we'll let the rc file figure out the rest.
if (is_first_stage) { mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"); mkdir("/dev/pts", 0755); mkdir("/dev/socket", 0755); mount("devpts", "/dev/pts", "devpts", 0, NULL); mount("proc", "/proc", "proc", 0, NULL); mount("sysfs", "/sys", "sysfs", 0, NULL); } // We must have some place other than / to create the device nodes for
// kmsg and null, otherwise we won't be able to remount / read-only // later on. Now that tmpfs is mounted on /dev, we can actually talk // to the outside world. open_devnull_stdio(); klog_init(); klog_set_level(KLOG_NOTICE_LEVEL); NOTICE("init%s started!\n", is_first_stage ? "" : " second stage"); if (!is_first_stage) { // Indicate that booting is in progress to background fw loaders, etc. close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000)); property_init(); // If arguments are passed both on the command line and in DT, // properties set in DT always have priority over the command-line ones. process_kernel_dt(); process_kernel_cmdline(); // Propogate the kernel variables to internal variables // used by init as well as the current required properties. export_kernel_boot_props(); } // Set up SELinux, including loading the SELinux policy if we're in the kernel domain. selinux_initialize(is_first_stage); // If we're in the kernel domain, re-exec init to transition to the init domain now // that the SELinux policy has been loaded. if (is_first_stage) { if (restorecon("/init") == -1) { ERROR("restorecon failed: %s\n", strerror(errno)); security_failure(); } char* path = argv[0]; char* args[] = { path, const_cast<char*>("--second-stage"), nullptr }; if (execv(path, args) == -1) { ERROR("execv(\"%s\") failed: %s\n", path, strerror(errno)); security_failure(); } } // These directories were necessarily created before initial policy load // and therefore need their security context restored to the proper value. // This must happen before /dev is populated by ueventd. INFO("Running restorecon...\n"); restorecon("/dev"); restorecon("/dev/socket"); restorecon("/dev/__properties__"); restorecon_recursive("/sys"); epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (epoll_fd == -1) { ERROR("epoll_create1 failed: %s\n", strerror(errno)); exit(1); } signal_handler_init(); property_load_boot_defaults(); start_property_service(); init_parse_config_file("/init.rc"); action_for_each_trigger("early-init", action_add_queue_tail); // Queue an action that waits for coldboot done so we know ueventd has set up all of /dev... queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done"); // ... so that we can start queuing up actions that require stuff from /dev. queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng"); queue_builtin_action(keychord_init_action, "keychord_init"); queue_builtin_action(console_init_action, "console_init"); // Trigger all the boot actions to get us started. action_for_each_trigger("init", action_add_queue_tail); // Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random // wasn't ready immediately after wait_for_coldboot_done queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng"); // Don't mount filesystems or start core system services in charger mode. char bootmode[PROP_VALUE_MAX]; if (property_get("ro.bootmode", bootmode) > 0 && strcmp(bootmode, "charger") == 0) { action_for_each_trigger("charger", action_add_queue_tail); } else { action_for_each_trigger("late-init", action_add_queue_tail); } // Run all property triggers based on current state of the properties. queue_builtin_action(queue_property_triggers_action, "queue_property_triggers"); while (true) { if (!waiting_for_exec) { execute_one_command(); restart_processes(); } int timeout = -1; if (process_needs_restart) { timeout = (process_needs_restart - gettime()) * 1000; if (timeout < 0) timeout = 0; } if (!action_queue_empty() || cur_action) { timeout = 0; } bootchart_sample(&timeout); epoll_event ev; int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout)); if (nr == -1) { ERROR("epoll_wait failed: %s\n", strerror(errno)); } else if (nr == 1) { ((void (*)()) ev.data.ptr)(); } } return 0; }

1.這個函式是否往下執行取決於傳入的引數,如果第0個引數的basename為ueventd,則執行ueventd_main(argc, argv);如果basename為watchdogd_main,則執行watchdogd_main(argc, argv);只有basename不為這二者時,才會繼續往下執行。
2.如果argv[1]不為”–second-stage”或者只有一個引數的話,那麼is_first_stage就為true,就會建立/dev/pts和”/dev/socket”兩個裝置檔案節點,並掛載一個檔案系統。可以看出來init程序分兩個階段,不同的階段有不同的行為。具體的內涵鄙人還沒搞明白。
3.啟動屬性服務。建立一個socket,並在之後的死迴圈中監聽這個socket返回的檔案描述符。
3.解析init.rc。這個過程也是我最感興趣的,也是最重要的複雜的。
4.對各個階段的action排序。
5.進入死迴圈。
6.第一次進入死迴圈後,action_queue裡面有很多時間,因此需要不斷呼叫execute_one_command來執行命令。此時,action_queue_empty為假,timeout 為0,init執行緒不會在epoll_wait方法中休眠,因為設定的timeout=0哦,這一點曾一度困擾了我。
7.所有的命令執行完後,init程序進入休眠,監聽property_set_fd和signal_read_fd兩個檔案描述符,一點他們有事件過來,立刻被喚醒,進而做事件處理。

init.rc梳理

在我們分析init.rc的解析過程之前,我們還需要先對init.rc有個基本的認識。
先看一張我根據理解繪製的圖:
這裡寫圖片描述
從圖來看,init.rc主要有section組成,section由on,import,section三個關鍵字標示。其中on標示的section叫做action。
import就不用說了,和c語言中的include功能有點類似。
service格式如下

service <name> <pathname> [ <argument> ]*  
   <option>  
   <option>  
   ...  

action後面會跟一個觸發器,然後另起一行開始放置命令(command),格式如下:

on <trigger>  
   <command>  
   <command>  
   <command>  

跟在service後面的是option,跟在action後面的是command.command都會對應一個處理函式,定義在keywords.h中:

...
    KEYWORD(loglevel,    COMMAND, 1, do_loglevel)
    KEYWORD(mkdir,       COMMAND, 1, do_mkdir)
    KEYWORD(mount_all,   COMMAND, 1, do_mount_all)
    KEYWORD(mount,       COMMAND, 3, do_mount)
    ...

命名也是很有規則的。比如mkdir,對應的函式就是do_mkdir。我們看看do_mkdir做了什麼:

int do_mkdir(int nargs, char **args)
{
    mode_t mode = 0755;
    int ret;

    /* mkdir <path> [mode] [owner] [group] */

    if (nargs >= 3) {
        mode = strtoul(args[2], 0, 8);
    }

    ret = make_dir(args[1], mode);
    /* chmod in case the directory already exists */
    if (ret == -1 && errno == EEXIST) {
        ret = fchmodat(AT_FDCWD, args[1], mode, AT_SYMLINK_NOFOLLOW);
    }
    if (ret == -1) {
        return -errno;
    }

    if (nargs >= 4) {
        uid_t uid = decode_uid(args[3]);
        gid_t gid = -1;

        if (nargs == 5) {
            gid = decode_uid(args[4]);
        }

        if (lchown(args[1], uid, gid) == -1) {
            return -errno;
        }

        /* chown may have cleared S_ISUID and S_ISGID, chmod again */
        if (mode & (S_ISUID | S_ISGID)) {
            ret = fchmodat(AT_FDCWD, args[1], mode, AT_SYMLINK_NOFOLLOW);
            if (ret == -1) {
                return -errno;
            }
        }
    }

    return e4crypt_set_directory_policy(args[1]);
}

其實就是呼叫了make_dir並做了一些許可權等方面的操作。所以,跟在action後面的命令並不能隨隨便便亂加,而是要確保這個命令被定義了,不然就會出錯。

init.rc的解析過程(以import為例)

因為init.rc的第一行程式碼就是Import語句。萬事開頭難,只要我們理清了第一行的解析過程,後面行的解析分析起來就不怎麼費勁了。所以下面我們主要看看init.rc中第一行的解析過程。
init.tc的解析函式為:init_parse_config_file

int init_parse_config_file(const char* path) {
    INFO("Parsing %s...\n", path);
    Timer t;
    std::string data;
    if (!read_file(path, &data)) {
        return -1;
    }

    data.push_back('\n'); // TODO: fix parse_config.
    parse_config(path, data);
    dump_parser_state();

    // MStar Android Patch Begin
    INFO("(Parsing %s took %.2fs.)\n", path, t.duration());
    // MStar Android Patch End
    return 0;
}

這個函式把/init.rc中的內容讀出來,並讓data這個string型別的變數指向它。
把讀出來的data傳遞給parse_config函式做真正的解析工作。parse_config函式如下:

static void parse_config(const char *fn, const std::string& data)
{
    char *args[UEVENTD_PARSER_MAXARGS];

    int nargs = 0;
    parse_state state;
    state.filename = fn;
    state.line = 1;
    state.ptr = strdup(data.c_str());  // TODO: fix this code!
    state.nexttoken = 0;
    state.parse_line = parse_line_no_op;
    for (;;) {
        int token = next_token(&state);
        switch (token) {
        case T_EOF:
            parse_line(&state, args, nargs);
            return;
        case T_NEWLINE:
            if (nargs) {
                parse_line(&state, args, nargs);
                nargs = 0;
            }
            state.line++;
            break;
        case T_TEXT:
            if (nargs < UEVENTD_PARSER_MAXARGS) {
                args[nargs++] = state.text;
            }
            break;
        }
    }
}

我看到這個函式的時候,我想起了xml解析方法之一的pull解析,感覺挺像的。每次迴圈都會找到一個token,token就是一個特定的符號,然後根據這個toke做不同的處理。這裡使用到了parse_state結構,啟動以如下:

struct parse_state
{
    char *ptr;
    char *text;
    int line;
    int nexttoken;
    void *context;
    void (*parse_line)(struct parse_state *state, int nargs, char **args);
    const char *filename;
    void *priv;
};

這個就夠中:ptr執行init.rc字元流的,text後面會用到,用來儲存引數,line當然就是行數了,nexttoken儲存下一個token,filename儲存init.rc的檔案描述符,filename當然是/init.rc了.parse_line是一個函式指標。context暫時沒明白…state.priv 指向Import的一個檔案連結串列。
我們開啟Init.rc看看,從頭分析它的解析過程。

import /init.environ.rc
import /init.usb.rc
import /init.${ro.hardware}.rc
import /init.${ro.zygote}.rc
import /init.trace.rc
...

init.rc前面幾行都是import語句,我們看看一開始的解析流程。
這個時候,parse_satate的狀態為:

    state.filename = fn;
    state.line = 1;
    state.ptr = strdup(data.c_str());  // TODO: fix this code!
    state.nexttoken = 0;
    state.parse_line = parse_line_no_op;
        list_init(&import_list);
    state.priv = &import_list;

step 1.第一次迴圈

然後進入死迴圈,第一次呼叫next_token函式:

int next_token(struct parse_state *state)
{
    char *x = state->ptr;
    char *s;

    if (state->nexttoken) {
        int t = state->nexttoken;
        state->nexttoken = 0;
        return t;
    }

    for (;;) {
        switch (*x) {
        case 0:
            state->ptr = x;
            return T_EOF;
        case '\n':
            x++;
            state->ptr = x;
            return T_NEWLINE;
        case ' ':
        case '\t':
        case '\r':
            x++;
            continue;
        case '#':
            while (*x && (*x != '\n')) x++;
            if (*x == '\n') {
                state->ptr = x+1;
                return T_NEWLINE;
            } else {
                state->ptr = x;
                return T_EOF;
            }
        default:
            goto text;
        }
    }

textdone:
    state->ptr = x;
    *s = 0;
    return T_TEXT;
text:
    state->text = s = x;
textresume:
    for (;;) {
        switch (*x) {
        case 0:
            goto textdone;
        case ' ':
        case '\t':
        case '\r':
            x++;
            goto textdone;
        case '\n':
            state->nexttoken = T_NEWLINE;
            x++;
            goto textdone;
        case '"':
            x++;
            for (;;) {
                switch (*x) {
                case 0:
                        /* unterminated quoted thing */
                    state->ptr = x;
                    return T_EOF;
                case '"':
                    x++;
                    goto textresume;
                default:
                    *s++ = *x++;
                }
            }
            break;
        case '\\':
            x++;
            switch (*x) {
            case 0:
                goto textdone;
            case 'n':
                *s++ = '\n';
                break;
            case 'r':
                *s++ = '\r';
                break;
            case 't':
                *s++ = '\t';
                break;
            case '\\':
                *s++ = '\\';
                break;
            case '\r':
                    /* \ <cr> <lf> -> line continuation */
                if (x[1] != '\n') {
                    x++;
                    continue;
                }
            case '\n':
                    /* \ <lf> -> line continuation */
                state->line++;
                x++;
                    /* eat any extra whitespace */
                while((*x == ' ') || (*x == '\t')) x++;
                continue;
            default:
                    /* unknown escape -- just copy */
                *s++ = *x++;
            }
            continue;
        default:
            *s++ = *x++;
        }
    }
    return T_EOF;
}

這時候,init.rc中的第一個符號應該是i(impor,省去空格),所以next_token直接進入到text:標籤執行,執行的結果是state->text = s = ‘i’;然後繼續執行textresume:標籤後面的內容:
標籤後面的for死迴圈中,發現第一個字元是i,於是執行default分支:*s++ = *x++;這樣直到import的t被檢測完以後,在下一次迴圈變得到一個空格,於是執行x++,並goto textdone.。textdown執行完後函式返回,返回後,state->ptr 指向’/’符號 。*s = 0;意味著state.text就是字串“import”,因為0就是字串結束符了。注意返回值為T_TEXT。這個時候執行parse_config函式中的case T_TEXT:分支。

        case T_TEXT:
            if (nargs < INIT_PARSER_MAXARGS) {
                args[nargs++] = state.text;
            }

這個時候,nargs為0,state.text位import,於是args陣列的第0項就存了”import”字串了。然後nargs++,也就是等於1了。然後進入下次迴圈。

step 2.第二次迴圈

第二次迴圈再次呼叫next_token函式,這次state->ptr=’\’,這我們分析過了。因此next_token函式不斷執行defaulty分支,最終state.text = “/init.environ.rc”,返回型別還是T_TEXT。於是和之前一樣,args[1]=”/init.environ.rc”,nargs=2。

step 3.第三次迴圈

這個時候一行結束,parse_config函式進入case T_NEWLINE:分支。
這個分支中,首先執行lookup_keyword函式,從名字來看是查詢關鍵字。肯定就是import了,它肯定是關鍵字。不信請看程式碼:

static int lookup_keyword(const char *s)
{
    switch (*s++) {
    case 'b':
        if (!strcmp(s, "ootchart_init")) return K_bootchart_init;
        break;
    case 'c':
        if (!strcmp(s, "opy")) return K_copy;
        if (!strcmp(s, "lass")) return K_class;
        if (!strcmp(s, "lass_start")) return K_class_start;
        if (!strcmp(s, "lass_stop")) return K_class_stop;
        if (!strcmp(s, "lass_reset")) return K_class_reset;
        if (!strcmp(s, "onsole")) return K_console;
        if (!strcmp(s, "hown")) return K_chown;
        if (!strcmp(s, "hmod")) return K_chmod;
        if (!strcmp(s, "ritical")) return K_critical;
        break;
    case 'd':
        if (!strcmp(s, "isabled")) return K_disabled;
        if (!strcmp(s, "omainname")) return K_domainname;
        break;
    case 'e':
        if (!strcmp(s, "nable")) return K_enable;
        if (!strcmp(s, "xec")) return K_exec;
        if (!strcmp(s, "xport")) return K_export;
        break;
    case 'g':
        if (!strcmp(s, "roup")) return K_group;
        break;
    case 'h':
        if (!strcmp(s, "ostname")) return K_hostname;
        break;
    case 'i':
        if (!strcmp(s, "oprio")) return K_ioprio;
        if (!strcmp(s, "fup")) return K_ifup;
        if (!strcmp(s, "nsmod")) return K_insmod;
        if (!strcmp(s, "mport")) return K_import;
        if (!strcmp(s, "nstallkey")) return K_installkey;
        break;
    case 'k':
        if (!strcmp(s, "eycodes")) return K_keycodes;
        break;
    case 'l':
        if (!strcmp(s, "oglevel")) return K_loglevel;
        if (!strcmp(s, "oad_persist_props")) return K_load_persist_props;
        if (!strcmp(s, "oad_all_props")) return K_load_all_props;
        break;
    case 'm':
        if (!strcmp(s, "kdir")) return K_mkdir;
        if (!strcmp(s, "ount_all")) return K_mount_all;
        if (!strcmp(s, "ount")) return K_mount;
        break;
    case 'o':
        if (!strcmp(s, "n")) return K_on;
        if (!strcmp(s, "neshot")) return K_oneshot;
        if (!strcmp(s, "nrestart")) return K_onrestart;
        break;
    case 'p':
        if (!strcmp(s, "owerctl")) return K_powerctl;
        break;
    case 'r':
        if (!strcmp(s, "estart")) return K_restart;
        if (!strcmp(s, "estorecon")) return K_restorecon;
        if (!strcmp(s, "estorecon_recursive")) return K_restorecon_recursive;
        if (!strcmp(s, "mdir")) return K_rmdir;
        if (!strcmp(s, "m")) return K_rm;
        break;
    case 's':
        if (!strcmp(s, "eclabel")) return K_seclabel;
        if (!strcmp(s, "ervice")) return K_service;
        if (!strcmp(s, "etenv")) return K_setenv;
        if (!strcmp(s, "etprop")) return K_setprop;
        if (!strcmp(s, "etrlimit")) return K_setrlimit;
        if (!strcmp(s, "ocket")) return K_socket;
        if (!strcmp(s, "tart")) return K_start;
        if (!strcmp(s, "top")) return K_stop;
        if (!strcmp(s, "wapon_all")) return K_swapon_all;
        if (!strcmp(s, "ymlink")) return K_symlink;
        if (!strcmp(s, "ysclktz")) return K_sysclktz;
        break;
    case 't':
        if (!strcmp(s, "rigger")) return K_trigger;
        break;
    case 'u':
        if (!strcmp(s, "ser")) return K_user;
        break;
    case 'v':
        if (!strcmp(s, "erity_load_state")) return K_verity_load_state;
        if (!strcmp(s, "erity_update_state")) return K_verity_update_state;
        break;
    case 'w':
        if (!strcmp(s, "rite")) return K_write;
        if (!strcmp(s, "ritepid")) return K_writepid;
        if (!strcmp(s, "ait")) return K_wait;
        break;
    }
    return K_UNKNOWN;
}

呼叫這個函式的時候,我們傳入的引數args[0]=”import”.顯而易見該函式返回K_import。它是一個整數。返回以後使用kw_is函式看他是不是一個Section。當然是一個section了,import也是一個section。不信看程式碼:

#define kw_is(kw, type) (keyword_info[kw].flags & (type))

keyword_info定義在system/core/init/keywords.h中:

    ...
    KEYWORD(group,       OPTION,  0, 0)
    KEYWORD(hostname,    COMMAND, 1, do_hostname)
    KEYWORD(ifup,        COMMAND, 1, do_ifup)
    KEYWORD(import,      SECTION, 1, 0)
    ...

擷取含有import的一部分程式碼,後面SECTION已經表明它是個Section了。KEYWORD自後一個引數是這個關鍵字對應的處理函式。比如這其中的hostname。如果你在init.rc中使用hostname 關鍵字,那麼最終會呼叫do_hostname函式來處理。
既然import是一個section。那麼parce_config就會呼叫state.parse_line函式,這裡是一個函式指標,其實呼叫的是parse_line_no_op,不記得回去看下state的初始就知道了。

static void parse_line_no_op(struct parse_state*, int, char**) {
}

這個函式是空的。接下來呼叫parse_new_section函式:

static void parse_new_section(struct parse_state *state, int kw,
                       int nargs, char **args)
{
    printf("[ %s %s ]\n", args[0],
           nargs > 1 ? args[1] : "");
    switch(kw) {
    case K_service:
        state->context = parse_service(state, nargs, args);
        if (state->context) {
            state->parse_line = parse_line_service;
            return;
        }
        break;
    case K_on:
        state->context = parse_action(state, nargs, args);
        if (state->context) {
            state->parse_line = parse_line_action;
            return;
        }
        break;
    case K_import:
        parse_import(state, nargs, args);
        break;
    }
    state->parse_line = parse_line_no_op;
}

我們當然是執行case K_import:分支了,想都不用想。所以接下來執行parse_import方法:

static void parse_import(struct parse_state *state, int nargs, char **args)
{
    struct listnode *import_list = (listnode*) state->priv;
    char conf_file[PATH_MAX];
    int ret;

    if (nargs != 2) {
        ERROR("single argument needed for import\n");
        return;
    }

    ret = expand_props(conf_file, args[1], sizeof(conf_file));
    if (ret) {
        ERROR("error while handling import on line '%d' in '%s'\n",
              state->line, state->filename);
        return;
    }

    struct import* import = (struct import*) calloc(1, sizeof(struct import));
    import->filename = strdup(conf_file);
    list_add_tail(import_list, &import->list);
    INFO("Added '%s' to import list\n", import->filename);
}

這個函式首先使用expand_props方法對args[1]也就是“/init.environ.rc”做進一步處理。這個函式如下:

int expand_props(char *dst, const char *src, int dst_size)
{
    char *dst_ptr = dst;
    const char *src_ptr = src;
    int ret = 0;
    int left = dst_size - 1;

    if (!src || !dst || dst_size == 0)
        return -1;

    /* - variables can either be $x.y or ${x.y}, in case they are only part
     *   of the string.
     * - will accept $$ as a literal $.
     * - no nested property expansion, i.e. ${foo.${bar}} is not supported,
     *   bad things will happen
     */
    while (*src_ptr && left > 0) {
        char *c;
        char prop[PROP_NAME_MAX + 1];
        char prop_val[PROP_VALUE_MAX];
        int prop_len = 0;
        int prop_val_len;

        c = strchr(src_ptr, '$');
        if (!c) {
            while (left-- > 0 && *src_ptr)
                *(dst_ptr++) = *(src_ptr++);
            break;
        }
        ...

可以看出,這個函式的作用是拓展args[1].這裡不需要拓展,因為我們的args[1]=”/init.environ.rc”沒有$符號,所以直接就跳出迴圈了。這裡應該是對那些有包含變數的字串,把變數的內容展開。
然後構建了一個import結構體。import中的filename項賦值為”/init.environ.rc”.並把它加入到import_list連結串列中。
import定義如下:

struct import {
    struct listnode list;
    const char *filename;
};

這樣,第一行就分析完了,從而我們也徹底明白了怎麼解析一個import 的section。
只要能看懂一個,其他的就簡單了。因為,他們都是類似的。

service的解析與啟動

service的解析

和import解析過程類似,遇到service關鍵字後,service關鍵字和後面的引數會儲存在args[]陣列中。然後通過對args[0]提取關鍵字,發現args[0]=”service”,於是開始執行parse_new_section函式。此時這個函式必然會進入 case K_service:分支執行:

    case K_service:
        state->context = parse_service(state, nargs, args);
        if (state->context) {
            state->parse_line = parse_line_service;
            return;
        }
        break;

這裡做了兩件事情非常重要,一件是呼叫parse_service解析service這個section。另一件事情是給state->parse_line賦值為parse_line_service。也就是service 關鍵字所在的行後面的那些options行都是使用parse_line_service函式來解析的。我們從parse_service看起:

static void *parse_service(struct parse_state *state, int nargs, char **args)
{
    if (nargs < 3) {
        parse_error(state, "services must have a name and a program\n");
        return 0;
    }
    if (!valid_name(args[1])) {
        parse_error(state, "invalid service name '%s'\n", args[1]);
        return 0;
    }

    service* svc = (service*) service_find_by_name(args[1]);
    if (svc) {
        parse_error(state, "ignored duplicate definition of service '%s'\n", args[1]);
        return 0;
    }

    nargs -= 2;
    svc = (service*) calloc(1, sizeof(*svc) + sizeof(char*) * nargs);
    if (!svc) {
        parse_error(state, "out of memory\n");
        return 0;
    }
    svc->name = strdup(args[1]);
    svc->classname = "default";
    memcpy(svc->args, args + 2, sizeof(char*) * nargs);
    trigger* cur_trigger = (trigger*) calloc(1, sizeof(*cur_trigger));
    svc->args[nargs] = 0;
    svc->nargs = nargs;
    list_init(&svc->onrestart.triggers);
    cur_trigger->name = "onrestart";
    list_add_tail(&svc->onrestart.triggers, &cur_trigger->nlist);
    list_init(&svc->onrestart.commands);
    list_add_tail(&service_list, &svc->slist);
    return svc;
}

可以看到和import做的事情差不多。import解析的最後,會建立一個import結構體,並把它新增到import_list雙向連結串列中。service解析從這裡看,也是構建一個service結構體,然後把service結構體新增到service_list連結串列中。
我們看下service結構體:

struct service {
    void NotifyStateChange(const char* new_state);

        /* list of all services */
    struct listnode slist;

    char *name;
    const char *classname;

    unsigned flags;
    pid_t pid;
    time_t time_started;    /* time of last start */
    time_t time_crashed;    /* first crash within inspection window */
    int nr_crashed;         /* number of times crashed within window */

    uid_t uid;
    gid_t gid;
    gid_t supp_gids[NR_SVC_SUPP_GIDS];
    size_t nr_supp_gids;

    const char* seclabel;

    struct socketinfo *sockets;
    struct svcenvinfo *envvars;

    struct action onrestart;  /* Actions to execute on restart. */

    std::vector<std::string>* writepid_files_;

    /* keycodes for triggering this service via /dev/keychord */
    int *keycodes;
    int nkeycodes;
    int keychord_id;

    IoSchedClass ioprio_class;
    int ioprio_pri;

    int nargs;
    /* "MUST BE AT THE END OF THE STRUCT" */
    char *args[1];
}; /*     

socketinfo 用來儲存socket option的相關資訊。
classname 給service定義一個類名,如果多個service使用相同的型別,可以方便進行批量操作。
nargs 儲存引數的個數。
很多欄位不理解,沒關係,我們看看parse_service函式給service做了那些初始化:
1. svc->name = strdup(args[1]);名字就是service 關鍵字後面的第一個引數
2. svc->classname = “default”; 類別名是default
3. memcpy(svc->args, args + 2, sizeof(char*) * nargs); svc->args[nargs] = 0;
把所有引數儲存在args陣列中,並把最有一個成員只為0。
4. svc->nargs = nargs; nargs儲存引數個數
5. trigger* cur_trigger = (trigger*) calloc(1, sizeof(*cur_trigger));
list_init(&svc->onrestart.triggers);
cur_trigger->name = “onrestart”;
list_add_tail(&svc->onrestart.triggers, &cur_trigger->nlist);
構建一個觸發器,並把它新增到service中的onrestart.triger列表中。
因此,我們可以知道麼一個service都會有一個onrestart的action,這個action有一個觸發器。這個action用來重啟service。
解析完service後,就會解析service後面的option了,這個時候會呼叫parse_line_service。

static void parse_line_service(struct parse_state *state, int nargs, char **args)
{
    struct service *svc = (service*) state->context;
    struct command *cmd;
    int i, kw, kw_nargs;

    if (nargs == 0) {
        return;
    }

    svc->ioprio_class = IoSchedClass_NONE;

    kw = lookup_keyword(args[0]);
    switch (kw) {
    case K_class:
        if (nargs != 2) {
            parse_error(state, "class option requires a classname\n");
        } else {
            svc->classname = args[1];
        }
        break;
    case K_console:
        svc->flags |= SVC_CONSOLE;
        break;
    case K_disabled:
        svc->flags |= SVC_DISABLED;
        svc->flags |= SVC_RC_DISABLED;
        break;
    case K_ioprio:
        if (nargs != 3) {
            parse_error(state, "ioprio optin usage: ioprio <rt|be|idle> <ioprio 0-7>\n");
        } else {
            svc->ioprio_pri = strtoul(args[2], 0, 8);

            if (svc->ioprio_pri < 0 || svc->ioprio_pri > 7) {
                parse_error(state, "priority value must be range 0 - 7\n");
                break;
            }

            if (!strcmp(args[1], "rt")) {
                svc->ioprio_class = IoSchedClass_RT;
            } else if (!strcmp(args[1], "be")) {
                svc->ioprio_class = IoSchedClass_BE;
            } else if (!strcmp(args[1], "idle")) {
                svc->ioprio_class = IoSchedClass_IDLE;
            } else {
                parse_error(state, "ioprio option usage: ioprio <rt|be|idle> <0-7>\n");
            }
        }
        break;
    case K_group:
        if (nargs < 2) {
            parse_error(state, "group option requires a group id