systemtap 探秘(三)- 类型、变量和数组
基本类型
stp 有三种基本类型:
- long
- string
- stats
long 类型虽然叫做 long
,但其实是 int64_t
的别名。所以即使在 32 位系统上,它还是 64 位整数。
string 类型的变量会被编译成 string_t
。而 string_t
只是 char[MAXSTRINGLEN]
的别名。由于大小是固定的,且没有存储 string 的真正长度,stp 里面的 string 有两种引人注目的特性:
-
如果实际数据长于
MAXSTRINGLEN
,会被截断。当然你能通过-DMAXSTRINGLEN
增大它。注意调高该项会增加内核的内存分配,不过一般不会有人一口气加几个零在后面,所以应该不至于出现耗尽内存的情况。 -
如果数据中存在
\0
,会被截断。比如下面的 stp 脚本,只会输出前三个字母abc
:
probe oneshot { a = "abc\0a" println(a) }
stats 类型的变量会被编译成 struct Stat
。 <<<
运算符会被编译成 _stp_stat_add
,而 @xxx(stat)
会被编译成类似于 _stp_stat_get(stat)->xxx
的代码。
为了让 stats 成为适合统计的类型,systemtap 做了一些优化:
-
struct Stat
里面的统计数据是 per CPU 的,所以计算时不需要加锁 -
每次加入新数据时,stats 类型都会进行计算。这样执行
@xxx(stat)
时只是纯粹地归总数据,不用重新计算。同时也不需要花费大量空间存储待计算的数据。 -
只计算用得到的部分。比如某个变量上只有
@count(stat)
和@max(stat)
操作,那么每次添加新数据时只会做加一和取两者中最大的操作。
本地和全局变量
在上一篇文章,我提到了 context
参数里有一个用于存储各个 probe 本地变量的 probe_xxx_locals
结构体。下面我们会通过一个例子详细看下这种结构体:
probe timer.ms(123) { i = 1 s = "abc" println(i) println(s) } probe timer.s(1) { a = "xyz" println(a) exit() }
生成的对应的 struct probe_xxx_locals
摘录在下:
struct context { #include "common_probe_context.h" union { struct probe_3964_locals { int64_t l_i; string_t l_s; union { /* block_statement: test.stp:1 */ struct { /* source: test.stp:4 */ int64_t __tmp4; }; struct { /* source: test.stp:5 */ string_t __tmp6; }; }; } probe_3964; struct probe_3965_locals { string_t l_a; union { /* block_statement: test.stp:8 */ struct { /* source: test.stp:10 */ string_t __tmp2; }; }; } probe_3965; } probe_locals; .... };
还记得上一篇文章中提到的,context 是在执行 probe 前后被复用的吗?由于一个 context 同时只能处于一个 probe 中,所以这里用 union 来把内存占用减少到最大的 probe 所用到的变量数。我们还可以看到,每个本地变量被编译成对应的 l_xxx
了。
接下来看看具体的 probe 代码中是怎么访问它们的:
// 因为贴出来的代码较长,所以我直接以注释的方式阐述它们。 // 对于用到的变量,systemtap 会进行初始化 l->l_i = 0; l->l_s[0] = '\0'; if (c->actionremaining last_error = "MAXACTION exceeded"; goto out; } { (void) ({ l->l_i = ((int64_t)1LL); ((int64_t)1LL); }); (void) ({ strlcpy (l->l_s, "abc", MAXSTRINGLEN); "abc"; }); // 由于每个语句用到的临时变量是不会互相影响的,所以 systemtap 也用 union 把 // 它们括起来,让整个本地变量结构体的大小只取决于本地变量的总和 + // 使用临时变量总大小最大的语句的临时变量大小 (void) ({ // systemtap 对临时变量的运用还是有优化空间的…… l->__tmp4 = l->l_i; #ifndef STP_LEGACY_PRINT c->printf_locals.stp_printf_1.arg0 = l->__tmp4; stp_printf_1 (c); #else // STP_LEGACY_PRINT _stp_printf ("%lld\n", l->__tmp4); #endif // STP_LEGACY_PRINT if (unlikely(c->last_error)) goto out; ((int64_t)0LL); }); (void) ({ strlcpy (l->__tmp6, l->l_s, MAXSTRINGLEN); #ifndef STP_LEGACY_PRINT c->printf_locals.stp_printf_2.arg0 = l->__tmp6; stp_printf_2 (c); #else // STP_LEGACY_PRINT _stp_printf ("%s\n", l->__tmp6); #endif // STP_LEGACY_PRINT if (unlikely(c->last_error)) goto out; ((int64_t)0LL); });
看完本地变量,我们再来看看一个全局变量的例子:
global a probe oneshot { a <<< 1 a <<< 2 a <<< 3 println(@count(a)) }
stats 类型只能用于全局变量,所以我们干脆拿它作为全局变量的范例好了。编译出来的结果是这样的:
// 跟本地变量是 probe 的参数的一部分不同,global 变量有自己独立的结构体 struct stp_globals { // 全局变量被加上了 s___global_ 前缀 Stat s___global_a; rwlock_t s___global_a_lock; #ifdef STP_TIMING atomic_t s___global_a_lock_skip_count; atomic_t s___global_a_lock_contention_count; #endif }; // 这里的 stp_global 是一个 stub,这个名字是固定的 static struct stp_globals stp_global = { }; ... // 访问全局变量时,通过 global 宏来访问。这个宏定义在 runtime/linux/common_session_state.h // 其实就是 #define global(name) (stp_global.name) (void) ({ _stp_stat_add (global(s___global_a), ((int64_t)1LL), 2, 0, 0, 0, 0); ((int64_t)1LL); }); ... // 这段代码是从 systemtap_module_init 里复制出来的。全局变量在这里初始化 // global_xxx 宏都是定义在 runtime/linux/common_session_state.h 的 global_set(s___global_a, _stp_stat_init (STAT_OP_COUNT, KEY_HIST_TYPE, HIST_NONE, NULL)); if (global(s___global_a) == NULL) rc = -ENOMEM; if (rc) { _stp_error ("global variable '__global_a' allocation failed"); goto out; } global_lock_init(s___global_a); #ifdef STP_TIMING atomic_set(global_skipped(s___global_a), 0); atomic_set(global_contended(s___global_a), 0); #endif
眼尖的读者会发现,虽然 struct stp_globals
里面定义了 lock,但是代码里没有加锁。这是为什么呢?
因为锁被优化掉了。
对于 stats 类型而言,因为数据是 per CPU 的,所以没有加锁的必要。另外 probe oneshot
只在 begin 阶段执行一次,所以不可能出现并发访问。
换个例子就能看到加锁操作了:
global b probe timer.ms(1) { b .= "xyz" } probe timer.s(1) { b .= "abc" }
生成的加锁代码如下:
static const struct stp_probe_lock locks[] = { { .lock = global_lock(s___global_b), .write_p = 1, #ifdef STP_TIMING .skipped = global_skipped(s___global_b), .contention = global_contended(s___global_b), #endif }, }; ... if (!stp_lock_probe(locks, ARRAY_SIZE(locks))) return;
关联数组
在本文的最后,我们来看下关联数据对应的 C 代码是怎么样。
global a global i probe timer.ms(1) { a[i] = i i++ }
生成的代码是这样的:
struct stp_globals { MAP s___global_a; ... (void) ({ l->__tmp0 = global(s___global_i); l->__tmp1 = global(s___global_i); c->last_stmt = "identifier 'a' at test.stp:5:5"; l->__tmp2 = l->__tmp1; { int rc = _stp_map_set_ii (global(s___global_a), l->__tmp0, l->__tmp2); if (unlikely(rc)) { c->last_error = "Array overflow, check MAXMAPENTRIES"; goto out; }}; l->__tmp1; });
我们可以看到,生成了一个 Map 类型的 s___global_a
。既然是关联数组嘛,必然是用 Map 伪造的数组。
在本系列的开篇,我曾提到过 stp 的数组大小取决于 MAXMAPENTRIES,是预先分配的。不同于其他语言只给 map 预分配少量内存,超过负载之后才扩大容量的做法,stp 是预先分配可容纳 MAXMAPENTRIES 的内存。所以如果 MAXMAPENTRIES 设置得过大,会导致内核占用许多内存,甚至会导致 kernel panic。
修改这个 Map 的方法叫 _stp_map_set_ii
。这个函数是在 runtime/map-gen.c
里面用宏生成出来的。对应的模板是
static int KEYSYM(_stp_map_set) (MAP map, ALLKEYSD(key), VSTYPE val)
_ii
后缀表示 key 为 long 且 value 为 long。如果是 _sx
则表示 key 为 string 且 value 为 stats。以此类推。
另外,由于关联数组的类型是在 C 代码里面固定下来的,同一个关联数组的 key 和 value 只能是固定的类型。
比如像这样的 stp 代码会导致编译失败:
global a global i probe timer.ms(1) { if (i % 2 == 0) { a[i] = i } else { a[i] = "a" } i++ }
错误信息为:
semantic error: type mismatch (long): identifier 'a' at test.stp:6:9 source: a[i] = i ^ semantic error: type was first inferred here (string): identifier 'a' at :8:9 source: a[i] = "a"
跟大多数语言不同,stp 的关联数组支持多维 key。我们接下来看看多维 key 数组的一个例子:
global a global i probe timer.ms(1) { a[i, i * 2, i * 3, i * 4] = "a" i++ }
生成的方法为 _stp_map_set_iiiis
,四个 long key 和一个 string value。同样,同一个关联数组的 key 个数是固定的。
预告
在下一篇文章,我们会开始看看某些 stp 语句对应的 C 代码是怎么样的。