时间轴

2025-12-02

init


热插拔

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

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

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

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

热插拔的机制

热插拔是内核和用户空间之间,通过调用用户空间程序(如 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// lib/kboject_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 消息,并根据消息中的设备信息和事件类型来执行相应的操作,例如创建或删除设备节点,加载或卸载驱动程序等

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>

struct kobject *mykobject01;
struct kset *mykset;
struct kobj_type mytype;

// 模块的初始化函数
static int mykobj_init(void)
{
int ret;

// 创建并添加一个kset
mykset = kset_create_and_add("mykset", NULL, NULL);

// 分配并初始化一个kobject
mykobject01 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
mykobject01->kset = mykset;

// 初始化并添加kobject到kset
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)
{
// 释放kobject
kobject_put(mykobject01);
kset_unregister(mykset);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL"); // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

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 规则的匹配和执行过程。可以通过该命令测试特定设备是否能够正确触发相应的规则。

kobject_uevent()

去掉了创建 kset 的步骤,则用户空间无法收到事件,分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/**
* 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 */
}
}
#endif

exit:
kfree(devpath);
kfree(env);
return retval;
}
EXPORT_SYMBOL_GPL(kobject_uevent_env);

因为 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/configfs.h>
#include <linux/kernel.h>
#include <linux/kobject.h>

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表示过滤掉
}
};

struct kset_uevent_ops my_uevent_ops = {
.filter = myfilter,
.uevent = myevent,
.name = myname,
};

// 模块的初始化函数
static int mykobj_init(void)
{
int ret;

// 创建并添加一个kset
mykset = kset_create_and_add("mykset", &my_uevent_ops, NULL);

// 分配并初始化一个kobject
mykobject01 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
mykobject01->kset = mykset;

// 初始化并添加kobject到kset
ret = kobject_init_and_add(mykobject01, &mytype, NULL, "%s", "mykobject01");

// 分配并初始化一个kobject
mykobject02 = kzalloc(sizeof(struct kobject), GFP_KERNEL);
mykobject02->kset = mykset;

// 初始化并添加kobject到kset
ret = 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)
{
// 释放kobject
kobject_put(mykobject01);
kobject_put(mykobject02);
kset_unregister(mykset);
}

module_init(mykobj_init); // 指定模块的初始化函数
module_exit(mykobj_exit); // 指定模块的退出函数

MODULE_LICENSE("GPL"); // 模块使用的许可证
MODULE_AUTHOR("topeet"); // 模块的作者

netlink监听广播信息

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

Netlink 机制的主要特点包括:

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

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

  1. 系统管理工具:如 ifconfig、ip 等工具使用 Netlink 与内核通信来获取和配置网络接口的信息。
  2. 进程间通信:进程可以使用 Netlink 进行跨进程通信,实现进程间的数据交换和协调。
  3. 内核模块和用户空间应用程序的通信:内核模块可以通过 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
    6
    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:端口 ID,表示进程的标识符。可以将其设置为当前进程的 PID,也可以设为 0,表示不加入任何多播组。
    • nl_groups:多播组掩码,用于指定感兴趣的多播组。当设置为 1 时,表示用户空间进程只会接收内核事件的基本组的内核事件。这意味着,用户空间进程将只接收到属于基本组的内核事件,而不会接收其他多播组的事件。
  • addrlen 参数:addrlen 参数是一个整数,指定了 addr 所指向的结构体对应的字节长度。它用于确保正确解析传递给 addr 参数的结构体的大小

示例

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

示例代码:

1
2
3
4
5
6
7
8
9
10
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);// 打印接收到的数据
}

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>

int main(int argc, char *argv[]) {
int ret;
struct sockaddr_nl nl;
int len = 0;
char buf[4096] = {0};
int i = 0;

bzero(&nl, sizeof(struct sockaddr_nl));
nl.nl_family = AF_NETLINK;
nl.nl_pid = 0;
nl.nl_groups = 1;

int socket_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);
if (socket_fd < 0) {
printf("socket error\n");
return -1;
}

ret = bind(socket_fd, (struct sockaddr *)&nl, sizeof(struct sockaddr_nl));
if (ret < 0) {
printf("bind error\n");
return -1;
}

while (1) {
bzero(buf, 4096);
len = recv(socket_fd, &buf, 4096, 0);

for (i = 0; i < len; i++) {
if (*(buf + i) == '\0') {
buf[i] = '\n';
}
}

printf("%s\n", buf);
}

return 0;
}

uevent_helper

kobject_uevent()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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_HELPERCONFIG_UEVENT_HELPER_PATH 两个宏:

配置方法 1:

1
2
3
4
5
6
7
8
9
10
11
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//选中

在上面的配置 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 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 属性的定义和相关操作的实现,具体内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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 进行操作。

1
2
3
4
5
6
7
8
9
10
// 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/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
2
3
4
5
6
7
8
9
10
11
12
13
#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;
}

编译:

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
2
sd[a-z][0-9] 0:0 666 @/etc/mdev/usb_insert.sh
sd[a-z] 0:0 666 $/etc/mdev/usb_remove.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 脚本。$ 符号表示执行的是一个内部命令。