splice()とvmsplice()を試す
最近リリースされたlinux-2.6.23の変更点を見てみると、sendfile()がsplice()で実装されるようになったらしいです。splice()自体は2.6.17から追加されていることですし、そろそろsplice()を使ってもいい頃なんじゃないか!といわけで、前から気になっていたsplice()とvmsplice()を試してみました。とりあえずは「動くかどうか」だけを試し、速度は試していません。
※追記:最後に速度も試しました。
ここから長くなるので最初に蛇足しておくと、sendfile()はファイルからソケットにデータを送るわけですが、splice(2)のmanpageには、infdとoutfdのどちらかはpipeでなければならないと書いてあるので、直接splice()は使えないはず。カーネルのソースを読んでみると、fs/read_write.c の sys_sendfile() → do_sendfile() → fs/splice.c の do_splice_direct() → splice_direct_to_actor() とコールされており、splice_direct_to_actor() のコメントには↓こう書いてあります。
/** * splice_direct_to_actor - splices data directly between two non-pipes * @in: file to splice from * @sd: actor information on where to splice to * @actor: handles the data splicing * * Description: * This is a special case helper to splice directly between two * points, without requiring an explicit pipe. Internally an allocated * pipe is cached in the process, and reused during the lifetime of * that process. * */
コメントを信じれば、一度pipeを仲介しているようです。私はコメントを信じます :-p
さて、以下が実際に使ったコードの、どうでもいい部分です。まだsplice()は入っていません。
splice()/vmsplice()を
- file→file
- file→socket
- socket→file
- socket→socket
の4パターンでテストするためのコードです。
#include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/sendfile.h> #include <sys/mman.h> #include <sys/sendfile.h> #include <sys/mman.h> #include <asm/page.h> /* PAGE_SIZE, PAGE_MASK*/ /* splice(), vmsplice() */ #ifndef _GNU_SOURCE #define _GNU_SOURCE #include <fcntl.h> #undef _GNU_SOURCE #else #include <fcntl.h> #endif // #include "splice_compat.h" /* splice()がfcntl.hで定義されていない場合用。Wikipedia(en)のspliceの項より自作 */ /* infd_tcp_socket()で使う待ち受けポート */ #define LISTEN_PORT 8000 /* outfd_tcp_socket()で使う宛先アドレス */ #define DEST_ADDR "192.168.0.4" /* outfd_tcp_socket()で使う宛先ポート */ #define DEST_PORT 8000 /* outfd_file()で使うファイル名(自動的に作られる) */ #define OUTPUT_FILE "splice_test_output_file.img" /* infd_file()で使うファイル名(事前に用意しておく) */ #define INPUT_FILE "splice_test_input_file.img" #define BUFFER_SIZE (128*1024) /* 手抜き用終了コード */ void perror_exit(const char* s, int status) { perror(s); exit(status); } /* socket -> x 用のファイルディスクリプタを作る */ int infd_tcp_socket(void) { int lsock, infd; struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_port = htons(LISTEN_PORT); addr.sin_addr.s_addr = INADDR_ANY; lsock = /* いつものようにsocket()→bind()→listen() … */ // …略… printf("accept...\n"); infd = accept(lsock, NULL, NULL); if (infd < 0) perror_exit("accept()", 1); printf("accept ok\n"); close(lsock); return infd; } /* x -> socket 用のファイルディスクリプタを作る */ int outfd_tcp_socket(void) { int outfd; struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(DEST_PORT); outfd = socket(AF_INET, SOCK_STREAM, 0); if (outfd < 0) perror_exit("socket()", 1); if ( !inet_aton(DEST_ADDR, &addr.sin_addr) ) perror_exit("inet_aton()", 1); if (connect(outfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) perror_exit("connect()", 1); return outfd; } /* file -> x 用のファイルディスクリプタを作る */ int infd_file(void) { int infd = open(INPUT_FILE, O_RDONLY, 0644); if (infd < 0) perror_exit("open()", 1); return infd; } /* x -> file 用のファイルディスクリプタを作る */ int outfd_file(void) { int outfd = open(OUTPUT_FILE, O_WRONLY | O_CREAT, 0644); if (outfd < 0) perror_exit("open()", 1); return outfd; } /* main() */ int main(void) { int infd, outfd; /* 実験ごとにコメントアウトを外して再コンパイル */ infd = infd_file(); //infd = infd_tcp_socket(); outfd = outfd_file(); //outfd = outfd_tcp_socket(); printf("fd ok.\n"); /* 本体を呼ぶ…後述… */ test_read_write(infd, outfd); //test_sendfile(infd, outfd); //test_splice(infd, outfd); //test_splice_pipe_splice(infd, outfd); //test_mmap_read_vmsplice_gift_pipe_splice(infd, outfd); return 0; }
このコードを使って、
- 普通にreadしてwrite
- 直接sendfile
- 直接splice
- パイプを介して、splice-pipe-splice
- バッファにread()し、バッファからpipeにvmsplice()して、pipeからsplice()
を試していきます。
socketでパケットを受け取るときは、別のマシンからncを使ってデータを送信しました。
[[email protected]]$ dd if=somefile | nc target.host.local. 8000
socketでパケットを送るときは、同じく別のマシンでncを使ってデータを受け取りました。
[[email protected]]$ nc -l -p 8000 > file
以下、環境は Linux vcore 2.6.23 #1 SMP Thu Oct 11 13:52:05 JST 2007 x86_64 AMD Athlon(tm) 64 X2 Dual Core Processor 5000+ GNU/Linux です。
普通にread-write
まずは普通にread()してwrite()してみます。
void test_read_write(int infd, int outfd) { char buf[BUFFER_SIZE]; ssize_t rlen, wlen; ssize_t written; while(1) { rlen = read(infd, buf, BUFFER_SIZE); if (rlen <= 0) perror_exit("read()", 1); written = 0; do { wlen = write(outfd, buf, rlen - written); if (wlen <= 0) perror_exit("write()", 1); written += wlen; } while (written < rlen); } }
当然ですが、これはすべてのパターンで動きました。
file→file | ○ |
file→socket | ○ |
socket→file | ○ |
socket→socket | ○ |
直接sendfile
sendfile()でinfdからoutfdに送ってみます。
void test_splice(int infd, int outfd) { while (splice(infd, NULL, outfd, NULL, BUFFER_SIZE, 0) > 0) ; perror_exit("splice()", 1); }
やっぱりfile→socketでしか動きません。その他ではInvalid argumentと言われてしまいます。
file→file | - |
file→socket | ○ |
socket→file | - |
socket→socket | - |
直接splice
直接splice()してみます。
void test_splice(int infd, int outfd) { while (splice(infd, NULL, outfd, NULL, BUFFER_SIZE, 0) > 0) ; perror_exit("splice()", 1); }
どのパターンでも動きません。まったくデータが送られることなくInvalid argumentです。どちらもpipeではないので、これは予想通り。
file→file | - |
file→socket | - |
socket→file | - |
socket→socket | - |
splice-pipe-splice
infdからpipeにsplice()し、pipeからoutfdにsplice()します。
void test_splice_pipe_splice(int infd, int outfd) { int pipedes[2]; ssize_t rlen, wlen; ssize_t written; if (pipe(pipedes) < 0) perror_exit("pipe()", 1); while(1) { rlen = splice(infd, NULL, pipedes[1], NULL, BUFFER_SIZE, SPLICE_F_MOVE); if (rlen <= 0) perror_exit("read splice()", 1); written = 0; do { wlen = splice(pipedes[0], NULL, outfd, NULL, rlen - written, SPLICE_F_MOVE); if (wlen <= 0) perror_exit("write splice()", 1); written += wlen; } while (written < rlen); } }
予想に反して、socketからpipeへのsplice()はダメなようです。Invalid argumentと言われてしまいます。fileからpipeへ、pipeからfileへ、pipeからsocketへのsplie()はうまく動きます。
file→file | ○ |
file→socket | ○ |
socket→file | - |
socket→socket | - |
mmap-read-vmsplice-gift-pipe-splice
infdからバッファへread()し、バッファからpipeにvmsplice()し、pipeからoutfdにsplice()します。
方針としては↓こうなのですが…
void test_read_vmsplice_copy_pipe_splice(int infd, int outfd) { char buf[BUFFER_SIZE]; int pipedes[2]; ssize_t rlen, vm_len, sp_len; ssize_t vm_written, sp_written; struct iovec vec; if (pipe(pipedes) < 0) perror_exit("pipe()", 1); while(1) { rlen = read(infd, buf, BUFFER_SIZE); if (rlen <= 0) perror_exit("read()", 1); vm_written = 0; do { vec.iov_base = buf + vm_written; /* ※追記: 修正 */; vec.iov_len = rlen - vm_written; vm_len = vmsplice(pipedes[1], &vec, 1, SPLICE_F_GIFT); /* ↑動かない! */ if (vm_len <= 0) perror_exit("vmsplice()", 1); sp_written = 0; do { sp_len = splice(pipedes[0], NULL, outfd, NULL, vm_len - sp_written, SPLICE_F_MOVE); if (sp_len <= 0) perror_exit("splice()", 1); sp_written += sp_len; } while(sp_written < vm_len); vm_written += vm_len; } while (vm_written < rlen); } }
このコードは動きません。vmsplice(2)に、vmsplice()でSPLICE_F_GIFTフラグを使うには、「データはメモリ上でページ境界にあっていなければならず、 長さもページ境界の倍数でなければならない」と書いてあります。実際、Invalid argumentになります。しかしSPLICE_F_GIFTフラグをオフにしてしまうと、コピーが発生してしまうらしいので、わざわざsplice()を使う意味がなさそうです。
というわけで、ページ境界に合わせるように修正したコードが↓これです。
void test_mmap_read_vmsplice_gift_pipe_splice(int infd, int outfd) { void *buf; int pipedes[2]; ssize_t rlen, vm_len, sp_len; ssize_t vm_written, sp_written; struct iovec vec; if (pipe(pipedes) < 0) perror_exit("pipe()", 1); /* ページ境界に合ったメモリを取得 */ buf = mmap(NULL, BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (!buf) perror_exit("mmap()", 1); while(1) { /* 端数切り上げ時にオーバーフローしないように最大サイズを制限 */ rlen = read(infd, buf, BUFFER_SIZE - PAGE_SIZE + 1); if (rlen <= 0) perror_exit("read()", 1); vm_written = 0; do { vec.iov_base = buf; /* 端数切り上げでページ境界に合わせる */ vec.iov_len = (rlen - vm_written + PAGE_SIZE - 1) & PAGE_MASK; vm_len = vmsplice(pipedes[1], &vec, 1, SPLICE_F_GIFT); if (vm_len <= 0) perror_exit("vmsplice()", 1); sp_written = 0; do { sp_len = splice(pipedes[0], NULL, outfd, NULL, vm_len - sp_written, SPLICE_F_MOVE); if (sp_len <= 0) perror_exit("splice()", 1); sp_written += sp_len; } while(sp_written < vm_len); vm_written += vm_len; } while (vm_written < rlen); } }
※追記:これも間違ってました。正しくは↓これです。
void test_mmap_read_vmsplice_gift_pipe_splice(int infd, int outfd) { void *buf; int pipedes[2]; ssize_t rlen, vm_len, sp_len; ssize_t vm_written, sp_written; struct iovec vec; ssize_t page_over; if (pipe(pipedes) < 0) perror_exit("pipe()", 1); /* ページ境界に合ったメモリを取得 */ /* 端数切り上げでオーバーフローしないように多めに確保 */ buf = mmap(NULL, BUFFER_SIZE + PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (!buf) perror_exit("mmap()", 1); while(1) { /* ここでreadするバッファサイズはページ境界に合っていた方が効率が良い */ rlen = read(infd, buf, BUFFER_SIZE); if (rlen <= 0) perror_exit("read()", 1); vm_written = 0; do { vec.iov_base = buf + vm_written; /* 修正 */ /* 端数切り上げでページ境界に合わせる */ vec.iov_len = (rlen - vm_written + PAGE_SIZE - 1) & PAGE_MASK; vm_len = vmsplice(pipedes[1], &vec, 1, SPLICE_F_GIFT); if (vm_len <= 0) perror_exit("vmsplice()", 1); /* 切り上げ分を元に戻す */ if (vm_len > (rlen - vm_written)) { page_over = vm_len - (rlen - vm_written); vm_len = rlen - vm_written; } else { page_over = 0; } sp_written = 0; do { sp_len = splice(pipedes[0], NULL, outfd, NULL, vm_len - sp_written, SPLICE_F_MOVE); if (sp_len <= 0) perror_exit("splice()", 1); sp_written += sp_len; } while(sp_written < vm_len); /* pipeに残った分を捨てる(捨てないとバッファが一杯になってしまう) */ if (page_over) read(pipedes[0], buf, page_over); vm_written += vm_len; } while (vm_written < rlen); } }
うーむ。システムコールが多い…単純にwriteした方が速い?
メモリをmmap()のMAP_ANONYMOUSで確保することで、開始位置をページ境界に合わせています。長さはPAGE_SIZEを足してPAGE_MASKでマスクすることでページ境界に合わせています。
このコードはすべてのパターンで動きました。
file→file | ○ |
file→socket | ○ |
socket→file | ○ |
socket→socket | ○ |
まとめ
以上のパターンをまとめると、↓こうなります。
read-write | 直接sendfile | 直接splice | splice-pipe-splice | mmap-read-vmsplice-pipe-splice | |
file→file | ○ | - | - | ○ | ○ |
file→socket | ○ | ○ | - | ○ | ○ |
socket→file | ○ | - | - | - | ○ |
socket→socket | ○ | - | - | - | ○ |
以上で終わりです。
それぞれの速度も測ってみたいところですが…どなたかやりませんか?
今回のソースコード(修正版)はここからダウンロードできます。
※追記:速度を測ってみました…が、どれもほとんど同じです。測り方が悪い?
1GBのファイルを作成し、それをread-write、splice-pipe-splice、mmap-read-vmsplice-gift-pipe-splice、read-vmsplice-copy-pipe-spliceでコピーしてみました(file→file)。
環境は先ほどと同じ Linux vcore 2.6.23 #1 SMP Thu Oct 11 13:52:05 JST 2007 x86_64 AMD Athlon(tm) 64 X2 Dual Core Processor 5000+ GNU/Linux で、メモリは4GBです。
read-write
$ for a in `seq 1 3`; do rm splice_test_output_file.img -f; time ./a.out ; done Command exited with non-zero status 1 0.00user 1.89system 0:08.86elapsed 21%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+146minor)pagefaults 0swaps Command exited with non-zero status 1 0.00user 2.13system 0:10.68elapsed 19%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+146minor)pagefaults 0swaps Command exited with non-zero status 1 0.00user 1.95system 0:08.79elapsed 22%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+146minor)pagefaults 0swaps Command exited with non-zero status 1
splice-pipe-splice
$ for a in `seq 1 3`; do rm splice_test_output_file.img -f; time ./a.out ; done Command exited with non-zero status 1 0.00user 2.29system 0:10.83elapsed 21%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+115minor)pagefaults 0swaps Command exited with non-zero status 1 0.00user 2.10system 0:08.93elapsed 23%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+115minor)pagefaults 0swaps Command exited with non-zero status 1 0.00user 2.38system 0:07.05elapsed 33%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+115minor)pagefaults 0swaps
mmap-read-vmsplice-gift-pipe-splice
$ for a in `seq 1 3`; do rm splice_test_output_file.img -f; time ./a.out ; done Command exited with non-zero status 1 0.00user 2.52system 0:09.10elapsed 27%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+147minor)pagefaults 0swaps Command exited with non-zero status 1 0.00user 2.37system 0:08.90elapsed 26%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+148minor)pagefaults 0swaps Command exited with non-zero status 1 0.00user 2.54system 0:10.79elapsed 23%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+147minor)pagefaults 0swaps
read-vmsplice-copy-pipe-splice
$ for a in `seq 1 3`; do rm splice_test_output_file.img -f; time ./a.out ; done Command exited with non-zero status 1 0.00user 2.60system 0:09.46elapsed 27%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+147minor)pagefaults 0swaps Command exited with non-zero status 1 0.00user 2.47system 0:08.97elapsed 27%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+146minor)pagefaults 0swaps Command exited with non-zero status 1 0.00user 2.77system 0:10.92elapsed 25%CPU (0avgtext+0avgdata 0maxresident)k 0inputs+0outputs (0major+146minor)pagefaults 0swaps
うーむ。ほとんど同じです。
ファイルのサイズを2GBに変えてもう一回やろうと思ったところ…「XFS metadata write error block 0x40 in dm-8」と言われました(笑
わー笑えない!余力でcat /proc/mdstatを見ると、RAID10のアレイが[UU__]に!あー!