第九十六章 热插拔简介
热插拔是指在设备运行时安全地插入或拔出硬件,无需关闭或重启系统。
它提供了方便性和灵活性,允许快速更换或添加硬件而无需中断任务。
以下是一些应用场景及支持热插拔所需的条件:
应用场景:
USB设备(鼠标、键盘、打印机、存储设备等)。
硬盘驱动器(扩展存储容量或替换故障驱动器)。
扩展卡(显卡、网卡、声卡等)。
支持条件:
硬件接口设计需支持热插拔。
系统需有相应的驱动程序和管理功能。
热插拔机制:
内核与用户空间通过用户空间程序(如hotplug、udev、mdev)交互。
Linux内核支持USB、PCI、CPU等部件的动态插入和拔出。
设备文件系统:
devfs(已过时):基于内核的动态设备文件系统,从Linux 2.6.13版本开始被移除。
mdev:轻量级热插拔设备文件系统,使用uevent_helper机制。
udev:广泛使用的热插拔设备文件系统,基于netlink机制监听uevent,动态创建和管理设备节点。
udev是目前应用最广泛的设备文件系统,提供丰富的配置选项,
mdev主要用于嵌入式系统,提供轻量级设备管理功能。
这些设备文件系统管理设备文件,使用户空间程序能够与底层硬件交互。
——————————————————————————————————————————
在Linux系统中,udev(Userspace DEV)是一个设备管理器,它负责在内核识别到新设备时,动态地管理这些设备的节点(即设备文件,通常位于/dev目录下)。
当你使用命令udevadm monitor &在后台启动udevadm工具的监视模式时,它会实时显示系统中发生的uevent(用户空间事件)事件。
这些事件通常与设备的添加、移除、更改等状态变化相关。
在驱动加载之后,如果udev接收到一个add动作,这意味着内核已经识别到了一个新设备,并且已经发送了一个uevent事件来通知用户空间。这个add动作是udev规则处理流程的一部分,它告诉udev需要为这个新设备创建或更新设备节点。
第九十七章 内核如何发送事件到用户空间
97.1 相关接口函数
kobject_uevent() 用于生成和发送 uevent 事件,实现内核与用户空间(如 udev)的通信。
/*生成和发送uevent事件*/
int kobject_uevent(struct kobject *kobj, //kobj对象enum kobject_action action); //事件类型,如添加、移除、属性变化等。/*常见 action 参数包括:
KOBJ_ADD:设备添加。
KOBJ_REMOVE:设备移除。
KOBJ_CHANGE:设备属性变化。
KOBJ_MOVE:设备移动。
KOBJ_ONLINE:设备上线。
KOBJ_OFFLINE:设备离线。
KOBJ_BIND / KOBJ_UNBIND:设备绑定/解绑。*/
97.2 udevadm 命令
udevadm 是 Linux 中与 udev 设备管理器交互的工具,
常用子命令包括:
udevadm info: 获取设备详细信息。
udevadm monitor: 监视系统中的 uevent 事件。
udevadm trigger: 手动触发设备事件。
udevadm settle: 等待 udev 处理所有事件。
udevadm control: 控制 udev 守护进程行为。
udevadm test:测试 udev 规则匹配。
udevadm info --query=all --name=/dev/sdX //获取设备详细信息
udevadm monitor --udev //监视系统中的uevent事件
udevadm trigger --action=add --path=/dev/sdX //手动触发设备事件
udevadm settle //等待udev处理所有的事件
udevadm control --reload-rules //控制udev守护进程的行为
udevadm test /dev/sdX //测试udev规则匹配
在 iTOP-RK3568 开发板上烧写 buildroot 系统,输入“udevadm monitor &”可以监视和显示当前系统中的 uevent 事件,在之后的实验中,我们要使用这个方法。
struct kobject *mykobject01;
struct kset *mykset;
struct kobj_type mytype;// 模块的初始化函数
static int mykobj_init(void)
{int ret;// 创建并添加一个 ksetmykset = kset_create_and_add("mykset", NULL, NULL);// 分配并初始化一个 kobjectmykobject01 = kzalloc(sizeof(struct kobject), GFP_KERNEL);mykobject01->kset = mykset;// 初始化并添加 kobject 到内核ret = kobject_init_and_add(mykobject01, &mytype, NULL, "%s", "mykobject01");// 触发一个 uevent 事件,表示 kobject 的属性发生了变化ret = kobject_uevent(mykobject01, KOBJ_CHANGE);return 0;
}// 模块退出函数
static void mykobj_exit(void)
{// 释放 kobjectkobject_put(mykobject01);
}
开发板启动之后,使用命令“udevadm monitor &”监视和显示当前系统中的 uevent 事件。
然后使用 insmode加载模块。
驱动加载之后,如图所示 udev 接收到 change 动作,说明 uevent 事件已经发送成功了。
/mykset/mykobject01 是 kobject 在根目录/sys/下的路径。
同时,需要注意,如果没有正确创建 kset会导致 uevent事件无法被正确发送到用户空间。
97.3 kobject_uevent发送通知流程
在Linux内核中,kobject_uevent()函数用于通知用户空间发生了某个事件。
这个函数依赖于kobject(内核对象)所属的 kset(内核对象集合)来正确发送事件。
1、 kobject_uevent() 函数用于通知用户空间uevent事件的发生。
2、 调用 kobject_uevent_env(),并传递NULL作为环境变量。
/*通知用户空间 uevent事件的发生*/
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{ return kobject_uevent_env(kobj, action, NULL);
}
kobject_uevent_env() 函数:
查找kobject所属的kset。
如果没有找到kset,则无法发送事件,返回错误。
3、检查是否跳过事件:
如果设置了 kobj->uevent_suppress,则跳过事件。
如果存在 uevent_ops->filter 且返回false,则跳过事件。
4、获取子系统名称:
尝试从uevent_ops->name获取子系统名称,否则使用 kset的名称。
5、准备并发送事件:
分配并填充 kobj_uevent_env结构体。
struct kobj_uevent_env { char *argv[3]; /* 用户空间可执行文件的路径及其参数 */ char *envp[UEVENT_NUM_ENVP]; /* 环境变量的地址数组 */ int envp_idx; /* 已使用的环境变量数量 */ char buf[UEVENT_BUFFER_SIZE]; /* 存储环境变量内容的缓冲区 */ int buflen; /* 缓冲区中已使用的长度或即将存储字符串的位置 */
};
/*当内核中的设备状态发生变化时(如设备添加、移除、更改等),
内核会通过 uevent 机制向用户空间发送通知。
此时,kobj_uevent_env 结构体被用来封装与事件相关的环境变量和参数。*/
//用户空间的应用程序可以监听这些 uevent 事件,并根据事件中的环境变量和参数来执行相应的操作。
添加标准环境变量如 ACTION, DEVPATH, SUBSYSTEM。
调用 kobject_uevent_net_broadcast()在系统中的所有网络命名空间中广播事件。
6、调用用户空间的 uevent_helper(如果配置):
在早期启动阶段,调用用户空间的程序来处理事件。
如果kobject没有所属的kset,内核无法正确设置事件的环境变量,也无法找到适当的子系统名称,因此无法有效地通知用户空间。
这就是为什么没有创建kset会导致用户空间无法接收事件的原因。
第九十八章 完善 kset_uevent_ops 结构体
struct kobject *mykobject01;
struct kobject *mykobject02;
struct kset *mykset;
struct kobj_type mytype;
// 定义一个回调函数,返回 kset 的名称
const char *myname(struct kset *kset, struct kobject *kobj)
{return "my_kset";
};// 定义一个回调函数,处理 kset 的 uevent 事件
int myevent(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env)
{//向设备事件相关的环境变量中添加一个新的变量add_uevent_var(env, "MYDEVICE=%s", "TOPEET");return 0;
};// 定义一个回调函数,用于过滤 kset 中的 kobject
int myfilter(struct kset *kset, struct kobject *kobj)
{if (strcmp(kobj->name, "mykobject01") == 0){return 0; // 返回 0 表示通过过滤}else{return 1; // 返回 1 表示过滤掉}
};/*填充 kset_uevent_opsm,包括过滤函数、uevent函数、和name函数*/
struct kset_uevent_ops my_uevent_ops = {.filter = myfilter,.uevent = myevent,.name = myname, };// 模块的初始化函数
static int mykobj_init(void)
{int ret;// 创建并添加一个 ksetmykset = kset_create_and_add("mykset", &my_uevent_ops, NULL);// 分配并初始化一个 kobjectmykobject01 = kzalloc(sizeof(struct kobject), GFP_KERNEL);mykobject01->kset = mykset;// 初始化并添加 kobject 到 ksetret = kobject_init_and_add(mykobject01, &mytype, NULL, "%s", "mykobject01");// 分配并初始化一个 kobjectmykobject02 = kzalloc(sizeof(struct kobject), GFP_KERNEL);mykobject02->kset = mykset;// 初始化并添加 kobject 到 ksetret = kobject_init_and_add(mykobject02, &mytype, NULL, "%s", "mykobject02");// 触发一个 uevent 事件,表示 mykobject01 的属性发生了变化ret = kobject_uevent(mykobject01, KOBJ_CHANGE);// 触发一个 uevent 事件,表示 mykobject02 被添加ret = kobject_uevent(mykobject02, KOBJ_ADD);return 0;
}// 模块退出函数
static void mykobj_exit(void)
{// 释放 kobjectkobject_put(mykobject01);kobject_put(mykobject02);kset_unregister(mykset);
}
开发板启动之后,使用命令“udevadm monitor &”监视和显示当前系统中的 uevent 事件。
驱动加载之后,如下图所示 udev 接收到 add 动作,说明 uevent 事件已经发送成功了。
/mykset/mykobject02 是 kobject 在根目录/sys/下的路径。
由于 kset_uevent_ops.filter过滤了 kobj01因此kobj01发送事件不成功。
第九十九章 netlink 监听广播信息
99.1 netlink 机制介绍
Netlink 是 Linux 内核与用户空间之间的一种双工通信机制,基于 socket,具备以下特点:
双工通信:内核与用户空间可双向通信。
可靠性:通过确认和重传机制保证消息可靠传递。
异步通信:双方独立发送和接收消息,无需同步等待。
多播支持:可向多个进程或套接字广播消息。
有序传输:保证消息按发送顺序接收。
Netlink 常见应用包括:
系统管理工具:如 ifconfig、ip 工具,用于获取和配置网络接口信息。
进程间通信:实现跨进程数据交换和协调。
内核模块与用户空间通信:内核模块向用户空间发送通知或接收指令。
99.2 netlink 的使用
99.2.1 创建 socket
在 Linux 中,创建套接字是网络编程的首要步骤,它充当了应用程序与网络间的桥梁。
要创建 Netlink 套接字,需包含头文件 <sys/types.h> 和 <sys/socket.h>,并
使用 socket() 系统调用创建 Netlink 套接字。
协议族:设为 AF_NETLINK,指定使用 Netlink 协议族,用于内核与用户空间通信。
套接字类型:选择 SOCK_RAW,表示创建原始套接字,直接访问 Netlink 底层协议。
协议类型:指定为 NETLINK_KOBJECT_UEVENT,用于接收内核对象事件通知。
#include <sys/types.h>
#include <sys/socket.h> /*创建socket套接字,使用socket系统调用*/
int socket_fd = socket(AF_NETLINK, //协议族,netlink协议族SOCK_RAW, //套接字类型,原始套接字NETLINK_KOBJECT_UEVENT);//协议类型,netlink_kobject_uevent类型,//用于接收内核对象事件通知
此代码创建了一个 Netlink 套接字,用于接收内核中的 kobject 对象变化事件通知。
99.2.2 绑定套接字
在 Linux 中,创建套接字后通过 bind() 系统调用将其与地址绑定,以便接收网络事件。
对于 Netlink 套接字,使用 sockaddr_nl 结构体指定地址信息。
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h> // for bzero int socket_fd = /* ... socket() call ... */; struct sockaddr_nl nl;
memset(&nl, 0, sizeof(nl)); // 初始化结构体为0
nl.nl_family = AF_NETLINK; //协议族,netlink协议族
nl.nl_pid = 0; // 0代表广播给所有进程,或设为当前进程PID
nl.nl_groups = 1; // 1代表接收基本组事件 int ret = bind(socket_fd, (struct sockaddr *)&nl, sizeof(nl));
if (ret < 0) { perror("bind error"); return -1;
}
这里的“基本组”是一个特定的多播组,它通常包含了内核生成的一些基础或通用的事件。
99.2.3 接收数据
Netlink 套接字接收数据时无需调用 listen,直接使用 recv() 函数即可。
#include <sys/types.h>
#include <sys/socket.h> ssize_t recv(int sockfd, //套接字描述符void *buf, //数据缓冲区指针size_t len, //数据缓冲区长度int flags); //接收操作的行为
/*
MSG_PEEK:查看数据但不从缓冲区移除
MSG_WAITALL:等待直到接收到完整的数据长度
MSG_OOB: 接受紧急带外数据
MSG_DONTWAIT:调用完立刻返回,不阻塞
0意味着使用该函数的默认行为,即阻塞接收、尝试完整读取、数据移除以及无特殊处理。
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h> // for bzero
#include <stdio.h> // for printf
#include <errno.h> // for errno int socket_fd = /* ... Netlink socket creation and setup ... */;
char buf[4096]; while (1) { memset(buf, 0, sizeof(buf)); // 初始化缓冲区 ssize_t len = recv(socket_fd, buf, sizeof(buf), 0); if (len > 0) { // 替换 '\0' 为 '\n' 并打印接收到的数据 for (ssize_t i = 0; i < len; i++) { if (buf[i] == '\0') { buf[i] = '\n'; } } printf("%s", buf); } else if (len == 0) { // 连接关闭(对 Netlink 套接字不常见) printf("Connection closed\n"); break; } else { // 错误发生 perror("recv error"); break; }
}
编译完后生成可执行程序,当驱动模块通过 kobject_uevent()发送 uevent事件时,可执行程序可以接收到 uevent事件执行相应处理。
第一百章 uevent_helper
前面我们介绍了通过 kobject_uevent()广播内核事件的方法,
下面我们介绍通过可执行程序发送事件到用户空间的方法。
uevent_helper 是 Linux 内核中用于处理热插拔(hotplug)事件的一个机制。
当内核检测到设备连接或断开等事件时,会生成一个 uevent(用户空间事件),并通过 uevent_helper 指定的程序来处理这个事件。
100.1 设置 uevent_helper
kobject_uevent()经历了查找kset、处理 filter、处理name、处理kobject_uevent_env结构体,
调用kobject_uevent_net_broadcast()在网络空间中广播事件。
在Linux内核中,uevent_helper用于在系统启动时处理设备事件。
它的路径可以在内核配置时通过CONFIG_UEVENT_HELPER_PATH宏定义,但通常这个宏是空的,需要在其他地方设置。
例如可以在内核源码的 menuconfnig界面配置支持 uevent_helper()。
在上面的配置 1 中设置了 uevent helper 和相对应的路径,这就是配置方法 1,但是这种方式需要重新编译内核,使用起来较为麻烦。其他配置方法请参考开发文档,此处不赘述。
uevent_helper的定义和存储:
char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
//CONFIG_UEVENT_HELPER_PATH通常为空,所以需要在运行时设置。
sysfs接口(kernel/ksysfs.c):
提供了 uevent_helper的读写接口。
#ifdef CONFIG_UEVENT_HELPER // 如果定义了CONFIG_UEVENT_HELPER宏 // 显示uevent_helper的路径
static ssize_t uevent_helper_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{ // 将uevent_helper的路径复制到buf中,并返回复制的字节数 return sprintf(buf, "%s\n", uevent_helper);
} // 设置uevent_helper的路径
static ssize_t uevent_helper_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{ // 如果buf的长度超过最大允许长度,则返回错误 if (count + 1 > UEVENT_HELPER_PATH_LEN) return -ENOENT; // 将buf的内容复制到uevent_helper中,并添加字符串结束符'\0' memcpy(uevent_helper, buf, count); uevent_helper[count] = '\0'; // 如果buf的最后一个字符是换行符,则将其替换为字符串结束符'\0' if (count && uevent_helper[count - 1] == '\n') uevent_helper[count - 1] = '\0'; // 返回成功复制的字节数 return count;
} // 定义一个可读写的内核属性,属性名为"uevent_helper"
KERNEL_ATTR_RW(uevent_helper); #endif // 结束宏定义的检查
proc接口(kernel/sysctl.c):
在/proc/sys/kernel/下提供了一个名为 hotplug的文件,用于设置uevent_helper。
#ifdef CONFIG_UEVENT_HELPER // 如果定义了CONFIG_UEVENT_HELPER宏 // 定义一个与/proc文件系统交互的内核参数结构体
{ // 参数在/proc文件系统中的名称,用户空间可以通过/proc/hotplug访问 .procname = "hotplug", // 指向要操作的数据的指针,这里是uevent_helper的路径 .data = &uevent_helper,// 允许的最大长度,确保不会超出uevent_helper的存储空间 .maxlen = UEVENT_HELPER_PATH_LEN, // 文件的权限,0644表示所有者可以读写,组和其他用户只能读 .mode = 0644, // 处理读写请求的函数,proc_dostring用于处理字符串类型的参数 .proc_handler = proc_dostring,
} #endif // 结束宏定义的检查
一百零一章 使用 udev 挂载 U 盘和 T 卡实验
101.1 配置 buildroot 文件系统支持 udev
烧写buildroot文件系统镜像后,可通过
ps -aux | grep -nR udevps //显示当前系统中的进程状态
-a //显示所有用户的进程
u //以用户为中心的格式显示进程信息
x //显示没有控制中断的进程| //管道操作符 前一个命令的输出作为后一个命令的输入
grep //正则表达式搜索字符串 ,在管道操作符后用于过滤前一个命令输出的内容
-n // 显示匹配行的行号
r // 递归搜索
udev //搜索包含 'udev'字符串的内容
查看 udev是否已支持。
检查到/sbin/udevd 进程就表示当前系统使用的是 udev。
101.2 使用 udev 挂载 U 盘
在上一小节中配置 buildroot 使能了 udev,而要想使用 udev 来实现 U 盘的自动挂载,还需在开发板的/etc/udev/rules.d 目录下创建相应的规则文件,
(/etc/udev/rules.d 目录不存在可以手动创建,一般都已经存在了),
这里我们创建一个名为 001.rules 的文件,
然后向该文件中添加以下内容:
/*这段配置是用于 Linux 系统中的 udev 规则,
它定义了当特定类型的设备被添加到系统或从系统中移除时应该执行哪些脚本。
udev 是 Linux 内核的设备管理器,
负责在设备连接或断开时动态地管理设备节点*//*指定了设备内核名称的匹配模式*/
KERNEL=="sd[a-z][0-9]", //sd开头,后面跟一个a~z,再跟一个0~9SUBSYSTEM=="block", //设备必须属于block子系统.//block 子系统包含所有块设备,这些设备以块为单位读写数据,//如硬盘、SSD、USB 存储设备等。/*当设备被添加到系统时触发规则。*/
ACTION=="add", //add 动作通常发生在设备首次连接到系统时/*这部分指定了当上述条件都满足时应该执行的脚本*/
RUN+="/etc/udev/rules.d/usb/usb-add.sh %k" //RUN+= 表示将指定的命令添加到已有的 RUN 列表中//%k 是一个占位符,它会被替换为设备的内核名称(如 sda),//这个名称随后被传递给 usb-add.sh 脚本。/*下面是另一个规则*/
SUBSYSTEM=="block", //设备类型
ACTION=="remove", //发生时机
RUN+="/etc/udev/rules.d/usb/usb-remove.sh //要执行的脚本
确保你的脚本 /etc/udev/rules.d/usb/usb-add.sh 和 /etc/udev/rules.d/usb/usb-remove.sh 是可执行的,并且具有适当的权限来执行它们需要完成的任务(如挂载和卸载文件系统)。
可以注意到当块设备被添加的时候会执行/etc/udev/rules.d/usb/usb-add.sh 脚本,块设备被删除的时候会执行/etc/udev/rules.d/usb/usb-remove.sh 脚本。
所以接下来我们要完善这两个脚本内容,首先在 /etc/udev/rules.d/目录下创建名为 usb 的文件夹,并在这个创建 usb-add.sh 和 usb-remove.sh 脚本。
//usb-add.sh//这是脚本的 shebang(或 hashbang、pound bang)行,它告诉系统这个脚本应该使用哪个解释器来执行。
#!/bin/sh //这行是脚本的主要命令,它调用 mount 程序来挂载一个文件系统。
/bin/mount -t vfat /dev/$1 /mnt/*
/bin/mount 这是mount程序的路径
-t vfat 指定了要挂载的文件系统类型
/dev/$1 是要挂载的设备的路径,其中 $1 是脚本的第一个参数。当脚本被调用时,你应该传递一个设备名称(如 sda1),它会被替换到 /dev/ 后面形成完整的设备路径(如 /dev/sda1)。
/mnt 是挂载点的路径,即设备内容将被访问的目录。
*/
//usb-remove.sh#!/bin/sh
sync //sync 命令用于将所有未写入磁盘的缓存数据刷新到磁盘上
/bin/umount -l /mnt
添加完成之后还需要使用 chmod 命令赋予两个脚本的可执行权限。
至此关于 udev 自动挂载 U 盘的相关配置文件完成了。
首先输入以下 df 命令查看当前的挂载情况,
可以看到当前并没有关于 U 盘相关的挂载信息,然后插入 U 盘,相关打印如下,
然后重新使用 df 命令查看当前的挂载情况,可以看到 U 盘 sda1 就成功挂载到了/mnt 目录,然后拔掉 U 盘,重新使用 df 命令查看当前挂载情况,会发现/dev/sda1 设备已经消失了。
使用 udev 实现 TF 卡的自动挂载类似,将 TF 卡挂载到/mnt 目录下,在不做任何修改的情况下,直接插入 TF 卡,会发现 TF 卡直接挂载到了/mnt/sdcard 目录,这是因为在/lib/udev/rules.d 目录下已经帮我们添加了很多的 udev 规则,这里的规则文件跟我们前面自己创建的规则文件所实现的作用是相同的,只是 /etc/udev/rules.d/目录的规则文件比/lib/udev/rules.d 目录的规则文件优先级高。
使用要 使用 mdev设备管理器挂载u盘或者 tf卡等其他设备,也需要buildroot支持mdev,其他地方则是规则文件的路径不同。