前言
与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中创建出的socketufd.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. Android(安卓)虚拟机Dalvik、Android各种java包功能、Android相
  2. android实现横竖屏不间断播放文件
  3. Android电源管理
  4. Android电源管理
  5. Android(安卓)Kotlin使用指南
  6. Android内置应用到系统的方法总结
  7. 箭头函数的基础使用
  8. NPM 和webpack 的基础使用
  9. Python技巧匿名函数、回调函数和高阶函数

随机推荐

  1. 【Android(安卓)Training - 05】与其他Ap
  2. 面向大众的移动技术:签名,封装和发布Andro
  3. Pro Android Media:5 Android音频处理入
  4. OSC首发:android中的左右滑屏实现By ViewP
  5. 《 Android物联网开发从入门到实战》国内
  6. Android图表之-Echarts
  7. 打造Android的中文Siri语音助手(一)——
  8. 【Android 设计】:样式_ 触感 | 度量与网
  9. chez scheme for android 移植完成
  10. Android偷偷进了PC村,Google是无心插柳还