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” 命名的子系统下的任何其他事件一样显示。这意味着希望附加到事件的工具需要使用 /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 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 中的示例代码。