virshのvcpupinとvcpuinfoで表示されるaffinityの違い

以前virsh vcpupinvirsh vcpuinfo で表示されるCPUのAffinity情報が違うということがありました.

% sudo virsh vcpupin vm1 
VCPU   CPU Affinity
----------------------
 0      0-47
 1      0-47
 2      0-47
[...]
% sudo virsh vcpuinfo vm1
VCPU:           0
CPU:            25
State:          running
CPU time:       508.0s
CPU Affinity:   ------------------------yyyyyyyyyyyyyyyyyyyyyyyy

動作的には virsh vcpuinfo の情報の方が正しく,なんでかなぁと思ってたんですが,ソースを調べてみたら virsh vcpuinfo は呼ばれるたびに sched_getaffinity() でアフィニティを取得するのに対して, virsh vcpupin の方はlibvirt内部の情報を参照しているようでした.結果としてlibvirtの外部からCPUアフィニティが変更されるとこのように情報に差異が生じるようです.

実際にlibvirtdをstraceするか,bpftraceなどで sched_getaffinity() の呼び出しを監視すると, virsh cpuinfo コマンドを実行すると shced_getaffinity() が実行されるのに対して,もう一方はされないことが確認できます(なお,virshコマンドはlibvirtdとRPCでやりとりしていますので,virshコマンドの方を対象に監視しても結果は得られません).

ということでlibvirtを使っているときゲストのCPU affinityを知りたいなら virsh vcpuinfo を使った方が良いという話でした(※こちら2020年10月ごろのlibvirtdをコンパイルして使ったときの情報になります).

bpftrace 2020

(この記事はLinux Advent Calendar 2020 - Qiitaの2日目の記事です.前日はmasami256さんのTiny Core LinuxでLinuxのinitプロセスが実行されるあたりを調べるでした.)


みなさんこんにちはこんばんは.bpftraceを使っていますか? (e)BPFといえば最近は専らCiliumなどネットワークが話題ですが,bpftraceはBPF*1を利用したトレーシングツールです.私は昨年からぼちぼち使い始めて,機能追加やバグ修正のパッチをいろいろと投げていた(130+)ら気づいたらコミッタになっていました.ということでbpftraceについては比較的よく知っていると思うので,今年にbpftraceに追加された主要な機能や変更点を紹介したいと思います.

bpftrace知らないなぁという方はこちら(拙文です)を,bpftrace使ってないなぁという方は折角ですから最後の小話だけでも読んでみてはいかがでしょうか.また最近bpftraceをmacから簡単に試せるものを作ったので,macユーザでbpftraceに興味のある方はそちらもどうぞ(勿論M1は対象外です..).

目次

本題に入る前に

bpftraceはこの一年間で多くの機能追加,バグ修正がおこなわれました.bpftraceをお使いの方は今一度アップデートの確認をおすすめします.もし以下で紹介する機能が使えない場合,bpftraceのバージョンが古い可能性が高いです.また一部機能はカーネルバージョン(主にカーネル5.6以上)や,libbpfと一緒にbpftraceをコンパイルする必要があります(特にBTFがそれに該当します).

bpftraceのバージョンやサポートしている機能の確認

bpftraceのバージョンや,サポートしている機能は,bpftrace --info コマンドから確認できます.

$ sudo bpftrace --info
System
  OS: Linux 5.9.0+ #26 SMP Mon Nov 16 11:50:54 JST 2020
  Arch: x86_64

Build
  version: v0.11.0-287-gc62f
  LLVM: 10.0.1
  foreach_sym: yes
  unsafe uprobe: no
  bfd: yes
  bpf_attach_kfunc: yes
  bcc_usdt_addsem: yes
  bcc bpf_attach_uprobe refcount: no
  libbpf: yes
  libbpf btf dump: yes
  libbpf btf dump type decl: yes

Kernel helpers
  probe_read: yes
  probe_read_str: yes
  probe_read_user: yes
  probe_read_user_str: yes
  probe_read_kernel: yes
  probe_read_kernel_str: yes
  get_current_cgroup_id: yes
  send_signal: yes
  override_return: yes
  get_boot_ns: yes
  dpath: no

Kernel features
  Instruction limit: 1000000
  Loop support: yes
  btf (depends on Build:libbpf): yes
  map batch (depends on Build:libbpf): yes
  uprobe refcount (depends on Build:bcc bpf_attach_uprobe refcount): no

Map types
  hash: yes
  percpu hash: yes
  array: yes
  percpu array: yes
  stack_trace: yes
  perf_event_array: yes

Probe types
  kprobe: yes
  tracepoint: yes
  perf_event: yes
  kfunc: yes

BTFサポートの強化

それでは早速追加された主要な機能を振り返っていきたいと思います.今年はなんといってもBTF (BPF Type Format) を利用した機能がいろいろと追加されました.以下ではどんなことができるか簡単に紹介していきます.

そもそもBTFとは

BTFにはいろいろな用途があるため,簡単に説明するのは難しいのですが,bpftraceの文脈でいうと/proc/kallsyms が現在動いているカーネルシンボルのアドレスを提供するのと同じように,BTFは現在動いているカーネルが利用している関数やデータ構造の情報を提供します.例えば,BTFを参照すればそのカーネルが利用している,プロセスの状態を保持する struct task_struct の構造を知ることができ,どのメンバにアクセスするにはどれだけのオフセットが必要かが分かります.他には例えば,vfs_open() 関数の引数の数や引数の型が分かります.これで何が嬉しいの? と思うかもしれませんが,カーネル内の関数や構造体はunstableで常に変化します.カーネルトレーシングをする場合,多くの場合はカーネルの関数や構造体にアクセスするため,関数や構造体が変化してしまうと折角書いたトレーシングスクリプトが特定のカーネルでしか動かないということになります.BTFによりそのカーネルが利用している関数やカーネルが分かれば,その情報を利用してスクリプトを補正することで,複数カーネルで動作するトレーシングスクリプトが動作できるようになるというわけです.鋭い方はそれってDWARFでできるのでは?と思うことでしょう.勿論DWARFでも可能ですが,BTFはトレーシング用途に特化してプロダクション環境でも利用できることを一つの目標にしているため,サイズが非常に小さいです(数MB程度).ただしその代わりDWARFと比べて当然情報量が少ないため,できることは必然的に限られます.

