Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android中的ueventd

Android中的ueventd

編輯:關於Android編程

前言
與Linux相同,Android中的應用程序通過設備驅動訪問硬件設備。設備節點文件是設備驅動的邏輯文件,應用程序使用設備節點文件來訪問驅動程序。

在Linux中,運行所需的設備節點文件被被預先定義在“/dev”目錄下。應用程序無需經過其它步驟,通過預先定義的設備節點文件即可訪問設備驅動程序。
但根據Android的init進程的啟動過程,我們知道,Android根文件系統的映像中不存在“/dev”目錄,該目錄是init進程啟動後動態創建的。
因此,建立Anroid中設備節點文件的重任,也落在了init進程身上。為此,init進程創建子進程ueventd,並將創建設備節點文件的工作托付給ueventd。

ueventd通過兩種方式創建設備節點文件。
第一種方式對應“冷插拔”(Cold Plug),即以預先定義的設備信息為基礎,當ueventd啟動後,統一創建設備節點文件。這一類設備節點文件也被稱為靜態節點文件。
第二種方式對應“熱插拔”(Hot Plug),即在系統運行中,當有設備插入USB端口時,ueventd就會接收到這一事件,為插入的設備動態創建設備節點文件。這一類設備節點文件也被稱為動態節點文件。

版本
android 6.0

背景知識
I
在Linux內核2.6版本之前,用戶必須直接創建設備節點文件。創建時,必須保證設備文件的主次設備號不發生重疊,再通過mknod進行實際地創建。這樣做的缺點是,用戶必須記住各個設備的主設備號和次設備號,還要避免設備號之間發生沖突,操作起來較為麻煩。
為了彌補這一不足,從內核2.6x開始引入udev(userspace device),udev以守護進程的形式運行。當設備驅動被加載時,它會掌握主設備號、次設備號,以及設備類型,而後在“/dev”目錄下自動創建設備節點文件。
從加載設備驅動到udev創建設備節點文件的整個過程如下圖所示:

 

這裡寫圖片描述

 

在系統運行中,若某個設備被插入,內核就會加載與該設備相關的驅動程序。
接著,驅動程序的啟動函數probe將被調用(定義於設備驅動程序中,由內核自動調用,用來初始化設備),將主設備號、次設備號、設備類型保存到“/sys”文件系統中。
然後,驅動程序發送uevent給udev守護進程。
最後,udev通過分析內核發出的uevent,查看注冊在/sys目錄下的設備信息,以在/dev目錄相應位置上創建節點文件。

II
uevent是內核向用戶空間進程傳遞信息的信號系統,即在添加或刪除設備時,內核使用uevent將設備信息傳遞到用戶空間。uevent包含設備名稱、類別、主設備號、次設備號、設備節點文件需要被創建的目錄等信息。

III
系統內核啟動後,udev進程運行在用戶空間內,它無法處理內核啟動過程中發生的uevent。雖然內核空間內的設備驅動程序可以正常運行,但由於未創建設備訪問驅動所需的設備節點文件,將會出現應用程序無法使用相關設備的問題。
Linux系統中,通過冷插拔機制來解決該問題。當內核啟動後,冷插拔機制啟動udev守護進程,從/sys目錄下讀取實現注冊好的設備信息,而後引發與各設備對應的uevent,創建設備節點。

總結一下:
熱插拔時,設備連接後,內核調用驅動程序加載信息到/sys下,然後驅動程序發送uevent到udev;
冷插拔時,udev主動讀取/sys目錄下的信息,然後觸發uevent給自己處理。之所以要有冷插拔,是因為內核加載部分驅動程序信息的時間,早於啟動udev的時間。

接下來,我們看看Android中的ueventd是怎麼做的。

正文
一、啟動ueventd

......
init_parse_config_file("/init.rc");

action_for_each_trigger("early-init", action_add_queue_tail);
......

