Linux内核编程(十二)热插拔

本文目录

一、知识点

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 表示成功,返回负值表示发生了错误。

c 复制代码
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 数据库等。

c 复制代码
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

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)被调用,这些处理程序会通过加载驱动程序,创建设备节点等来响应热插拔事件。

c 复制代码
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的事件。

c 复制代码
#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>

c 复制代码
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)。

c 复制代码
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事件信息

包含头文件:

c 复制代码
  #include <sys/types.h>
  #include <sys/socket.h>

注意 netlink中是不用调用listen 所监听的。可以直接使用recv函数进行接收。

c 复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);   // 从套接字接收数据
// 参数:
	// sockfd - 套接字文件描述符
	// buf    - 存储接收到数据的缓冲区
	// len    - 缓冲区的长度
	// flags  - 操作标志,通常设置为0.
// 返回值:接收到的字节数,如果出错则返回 -1

★示例代码

app.c

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事件信息,结果如下所示。

实践一:使用udev自动挂载U盘

1. 配置系统支持udev--配置 Buildroot文件系统

要配置 Buildroot 以支持 udev,进入Buildroot 目录下,通过 menuconfig 图形化配置界面设置 :System configuration ---dev management 为 Dynamic using devtmpfs + eudev。即使用udev去管理。配置完成后,编译镜像文件烧录到开发版。

2. 创建配置规则

(1)检查 /etc/udev/rules.d/ 目录是否存在。如果不存在,则使用 mkdir 命令创建该目录。并在该目录下创建 usb 目录。在usb目录下编写 usb-add.shusb-remove.sh 脚本文件,并修改脚本文件的权限为 777。 (脚本名称自定义)

usb-add.sh

sh 复制代码
#!/bin/sh
/bin/mount -t vfat /dev/$1 /mnt
sync

usb-remove.sh

sh 复制代码
#!/bin/sh
sync
/bin/umount -l /mnt

(2)在/etc/udev/rules.d/ 目录下创建 01-usb.rules 文件并写入一下内容。(在该目录下所有的规则文件必须以 ".rules" 为后缀名)

c 复制代码
KERNEL=="sd[a-z][0-9]", SUBSYSTEM=="block", ACTION=="add", RUN+="/etc/udev/rules.d/usb/usb-add.sh %k" 
SUBSYSTEM=="block", ACTION=="remove", RUN+="/etc/udev/rules.d/usb/usb-remove.sh" 
/*
KERNEL=="sd[a-z][0-9]" : 匹配所有以 "sd" 开头,并且后面跟着一个小写字母和一个数字的设备节点
SUBSYSTEM=="block" : 仅在 SUBSYSTEM 是 "block"(块设备)的情况下才应用规则
ACTION=="add"  :执行的动作。
RUN+= :
	设备匹配时,执行指定的脚本 "/etc/udev/rules.d/usb/usb-add.sh"
	"%k" 作为参数传递,代表设备名称(例如 sda1)
*/

当新增一个 usb 设备,执行/etc/udev/rules.d/usb-add.sh脚本文件,并传入参数 sd[a-z][0-9]。当移除一个 usb 设备,执行/etc/udev/rules.d/usb-remove.sh 脚本文件。

3. 测试

将U盘进行插入,使用df命令查看是否有U盘自动挂载。再将U盘拔出,使用命令df查看U盘是否退出。

实践二:使用udev自动挂载TF卡

  1. 问题一:我们在不先编写本节代码时,给开发板插上TF卡,会发下开发板可以自动识别 TF 卡,这是为什么呢?
      答:这是因为在开发板其他目录下也有很多规则文件,例如在/lib/udev/rules.d目录下就拥有tf的规则文件,即使我们不写,系统也会去该目录下自动调用,从而完成TF卡的自动挂载。
  2. 问题二:那么如果我们自己写规则文件的话,应该如何写呢?
      答:其实和U盘的挂载是一样的,唯一的区别就是规则文件中的命名不同,TF卡的命名规则固定为:mmcblk[0-9]p[0-9]。其他步骤完全一致!
    ●步骤如下:
      ①我们可以在/etc/udev/rules.d/ 目录下创建 tf 目录,然后在该目录中添加两个脚本文件。文件命名自定义,最好可以是可以一眼就知道是什么作用的命名方式,脚本内容和U盘的脚本内容完全一致。这里我们使用tf-add.shtf-remove.sh作为命名来演示。
       ②在/etc/udev/rules.d/ 目录下创建02-tf.rules规则文件。内容如下:
c 复制代码
KERNEL=="mmcblk[0-9]p[0-9]", SUBSYSTEM=="block", ACTION=="add", RUN+="/etc/udev/rules.d/tf/tf-add.sh %k" 
SUBSYSTEM=="block", ACTION=="remove", RUN+="/etc/udev/rules.d/tf/tf-remove.sh" 
  1. 问题三:既然其他目录文件中有sd/tf卡的规则文件,然后我们自己又写了一个tf卡的规则文件,那么系统到底是执行哪个规则文件呢?
      答:/lib/udev/rules.d目录下的规则文件的优先级要低于/etc/udev/rules.d目录下的规则文件。如果两个目录中的规则文件针对相同的设备做出不同的操作,那么 /etc/udev/rules.d/中的规则会优先应用。
      注意:在同一个目录中,文件名的数字前缀会决定规则的应用顺序。数字越小,优先级越高。例如,10-custom.rules 会在 20-default.rules 之前应用。

