user_events: 基于用户的事件跟踪

作者:

Beau Belgrave

概述

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

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

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

通常,程序会注册一组它们希望暴露给可以读取 trace_events 的工具(例如 ftrace 和 perf)的事件。注册过程会告诉内核,如果有任何工具启用了该事件并且应该写入数据,则需要反映哪个地址和位。注册将返回一个写入索引,该索引描述在对 /sys/kernel/tracing/user_events_data 文件调用 write() 或 writev() 时的数据。

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

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

注册

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

此命令采用打包的 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: 要使用的标志(如果有)。 调用者应首先尝试使用标志,然后重试不使用标志,以确保支持较低版本的内核。 如果不支持某个标志,则会返回 -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: 用于此文件描述符的索引,表示写入数据时此事件。 该索引对于用于注册的文件描述符的此实例是唯一的。 有关详细信息,请参见写入数据。

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

注意: 事件子系统名称默认为 “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 mytype {
  char data[20];
};

将由以下字段表示

struct mytype myname 20

删除

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

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

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

取消注册

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

此命令采用打包的 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 必须位于数据的开头,然后其余数据将视为事件的有效负载。

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

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

int index;
int payload;

用户程序可能具有他们希望用来作为有效负载发出的众所周知的结构。 在这种情况下,可以使用 writev(),其中第一个向量是索引,而后续向量是实际的事件有效负载。

例如,如果我有一个像这样的结构

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 不会发出到正在记录的跟踪中。

示例代码

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