pipe, splice, sendfile, vmsplice, tee

ファイルから読んだデータを、CPUで加工せずにソケットに送りたいとする。

read(in_fd, buffer, 4096);
write(out_fd, buffer, 4096);

とか、やりたくなるが、これは実際には非常に無駄で、read 時に、ファイルシステムキャッシュからユーザ空間へコピー、ユーザ空間からskbへコピーしている。

まあ1Gb Etherなら全然大したオーバーヘッドにはならないけど(それがなるんだよなぁ…(なんで?))、NVMe Flash から、100Gb イーサに送るとかすると、memcpy が一回増えるだけで無視できないオーバーヘッドになるので、無駄がある。


そこで sendfile というシステムコールがある。

上のread,writeは

sendfile(out_fd, in_fd, NULL, 4096);

とか書くと、カーネル内のページをなんかうまくやってくれる。
例えば、in_fd がregular ファイルで、out_fd がソケットだった場合、ファイルシステムキャッシュに使われてるページメモリが、そのままイーサネットドライバに渡される。


このsendfileの処理をもう少し分割したspliceというシステムコールがある。

キャッシュシステムを自分で持っていて、ファイルシステムキャッシュが必要無いアプリケーションの場合、キャッシュを自分で管理したい場合があるかもしれない。そういう場合、Linux では、POSIX の pipe をカーネル内のページへのポインタとして使うことができる。


splice で、出力先をpipeにすると、そのpipeが、入力fdのカーネル内ページキャッシュのページを保持するようになる。

splice で、入力元をpipeにすると、pipeが保持しているポインタをドライバとかに直接送れる。

vmsplice すると、ユーザ空間に置いてあるメモリを、カーネル内からpipeで参照できるようになる。

tee するとpipeが指してるポインタを別のpipeにコピーできる。


一個のpipeが保持できるバッファサイズは標準だと64KBだけど、fcntl(pipe, F_SETPIPE_SZ, 1024*1024) とかすると変えられる(CAP_SYS_RESOURCE 権限が無いとサイズ制限が付く)


sendfileは実際には、

splice(in_fd, NULL, 
       current->pipe, NULL, 4096, 0); // current->pipe が in_fd のページキャッシュを保持する
splice(current->pipe, NULL,
       out_fd, NULL, 4096, 0); // current->pipe が保持しているページを out_fd へ

みたいな処理になっている。


(あとで計測したい)