在init進程的啟動過程中,解析完init.rc文件後,首先將“early-init”對應的action加入到運行隊列中。因此,當init進程開始處理運行隊列中的事件時,首先會處理該action。

on early-init
    # Set init and its forked children's oom_adj.
    write /proc/1/oom_score_adj -1000

    # Set the security context of /adb_keys if present.
    restorecon /adb_keys

    start ueventd

如上所示,為init.rc內“early-init”對應的action,我們可以看到,將執行start ueventd的命令。

根據keywords.h中的定義,我們知道action的start關鍵字,對應函數do_start,定義於system/core/init/builtins.cpp中:

int do_start(int nargs, char **args)
{
    struct service *svc;
    svc = service_find_by_name(args[1]);
    if (svc) {
        service_start(svc, NULL);
    }
    return 0;
}

如上代碼所示,do_start函數通過service_find_by_name函數,從service_list鏈表中,根據參數找到需啟動的service,然後調用service_start函數啟動service。

service_start函數定義於init.cpp文件中:

void service_start(struct service *svc, const char *dynamic_args) {
    ..............
    pid_t pid = fork();
    if (pid == 0) {
        ........
        if (!dynamic_args) {
            if (execve(svc->args[0], (char**) svc->args, (char**) ENV) < 0) {
                ..............
            }
        } else {
            ............
            execve(svc->args[0], (char**) arg_ptrs, (char**) ENV);
    }
}

該函數對參數進行檢查後,利用fork函數創建出子進程,然後按照service在init.rc中的定義,對service進行配置,最後調用Linux系統函數execve啟動service。

二、ueventd的主要工作
ueventd_main定義於文件system/core/init/ueventd.cpp中,主要進行以下工作:

int ueventd_main(int argc, char **argv) {
    //與init進程啟動一樣,ueventd首先調用umask(0)以清除屏蔽字,保證新建的目錄訪問權限不受屏蔽字影響
    umask(000);

    //忽略子進程終止信號
    signal(SIGCHLD, SIG_IGN);
    ...........
}

如上面代碼所示,ueventd調用signal函數,忽略子進程終止產生的SIGCHLD信號。

=============================以下非主干,可跳過=============================
I
signal函數的功能是:為指定的信號安裝一個新的信號處理函數。

signal函數的原型是:
void ( signal( int signo, void (func)(int) ) )(int);
其中:
signo參數是信號名;

func的值是常量SIG_IGN、常量SIG_DFL或當接到此信號後要調用的函數的地址。
如果指定SIG_IGN,則向內核表示忽略此信號(記住有兩個信號SIGKILL和SIGSTOP不能忽略);
如果指定SIG_DFL,則表示接到此信號後的動作是系統默認動作;
當指定函數地址時,則在信號發生時,調用該函數。我們稱這種處理為“捕捉”該信號,稱此函數為信號處理程序(signal handler)或信號捕捉函數(signal catching function)。

signal的返回值是指向之前的信號處理程序的指針。(也就是返回執行signal 函數之前,對信號signo的信號處理程序指針)。

II
對於某些進程,特別是服務器進程,往往在請求到來時生成子進程進行處理。如果父進程不處理子進程結束的信號,子進程將成為僵屍進程(zombie)從而占用系統資源;如果父進程處理子進程結束的信號,將增加父進程的負擔,影響服務器進程的並發性能。在Linux下可以簡單地將 SIGCHLD信號的操作設為SIG_IGN,可讓內核把子進程的信號轉交給init進程去處理。

回憶init進程的啟動過程,我們知道init進程確實注冊了針對SIGCHLD的信號處理器。

=============================以上非主干,可跳過=============================

我們回到ueventd的ueventd_main函數:

..........
//與init進程一樣,屏蔽標准輸入輸出
open_devnull_stdio();
//初始化內核log系統
klog_init();
klog_set_level(KLOG_NOTICE_LEVEL);

NOTICE("ueventd started!\n");

selinux_callback cb;
cb.func_log = selinux_klog_callback;
//注冊selinux相關的用於打印log的回調函數
selinux_set_callback(SELINUX_CB_LOG, cb);
...........

//獲取硬件相關信息
char hardware[PROP_VALUE_MAX];
property_get("ro.hardware", hardware);

//解析ueventd.rc文件
ueventd_parse_config_file("/ueventd.rc");
//解析廠商相關的ueventd.{hardware}.rc文件
ueventd_parse_config_file(android::base::StringPrintf("/ueventd.%s.rc", hardware).c_str());
..........

在分析ueventd_parse_config_file函數前,我們先看看ueventd.rc中大概的內容。

...........
/dev/null                 0666   root       root
/dev/zero                 0666   root       root
/dev/full                 0666   root       root
/dev/ptmx                 0666   root       root
/dev/tty                  0666   root       root
/dev/random               0666   root       root
..........

從上面的代碼,可以看出ueventd.rc中主要記錄的就是設備節點文件的名稱、訪問權限、用戶ID、組ID。

ueventd_parse_config_file函數定義於system/core/init/ueventd_parser.cpp中:

int ueventd_parse_config_file(const char *fn)
{
    std::string data;
    //將文件讀取成string
    if (!read_file(fn, &data)) {
        return -1;
    }

    data.push_back('\n'); // TODO: fix parse_config.
    //解析string
    parse_config(fn, data);
    dump_parser_state();
    return 0;
}

從上面代碼可以看出,與init進程解析init.rc文件一樣,ueventd也是利用ueventd_parse_config_file函數,將指定路徑對應的文件讀取出來,然後再做進一步解析。

static void parse_config(const char *fn, const std::string& data) {
    ........
    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;
        }
    }
}

