編輯:關於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的簡要分析,這裡主要需要了解“冷啟動”和“熱啟動”的概念,了解概念後,代碼相對還是比較好理解的。
趁著周一休息,更新一下博客。最近項目中使用到了分組管理,需要實現Listview的Item拖動處理。查略一下資料和借鑒了別人的代碼將功能實現了。現在整理一下代碼,方便自己
一、View、ViewGroup的基本屬性1,View事件:public boolean dispatchTouchEvent(MotionEvent event)
隨著移動開發的不斷演進,項目開發設計模式也變的越來越新穎,越來越便捷。而各個團隊都在追求良好的項目架構,不僅能加快工程的進度,也對後續項目的維護和擴展起來很重要的做用。但
在安卓操作系統下對於 TextView 字體的支持非常有限,默認情況下 TextView 的 typeface 屬性支持 Sans,serif,monospace 這三種