user_events: 基于用户的事件跟踪

作者:

Beau Belgrave

概述

基于用户的跟踪事件允许用户进程创建事件并跟踪数据,这些数据可以通过现有工具(如 ftrace 和 perf)进行查看。要启用此功能,请在构建内核时配置 CONFIG_USER_EVENTS=y。

程序可以通过 /sys/kernel/tracing/user_events_status 查看事件状态,并通过 /sys/kernel/tracing/user_events_data 注册并写出数据。

程序还可以通过带有 u: 前缀的 /sys/kernel/tracing/dynamic_events 来注册和删除基于用户的事件。发送给 dynamic_events 的命令格式与应用了 u: 前缀的 ioctl 相同。由于事件会持久化,这需要 CAP_PERFMON 权限,否则会返回 -EPERM。

通常,程序会注册一组它们希望向能够读取 trace_events 的工具(如 ftrace 和 perf)公开的事件。注册过程会告诉内核:如果有任何工具启用了该事件并且应该写入数据,则应反映在哪个地址和哪个位上。注册会返回一个写入索引(write index),当在 /sys/kernel/tracing/user_events_data 文件上调用 write() 或 writev() 时,该索引用于描述数据。

本文档中引用的结构体包含在源码树的 /include/uapi/linux/user_events.h 文件中。

注意: user_events_status 和 user_events_data 都位于 tracefs 文件系统下,并且可能挂载在与上述不同的路径下。

注册

在用户进程内部进行注册是通过对 /sys/kernel/tracing/user_events_data 文件调用 ioctl() 来完成的。要发出的命令是 DIAG_IOCSREG。

该命令接受一个紧凑的(packed)struct user_reg 作为参数

struct user_reg {
      /* Input: Size of the user_reg structure being used */
      __u32 size;

      /* Input: Bit in enable address to use */
      __u8 enable_bit;

      /* Input: Enable size in bytes at address */
      __u8 enable_size;

      /* Input: Flags to use, if any */
      __u16 flags;

      /* Input: Address to update when enabled */
      __u64 enable_addr;

      /* Input: Pointer to string with event name, description and flags */
      __u64 name_args;

      /* Output: Index of the event to use when writing data */
      __u32 write_index;
} __attribute__((__packed__));

struct user_reg 要求正确设置以上所有输入项。

  • size:必须设置为 sizeof(struct user_reg)。

  • enable_bit:用于在 enable_addr 指定的地址处反映事件状态的位。

  • enable_size:enable_addr 指定的值的大小。这必须是 4(32位)或 8(64位)。64位值仅允许在 64 位内核上使用,然而 32 位可以用于所有内核。

  • flags:要使用的标志(如果有)。调用者应首先尝试使用 flags,如果失败则在不使用 flags 的情况下重试,以确保对旧版本内核的支持。如果某个标志不受支持,则会返回 -EINVAL。

  • enable_addr:用于反映事件状态的值的地址。这必须是自然对齐的,且在用户程序内具有写访问权限。

  • name_args:描述事件的名称和参数,详情请参见命令格式。

当前支持以下标志。

  • USER_EVENT_REG_PERSIST:当最后一个引用关闭时,事件不会被删除。如果希望事件在进程关闭或注销事件后仍然存在,调用者可以使用此标志。需要 CAP_PERFMON 权限,否则返回 -EPERM。

  • USER_EVENT_REG_MULTI_FORMAT:事件可以包含多种格式。这允许程序在事件格式发生变化且它们希望使用相同名称时,防止自身被阻塞。当使用此标志时,跟踪点名称将采用“name.unique_id”的新格式,而不是旧格式“name”。将为每个唯一的名称和格式对创建一个跟踪点。这意味着如果多个进程使用相同的名称和格式,它们将使用相同的跟踪点。如果有另一个进程使用相同的名称,但格式与其他进程不同,它将使用带有新唯一 ID 的不同跟踪点。记录程序需要扫描 tracefs,以查找其感兴趣记录的事件名称的各种不同格式。跟踪点的系统名称也将使用 “user_events_multi” 而不是 “user_events”。这可以防止单格式事件名称与 tracefs 中的任何多格式事件名称冲突。unique_id 以十六进制字符串形式输出。记录程序应确保跟踪点名称以它们注册的事件名称开头,并且具有以 . 开头且仅包含十六进制字符的后缀。例如,要查找事件 “test” 的所有版本,可以使用正则表达式 “^test.[0-9a-fA-F]+$”。

注册成功后,将设置以下内容。

  • write_index:在写出数据时,用于代表此事件的文件描述符的索引。该索引对于用于注册的文件描述符实例是唯一的。详见“写入数据”。

基于用户的事件会像名为“user_events”子系统下的任何其他事件一样显示在 tracefs 下。这意味着希望附加到这些事件的工具在附加/录制时需要使用 /sys/kernel/tracing/events/user_events/[name]/enable 或 perf record -e user_events:[name]。

