前言
与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
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
在init进程的启动过程中,解析完init.rc文件后,首先将“early-init”对应的action加入到运行队列中。因此,当init进程开始处理运行队列中的事件时,首先会处理该action。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
如上所示,为init.rc内“early-init”对应的action,我们可以看到,将执行start ueventd的命令。
根据keywords.h中的定义,我们知道action的start关键字,对应函数do_start,定义于system/core/init/builtins.cpp中:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
如上代码所示,do_start函数通过service_find_by_name函数,从service_list链表中,根据参数找到需启动的service,然后调用service_start函数启动service。
service_start函数定义于init.cpp文件中:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
该函数对参数进行检查后,利用fork函数创建出子进程,然后按照service在init.rc中的定义,对service进行配置,最后调用Linux系统函数execve启动service。
二、ueventd的主要工作
ueventd_main定义于文件system/core/init/ueventd.cpp中,主要进行以下工作:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
如上面代码所示,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函数:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
在分析ueventd_parse_config_file函数前,我们先看看ueventd.rc中大概的内容。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
从上面的代码,可以看出ueventd.rc中主要记录的就是设备节点文件的名称、访问权限、用户ID、组ID。
ueventd_parse_config_file函数定义于system/core/init/ueventd_parser.cpp中:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
从上面代码可以看出,与init进程解析init.rc文件一样,ueventd也是利用ueventd_parse_config_file函数,将指定路径对应的文件读取出来,然后再做进一步解析。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
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中:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
从上面的代码可以看出,parse_line根据解析出来的关键字,调用不同的函数进行处理。其中,parse_new_section主要用于处理ueventd.rc文件中,subsystem对应的数据;对于dev对应的数据,需要调用parse_line_device进行处理。
parse_line_device主要调用set_device_permission函数:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
set_device_permission函数定义于/system/core/init/ueventd.cpp中,主要根据参数,获取设备名、uid、gid、权限等,然后调用add_dev_perms函数。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
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函数的后续过程。
- 1
- 2
- 3
- 1
- 2
- 3
device_init定义于system/core/init/devices.cpp中,我们来看看该函数的实际工作:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
根据上述代码,我们知道了,ueventd调用device_init函数,创建一个socket来接收uevent,再对内核启动时注册到/sys/下的驱动程序进行“冷插拔”处理,以创建对应的节点文件。
我们来看看coldboot的过程:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
从上面的代码,我们可以看出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剩余的工作,就是监听并处理热插拔事件了。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
从上面的代码可以看出,ueventd监听到uevent事件后,主要利用handle_device_fd函数进行处理。handle_device_fd定义于/system/core/init/devices.cpp中:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
从上面代码可以看出,实际处理uevent的函数为handle_device_event。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
handle_device_event根据uevent的类型调用相应的函数进行处理。此处,我们重点看看handle_generic_device_event函数。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
handle_generic_device_event函数代码较多(大量if、else),其实就是从uevent中解析出设备的信息,然后根据设备的类型在dev下创建出对应的目录。
在创建完目录后,将调用函数handle_device,最终通过mknod创建出设备节点文件。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
结束语
以上对android ueventd的简要分析,这里主要需要了解“冷启动”和“热启动”的概念,了解概念后,代码相对还是比较好理解的。