Zig言語でLinuxのパイプの読み書きの効率を上げる実装例
パイプ
Linuxで簡単に使用できるプロセス間通信にパイプがあります。
パイプの簡単な使用例。
カレントディレクトリにあるファイルの個数を数える。
$ 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つのコマンド、v4l2captureとconvert2i420 をパイプでつないでいます。
保存できた動画のファイルは以下のようにして再生することができます。
ffplay -f rawvideo -pixel_format yuv420p -video_size 640x360 -framerate 15 out.i420
このときの一枚のフレームの大きさは 640x360x2 = 460,800 バイトになります。
パイプのデフォルトのバッファサイズは64KBなので、8回の書き込みでやっと一枚のフレームを送ることができます。これをガツンと一回で書けるようにしたいですね。
パイプのバッファサイズの変更方法
さきほどのマニュアルページに触れられているのですが、fcntl(2)でパイプのバッファサイズを変更することができます。
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で書きました。
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_SZ
とF_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バイトを読めるようになりました。
続き
パイプに関してまだ続きがあります。
関連
Discussion