内核发送uevent的API
内核发送uevent的API由lib/kobject_event.c文件实现,include/linux/kobject.h是头文件。
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_MAX
};
/* kobject_uevent不能用在中断上下文 */
int kobject_uevent(struct kobject *kobj, enum kobject_action action);
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp[]);
在driver中可以调用kobject_uevent或者kobject_uevent_env来向用户空间发送uevent
kobject_uevent默认会发送”ACTION=xxx”,”DEVPATH=xxx”,”SUBSYSTEM=xxx”这三个uevent环境变量。
kobject_uevent_env可以发送一些如”xxx=xxx”的自定义的uevent环境变量。
用户空间解析uevent
从android代码system/core/init/devices.c中抽取
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#define UEVENT_MSG_LEN 4096
struct luther_gliethttp {
const char *action;
const char *path;
const char *subsystem;
const char *firmware;
int major;
int minor;
};
static int open_luther_gliethttp_socket(void);
static void parse_event(const char *msg, struct luther_gliethttp *luther_gliethttp);
int main(int argc, char* argv[])
{
int device_fd = -1;
char msg[UEVENT_MSG_LEN+2];
int n;
device_fd = open_luther_gliethttp_socket();
printf("device_fd = %d\n", device_fd);
do {
while((n = recv(device_fd, msg, UEVENT_MSG_LEN, 0)) > 0) {
struct luther_gliethttp luther_gliethttp;
if(n == UEVENT_MSG_LEN)
continue;
msg[n] = '\0';
msg[n+1] = '\0';
parse_event(msg, &luther_gliethttp);
}
} while(1);
}
static int open_luther_gliethttp_socket(void)
{
struct sockaddr_nl addr;
int sz = 64*1024;
int s;
memset(&addr, 0, sizeof(addr));
addr.nl_family = AF_NETLINK;
addr.nl_pid = getpid();
addr.nl_groups = 0xffffffff;
s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
if (s < 0)
return -1;
setsockopt(s, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz));
if (bind(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
close(s);
return -1;
}
return s;
}
static void parse_event(const char *msg, struct luther_gliethttp *luther_gliethttp)
{
luther_gliethttp->action = "";
luther_gliethttp->path = "";
luther_gliethttp->subsystem = "";
luther_gliethttp->firmware = "";
luther_gliethttp->major = -1;
luther_gliethttp->minor = -1;
printf("========================================================\n");
while (*msg) {
printf("%s\n", msg);
if (!strncmp(msg, "ACTION=", 7)) {
msg += 7;
luther_gliethttp->action = msg;
} else if (!strncmp(msg, "DEVPATH=", 8)) {
msg += 8;
luther_gliethttp->path = msg;
} else if (!strncmp(msg, "SUBSYSTEM=", 10)) {
msg += 10;
luther_gliethttp->subsystem = msg;
} else if (!strncmp(msg, "FIRMWARE=", 9)) {
msg += 9;
luther_gliethttp->firmware = msg;
} else if (!strncmp(msg, "MAJOR=", 6)) {
msg += 6;
luther_gliethttp->major = atoi(msg);
} else if (!strncmp(msg, "MINOR=", 6)) {
msg += 6;
luther_gliethttp->minor = atoi(msg);
}
while(*msg++)
;
}
printf("event { '%s', '%s', '%s', '%s', %d, %d }\n",
luther_gliethttp->action, luther_gliethttp->path, luther_gliethttp->subsystem,
luther_gliethttp->firmware, luther_gliethttp->major, luther_gliethttp->minor);
}
这个程序放在后台运行,当内核中通过kobject_uevent_env或者kobject_uevent发送uevent至用户空间的时候,这个进程将对应的uevent消息打印出来。
udev与mdev
kobject_uevent和kobject_uevent_env这两个函数都会
调用netlink发送uevent消息,
调用uevent_helper,最终转换成对用户空间/sbin/mdev的调用。
uevent的用户空间程序有两个,一个是udev,另一个是mdev。
udev通过netlink监听uevent消息,他在系统启动的时候运行了一个daemon(守护进程)程序udevd,通过监听内核发送的uevent来执行相应的热插拔动作,他主要完成两个功能:
1\ 自动加载模块
2\ 根据uevent消息在dev目录下添加、删除设备节点
mdev是基于uevent_helper机制的,他在系统启动时修改了内核中的uevent_helper变量(通过写/proc/sys/kernel/hotplug),这样内核产生uevent时会调用uevent_helper所指的用户级程序,也就是mdev来执行相应的热插拔动作,uevent_helper的初始值在内核编译时刻配置:
—> Device Drivers
—> Generic Driver Options
—> (/proc/sys/kernel/hotplug) path to uevent helper
还有在android系统中是ueventd这个守护进程在接收uevent事件,源码位于system/core/init/ueventd.c,他读取的配置文件是ueventd.rc
udev
/etc目录下有一个uevent规则文件/etc/udev/rules.d/50-udev.rules
udev收到uevent消息后,在这个规则文件里匹配,如果匹配成功,则执行这个匹配定义的shell命令,例如规则文件中有这么一行:
ACTION=="add", SUBSYSTEM=="?*", ENV{MODALIAS}=="?*", RUN+="/sbin/modprobe $env{MODALIAS}"
所以,当收到uevent的add事件后,shell能自动加载在MODALIAS中定义的模块。
mdev
mdev配置
1\ 内核配置以支持mdev
保证CONFIG_HOTPLUG=Y和CONFIG_NET=Y
然后设置hotplug执行的路径,这里可以不设置,但是在开机的启动脚本中设置”echo /bin/mdev > /sys/proc/kernel/hotplug”
Device Drivers —>
Generic Driver Options —>
() path to uevent helper
2\ busybox配置mdev工具及对应的配置文件
Linux System Utilities —>
[*] mdev
[*] Support /etc/mdev.conf
[*] Support subdirs/symlinks
[*] Support regular expressions substitutions when renaming device
[*] Support command execution at device addition/removal
[*] Support loading of firmwares
mdev原理
mdev在busybox的代码包中能找到,位于busybox/util-linux/mdev.c文件中,他通过uevent_helper函数被调用。
a、执行mdev -s命令时,mdev扫描/sys/block(块设备保存在/sys/block目录下,内核2.6.25版本以后,块设备也保存在/sys /class/block目录下。mdev扫描/sys/block是为了实现向后兼容)和/sys/class两个目录下的dev属性文件,从该dev 属性文件中获取到设备编号(dev属性文件以”major:minor\n”形式保存设备编号),并以包含该dev属性文件的目录名称作为设备名 device_name(即包含dev属性文件的目录称为device_name,而/sys/class和device_name之间的那部分目录称为 subsystem。也就是每个dev属性文件所在的路径都可表示为/sys/class/subsystem/device_name/dev),在 /dev目录下创建相应的设备文件。例如,cat /sys/class/tty/tty0/dev会得到4:0,subsystem为tty,device_name为tty0。
b、当mdev因uevnet事件(以前叫hotplug事件)被调用时,mdev通过由uevent事件传递给它的环境变量获取到:引起该uevent 事件的设备action及该设备所在的路径device path。然后判断引起该uevent事件的action是什么。若该action是add,即有新设备加入到系统中,不管该设备是虚拟设备还是实际物理 设备,mdev都会通过device path路径下的dev属性文件获取到设备编号,然后以device path路径最后一个目录(即包含该dev属性文件的目录)作为设备名,在/dev目录下创建相应的设备文件。若该action是remote,即设备已 从系统中移除,则删除/dev目录下以device path路径最后一个目录名称作为文件名的设备文件。如果该action既不是add也不是remove,mdev则什么都不做。
由上面可知,如果我们想在设备加入到系统中或从系统中移除时,由mdev自动地创建和删除设备文件,那么就必须做到以下三点:1、在/sys/class 的某一subsystem目录下,2、创建一个以设备名device_name作为名称的目录,3、并且在该device_name目录下还必须包含一个 dev属性文件,该dev属性文件以”major:minor\n”形式输出设备编号。
mdev.txt
mdev配置文件的用法看busybox/docs/mdev.txt文件
mdev的配置文件是/etc/mdev.conf
以下是mdev.txt部分原文内容:
-----------
Basic Use
-----------
Mdev has two primary uses: initial population and dynamic updates. Both require sysfs support in the kernel and have it mounted at /sys. For dynamic updates, you also need to have hotplugging enabled in your kernel.
Here's a typical code snippet from the init script:
[0] mount -t proc proc /proc
[1] mount -t sysfs sysfs /sys
[2] echo /sbin/mdev > /proc/sys/kernel/hotplug
[3] mdev -s
-------------
MDEV Config (/etc/mdev.conf)
-------------
Mdev has an optional config file for controlling ownership/permissions of device nodes if your system needs something more than the default root/root 660 permissions.
The file has the format:
[-]<device regex> <uid>:<gid> <permissions>
or
@<maj[,min1[-min2]]> <uid>:<gid> <permissions>
or
$envvar=<regex> <uid>:<gid> <permissions>
For example:
hd[a-z][0-9]* 0:3 660
You can rename/move device nodes by using the next optional field.
<device regex> <uid>:<gid> <permissions> [=path]
So if you want to place the device node into a subdirectory, make sure the path has a trailing /. If you want to rename the device node, just place the name.
hda 0:3 660 =drives/
This will move "hda" into the drives/ subdirectory.
hdb 0:3 660 =cdrom
This will rename "hdb" to "cdrom".
Similarly, ">path" renames/moves the device but it also creates a direct symlink /dev/DEVNAME to the renamed/moved device.
You can also prevent creation of device nodes with the 4th field as "!":
tty[a-z]. 0:0 660 !
pty[a-z]. 0:0 660 !
If you also enable support for executing your own commands, then the file has the format:
<device regex> <uid>:<gid> <permissions> [=path] [@|$|*<command>]
or
<device regex> <uid>:<gid> <permissions> [>path] [@|$|*<command>]
or
<device regex> <uid>:<gid> <permissions> [!] [@|$|*<command>]
For example:
# block devices
([hs]d[a-z]) root:disk 660 >disk/%1/0
([hs]d[a-z])([0-9]+) root:disk 660 >disk/%1/%2 mmcblk([0-9]+) root:disk 660 >disk/mmc/%1/0 mmcblk([0-9]+)p([0-9]+) root:disk 660 >disk/mmc/%1/%2
# network devices
(tun|tap) root:network 660 >net/%1
The special characters have the meaning:
@ Run after creating the device.
$ Run before removing the device.
* Run both after creating and before removing the device.
The command is executed via the system() function (which means you're giving a command to the shell), so make sure you have a shell installed at /bin/sh. You should also keep in mind that the kernel executes hotplug helpers with stdin, stdout, and stderr connected to /dev/null.
For your convenience, the shell env var $MDEV is set to the device name. So if the device "hdc" was matched, MDEV would be set to "hdc".
参考文章
- 热插拔uevent事件用户空间截获方法和具体实现
- linux下热插拔事件的产生是怎样通知到用户空间,kobject_uevent_env之uevent