HID-BPF¶
HID 是输入设备的标准协议,但某些设备可能需要自定义调整,传统上使用内核驱动程序修复来完成。 使用 eBPF 功能可以加快开发速度,并为现有的 HID 接口添加新的功能。
何时(以及为什么)使用 HID-BPF¶
在某些情况下,使用 HID-BPF 比标准的内核驱动程序修复更好
操纵杆的死区¶
假设你有一个旧的操纵杆,通常会看到它在中性点附近晃动。 这通常在应用程序级别通过为此特定轴添加死区来过滤。
使用 HID-BPF,我们可以直接在内核中应用此过滤,因此当输入控制器上没有其他事情发生时,用户空间不会被唤醒。
当然,鉴于此死区特定于单个设备,我们无法为所有相同的操纵杆创建通用修复程序。 为此添加自定义内核 API(例如,通过添加 sysfs 条目)并不能保证此新内核 API 将被广泛采用和维护。
HID-BPF 允许用户空间程序加载程序本身,确保我们只在有用户时才加载自定义 API。
报告描述符的简单修复¶
在 HID 树中,一半的驱动程序仅修复报告描述符中的一个键或一个字节。 这些修复都需要内核补丁以及后续的发布管理,这对用户来说是一个漫长而痛苦的过程。
我们可以通过提供 eBPF 程序来减轻这种负担。 一旦用户验证了这样的程序,我们可以将源代码嵌入到内核树中,并直接运送和加载 eBPF 程序,而不是加载特定的内核模块。
注意:eBPF 程序的发布及其包含在内核中尚未完全实现
添加需要新的内核 API 的新功能¶
此类功能的一个示例是通用手写笔接口 (USI) 笔。 基本上,USI 笔需要一个新的内核 API,因为我们的 HID 和输入堆栈不支持新的通信通道。 我们可以依靠 eBPF 来让消费者控制内核 API,并且每次有事件发生时都不会唤醒用户空间,从而不会影响性能,而不是使用 hidraw 或创建新的 sysfs 条目或 ioctl。
将设备变形为其他设备并从用户空间控制它¶
内核具有 HID 项目到 evdev 位的相对静态的映射。 它无法决定动态地将给定设备转换为其他设备,因为它没有所需的上下文,并且用户空间无法撤消(甚至无法发现)任何此类转换。
但是,某些设备在这种静态定义设备的方式下是无用的。 例如,Microsoft Surface Dial 是一个带有触觉反馈的按钮,到目前为止几乎无法使用。
使用 eBPF,用户空间可以将该设备变形为鼠标,并将拨号事件转换为滚轮事件。 此外,用户空间程序可以根据上下文设置/取消设置触觉反馈。 例如,如果屏幕上可见一个菜单,我们可能需要每 15 度进行一次触觉点击。 但是,当在网页中滚动时,设备以最高分辨率发出事件时,用户体验会更好。
防火墙¶
如果我们想阻止其他用户访问设备的特定功能怎么办? (考虑一个可能已损坏的固件更新入口点)
使用 eBPF,我们可以拦截发送到设备的任何 HID 命令并验证它。
这也允许在用户空间和内核/bpf 程序之间同步状态,因为我们可以拦截任何传入的命令。
跟踪¶
最后一个用途是跟踪事件以及我们可以使用 BPF 来汇总和分析事件的所有乐趣。
现在,跟踪依赖于 hidraw。 除了几个问题外,它工作正常
如果驱动程序不导出 hidraw 节点,我们将无法跟踪任何内容(eBPF 在这里将是一个“上帝模式”,因此这可能会引起一些人的注意)
hidraw 不捕获其他进程对设备的请求,这意味着我们需要向内核添加 printk 才能了解发生了什么。
HID-BPF 的高级视图¶
HID-BPF 背后的主要思想是它在字节数组级别上工作。 因此,HID 报告和 HID 报告描述符的所有解析都必须在加载 eBPF 程序的用户的组件中实现。
例如,在上面的死区操纵杆中,知道数据流中的哪些字段需要设置为 0
需要由用户空间计算。
由此得出,HID-BPF 不了解内核中可用的其他子系统。 您不能直接从 eBPF 通过输入 API 发出输入事件。
当 BPF 程序需要发出输入事件时,它需要与 HID 协议通信,并依靠 HID 内核处理将 HID 数据转换为输入事件。
树内 HID-BPF 程序和 udev-hid-bpf
¶
官方设备修复程序以源代码的形式在内核树中的 drivers/hid/bpf/progs
目录中发布。 这允许在 tools/testing/selftests/hid
中向它们添加自测。
但是,鉴于它们需要外部工具才能加载,因此这些对象的编译不是常规内核编译的一部分。 此工具当前是 udev-hid-bpf。
为方便起见,该外部存储库将其自己的 src/bpf/stable
目录中 drivers/hid/bpf/progs
中的文件重复。 这允许发行版不必提取整个内核源代码树来发布和打包这些 HID-BPF 修复程序。 udev-hid-bpf
还具有根据用户运行的内核处理多个对象文件的能力。
可用程序类型¶
HID-BPF 构建在 BPF 之上,这意味着我们使用 bpf struct_ops 方法来声明我们的程序。
HID-BPF 具有以下可用的附件类型
使用 libbpf 中的
SEC("struct_ops/hid_device_event")
进行事件处理/过滤来自用户空间的操作,在 libbpf 中使用
SEC("syscall")
在 libbpf 中使用
SEC("struct_ops/hid_rdesc_fixup")
或SEC("struct_ops.s/hid_rdesc_fixup")
更改报告描述符
当从设备收到事件时,hid_device_event
正在调用 BPF 程序。 因此,我们在 IRQ 上下文中,可以对数据执行操作或通知用户空间。 鉴于我们在 IRQ 上下文中,我们无法与设备对话。
syscall
意味着用户空间调用了 syscall BPF_PROG_RUN
工具。 这一次,我们可以执行 HID-BPF 允许的任何操作,并且允许与设备对话。
最后,hid_rdesc_fixup
与其他程序不同,因为此类型的 BPF 程序只能有一个。 当驱动程序从 probe
调用它时,允许从 BPF 程序更改报告描述符。 一旦加载了 hid_rdesc_fixup
程序,除非插入它的程序通过固定程序并关闭指向它的所有 fd 来允许我们,否则无法覆盖它。
请注意,hid_rdesc_fixup
可以声明为可睡眠 (SEC("struct_ops.s/hid_rdesc_fixup")
)。
开发者 API:¶
HID-BPF 的可用 struct_ops
:¶
-
struct hid_bpf_ops¶
一个 BPF struct_ops 回调,允许将 HID-BPF 程序附加到 HID 设备
定义:
struct hid_bpf_ops {
int hid_id;
u32 flags;
int (*hid_device_event)(struct hid_bpf_ctx *ctx, enum hid_report_type report_type, u64 source);
int (*hid_rdesc_fixup)(struct hid_bpf_ctx *ctx);
int (*hid_hw_request)(struct hid_bpf_ctx *ctx, unsigned char reportnum,enum hid_report_type rtype, enum hid_class_request reqtype, u64 source);
int (*hid_hw_output_report)(struct hid_bpf_ctx *ctx, u64 source);
};
成员
hid_id
要附加到的 HID 唯一 ID。 这在
load()
之前是可写的,之后无法更改标志
将 struct_ops 附加到设备时使用的标志。 当前唯一可用的值是
0
或BPF_F_BEFORE
。 仅在load()
之前可写hid_device_event
每当设备传入事件时调用
它具有以下参数
ctx
:作为struct hid_bpf_ctx
的 HID-BPF 上下文返回:成功时
0
并保持处理;一个正值以更改传入的大小缓冲区;一个负错误代码以中断此事件的处理上下文:中断上下文。
hid_rdesc_fixup
当 probe 函数解析 HID 设备的报告描述符时调用
它具有以下参数
ctx
:作为struct hid_bpf_ctx
的 HID-BPF 上下文返回:成功时
0
并保持处理;一个正值以更改传入的大小缓冲区;一个负错误代码以中断此设备的处理hid_hw_request
每当在 HID 设备上发出 hid_hw_raw_request() 调用时调用
它具有以下参数
ctx
:作为struct hid_bpf_ctx
的 HID-BPF 上下文reportnum
:报告编号,如 hid_hw_raw_request() 中rtype
:报告类型 (HID_INPUT_REPORT
、HID_FEATURE_REPORT
、HID_OUTPUT_REPORT
)reqtype
:请求source
:引用唯一但可识别源的 u64。 如果0
,则内核本身发出了该调用。 对于 hidraw,source
设置为相关的struct file *
。返回:
0
以保持 hid-core 处理请求;任何其他值都会阻止 hid-core 处理该事件。 应该返回一个正值,其中包含在传入缓冲区中返回的字节数;一个负错误代码会中断此调用的处理。hid_hw_output_report
每当在 HID 设备上发出 hid_hw_output_report() 调用时调用
它具有以下参数
ctx
:作为struct hid_bpf_ctx
的 HID-BPF 上下文source
:引用唯一但可识别源的 u64。 如果0
,则内核本身发出了该调用。 对于 hidraw,source
设置为相关的struct file *
。返回:
0
以保持 hid-core 处理请求;任何其他值都会阻止 hid-core 处理该事件。 应该返回一个正值,其中包含写入设备的字节数;一个负错误代码会中断此调用的处理。
程序中可用的用户 API 数据结构:¶
-
struct hid_bpf_ctx¶
所有 HID 程序的用户可访问数据
定义:
struct hid_bpf_ctx {
struct hid_device *hid;
__u32 allocated_size;
union {
__s32 retval;
__s32 size;
};
};
成员
hid
表示设备本身的
struct hid_device
allocated_size
分配的数据大小。
这是可用的内存量,可以由 HID 程序请求。 请注意,对于
HID_BPF_RDESC_FIXUP
,该内存设置为4096
(4 KB){unnamed_union}
匿名
retval
先前程序的返回值。
大小
数据字段中的有效数据。
程序可以通过获取此字段来获取数据中可用的有效大小。 程序还可以通过在程序中返回一个正数来更改此值。 要丢弃该事件,请返回一个负错误代码。
size
必须始终小于或等于allocated_size
(一旦所有 BPF 程序都已运行,它就会强制执行)。
描述
data
不能直接从上下文中访问。 我们需要调用 hid_bpf_get_data()
才能获取指向该字段的指针。
hid
和 allocated_size
是只读的,size
和 retval
是可读写的。
可以在所有 HID-BPF struct_ops 程序中使用的可用 API:¶
-
__bpf_kfunc __u8 *hid_bpf_get_data(struct hid_bpf_ctx *ctx, unsigned int offset, const size_t rdwr_buf_size)¶
获取与上下文 ctx 关联的内核内存指针
参数
struct hid_bpf_ctx *ctx
HID-BPF 上下文
unsigned int offset
内存中的偏移量
const size_t rdwr_buf_size
缓冲区的 const 大小
描述
返回 错误时 NULL
,成功时 __u8
内存指针
可以在 syscall HID-BPF 程序或可睡眠 HID-BPF struct_ops 程序中使用的可用 API:¶
-
__bpf_kfunc struct hid_bpf_ctx *hid_bpf_allocate_context(unsigned int hid_id)¶
为给定的 HID 设备分配一个上下文
-
__bpf_kfunc void hid_bpf_release_context(struct hid_bpf_ctx *ctx)¶
释放先前分配的上下文 ctx
参数
struct hid_bpf_ctx *ctx
要释放的 HID-BPF 上下文
-
__bpf_kfunc int hid_bpf_hw_request(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz, enum hid_report_type rtype, enum hid_class_request reqtype)¶
与 HID 设备通信
参数
struct hid_bpf_ctx *ctx
先前在
hid_bpf_allocate_context()
中分配的 HID-BPF 上下文__u8 *buf
一个
PTR_TO_MEM
缓冲区size_t buf__sz
要传输的数据的大小
enum hid_report_type rtype
报告的类型 (
HID_INPUT_REPORT
,HID_FEATURE_REPORT
,HID_OUTPUT_REPORT
)enum hid_class_request reqtype
请求的类型 (
HID_REQ_GET_REPORT
,HID_REQ_SET_REPORT
, ...)
描述
返回值:成功时返回 0
,否则返回负的错误代码。
-
__bpf_kfunc int hid_bpf_hw_output_report(struct hid_bpf_ctx *ctx, __u8 *buf, size_t buf__sz)¶
向 HID 设备发送输出报告
参数
struct hid_bpf_ctx *ctx
先前在
hid_bpf_allocate_context()
中分配的 HID-BPF 上下文__u8 *buf
一个
PTR_TO_MEM
缓冲区size_t buf__sz
要传输的数据的大小
描述
成功时返回传输的字节数,否则返回负的错误代码。
-
__bpf_kfunc int hid_bpf_try_input_report(struct hid_bpf_ctx *ctx, enum hid_report_type type, u8 *buf, const size_t buf__sz)¶
从 HID 设备向内核注入 HID 报告
参数
struct hid_bpf_ctx *ctx
先前在
hid_bpf_allocate_context()
中分配的 HID-BPF 上下文enum hid_report_type type
报告的类型 (
HID_INPUT_REPORT
,HID_FEATURE_REPORT
,HID_OUTPUT_REPORT
)u8 *buf
一个
PTR_TO_MEM
缓冲区const size_t buf__sz
要传输的数据的大小
描述
成功时返回 0
,否则返回负的错误代码。 如果设备不可用,此函数将立即失败,因此可以安全地在 IRQ 上下文中使用。
-
__bpf_kfunc int hid_bpf_input_report(struct hid_bpf_ctx *ctx, enum hid_report_type type, u8 *buf, const size_t buf__sz)¶
从 HID 设备向内核注入 HID 报告
参数
struct hid_bpf_ctx *ctx
先前在
hid_bpf_allocate_context()
中分配的 HID-BPF 上下文enum hid_report_type type
报告的类型 (
HID_INPUT_REPORT
,HID_FEATURE_REPORT
,HID_OUTPUT_REPORT
)u8 *buf
一个
PTR_TO_MEM
缓冲区const size_t buf__sz
要传输的数据的大小
描述
成功时返回 0
,否则返回负的错误代码。 此函数将等待设备可用,然后再注入事件,因此需要在可睡眠的上下文中调用。
HID-BPF 程序的一般概述¶
访问附加到上下文的数据¶
struct hid_bpf_ctx
不直接导出 data
字段,要访问它,bpf 程序需要首先调用 hid_bpf_get_data()
。
offset
可以是任何整数,但 size
需要是常量,在编译时已知。
这允许以下操作
对于给定的设备,如果我们知道报告长度始终为某个值,我们可以请求
data
指针指向完整的报告长度。内核将确保我们使用正确的尺寸和偏移量,而 eBPF 将确保代码不会尝试在边界之外读取或写入
__u8 *data = hid_bpf_get_data(ctx, 0 /* offset */, 256 /* size */); if (!data) return 0; /* ensure data is correct, now the verifier knows we * have 256 bytes available */ bpf_printk("hello world: %02x %02x %02x", data[0], data[128], data[255]);
如果报告长度是可变的,但我们知道
X
的值始终是 16 位整数,那么我们可以拥有一个指向该值的指针__u16 *x = hid_bpf_get_data(ctx, offset, sizeof(*x)); if (!x) return 0; /* something went wrong */ *x += 1; /* increment X by one */
HID-BPF 程序的效果¶
对于除 hid_rdesc_fixup()
之外的所有 HID-BPF 附加类型,可以将多个 eBPF 程序附加到同一设备。 如果 HID-BPF struct_ops 具有 hid_rdesc_fixup()
,而另一个已经附加到设备,则内核在附加 struct_ops 时将返回 -EINVAL。
除非在附加程序时将 BPF_F_BEFORE
添加到标志,否则新程序将附加到列表的末尾。 BPF_F_BEFORE
会将新程序插入到列表的开头,这对于例如跟踪非常有用,我们需要从设备获取未经处理的事件。
请注意,如果有多个程序使用 BPF_F_BEFORE
标志,则只有最近加载的程序实际上是列表中的第一个。
SEC("struct_ops/hid_device_event")
¶
每当引发匹配的事件时,eBPF 程序会一个接一个地被调用,并且作用于相同的数据缓冲区。
如果程序更改了与上下文关联的数据,则下一个程序将看到修改后的数据,但它不知道原始数据是什么。
一旦所有程序运行完毕并返回 0
或正值,HID 堆栈的其余部分将作用于修改后的数据,其中最后一个 hid_bpf_ctx 的 size
字段是数据输入流的新大小。
返回负错误的 BPF 程序会丢弃该事件,即 HID 堆栈不会处理该事件。 客户端(hidraw、input、LED)将看不到此事件。
SEC("syscall")
¶
syscall
未附加到给定的设备。 为了区分我们正在使用的设备,用户空间需要通过其唯一的系统 ID 来引用该设备(sysfs 路径中的最后 4 个数字:/sys/bus/hid/devices/xxxx:yyyy:zzzz:0000
)。
要检索与设备关联的上下文,程序必须调用 hid_bpf_allocate_context()
,并且必须在返回之前使用 hid_bpf_release_context()
释放它。 检索到上下文后,还可以使用 hid_bpf_get_data()
请求指向内核内存的指针。 此内存足够大,可以支持给定设备的所有输入/输出/特征报告。
SEC("struct_ops/hid_rdesc_fixup")
¶
hid_rdesc_fixup
程序的运作方式类似于 struct hid_driver
的 .report_fixup
。
当探测设备时,内核使用报告描述符的内容设置上下文的数据缓冲区。 与该缓冲区关联的内存是 HID_MAX_DESCRIPTOR_SIZE
(当前为 4kB)。
eBPF 程序可以随意修改数据缓冲区,内核使用修改后的内容和大小作为报告描述符。
每当附加包含 SEC("struct_ops/hid_rdesc_fixup")
程序的 struct_ops 时(如果之前未附加程序),内核会立即断开 HID 设备并进行重新探测。
同样,当分离此 struct_ops 时,内核会在设备上发出断开连接。
HID-BPF 中没有 detach
工具。 当指向 HID-BPF struct_ops 链接的所有用户空间文件描述符关闭时,会发生分离程序。 因此,如果我们需要替换报告描述符修复程序,则需要原始报告描述符修复程序的拥有者进行一些协作。 前一个拥有者可能会将 struct_ops 链接固定在 bpffs 中,然后我们可以通过正常的 bpf 操作来替换它。
将 bpf 程序附加到设备¶
我们现在使用通过 bpf_map__attach_struct_ops()
的标准 struct_ops 附加。 但是鉴于我们需要将 struct_ops 附加到专用的 HID 设备,因此调用者必须在将程序加载到内核之前在 struct_ops 映射中设置 hid_id
。
hid_id
是 HID 设备的唯一系统 ID(sysfs 路径中的最后 4 个数字:/sys/bus/hid/devices/xxxx:yyyy:zzzz:0000
)
也可以设置 flags
,它的类型是 enum hid_bpf_attach_flags
。
我们不能依靠 hidraw 将 BPF 程序绑定到 HID 设备。 hidraw 是 HID 设备处理的产物,并且不稳定。 某些驱动程序甚至禁用它,因此消除了对这些设备进行跟踪的功能(在这些设备上获取非 hidraw 跟踪很有趣)。
另一方面,对于 HID 设备的整个生命周期,即使我们更改其报告描述符,hid_id
也是稳定的。
鉴于当设备断开/重新连接时 hidraw 不稳定,我们建议通过 sysfs 访问设备的当前报告描述符。 可以在 /sys/bus/hid/devices/BUS:VID:PID.000N/report_descriptor
中以二进制流的形式访问它。
解析报告描述符是 BPF 程序员或加载 eBPF 程序的用户空间组件的责任。
一个(几乎)完整的 BPF 增强型 HID 设备示例¶
前言:在大多数情况下,这可以作为内核驱动程序来实现
让我们想象一下,我们有一个新的平板电脑设备,它具有一些触觉功能来模拟用户在其上刮擦的表面。 该设备还将具有一个特定的 3 位开关,用于在纸上的铅笔、墙上的蜡笔和绘画画布上的刷子之间切换。 为了使事情变得更好,我们可以通过特征报告来控制开关的物理位置。
当然,该开关依赖于一些用户空间组件来控制设备本身的触觉功能。
过滤事件¶
第一步是过滤来自设备的事件。 鉴于开关位置实际上是在笔事件流中报告的,因此使用 hidraw 来实现该过滤意味着我们为每个事件唤醒用户空间。
这对 libinput 来说是可以的,但是如果有一个外部库只对报告中的一个字节感兴趣,那就不太理想了。
为此,我们可以为我们的 BPF 程序创建一个基本骨架
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
/* HID programs need to be GPL */
char _license[] SEC("license") = "GPL";
/* HID-BPF kfunc API definitions */
extern __u8 *hid_bpf_get_data(struct hid_bpf_ctx *ctx,
unsigned int offset,
const size_t __sz) __ksym;
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 4096 * 64);
} ringbuf SEC(".maps");
__u8 current_value = 0;
SEC("struct_ops/hid_device_event")
int BPF_PROG(filter_switch, struct hid_bpf_ctx *hid_ctx)
{
__u8 *data = hid_bpf_get_data(hid_ctx, 0 /* offset */, 192 /* size */);
__u8 *buf;
if (!data)
return 0; /* EPERM check */
if (current_value != data[152]) {
buf = bpf_ringbuf_reserve(&ringbuf, 1, 0);
if (!buf)
return 0;
*buf = data[152];
bpf_ringbuf_commit(buf, 0);
current_value = data[152];
}
return 0;
}
SEC(".struct_ops.link")
struct hid_bpf_ops haptic_tablet = {
.hid_device_event = (void *)filter_switch,
};
要附加 haptic_tablet
,用户空间需要首先设置 hid_id
static int attach_filter(struct hid *hid_skel, int hid_id)
{
int err, link_fd;
hid_skel->struct_ops.haptic_tablet->hid_id = hid_id;
err = hid__load(skel);
if (err)
return err;
link_fd = bpf_map__attach_struct_ops(hid_skel->maps.haptic_tablet);
if (!link_fd) {
fprintf(stderr, "can not attach HID-BPF program: %m\n");
return -1;
}
return link_fd; /* the fd of the created bpf_link */
}
我们的用户空间程序现在可以监听环形缓冲区上的通知,并且仅当值更改时才会被唤醒。
当用户空间程序不再需要监听事件时,它只需关闭从 attach_filter()
返回的 bpf 链接,这将告诉内核从 HID 设备分离程序。
当然,在其他用例中,用户空间程序还可以像任何 bpf_link 一样,通过调用 bpf_obj_pin()
将 fd 固定到 BPF 文件系统。
控制设备¶
为了能够更改平板电脑的触觉反馈,用户空间程序需要在设备本身上发出特征报告。
我们可以创建一个与此相关的 SEC("syscall")
程序,而不是使用 hidraw
/* some more HID-BPF kfunc API definitions */
extern struct hid_bpf_ctx *hid_bpf_allocate_context(unsigned int hid_id) __ksym;
extern void hid_bpf_release_context(struct hid_bpf_ctx *ctx) __ksym;
extern int hid_bpf_hw_request(struct hid_bpf_ctx *ctx,
__u8* data,
size_t len,
enum hid_report_type type,
enum hid_class_request reqtype) __ksym;
struct hid_send_haptics_args {
/* data needs to come at offset 0 so we can do a memcpy into it */
__u8 data[10];
unsigned int hid;
};
SEC("syscall")
int send_haptic(struct hid_send_haptics_args *args)
{
struct hid_bpf_ctx *ctx;
int ret = 0;
ctx = hid_bpf_allocate_context(args->hid);
if (!ctx)
return 0; /* EPERM check */
ret = hid_bpf_hw_request(ctx,
args->data,
10,
HID_FEATURE_REPORT,
HID_REQ_SET_REPORT);
hid_bpf_release_context(ctx);
return ret;
}
然后,用户空间需要直接调用该程序
static int set_haptic(struct hid *hid_skel, int hid_id, __u8 haptic_value)
{
int err, prog_fd;
int ret = -1;
struct hid_send_haptics_args args = {
.hid = hid_id,
};
DECLARE_LIBBPF_OPTS(bpf_test_run_opts, tattrs,
.ctx_in = &args,
.ctx_size_in = sizeof(args),
);
args.data[0] = 0x02; /* report ID of the feature on our device */
args.data[1] = haptic_value;
prog_fd = bpf_program__fd(hid_skel->progs.set_haptic);
err = bpf_prog_test_run_opts(prog_fd, &tattrs);
return err;
}
现在我们的用户空间程序知道触觉状态,并且可以控制它。 该程序可以使该状态进一步可用于其他用户空间程序(例如,通过 DBus API)。
这里有趣的是,我们没有为此创建新的内核 API。 这意味着,如果我们的实现中存在错误,我们可以随意更改与内核的接口,因为用户空间应用程序负责其自身的使用。