Android中的ueventd
来源:互联网 发布:linux查看网卡配置 编辑:程序博客网 时间:2024/06/06 07:25
前言
与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的简要分析,这里主要需要了解“冷启动”和“热启动”的概念,了解概念后,代码相对还是比较好理解的。
</div> </div>
- Android中的ueventd
- android ueventd
- Android ueventd解析
- Android ueventd浅析
- android init进程分析 ueventd
- android ueventd 本地native部分源码分析
- android ueventd 本地native部分源码分析
- 往android的内核添加驱动及 ueventd.rc 修改
- 往android的内核添加驱动及 ueventd.rc 修改
- 往android的内核添加驱动及ueventd.rc 修改
- ueventd.rc 处理硬件设备权限和android init 对其解析
- ueventd.rc 处理硬件设备权限和android init 对其解析
- android init进程分析 ueventd — 设备节点的创建、固件更新过程
- Android修改ueventd.rc之后另一种快速打包ramdisk.img的方法
- ueventd.rc 处理硬件设备权限和android init 对其解析
- android6.0 ueventd
- Android6.0 ueventd
- android中的
- C++字符串操作详解(整理)
- 黑白棋的落子
- 直播 | 某小公司DevOps落地实践
- 功能至上!国内外最实用的协作类软件盘点
- caffe之训练数据格式
- Android中的ueventd
- 互联网新时代,国内首个 IPv6 公共 DNS 正式发布
- [bigdata-34] pyenv + anaconda 4.2 + python3.5+ ubuntu 16.04 安装
- Git 命令
- 设计模式,六大设计原则,类的特性
- Jmeter脚本录制
- 欢迎使用CSDN-markdown编辑器
- opencv使用第一步 用opencv打开图片 视频 和摄像头
- 机器人工程的What、Why、When、How、Ask、Teach?(草稿)