3

经过前面几篇文章,我们已经走完了 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 的部分。


spacewander
5.6k 声望1.5k 粉丝

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.


引用和评论

0 条评论