Linux热插拔
时间轴
2025-12-02
init
热插拔
热插拔是指在设备运行的情况下,能够安全地插入或拔出硬件设备,而无需关闭或重启系统。这意味着你可以在计算机或其他电子设备上插入或拔出硬件组件(比如 USB 设备,扩展卡,硬件驱动器等),而无需关机或中断正在进行的操作。
热插拔的主要目的是提供方便性和灵活性。通过热插拔,你可以快速更换或添加硬件设备,而无需停止正在进行的任务。这在许多场景下非常有用,比如:
- USB 设备:你可以随时插入或拔出 USB 设备,比如鼠标,键盘,打印机,存储设备等,而无需重新启动系统。
- 硬盘驱动器:在某些服务器或存储系统中,你可以在运行时添加或替换硬盘驱动器,以扩展存储容量或替换故障驱动器。
- 扩展卡:你可以在计算机上插入或拔出显卡,网卡或声卡等扩展卡,以满足不同的需求或升级硬件性能。
为了支持热插拔功能,硬件设备和系统必须具备相应的支持。硬件方面,设备接口必须设计成可以插入和拔出而不会损坏设备或系统。系统需要提供相应的驱动程序和管理功能,以便在插入和拔出设备时进行正确的配置和识别。
热插拔的机制
热插拔是内核和用户空间之间,通过调用用户空间程序(如 hotplug、udev 和 mdev)的交互。当需要通知用户内核发生了某种热插拔事件时,内核才调用这个用户空间程序来实现交互。
在 Linux 内核中,热插拔机制支持 USB 设备、PCI 设备甚至 CPU 等部件的动态插入和拔出。这个机制实现了底层硬件、内核空间和用户空间程序之间的连接,并且一直在不断演变和改进。设备文件系统是用来管理设备文件的一种机制,在 Linux 中有三种常见的设备文件系统:devfs、mdev 和 udev。
- devfs:devfs 是基于内核的动态设备文件系统,最早出现在 Linux 2.3.46 内核中。它通过动态创建和管理设备节点的方式来处理设备文件。然而,devfs 存在一些限制和性能问题,从 Linux 2.6.13 版本开始被移除。
- mdev:mdev 是一个轻量级的热插拔设备文件系统,通常用于嵌入式 Linux 系统。它是 udev的简化版本,使用 uevent_helper 机制来处理设备的插入和拔出事件。mdev 在设备插入时调用相应的用户程序来创建设备节点。
- udev:udev 是目前在 PC 机上广泛使用的热插拔设备文件系统。它基于 netlink 机制,监听内核发送的 uevent 来处理设备的插入和拔出。udev 能够动态创建和管理设备节点,并在设备插入时加载适当的驱动程序。它提供了丰富的配置选项,使用户能够灵活地管理设备文件。
内核发送事件到用户空间
kobject_uevent 是 Linux 内核中的一个函数,用于生成和发送 uevent 事件。它是 udev 和其他设备管理工具与内核通信的一种方式。
1 | // lib/kboject_uevent.c |
参数说明:
- kobj : 要发送 uevent 事件的内核对象(kobject)
- action: 表示触发 uevent 的动作,可以是设备的插入,拔出,属性变化等。以下是一些常见的 action 参数值。这些动作类型用于描述设备发生的不同事件,通过将相应的动作类型作为action 参数传递给 kobject_uevent 函数,可以触发相应的 uevent 事件,通知用户空间的 udev进行相应的操作。
- KOBJ_ADD:表示设备的添加或插入操作,表示添加一个对象到内核对象系统中。
- KOBJ_REMOVE:表示设备的移除或拔出操作,表示从内核对象系统中删除一个对象。
- KOBJ_CHANGE:表示设备属性的修改操作,表示对内核对象进行更改,例如属性修改等。
- KOBJ_MOVE:表示设备的移动操作,即设备从一个位置移动到另一个位置。
- KOBJ_ONLINE:表示设备的上线操作,即设备从离线状态变为在线状态,使其可以被访问。
- KOBJ_OFFLINE:表示设备的离线操作,即设备从在线状态变为离线状态,使其不可以被访问。
- KOBJ_BIND:表示将一个设备连接到内核对象上
- KOBJ_UNBIND: 表示从内核对象上将一个设备解绑
- KOBJ_MAX:表示枚举类型的最大值,通常用于表示没有任何操作行为。
kobject_uevent 函数的主要作用是在内核中生成 uevent 事件,并通过 netlink 机制将该事件发送给用户空间的 udev。在调用该函数时,内核会将相关的设备信息和事件类型封装为 uevent消息,并通过 netlink 套接字将消息发送给用户空间。
用户空间的 udev 会接收到这些 uevent 消息,并根据消息中的设备信息和事件类型来执行相应的操作,例如创建或删除设备节点,加载或卸载驱动程序等
示例
1 |
|
udevadm 命令
udevadm 是一个用于与 udev 设备管理器进行交互的命令行工具。它提供了一系列的子命令,用于查询和管理设备、触发 uevent 事件以及执行其他与 udev 相关的操作。一些常见的udevadm 子命令及其功能如下:
- udevadm info:用于获取设备的详细信息,包括设备路径、属性、驱动程序等。
- udevadm monitor:用于监视和显示当前系统中的 uevent 事件。它会实时显示设备的插入、拔出以及其他相关事件。
- udevadm trigger:用于手动触发设备的 uevent 事件。可以使用该命令模拟设备的插入、拔出等操作,以便触发相应的事件处理。
- udevadm settle:用于等待 udev 处理所有已排队的 uevent 事件。它会阻塞直到 udev 完成当前所有的设备处理操作。
- udevadm control:用于与 udev 守护进程进行交互,控制其行为。例如,可以使用该命令重新加载 udev 规则、设置日志级别等。
- udevadm test:用于测试 udev 规则的匹配和执行过程。可以通过该命令测试特定设备是否能够正确触发相应的规则。
kobject_uevent()
去掉了创建 kset 的步骤,则用户空间无法收到事件,分析如下:
1 | /** |
因为 uevent 是通过 netlink socket 发送给用户空间的应用程序的,而netlink socket 是基于 kset 的。
kobject_uevent_net_broadcast 是一个内核函数,用于将一个 uevent 事件发送到系统中所有的网络命名空间中。它的参数包括 kobj,env,action_string 和 devpath。
- kobj 是与uevent 事件相关的内核对象
- env 是一个包含 uevent 环境变量的列表
- action_string 是一个字符串,表示 uevent 事件的类型
- devpath 是一个字符串,表示与 uevent 事件相关的设备路径。
该函数会遍历系统中所有的网络命名空间,并将 uevent 事件发送到每个网络命名空间中。这个函数的主要作用是在内核中广播一个 uevent 事件,以便用户空间的应用程序可以接收并处理这些事件。
在内核中调用用户空间的 uevent_helper 程序来处理 uevent 事件。uevent_helper 是一个用 户 空 间 程 序 , 它 可 以 在 内 核 空 间 生 成uevent事 件 时 被 调 用 。 如果CONFIG_UEVENT_HELPER 宏 被 定 义 , 那 么 内 核 会 在 生 成uevent事 件 时 调用 uevent_helper 程序,以便在用户空间中处理这些事件。在上述代码中,如果 uevent_helper 变量不为空且 kobj_usermode_filter 函数返回 false,那么就会调用 call_usermodehelper_setup 函数来启动一个用户空间进程,并将 env 中的参数传递给该进程。在这个过程中,env 中的参数将会被转换成环境变量,并被传递给用户空间进程。
完善kset_uevent_ops结构体
1 |
|
netlink监听广播信息
Netlink 是 Linux 内核中用于内核和用户空间之间进行双工通信的机制。它基于 socket 通信机制,并提供了一种可靠的、异步的、多播的、有序的通信方式。
Netlink 机制的主要特点包括:
- 双工通信:Netlink 允许内核和用户空间之间进行双向通信,使得内核可以向用户空间发送消息,同时也可以接收来自用户空间的消息。
- 可靠性:Netlink 提供了可靠的消息传递机制,保证消息的完整性和可靠性。它使用了确认和重传机制,以确保消息的可靠传输。
- 异步通信:Netlink 支持异步通信,即内核和用户空间可以独立地发送和接收消息,无需同步等待对方的响应。
- 多播支持:Netlink 允许向多个进程或套接字广播消息,以实现一对多的通信。
- 有序传输:Netlink 保证消息的有序传输,即发送的消息按照发送的顺序在接收端按序接收。
Netlink 的应用广泛,常见的应用包括:
- 系统管理工具:如 ifconfig、ip 等工具使用 Netlink 与内核通信来获取和配置网络接口的信息。
- 进程间通信:进程可以使用 Netlink 进行跨进程通信,实现进程间的数据交换和协调。
- 内核模块和用户空间应用程序的通信:内核模块可以通过 Netlink 向用户空间应用程序发送通知或接收用户空间应用程序的指令。
netlink的使用
创建socket
在 Linux socket 编程中,创建套接字是构建网络应用程序的第一步。套接字可以理解为应用程序和网络之间的桥梁,用于在网络上进行数据的收发和处理。该系统调用的原型和所需头文件如下所示:
| 头文件 | 函数原型 |
|---|---|
#include<sys/types.h>#include<sys/socket.h> |
int socket(int domain, int type, int protocol); |
- domain 参数指定了套接字的协议族
协议族指定了套接字所使用的协议类型,常用的协议族包括 AFINET、AF_INET6、AF_UNIX等。其中,AF_INET 表示 IPv4 协议族,AF_INET6 表示 IPv6 协议族,AF_UNIX 表示 Unix 域协议族,这里的协议族为 netlink,所以该参数要在程序中设置为 AF NETLINK。
- type 参数指定了套接字的类型
套接字类型指定了套接字的数据传输方式,常用的套接字类型包括 SOCK_STREAM、SOCK_DGRAM、SOCK_RAW 等。其中,SOCK_STREAM 表示面向连接的流套接字,主要用于可靠传输数据,例如 TCP 协议。SOCK_DGRAM 表示无连接的数据报套接字,主要用于不可靠传输数据,例如 UDP 协议。在本实验中该参数要设置为 SOCK_RAW 表示原始套接字,可以直接访问底层网络协议。
- protocol 参数指定了套接字所使用的具体协议。下面分别介绍这三个参数的含义:
协议类型指定了套接字所使用的具体协议类型,常用的协议类型包括 IPPROTOTCP、IPPROTO_UDP、IPPROTO_ICMP 等。其中,IPPROTO_TCP 表示 TCP 协议,IPPROTO_UDP 表示 UDP协议,IPPROTO_ICMP 表示 ICMP 协议,在本实验中,我们要设置为 `NETLINK KOBJECT UEVENT`
将使用以下代码创建一个新的套接字
1 | int socket_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT); |
AF_NETLINK:指定了使用 Netlink 协议族。Netlink 协议族是一种 Linux 特定的协议族,用于内核和用户空间之间的通信
SOCK_RAW:指定了创建原始套接字,这种套接字类型可以直接访问底层协议,而不需要进行协议栈处理。在这种情况下,我们可以直接使用 Netlink 协议进行通信。
NETLINK_KOBJECT_UEVENT:指定了 Netlink 协议的一种类型,即 kobject uevent 类型。kobjectuevent 用于内核对象相关的事件通知,当内核中的 kobject 对象发生变化时,会通过此类型的Netlink 消息通知用户空间。
绑定套接字
| 所需头文件 | 函数原型 |
|---|---|
#include<sys/types.h>#include<sys/socket.h> |
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
sockfd 参数指定了需要绑定的套接字描述符,
addr 参数指定了需要绑定的地址信息,这里使用 sockaddr_nl 结构体,sockaddr_nl 结构体的定义如下:
1
2
3
4
5
6struct sockaddr_nl {
sa_family_t nl_family; // AF_NETLINK
unsigned short nl_pad; // zero
uint32_t nl_pid; // port ID
uint32_t nl_groups;// multicast groups mask
};- nl_family:表示地址族,此处固定为 AF_NETLINK,指示使用 Netlink 协议族。
- nl_pad:填充字段,设置为 0。在结构体中进行字节对齐时使用。
- nl_pid:端口 ID,表示进程的标识符。可以将其设置为当前进程的 PID,也可以设为 0,表示不加入任何多播组。
- nl_groups:多播组掩码,用于指定感兴趣的多播组。当设置为 1 时,表示用户空间进程只会接收内核事件的基本组的内核事件。这意味着,用户空间进程将只接收到属于基本组的内核事件,而不会接收其他多播组的事件。
addrlen 参数:addrlen 参数是一个整数,指定了 addr 所指向的结构体对应的字节长度。它用于确保正确解析传递给 addr 参数的结构体的大小
示例
1 | struct sockaddr_nl nl; // 定义一个指向 struct sockaddr_nl 结构体的指针 nl |
接收数据
Netlink 套接字在接收数据时不需要调用 listen 函数,而是可以直接使用 recv 函数进行接收。
| 所需头文件 | 函数原型 |
|---|---|
#include<sys/types.h>#include<sys/socket.h> |
ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
函数参数:
- sockfd:指定套接字描述符,即要接收数据的 Netlink 套接字。
- buf:指向数据接收缓冲区的指针,用于存储接收到的数据。
- len:指定要读取的数据的字节大小。
- flags:指定一些标志,用于控制数据的接收方式。通常情况下,可以将其设置为 0。
返回值:
- 成功情况下,返回实际读取到的字节数。
- 如果返回值为 0,表示对方已经关闭了连接。
- 如果返回值为-1,表示发生了错误,可以通过查看 errno 变量来获取具体的错误代码。
使用 recv 函数可以从指定的 Netlink 套接字中接收数据,并将其存储在提供的缓冲区中。函数的返回值表示实际读取到的字节数,可以根据返回值来判断是否成功接收到数据。
示例代码:
1 | while (1) { |
示例代码
1 |
|
uevent_helper
在kobject_uevent()中
1 |
|
第 3 行为一个 if 表达式,它检查 uevent_helper 数组的第一个元素是否为真。并调用 kobj_usermode_filter 函数进行用户模式过滤,uevent_helper定义如下:
1 | char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH; |
CONFIG_UEVENT_HELPER_PATH是 一 个 宏 定 义 在 内 核 源 码 的include/generated/autoconf.h文件中,如下所示:
1 |
该宏为空 ,所以为了使能 uevent_helper 功能需要在图形配置界面使能CONFIG_UEVENT_HELPER 和 CONFIG_UEVENT_HELPER_PATH 两个宏:
配置方法 1:
1 | Device Drivers |
在上面的配置 1 中设置了 uevent helper 和相对应的路径,这就是配置方法 1,但是这种方式需要重新编译内核,使用起来较为麻烦,除了方法一之外还有更快捷的方法 2 和方法 3:
配置方法 2:
无论是否配置了 CONFIG_UEVENT_HELPER_PATH,在系统启动后,可以使用以下命令来设置 uevent_helper:
1 | echo /sbin/mdev > /sys/kernel/uevent_helper |
这将把 uevent_helper 设置为/sbin/mdex。
配置方法 3:
无论是否配置了 CONFIG_UEVENT_HELPER_PATH,在系统启动后,可以使用以下命令来设置 uevent_helper:
1 | echo /sbin/mdev > /proc/sys/kernel/hotplug |
这将把 uevent_helper 设置为/sbin/mdev。
需要注意的是配置方法 2 和配置方法 3 依赖于上面的配置的File systems和Networking选项,并且可以通过配置方法 2 和配置方法 3 修改配置方法 1 中已经写好的值。对/proc/sys/kernel/hotplug 和/sys/kernel/uevent_helper 进行读写都是为了对 uevent_helper属性进行读写操作。
/sys/kernel/uevent_helper 是 sysfs 文件系统中的一个文件,它是 uevent_helper 属性的接口。通过对该文件进行读写操作,可以读取或修改 uevent_helper 属性的值。在内核源码的kernel/ksysfs.c 目录下可以找到对 uevent_helper 属性的定义和相关操作的实现,具体内容如下所示:
1 | // kernel/ksysfs.c |
uevent_helper_show 函数用于将 uevent_helper 的值写入 buf 中,并返回写入的字符数。
uevent_helper_store 函数用于将 buf 中的值复制到 uevent_helper 中,并根据需要进行处理,然后返回写入的字符数。
/proc/sys/kernel/hotplug 是一个虚拟文件,用于配置内核中的热插拔事件处理程序。通过对该文件进行写操作,可以设置 uevent_helper 属性的值。在内核源码的 kernel/sysctl.c 文件中,可以看到对 hotplug 操作其实是对 uevent_helper 进行操作。
1 | // kernel/sysctl.c` |
这段代码定义了一个名为 hotplug 的文件,用于处理 uevent 事件。它与 uevent_helper 属性相关联。
.procname表示文件名,即/proc/hotplug。.data是一个指向 uevent_helper 结构体的指针,用于保存与该文件相关的数据。该指针指向 uevent_helper 结构体,用于处理 uevent 事件。.maxlen表示文件的最大长度,即文件内容的最大长度。该值为 UEVENT_HELPER_PATH_LEN,表示文件内容的最大长度为 UEVENT_HELPER_PATH_LEN。.mode表示文件的访问权限。该值为 0644,表示该文件的权限为-rw-r--r--,即所有用户都可以读取该文件,但只有 root 用户可以写入该文件。
测试:
在内核中调用用户空间的 uevent_helper 程序来处理 uevent 事件。
uevent_helper 是一个用 户 空 间 程 序 , 它 可 以 在 内 核 空 间 生 成uevent事 件 时 被 调 用 。 如果CONFIG_UEVENT_HELPER 宏 被 定 义 , 那 么 内 核 会 在 生 成uevent事 件 时 调用 uevent_helper 程序,以便在用户空间中处理这些事件。在kobject_uevent_env()函数中,如果 uevent_helper 变量不为空且 kobj_usermode_filter 函数返回 false,那么就会调用 call_usermodehelper_setup 函数来启动一个用户空间进程,并将 env 中的参数传递给该进程。在这个过程中,env 中的参数将会被转换成环境变量,并被传递给用户空间进程。
1 |
|
编译:
1 | aarch64-linux-gnu-gcc -o mdev mdev.c |
使用 udev 挂载 U 盘和 T 卡
想使用 udev 来实现 U 盘的自动挂载,还需在/etc/udev/rules.d 目录下创建相应的规则文件
使用 mdev 挂载 U 盘和 T 卡
跟 udev 相同,mdev 也需要添加相应的规则,不同的是 mdev 使用/etc/mdev.conf 文件来配置 mdev 工具的规则和行为,要想使用 mdev 自动挂载 U 盘需要向/etc/mdev.conf 文件中添加以下两条规则
1 | sd[a-z][0-9] 0:0 666 @/etc/mdev/usb_insert.sh |
这两个规则用于处理 U 盘的热插拔事件,并执行相应的操作。在 /etc/mdev.conf 文件中,每一行都是一个规则,具有以下格式:
1 | <设备节点正则表达式> <设备的所有者:设备的所属组> <设备的权限> <设备插入或移除时需要执行的命令> |
下面是对上述两个规则的详细介绍:
sd[a-z][0-9]是一个正则表达式模式,用于匹配以 “sd” 开头,后跟一个小写字母和一个数字的设备节点,例如/dev/sda1、/dev/sdb2 等。0:0 666表示设置设备节点的所有者和权限。0:0 表示所有者和所属组的用户ID和组 ID 均为 0,即 root 用户。666 表示权限为可读可写。@/etc/mdev/usb_insert.sh表示当符合规则的设备插入时,mdev 会执行/etc/mdev/usb_insert.sh脚本。@ 符号表示执行的是一个 shell 命令。$/etc/mdev/usb_remove.sh表示当符合规则的设备移除时,mdev 会执行/etc/mdev/usb_remove.sh脚本。$ 符号表示执行的是一个内部命令。

