本文目录
- 一、知识点
- 1. 热插拔概念
- 2. 热插拔机制
- 3. Netlink机制
- 二、内核发送uevent事件到用户空间
- 1. kobject发送uevent事件
- 2. udevadm命令查看
- ★示例代码:
- ★优化:完善kset_uevent_ops(热插拔事件结构体)
- 三、用户空间使用Netlink接收uevent事件
- 1. 创建socket
- 2. 绑定socket
- 3. 接收uevent事件信息
- ★示例代码
一、知识点
1. 热插拔概念
热插拔就是带电插拔,用人话讲就是允许用户在不关闭系统,不切断电源的情况下拆卸或安装硬盘,板卡等设备。热插拔是内核和用户空间之间,通过调用用户空间程序实现交互来实现的。当内核发生了某种热拔插事件时,内核就会调用用户空间的程序来实现交互。
2. 热插拔机制
热插拔机制有devfs、udev、mdev。其中devfs已经不再使用。嵌入式设备上一般使用mdev,X86上一般用udev,当然嵌入式设备上也可以用udev。与 udev 不同,mdev 的设计更加简洁,是udev的简化版本。
(1) udev
是基于Netlink 机制实现的。 工作原理如下:
① 当有设备插入或移除时,内核会生成一个 uevent 事件。
② 内核通过 Netlink 套接字将 uevent 事件发送给用户空间。
③用户空间的 udev 守护进程会打开一个 Netlink 套接字并持续监听,通过监听内核发送的 uevent 来执行相应的热插拔操作,如创建设备节点、设置权限、运行脚本等。
(2)mdev
主要工作机制是基于 uevent_helper。工作原理如下:
①当设备插入、移除或状态改变时,内核会生成一个 uevent 事件。
②内核通过 uevent_helper 机制调用用户空间的程序来处理这些事件。uevent_helper 的路径存储在 /proc/sys/kernel/hotplug
文件中,通常指向 /sbin/mdev
。
③mdev 作为一个可执行程序被内核调用,通过读取环境变量中的事件信息来处理设备事件。它根据配置文件(通常是 /etc/mdev.conf
)中定义的规则,执行相应的操作。
3. Netlink机制
Linux提供了多种方式实现内核和用户空间的数据交换,比如我们之前讲过的系统调用,sysfs,等,但是这种通信机制均为单工通信机制。而 netlink,是基于 socket 通信机制,具体双工的特点。可以很好的满足内核和用户空间的数据交换。
二、内核发送uevent事件到用户空间
前提:需要创建kest来完成。
1. kobject发送uevent事件
返回 0 表示成功,返回负值表示发生了错误。
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
/*
struct kobject *kobj:指向 kobject 结构体的指针,表示发生事件的内核对象。
enum kobject_action action:表示事件的类型。
KOBJ_ADD, // 表示有新的设备或内核对象被添加到系统中。
KOBJ_REMOVE, // 表示设备或内核对象被从系统中移除。
KOBJ_CHANGE, // 表示设备或内核对象的状态发生变化。
KOBJ_MOVE, // 表示设备或内核对象在系统中被移动到另一个位置。
KOBJ_ONLINE, // 表示设备或内核对象上线,准备使用。
KOBJ_OFFLINE, // 表示设备或内核对象下线,不再可用。
KOBJ_MAX // 这是枚举值的最大值,通常用于检查枚举的范围。
*/
2. udevadm命令查看
udevadm 是 udev 的命令行工具,提供了用于调试和管理 udev 设备管理器的各种功能。它允许用户查询设备信息、模拟设备事件、测试规则和管理 udev 数据库等。
udevadm info //用于显示设备的详细信息。常用选项包括 --query=all(显示所有信息)和 --name=DEVICE(指定设备节点,例如 /dev/sda)。
udevadm monitor //用于实时监视 udev 事件。可以使用 --udev(仅显示 udev 事件)和 --kernel(显示内核事件)选项。
udevadm test //用于测试 udev 规则对设备的作用。常用选项包括 --action=ACTION(指定动作类型,如 add 或 remove),需要指定设备路径,例如 /sys/class/net/eth0。
udevadm trigger //用于手动触发 udev 事件。可以使用 --action=ACTION(指定触发的动作类型,例如 add 或 remove)和 --subsystem-match=SUBSYSTEM(仅触发匹配指定子系统的设备)选项。
udevadm control //用于控制 udev 守护进程的行为。常用选项包括 --reload(重新加载 udev 配置文件和规则)、--stop(停止 udev 守护进程)和 --start(启动 udev 守护进程)。
udevadm settle //用于等待所有当前的 udev 事件处理完毕。可以设置超时时间(秒),使用 --timeout=TIME 选项,默认超时时间是 30 秒。
udevadm --version //显示 udevadm 的版本信息.
★示例代码:
uevent.c
#include <linux/module.h>
#include <linux/kobject.h>
#include <linux/slab.h> // For kzalloc
struct kobject *mykobject01;
struct kset *my_kset;
struct kobj_type mytype;
static int __init mykobj_init(void)
{
int ret;
//1. 创建kest
my_kset=kset_create_and_add("my_kset",NULL,NULL); //在sys下创建my_kset目录.
//2. 创建kobject1
mykobject01= kzalloc(sizeof(struct kobject), GFP_KERNEL);
mykobject01->kset=my_kset;
ret = kobject_init_and_add(mykobject01, &mytype, NULL, "mykobject01");
// 发送uevent事件
ret=kobject_uevent(mykobject01,KOBJ_CHANGE);
return 0;
}
static void __exit mykobj_exit(void)
{
kobject_put(mykobject01);
kset_unregister(my_kset);
}
module_init(mykobj_init);
module_exit(mykobj_exit);
MODULE_LICENSE("GPL");
测试:首先使用udevadm monitor &
命令来后台实时监视kobject 的 udev 事件,然后我们将uevent.ko文件加载、卸载时,查看输出信息如下。因为我们检测的是状态改变,只要发送改变就会发送uevent 事件。
★优化:完善kset_uevent_ops(热插拔事件结构体)
热插拔事件意思就是当kset目录下有任何变动,包括目录的移动,增加目录或者属性文件等操作。
当系统配置发生变化时,如添加kset到系统或移动kobject,一个通知会从内核空间发送到用户空间,这就是热插拔事件。热插拔事件会导致用户空间中的处理程序(如udev,mdev)被调用,这些处理程序会通过加载驱动程序,创建设备节点等来响应热插拔事件。
struct kset_uevent_ops {
//过滤事件,决定是否产生事件,如果返回0,将不产生事件。
int (* const filter)(struct kset *kset, struct kobject *kobj);
//向用户空间传递一个合适的字符串
const char *(* const name)(struct kset *kset, struct kobject *kobj);
//通过环境变量传递任何热插拔脚本需要的信息,他会在(udev或mdev)调用之前,提供添加环境变量的机会。
int (* const uevent)(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env);
};
优化代码:
功能:会屏蔽mykobject01发送的uevent事件,只响应mykobject02的事件。
#include <linux/module.h>
#include <linux/kobject.h>
#include <linux/slab.h> // For kzalloc
struct kobject *mykobject01;
struct kobject *mykobject02;
struct kset *my_kset;
struct kobj_type mytype;
int my_filter(struct kset *kset, struct kobject *kobj)
{
if(strcmp(kobj->name,"mykobject01")==0){ //过滤掉mykobject01的uevent事件。
return 0;
}else{
return 1;
}
}
const char *my_name(struct kset *kset, struct kobject *kobj)
{
return "QJL_test"; //向用户空间传递一个合适的字符串
}
int my_uevent(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env)
{
add_uevent_var(env,"MYDEVICE:%s","QJL"); //添加环境变量
return 0;
}
struct kset_uevent_ops my_uevent_ops={
.filter=my_filter,
.name= my_name,
.uevent =my_uevent,
};
static int __init mykobj_init(void)
{
int ret;
//1. 创建kest
my_kset=kset_create_and_add("my_kset", &my_uevent_ops, NULL); //在sys下创建my_kset目录.
//2. 创建kobject1
mykobject01= kzalloc(sizeof(struct kobject), GFP_KERNEL);
mykobject01->kset=my_kset;
ret = kobject_init_and_add(mykobject01, &mytype, NULL, "mykobject01");
//3. 创建kobject2
mykobject02= kzalloc(sizeof(struct kobject), GFP_KERNEL);
mykobject02->kset=my_kset;
ret = kobject_init_and_add(mykobject02, &mytype, NULL, "mykobject02");
// 发送uevent事件
ret=kobject_uevent(mykobject01, KOBJ_CHANGE);
ret=kobject_uevent(mykobject02, KOBJ_ADD);
return 0;
}
static void __exit mykobj_exit(void)
{
kobject_put(mykobject01);
kobject_put(mykobject02);
kset_unregister(my_kset);
}
module_init(mykobj_init);
module_exit(mykobj_exit);
MODULE_LICENSE("GPL");
三、用户空间使用Netlink接收uevent事件
因为netlink 是基于socket通信机制,在用户空间使用socket接口,如socket、bind、sendmsg、recvmsg、close 就可以使用 netlink,上手容易。这里我就不再讲解socket的API函数,详情查看:socket使用步骤详情查看地址。
1. 创建socket
对于netlink,使用下面固定的协议类型。其中protocol指定 netlink协议类型,目前已经支持的协议类型在 linux/netlink.h
中定义,,所以需要包含头文件#include <linux/netlink.h>
。
int socket(int domain, int type, int protocol);
/*
int domain: 选择 AF_NETLINK
int type : 选择 SOCK_RAW
int protocol :在#include <linux/netlink.h>中选择。
*/
2. 绑定socket
注意:对于sockaddr_nl
结构体成员填写内容:nl_family(AF_NETLINK) 、nl_pad (0) 、nl_pid(0)、nl_groups(1)。
int bind(int sockfd, struct sockaddr* my_addr, int addrlen);
/*
sockfd :socket 描述符
addr:指向一个 struct sockaddr 类型指针。这里我们使用sockaddr_nl结构体,然后进行类型转换。
struct sockaddr_nl {
__kernel_sa_family_t nl_family; // 套接字地址族。 这里使用 AF_NETLINK。
unsigned short nl_pad; // 填充,用于对齐。 这里使用 0。
__u32 nl_pid; // 进程标识符 (PID) 也可以设置为0,表示不加入任何多播组。
__u32 nl_groups; // 多播组掩. 设置为1时,表示用户空间进程只会接收内核事件的基本组的内核事件。
};
int addrlen :结构体长度。
*/
3. 接收uevent事件信息
包含头文件:
#include <sys/types.h>
#include <sys/socket.h>
注意 netlink中是不用调用listen 所监听的。可以直接使用recv函数进行接收。
ssize_t recv(int sockfd, void *buf, size_t len, int flags); // 从套接字接收数据
// 参数:
// sockfd - 套接字文件描述符
// buf - 存储接收到数据的缓冲区
// len - 缓冲区的长度
// flags - 操作标志,通常设置为0.
// 返回值:接收到的字节数,如果出错则返回 -1
★示例代码
app.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <linux/netlink.h>
#include <sys/socket.h> // 修正了包含的头文件
#include <sys/types.h>
int main() {
int ret;
int socketed;
ssize_t len;
int i;
char buf[4096] = {0};
// 创建 Netlink 套接字
socketed = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);
if (socketed < 0) {
perror("socket error"); // 更改为 perror 可以输出更详细的错误信息
return -1;
}
// 定义并初始化 sockaddr_nl 结构体
struct sockaddr_nl my_sockaddr_nl = {
.nl_family = AF_NETLINK,
.nl_pad = 0,
.nl_pid = 0, // 绑定到内核
.nl_groups = 1 // 监听内核组播消息
};
// 绑定 Netlink 套接字
ret = bind(socketed, (struct sockaddr*)&my_sockaddr_nl, sizeof(struct sockaddr_nl));
if (ret < 0) {
perror("bind error"); // 错误处理
close(socketed);
return -1;
}
// 循环接收并打印消息
while (1) {
memset(buf, 0, sizeof(buf)); // 清空缓冲区
len = recv(socketed, buf, sizeof(buf), 0);
for(i=0;i<len; i++){
if(buf[i]=='\0') buf[i]='\n';
}
// 打印接收到的消息
printf("%s\n", buf); // 使用 %.*s 打印指定长度的字符串
}
close(socketed); // 关闭套接字
return 0;
}
实验现象:我们使用完善kset_uevent_ops
的代码作为内核代码,通过本示例来获取内核中uevent事件信息,结果如下所示。