SystemTapでfizzbuzz
とりあえず、advent calenderに穴を開けたくないので、何か書こうと思っていろいろ考えてみたが、あまり手をかける余裕もないので、SystemTapでちょっとしたものを書くことにする。
/dev/zeroに対抗して/dev/0を作る
読み出したら zero fillならぬ0 fillをするデバイスを作る。とはいっても、デバイスファイルの登録とか面倒だし、SystemTapでちょっと小細工をする。
出来たのが以下。
global fillstr; global candidate; global readpos; %{ #include <linux/string.h> %} function basename:string (path:string) %{ const char *p, *s = (void *)(unsigned long)THIS->path; p = strrchr(s, '/'); if (!p) p = s; else p++; if (p) strlcpy(THIS->__retvalue, p, MAXSTRINGLEN); %} function write_string (buf:long, str:string, len:long, pos:long) %{ char *buf = (void *)(unsigned long)THIS->buf; int64_t len = THIS->len, slen = strlen(THIS->str); int64_t cnt, pos = THIS->pos; if (pos % slen) { memcpy(buf, (const char *)THIS->str + (pos % slen), slen - (pos % slen)); buf += slen - (pos % slen); len -= slen - (pos % slen); } for (cnt = 0; (cnt + 1) * slen < len; cnt++) strlcpy(buf + (cnt * slen), THIS->str, slen + 1); memcpy(buf + (cnt * slen), THIS->str, len - cnt * slen); %} probe syscall.open { candidate[pid()] = basename(filename) } probe kernel.function("memory_open") { fillstr[pid()] = candidate[pid()] } probe kernel.function("read_zero").return { if (fillstr[pid()] == "zero") next len = $return if (len > 0) { write_string($buf, fillstr[pid()], len, readpos[tid()]) readpos[tid()] += len; } }
使い方は、このスクリプトをGuruモードで走らせてから/dev/zeroから/dev/0へSymbolic linkを張るだけ。
stap -gF zerohook.stp ln -s /dev/zero /dev/0 less /dev/0
適当に長いbasenameでも使えるし、/devにある必要すらないので、ln -s /dev/zero hogehugaとかするとhogehuga fillされたデータが出てくる。
基本アイデアは、system callのopenをフックしてbasenameを取得、次に/dev/zeroの処理をフックして、渡されたバッファを先ほどのbasenameで埋め尽くすというもの。
工夫というほどでもないが、単にフックするだけだと他のプロセスやスレッドのopenと重なる場合があるので、tidごとに変数を分けるようにしている。
fizzbuzzっぽいもの
これだけだと面白くないので、fizzbuzzモードを追加した
global fillstr; global candidate; global readpos; %{ #include <linux/string.h> %} function fizzbuzz:string (num:long) { s = "" if (num % 3 == 0) s = "fizz" if (num % 5 == 0) s = s . "buzz" return s } function basename:string (path:string) %{ const char *p, *s = (void *)(unsigned long)THIS->path; p = strrchr(s, '/'); if (!p) p = s; else p++; if (p) strlcpy(THIS->__retvalue, p, MAXSTRINGLEN); %} function write_string (buf:long, str:string, len:long, pos:long) %{ char *buf = (void *)(unsigned long)THIS->buf; int64_t len = THIS->len, slen = strlen(THIS->str); int64_t cnt, pos = THIS->pos; if (pos % slen) { memcpy(buf, (const char *)THIS->str + (pos % slen), slen - (pos % slen)); buf += slen - (pos % slen); len -= slen - (pos % slen); } for (cnt = 0; (cnt + 1) * slen < len; cnt++) strlcpy(buf + (cnt * slen), THIS->str, slen + 1); memcpy(buf + (cnt * slen), THIS->str, len - cnt * slen); %} probe syscall.open { candidate[pid()] = basename(filename) } probe kernel.function("memory_open") { fillstr[pid()] = candidate[pid()] } probe kernel.function("read_zero").return { if (fillstr[pid()] == "zero") next len = $return str = fillstr[pid()] if (str == "fizzbuzz") { str = fizzbuzz(pid()); printf("fizzbuzz: %d, %s\n", pid(), str) if (str == "") next } if (len > 0) { write_string($buf, str, len, readpos[tid()]) readpos[tid()] += len; } }
リンク先のファイル名がfizzbuzzの場合、fizzbuzzモードに入る。fizzbuzzモードでは、PIDが3の倍数ならfizz fill、5の倍数ならbuzz fill、15の倍数ならfizzbuzz fill、それ以外はzero fillするというもの。
ちなみにSystemTapでは"."が文字列結合演算になるが、これを利用している例というのはほとんど見たことがないぐらいマイナーだ。たぶん、初めにこの演算を定義した本人も悔やんでいると思う。というのは、大体の場合、この演算子は以下のように間違って使われてエラーになり、ユーザが迷惑する仕様になっている。
var = $struct.member
ただしくは、以下のようにどのような場合でもアロー演算子を使うのがSystemTap流。
var = $struct->member
perf probeではこの教訓を生かし、Cと同じく.と->を分けている。何事も他山の石だと思う。
短いがこのあたりでさらば。