1. SystemTap是什么?
SystemTap是一款Linux内核调试和应用跟踪调试的工具,可以读取和更改应用程序运行过程中的状态,具有低时延、动态调试的特点。使用SystemTap你需要编写systemtap语法的脚本,SystemTap会自动把脚本翻译成C语言的代码,再编译成一个系统的内核。在脚本运行期间,内核会加载到系统中去,运行结束后,内核模块会被卸载。这个跟目前业界很火的EBPF很像,但是SystemTap没有EBPF的安全检查机制,比如循环检测、禁止不可达指令等机制。但是SystemTap发展了这么多年,有很多配套的工具和成熟的解决方案,还是很值得我们学习的。
从脚本到C语言再到内核模块,再到probe信息输出的流程图如下:
如何安装SystemTap的使用环境,请参考我的这一篇文章:手把手教你使用火焰图查看诊断OpenResty(nginx)的Lua代码性能瓶颈
2. Hello World
hello-world.stp的内容如下:
#!/usr/bin/env stap
probe oneshot {
# 嵌入C代码
%{ printk(KERN_ALERT "Hello Wrold from systemtap c\n") %};
# 标准stap的脚本语法
printf("Hello World\n");
exit();
}
运行后,输出如下:
[root@localhost systemtap]# stap -g hello-world.stp Hello World
参数解析:
- -g参数,代表guru模式,运行嵌入C代码的时候,需要开启guru模式
代码详解:
- probe oneshot代表此block的脚本之运行一次,这个语法与AWK这个工具很相似。
- %{%} 内部可以嵌入C的代码,我们就可以实现调用内核开发中使用的printk函数,打印输出(注意这里的输出不会到标准输出中,需要用dmesg去查看)
- printf是stap脚本的打印,常用于打印统计的数据
嵌入C的代码输入如下
[root@localhost systemtap]# dmesg|tail -n 1 [51913.062057] Hello Wrold from systemtap c
3. 基本语法
术语:
-
event: 事件,有定时器事件,有函数事件(enter/exit)
-
handler: 某个event发生的时候,执行的代码
-
probe = event + handler, probe定义的语法是
probe event {statements}
-
scripte: systemtap的脚本,一个脚本里面可以有多个probe
-
function: 函数,probe的statements中可以使用函数
function function_name(arguements) {statements}
probe evetn {function_name(arguments)}
3.1 Event
systemtap中有两大类事件,分别是同步事件和异步事件。同步事件:有代码运行到了指定的位置,而触发的事件;异步事件:与特定的代码运行无关。
可以使用man stapprobes 查看详细的Events有哪些。
同步事件例子:
- syscall.system_call :系统调用
- vfs.file_operation
- module("module").funciton("function")
- kernel.function("function")
- kernel.trace("tracepoint")
异步事件例子:
-
begin
-
end
-
oneshot
-
timer事件
[root@localhost systemtap]# stap -e 'probe timer.s(1) {print("1 second elapsed...\n")}' 1 second elapsed... 1 second elapsed... 1 second elapsed...
可以使用stap -l查看系统预设的Event。
-
查看内核函数
[vagrant@localhost openresty-1.21.4.1]$ sudo stap -l 'kernel.function("vfs_read")' kernel.function("vfs_read@fs/read_write.c:436") kernel.function("vfs_readlink@fs/namei.c:4598") kernel.function("vfs_readv@fs/read_write.c:833")
-
查看syscall
[vagrant@localhost openresty-1.21.4.1]$ sudo stap -l 'syscall.*read' syscall.pread syscall.read
-
查看trace point
[vagrant@localhost ~]$ sudo stap -l 'kernel.trace("*readpage")' kernel.trace("ext3:ext3_readpage") kernel.trace("ext4:ext4_readpage") kernel.trace("f2fs:f2fs_readpage")
-
查看某个用户态程序的函数
[vagrant@localhost ~]$ sudo stap -l 'process("/usr/local/openresty/nginx/sbin/nginx").function("ngx_http_write*")' process("/usr/local/openresty/nginx/sbin/nginx").function("ngx_http_write_filter@src/http/ngx_http_write_filter_module.c:48") process("/usr/local/openresty/nginx/sbin/nginx").function("ngx_http_write_filter_init@src/http/ngx_http_write_filter_module.c:357") process("/usr/local/openresty/nginx/sbin/nginx").function("ngx_http_write_request_body@src/http/ngx_http_request_body.c:529") process("/usr/local/openresty/nginx/sbin/nginx").function("ngx_http_writer@src/http/ngx_http_request.c:2826")
-
查看tapset中预设的probe
[vagrant@localhost ~]$ sudo stap -l 'netdev.change' netdev.change_mac netdev.change_mtu netdev.change_rx_flag
3.2 tapset
系统自带的tapset的stp文件在/usr/share/systemtap/tapset中,里面有自定义的probes(event)和函数。
预设的probe例子:
probe netdev.receive
= kernel.function("netif_receive_skb_internal") !,
kernel.function("netif_receive_skb")
{
try { dev_name = get_netdev_name($skb->dev) } catch { }
try { length = $skb->len } catch { }
try { protocol = $skb->protocol } catch { }
try { truesize = $skb->truesize } catch { }
}
定义了netdev.receive这个probe,它是内核函数netif_receive_skb_internal(如果存在),或者netif_receive_skb函数,并且里面还提前准备了输入参数:
- dev_name
- length
- protocol
- truesize
netdev.receive 定义中,!符号代表的是:netif_receive_skb_internal存在的时候,就使用这个函数。当netif_receive_skb_internal不存在的时候,就使用!后面的函数netif_receive_skb。
- 还有另外一个与!相似的marker: ?。 ?表示该探测点不在的时候,脚本运行也不要报错。
- if {expr} 的maker。signal.*? if (switch) 表示,当switch为true的时候,该探测点才生效。
function的例子:
function strlen:long(s:string)
%{ /* pure */ /* unprivileged */ /* unmodified-fnargs */
STAP_RETURN(strlen(STAP_ARG_s));
%}
函数strlen返回字符串的长度。
使用tapset的例子nettop.stp:
#! /usr/bin/env stap
global ifxmit, ifrecv
global ifmerged
probe netdev.transmit
{
ifxmit[pid(), dev_name, execname(), uid()] <<< length
ifmerged[pid(), dev_name, execname(), uid()] <<< 1
}
probe netdev.receive
{
ifrecv[pid(), dev_name, execname(), uid()] <<< length
ifmerged[pid(), dev_name, execname(), uid()] <<< 1
}
function print_activity()
{
printf("%5s %5s %-12s %7s %7s %7s %7s %-15s\n",
"PID", "UID", "DEV", "XMIT_PK", "RECV_PK",
"XMIT_KB", "RECV_KB", "COMMAND")
foreach ([pid, dev, exec, uid] in ifmerged-) {
n_xmit = @count(ifxmit[pid, dev, exec, uid])
n_recv = @count(ifrecv[pid, dev, exec, uid])
printf("%5d %5d %-12s %7d %7d %7d %7d %-15s\n",
pid, uid, dev, n_xmit, n_recv,
@sum(ifxmit[pid, dev, exec, uid])/1024,
@sum(ifrecv[pid, dev, exec, uid])/1024,
exec)
}
print("\n")
delete ifxmit
delete ifrecv
delete ifmerged
}
probe timer.ms(5000), end, error
{
print_activity()
}
此脚本使用了预设tapset的probe点:netdev.receive,统计使用网络的进程。
输出如下:
> [root@localhost systemtap]# stap nettop.stp
PID UID DEV XMIT_PK RECV_PK XMIT_KB RECV_KB COMMAND
0 0 eth0 0 28 0 1 swapper/3
3201 1000 eth0 14 0 1 0 sshd
14913 1000 eth0 5 0 0 0 ping
1983 1000 eth0 1 0 0 0 sshd
3.3 修饰符
probe函数的时候,我们常看到一些修饰符,比如.inline / .call / .return等。
- .return是probe函数运行返回的时候,在此probe的handler中,我们可以通过$return变量获取到函数的返回值
- .inline代表的,probe的是内联函数。
- .call是与.inline相反的
- .maxactive 修饰该探测点可以同时有多少个实例在运行,如果一个函数由于系统默认的maxactive过低导致没有被探测到,可以适当使用.maxactive调整
- ?表示该探测点不在的时候,脚本运行也不要报错
- ! 表示探测点满足了一个,就不再继续解析下去(resolve)
3.4 stap常用命令行参数
-
-x指定进程的PID进行追踪,target()函数返回的就是-x指定的
-
-c 运行一个命令,并且追踪这个命令
-
-e 从命令行中输入脚本
[root@localhost vagrant]# stap -v -e 'probe vfs.read{ printf("read performed"); exit()}'
Pass 1: parsed user script and 473 library scripts using 271956virt/69188res/3504shr/65752data kb, in 420usr/20sys/442real ms. Pass 2: analyzed script: 1 probe, 1 function, 7 embeds, 0 globals using 439512virt/233636res/4820shr/233308data kb, in 1360usr/460sys/1813real ms. Pass 3: using cached /root/.systemtap/cache/89/stap_89794dac39b59bfbd6e29e0bad1d4100_2803.c Pass 4: using cached /root/.systemtap/cache/89/stap_89794dac39b59bfbd6e29e0bad1d4100_2803.ko Pass 5: starting run. read performedPass 5: run completed in 0usr/40sys/595real ms.
-
-l :list probe points
[root@localhost vagrant]# stap -l 'syscall.write*' syscall.write syscall.writev
-
-L: 输出probe points的详细信息,包括支持的变量
[vagrant@localhost ~]skb:struct sk_buff*
netdev.receive可以使用的变量有:dev_name / length / protocol/ truesize / $skb
-
--dump-probe-type 获取stap支持的probe语法
[root@localhost systemtap]# stap --dump-probe-type begin begin(number) end end(number) error error(number) java(number).class(string).method(string) java(number).class(string).method(string).return java(string).class(string).method(string) java(string).class(string).method(string).return kernel.data(number).length(number).rw kernel.data(number).length(number).write kernel.data(number).rw kernel.data(number).write kernel.data(string).rw kernel.data(string).write kernel.function(number) kernel.function(number).call kernel.function(number).exported
.... 省略
-
--dump-probe-aliases 获取tapset中预设的probe aliases
[root@localhost systemtap]# stap --dump-probe-aliases|grep netdev netdev.change_mac = kernel.function("dev_set_mac_address")? netdev.change_mtu = kernel.function("dev_set_mtu") netdev.change_rx_flag = kernel.function("dev_change_rx_flags")? netdev.close = kernel.function("dev_close") netdev.get_stats = kernel.function("dev_get_stats")? netdev.hard_transmit = kernel.function("dev_hard_start_xmit")? netdev.ioctl = kernel.function("dev_ioctl") netdev.open = kernel.function("dev_open") netdev.receive = kernel.function("netif_receive_skb_internal")!, kernel.function("netif_receive_skb") netdev.register = kernel.function("register_netdevice"), kernel.function("register_netdev") netdev.rx = kernel.function("netif_rx") netdev.set_promiscuity = kernel.function("dev_set_promiscuity") netdev.transmit = kernel.function("__dev_queue_xmit")!, kernel.function("dev_queue_xmit") netdev.unregister = kernel.function("unregister_netdev")
-
--dump-functions 获取tapset中预设的函数
[root@localhost systemtap]# stap --dump-functions |grep user_string set_user_string:unknown (addr:long, val:string) /* guru / set_user_string_n:unknown (addr:long, n:long, val:string) / guru / user_string2:string (addr:long, err_msg:string) / unprivileged */ user_string2_n_warn:string (addr:long, n:long, warn_msg:string) user_string2_utf16:string (addr:long, err_msg:string)
3.5 systemtap内置函数
所谓的内置函数,也是在tapset中预设的。大部分的函数都是使用内嵌C代码的方式,完成其逻辑。
- pp() 当前probe point的名字
- target() , -x PID 或 -c CMD 指定的进程PID
- ctime(),
- cpu() 当前CPU的编号
- probefunc() 当前probe point所在的函数
相关的函数文档可以参考: sourceware.org/systemtap/t…
以probefunc为例子,其实现在linux/context-symbols.stp文件中。
3.6 联合数组
3.7 获取命令行输入参数
3.9 脚本中使用C代码
3.10 label和marker
3.9 跨机器编译
systemtap支持跨机器编译,实现在A机器上编译成内核模块,B机器上执行此内核模块,详细可参考:sourceware.org/systemtap/S…
4. 用户空间事件
5. 参考
- systemtap脚本例子:sourceware.org/systemtap/e…