systemtap-探秘五-编译和运行

36次阅读

共计 2434 个字符,预计需要花费 7 分钟才能阅读完成。

经过前面几篇文章,我们已经走完了 systemtap 运行的前三个流程,只差最后的编译和运行了。

编译

编译阶段没有什么要说的,唯一要说明的是 stap 生成的内核模块编译起来很耗时。一般来说,整个编译阶段会花上十几二十秒。所以在生成火焰图时,我通常会让 stap 空跑一遍,让它把内核模块编译出来,完成编译阶段后 Ctrl+C 中断掉它。等到正式压测时,再跑一遍。第二次跑的时候,由于可以用上第一次编译出来的内核,花在前四个阶段的时间会减少很多。

来说下最后的运行阶段。

加载

stap 的最后一个阶段,其实是通过运行一个独立的 staprun 二进制文件实现的。这么设计的目的在于把对内核模块的准备和运行分离开来。我们之前执行的 stap 二进制文件可以只负责生成内核模块,然后在目标机器上通过 staprun 运行。这样做的一个好处在于目标机器上可以不用装许多依赖(比如 kernel debuginfo)。另外,在有些公司,服务器只能运行签过名的内核模块,这时候就能先通过 stap 生成内核模块,签了名之后再通过 staprun 运行。具体怎么操作,烦请参考 man staprun

staprun/staprun_funcs.c 文件下,insert_module 函数调用了系统 API init_module,把生成的内核模块加载上去。感兴趣的可以 man init_module 了解下这个 API。

通讯

加载的内核模块会创建 /sys/kernel/debug/systemtap/$module_name 这个目录,

        __stp_module_dir = debugfs_create_dir(module_name, root_dir);

然后创建 /sys/kernel/debug/systemtap/$module_name/.cmd 这个文件。

    /* create [debugfs]/systemtap/module_name/.cmd  */
    _stp_cmd_file = debugfs_create_file(".cmd", 0600, module_dir,
                        NULL, &_stp_ctl_fops_cmd);

debugfs_create_dirdebugfs_create_file 两者是内核模块提供的 API,用来创建一个 debugfs 下的“伪文件”。本着“一切皆文件”的传统,用户态程序可以通过读写这些“伪文件”来调用内核模块指定函数。

_stp_ctl_fops_cmd 这个结构体顾名思义,就是用在发送控制指令并接收响应的。
我们可以看下它的定义:

static struct file_operations _stp_ctl_fops_cmd = {
    .owner = THIS_MODULE,
    /* 读文件时触发 */
    .read = _stp_ctl_read_cmd,
    /* 写文件时触发 */
    .write = _stp_ctl_write_cmd,
    .open = _stp_ctl_open_cmd,
    .release = _stp_ctl_close_cmd,
    .poll = _stp_ctl_poll_cmd
};

同样该内核模块也会注册 /sys/kernel/debug/systemtap/$module_name/trace%d 模式的文件,用于数据流的传输。

让我们从内核态跳回到用户态来。staprun 并非是运行阶段最后执行的二进制文件 – 它在加载了内核模块之后,会 exec 成一个 stapio 进程,负责跟加载的内核模块通讯。

stapio 跟内核模块交互的部分(其实就是对文件的读写),主要位于

int stp_main_loop(void)

(负责读写控制流)

static void *reader_thread(void *data)

(负责读数据流)

这两个函数。

我们可以在 stp_main_loop 这个函数里读到对于不同的控制指令,在用户态部分是如何处理的。具体各个控制指令的定义,在 runtime/transport/transport_msgs.h 里面能看到。在 _stp_ctl_write_cmd 这个函数里能看到对应的控制指令在内核态部分的处理逻辑。因为这部分逻辑相对简单,大抵上就是消息解包 / 打包之后执行相应的动作,外加上有充足的注释,所以我就不赘述了。

卸载

当以下两个条件中的一个得到满足时,staprun 就会卸载内核模块:

  1. stap 脚本走到 exit() 这一步
  2. 给 staprun(其实现在已经 exec 成 stapio 了)发送信号(比如 Ctrl+C 一下)

在内核模块认为自己要退出时,它会把一个 STP_REQUEST_EXIT 控制消息放到伪文件 .cmd 的 buffer 里。当 stapio 读控制流时,如果遇到 STP_REQUEST_EXIT 消息,它就响应 STP_EXIT 消息。内核模块看到 STP_EXIT 后做对应的清理工作,然后返回 STP_EXIT 给 stapio。stapio 看到 STP_EXIT 后就会卸载内核模块。

相应地,如果 stapio 收到了某些要退出的信号,它会给内核模块发送 STP_EXIT 消息,然后再卸载内核模块。

有趣的是,stapio 卸载内核模块,是通过创建一个执行卸载操作的 staprun 进程来实现的。这么一来,内核模块的加载和卸载都是由 staprun 实现,而 stapio 只负责通讯的部分。

结语

整个 systemtap 的会话流程的介绍,到了本篇算是落下帷幕了。限于篇幅所限,有些无关主流程的细节在本系列文章中不得不舍去。感兴趣的读者可以进一步阅读 systemtap 的源码。

本系列文章讲的是 systemtap 的 kernel backend,也即生成内核模块的后端。对于现阶段主流的 Linux 内核版本(CentOS 7 对应的 3.x),通过生成内核模块来做 profile 是通常的做法。考虑到 CentOS 8 都开始用 4.1x 的内核了,也许在不久的将来,即使用 systemtap 也是用它的 bpf backend,即生成 ebpf 的后端。如果有打算阅读源码的话,建议着重看 bpf backend 的部分。

正文完
 0