Cover image for Linux热插拔

Linux热插拔

字数 11.3k
阅读
访客

时间轴

时间轴

2025-12-02

init

本文介绍了Linux系统中的热插拔机制,阐述了热插拔是指在设备运行状态下安全插入或拔出硬件设备而无需重启系统的技术,其目的在于提升设备的便捷性与灵活性,广泛应用于USB设备、硬盘驱动器和扩展卡等场景。文章详细说明了热插拔的实现机制,即内核通过调用用户空间程序(如mdev和udev)进行交互,其中mdev适用于嵌入式Linux系统,而udev广泛用于PC机。重点分析了内核函数kobject_uevent()的作用,该函数通过netlink机制将设备添加、移除、修改等动作封装为uevent事件并发送至用户空间,udev接收后执行创建设备节点或加载驱动等操作。此外,文章还提及了udevadm命令行工具,用于查询和管理设备、触发uevent事件。整体内容涵盖了热插拔的底层原理、内核与用户空间的协作流程及常用管理工具。

Linux 驱动笔记


热插拔

热插拔是指在设备运行的情况下,能够安全地插入或拔出硬件设备,而无需关闭或重启系统。这意味着你可以在计算机或其他电子设备上插入或拔出硬件组件(比如 USB 设备,扩展卡,硬件驱动器等),而无需关机或中断正在进行的操作。

热插拔的主要目的是提供方便性和灵活性。通过热插拔,你可以快速更换或添加硬件设备,而无需停止正在进行的任务。这在许多场景下非常有用,比如:

  1. USB 设备:你可以随时插入或拔出 USB 设备,比如鼠标,键盘,打印机,存储设备等,而无需重新启动系统。
  2. 硬盘驱动器:在某些服务器或存储系统中,你可以在运行时添加或替换硬盘驱动器,以扩展存储容量或替换故障驱动器。
  3. 扩展卡:你可以在计算机上插入或拔出显卡,网卡或声卡等扩展卡,以满足不同的需求或升级硬件性能。

为了支持热插拔功能,硬件设备和系统必须具备相应的支持。硬件方面,设备接口必须设计成可以插入和拔出而不会损坏设备或系统。系统需要提供相应的驱动程序和管理功能,以便在插入和拔出设备时进行正确的配置和识别。

热插拔的机制

热插拔是内核和用户空间之间,通过调用用户空间程序(如hotplugudevmdev)的交互。当需要通知用户内核发生了某种热插拔事件时,内核才调用这个用户空间程序来实现交互。

在 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()

kobject_uevent是 Linux 内核中的一个函数,用于生成和发送 uevent 事件。它是 udev 和其他设备管理工具与内核通信的一种方式。

12345678910111213141516
// lib/kobject_uevent.c/** * kobject_uevent - notify userspace by sending an uevent * * @kobj: struct kobject that the action is happening to * @action: action that is happening * * Returns 0 if kobject_uevent() is completed with success or the * corresponding error when it fails. */int kobject_uevent(struct kobject *kobj, enum kobject_action action){	return kobject_uevent_env(kobj, action, NULL);}EXPORT_SYMBOL_GPL(kobject_uevent);

参数说明:

  • 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 消息,并根据消息中的设备信息和事件类型来执行相应的操作,例如创建或删除设备节点,加载或卸载驱动程序等

示例

123456789101112131415161718192021222324252627282930313233343536373839404142
#include <linux/init.h>#include <linux/module.h>#include <linux/slab.h>struct kobject *mykobject01;struct kset *mykset;struct kobj_type mytype;static int __init mykobj_uevent_init(void){        int ret;        // 创建并添加一个kset        mykset = kset_create_and_add("mykset", NULL, NULL);        // 初始化并添加kobject到kset        mykobject01 = kzalloc(sizeof(*mykobject01), GFP_KERNEL);        mykobject01->kset = mykset;        ret = kobject_init_and_add(mykobject01, &mytype, NULL, "%s", "mykobject01");        // 触发一个uevent 事件        ret = kobject_uevent(mykobject01, KOBJ_CHANGE);                return 0;}static void __exit mykobj_uevent_exit(void){        kobject_put(mykobject01);        kset_unregister(mykset);}module_init(mykobj_uevent_init);module_exit(mykobj_uevent_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@outlook.com>");MODULE_DESCRIPTION("This is a test sample for kobject uevent");

udevadm 命令

udevadm是一个用于与 udev 设备管理器进行交互的命令行工具。它提供了一系列的子命令,用于查询和管理设备、触发 uevent 事件以及执行其他与 udev 相关的操作。一些常见的udevadm子命令及其功能如下:

  1. udevadm info:用于获取设备的详细信息,包括设备路径、属性、驱动程序等。
  2. udevadm monitor:用于监视和显示当前系统中的 uevent 事件。它会实时显示设备的插入、拔出以及其他相关事件。
  3. udevadm trigger:用于手动触发设备的 uevent 事件。可以使用该命令模拟设备的插入、拔出等操作,以便触发相应的事件处理。
  4. udevadm settle:用于等待 udev 处理所有已排队的 uevent 事件。它会阻塞直到 udev 完成当前所有的设备处理操作。
  5. udevadm control:用于与 udev 守护进程进行交互,控制其行为。例如,可以使用该命令重新加载 udev 规则、设置日志级别等。
  6. udevadm test:用于测试 udev 规则的匹配和执行过程。可以通过该命令测试特定设备是否能够正确触发相应的规则。

测试:

