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。 除了几个问题外,它工作正常

  1. 如果驱动程序不导出 hidraw 节点,我们将无法跟踪任何内容(eBPF 在这里将是一个“上帝模式”,因此这可能会引起一些人的注意)

  2. 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 具有以下可用的附件类型

  1. 使用 libbpf 中的 SEC("struct_ops/hid_device_event") 进行事件处理/过滤

  2. 来自用户空间的操作,在 libbpf 中使用 SEC("syscall")

  3. 在 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 附加到设备时使用的标志。 当前唯一可用的值是 0BPF_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_REPORTHID_FEATURE_REPORTHID_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() 才能获取指向该字段的指针。

hidallocated_size 是只读的,sizeretval 是可读写的。

可以在所有 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 设备分配一个上下文

参数

unsigned int hid_id

HID 设备的系统唯一标识符

描述

返回 成功时指向 struct hid_bpf_ctx 的指针,错误时 NULL

__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 需要是常量,在编译时已知。

这允许以下操作

  1. 对于给定的设备,如果我们知道报告长度始终为某个值,我们可以请求 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]);
    
  2. 如果报告长度是可变的,但我们知道 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。 这意味着,如果我们的实现中存在错误,我们可以随意更改与内核的接口,因为用户空间应用程序负责其自身的使用。