Linux Kernel 3.9 には SO_REUSEPORT が追加されているそうです。SO_REUSEPORT でググると日本語のページの中で一番上に出てくる当blog(2013-06-02現在)としては調査しないわけにはいきません :-)

Linux の SO_REUSEPORT は「TCP ソケットを完全重複 bind し、受け付けたコネクションをそれぞれのソケットに適当に割り振る」という機能のようです。これは、BSD の SO_REUSEPORT より機能が「強い」ということです(参考)。SO_REUSEADDR の時もそうですが、同じ名前で違う機能にするのやめてくれないかなぁ…

実験

SO_REUSEPORT が追加されたのは3月10日ごろのようなのですが、既に3か月たっていて Fedora 18 が Kernel 3.9 ベースになっており、試すためにソースコードからビルドとかはしなくても構いません。

[root@h150n000 ~]# uname -a
Linux h150n000.local 3.9.4-200.fc18.i686.PAE #1 SMP Fri May 24 20:24:58 UTC 2013 i686 i686 i386 GNU/Linux
[root@h150n000 ~]# grep -r SO_REUSEPORT /usr/include
/usr/include/asm-generic/socket.h:#define SO_REUSEPORT  15
[root@h150n000 ~]# 

というわけでテストプログラムです。子プロセスを4つ作って、それぞれが SO_REUSEPORT を付けたソケットを同一のアドレス/ポートに bind します。

[root@h150n000 ~]# cat reuse.c
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>

#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <netinet/in.h>

void child(int idx)
{
        int S, C;
        const int on = 1;
        struct addrinfo *res;
        struct addrinfo hints;
        struct sockaddr_in sin4;
        socklen_t slen;

        memset(&hints, sizeof(hints), 0);
        hints.ai_family = AF_INET;
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_protocol = IPPROTO_TCP;
        hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;
        getaddrinfo("127.0.0.1", "10000", &hints, &res);
        S = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        setsockopt(S, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));
        bind(S, res->ai_addr, res->ai_addrlen);
        freeaddrinfo(res);
        listen(S, 100);

        slen = sizeof(sin4);
        while ((C = accept(S, (struct sockaddr *)&sin4, &slen)) >= 0) {
                close(C);
                printf("accepted by child #%d\n", idx);
        }
}

int main(void)
{
        int status;
        int i;

        for (i = 0; i < 4; i++) {
                pid_t pid = fork();
                if (pid == 0) {
                        child(i);
                        return 0;
                } else if (pid < 0) {
                        perror("fork");
                        return 1;
                }
        }

        wait(&status);

        return 0;
}
[root@h150n000 ~]#

走らせて同一ホスト上から nc 127.0.0.1 10000 と連打すると、例えば以下のような結果になります。

[root@h150n000 ~]# ./reuse
accepted by child #0
accepted by child #2
accepted by child #1
accepted by child #0
accepted by child #2
accepted by child #0
accepted by child #1
accepted by child #3
accepted by child #0
accepted by child #1
accepted by child #3
accepted by child #1
accepted by child #2
accepted by child #2
accepted by child #3
accepted by child #3
accepted by child #3

おおむね適当に割り振られているように見えます。

一方、同じプログラムを BSD 上で動かすと、例えば以下のような結果になります。

[root@h150n000 ~]# ./reuse
accepted by child #3
accepted by child #3
accepted by child #3
accepted by child #3
accepted by child #3
accepted by child #3
accepted by child #3
accepted by child #3
accepted by child #3
accepted by child #3
accepted by child #3

BSD の場合は「適当に割り振る」という機能がないので、何回やっても同じソケットで accept されます。

用途

複数のプロセス(あるいはスレッド。以下同じ)で単一のアドレス/ポートを待ち受ける場合であっても、待ち受けるポートが1つで、かつ待ってる間何もしなくていい(accept でブロックしていていい)のであれば、SO_REUSEPORT は全く必要ありません。単にソケットを1つだけ作って bind し、それを全てのプロセスで accept すればいいだけです。

SO_REUSEPORT に意味が出てくるのは待ち受けるポートがたくさんある(かつ、ポートごとに担当用のプロセスを用意しない)場合です。この状態では各々のプロセスが複数のポート=ソケットの面倒を見る必要がありますが、select などで待っていると1つリクエストが来ただけで全プロセスが一斉に起き上がってしまう(でも実際に accept するのは1プロセスだけ)ため、無駄が出ます。

各プロセスが SO_REUSEPORT で全ポートにソケットを作っておけば、起きるプロセスは1つだけになります。ただし、この場合、暇なプロセスが他にあるのに忙しいプロセス(のソケット)にリクエストが到着すると残念なことになるので、その辺をちゃんと考えておかないと結局パフォーマンスが上がらないかもしれません。

# リスナーソケットの配列を渡してどれかから accept する、というシステムコールがあればいいんじゃねーの?と今思った

UDP の場合は?

試してません。

Trackback

only 1 comment untill now

  1. […] また、Linux(正確にはKernel 3.9以降)におけるSO_REUSEPORTについては、 或るプログラマの一生 » Linux Kernel 3.9 の SO_REUSEPORT linux – Socket options SO_REUSEADDR and SO_REUSEPORT, how do they differ? Do they mea […]

Add your comment now