4、uevent 机制:内核如何通知用户空间设备变化
设备热插拔,说白了就是「插上就能用,拔掉就消失」。但内核怎么把这个消息告诉用户空间呢?
答案就是 uevent。这是内核设备模型里最关键的通信机制之一。我个人习惯把它叫做「内核的小喇叭」——设备一有变化,它就喊一嗓子。
4.1 uevent 是什么?
uevent 的全称是 user-space event。它是内核向用户空间发送的设备事件通知。
常见的 uevent 类型包括:
- ADD —— 设备被插入
- REMOVE —— 设备被拔出
- CHANGE —— 设备状态发生变化
- ONLINE/OFFLINE —— 设备上线/下线
- MOVE —— 设备在 sysfs 中的位置改变
每个 uevent 都携带一组键值对(key=value),描述设备的属性。比如:
ACTION=add
DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-1
SUBSYSTEM=usb
DEVNAME=bus/usb/001/002
DEVTYPE=usb_device
PRODUCT=8087/0024/0
TYPE=9/0/0
嗯,这里要注意:uevent 不是直接发给应用程序的。它走的是 netlink 套接字,或者通过 uevent_helper 调用用户空间的 helper 程序。
4.2 uevent 的发送路径
我画了一张流程图,帮你理清 uevent 从内核到用户空间的完整路径:
整个流程其实不复杂。设备核心层调用 kobject_uevent(),然后填充环境变量,最后通过 netlink 广播出去。用户空间的 udev 收到后,再根据规则创建设备节点、加载驱动模块。
4.3 核心数据结构与 API
内核里 uevent 的核心是 struct kobj_uevent_env。我带你看看它的样子:
struct kobj_uevent_env {
char *envp[UEVENT_NUM_ENVP]; // 环境变量指针数组
int envp_idx; // 当前环境变量数量
char buf[UEVENT_BUF_SIZE]; // 环境变量数据缓冲区
int buflen; // 已使用的缓冲区长度
};
关键 API 就几个:
| 函数 | 作用 |
|---|---|
kobject_uevent() |
发送 uevent 事件,指定 action 类型 |
kobject_uevent_env() |
发送 uevent 并附加自定义环境变量 |
add_uevent_var() |
向 uevent 环境变量列表中添加键值对 |
dev_uevent_send() |
设备模型专用的 uevent 发送接口 |
重要提示: 驱动开发者通常不需要直接调用这些 API。设备核心层在设备注册和注销时,会自动发送 uevent。你只需要实现
struct device_driver 里的 uevent 回调,添加一些自定义的环境变量即可。
4.4 驱动中如何添加自定义 uevent
我在项目中遇到过这样一个场景:一个 USB 摄像头驱动,需要告诉用户空间摄像头的分辨率信息。怎么做?
实现驱动的 uevent 回调函数:
static int my_usb_camera_uevent(struct device *dev,
struct kobj_uevent_env *env)
{
struct my_camera_dev *camera = dev_get_drvdata(dev);
// 添加自定义环境变量
add_uevent_var(env, "CAMERA_RESOLUTION=%dx%d",
camera->width, camera->height);
add_uevent_var(env, "CAMERA_FPS=%d", camera->fps);
return 0;
}
static struct device_driver my_camera_driver = {
.name = "my_usb_camera",
.bus = &usb_bus_type,
.probe = my_camera_probe,
.remove = my_camera_remove,
.uevent = my_usb_camera_uevent, // 注册回调
};
用户空间的 udev 规则可以这样匹配:
# 匹配分辨率大于 1080p 的摄像头
SUBSYSTEM=="usb", ENV{CAMERA_RESOLUTION}=="?*",
RUN+="/usr/local/bin/high_res_cam.sh"
小技巧: 调试 uevent 可以用
udevadm monitor 命令。它会实时打印内核发出的所有 uevent。我曾经靠这个命令排查过一个 USB 设备无法自动挂载的问题——原来是 uevent 根本没发出来。
4.5 uevent 的两种发送方式
内核支持两种 uevent 发送方式。我建议你重点理解 netlink 方式:
| 方式 | 原理 | 特点 |
|---|---|---|
| netlink | 通过 NETLINK_KOBJECT_UEVENT 协议广播 | 高效、可靠、支持多播。现代系统默认使用 |
| uevent_helper | 调用用户空间的 helper 程序(如 /sbin/hotplug) | 已废弃。效率低,安全性差。内核 2.6 之后逐渐淘汰 |
你想想看,为什么 netlink 成了主流?因为它不需要轮询,事件一来就能收到。而且多个进程可以同时监听同一个 netlink 组,互不干扰。
4.6 避坑指南
我曾经踩过的坑:
- 环境变量缓冲区溢出:
kobj_uevent_env的 buf 只有 2KB(UEVENT_BUF_SIZE)。如果你添加太多自定义变量,会触发截断。我遇到过驱动添加了 30 多个环境变量,结果后面的全丢了。 - 不要在中断上下文发送 uevent:
kobject_uevent()可能会睡眠(分配内存、调用 netlink 发送)。如果你在中断或 spinlock 保护区域调用,系统会死锁。 - uevent 不是实时事件: 从设备插入到用户空间收到 uevent,中间有延迟。对于需要微秒级响应的场景,别依赖 uevent。
4.7 用户空间如何接收 uevent
用户空间监听 uevent 有两种主流方式:
- udev / systemd-udevd: 系统服务,自动处理设备节点创建、模块加载等。
- libudev / sd-device: 编程接口,应用程序可以自己监听 uevent。
用 C 语言监听 uevent 的示例:
#include <libudev.h>
struct udev *udev = udev_new();
struct udev_monitor *mon = udev_monitor_new_from_netlink(udev, "udev");
// 监听所有 uevent
udev_monitor_filter_add_match_subsystem_devtype(mon, "usb", NULL);
udev_monitor_enable_receiving(mon);
int fd = udev_monitor_get_fd(mon);
while (1) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd, &fds);
select(fd + 1, &fds, NULL, NULL, NULL);
if (FD_ISSET(fd, &fds)) {
struct udev_device *dev = udev_monitor_receive_device(mon);
if (dev) {
printf("ACTION: %s\n", udev_device_get_action(dev));
printf("DEVPATH: %s\n", udev_device_get_devpath(dev));
udev_device_unref(dev);
}
}
}
核心要点: uevent 是内核设备模型与用户空间之间的「桥梁」。没有它,udev 不知道设备插拔,systemd 不知道硬件变化,你的应用程序也无法感知设备状态。理解 uevent,你就掌握了内核与用户空间通信的「最后一公里」。
公众号:蓝海资料掘金营,微信deep3321