12345678910111213
root@topeet:/root# udevadm monitor &[1] 1561root@topeet:/root# monitor will print the received events for:UDEV - the event which udev sends out after rule processingKERNEL - the kernel ueventroot@topeet:/root# insmod uevent_test.koKERNEL[1812.051745] change   /mykng out-of-tree module taints kernel.                                 set/mykobject01 (mykset)KERNEL[1812.056798] add      /module/uevent_test (module)root@topeet:/root# UDEV  [1812.061846] change   /mykset/mykobject01 (mykset)UDEV  [1812.068604] add      /module/uevent_test (module)

uevent 是基于 kset 实现的

上面的驱动代码中如果去掉了创建 kset 的步骤,用户空间无法收到 uevent 事件,分析如下:

kobject_uevent_env()

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
/** * kobject_uevent_env - send an uevent with environmental data * * @kobj: struct kobject that the action is happening to * @action: action that is happening * @envp_ext: pointer to environmental data * * Returns 0 if kobject_uevent_env() is completed with success or the * corresponding error when it fails. */int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,		       char *envp_ext[]){	struct kobj_uevent_env *env;//指向 kobj_uevent_env 结构体的指针,用于存储发送的事件和环境变量	const char *action_string = kobject_actions[action];//事件的类型	const char *devpath = NULL;//存放 kobject 的路径	const char *subsystem;//存放所属子系统的名称	struct kobject *top_kobj;//指向顶层 top_kobj 的 kobject 指针	struct kset *kset;//指向 kset 的指针,表示 kobject 所属的 kset	const struct kset_uevent_ops *uevent_ops;//指向 struct kset_uevent_ops 结构体的指针	int i = 0;//计数器 i,用来编译环境变量数组	int retval = 0;//表示函数的执行结果,也就是返回值	/*	 * Mark "remove" event done regardless of result, for some subsystems	 * do not want to re-trigger "remove" event via automatic cleanup.	 */	if (action == KOBJ_REMOVE)//检查action是否为KOBJ_REMOVE, 如果是 , 则设置kobj->state_remove_uevent_sent 为 1,表示“remove”事件已发送。		kobj->state_remove_uevent_sent = 1;	pr_debug("kobject: '%s' (%p): %s\n",		 kobject_name(kobj), kobj, __func__);	/* search the kset we belong to */	top_kobj = kobj;	while (!top_kobj->kset && top_kobj->parent)// 通过循环查找 kobj 所属的 kset,直到找到具体有效 kset 的顶层 kobj,即 kset 的根节点。		top_kobj = top_kobj->parent;	if (!top_kobj->kset) {// 如果kobj没有kset,直接返回		pr_debug("kobject: '%s' (%p): %s: attempted to send uevent "			 "without kset!\n", kobject_name(kobj), kobj,			 __func__);		return -EINVAL;	}	kset = top_kobj->kset;	uevent_ops = kset->uevent_ops;// 顶层kset的uevent_ops结构体	/* skip the event, if uevent_suppress is set*/	if (kobj->uevent_suppress) {// 检 查 kobj->uevent_suppress 是 否 为 1 , 如 果 设 置kobj->uevent_suppress,则输出调试信息表示该事件被跳过		pr_debug("kobject: '%s' (%p): %s: uevent_suppress "				 "caused the event to drop!\n",				 kobject_name(kobj), kobj, __func__);		return 0;	}	/* skip the event, if the filter returns zero. */	if (uevent_ops && uevent_ops->filter)		if (!uevent_ops->filter(kset, kobj)) {			pr_debug("kobject: '%s' (%p): %s: filter function "				 "caused the event to drop!\n",				 kobject_name(kobj), kobj, __func__);			return 0;		}	/* originating subsystem */    	// 根据 uevent_ops 中的 name 字段获取原始子系统的名称。如果 uevent_ops->name 存在,则调用 uevent_ops->name(kset, kobj)函数获取子系统名称,否则使用 kset 的名称作为子系统名称。	if (uevent_ops && uevent_ops->name)		subsystem = uevent_ops->name(kset, kobj);	else		subsystem = kobject_name(&kset->kobj);	if (!subsystem) {		pr_debug("kobject: '%s' (%p): %s: unset subsystem caused the "			 "event to drop!\n", kobject_name(kobj), kobj,			 __func__);		return 0;	}	/* environment buffer */	env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);	if (!env)		return -ENOMEM;	/* complete object path */	devpath = kobject_get_path(kobj, GFP_KERNEL);	if (!devpath) {		retval = -ENOENT;		goto exit;	}	/* default keys */	retval = add_uevent_var(env, "ACTION=%s", action_string);	if (retval)		goto exit;	retval = add_uevent_var(env, "DEVPATH=%s", devpath);	if (retval)		goto exit;	retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);	if (retval)		goto exit;	/* keys passed in from the caller */	if (envp_ext) {		for (i = 0; envp_ext[i]; i++) {			retval = add_uevent_var(env, "%s", envp_ext[i]);			if (retval)				goto exit;		}	}	/* let the kset specific function add its stuff */	if (uevent_ops && uevent_ops->uevent) {		retval = uevent_ops->uevent(kset, kobj, env);		if (retval) {			pr_debug("kobject: '%s' (%p): %s: uevent() returned "				 "%d\n", kobject_name(kobj), kobj,				 __func__, retval);			goto exit;		}	}	switch (action) {	case KOBJ_ADD:		/*		 * Mark "add" event so we can make sure we deliver "remove"		 * event to userspace during automatic cleanup. If		 * the object did send an "add" event, "remove" will		 * automatically generated by the core, if not already done		 * by the caller.		 */		kobj->state_add_uevent_sent = 1;		break;	case KOBJ_UNBIND:		zap_modalias_env(env);		break;	default:		break;	}	mutex_lock(&uevent_sock_mutex);	/* we will send an event, so request a new sequence number */    	// 将一个名为 "SEQNUM" 的环境变量添加到 uevent 环境变量列表中,并将其值 	//设置为 uevent_seqnum 的值加 1。其中,add_uevent_var 是一个内部函数,用于将一个键值	//对添加到 uevent 环境变量列表中。如果添加失败,函数将返回一个非零值,同时会释放	//uevent_sock_mutex 互斥锁并跳转到 exit 标签处进行清理操作。这个函数的主要作用是为	//uevent 事件添加一个唯一的序列号,以便在处理 uevent 事件时可以识别它们的顺序。通俗	//的话讲就是每次发送一个事件,都要有它的事件号,该事件号不能重复,也会被加到环境变量里面。	retval = add_uevent_var(env, "SEQNUM=%llu", ++uevent_seqnum);	if (retval) {		mutex_unlock(&uevent_sock_mutex);		goto exit;	}	retval = kobject_uevent_net_broadcast(kobj, env, action_string,					      devpath);// 内核广播一个uevent事件	mutex_unlock(&uevent_sock_mutex);#ifdef CONFIG_UEVENT_HELPER	/* call uevent_helper, usually only enabled during early boot */	if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {		struct subprocess_info *info;		retval = add_uevent_var(env, "HOME=/");		if (retval)			goto exit;		retval = add_uevent_var(env,					"PATH=/sbin:/bin:/usr/sbin:/usr/bin");		if (retval)			goto exit;		retval = init_uevent_argv(env, subsystem);		if (retval)			goto exit;		retval = -ENOMEM;		info = call_usermodehelper_setup(env->argv[0], env->argv,						 env->envp, GFP_KERNEL,						 NULL, cleanup_uevent_env, env);		if (info) {			retval = call_usermodehelper_exec(info, UMH_NO_WAIT);			env = NULL;	/* freed by cleanup_uevent_env */		}	}#endifexit:	kfree(devpath);	kfree(env);	return retval;}EXPORT_SYMBOL_GPL(kobject_uevent_env);

因为 uevent 是通过netlink socket发送给用户空间的应用程序的,而kobject_uevent_env()要求 kobject 必须属于一个 kset——需要通过 kset 获取uevent_ops(filter/name/uevent 回调)以及子系统名称(SUBSYSTEM),否则会直接返回-EINVAL而无法发送事件。

kobject_uevent_net_broadcast是一个内核函数,用于将一个 uevent 事件发送到系统中所有的网络命名空间中。它的参数包括:

  • 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

123
extern struct kset * __must_check kset_create_and_add(const char *name,					const struct kset_uevent_ops *u,					struct kobject *parent_kobj);

struct kset_uevent_ops

struct kset_uevent_ops定义如下

123456
struct kset_uevent_ops {	int (* const filter)(struct kset *kset, struct kobject *kobj);	const char *(* const name)(struct kset *kset, struct kobject *kobj);	int (* const uevent)(struct kset *kset, struct kobject *kobj,		      struct kobj_uevent_env *env);};

1.filter事件过滤器

  • 作用:决定是否阻止向用户空间发送该kobject的 uevent。

  • 返回值

    • 0过滤掉(不发送 uevent)
    • 0允许发送
  • 典型用途

    • 某些内部设备不需要用户空间干预;
    • 避免为虚拟设备(如 debugfs 条目)触发 uevent。
  • 示例

12345
static int my_filter(struct kset *kset, struct kobject *kobj){    /* 如果设备名以 "internal_" 开头,不通知用户空间 */    return strncmp(kobject_name(kobj), "internal_", 9) != 0;}

2.name子系统名称提供者

  • 作用:返回该kobject所属的 “subsystem” 名称,作为 uevent 中的SUBSYSTEM=字段。
  • 返回值:字符串(如"usb","block","mybus")。
  • 重要性:用户空间(如 udev rules)常根据SUBSYSTEM匹配规则。
  • 示例
1234
static const char *my_name(struct kset *kset, struct kobject *kobj){    return "mybus";  // 所有此 kset 下的设备 uevent 都带 SUBSYSTEM=mybus}

对应的 uevent 消息会包含

1234
ACTION=addDEVPATH=/devices/mydeviceSUBSYSTEM=mybus   ← 来自此回调...

注意:如果不提供name回调,内核默认使用kset->uevent_ops所在kset的名字(通常是父目录名)。

3.uevent自定义环境变量注入器

  • 作用:向 uevent 的环境变量缓冲区(env)中添加自定义键值对

  • 参数

    • env:类型为struct kobj_uevent_env,内部是一个字符串数组(最大 32 项,每项 2048 字节)。
  • 调用方式:使用add_uevent_var(env, "KEY=%s", value)

  • 典型用途

    • 传递设备属性(如厂商 ID、序列号);
    • 触发特定用户空间逻辑。
  • 示例

1234567
static int my_uevent(struct kset *kset, struct kobject *kobj,                    struct kobj_uevent_env *env){    add_uevent_var(env, "MYBUS_VERSION=1.0");    add_uevent_var(env, "DEVICE_TYPE=sensor");    return 0;}

对应的 uevent 将包含

12
MYBUS_VERSION=1.0DEVICE_TYPE=sensor

示例:完善kset_uevent_ops结构体

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
#include <linux/module.h>#include <linux/init.h>#include <linux/kobject.h>#include <linux/slab.h>struct kobject *mykobject01;struct kobject *mykobject02;struct kset *mykset;struct kobj_type myktype;int myfilter(struct kset *kset, struct kobject *kobj){        if (strcmp(kobj->name, "mykobject01") == 0) {                return 0; // 返回0表示过滤掉,不发送uevent        }        return 1;}const char *myname(struct kset *kset, struct kobject *kobj){        return "my_kset";}int myuevent(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env){        add_uevent_var(env, "MYDEVICE=%s", "RK3568");        return 0;}static struct kset_uevent_ops my_uevent_ops = {        .filter = myfilter,        .name = myname,        .uevent = myuevent,};static int __init kset_uevent_ops_init(void){        int ret = 0;        // 创建并添加一个kset        mykset = kset_create_and_add("mykset", &my_uevent_ops, NULL);        // 创建kboject01        mykobject01 = kzalloc(sizeof(*mykobject01), GFP_KERNEL);        mykobject01->kset = mykset;        ret = kobject_init_and_add(mykobject01, &myktype, NULL, "mykobject01");        // 创建kboject02        mykobject02 = kzalloc(sizeof(*mykobject02), GFP_KERNEL);        mykobject02->kset = mykset;        ret = kobject_init_and_add(mykobject02, &myktype, NULL, "mykobject02");        // 触发uevent事件        ret = kobject_uevent(mykobject01, KOBJ_ADD);        ret = kobject_uevent(mykobject02, KOBJ_ADD);        return ret;}static void __exit kset_uevent_ops_exit(void){        kobject_put(mykobject01);        kobject_put(mykobject02);        kset_unregister(mykset);}module_init(kset_uevent_ops_init);module_exit(kset_uevent_ops_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("even629<asqwgo@outlook.com>");MODULE_DESCRIPTION("This is a test sample for kset_uevent_ops");

测试:

1234567891011
root@topeet:/root# udevadm monitor &[1] 1341root@topeet:/root# monitor will print the received events for:UDEV - the event which udev sends out after rule processingKERNEL - the kernel ueventroot@topeet:/root# insmod kset_uevent_ops_test.ko[  170.300220] kset_uevent_ops_test: loading out-of-tree module taintsKERNEL[167.589323] add       /mykset/mykobject02 (my_ksekt)eKERNEL[167.594449] add      /module/kset_uevent_ops_test (module)root@topneet:/root# UDEV  [167.600297] add      /myeksetl/mykobject02 (my_kset)e)EV  [167.60.7308] add      /module/kset_uevent_ops_test (modul

Netlink 是 Linux 内核中用于内核和用户空间之间进行双工通信的机制。它基于 socket 通信机制,并提供了一种可靠的、异步的、多播的、有序的通信方式。

Netlink 机制的主要特点包括:

  1. 双工通信:Netlink 允许内核和用户空间之间进行双向通信,使得内核可以向用户空间发送消息,同时也可以接收来自用户空间的消息。
  2. 可靠性:Netlink 提供了可靠的消息传递机制,保证消息的完整性和可靠性。它使用了确认和重传机制,以确保消息的可靠传输。
  3. 异步通信:Netlink 支持异步通信,即内核和用户空间可以独立地发送和接收消息,无需同步等待对方的响应。
  4. 多播支持:Netlink 允许向多个进程或套接字广播消息,以实现一对多的通信。
  5. 有序传输:Netlink 保证消息的有序传输,即发送的消息按照发送的顺序在接收端按序接收。

Netlink 的应用广泛,常见的应用包括:

  1. 系统管理工具:如 ifconfig、ip 等工具使用 Netlink 与内核通信来获取和配置网络接口的信息。
  2. 进程间通信:进程可以使用 Netlink 进行跨进程通信,实现进程间的数据交换和协调。
  3. 内核模块和用户空间应用程序的通信:内核模块可以通过 Netlink 向用户空间应用程序发送通知或接收用户空间应用程序的指令。
创建socket

在 Linux socket 编程中,创建套接字是构建网络应用程序的第一步。套接字可以理解为应用程序和网络之间的桥梁,用于在网络上进行数据的收发和处理。该系统调用的原型和所需头文件如下所示:

头文件函数原型
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);

domain 参数指定了套接字的协议族

协议族指定了套接字所使用的协议类型,常用的协议族包括AF_INETAF_INET6AF_UNIX等。其中:

  • AF_INET表示 IPv4 协议族
  • AF_INET6表示 IPv6 协议族
  • AF_UNIX表示 Unix 域协议族
  • AF_NETLINK表示 Netlink 域协议族。

type 参数指定了套接字的类型

套接字类型指定了套接字的数据传输方式,常用的套接字类型包括SOCK_STREAMSOCK_DGRAMSOCK_RAW等。其中:

  • SOCK_STREAM表示面向连接的流套接字,主要用于可靠传输数据,例如 TCP 协议。
  • SOCK_DGRAM表示无连接的数据报套接字,主要用于不可靠传输数据,例如 UDP 协议。
  • SOCK_RAW表示原始套接字,可以直接访问底层网络协议。

protocol 参数指定了套接字所使用的具体协议。下面分别介绍这三个参数的含义:

协议类型指定了套接字所使用的具体协议类型,常用的协议类型包括IPPROTO_TCPIPPROTO_UDPIPPROTO_ICMP等。其中:

  • IPPROTO_TCP表示 TCP 协议
  • IPPROTO_UDP表示 UDP协议
  • IPPROTO_ICMP表示 ICMP 协议
  • NETLINK_KOBJECT_UEVENT表示 NETLINK_KOBJECT 协议

将使用以下代码创建一个新的套接字

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 类型。kobject uevent 用于内核对象相关的事件通知,当内核中的 kobject 对象发生变化时,会通过此类型的Netlink 消息通知用户空间。
绑定套接字
所需头文件函数原型
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd 参数:指定了需要绑定的套接字描述符,

  • addr 参数:指定了需要绑定的地址信息,这里使用struct sockaddr_nl结构体,struct sockaddr_nl结构体的定义如下:

    123456
    struct 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:Port ID, 设置为0表示由内核分配。

    • nl_groups:多播组掩码,用于指定感兴趣的多播组。当设置为 1 时,表示用户空间进程只会接收内核事件的基本组的内核事件。这意味着,用户空间进程将只接收到属于基本组的内核事件,而不会接收其他多播组的事件。

  • addrlen 参数:addrlen 参数是一个整数,指定了 addr 所指向的结构体对应的字节长度。它用于确保正确解析传递给 addr 参数的结构体的大小

示例

1234567891011121314151617
{	...	struct sockaddr_nl nl; // 定义一个指向 struct sockaddr_nl 结构体的指针 nl	bzero(&nl, sizeof(struct sockaddr_nl));// 将 nl 指向的内存区域清零,确保结构体的字段初始化为 0	nl.nl_family = AF_NETLINK;// 设置 nl 结构体的 nl_family 字段为 AF_NETLINK,指定地址族为Netlink	nl.nl_pid = 0;// 设置 nl 结构体的 nl_pid 字段为 0,表示目标进程 ID 为 0,即广播给所有进程	nl.nl_groups = 1;// 设置 nl 结构体的 nl_groups 字段为 1,表示只接收基本组的内核事件		// 使用 bind 函数将 socket_fd 套接字与 nl 地址结构体绑定在一起	ret = bind(socket_fd, (struct sockaddr *)&nl, sizeof(struct sockaddr_nl));	if (ret < 0) {		printf("bind error\n");		return -1;	}	...}
接收数据

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 套接字中接收数据,并将其存储在提供的缓冲区中。函数的返回值表示实际读取到的字节数,可以根据返回值来判断是否成功接收到数据。

示例代码:

12345678910
while (1) {	bzero(buf, 4096); // 将缓冲区 buf 清零,确保数据接收前的初始化	len = recv(socket_fd, buf, 4096, 0);// 从 socket_fd 套接字接收数据,存储到缓冲区 buf 中,最大接收字节数为 4096	for (i = 0; i < len; i++) {		if (*(buf + i) == '\0') {// 如果接收到的数据中有 '\0' 字符,将其替换为 '\n',以便在打印时换行显示			buf[i] = '\n';		}	}	printf("%s\n", buf);// 打印接收到的数据}
示例代码
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/socket.h>#include <sys/types.h>#include <linux/netlink.h>#include <unistd.h>#define UEVENT_BUF_SIZE 8192int main(int argc, char *argv[]){        int ret;        int len = 0, i = 0;        int socket_fd;        char buf[UEVENT_BUF_SIZE] = { 0 };                struct sockaddr_nl nl = {                .nl_family = AF_NETLINK,                .nl_pid = 0, // 由内核分配 Port ID(用户空间通常设为 0)                .nl_groups = 1, // 只接收属于属于基本组的内核事件        };        socket_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);        if (socket_fd < 0) {                perror("socket");                exit(EXIT_FAILURE);        }        ret = bind(socket_fd, (const struct sockaddr *)&nl, sizeof(struct sockaddr_nl));        if (ret < 0) {                perror("bind");                close(socket_fd);                exit(EXIT_FAILURE);        }        printf("Listening for uevents...\n");        while (1) {                memset(buf, 0, sizeof(buf));                len = recv(socket_fd, buf, sizeof(buf), 0);                if (len < 0) {                        perror("recv");                        break;                }                for (i = 0; i < len && i < sizeof(buf) - 1; i++) {                        if (*(buf + i) == '\0') {                                buf[i] = '\n';                        }                }                buf[len] = '\0'; // 确保字符终止                printf("\n--- Uevent Received ---\n%s", buf);        }        close(socket_fd);        return 0;}

测试:

123456789101112131415161718
root@topeet:/root# ./netlink_test.o &[1] 2275root@topeet:/root# Listening for uevents...root@topeet:/root# insmod kset_uevent_ops_test.ko--- Uevent Received ---add@/mykset/mykobject02ACTION=addDEVPATH=/mykset/mykobject02SUBSYSTEM=my_ksetMYDEVICE=RK3568SEQNUM=3288--- Uevent Received ---root@topeet:/root# add@/module/kset_uevent_ops_testACTION=addDEVPATH=/module/kset_uevent_ops_testSUBSYSTEM=moduleSEQNUM=3289

uevent_helper

kobject_uevent_env()函数中有:

1234567891011121314151617181920212223242526
#ifdef CONFIG_UEVENT_HELPER	/* call uevent_helper, usually only enabled during early boot */	if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {		struct subprocess_info *info;		retval = add_uevent_var(env, "HOME=/");		if (retval)			goto exit;		retval = add_uevent_var(env,					"PATH=/sbin:/bin:/usr/sbin:/usr/bin");		if (retval)			goto exit;		retval = init_uevent_argv(env, subsystem);		if (retval)			goto exit;		retval = -ENOMEM;		info = call_usermodehelper_setup(env->argv[0], env->argv,						 env->envp, GFP_KERNEL,						 NULL, cleanup_uevent_env, env);		if (info) {			retval = call_usermodehelper_exec(info, UMH_NO_WAIT);			env = NULL;	/* freed by cleanup_uevent_env */		}	}#endif

第 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
#define CONFIG_UEVENT_HELPER_PATH ""

该宏为空 ,所以为了使能uevent_helper功能需要在图形配置界面使能CONFIG_UEVENT_HELPER宏来使能uevent_helperCONFIG_UEVENT_HELPER_PATH宏来设置默认uevent_helper路径(如/sbin/mdev);

uevent_helper 配置方法 1

1234567891011
Device Drivers	Generic Driver Options		[*] Support for uevent helper // 选中		(/sbin/mdev) path to uevent helper // 设置 mdev 路径		File systems	Pseudo filesystems.		[*]/proc file system support // 选中		[*] Sysctl support(/proc/sys) // 选中[*]Networking support // 选中

保存配置:

123
cp .config arch/arm64/configs/rockchip_linux_defconfig../build.sh kernelcp boot.img ~/tftp

在上面的配置 1 中设置了uevent helper和相对应的路径,这就是配置方法 1,但是这种方式需要重新编译内核,使用起来较为麻烦,除了方法1之外还有更快捷的方法 2 和方法 3:

uevent_helper 配置方法 2

无论是否配置了CONFIG_UEVENT_HELPER_PATH,在系统启动后,可以使用以下命令来设置uevent_helper

1
echo /sbin/mdev > /sys/kernel/uevent_helper

这将把uevent_helper设置为/sbin/mdev

uevent_helper 配置方法 3

无论是否配置了CONFIG_UEVENT_HELPER_PATH,在系统启动后,可以使用以下命令来设置uevent_helper

1
echo /sbin/mdev > /proc/sys/kernel/hotplug

这将把uevent_helper设置为/sbin/mdev

需要注意的是配置方法 2 和配置方法 3 依赖于上面的配置的Support for uevent helper,File systemsNetworking选项,并且可以通过配置方法 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属性的定义和相关操作的实现,具体内容如下所示:

12345678910111213141516171819202122
// kernel/ksysfs.c#ifdef CONFIG_UEVENT_HELPER/* uevent helper program, used during early boot */static ssize_t uevent_helper_show(struct kobject *kobj,				  struct kobj_attribute *attr, char *buf){	return sprintf(buf, "%s\n", uevent_helper);}static ssize_t uevent_helper_store(struct kobject *kobj,				   struct kobj_attribute *attr,				   const char *buf, size_t count){	if (count+1 > UEVENT_HELPER_PATH_LEN)		return -ENOENT;	memcpy(uevent_helper, buf, count);	uevent_helper[count] = '\0';	if (count && uevent_helper[count-1] == '\n')		uevent_helper[count-1] = '\0';	return count;}KERNEL_ATTR_RW(uevent_helper);#endif
  • uevent_helper_show函数用于将uevent_helper的值写入 buf 中,并返回写入的字符数。

  • uevent_helper_store函数用于将 buf 中的值复制到uevent_helper中,并根据需要进行处理,然后返回写入的字符数。

/proc/sys/kernel/hotplug是一个虚拟文件,用于配置内核中的热插拔事件处理程序。通过对该文件进行写操作,可以设置uevent_helper属性的值。在内核源码的kernel/sysctl.c文件中,可以看到对hotplug操作其实是对uevent_helper进行操作。

12345678910
// kernel/sysctl.c#ifdef CONFIG_UEVENT_HELPER	{		.procname	= "hotplug",		.data		= &uevent_helper,		.maxlen		= UEVENT_HELPER_PATH_LEN,		.mode		= 0644,		.proc_handler	= proc_dostring,	},#endif

这段代码定义了一个名为hotplug的文件,用于处理 uevent 事件。它与uevent_helper属性相关联。

  • .procname表示文件名,即/proc/sys/kernel/hotplug
  • .data是一个指向uevent_helper结构体的指针,用于保存与该文件相关的数据。该指针指向uevent_helper结构体,用于处理 uevent 事件。
  • .maxlen表示文件的最大长度,即文件内容的最大长度。该值为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 中的参数将会被转换成环境变量,并被传递给用户空间进程。

12345678910111213
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>int main(int argc, char *argv[]){	int fd = open("/dev/ttyFIQ0", O_WRONLY);	dup2(fd, STDOUT_FILENO);	printf("SUBSYSTEM is %s\n", getenv("SUBSYSTEM"));	return 0;}

方法2:

12345678
root@topeet:/root# echo /root/uevent_helper_test.o > /sys/kernel/uevent_helperroot@topeet:/root# insmod kset_uevent_ops_test.ko[   63.445449] kset_uevent_ops_test: loading out-of-tree module taints kernel.root@topeet:/root# SUBSYSTEM is my_ksetSUBSYSTEM is moduleroot@topeet:/root# rmmod kset_uevent_ops_test.koSUBSYSTEM is my_ksetSUBSYSTEM is module

方法3:

1234567
root@topeet:/root# echo /root/uevent_helper_test.o > /proc/sys/kernel/hotplugroot@topeet:/root# insmod kset_uevent_ops_test.koroot@topeet:/root# SUBSYSTEM is moduleSUBSYSTEM is my_ksetroot@topeet:/root# rmmod kset_uevent_ops_test.koSUBSYSTEM is moduleSUBSYSTEM is my_kset

可以看到上面两种配置都可以打印出内核加载时传递的SUBSYSTEM环境变量。

使用 udev 挂载 U 盘和 TF 卡

配置 buildroot 支持 udev

123456
System configuration	/dev management (Dynamic using devtmpfs + eudev) -->		( ) Static using device table                ( ) Dynamic using devtmpfs only                ( ) Dynamic using devtmpfs + mdev                (X) Dynamic using devtmpfs + eudev    

挂载 U 盘

想使用 udev 来实现 U 盘的自动挂载,还需在/etc/udev/rules.d目录下创建相应的规则文件

12
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]
    • KERNEL:表示匹配设备的内核名。
    • sd[a-z][0-9]:是一个正则表达式模式,
      • sd:表示设备名以 “sd” 开头
      • [a-z]:表示设备名的第三个字符是小写字母
      • [0-9]:表示设备名的第四个字符是数字。

这个模式用于匹配 USB 存储设备的块设备节点,如/dev/sda1/dev/sdb2等。

  • SUBSYSTEM=="block"
    • SUBSYSTEM:表示匹配设备的子系统名称。
    • block:表示设备的子系统是块设备子系统,即与磁盘、分区等相关的设备。这部分规则是为了确保只匹配块设备子系统下的设备。
  • ACTION=="add"ACTION=="remove"
    • ACTION:表示匹配设备的动作。
      • add:表示设备被添加。
      • remove:表示设备被移除。

这部分规则是为了处理设备被添加和被删除的事件。

  • RUN+="/etc/udev/rules.d/usb/usb-add.sh %k
    • RUN+="...":表示在匹配的设备上执行指定的命令。
    • "/etc/udev/rules.d/usb/usb-add.sh":是要执行的命令的路径,即在设备添加时执行/etc/udev/rules.d/usb/usb-add.sh脚本文件。
    • %k:是 udev 提供的一个变量,表示匹配的设备的内核名

当块设备被添加的时候会执行/etc/udev/rules.d/usb/usb-add.sh脚本,块设备被删除的时候会执行/etc/udev/rules.d/usb/usb-remove.sh脚本

  • /etc/udev/rules.d/usb/usb-add.sh内容如下

    12
    #!/bin/sh/bin/mount -t vfat /dev/$1 /mnt
    • /bin/mount:调用 mount 命令(显式指定完整路径)。
    • -t vfat:指定要挂载的文件系统类型为 VFAT(即 FAT32 或 FAT16,常用于 U 盘、SD 卡等)。
    • /dev/ $ 1:表示设备文件,其中$ 1是脚本或命令行传入的第一个参数。
  • /etc/udev/rules.d/usb/usb-remove.sh内容如下

    123
    #!/bin/shsync/bin/umount -l /mnt
    • -l:表示 “lazy unmount”(延迟卸载)。它会立即从文件系统命名空间中解除挂载点,但实际的清理(比如释放设备)会等到设备不再繁忙(没有进程在使用)时才进行。常用于设备“忙”无法正常卸载的情况(比如有 shell 当前在/mnt目录里)。

然后要赋予这两个脚本的执行权限

1
chmod a+x /etc/udev/rules.d/usb/usb-add.sh /etc/udev/rules.d/usb/usb-remove.sh

挂载 TF 卡

/etc/udev/rules.d/目录下,创建一个名为002.rules的文件

12
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"
  • KERNEL=="mmcblk[0-9]p[0-9]"
    • KERNEL:表示匹配设备的内核名。
    • "mmcblk[a-z][0-9]":是一个正则表达式模式
      • mmcblk:表示设备名以"mmcblk"开头
      • [0-9]:表示设备名的第 7 个字符和第 9 个字符是数字。

这个模式用于匹配 TF 卡存储设备的块设备节点,如/dev/mmcblk1p1等。

  • SUBSYSTEM=="block"
    • SUBSYSTEM:表示匹配设备的子系统名称。
    • "block":表示设备的子系统是块设备子系统,即与磁盘、分区等相关的设备。

这部分规则是为了确保只匹配块设备子系统下的设备。

  • ACTION=="add"ACTION=="remove"
    • ACTION:表示匹配设备的动作。
      • "add":表示设备被添加。
      • "remove":表示设备被移除。

这部分规则是为了处理设备被添加和被删除的事件。

  • RUN+="/etc/udev/rules.d/tf/tf-add.sh %k"
    • RUN+="...":表示在匹配的设备上执行指定的命令。
    • "/etc/udev/rules.d/tf/tf-add.sh":是要执行的命令的路径,即在设备添加时执行/etc/udev/rules.d/tf/tf-add.sh脚本文件。
    • %k:是 udev 提供的一个变量,表示匹配的设备的内核名

当 TF 卡块设备被添加的时候会执行/etc/udev/rules.d/tf/tf-add.sh脚本,TF 卡块设备被删除的时候会执行/etc/udev/rules.d/tf/tf-remove.sh脚本

/etc/udev/rules.d/tf/tf-add.sh文件中写入以下内容:

12
#!/bin/sh/bin/mount -t vfat /dev/$1 /mnt

/etc/udev/rules.d/tf/tf-remove.sh文件中写入以下内容

123
#!/bin/shsync/bin/umount -l /mnt

然后要赋予这两个脚本的执行权限

1
chmod a+x /etc/udev/rules.d/tf/tf-add.sh /etc/udev/rules.d/tf/tf-remove.sh

使用 mdev 挂载 U 盘和 TF 卡

配置 buildroot 支持 mdev

12
make rockchip_rk3568_defconfigmake menuconfig

配置如下:

123456
System configuration	/dev management (Dynamic using devtmpfs + eudev) -->		( ) Static using device table                ( ) Dynamic using devtmpfs only                (X) Dynamic using devtmpfs + mdev                ( ) Dynamic using devtmpfs + eudev    

除了 buildroot 需要配置之外,还需要配置 busybox 的相关选项,因为buildroot使用了busybox

1
make busybox-menuconfig

配置如下:

12345
Linux System Utilities	[*] mdev (17 kb)	[*]   Support /etc/mdev.conf	[*]     Support subdirs/symlinks	[ ]       Support regular expressions substitutions when renaming device

挂载 U 盘

跟 udev 相同,mdev 也需要添加相应的规则,不同的是 mdev 使用/etc/mdev.conf文件来配置 mdev 工具的规则和行为,要想使用 mdev 自动挂载 U 盘需要向/etc/mdev.conf文件中添加以下两条规则

12
sd[a-z][0-9] 0:0 666 @/etc/mdev/usb_insert.shsd[a-z] 0:0 666 $/etc/mdev/usb_remove.sh

这两个规则用于处理 U 盘的热插拔事件,并执行相应的操作。在/etc/mdev.conf文件中,每一行都是一个规则,具有以下格式:

1
<设备节点正则表达式> <设备的所有者:设备的所属组> <设备的权限> <设备插入或移除时需要执行的命令>

下面是对上述两个规则的详细介绍:

  • sd[a-z][0-9]是一个正则表达式模式,用于匹配以"sd"开头,后跟一个小写字母和一个数字的设备节点,例如/dev/sda1等。
  • 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脚本。$符号表示执行的是一个内部命令。

/etc/mdev/usb_insert.sh/etc/mdev/usb_remove.sh两个文件如下图所示:

  • /etc/mdev/usb_insert.sh
123456
#!/bin/shif [ -d /sys/block/*/$MDEV ]; then	mount /dev/$MDEV /mnt	syncfi
  • /etc/mdev/usb_remove.sh
123
#!/bin/shsync/bin/umount -l /mnt

然后要赋予这两个脚本的执行权限

1
chmod a+x /etc/mdev/usb_insert.sh /etc/mdev/usb_remove.sh

挂载 TF 卡

/etc/mdev.conf文件中添加以下两条规则

12
mmcblk[0-9]p[0-9] 0:0 666 @/etc/mdev/tf_insert.shmmcblk[0-9] 0:0 666 $/etc/mdev/tf_remove.sh
  • mmcblk[0-9]p[0-9]是一个正则表达式模式,用于匹配以"mmcblk"开头的 TF 卡块设备,例如/dev/mmcblk1p1等。
  • 0:0 666表示设置设备节点的所有者和权限。0:0表示所有者和所属组的用户 ID 和组 ID 均为 0,即 root 用户。666表示权限为可读可写。
  • @/etc/mdev/tf_insert.sh表示当符合规则的设备插入时,mdev 会执行/etc/mdev/tf_insert.sh脚本。@符号表示执行的是一个 shell 命令。
  • $/etc/mdev/tf_remove.sh表示当符合规则的设备移除时,mdev 会执行/etc/mdev/tf_remove.sh脚本。$符号表示执行的是一个内部命令。

/etc/mdev/tf_insert.sh/etc/mdev/tf_remove.sh如下所示

  • /etc/mdev/tf_insert.sh
12345
#!/bin/shif [ -d /sys/block/*/$MDEV ]; then	mount /dev/$MDEV /mnt	syncfi
  • /etc/mdev/tf_remove.sh
123
#!/bin/shsync/bin/umount -l /mnt

然后要赋予这两个脚本的执行权限

1
chmod a+x /etc/mdev/tf_insert.sh /etc/mdev/tf_remove.sh

固定USB设备的设备节点

需求

在 Linux 系统中,当使用多个 USB 转串口设备时,经常会遇到插拔 USB 转串口的顺序变化导致设备节点不稳定的情况。为了解决这个问题,我们可以通过使用 udev 来固定设备节点,以确保设备节点不受 USB 转串口插拔顺序的影响

解决方法

插入USB设备:

1
udevadm info -a -n /dev/ttyUSB0

udevadm info -a -n /dev/ttyUSB0是一个用于获取有关特定设备节点的详细信息的命令,包括设备的属性、驱动程序、设备路径等。这些命令的参数含义如下所示:

  • -a:显示与指定设备节点相关联的所有属性。
  • -n:指定设备节点的路径或名称。

udevadm info -a -n /dev/ttyUSB0
udevadm info -a -n /dev/ttyUSB0

这个命令的输出可能会包含以下信息:

  1. 设备路径(DEVPATH):设备在系统中的路径,例如/devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0/ttyUSB0/tty/ttyUSB0
  2. 设备节点(DEVNAME):设备在文件系统中的节点,即/dev/ttyUSB0
  3. 设备的属性(Device Attributes):包括设备的供应商 ID、产品 ID、序列号等信息。
  4. 设备的驱动程序(Device Driver):设备所使用的驱动程序的名称和路径。
  5. 设备类型(SUBSYSTEM):usb

接下来在开发板的etc/udev/rules.d/目录下,创建一个名为 001.rules 的规则文件:

1
KERNELS==”5-1:1.0”,SUBSYSTEMS==”usb”,MODE:=”0777”,SYMLINK+=”myusb”
  1. KERNELS=="5-1:1.0"

    • 匹配设备的 内核设备名(Kernel Device Name)

    • 5-1:1.0是 USB 设备在内核中的路径标识:

      • 5: USB 总线号(Bus)
      • 1: USB 设备号(Device)
      • 1.0: 接口编号(Interface),表示第 1 个接口的第 0 个端点
      • 这个值来自udevadm info输出中的KERNELS字段
  2. SUBSYSTEMS=="usb"

    • 匹配设备所属的 子系统(Subsystem)

    • 表示这是一个 USB 类型的设备

    • 可以用来过滤所有 USB 设备

  3. MODE:="0777"

    • 设置该设备文件的 权限(mode)
  • 0777表示:
    • 所有用户(root、普通用户、其他用户)都拥有读、写、执行权限
    • 即:rwxrwxrwx,这样任何程序都可以直接访问该设备(如/dev/ttyUSB0

⚠️ 注意:MODE前面是:=,不是==:=表示“赋值”,而==是“匹配”。

  1. SYMLINK+="myusb"

    • 为设备创建一个 符号链接(symbolic link)

    • 比如原本设备是/dev/ttyUSB0,现在会多一个软链接:/dev/myusb -> /dev/ttyUSB0

    • 可以用myusb来代替ttyUSB0,更方便命名和脚本调用

效果
效果

评论加载中…