📌

ZigでLinuxのepollとsignalfdのシステムコールを使うサンプルプログラム

2022/12/18に公開

Zig Advent Calendar 2022 に参加しています。
https://qiita.com/advent-calendar/2022/ziglang

あるプログラムを作ろうと思ったのですが、それにはepollsignalfdのシステムコールを使うのが良さそうだということがわかりました。これらのシステムコールを使うのは初めてだったのでまずは理解のために簡単なサンプルプログラムを作りました。そしてせっかくなのでそれをZig言語で書き直しました。

システムコール epoll について

epollはLinuxでのI/Oイベントの通知のしくみです。これを使って複数の入出力を待つことができます。似たものにselectpollがありますが、epollはそれらの中でも後発で、他のものにあった欠点を解決し柔軟性も性能も高いものになっています。なので、これから新規に作るならばepollを使うのがよいでしょう。なお、Zig言語のライブラリのstd.os.linuxではselectはサポートされていません。

epollは単一のシステムコールではなく以下の3つのシステムコールを組み合わせて使用します。

  • epoll_create1
  • epoll_ctl
  • epoll_wait

このように分離することで、動作の途中で監視対象のファイルディスクリプタを増やしたり減らしたりすることもできるようになりました。epoll_create1で取得したファイルディスクリプタは不要になったらcloseする必要があります。

注意点としてepollはpipeやsocketを対象としており通常のファイルは扱えません。

https://man7.org/linux/man-pages/man7/epoll.7.html

システムコール signalfd について

シグナルはプロセス間通信のひとつです。当初はシグナルを処理するにはシグナルハンドラを登録する必要がありました。しかしシグナルハンドラは通常のコンテキストと異なるので使用しても良いライブラリ関数が限定されたり、排他制御を意識する必要があるなどの扱いにくさがありました。

signalfdはシグナルを受信するためにファイルディスクリプタを作成するシステムコールです。これで作成したファイルディスクリプタはepollで他の入出力とまとめて監視することができるようになります。スレッドを分ける必要もなくシンプルな構成になります。

https://man7.org/linux/man-pages/man2/signalfd.2.html

サンプルプログラムの要件

標準入力(stdin)から読み取ってそれを標準出力(stdout)に書き出します。いわゆるフィルターです。ログは標準エラー出力(stderr)に出します。通常は入力されたデータに何か加工をしてから出力するのですが、今回はサンプルなので入力と同じ内容を出力しています。さらに以下の要件があります。

  • stdinから入力があったときに内部変数verboseがtrueのときは入力されたバイト数をログに出します。
  • 一定時間(10秒間)入力がなかったことを検出し、"timeout"とログを出します。
  • SIGINT, SIGTERMのシグナルを受信したときには、そのことをログに出して終了します。
  • SIGUSR1を受信したときには内部変数verboseをfalseに変更して処理を続行します。
  • SIGUSR2を受信したときには内部変数verboseをtrueに変更して処理を続行します。

C言語でのサンプルプログラム

Zigで書き直したサンプルプログラム

それほど長いものではなかったので、特にツールなど使わずに白紙の状態からテキストエディタで書き下していきました。

epollsignalfdはどちらも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_siginfoextern 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);

https://zenn.dev/tetsu_koba/articles/5ddcf30d63d12b

関数ポインタの型の定義

関数ポインタの型を定義するときには関数の前に *const をつけます。

const filter_op = *const fn (rbuf: []u8, wbuf: []u8) usize;

命名規則

この記事の公開直前に命名規則のことを思い出して、関数名をcamelCaseに修正しました。
https://ziglang.org/documentation/master/#toc-Names

関連

https://zenn.dev/tetsu_koba/articles/5ddcf30d63d12b

Discussion