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

为方便起见,该外部存储库将此处 drivers/hid/bpf/progs 中的文件复制到其自己的 src/bpf/stable 目录中。这允许发行版不必提取整个内核源代码树来发布和打包这些 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 程序,除非插入该程序的程序通过锁定该程序并关闭所有指向该程序的文件描述符来允许我们这样做,否则无法覆盖它。

请注意,hid_rdesc_fixup 可以声明为可睡眠的 (SEC("struct_ops.s/hid_rdesc_fixup"))。

开发者 API:

HID-BPF 的可用 struct_ops:

struct hid_bpf_ops

允许将 HID-BPF 程序附加到 HID 设备的 BPF struct_ops 回调。

定义:

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() 之前可写,之后无法更改。

flags

将 struct_ops 附加到设备时使用的标志。当前唯一可用的值是 0BPF_F_BEFORE。仅在 load() 之前可写。

hid_device_event

每当有来自设备的事件进入时调用。

它具有以下参数:

ctx: HID-BPF 上下文,类型为 struct hid_bpf_ctx

返回值:成功时返回 0 并继续处理;返回正值以更改传入的大小缓冲区;返回负错误代码以中断此事件的处理。

上下文:中断上下文。

hid_rdesc_fixup

当 probe 函数解析 HID 设备的报告描述符时调用。

它具有以下参数:

ctx: HID-BPF 上下文,类型为 struct hid_bpf_ctx

返回值:成功时返回 0 并继续处理;返回正值以更改传入的大小缓冲区;返回负错误代码以中断此设备的处理。

hid_hw_request

每当在 HID 设备上发出 hid_hw_raw_request() 调用时调用。

它具有以下参数:

ctx: HID-BPF 上下文,类型为 struct hid_bpf_ctx

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: HID-BPF 上下文,类型为 struct hid_bpf_ctx

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

数据字段中的有效数据。

程序可以通过获取此字段来获得数据中可用的有效大小。程序还可以通过在程序中返回一个正数来更改此值。要丢弃该事件,请返回负错误代码。

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

缓冲区的大小常量

描述

返回值:错误时返回 NULL,成功时返回 __u8 内存指针。

可在系统调用 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_id 在 HID 设备的整个生命周期中都是稳定的。

鉴于 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。这意味着如果我们的实现中存在错误,我们可以随意更改与内核的接口,因为用户空间应用程序负责自己的使用。