注意: 事件子系统名称默认为“user_events”。调用者不应假设它将始终是“user_events”。操作员保留在未来按进程更改子系统名称以适应事件隔离的权利。此外,如果使用了 USER_EVENT_REG_MULTI_FORMAT 标志,跟踪点名称将附加一个唯一 ID,并且系统名称将如上所述变为“user_events_multi”。

命令格式

命令字符串格式如下

name[:FLAG1[,FLAG2...]] [Field1[;Field2...]]

支持的标志

暂无

字段格式

type name [size]

支持基本类型(__data_loc、u32、u64、int、char、char[20] 等)。建议用户程序使用具有明确大小的类型,如 u32。

注意: 由于大小在用户空间和内核空间之间可能有所不同,因此不支持 long。

大小仅对以 struct 前缀开头的类型有效。如果需要,这允许用户程序向工具描述自定义结构体。

例如,C 语言中的 struct in 看起来像这样

struct mytype {
  char data[20];
};

将由以下字段表示

struct mytype myname 20

删除

从用户进程内部删除事件是通过对 /sys/kernel/tracing/user_events_data 文件调用 ioctl() 来完成的。要发出的命令是 DIAG_IOCSDEL。

此命令仅需要一个字符串,通过其名称指定要删除的事件。只有在对该事件没有剩余引用(在用户空间和内核空间中)时,删除才会成功。因此,用户程序应使用与注册时不同的文件来请求删除。

注意: 默认情况下,当事件没有剩余引用时,事件将自动删除。如果程序不希望自动删除,必须在注册事件时使用 USER_EVENT_REG_PERSIST 标志。一旦使用该标志,事件将一直存在,直到调用 DIAG_IOCSDEL。持久化事件的注册和删除都需要 CAP_PERFMON,否则会返回 -EPERM。当存在相同事件名称的多种格式时,将尝试删除所有同名事件。如果只想删除特定版本,则应使用 /sys/kernel/tracing/dynamic_events 文件来删除该特定格式的事件。

注销

如果在注册事件后不再希望更新它,可以通过对 /sys/kernel/tracing/user_events_data 文件调用 ioctl() 来禁用它。要发出的命令是 DIAG_IOCSUNREG。这与删除不同,删除实际上是从系统中移除事件。注销只是告诉内核你的进程不再对该事件的更新感兴趣。

该命令接受一个紧凑的(packed)struct user_unreg 作为参数

struct user_unreg {
      /* Input: Size of the user_unreg structure being used */
      __u32 size;

      /* Input: Bit to unregister */
      __u8 disable_bit;

      /* Input: Reserved, set to 0 */
      __u8 __reserved;

      /* Input: Reserved, set to 0 */
      __u16 __reserved2;

      /* Input: Address to unregister */
      __u64 disable_addr;
} __attribute__((__packed__));

struct user_unreg 要求正确设置以上所有输入项。

  • size:必须设置为 sizeof(struct user_unreg)。

  • disable_bit:必须设置为要禁用的位(与之前通过 enable_bit 注册的位相同)。

  • disable_addr:必须设置为要禁用的地址(与之前通过 enable_addr 注册的地址相同)。

注意: 调用 execve() 时,事件会自动注销。在 fork() 期间,注册的事件将被保留,如果需要,必须在每个进程中手动注销。

状态

当工具附加/录制基于用户的事件时,事件的状态会实时更新。这使得用户程序仅在有程序积极附加到该事件时,才承担 write() 或 writev() 调用的开销。

当工具附加/从事件分离时,内核将更新为该事件注册的指定位。用户程序只需检查该位是否被设置,即可知道是否有工具附加。

管理员可以通过终端直接读取 user_events_status 文件来轻松检查所有已注册事件的状态。输出如下

Name [# Comments]
...

Active: ActiveCount
Busy: BusyCount

例如,在一个只有一个事件的系统上,输出看起来像这样

test

Active: 1
Busy: 0

如果用户通过 ftrace 启用用户事件,输出将更改为

test # Used by ftrace

Active: 1
Busy: 1

写入数据

注册事件后,用于注册的同一个文件描述符(fd)可用于为该事件写入条目。返回的 write_index 必须位于数据的开头,然后其余数据被视为事件的有效负载(payload)。

例如,如果返回的 write_index 是 1,并且我想要写出一个 int 类型的事件有效负载。那么数据的大小必须是 8 字节(2 个 int),前 4 字节等于 1,最后 4 字节等于我作为有效负载所需的值。

在内存中,这看起来像这样

int index;
int payload;

用户程序可能拥有它们希望用作有效负载发出的知名结构体。在这些情况下,可以使用 writev(),其中第一个向量(vector)是索引,随后的向量是实际的事件有效负载。

例如,如果我有一个这样的 struct like

struct payload {
      int src;
      int dst;
      int flags;
} __attribute__((__packed__));

建议用户程序执行以下操作

struct iovec io[2];
struct payload e;

io[0].iov_base = &write_index;
io[0].iov_len = sizeof(write_index);
io[1].iov_base = &e;
io[1].iov_len = sizeof(e);

writev(fd, (const struct iovec*)io, 2);

注意: write_index 不会输出到正在记录的跟踪(trace)中。

示例代码

请参阅 samples/user_events 中的示例代码。