systemtap 探秘(三)- 類型、變量和數組

7

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

你可能感興趣的

載入中...