上一篇文章,我們展示了幾個常見的 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 程式碼是怎麼樣的。