第一百五十二章:热插拔
合道期涉及内核源码:
一
林小源来到设备山脉的一处入口广场。广场上人来人往——每隔几息,就有一个新的设备从广场边缘的传送门中出现,或者一个已有设备被传送门吸走。
一个穿着橙色制服的哨兵站在广场中央,他的眼睛紧盯着每一个传送门。每当有新设备出现,他就吹响一声号角——号角声沿着广场扩散,触发一连串的连锁反应。
"我是 hotplug 机制。"哨兵说,"设备随时可能插入或拔出——USB 闪存、外接显示器、PCIe 网卡——我负责在设备出现或消失时通知整个系统。"
一个 USB 闪存盘从传送门中浮现。哨兵立刻吹响号角,号角声化作一道波纹向四面八方扩散。波纹首先到达内核空间的一个监听者,监听者开始检测设备类型、加载驱动、初始化硬件。然后波纹继续扩散到用户空间——一个叫 udev 的管家接收到波纹,根据预设的规则在 /dev/ 下创建了设备节点。
"从设备插入到可用,整个过程不超过一秒钟。"哨兵说。
林小源看着那些不断出现和消失的设备,感叹道:即插即用,说起来简单,背后可是一整套机制在撑着。
/*
* 热插拔:
*
* 设备插入 → 内核检测 → 加载驱动 → 设备可用
* 设备拔出 → 内核检测 → 卸载驱动 → 设备移除
*
* 热插拔的机制:
* 1. 硬件检测(中断、轮询)
* 2. 内核通知(kobject_uevent)
* 3. 用户空间响应(udev)
*
* udev:
* 用户空间设备管理器
* 根据内核事件创建/删除设备节点
* 根据规则设置权限
*
* 热插拔事件:
* KOBJ_ADD — 设备添加
* KOBJ_REMOVE — 设备移除
* KOBJ_CHANGE — 设备变化
*/
printf("=== 热插拔 — 动态管理设备 ===\n\n");
printf("热插拔:\n\n");
printf(" 设备插入 → 内核检测 → 加载驱动\n");
printf(" 设备拔出 → 内核检测 → 卸载驱动\n\n");
printf("--- 热插拔的机制 ---\n");
printf("1. 硬件检测:\n");
printf(" 中断或轮询\n");
printf(" 检测设备变化\n\n");
printf("2. 内核通知:\n");
printf(" kobject_uevent()\n");
printf(" 发送 uevent 到用户空间\n\n");
printf("3. 用户空间响应:\n");
printf(" udev 接收事件\n");
printf(" 创建/删除设备节点\n\n");
printf("--- udev ---\n");
printf("用户空间设备管理器:\n");
printf(" 接收内核 uevent\n");
printf(" 创建设备节点 /dev/*\n");
printf(" 设置权限\n\n");
printf("udev 规则:\n");
printf(" /etc/udev/rules.d/\n");
printf(" 根据设备属性匹配\n\n");
printf("--- 热插拔事件 ---\n");
printf("KOBJ_ADD:\n");
printf(" 设备添加\n\n");
printf("KOBJ_REMOVE:\n");
printf(" 设备移除\n\n");
printf("KOBJ_CHANGE:\n");
printf(" 设备变化\n\n");
printf("--- 查看热插拔事件 ---\n");
printf("udevadm monitor\n");
printf(" 显示内核事件\n\n");
printf("dmesg | grep -i usb\n");
printf(" 查看 USB 热插拔日志\n");#include <stdio.h>
/*
* 热插拔:
*
* 设备插入 → 内核检测 → 加载驱动 → 设备可用
* 设备拔出 → 内核检测 → 卸载驱动 → 设备移除
*
* 热插拔的机制:
* 1. 硬件检测(中断、轮询)
* 2. 内核通知(kobject_uevent)
* 3. 用户空间响应(udev)
*
* udev:
* 用户空间设备管理器
* 根据内核事件创建/删除设备节点
* 根据规则设置权限
*
* 热插拔事件:
* KOBJ_ADD — 设备添加
* KOBJ_REMOVE — 设备移除
* KOBJ_CHANGE — 设备变化
*/
int main() {
printf("=== 热插拔 — 动态管理设备 ===\n\n");
printf("热插拔:\n\n");
printf(" 设备插入 → 内核检测 → 加载驱动\n");
printf(" 设备拔出 → 内核检测 → 卸载驱动\n\n");
printf("--- 热插拔的机制 ---\n");
printf("1. 硬件检测:\n");
printf(" 中断或轮询\n");
printf(" 检测设备变化\n\n");
printf("2. 内核通知:\n");
printf(" kobject_uevent()\n");
printf(" 发送 uevent 到用户空间\n\n");
printf("3. 用户空间响应:\n");
printf(" udev 接收事件\n");
printf(" 创建/删除设备节点\n\n");
printf("--- udev ---\n");
printf("用户空间设备管理器:\n");
printf(" 接收内核 uevent\n");
printf(" 创建设备节点 /dev/*\n");
printf(" 设置权限\n\n");
printf("udev 规则:\n");
printf(" /etc/udev/rules.d/\n");
printf(" 根据设备属性匹配\n\n");
printf("--- 热插拔事件 ---\n");
printf("KOBJ_ADD:\n");
printf(" 设备添加\n\n");
printf("KOBJ_REMOVE:\n");
printf(" 设备移除\n\n");
printf("KOBJ_CHANGE:\n");
printf(" 设备变化\n\n");
printf("--- 查看热插拔事件 ---\n");
printf("udevadm monitor\n");
printf(" 显示内核事件\n\n");
printf("dmesg | grep -i usb\n");
printf(" 查看 USB 热插拔日志\n");
return 0;
}二
林小源跟着 udev 管家走进了广场旁的一间规则室。规则室的墙壁上挂满了规则牌——每块规则牌上都写着匹配条件和执行动作。
"我就是 udev。"管家是个中年人,戴着一副圆框眼镜,手里拿着一本厚厚的规则手册,"内核通过 kobject_uevent() 发送事件给我——KOBJ_ADD 是设备添加,KOBJ_REMOVE 是设备移除,KOBJ_CHANGE 是设备状态变化。我收到事件后,根据这些规则来决定做什么。"
他翻开规则手册,指着一行规则:"比如这条——当一个 USB 设备的 vendor ID 是 0x1234 且 product ID 是 0x5678 时,在 /dev/ 下创建一个叫 'my_device' 的节点,并设置权限为 0666。"
林小源看到管家的工作台上有三个收集箱,分别标着 ADD、REMOVE、CHANGE。每当哨兵的号角声传来,一个事件令牌就从天花板上落下,落入对应的收集箱。管家捡起令牌,翻看规则手册,然后执行对应的动作。
"你不能自己决定规则吗?"林小源问。
"规则在 /etc/udev/rules.d/ 里,由管理员编写。"管家推了推眼镜,"我只负责执行。内核告诉我发生了什么,规则告诉我该怎么做。我只是一个忠实的执行者。"
远处传来一声低沉的号角——一个 USB 设备被拔出了。管家从 REMOVE 箱中捡起一个令牌,看了看设备名,然后走到 /dev/ 目录下,删除了对应的节点。整个过程干净利落。
udev 这个管家倒是省心——内核说啥它干啥,规则怎么写它就怎么执行。
三
林小源回到广场,仔细观察哨兵的号角波纹。他发现波纹的传播并不是简单的广播——它是通过 netlink socket 发送到用户空间的。
"kobject_uevent() 是关键函数,"哨兵说,"当一个 kobject 的状态发生变化时,内核调用 kobject_uevent() 构造一个 uevent 消息,然后通过 netlink 广播到所有监听者。"
林小源看到波纹中携带着丰富的信息——设备名、子系统名、设备属性、动作类型。这些信息被编码成键值对,附在 uevent 消息里。
"udev 不是唯一的监听者,"哨兵补充道,"任何程序都可以监听 netlink socket 来接收 uevent。有些嵌入式系统用 mdev 代替 udev——更轻量,但原理相同。"
广场边缘又出现了一个新设备——这次是一个 PCIe 网卡。哨兵没有吹号角——PCIe 的热插拔需要 BIOS 和内核的配合,不像 USB 那么简单。
"不是所有总线都支持热插拔,"哨兵说,"USB 天生就是为热插拔设计的——它的电气规范支持设备的动态连接和断开。PCIe 的热插拔需要额外的硬件支持。但无论是哪种总线,通知机制都是一样的:kobject_uevent → udev。"
他忽然明白,热插拔的核心不是物理上的插拔,而是那套事件通知机制——kobject_uevent 到 udev,一路传递下去。
道藏笔记
内核启示
热插拔的流程其实很直白:硬件检测到设备变化(中断或轮询),内核通过 kobject_uevent() 构造一条 uevent 消息,通过 netlink socket 广播到用户空间。udev 作为忠实的执行者,收到消息后查规则手册,该创建节点就创建、该删除就删除。KOBJ_ADD 是设备来了,KOBJ_REMOVE 是设备走了,KOBJ_CHANGE 是设备状态变了。不是所有总线都支持热插拔——USB 天生就是为这个设计的,PCIe 需要额外硬件支持。但不管哪种总线,通知机制都一样:内核发事件,用户空间响应。你可以用 udevadm monitor 实时查看这些事件。
热插拔之试
热插拔事件需要通知用户空间时,本章提到内核会发出哪个 uevent 接口?