上一篇文章,我们展示了几个常见的 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 代码是怎么样的。

09-14 06:07