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 へ
みたいな処理になっている。
(あとで計測したい)