🚋

Zig言語でLinuxのパイプの読み書きの効率を上げる実装例

2023/05/21に公開

パイプ

Linuxで簡単に使用できるプロセス間通信にパイプがあります。

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

パイプの簡単な使用例。
カレントディレクトリにあるファイルの個数を数える。

$ ls | wc -l
      20

パイプで大きなサイズのデータを連続的に流したい

カメラでキャプチャした動画をパイプで別のプロセスに渡したいとします。

#!/bin/sh -eux

VIDEODEV=/dev/video0
WIDTH=640
HEIGHT=360
FRAMERATE=15

v4l2capture $VIDEODEV /dev/stdout $WIDTH $HEIGHT $FRAMERATE YUYV | \
    convert2i420 /dev/stdin out.i420 $WIDTH $HEIGHT YUYV 

私のgithubのリポジトリにある2つのコマンド、v4l2captureconvert2i420 をパイプでつないでいます。
保存できた動画のファイルは以下のようにして再生することができます。

ffplay -f rawvideo -pixel_format yuv420p -video_size 640x360 -framerate 15 out.i420 

このときの一枚のフレームの大きさは 640x360x2 = 460,800 バイトになります。
パイプのデフォルトのバッファサイズは64KBなので、8回の書き込みでやっと一枚のフレームを送ることができます。これをガツンと一回で書けるようにしたいですね。

パイプのバッファサイズの変更方法

さきほどのマニュアルページに触れられているのですが、fcntl(2)でパイプのバッファサイズを変更することができます。
https://man7.org/linux/man-pages/man2/fcntl.2.html

Changing the capacity of a pipe
F_SETPIPE_SZ (int; since Linux 2.6.35)
F_GETPIPE_SZ (void; since Linux 2.6.35)

設定可能なサイズの上限は/proc/sys/fs/pipe-max-sizeで知ることができます。

Zig言語でパイプのバッファサイズを変更する

指定されたファイルディスクプリタがPIPE(FIFO)であるかどうかを判定する関数と指定されたファイルディスクプリタのパイプのバッファサイズを上限値にセットする関数をZigで書きました。

set_pipe_size.zig
const std = @import("std");
const fs = std.fs;
const os = std.os;
const c = @cImport({
    @cDefine("_GNU_SOURCE", "");
    @cInclude("unistd.h");
    @cInclude("fcntl.h");
});

// Check if the given file descriptor is a pipe
pub fn isPipe(fd: os.fd_t) !bool {
    var stat: os.linux.Stat = undefined;
    if (0 != os.linux.fstat(fd, &stat)) {
        return error.Fstat;
    }
    return (stat.mode & os.linux.S.IFMT) == os.linux.S.IFIFO;
}

// Set the size of the given pipe file descriptor to the maximum size
pub fn setPipeMaxSize(fd: os.fd_t) !void {
    // Read the maximum pipe size
    var pipe_max_size_file = try fs.cwd().openFile("/proc/sys/fs/pipe-max-size", .{});
    defer pipe_max_size_file.close();

    var reader = pipe_max_size_file.reader();
    var buffer: [128]u8 = undefined;
    const max_size_str = std.mem.trimRight(u8, buffer[0..(try reader.readAll(&buffer))], &std.ascii.whitespace);
    const max_size = std.fmt.parseInt(c_int, max_size_str, 10) catch |err| {
        std.debug.print("Failed to parse /proc/sys/fs/pipe-max-size: {}\n", .{err});
        return err;
    };

    // If the current size is less than the maximum size, set the pipe size to the maximum size
    var current_size = c.fcntl(fd, c.F_GETPIPE_SZ);
    if (current_size < max_size) {
        if (max_size != c.fcntl(fd, c.F_SETPIPE_SZ, max_size)) {
            return error.FaiedToSetPipeSize;
        }
    }
}

Zigからfcntlのシステムコールを呼ぶことはできるのですが、F_SETPIPE_SZF_GETPIPE_SZの値がZigの標準ライブラリでは定義されていなかったので、C言語のヘッダをインポートしてCの関数として呼ぶようにしました。Zig言語はC言語との親和性が高いので、このようなときに簡単にCの関数を呼び出して使うことができるようになっています。

効果の確認

2つのプロセスの実行にstraceコマンドをかませてシステムコールログをとってみます。

#!/bin/sh -eux

