前言
与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的简要分析,这里主要需要了解“冷启动”和“热启动”的概念,了解概念后,代码相对还是比较好理解的。