parse_config定義於system/core/init/ueventd_parser.cpp中,如上面代碼所示,我們可以看出ueventd解析ueventd.rc的邏輯,與init進程解析init.rc文件基本一致,即以行為單位,調用parse_line逐行地解析ueventd.rc文件。

parse_line定義於system/core/init/ueventd_parser.cpp中:

static void parse_line(struct parse_state *state, char **args, int nargs) {
    int kw = lookup_keyword(args[0]);
    .........
    if (kw_is(kw, SECTION)) {
        parse_new_section(state, kw, nargs, args);
    } else if (kw_is(kw, OPTION)) {
        state->parse_line(state, nargs, args);
    } else {
        parse_line_device(state, nargs, args);
    }
}

static void parse_new_section(struct parse_state *state, int kw, int nargs, char **args)
{
    ..........
    switch(kw) {
    case K_subsystem:
        state->context = parse_subsystem(state, nargs, args);
        if (state->context) {
            state->parse_line = parse_line_subsystem;
            return;
        }
        break;
    }
    state->parse_line = parse_line_no_op;
}

static void parse_line_device(parse_state*, int nargs, char** args) {
    set_device_permission(nargs, args);
}

從上面的代碼可以看出,parse_line根據解析出來的關鍵字,調用不同的函數進行處理。其中,parse_new_section主要用於處理ueventd.rc文件中,subsystem對應的數據;對於dev對應的數據,需要調用parse_line_device進行處理。

parse_line_device主要調用set_device_permission函數:

void set_device_permission(int nargs, char **args) {
    .......
    add_dev_perms(name, attr, perm, uid, gid, prefix, wildcard);
    .......
}

set_device_permission函數定義於/system/core/init/ueventd.cpp中,主要根據參數,獲取設備名、uid、gid、權限等,然後調用add_dev_perms函數。

struct perm_node {
    struct perms_ dp;
    struct listnode plist;
};

int add_dev_perms(.....) {
    struct perm_node *node = (perm_node*) calloc(1, sizeof(*node));
    //根據輸入參數構造結構體perm_node
    ......
    if (attr)
        list_add_tail(&sys_perms, &node->plist);
    else
        list_add_tail(&dev_perms, &node->plist);

    return 0;
}

