十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux内核uevent机制详解:从kobject到用户空间设备通知

Linux内核uevent机制详解:从kobject到用户空间设备通知 做内核驱动开发的人应该都有一个共同的感受明明设备已经插入用户态却一点感知都没有应用层只能轮询或者干等。Linux 内核早就考虑到了这个问题设备模型里专门设计了一套“广播通知”机制也就是本文要聊的uevent。它在内核里负责把设备状态变化插入、拔出、改名、电量变化等打包成一条消息再通过内核与用户空间的通道送出去用户空间收到后就能立刻执行 udev 规则、挂载文件系统、刷新界面或者触发你自己的业务逻辑。这篇文章我会从 Linux 设备模型里 uevent 的定位讲起先拆解内核侧发送 uevent 的实现路径和关键 API再给出一份用户空间用 netlink socket 接收 uevent 的完整代码最后把我在实际开发中踩过的坑和排查思路一并列出来。适合正在写驱动、做嵌入式 BSP、或者需要让用户空间感知设备热插拔的开发者阅读。1. uevent 机制全景一条从内核到用户空间的消息管道1.1 什么是 uevent它解决什么问题uevent 全称是 user event直译过来就是“用户事件”。名字很直白它本质上是内核设备模型对外发送的事件通知用来告诉用户空间某个设备对象kobject的状态发生了变化。Linux 设备模型的核心结构是 kobject它相当于设备模型里的“挂载点”几乎所有设备、驱动、类class都附着在 kobject 上。而 kobject 自身提供了一套事件发布能力当设备被创建、销毁、属性改变时内核会调用kobject_uevent系列函数把事件抛出去。用户空间的 udev、systemd-udevd、mdevBusyBox 环境常用都是靠接收这些事件来工作的。做个不太严谨但很好懂的生活化类比内核就像小区物业设备是业主。业主要搬家设备拔出、入住设备插入、或者装修改结构设备属性变化物业都要在公告栏贴一张通知uevent各家各户用户空间程序看到通知后再决定要不要行动。没有这套通知住户就得每天去物业问一遍“今天有没有人搬家”这就是轮询的痛苦。这个机制解决的核心问题就是把内核中设备状态的变化主动推送给用户空间避免用户空间做无意义的频繁轮询。典型的应用场景包括USB 设备插入后udev 根据 uevent 里的厂商 ID、产品 ID、设备类型等信息自动在 /dev 下创建对应设备节点SD 卡插入时桌面环境收到 uevent 后自动执行挂载弹出文件管理器窗口网络接口插拔网线时NetworkManager 根据 uevent 判断链路状态并切换网络电池电量变化时内核通过 uevent 把容量、状态同步给用户空间的电源管理服务驱动开发者自己定义的私有事件比如某个外设采集到阈值、固件升级完成都可以通过 uevent 通知应用层。1.2 三条投递通道helper、netlink 与 sysfs 触发uevent 从内核到用户空间历史上实际存在过三种投递方式。第一种是uevent_helper也叫 hotplug 助手。早期内核通过call_usermodehelper直接拉起一个用户态脚本比如 /sbin/hotplug然后把 ACTION、DEVPATH 等环境变量传递给它。这种方式实现简单但开销非常大——每个事件都要创建一次用户态进程遇到大量设备同时插拔时直接卡成狗。2.6.39 版本之后内核默认移除了对 helper 的自动调用即使你配置了 CONFIG_UEVENT_HELPER也基本只用于调试生产环境不推荐。第二种是netlink socket也就是NETLINK_KOBJECT_UEVENT协议。这是目前最主流、也是内核推荐的事件投递通道。内核把 uevent 以多播消息的形式发送到 netlink 协议族用户空间程序创建一个 AF_NETLINK socket 加入对应组播组就能收到。它不依赖用户态脚本、不要求 root 环境下预先配置任何东西、性能高、支持多订阅者所以 udev 和 systemd 都走这条路。第三种是sysfs 下的 uevent 属性文件。每个 kobject 在 sysfs 中对应的目录里通常有一个名为uevent的文件。向这个文件写入字符串比如add、change、remove可以主动触发一次对应类型的 uevent。它的主要用途不是接收而是模拟事件方便调试和测试。当然你也能通过cat /sys/.../uevent读到一些内核返回的额外属性但接收场景基本都用 netlink。下图不是图是我的话三条通道中收事件用 netlink测试发事件用 sysfs 的 uevent 文件老古董 helper 路径了解就行别投入精力。2. 内核侧发送 uevent从 KOBJ_ADD 到 kobject_uevent_env2.1 内核里谁在调用 uevent内核中发送 uevent 的入口主要在lib/kobject_uevent.c。设备模型在设备生命周期的关键节点比如设备注册、注销、驱动绑定、属性变更时都会主动调用 uevent 发送接口。具体来说drivers/base/core.c里device_add成功后内核会调用kobject_uevent(dev-kobj, KOBJ_ADD)通知用户空间设备已经加入device_del时调用KOBJ_REMOVEdrivers/base/dd.c里驱动与设备绑定成功、解绑时也会发送KOBJ_BIND和KOBJ_UNBIND。你可以自己在内核源码里搜一下kobject_uevent(能看到非常多的调用点。内核定义了一组标准的动作枚举在include/linux/kobject.h里enum kobject_action { KOBJ_ADD, KOBJ_REMOVE, KOBJ_CHANGE, KOBJ_MOVE, KOBJ_ONLINE, KOBJ_OFFLINE, KOBJ_BIND, KOBJ_UNBIND, };这些动作覆盖了设备的完整生命周期。但实际在用户空间收到的 uevent 字符串里你看到的不是KOBJ_ADD这样的枚举名而是转换成了小写的字符串。内核里通过kobject_actions数组把枚举映射成字符串static const char * const kobject_actions[] { [KOBJ_ADD] add, [KOBJ_REMOVE] remove, [KOBJ_CHANGE] change, [KOBJ_MOVE] move, [KOBJ_ONLINE] online, [KOBJ_OFFLINE] offline, [KOBJ_BIND] bind, [KOBJ_UNBIND] unbind, };所以用户空间程序判断事件类型时比对的是add、remove、change这些字符串而不是枚举数字。2.2 核心 APIkobject_uevent 与 kobject_uevent_env内核对外暴露的 uevent 发送接口主要有三个int kobject_uevent(struct kobject *kobj, enum kobject_action action); int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp[]); int kobject_uevent_probe(struct kobject *kobj, enum kobject_action action);kobject_uevent是最常用的封装内部直接调用kobject_uevent_env但传入的环境变量指针为 NULL只发送内核自动补充的标准字段。kobject_uevent_env是真正的核心实现。它允许调用者通过envp参数携带自定义的环境变量。envp 是一个以 NULL 结尾的字符串指针数组每个字符串形如MY_VARvalue。内核执行时会把这些自定义变量追加到标准变量后面然后一起发送到 netlink 组播组。kobject_uevent_probe是相对新的接口主要用于设备驱动模型在 probe 阶段主动向用户空间广播事件比如某些子系统在设备初始化完成后需要通知用户态加载固件或切换模式。这些函数内部做了几件事首先检查 kobject 是否注册了 uevent 回调kobj-ktype-uevent_ops然后调用回调填充环境变量最后通过kobject_uevent_net_broadcast把消息发到 netlink。如果内核开启了 uevent helper还会尝试调用用户态助手一般不建议开。2.3 内核模块内主动发送 uevent 的完整实验光看 API 不落地等于没看。我们直接写一个内核模块演示两种发送方式一是在模块加载创建 miscdevice 时发送 ADD 事件二是通过 sysfs 属性文件触发自定义 CHANGE 事件。// uevent_demo.c #include linux/module.h #include linux/kobject.h #include linux/sysfs.h #include linux/miscdevice.h #include linux/uaccess.h static struct kobject *demo_kobj; static ssize_t notify_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) { return sprintf(buf, write 1 to trigger uevent\n); } static ssize_t notify_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) { char *envp[] { DEMO_TRIGGERsysfs, LEVELinfo, NULL }; /* 发送一个自定义的 change 事件 */ kobject_uevent_env(kobj, KOBJ_CHANGE, envp); return count; } static struct kobj_attribute notify_attr __ATTR(notify, 0664, notify_show, notify_store); static struct attribute *demo_attrs[] { notify_attr.attr, NULL, }; static struct attribute_group demo_attr_group { .attrs demo_attrs, }; static int __init uevent_demo_init(void) { int ret; /* 在 /sys/kernel 下创建一个 kobject 目录 */ demo_kobj kobject_create_and_add(uevent_demo, kernel_kobj); if (!demo_kobj) return -ENOMEM; ret sysfs_create_group(demo_kobj, demo_attr_group); if (ret) { kobject_put(demo_kobj); return ret; } /* 主动发送设备添加事件 */ kobject_uevent(demo_kobj, KOBJ_ADD); pr_info(uevent_demo: registered and add event sent\n); return 0; } static void __exit uevent_demo_exit(void) { kobject_uevent(demo_kobj, KOBJ_REMOVE); sysfs_remove_group(demo_kobj, demo_attr_group); kobject_put(demo_kobj); pr_info(uevent_demo: removed\n); } module_init(uevent_demo_init); module_exit(uevent_demo_exit); MODULE_LICENSE(GPL);写个简单的 Makefileobj-m uevent_demo.o KERNEL_DIR ? /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KERNEL_DIR) M$(PWD) modules clean: $(MAKE) -C $(KERNEL_DIR) M$(PWD) clean编译安装后测试make sudo insmod uevent_demo.ko # 查看内核日志 dmesg | tail -5 # 触发一次自定义 change 事件 echo 1 | sudo tee /sys/kernel/uevent_demo/notify # 卸载模块 sudo rmmod uevent_demo打开另一个终端用udevadm monitor观察sudo udevadm monitor --env加载模块时应该能看到ACTIONadd、KERNELuevent_demo、SUBSYSTEM...等字段执行 echo 触发后会看到ACTIONchange和自定义的DEMO_TRIGGERsysfs。这段实验的价值在于你不需要等真实设备插拔就能完全掌控 uevent 的产生时机和内容这在调试用户空间接收程序时非常重要。2.4 内核会自动补充哪些环境变量用户空间通过 netlink 收到的 uevent 通常是一串 keyvalue 对其中大部分是内核自动补充的标准字段不需要驱动开发者额外添加。在drivers/base/core.c里dev_uevent函数会根据设备对象自动填充变量名含义示例ACTION事件动作add / remove / changeDEVPATH设备在 sysfs 中的路径/devices/pci0000:00/...SUBSYSTEM设备所属子系统usb / pci / block / platformDEVNAME设备节点名部分设备有sda / ttyUSB0DEVTYPE设备类型部分设备有disk / partitionMAJOR主设备号块设备等8MINOR次设备号1SEQNUM事件序号12345IFINDEX网络接口索引网络设备2INTERFACE网络接口名eth0另外drivers/base/class.c里类子系统class也会追加一些字段比如CLASSxxx。drivers/base/bus.c则可能追加DRIVERxxx。驱动开发者如果想要自己的业务字段就必须走kobject_uevent_env传入envp这是唯一途径。这里有个容易忽略的点envp 中的自定义变量如果很多内核并不会无限制接收。内核在构造 uevent 消息时会调用kobject_uevent_env里的循环把所有变量拼接到一个临时缓冲区最终通过 netlink 发送。如果环境变量过长拼接阶段就可能因为缓冲区不够而截断所以自定义动态内容不要太长简单的枚举标志、短字符串问题不大。3. 用户空间接收 ueventnetlink socket 全流程实战3.1 用 socket() 绑定 NETLINK_KOBJECT_UEVENT用户空间接收 uevent最本质、最底层的方式就是创建一个 netlink socket加入NETLINK_KOBJECT_UEVENT组播组。系统里的 udev、systemd-udevd 底层都是这么干的只是包了一层库我们直接把外层剥掉看看最朴素的实现。完整接收程序// uevent_listen.c // 编译: gcc uevent_listen.c -o uevent_listen #include stdio.h #include string.h #include stdlib.h #include unistd.h #include sys/socket.h #include linux/netlink.h #define UEVENT_BUFFER_SIZE 2048 int main(int argc, char *argv[]) { int sockfd; struct sockaddr_nl addr; char buf[UEVENT_BUFFER_SIZE]; sockfd socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT); if (sockfd 0) { perror(socket); return -1; } memset(addr, 0, sizeof(addr)); addr.nl_family AF_NETLINK; addr.nl_pid getpid(); // 内核会自动分配唯一标识这里用 pid 更直观 addr.nl_groups 1; // 订阅 UEVENT 组播组 if (bind(sockfd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); close(sockfd); return -1; } printf(listening uevent...\n); while (1) { ssize_t len recv(sockfd, buf, sizeof(buf), 0); if (len 0) { perror(recv); break; } buf[len] \0; printf(----------------------\n); /* netlink 消息里多段 keyvalue 之间用 \0 分隔解析时逐段打印 */ char *p buf; while (p buf len *p) { printf(%s\n, p); p strlen(p) 1; } } close(sockfd); return 0; }编译运行gcc uevent_listen.c -o uevent_listen sudo ./uevent_listen再开一个终端插拔一个 U 盘或者运行上一节的内核模块触发事件就能在终端里看到完整的 uevent 字段输出。这段代码里有两个细节非常关键。第一addr.nl_pid。很多老例子会写成addr.nl_pid 0意思是让内核自动分配。实际测试中手动设成进程 PID 也完全没问题因为 netlink 内核会用进程的 pid 做一个唯一标识。两种写法都能工作但建议显式设置避免某些场景下多个程序用同一个 nl_pid 导致 bind 失败。第二addr.nl_groups 1。这行的作用是订阅组播组。虽然NETLINK_KOBJECT_UEVENT本身就是一个专用的 netlink 协议但内核仍然把 uevent 事件作为组播消息发送如果不指定nl_groups或者写 0你很可能收不到任何事件。内核代码里kobject_uevent_net_broadcast的发送目标是uevent_sock的组播组所以订阅组这一步必须做。另外socket 类型要使用SOCK_RAW。netlink 没有SOCK_DGRAM的用户态封装用SOCK_RAW才能直接拿到原始消息。接收缓冲区方面内核默认给 netlink socket 的接收缓冲可能不够大如果系统里设备事件特别密集比如批量磁盘扫描、插拔一大堆 USB 设备建议主动调大int rcvbuf 1024 * 1024; setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, rcvbuf, sizeof(rcvbuf));3.2 更好的工程实现libudev、pyudev 和 udevadm用裸 socket 编程能够帮你彻底理解 uevent 的底层机制但真实项目里我更推荐直接基于 libudev 做用户态工具。它封装了 socket 创建、绑定、解析、设备对象构造等琐碎操作代码可读性高还自动处理了 netlink 乱序、重复消息等问题。一个基于 libudev 的监听器核心代码#include libudev.h #include stdio.h #include stdlib.h int main(void) { struct udev *udev udev_new(); if (!udev) { fprintf(stderr, udev_new failed\n); return -1; } struct udev_monitor *mon udev_monitor_new_from_netlink(udev, udev); if (!mon) { fprintf(stderr, udev_monitor_new_from_netlink failed\n); udev_unref(udev); return -1; } udev_monitor_filter_add_match_subsystem_devtype(mon, block, 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); int ret select(fd 1, fds, NULL, NULL, NULL); if (ret 0) continue; struct udev_device *dev udev_monitor_receive_device(mon); if (!dev) continue; printf(ACTION%s\n, udev_device_get_action(dev)); printf(DEVPATH%s\n, udev_device_get_devpath(dev)); printf(SUBSYSTEM%s\n, udev_device_get_subsystem(dev)); printf(DEVTYPE%s\n, udev_device_get_devtype(dev)); const char *node udev_device_get_devnode(dev); if (node) printf(DEVNODE%s\n, node); const char *sysname udev_device_get_sysname(dev); if (sysname) printf(SYSNAME%s\n, sysname); udev_device_unref(dev); } udev_unref(udev); return 0; }编译gcc udev_listen.c -o udev_listen $(pkg-config --cflags --libs libudev)udev_monitor_filter_add_match_subsystem_devtype是过滤器能让进程只关注 block 子系统的事件避免被无关事件淹没。实际部署时建议先用裸 socket 搞清楚消息长什么样再用 libudev 做工程化封装两条路都走一遍理解才完整。Python 开发则可以直接用pyudevimport pyudev context pyudev.Context() monitor pyudev.Monitor.from_netlink(context) monitor.filter_by(subsystemblock) for device in iter(monitor.poll, None): if device.action add: print(fblock device added: {device.device_node})很适合快速写原型、写测试脚本。底层原理跟 C 完全一样都是 netlink socket。3.3 与内核驱动联调一个完整的观察场景为了验证“内核发送、用户空间接收”的闭环我给你一个可以手动复现的完整场景。先编译并加载 2.3 节的内核模块同时在两个终端分别运行裸 socket 接收程序和udevadm monitor --env。加载模块后你应该看到两个窗口都有 ADD 事件输出。接着执行echo 1 | sudo tee /sys/kernel/uevent_demo/notify两个窗口都会出现 CHANGE 事件并且带有我们自定义的DEMO_TRIGGERsysfs和LEVELinfo字段。这个实验能说明三件事内核模块可以用kobject_uevent_env往 uevent 里塞自定义字段用户空间原样收到裸 socket 和 udevadm 底层走的是同一条通道事件完全一致sysfs 的 uevent 属性文件是触发事件的入口和接收通道解耦。如果你的目标是接管某类特定设备的通知比如只关心自家 USB 视频设备的插拔那么可以在裸 socket 程序里对SUBSYSTEMusb和PRODUCTxxx做匹配匹配成功才继续处理否则直接忽略。注意 uevent 是按\0分段的解析时不要用strtok一刀切否则可能把 payload 截断。4. 常见问题排查与避坑经验4.1 netlink 收不到数据的几个原因在实际开发里用裸 socket 跑recv却一直阻塞、什么事件都收不到是高频踩坑现场。我按踩到的概率依次排排查第一nl_groups 没设置。这是最常见的问题。看代码里addr.nl_groups是不是 0。之前提过必须设置成 1 或者1UL 0才能真正加入内核 uevent 的组播组。很多人照抄老代码只设置了nl_pid结果 bind 成功但收不到任何消息。第二权限不够。netlink 的NETLINK_KOBJECT_UEVENT协议对读写权限有要求普通用户运行的话很可能 bind 时报Operation not permitted或者 bind 成功但系统里 udev 不允许新进程注册接收。建议第一步一律先sudo运行排除权限因素。第三socket 类型不对。必须用SOCK_RAW用SOCK_DGRAM会直接创建失败或行为异常。我不止一次看到有人把 AF_NETLINK 和 SOCK_DGRAM 拼在一起然后怀疑内核没发事件。第四内核配置问题。如果内核编译时没有启用CONFIG_NET或者裁剪了网络子系统netlink 通道自然不工作。这种情况在嵌入式环境里要注意部分精简内核会关掉 netlink只留老旧的uevent_helper。第五进程没跑在正确的时间点。netlink 的 uevent 消息本质上是广播没有历史回放功能。你程序启动得太晚前面的 add 事件已经错过了自然看不到。这不是程序 bug是设计如此。如果你想在程序启动后还能拿到当前系统的设备列表需要额外遍历 /sys 目录或通过/proc自行枚举不能只依赖实时事件流。4.2 缓冲区与丢包问题netlink 的 uevent 传输是无连接、不可靠的类似于 UDP。如果内核短时间内产生大量事件用户空间进程处理不过来内核就会丢弃一部分消息。这种现象在高密度设备热插拔场景里非常明显比如批量插入多个 USB HUB。我实测过默认接收缓冲区约 212KB在短时间插入几十个设备时recv端能收到的消息可能会出现断档明明插了 30 个设备u 只打印了 25 条 add。解决方式就是上文中提到过的把 socket 的SO_RCVBUF调大比如 1MB 或更大。同时用户空间接收逻辑不要做太多耗时操作收到事件后先入队、异步处理让接收线程尽快回到recv等待状态。还有个小技巧打印解析时如果同一个事件里有多个环境变量接收端最好把完整消息先暂存下来而不是边收边处理否则快速连续的多个事件容易被截断混在一起。4.3 内核驱动侧 uevent 发送失败的排查驱动里调了kobject_uevent但用户空间看不到这种问题排查起来更费劲因为处理器没有直接报错。我的排查顺序如下。先看内核日志。调用kobject_uevent_env失败时很多路径会打印kobject_uevent_env: oom、uevent: failed to send之类的错误。先dmesg看一遍能过滤掉一半问题。确认 kobject 是否已经注册。如果你的 kobject 只是被kzalloc分配了内存还没有通过kobject_add挂到 sysfs 上调用kobject_uevent会得到-EINVAL或直接没有反应。因为 uevent 消息里需要带DEVPATH而 devpath 来自 kobject 在 sysfs 中的路径没注册就没有路径。所以发送 uevent 的时机一定放在kobject_add/device_register成功之后。确认 uevent_ops 是否被初始化。内核在发送前会调用kobj-ktype-uevent_ops-uevent()填充设备相关字段。不同子系统的 ktype 需要正确设置否则即使kobject_uevent返回 0用户空间收到的事件里也可能缺少关键的SUBSYSTEM、DEVNAME字段。自定义 kobject 要记得指定合理的 ktype。自定义环境变量太长。我在 2.4 节提过envp 里的内容过长消息构造阶段可能失败或截断。对于动态字符串建议限制在 256 字节以内整条 uevent 消息控制在 2KB 以内比较稳妥。如果确实需要传大块数据不应该塞 uevent应该让用户空间通过 sysfs 或 debugfs 去主动读取。4.4 问题速查表现象可能原因排查建议bind 报 Operation not permitted普通用户权限不足sudo 运行或提高进程权限bind 返回 Address already in usenl_pid 与其他进程冲突将 nl_pid 设为 0 让内核分配recv 一直阻塞收不到任何事件nl_groups 未设置检查 sockaddr_nl 中 nl_groups 是否为 1收不到 add 事件但能收到 change程序启动前事件已广播结合 sysfs 遍历 /sys 枚举现有设备高并发插入时事件丢失接收缓冲区过小setsockopt 调大 SO_RCVBUF收到事件缺少 SUBSYSTEMktype 的 uevent_ops 未正确设置检查 kobject 所属 kset/ktype驱动调用 kobject_uevent 返回非 0kobject 未注册到 sysfs确保在 kobject_add 之后调用内核日志报 uevent: oom环境变量拼接内存不足缩短自定义变量减少动态内容5. 个人体会与建议uevent 应该这么用文章最后我分享几点做驱动和嵌入式 BSP 这几年的真实体会。第一个建议是能用现成的 libudev 就别重复造轮子。裸 socket 适合你第一次理解机制、做教学实验工程代码里直接用 libudev它帮你把缓冲管理、消息解析、设备属性提取都封装好了还兼容不同内核版本的细节差异。你自己用裸 socket 实现很容易遗漏一些边界条件比如消息被截断时如何恢复、多订阅者时 netlink 消息的竞争处理等。第二个建议是事件驱动要和控制读取分开。uevent 只负责告诉你“发生了什么”。比如一张 SD 卡插入uevent 告诉你 devpath 和子系统但卡的具体容量、文件系统类型、坏块信息用不着塞进 uevent而是让用户空间收到事件后再去/sys/block/mmcblk0/...或通过 ioctl 主动读取。这样消息体积小、时延低耦合度也低。第三个建议是测试 uevent 不要总依赖真实硬件。学会用 sysfs 的 uevent 文件制造假事件、用上一节的内核模块主动发送自定义事件这对调试用户态程序极其重要。我做设备服务时基本都是先把接收程序写好再写一个附带自定义事件的内核模块来模拟外设等服务端逻辑稳定了再对接真实硬件效率翻倍。最后再提一个小技巧如果你的用户空间程序用select/epoll同时监听 netlink fd 和其他 fd比如配置文件的 inotify记得给 netlink fd 设置非阻塞模式并且每次读到 EOF 或-1时要判断错误码EAGAIN否则容易把事件循环卡死。这套逻辑在 udev 自身源码里也是这么处理的你在阅读 libudev monitor 的实现时会看到同样的模式。uevent 这个机制不算复杂但它把内核和用户空间恰到好处地连接在一起。理解并掌握这套“内核广播、用户订阅”的模型以后不管是写设备驱动、做系统服务还是排查设备热插拔问题都会顺手很多。
返回列表