bpftraceは実行時にトレーシングスクリプトコンパイルする形です.BTFが利用可能な場合は,bpftraceはコンパイル時にBTF情報を利用して,適切なオフセットでデータにアクセスするBPFプログラムを生成します(もしBTFが利用できない場合は,includeしたヘッダに含まれる定義情報を利用します).一方で,実行前にトレースプログラムをあるカーネルバージョン上でコンパイルしておき,BTFを利用してそのトレースプログラムをロードする前に,データ構造アクセスのオフセットの修正をおこなうという方法もあります.こちらは BPF CO-REと呼ばれる機能で,こちらはこちらで非常に重要で積極的に開発が進められていますが(特にiovisor/bccの方で),bpftraceとは直接は関係ありません.

BTFカーネルの関数や構造体の情報を提供するだけのものではありません.BTFについてより詳しくはこの辺り(拙文です)カーネルのドキュメントをご参照ください.

BTFを使用するには

BTFの機能を利用するには,CONFIG_DEBUG_INFO_BTF を有効にしたカーネルが必要です.またカーネル5.6以上でないと以下で説明するkfuncの機能は使えません.bpftrace自体もlibbpfと一緒にコンパイルする必要があります. 

bpftrace --infoBTFの機能が利用できるか確認できます.特に,

  • BTFを使いたい場合は "Kernel features" のbtf が yes
  • kfuncを使いたい場合は Probe typesの kfunc がyes

である必要あります.

BTFでできること

それではBTFでできることをみていきましょう.

カーネル構造体の自動解決

例として,vfs_open(struct path*, strust file *) 関数の呼び出しをフックして,vfs_openが呼ばれた時のpath のファイル名を呼び出すスクリプトを考えます(これはbptraceのreference guideからの引用です).BTFが無い以前は,スクリプトが利用する struct path や. struct dentry が定義してあるヘッダをインクルードする必要がありました.

#include <linux/path.h>
#include <linux/dcache.h>

kprobe:vfs_open
{
    printf("open path: %s\n", str(((struct path *)arg0)->dentry->d_name.name));
}

もしBTFが使用できる環境であれば,以下のようにヘッダをインクルードしなくても,スクリプト側で利用するカーネル構造体が,特に何もしなくても利用できるようになります.

kprobe:vfs_open
{
    printf("open path: %s\n", str(((struct path *)arg0)->dentry->d_name.name));
}

内部的にはまずBTFから struct path の情報を取ってきて,そこからスクリプトがアクセスする構造体情報を集めてきてそれを利用しています.

kfuncの利用

上記のスクリプト例ではkprobeを利用していました.kprobeは基本的にカーネル内のどのアドレスにもアタッチできるの非常に便利ですが,一方でbpftraceでは関数にアタッチしたとき,自分で引数をキャストする必要(上記の例だと (struct path *)arg0) がありました.)カーネル5.6から,kfuncという,kprobeとはまた違うトレース方法が追加されました.kfuncはkprobeと違い関数のentryまたはexitにしかアタッチできませんが,BTFの情報を利用することで,以下のように構造体のキャストなしに関数の引数にアクセスできます.

kfunc:vfs_open
{
    printf("open path: %s\n", str(args->path->dentry->d_name.name));
}

ポイントは args->path で,これにより vfs_open() の引数 path にアクセスしています.何故これができるかというと,BTFには関数の情報も入っているので,まず vfs_open の関数のプロトタイプ情報をとてきて,それから args->pathvfs_open のpathにアクセスしてることが分かるので,そこからスクリプトが生成できるというわけです. またkfuncの場合kprobeと違って引数が自動でキャストされるので,間違ったキャストをしてしまうというミスもなくなります.

カーネル関数の引数や構造体の確認

ややおまけ的機能ですが,以下のようにしてBTFを利用して関数のプロトタイプや,構造体のメンバが確認できます.

% sudo bpftrace -lv "kfunc:vfs_open"
kfunc:vfs_open
    const struct path * path;
    struct file * file;
% sudo ./src/bpftrace -lv "struct path"
struct path {
        struct vfsmount *mnt;
        struct dentry *dentry;
};

この構造体のメンバなんだっけ?となったときカーネルソースを確認する必要がなくなります.

現状のBTFの制約

BTFは非常に便利ですが,以下のような制約・課題があります.

  • 現状BTFに含まれる関数はnon-statitcなもののみ (kprobeはstaticな関数にも対応しています)
  • カーネルモジュールに未対応 (もしカーネルモジュールをBTFでトレーシングしたい場合は,カーネル側にモジュールえはなく組み込んでビルドする必要があります)
  • define 値の欠如

この辺りの問題はいろいろと解決に向けて議論がされています.特に最後のdefineされている値が欠如されているというのは地味にトレースプログラムを書く時に障害になります.例えば AF_INET の値を知りたいときは,BTFがあってもそれがdefineされているヘッダをインクルードする必要があるわけです.解決策の一つは#defineではなくenumを使うことで (enumの情報はBTFに含まれます),実は一部ヘッダはBTFに対応できるようにそのような変更が加えられています.将来的にはこの辺りの問題が解決されると嬉しいですね.

Docker ビルド・スタティックビルドの提供

x86_64用ですが,ホストをトレースするためのDockerビルドがあります.以下のようにして利用できます.