add_dev_perms定於文件/system/core/init/devices.cpp中,如上面代碼所示,根據輸入參數構造結構體perm_node,然後將perm_node加入到對應的雙向鏈表中(perm_node中也是通過包含listnode來構建雙向鏈表的)。

注意到,根據參數attr,構造出的perm_node將分別被加入到sys_perms和dev_perms中。
attr的值由之前的set_device_permission函數決定,當ueventd.rc中的設備名以/sys/開頭時,attr的值才可能為1。一般的設備以/dev/開頭,應該被加載到dev_perms鏈表中。

看完解析ueventd.rc的過程後,我們再次將視角拉回到uevent_main函數的後續過程。

...........
device_init();
...........

device_init定義於system/core/init/devices.cpp中,我們來看看該函數的實際工作:

void device_init() {
    sehandle = NULL;
    if (is_selinux_enabled() > 0) {
        //進行安全相關的操作
        sehandle = selinux_android_file_context_handle();
        selinux_status_open(true);
    }

    //創建socket,該socekt用於監聽後續的uevent事件
    device_fd = uevent_open_socket(256*1024, true);
    if (device_fd == -1) {
        return;
    }
    //通過fcntl函數,將device_fd置為非阻塞。
    fcntl(device_fd, F_SETFL, O_NONBLOCK);

    //通過access函數判斷文件/dev/.coldboot_done(COLDBOOT_DONE)是否存在
    //若該路徑下的文件存在,表明已經進行過冷插拔。
    if (access(COLDBOOT_DONE, F_OK) == 0) {
        NOTICE("Skipping coldboot, already done!\n");
        return;
    }

    //調用coldboot函數,處理/sys/目錄下的驅動程序
    Timer t;
    coldboot("/sys/class");
    coldboot("/sys/block");
    coldboot("/sys/devices");
    //冷插拔處理完畢後,創建文件/dev/.coldboot_done
    close(open(COLDBOOT_DONE, O_WRONLY|O_CREAT|O_CLOEXEC, 0000));
    NOTICE("Coldboot took %.2fs.\n", t.duration());
}

根據上述代碼,我們知道了,ueventd調用device_init函數,創建一個socket來接收uevent,再對內核啟動時注冊到/sys/下的驅動程序進行“冷插拔”處理,以創建對應的節點文件。

我們來看看coldboot的過程:

static void coldboot(const char *path)
{
    //打開路徑對應目錄
    //opendir函數打開path指向的目錄,如果成功則返回一個DIR類型的指針,DIR指針指向path目錄下的第一個條目
    DIR *d = opendir(path);
    if(d) {
        //實際的“冷啟動”
        do_coldboot(d);
        closedir(d);
    }
}

static void do_coldboot(DIR *d)
{
    struct dirent *de;
    int dfd, fd;

    //取得目錄流文件描述符
    dfd = dirfd(d);

    fd = openat(dfd, "uevent", O_WRONLY);
    if(fd >= 0) {
        //寫入事件,觸發uevent
        write(fd, "add\n", 4);
        close(fd);
        //接收uevent,並進行處理
        handle_device_fd();
    }

    //遞歸文件目錄,繼續執行do_coldboot
    //readdir() 會返回參數對應條目的信息,以struct dirent形式展現,然後DIR指針會指向下一個條目
    while((de = readdir(d))) {
        DIR *d2;

        if(de->d_type != DT_DIR || de->d_name[0] == '.')
            continue;

        fd = openat(dfd, de->d_name, O_RDONLY | O_DIRECTORY);
        if(fd < 0)
            continue;

        d2 = fdopendir(fd);
        if(d2 == 0)
            close(fd);
        else {
            do_coldboot(d2);
            closedir(d2);
        }
    }
}

從上面的代碼,我們可以看出do_coldboot遞歸查詢“/sys/class”、“/sys/block”和“/sys/devices”目錄下所有的“uevent”文件,然後在這些文件中寫入“add”,而後會強制觸發uevent,並調用handle_device_fd()。handle_device_fd函數負責接收uevent信息,並創建節點文件(後文介紹其代碼)。

