ZigでLinuxのepollとsignalfdのシステムコールを使うサンプルプログラム
Zig Advent Calendar 2022 に参加しています。
あるプログラムを作ろうと思ったのですが、それにはepoll
とsignalfd
のシステムコールを使うのが良さそうだということがわかりました。これらのシステムコールを使うのは初めてだったのでまずは理解のために簡単なサンプルプログラムを作りました。そしてせっかくなのでそれをZig言語で書き直しました。
システムコール epoll について
epoll
はLinuxでのI/Oイベントの通知のしくみです。これを使って複数の入出力を待つことができます。似たものにselect
やpoll
がありますが、epoll
はそれらの中でも後発で、他のものにあった欠点を解決し柔軟性も性能も高いものになっています。なので、これから新規に作るならばepoll
を使うのがよいでしょう。なお、Zig言語のライブラリのstd.os.linuxではselect
はサポートされていません。
epollは単一のシステムコールではなく以下の3つのシステムコールを組み合わせて使用します。
- epoll_create1
- epoll_ctl
- epoll_wait
このように分離することで、動作の途中で監視対象のファイルディスクリプタを増やしたり減らしたりすることもできるようになりました。epoll_create1
で取得したファイルディスクリプタは不要になったらclose
する必要があります。
注意点としてepoll
はpipeやsocketを対象としており通常のファイルは扱えません。
システムコール signalfd について
シグナルはプロセス間通信のひとつです。当初はシグナルを処理するにはシグナルハンドラを登録する必要がありました。しかしシグナルハンドラは通常のコンテキストと異なるので使用しても良いライブラリ関数が限定されたり、排他制御を意識する必要があるなどの扱いにくさがありました。
signalfd
はシグナルを受信するためにファイルディスクリプタを作成するシステムコールです。これで作成したファイルディスクリプタはepoll
で他の入出力とまとめて監視することができるようになります。スレッドを分ける必要もなくシンプルな構成になります。
サンプルプログラムの要件
標準入力(stdin)から読み取ってそれを標準出力(stdout)に書き出します。いわゆるフィルターです。ログは標準エラー出力(stderr)に出します。通常は入力されたデータに何か加工をしてから出力するのですが、今回はサンプルなので入力と同じ内容を出力しています。さらに以下の要件があります。
- stdinから入力があったときに内部変数verboseがtrueのときは入力されたバイト数をログに出します。
- 一定時間(10秒間)入力がなかったことを検出し、"timeout"とログを出します。
- SIGINT, SIGTERMのシグナルを受信したときには、そのことをログに出して終了します。
- SIGUSR1を受信したときには内部変数verboseをfalseに変更して処理を続行します。
- SIGUSR2を受信したときには内部変数verboseをtrueに変更して処理を続行します。
C言語でのサンプルプログラム
Zigで書き直したサンプルプログラム
それほど長いものではなかったので、特にツールなど使わずに白紙の状態からテキストエディタで書き下していきました。
epoll
とsignalfd
はどちらもLinux固有のものなので、MacやWindows向けにクロスコンパイルして動かすことはできません。Linuxではx86_64とarm64の両方で動作することを確認しています。
書き直した感想
Cで書いたものと大きく構造を変えずに書き直すことができました。
Zig流のエラーハンドリングのおかげでエラーチェックのif文が減ってすっきりしました。defer
によるリソースの解放も便利です。
Cではusize, i32などを雑にintで書いてしまうところをきっちりと区別されます。 うっかりしたバグを未然の防げるのがいいですね。
システムコールを直接使った場合には返り値がEINTRの場合はリトライが必要なためループにしなければならなくて煩雑なのですが、std.os.read
, std.io.write
ではこの中でリトライしてくれるのですっきり書けます。
クロスビルドも楽です。以下のコマンド一発でjetson nanoで動作する実行ファイルを作れました。
$ zig build-exe epoll_example.zig -target aarch64-linux-gnu
$ file epoll_example
epoll_example: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped
Tips
ちょっとつまずいたところを共有します。
SIGINT は std.os.linux.SIG.INT
signal.h
にあるSIGINT
の定数定義に相当するものをzigのソースディレクトリで検索したのですが最初は見つけることができませんでした。しかたなく自分で
const SIGINT = 2
などを追加して使っていたのですが、std.os.linux.SIG.INT
を発見しました。
extern struct の扱い方
受信したシグナルの情報を格納するsignalfd_siginfo
というstructがあります。しかし
var info: os.linux.signalfd_siginfo = .{};
と書いてもこれはコンパイルエラーになります。これはstd.os.linux.signalfd_siginfo
が extern struct
として定義されているためです。通常のstruct
はフィールドの配置は自動で最適化されるのですが、extern struct
ではそれを行わずC言語とバイナリ互換性を保つようになるところが異なります。
いろいろさがしてたどりついた方法は以下の通りです。
- 一度u8の配列としてその大きさの領域を確保する
-
std.os.read
により内容を読み込む - そのポインタを
*os.linux.signalfd_siginfo
にキャストする
var buf: [@sizeOf(os.linux.signalfd_siginfo)]u8 align(8) = undefined;
if (buf.len != try os.read(signal_event.data.fd, &buf)) {
return os.ReadError.ReadError;
}
const info = @ptrCast(*os.linux.signalfd_siginfo, &buf);
switch (info.signo) {
...
[]u8 から[*]u8を得る方法
[]u8
はスライスで、[*]u8
はmany-item pointerです。
@memcpy
は以下のように[*]u8
の引数をとります。
@memcpy(noalias dest: [*]u8, noalias source: [*]const u8, byte_count: usize)
スライスからmany-item pointerを得るには.ptr
をつけます。
fn nop_filter(rbuf: []u8, wbuf: []u8) usize {
// Just copy now, replace this to do something great
@memcpy(wbuf.ptr, rbuf.ptr, rbuf.len);
return rbuf.len;
}
** 2023/04/29 追記
version 0.11に向けたmasterで@memcpyの仕様が変わり、以下のように書くようになりました。
@memcpy(wbuf, rbuf);
関数ポインタの型の定義
関数ポインタの型を定義するときには関数の前に *const
をつけます。
const filter_op = *const fn (rbuf: []u8, wbuf: []u8) usize;
命名規則
この記事の公開直前に命名規則のことを思い出して、関数名をcamelCaseに修正しました。
関連
Discussion