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を使ってデータを送信しました。

[user@192.168.0.4]$ dd if=somefile | nc target.host.local. 8000

socketでパケットを送るときは、同じく別のマシンでncを使ってデータを受け取りました。

[user@192.168.0.4]$ 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__]に!あー!