VIDEODEV=/dev/video0
WIDTH=640
HEIGHT=360
FRAMERATE=15

strace -f -o st_write.log zig-out/bin/v4l2capture $VIDEODEV /dev/stdout $WIDTH $HEIGHT $FRAMERATE YUYV | \
    strace -f -o st_read.log convert2i420 /dev/stdin out.i420 $WIDTH $HEIGHT YUYV 

変更前

straceのログは横スクロールさせて見てください。

書き込み側

$ grep write oldlog/st_write.log

...
3254585 write(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254585 write(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254585 write(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254585 write(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254585 write(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254585 write(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
...

事前の予想とは違って、書き込み側はひとつのフレームの460800バイトを1回のwrite(2)で書けていますね。

読み込み側

$ grep read oldlog/st_read.log

...
3254584 read(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 65536
3254584 read(3, "L\204L\200L\204L\200L\204K\200K\204K\200K\204K\200K\204K\200K\204K\200K\204K\200"..., 395264) = 65536
3254584 read(3, "B\204B\200B\204B\200B\204B\200B\204B\200B\204B\200B\204B\200B\204B\200B\203B\200"..., 329728) = 65536
3254584 read(3, "\320\202\331y\327\203\312x\310\203\312w\312\204\305v\277\204\270v\260\204\271v\262\204\267u\265\204\251u"..., 264192) = 65536
3254584 read(3, "\321y\321\207\322y\322\207\322y\322\207\322y\322\207\322y\322\207\322y\322\207\322y\323\206\323y\323\206"..., 198656) = 65536
3254584 read(3, "\211y\210\205\205y\202\205\177y|\205yzv\205rzo\205l{g\205c|_\204Z|V\204"..., 133120) = 65536
3254584 read(3, "T\200V\201X\177T\201e\177\211\200\243\177\255\200\271\177\305\200\315\177\323\200\331\177\334\200\341\177\344\200"..., 67584) = 65536
3254584 read(3, "\330\177\330\177\331\177\330\177\327\177\330\177\330\177\330\177\327\177\326\177\326\177\326\177\325\177\323\177\320\177\316\177"..., 2048) = 2048
3254584 read(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 65536
3254584 read(3, "L\204L\200K\204K\200K\204K\200K\204K\200K\204K\200K\204K\200K\204K\200K\204J\200"..., 395264) = 65536
3254584 read(3, "B\204B\200B\204B\200B\203B\200B\203B\200B\203B\200B\203B\200B\203B\200B\203B\200"..., 329728) = 65536
3254584 read(3, "\317\202\326y\327\203\312x\312\203\312w\312\204\306v\277\204\270v\257\204\277v\267\204\266u\263\204\252v"..., 264192) = 65536
3254584 read(3, "\321y\321\207\322y\322\207\322y\322\206\322y\322\206\323y\323\206\323y\323\206\323y\323\206\324y\323\206"..., 198656) = 65536
3254584 read(3, "\211y\207\205\203y\201\205~y|\205yzv\205rzn\205k{f\205b|_\204Z|U\204"..., 133120) = 65536
3254584 read(3, "T\177W\201V\177S\200f\177\211\200\244\177\256\200\272\177\305\200\315\177\324\200\331\177\336\200\341\177\343\200"..., 67584) = 65536
3254584 read(3, "\330\177\331\177\331\177\330\177\327\177\330\177\331\177\330\177\327\177\326\177\326\177\326\177\325\177\323\177\320\177\317\177"..., 2048) = 2048
...

読み込み側では1回のread(2)で読めるサイズがパイプのバッファサイズの64KBに制限されているので、一枚のフレームの読み込みを8回に分けて読むようになっています。

変更後

書き込み側は同じ。
読み込み側

$ grep read newlog/st_read.log
...
3254719 read(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254719 read(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254719 read(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254719 read(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254719 read(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254719 read(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
3254719 read(3, "\347\177\347\177\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200\347\200"..., 460800) = 460800
...

1回のread(2)で460800バイトを読めるようになりました。

続き

パイプに関してまだ続きがあります。
https://zenn.dev/tetsu_koba/articles/43d8ed81cc1c2f

関連

https://zenn.dev/tetsu_koba/articles/29bc1c5192a7ab
https://zenn.dev/tetsu_koba/articles/f9c4d250a497c9
https://zenn.dev/tetsu_koba/articles/43d8ed81cc1c2f

Discussion