int openat(int dirfd, const char *pathname, int flags)
openat系統調用與open功能類似,但用法上有以下不同:
如果pathname是相對地址,則以dirfd作為相對地址的尋址目錄,而open是從當前目錄開始尋址的;
如果pathname是相對地址,且dirfd的值是AT_FDCWD,則openat的行為與open一樣,從當前目錄開始相對尋址;
如果pathname是絕對地址,則dirfd參數不起作用。

冷插拔結束後,uevent_main剩余的工作,就是監聽並處理熱插拔事件了。

.......
ollfd ufd;
ufd.events = POLLIN;
//獲取device_init中創建出的socket
ufd.fd = get_device_fd();

while (true) {
    ufd.revents = 0;
    //監聽來自驅動的uevent
    int nr = poll(&ufd, 1, -1);
    if (nr <= 0) {
        continue;
    }
    if (ufd.revents & POLLIN) {
        //進行實際的事件處理
        handle_device_fd();
    }
}

    return 0;
}

從上面的代碼可以看出,ueventd監聽到uevent事件後,主要利用handle_device_fd函數進行處理。handle_device_fd定義於/system/core/init/devices.cpp中:

void handle_device_fd() {
    ........
    //uevent_kernel_multicast_recv的功能就是讀取寫入到device_fd上的數據,其中封裝調用了recvmsg函數
    //讀取數據將被存入到msg變量中,數據的長度為n
    while ((n = uevent_kernel_multicast_recv(device_fd, msg, UEVENT_MSG_LEN)) > 0) {
        .........
        //parse_event的功能是按格式將收到的數據解析成uevent
        parse_event(msg, &uevent);
        ....
        handle_device_event(&uevent);
        //處理firmware對應的uevent的函數,在此不做分析
        handle_firmware_event(&uevent);
    }
}

從上面代碼可以看出,實際處理uevent的函數為handle_device_event。

static void handle_device_event(struct uevent *uevent){
    ........
    if (!strncmp(uevent->subsystem, "block", 5)) {
        handle_block_device_event(uevent);
    } else if (!strncmp(uevent->subsystem, "platform", 8)) {
        handle_platform_device_event(uevent);
    } else {
        handle_generic_device_event(uevent);
    }
}

handle_device_event根據uevent的類型調用相應的函數進行處理。此處,我們重點看看handle_generic_device_event函數。

static void handle_generic_device_event(struct uevent *uevent) {
    .........
    name = parse_device_name(uevent, 64);
    .........
    if (subsystem) {
        ......
    } else if (!strncmp(uevent->subsystem, "usb", 3)) {
        ......
    } else if (!strncmp(uevent->subsystem, "graphics", 8)) {
         base = "/dev/graphics/";
         make_dir(base, 0755);
    } else if (!strncmp(uevent->subsystem, "drm", 3)) {
         base = "/dev/dri/";
         make_dir(base, 0755);
    } ................
       else {
       base = "/dev/";
    }
    .........
    handle_device(uevent->action, devpath, uevent->path, 0, uevent->major, uevent->minor, links);
}

handle_generic_device_event函數代碼較多(大量if、else),其實就是從uevent中解析出設備的信息,然後根據設備的類型在dev下創建出對應的目錄。
在創建完目錄後,將調用函數handle_device,最終通過mknod創建出設備節點文件。

static void handle_device(......) {
    ........
    make_device(devpath, path, block, major, minor, (const char **)links);
     ........
}

static void make_device(......) {
    .............
    mode = get_device_perm(path, links, &uid, &gid) | (block ? S_IFBLK : S_IFCHR);
    ..............
    mknod(path, mode, dev);
    .............
}

結束語
以上對android ueventd的簡要分析,這裡主要需要了解“冷啟動”和“熱啟動”的概念,了解概念後,代碼相對還是比較好理解的。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved