上一篇文章,我们展示了几个常见的 probe 生成的 C 代码是怎么样的。本文则讨论 stp 的几种类型,两种变量,以及关联数组。
基本类型
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 < 4) { c->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 代码是怎么样的。