systemtap 探祕(三)- 型別、變數和陣列

spacewander發表於2019-09-13

上一篇文章,我們展示了幾個常見的 probe 生成的 C 程式碼是怎麼樣的。本文則討論 stp 的幾種型別,兩種變數,以及關聯陣列。

基本型別

stp 有三種基本型別:

  • long
  • string
  • stats

long 型別雖然叫做 long,但其實是 int64_t 的別名。所以即使在 32 位系統上,它還是 64 位整數。

string 型別的變數會被編譯成 string_t。而 string_t 只是 char[MAXSTRINGLEN] 的別名。由於大小是固定的,且沒有儲存 string 的真正長度,stp 裡面的 string 有兩種引人注目的特性:

  1. 如果實際資料長於 MAXSTRINGLEN,會被截斷。當然你能通過 -DMAXSTRINGLEN 增大它。注意調高該項會增加核心的記憶體分配,不過一般不會有人一口氣加幾個零在後面,所以應該不至於出現耗盡記憶體的情況。
  2. 如果資料中存在 \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 做了一些優化:

  1. struct Stat 裡面的統計資料是 per CPU 的,所以計算時不需要加鎖
  2. 每次加入新資料時,stats 型別都會進行計算。這樣執行 @xxx(stat) 時只是純粹地歸總資料,不用重新計算。同時也不需要花費大量空間儲存待計算的資料。
  3. 只計算用得到的部分。比如某個變數上只有 @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 程式碼是怎麼樣的。

相關文章