% sudo docker run -it --privileged -v /sys/kernel/debug:/sys/kernel/debug:rw -v /lib/modules/:/lib/modules:ro  -v /usr/src:/usr/src:ro quay.io/iovisor/bpftrace tcpconnect.bt

適切にdebugfsやカーネルヘッダの場所をコンテナ側にマウントする必要がありますが,docker buildを使うとbpftraceの最新版が使えて便利です.ただ残念ながら現状BTFのサポート(というかlibbpfとのビルド)はありません.

また,github actionによりスタティックビルドが提供されるようになっています.Releaseからリリースバージョンのスタティックビルドが取得できる他,やや分かりにくいですがgithubアクションのembedded buildの項目から最新のスタティックビルドが取得できます.

知っておくと便利な機能

タプル

以下のようにタプルが使えるようになりました.わざわざ複数マップを作らなくても値が格納できます.

% bpftrace -e 'BEGIN { @ = (0, 1, "abc"); printf("%d\n", @.0); print(@); }'
Attaching 1 probe...
0
@: (0, 1, abc)

マップの複数キーの利用

別にこれ自体は前からあった機能なのですが,いまいち認知されていないようなので書いておきます.以下のように @[] に使うキーは複数個指定できます.

% sudo ./share/bpftrace -e 'BEGIN { @[pid, cpu] = 1; print(@); }'
Attaching 1 probe...
@[10189, 35]: 1

これを使うと条件(例えばCPU番号)に応じた集計などが簡単にできます.

バイナリ値のダンプ

buf() を使うと,ポインタ先のデータをダンプができます.似たような関数に str() がありますが, str() はNULL終端された文字列を扱う点が異なります.

% bpftrace -e 'tracepoint:syscalls:sys_enter_sendto
    { printf("Datagram bytes: %r\n", buf(args->buff, args->len)); }' -c 'ping 8.8.8.8 -c1'
Attaching 1 probe...
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
Datagram bytes: \x08\x00@\xc3\x05\xb6\x00\x01\xe9\x0aW_\x00\x00\x00\x00\xb0H\x02\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x1
6\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&'()*+,-./01234567
^C
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

エラーチェック

-k あるいは -kk オプションでBPFプログラム実行中のエラーをチェックするようになります.

% bpftrace -kk -e 'BEGIN { @[0] = 1; @[1]++; }'
Attaching 1 probe...
stdin:1:19-25: WARNING: Failed to map_lookup_elem: 0
BEGIN { @[0] = 0; @[1]++;}
                  ~~~~~~

もしbpftraceのスクリプトが期待したとおりに動かない場合,とりあえず -kk をつけて実行してみると良いと思います.

この機能について補足すると,BPFプログラムにはヘルパー関数と呼ばれる関数を利用して,カーネルの処理を呼び出す機能があります.これはいわばユーザプログラムにおけるシステムコールのようなもので,BPFマップの操作や出力処理などの機能は全てこのヘルパー関数によって成り立っています.bpftraceを利用する場合は特にこのヘルパー関数を意識する必要はないですが,重要なのはヘルパー関数の処理は何らかの理由によって失敗することがあるということです.このとき,ヘルパー関数は戻り値としてエラーを返しますが,デフォルトではこのエラー値は無視されます.これをチェックするのが -k および -kk オプションです. -k-kk の違いは,チェックするヘルパー関数の数です.bpftraceはデータの読み出しをおこなうヘルパー関数が失敗すると,値を0として返すという仕様になっています.これによって,例えば / @[tid] / のように書くと, @[tid] が存在しない場合はその処理がスキップできることになります.このようなテクニックはbpftraceのスクリプトで広く使われていて,このエラーをチェックしてしまうと冗長な場合があるため, -k オプションではこのようなデータ読み出しの関数に関してエラーはチェックしません. -kk だと全てのヘルパー関数のエラーをチェックします.一つ注意点として,エラーチェック機能を有効にするとBPFプログラムがその分長くなります.場合によってはそのオーバヘッドが問題になる可能性があります.また,ロードできるBPFプログラム長には制限があるため,大きなbpftraceスクリプトではエラーチェック機能が利用できない可能性があります.

whileループ

もともとBPFプログラムは停止性保証のためにループ(後方ジャンプ)が一切許可されていませんでしたが,verifierが賢くなって有限回で停止するループ処理が書けるようになりました.カーネル5.3以上が必要です.bpftraceでは while() を使います.

% bpftrace -e 'i:ms:100 { $i = 0; while ($i <= 100) { printf("%d ", $i); $i++} exit(); }'

ここで,verifierは実際にループ処理を一つ一つ解釈していき,そのループが停止するかをみています.従って,極端に大きなループはverifierの命令解釈数上限に引っかかるため実行できません.例えば,以下のプログラムはエラーになります.

$i = 0; while ($i <= rand) { printf("%d ", $i); $i++} exit(); }

これは rand はuint64の値を返す関数で,verifierはその値の最大を想定してverificationをおこなうためです.以下のように,明示的にループ上限を定めてあげるとプログラムが実行できるようになります.

$i = 0; $max = rand; if ($max > 100) { $max = 100;}
while ($i <= $max) { printf("%d ", $i); $i++} exit();}

その他主要な変更・更新点

s390x, aarch64のサポート

ビッグエンディアンアーキテクチャに存在した一部問題が修正されました.またaarch64などのアーキテクチャでは,カーネルモードとユーザモードのアドレススペースが異なることに起因して,それぞれのデータの読み出しに専用のヘルパー関数*2を利用する必要があり,しばらくの間bpftraceはこれをサポートしていませんでしたが,つい最近この機能がサポートされました.結果としてaarch64でも問題なくbpftraceが使えるようになっているはずです.

日本語版ドキュメント