实践三:使用mdev自动挂载U盘

在嵌入式设备中,我们常用mdev。

1. 配置系统支持mdev

方法一:

要配置 Buildroot 以支持 mdev,进入Buildroot 目录下,通过 menuconfig 图形化配置界面设置 :System configuration ---dev management 为 Dynamic using devtmpfs + mdev。即使用mdev去管理。配置完成后,编译镜像文件烧录到开发版。

方法二:

使用命令:echo /sbin/mdev > /proc/sys/kernel/hotplug将 /sbin/mdev 设置为内核的热插拔(hotplug)管理程序。当内核检测到一个设备的插入或移除时,会调用这个程序来处理相关的设备事件。只不过每次开机都需要重新配置,或者将该命令写入开机脚本。

2. 配置mdev规则

/etc/mdev.conf 文件末尾中添加 U 盘热插拔的事件响应规则,具体配置如下:

sh 复制代码
# U盘插入事件处理
sd[a-z][0-9] 0:0 666 @/etc/mdev/usb_insert.sh
	# 0:0 666: 设置设备的所有者为 root:root (0:0),并将设备权限设置为 666 (所有用户读写权限)。
	
# U盘移除事件处理
sd[a-z] 0:0 666 $/etc/mdev/usb_remove.sh

在 mdev中,规则文件中的每一行都是由4个字段组成的,分别是:<设备节点正则表达式> <设备的主设备号:次设备号> <设备的权限> <设备插入或移除时需要执行的命令>

其中,第四个字段表示设备插入或移除时需要执行的命令,这个字段可以使用@符号或$符号开头来表示不同的含义。

当命令字段以@符号开头时,表示该命令是一个shell命令,需要在shell 中执行。当命令字段以$符号开头时,表示该命令是一个系统命令或可执行文件,可以直接在系统中执行。

3. 创建脚本文件

在/etc/mdev目录下创建usb_insert.shusb_remove.sh脚本文件。脚本内容如下,并将脚本文件的权限改为0777。

usb_insert.sh

sh 复制代码
#!/bin/sh

if [ -d /sys/block/$MDEV ]; then     #$MDEV 是 udev/mdev 自动设置的环境变量,通常表示设备名
    mount /dev/$MDEV /mnt    #当设备被检测到(通过 /sys/block/$MDEV 判断),它会自动将该设备挂载到 /mnt 目录,并同步数据到磁盘。
    sync
fi

usb_remove.sh

sh 复制代码
#!/bin/sh
sync
umount -l /mnt

4. 测试

将U盘进行插入,使用df命令查看是否有U盘自动挂载。再将U盘拔出,使用命令df查看U盘是否退出。

实践四:使用mdev自动挂载TF卡

这里的使用步骤和实践三完全一致,脚本内容也完全一致。唯一的不同就是命令规则不同,TF卡的命名规则为mmcblk[0-9]p[0-9]

唯一不同:在 /etc/mdev.conf 文件末尾中添加TF卡热插拔的事件响应规则,具体配置如下:

sh 复制代码
# U盘插入事件处理
mmcblk[0-9]p[0-9] 0:0 666 @/etc/mdev/tf_insert.sh
	# 0:0 666: 设置设备的所有者为 root:root (0:0),并将设备权限设置为 666 (所有用户读写权限)。
	
# U盘移除事件处理
mmcblk[0-9] 0:0 666 $/etc/mdev/tf_remove.sh

实践五:使用usbmount提高工作效率

usbmount 是一个用于自动挂载 USB 存储设备的小工具,在检测到 USB 设备插入时,usbmount 会自动将其挂载到一个预定义的目录中,例如 /media/usb0、/media/usb1 等。它通常在没有完整的桌面环境(如 GNOME 或 KDE)的嵌入式系统或轻量级系统上使用。

在 Debian 或 Ubuntu 系统上,你可以使用以下命令安装 usbmount:sudo apt-get install usbmount,插入 USB 设备后,系统会自动将其挂载到 /media/usbX 中(X 为数字)。

★补充问题:无法识别U盘

如果识别不到U盘,则需要看U盘格式是否为FAT32格式,若不是,则格式化为FAT32的文件格式。且配置完文件刷新文件或重新开机使其生效。

相关推荐
tan180°25 分钟前
Boost搜索引擎 网络库与前端(4)
linux·网络·c++·搜索引擎
Mr. Cao code1 小时前
Docker:颠覆传统虚拟化的轻量级革命
linux·运维·ubuntu·docker·容器
抓饼先生2 小时前
Linux control group笔记
linux·笔记·bash
挺6的还2 小时前
25.线程概念和控制(二)
linux
您的通讯录好友2 小时前
conda环境导出
linux·windows·conda
代码AC不AC3 小时前
【Linux】vim工具篇
linux·vim·工具详解
码农hbk3 小时前
Linux signal 图文详解(三)信号处理
linux·信号处理
bug攻城狮4 小时前
Skopeo 工具介绍与 CentOS 7 安装指南
linux·运维·centos
宇宙第一小趴菜4 小时前
08 修改自己的Centos的软件源
linux·运维·centos
bug攻城狮4 小时前
彻底禁用 CentOS 7.9 中 vi/vim 的滴滴声
linux·运维·服务器·centos·vim