ワンライナーチュートリアルには日本語訳(拙訳です)があります.Reference Guideは英語のみです.

Discourse フォーラム

Discourceにbpftraceのフォーラムができました.質問はここに投げると良いと思います.開発者の方が見ています.

より詳しく知りたい方

実際には他にもいろいろと機能が追加されています.詳しくは CHANGELOG をどうぞ.

バグ情報

最後に,現在のbpftraceには printf()したときに間違った値がごくまれに表示される という割と致命的なバグがあります.長い間原因が分からず開発者を困らせていまいしたが,どうやらLLVMのバックエンドに問題があるということが分かりました(詳しくは #1305, Bugzilla – Bug 47591 をどうぞ).2020年12月現在このバグは修正されておらず,現状これに対応する完璧な方法はないのですが,printf()の位置を変えたりすると結果的にBPFのコードが変わるので問題が解決する場合があります.上記で紹介した -k あるいは -kk オプションも有効です.もしbpftraceを使っていて明らかに出力がおかしい場合はこのバグを疑ってみてください.ちなみに本当にこのバグかどうかはBPFプログラムのアセンブラコードを眺めてみると分かります.不自然にloadとstore命令が入れ替わっている箇所があるはずです.

2020-12-4 追記

このバグですが,LLVM12で修正されたそうです.ただしLLVM12が実際に利用されるようになるのは数年かかるでしょうし,bpftrace側でもなんとか修正できないかいろいろと検討しています.

追記ここまで

おまけ

bpftaceによるBitVisorのトレース

先日BitVisorをbpftraceトレースするという内容の発表をBitVisor Summit 9でおこないました.言ってる意味が不明かもしれませんが,興味のある方はご笑覧ください ⇒ スライド

macからbpftraceを試す

最初に書きましたが,Intel macからbpftraceを簡単に試せるものを作りました.詳しくはこちらをどうぞ

システム系論文の情報収集方法

あまりBPFと関係なくて恐縮ですが,昨日システム系論文の情報収集方法というエントリを書きました.学術的にもBPFを使った論文は結構出ているので,気になる方はチェックしてみてください.

最後に-- bpftraceに関わってきて

最後に折角の機会なので一年間bpftraceに関わってきた感想を書いてみようと思います.まず.bpftraceはもともとAlastair Robertsonさん一人によって2017年から開発されたプロジェクトです(すごい).その後,Brendan Greggさんにより多くのツールが追加され,さらにNetflixのエンジニアの方によって基本的な機能の多くが追加されました.だいたい去年の春先ごろのことだと思います.書籍"BPF Performance Tools" (Brendan Gregg箸)はその辺のバージョンのbpftraceをもとに書かれています.そのころと比べて,bpftraceは結構進化していると思います.現在は前述の3人はコミットに関しては控えめで,今は私を含めた別の3人が主にプロジェクトに関わっています.特にその中の一人のFBのエンジニアの方がメインのメンテナといった感じです.その他Red HatIBMの方からも定期的にコントリビューションがあります.

これまでに私がコミットした主な内容には以下があります.

  • 整数型ポインタのキャストのサポート#942
  • kprobe offset に対するアタッチ機能の追加 #956
  • BPFコンテキストアクセスの修正 #1104
  • bpftrace oneliner ドキュメント翻訳 #1176, 訳文
  • bpf helper functionエラーのレポート機能 #1276
  • fuzzingのサポート #1601, #1633, ドキュメント
  • その他のバグ修正など

一番最初のPRは整数型ポインタのキャストのサポート ((int8*)$var みたいなやつ) でした.これはbpftraceを使い始めてすぐにこの機能がなくて不便だったので,なんとなくソースを見て修正してPRを送ったものです.C++なんて3年以上まともに書いてなかったし,そもそもC++は理解してないし,このとき多分1割ぐらいしかbpftraceのことを分かってなかったんですが,少しフィードバックもらったあとにマージされたのがはじまりでした.bpftraceは使っていくほどいろいろと問題に遭遇したので,息抜きとして空いてる時間にそれを直して行ったら今の状態になったというのが実情です.ここまで関わったOSSプロジェクトはbpftraceが初めてですが,やはり世界中の人と作業をしていくというのはOSSの醍醐味で,とても楽しい経験です.作者の方に"good work!"と言われるとやはり嬉しいものです.

bpftraceにコミットすることでBPFに関してより詳しくなったのは勿論のこと,もともとも私はLinuxのトレーシング手法についていろいろと調査して使ってきましたが,具体的にそれらが利用されるかについての理解が深まりました.最近改めて"BPF Performance Tools"を読み返してみたのですが,ついに真に理解できるようになったような気がしています.

またbpftrace自体はflex/bisonによる字句解析,構文解析をおこなった後,LLVMによりIRを出力するコンパイラです.よくCPU, OS, コンパイラを自作すると良いなんて話がありますが,私は簡単なCPUやOSは作ったことがあったものの*3コンパイラは真面目に取り組んだことがなかったので,その辺もいろいろと勉強になりました.(と同時にLLVMの難しさも知ることに...)bpftraceはコンパクトにいろいろとまとまってると思うので,これらについて興味のある方はコードを見てみると楽しいのではないかなと思います.

来年の抱負

来年は今年と同様,本業が一番なのは勿論ですが,bpftraceに関するコミットは継続しつつ,カーネルの方に対してもコミットしていけたら...なんて思います.FBのBPFやってるエンジニアはカーネルがいじれるのは勿論のこと,LLVM(本体の方)もできるのでみんなすごいなぁと思う次第です.まだまだ自分も頑張らないといけませんね.

それではみなさん,ちょっと早いですがよいお年を.

*1:以後,BPFと言えばeBPFを指します

*2: bpf_probe_read_kern, bpf_probe_read_user

*3:といっても自作したCPUで自作したOSを動かした訳ではないのでその辺りはまだまだですね

システム系論文の情報収集方法

(この記事は東京大学 品川研究室 Advent Calendar 2020の1日目の記事として書かれたものです)

この記事では簡単にシステム系の会議の論文に関してどうやって論文を探して読むのか.個人的な方法を書いてみようと思います.ここに書いてあることが別にすべてではないので,是非自分に合う方法を各自探してみてください.

目次

会議を知る

会議にはランク(被引用数などで決めたスコアによる順位付け)があります.一般にはランクの高い会議 = レベルが高い会議ということになります.論文を探すとき,このランクは一つの判断基準となります.ランクの高い会議である方が,良い論文がある可能性が高いためです.情報系のトップの会議については,例えば,以下のサイトにまとまっています.

ただこれは画像処理や機械学習など,いろいろなジャンルを含むので,システム系に関しては以下のものを参考にするとよいと思います.

また,セキュリティ系の会議のランクは以下にまとまっています.

ランクではないですが,クラウド系の会議の情報は以下にまとまっています.

会議のランク付け方法としては,CORE, Qualis, MSAR といったものがあります.これらは conferenceranks というサイトから調べることができます.ただしあくまで情報が最新であるとは限らない点に注意してください.また当然ですが,ランクが低いからといって良い論文がないわけではないです.

システム系で特に主要な会議としては,USENIXの会議全般 (OSDI, ATC, NSDI, FAST, SEC など) 及び IEEE の SOSP, ACMのASPLOS, VEE, EuroSys などで,他にも分野ごとにいろいろとあります.またセキュリティに関しては USENIX SEC, NDSS, IEEE S&P, ACM CCSが4大会議として知られています(暗号系を除く).

会議の日程のチェック

会議の日程は会議のサイトの他,上記に示した

にあります.気になる会議はカレンダーに登録しておくと良いでしょう.

論文を読む(サーベイする)

とりあえずシステム系の論文がどんなものか知りたい,という場合はOSDIやSOSP, ATC, EuroSysなどシステム系全般を扱っている会議で発表されている論文を眺めてみるのが良いと思います.またもし例えばネットワーク系に興味があるならNSDIなどネットワークに特化した会議をみてみると良いでしょう.Google Scholarでキーワード検索して論文を探すのも当然有効です.その際,個人的には近年のトップの会議の最近の論文を探して,そこからその論文の引用・被引用論文を辿るというやり方をしてサーベイすることが多いです.

もし大学に所属している場合は,多くの場合大学がACMIEEE包括契約しているため論文が読めるはずですが,そうでない場合も最近はだいぶオープンアクセスの流れがあり,多くの論文が無料で公開されています.とくにUSENIX系の会議は全てオープンアクセスとなっています.また,オープンアクセスでない会議でも著者のサイトに個人的なコピーがある場合があるので,読みたい論文がある場合はそちらを探してみるのも一つの手です.また最近はありがたいことに発表スライドに加えて発表動画が公開されていることも珍しくありません.積極的に活用すると良いでしょう.

論文の読み方に関してはいろいろと流儀があるので,以下に紹介するものを読むなどして自分に合う方法を探してみるのが良いと思います.

個人的にはアブストと最後のまとめを見てこの論文の動機は何で,何が貢献なのかをざっと見て,自分が探しているものと合いそうであればもう少し中身をみてみるといったパターンが多いです.上記のリンクにも書いてありますが,時間は有限でいつまでもサーベイすることはできないので,しっかり時間を決めて集中して論文ごとの貢献や関係を整理していくことが大事だと思います.

文献管理

論文を管理するためのソフトウェアが文献管理ソフトウェアです.文献管理ソフトウェアは探せばたくさんあります.特に比較はしていませんが,自分はZoteroを使っていてます.Chrome拡張機能から自動でローカルのzoteroにPDF含めて情報がインポートできるほか,クラウドのアカウントにも自動で同期されます(無料枠では300Mまで).ノートやextraの部分にメモが残せるので,読んだときに軽くメモを残しておくとができ便利です.

こうした機能はどの文献管理ソフトにもあると思います.またこれらの文献管理ソフトは簡単に論文作成には欠かせないbibtexをエクスポートする機能があるはずなので,何かしらは使った方が良いと思います.

産業系カンファレンス

システム系の場合は産業系のカンファレンスも,学術的とはちょっと毛色が異なりますが,参考になるものが多くあります.例えばLinux foundationが主宰のOpen Source Summit,KVM Forumなどや,ストレージ系ならSNIAStorage Developer Conferenceなどです.これらからは実務に即した最新の話題や課題が得られるので,論文読むのに疲れた時にチェックするといいと思います.

最新の動向を追う

本当に最新の情報を知りたい場合は,Googleアラートで特定のキーワードを指定しておく方法があります.キーワードをうまく設定しないとたくさん通知が来すぎる点には注意です.また,最近はarXivに論文を上げることも,システム系でも珍しくなくなってきています(トップ会議のほとんどはdouble blindですが,arXivなどへの投稿は許可されていることが多いです.ただし全てではありません).arXivを追う場合はOperating Systemsや,セキュリティならCryptography and Securityになるでしょう.個人的に情報が多すぎるのでarXivを追うことはあまりしていませんが,たまにキーワード検索で論文を探したりすることはあります.採択が決まっているけれどまだ会議前で公開されてない論文があるケースもあります.またLinuxであればLKMLのメーリスに登録すれば最新のカーネル情報が流れてきますが,これを真面目に追うのはかなり大変です.Gmailのフィルタ機能なので特定の機能やパッチの更新があったときに通知するようにすると良いと思います.

その他

ジャンルによってはサーベイやニュースを定期的に出したりまとめたりしている人がいます.それらを活用しない手はないでしょう.例えばBPFに関してはciliumがnewsを出しています.またブログやGitHubリポジトリに読んだ論文をまとめていたりする人もいます.他にも今はwebで検索すればいろいろとヒットすると思うので,勿論一番大事なのは論文本体ですが,たまにはそれ以外にも目を向けてみるのも悪くないと思います.ただしもしそれらで良い情報を見つけたら,一次文献に当たることは必然です.

最後に

最初にも書きましたが,論文調査方法はいろいろあるので,いろんな方法を試して自分に合うのを見つけることが大事だと思います.ここに書いたものもあくまで自分がいろいろ試した中で今利用している方法です.意外とこういった情報は共有されないことが多いので書いてみました.是非みなさんも何か良い方法があれば教えてください.

macでbpftraceを試す

全然使ってなかったので最近気づいたのですが,いつのまにか Docker for mac で使われている linuxkit のカーネルが 5.4ベースになっていて,BPFの基本的な機能が使えるようになってました(念のため言っておくと,docker for macLinuxをハイパーバイザ上で動かして,その上でdockerを動かしています).ということで簡単にmacからbpftraceが試せるように以下のようなものを作ってみました.

github.com

macにdockerをインストールして,./build.sh && ./run.sh で動くと思います.bpftraceのデフォルトのツールと,前に自分が作ったtetrisが入ってます.トレーシングは何かしらアプリケーションが動いてないとあまり面白くないので,tmuxでも使ってトレースしながら何か動かしてみるのが良いと思います.以下が実行例です.なんかもっさりしてる部分がありますがGIF化したときの影響なので実際は普通に動いています.

f:id:mm_i:20201117175238g:plain

VM作った方がいろいろと弄れるわけですが,もしbpftraceどんなものか知りたいと思ってるmacユーザの方は試してみてはいかがでしょうか.ちなみにBTFのサポートとかはないので ,bpftraceの100%の力を発揮できるわけではないのでそこはご了承ください.本当のbpftraceの力を知るには... 自分でカーネルとbpftraceをコンパイルしましょう.

BPF_PROG_TYPE_RAW_TRACEPOINT (raw tracepoint) について

BPFのプログラムタイプの一つにBPF_PROG_TYPE_RAW_TRACEPOINTがあります (commit).これを利用するとtracepointの変換前の引数にアクセスすることができます.

例としてkernel/sched/core.cで定義されるsched_swtich のtracepointを考えます.

     trace_sched_switch(preempt, prev, next);

このtracepointはinclude/trace/events.sched.hで定義されます.

TRACE_EVENT(sched_switch,

    TP_PROTO(bool preempt,
         struct task_struct *prev,
         struct task_struct *next),

    TP_ARGS(preempt, prev, next),

    TP_STRUCT__entry(
        __array(    char,  prev_comm,  TASK_COMM_LEN   )
        __field(    pid_t,  prev_pid            )
        __field(    int,   prev_prio           )
        __field(    long,  prev_state          )
        __array(    char,  next_comm,  TASK_COMM_LEN   )
        __field(    pid_t,  next_pid            )
        __field(    int,   next_prio           )
    ),

    TP_fast_assign(
        memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
        __entry->prev_pid    = prev->pid;
        __entry->prev_prio   = prev->prio;
        __entry->prev_state  = __trace_sched_switch_state(preempt, prev);
        memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
        __entry->next_pid    = next->pid;
        __entry->next_prio   = next->prio;
        /* XXX SCHED_DEADLINE */
    ),

    TP_printk("prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s%s ==> next_comm=%s next_pid=%d next_prio=%d",
        __entry->prev_comm, __entry->prev_pid, __entry->prev_prio,

        (__entry->prev_state & (TASK_REPORT_MAX - 1)) ?
          __print_flags(__entry->prev_state & (TASK_REPORT_MAX - 1), "|",
                { TASK_INTERRUPTIBLE, "S" },
                { TASK_UNINTERRUPTIBLE, "D" },
                { __TASK_STOPPED, "T" },
                { __TASK_TRACED, "t" },
                { EXIT_DEAD, "X" },
                { EXIT_ZOMBIE, "Z" },
                { TASK_PARKED, "P" },
                { TASK_DEAD, "I" }) :
          "R",

        __entry->prev_state & TASK_REPORT_MAX ? "+" : "",
        __entry->next_comm, __entry->next_pid, __entry->next_prio)
);

ここで,通常のtracepointであれば,TP_STRUCT__entryで定義されている項目 (+ tracepointに共通の項目)にアクセスできます.これは以下のtracing/events/sched/sched_switch/formatからも確認できます.

# cat /sys/kernel/debug/tracing/events/sched/sched_switch/format
name: sched_switch
ID: 318
format:
        field:unsigned short common_type;       offset:0;       size:2; signed:0;
        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
        field:int common_pid;   offset:4;       size:4; signed:1;

        field:char prev_comm[16];       offset:8;       size:16;        signed:1;
        field:pid_t prev_pid;   offset:24;      size:4; signed:1;
        field:int prev_prio;    offset:28;      size:4; signed:1;
        field:long prev_state;  offset:32;      size:8; signed:1;
        field:char next_comm[16];       offset:40;      size:16;        signed:1;
        field:pid_t next_pid;   offset:56;      size:4; signed:1;
        field:int next_prio;    offset:60;      size:4; signed:1;

print fmt: "prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s%s ==> next_comm=%s next_pid=%d next_prio=%d", REC->prev_comm, REC->prev_pid, REC->prev_prio, (REC->prev_state & ((((0x0000 | 0x0001 | 0x0002 | 0x0004 | 0x0008 | 0x0010 | 0x0020 | 0x0040) + 1) << 1) - 1)) ? __print_flags(REC->prev_state & ((((0x0000 | 0x0001 | 0x0002 | 0x0004 | 0x0008 | 0x0010 | 0x0020 | 0x0040) + 1) << 1) - 1), "|", { 0x0001, "S" }, { 0x0002, "D" }, { 0x0004, "T" }, { 0x0008, "t" }, { 0x0010, "X" }, { 0x0020, "Z" }, { 0x0040, "P" }, { 0x0080, "I" }) : "R", REC->prev_state & (((0x0000 | 0x0001 | 0x0002 | 0x0004 | 0x0008 | 0x0010 | 0x0020 | 0x0040) + 1) << 1) ? "+" : "", REC->next_comm, REC->next_pid, REC->next_prio

一方でraw tracepiontの場合,アクセスできるのはtrace_sched_switch()に与えた引数 (TP_PROTOの引数)になります.sched_switchの場合は

 TP_PROTO(bool preempt,
         struct task_struct *prev,
         struct task_struct *next)

になります.

raw tracepintのもともとの導入の経緯は,sched_switchにおいてprevnexttask_structの中身にアクセスしたいという要求からだったようです(参考).通常のtracepointであればBPFプログラム呼び出し前にtracepointのデータを整形する必要がありますが,raw tracepointではその必要がないため,その分のオーバヘッドが抑えられます.ただし,raw tracepointの引数で与えられたポインタのデータにアクセスするにはbpf_probe_read()を使う必要があります.基本的にはtarcepiontの方が扱いやすいので,そちらを使えば良いと思いますが,パフォーマンスが問題であったり,あるいはtracepointの情報だけでは不足している場合はraw tracepointを検討してみると良いと思います.

BPFプログラムからカーネル内のデータ構造にアクセスする方法

kprobeやtracepointなどにアタッチしたBPFプログラムは,BPF_FUNC_probe_read()関数でカーネル内のデータ構造にアクセスすることができます.これはprobe_kernel_read()のラッパーで,もし変な領域にアクセスしようした場合(page faultが発生した場合)は-EFAULTが返ります. また,BPF_FUNC_get_current_task()でcurrentのtask_structを得ることができます.

例として,/bin/catが実行されたときに,そのプロセスのvm_area_structの中身をダンプするプログラムを書いてみます.bccを利用して以下のように書けます.

#!/usr/bin/env python

import bcc

text = r"""
#include <linux/ptrace.h>
#include <linux/sched.h>

int probe(struct pt_regs *ctx) {
    int i;
    struct task_struct *t = (struct task_struct *)bpf_get_current_task();
    struct vm_area_struct *vma = t->mm->mmap;

    #pragma clang loop unroll(full)
    for (i = 0; i < 20; i++) {
        if (vma != NULL) {
            unsigned long inode = vma->vm_file->f_inode->i_ino;
            bpf_trace_printk("%lx-%lx %ld\n", vma->vm_start, vma->vm_end, inode);
            vma = vma->vm_next;
        }
    }

    return 0;
}
"""


def main():
    b = bcc.BPF(text=text, debug=0)
    b.attach_uprobe(name="/bin/cat", sym="main", fn_name="probe")
    b.trace_print()


if __name__ == "__main__":
    main()

ここで注意点としては,(古いカーネルだと)BPFプログラムはループを扱えないので*1,pragmaを利用して展開する必要があります.またbreak文を含むようなプログラムはverifierにおこられるので,ちょっと書き方に工夫がいります.

bccでは,ポインタ参照は原則bpf_probe_read()関数呼び出しになります*2.ここで,上記プログラムではポインタがNULLか確認していません*3.これで大丈夫?と思うかもしれませんが,実際のポインタ参照はbpf_probe_read()を利用していて,もし読み出しに失敗した場合はbccは事前にバッファをクリアするので得られる値は0になります.アドレス0に対するbpf_probe_read()は失敗するでしょうが,プログラムがクラッシュするといったことはないので,これで大丈夫です.結果としてはvma->vm_fileがNULLならinodeは0になります.

上記プログラムを実行しておいて*4,以下のようにcatを実行すると,

% cat /proc/self/maps
 55e4824bf000-55e4824c7000 r-xp 00000000 08:02 2621482                    /bin/cat
 55e4826c6000-55e4826c7000 r--p 00007000 08:02 2621482                    /bin/cat
 55e4826c7000-55e4826c8000 rw-p 00008000 08:02 2621482                    /bin/cat
 55e4835b0000-55e4835d1000 rw-p 00000000 00:00 0                          [heap]
 7f1b8bfcf000-7f1b8c1b6000 r-xp 00000000 08:02 6685444                    /lib/x86_64-linux-gnu/libc-2.27.so
 7f1b8c1b6000-7f1b8c3b6000 ---p 001e7000 08:02 6685444                    /lib/x86_64-linux-gnu/libc-2.27.so
 7f1b8c3b6000-7f1b8c3ba000 r--p 001e7000 08:02 6685444                    /lib/x86_64-linux-gnu/libc-2.27.so
 7f1b8c3ba000-7f1b8c3bc000 rw-p 001eb000 08:02 6685444                    /lib/x86_64-linux-gnu/libc-2.27.so
 7f1b8c3bc000-7f1b8c3c0000 rw-p 00000000 00:00 0
 7f1b8c3c0000-7f1b8c3e7000 r-xp 00000000 08:02 6685432                    /lib/x86_64-linux-gnu/ld-2.27.so
 7f1b8c41a000-7f1b8c43c000 rw-p 00000000 00:00 0
 7f1b8c43c000-7f1b8c5d7000 r--p 00000000 08:02 4326139                    /usr/lib/locale/locale-archive
 7f1b8c5d7000-7f1b8c5d9000 rw-p 00000000 00:00 0
 7f1b8c5e7000-7f1b8c5e8000 r--p 00027000 08:02 6685432                    /lib/x86_64-linux-gnu/ld-2.27.so
 7f1b8c5e8000-7f1b8c5e9000 rw-p 00028000 08:02 6685432                    /lib/x86_64-linux-gnu/ld-2.27.so
 7f1b8c5e9000-7f1b8c5ea000 rw-p 00000000 00:00 0
 7fffea906000-7fffea927000 rw-p 00000000 00:00 0                          [stack]
 7fffea9fc000-7fffea9ff000 r--p 00000000 00:00 0                          [vvar]
 7fffea9ff000-7fffeaa00000 r-xp 00000000 00:00 0                          [vdso]
 ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

以下の出力が得られます*5.BPFプログラム側からvma_structの情報が取得できていることが分かります.

 <...>-8164  [001] .... 512025.107366: 0: 55e4824bf000-55e4824c7000 2621482
 <...>-8164  [001] .... 512025.107381: 0: 55e4826c6000-55e4826c7000 2621482
 <...>-8164  [001] .... 512025.107382: 0: 55e4826c7000-55e4826c8000 2621482
 <...>-8164  [001] .... 512025.107383: 0: 7f1b8bfcf000-7f1b8c1b6000 6685444
 <...>-8164  [001] .... 512025.107384: 0: 7f1b8c1b6000-7f1b8c3b6000 6685444
 <...>-8164  [001] .... 512025.107384: 0: 7f1b8c3b6000-7f1b8c3ba000 6685444
 <...>-8164  [001] .... 512025.107385: 0: 7f1b8c3ba000-7f1b8c3bc000 6685444
 <...>-8164  [001] .... 512025.107390: 0: 7f1b8c3bc000-7f1b8c3c0000 0
 <...>-8164  [001] .... 512025.107391: 0: 7f1b8c3c0000-7f1b8c3e7000 6685432
 <...>-8164  [001] .... 512025.107394: 0: 7f1b8c5d7000-7f1b8c5d9000 0
 <...>-8164  [001] .... 512025.107395: 0: 7f1b8c5e7000-7f1b8c5e8000 6685432
 <...>-8164  [001] .... 512025.107396: 0: 7f1b8c5e8000-7f1b8c5e9000 6685432
 <...>-8164  [001] .... 512025.107399: 0: 7f1b8c5e9000-7f1b8c5ea000 0
 <...>-8164  [001] .... 512025.107402: 0: 7fffea906000-7fffea927000 0
 <...>-8164  [001] .... 512025.107405: 0: 7fffea9fc000-7fffea9ff000 0
 <...>-8164  [001] .... 512025.107408: 0: 7fffea9ff000-7fffeaa00000 0

またbpftraceなら以下のように書けます.

#include <linux/sched.h>
#include <linux/fs.h>

uprobe:/bin/cat:main {
  $vma = (struct vm_area_struct*)curtask->mm->mmap;

  unroll(20) {
    if ($vma != 0) {
      $inode = $vma->vm_file->f_inode->i_ino;
      printf("%lx-%lx %ld\n", $vma->vm_start, $vma->vm_end, $inode);
      $vma = $vma->vm_next;
    }
  }
}

(ハイライトが欲しい...)

使い所が限られますが,あの情報ここにあったっけ?という時にカーネルモジュールを書かなくても確認できます.

*1:Linux5.3からBounded Loopに対応しました

*2:BPFプログラムに与えられる引数(ctx)に対するアクセスはload命令になります

*3:例えば,vma->vm_fileはmmapされた領域でなければNULLになります

*4:attach_uprobeする際はデバッグ情報をみてアドレス解決するので,デバッグシンボル (ubuntuならcoreutils-dbgsym)をインストールしておく必要があります.

*5:[heap]などの情報が表示されていないのはこれはカーネル側で表示している情報であるためです

RustでUnix Domain Socketを扱う方法

"rust domain socket"とかで検索するといろいろ引っかかるけどいかんせんライブラリの更新が早くて情報が古かったりするのでここに現状を書きます(この文章もすぐに古くなるかもしれないので後から読む人は注意してください..)

基本

async/await

HTTPサーバ

std::os::unix::net::{UnixListner, UnixStream}

use std::fs;
use std::io::prelude::*;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::Path;
use std::thread;

fn handle_client(mut stream: UnixStream) -> std::io::Result<()> {
    let mut buf = [0; 1024];

    let n = stream.read(&mut buf)?;
    let s = String::from_utf8_lossy(&buf[..n]);
    println!("{}", s);

    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let sockfile = Path::new("/tmp/uds.sock");
    if sockfile.exists() {
        fs::remove_file(&sockfile)?;
    }

    let listner = UnixListener::bind(sockfile)?;
    for stream in listner.incoming() {
        let stream = stream?;
        thread::spawn(move || handle_client(stream).unwrap());
    }

    Ok(())
}

tokio

// cargo-deps: tokio="0.2.0-alpha.6", tokio-net="0.2.0-alpha.6", futures-preview="0.3.0-alpha.19"

use std::fs;
use std::path::Path;

use futures::StreamExt;
use tokio;
use tokio::io::AsyncReadExt;
use tokio::net::unix::{UnixListener, UnixStream};

async fn handle_client(mut stream: UnixStream) -> Result<(), Box<dyn std::error::Error>> {
    let mut buf = [0; 1024];

    let n = stream.read(&mut buf).await?;
    let s = String::from_utf8_lossy(&buf[..n]);
    println!("{}", s);

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let sockfile = Path::new("/tmp/uds.sock");
    if sockfile.exists() {
        fs::remove_file(&sockfile)?;
    }

    let listner = UnixListener::bind(sockfile)?;
    let mut incoming = listner.incoming();
    while let Some(stream) = incoming.next().await {
        let stream = stream?;
        tokio::spawn(async move {
            handle_client(stream).await.unwrap();
        });
    }

    Ok(())
}