systemdによるsocket管理

/ Linux

すぐに使えるsystemdによるsocket管理について具体例を用いて解説する。socketによるプロセス間通信とsystemdのサービスユニットに関する基本的な知識は前提となっている。

マニュアルページの要約

名称が.socketで終了するユニットファイルは、systemdのsocket-basedアクティベーションによって管理されるプロセス間通信、ネットワーク、ファイルシステムFIFOを記述する。

中略

各socketユニットについて、そのsocketに対する通信を受け付けるserviceユニットが存在していなければならない。通信を受け付けるserviceユニットの名称は、[Socket]セクション内のService=によって指定することができる。Service=が指定されていないとき、Accept=の指定によって通信を受け付けるserviceユニットの名称は次のように決定される。

  • Accept=noのとき、同一名称で末尾.serviceで終わるサービスユニット
  • Accept=yesのとき、同一名称で末尾@.serviceで終わるサービステンプレートユニット

対応するserviceユニットについて、暗黙的なWantedBy=やRequiredBy=は付与されない。.serviceまたは@.serviceはsystemdによるsocket-basedアクティベーションに限らず、(普通のサービスユニットとして)開始できるが、.serviceまたは@.serviceがsystemdによるsocket-basedアクティベーションを前提として動作する場合は明示的なWantedBy=やRequiredBy=を指定する。

socket-basedアクティベーションによって起動される.service@.serviceのデーモンプロセスはsystemdによって渡されるsocketを受け取り、accpet処理を実行する。socketの受け取りにはsd_listen_fdsを用いる。

中略

デフォルトはAccep=noとなる。このとき、systemdはsocketユニットに記述された通りにlistenとbindまで行い、デーモンプロセスに対してファイルディスクリプタを渡す。 デーモンプロセスはファイルディスクリプタを受け取り、acceptはデーモンプロセス側で行われる。 Accept=オプションがyesのとき、systemdはlistenとbindを行い、さらにacceptも行う。systemdはacceptが返したコネクション確立済みのファイルディスクリプタをデーモンプロセスに渡す。systemdは複数のコネクションを受け付けられるようにlistenとbind、およびacceptを実行し、コネクションの数だけデーモンプロセスを起動する。

パフォーマンスの観点から、これから新しくデーモンプロセスをつくる場合はAccept=noで動作するように作ることを推奨する。

行間補足:下記の場合を除いてAccept=yesは非推奨。

  • (例えば、traditionalな inetd-styleのsocket交換を用いるような)古いデーモンプロセスを再利用する場合
  • パフォーマンスを気にしない場合や、Accept=yesにおける動作そのものの実験を行う場合

事実sd_listen_fdsなどはAccept=noを前提として作られており、systemd側でaccpetを行う動作は今後特に知らなくてもよい(Accept=noとするのが基本と覚えておけばよい)。以下はAccept=noの場合だけ解説する。こちらは要約の範疇ではないので注意。

具体例を使った解説

具体的なサービスファイルの記述と、sd_listen_fds()を用いたサーバープログラムの一部を示す。

socket-basedアクティベーションを前提としたサーバープログラム

sd-sample.service

[Unit]
Description=Systemd sample service
After=sd-sample.socket
Requires=sd-sample.socket

[Service]
Type=simple
ExecStart=/usr/bin/sd-sample

[Install]
WantedBy=multi-user.target

sd-sample.socket

[Unit]
Description=Systemd sample service
PartOf=sd-sample.service

[Socket]
Accept=no
ListenStream=/run/sd-sample.socket

[Install]
WantedBy=sockets.target

.serviceのほうにAfter=およびRequires=を書くのと.socketのほうにBefore=RequiredBy=を書くのは等価のためどちらを採用してもよい。そのサービスのプロセスはsystemdによるsocket管理を前提としていることを示すため、.serviceのほうに書いたほうがわかりやすいためここではこちらを推奨する。Accept=noはデフォルトであるため記載する必要はないが、こちらも記載を推奨する。最後にPartOf=について、これは.serviceのstart/stop/restart時に.socketも併せてstart/stop/restartさせる処理となる。

このような記述のサービスに対するメインプロセスの典型的な実装例は下記。

sd-sample.c

#include <systemd/sd-daemon.h>
int accept_systemd_socket()
{
    int nfds = sd_listen_fds(0);

    /* Failure */
    if (nfds < 0) {
        return nfds;
    }

    if (nfds == 0) {
        /* Failure if no socket is passed from systemd */
        return -100;
    } else if (nfds == 1) {
        /* Exactly one socket descriptor is passed from systemd */
        int serverfd = SD_LISTEN_FDS_START;
        return accept(serverfd, NULL, NULL);
    } else {
        /* Failure */
        return -200;
    }
}

コンパイル時にはリンクオプション-lsystemdが必要になる。sd_listen_fds()関数はsystemdがそのプロセスに渡してきたsocketのFD (File Descriptor)の数を返却する。負値はerrno形式のエラーのためエラーであれば終了。ここで.socketには複数のListenStream=行を書くことができるので、書かれている行数の分だけここの戻り値は大きな数字となり、SD_LISTEN_FDS_START = 3から始まって順番に4, 5, 6,と FD数の分だけFDが対応する。

この例ではサービスファイルの記述を把握しており、そのサービスに渡されるべきFDの数がわかっている状況であるとして、それ以外の数が返された場合にはエラーで終了するようにしている。

socket-basedアクティベーションを前提としないサーバープログラム

それぞれ.serviceにて下記のようにRequires=でsocketユニットを指定しない場合には普通のサーバを起動するユニットとなる。

sd-sample.service

[Unit]
Description=Systemd sample service

[Service]
Type=simple
Environment=SERVER_SOCKET=/run/sd-sample.socket
ExecStart=/usr/bin/sd-sample

[Install]
WantedBy=multi-user.target

この場合でも.socketはいてもいなくてもよい。サーバープログラムは次のような記述となる。

sd-sample.c

#include <systemd/sd-daemon.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <stdlib.h>

int accept_systemd_socket()
{
    int nfds = sd_listen_fds(0);

    /* Failure */
    if (nfds < 0) {
        return nfds;
    }

    if (nfds == 0) {
        /* No systemd socket is passed before launching server */
        int serverfd, ret;
        struct sockaddr_un addr;

        serverfd = socket(AF_UNIX, SOCK_STREAM, 0);
        if (serverfd < 0) {
            return serverfd;
        }

        memset(&addr, 0, sizeof(addr));
        addr.sun_family = AF_UNIX;
        strcpy(addr.sun_path, getenv("SERVER_SOCKET"));

        ret = bind(serverfd, (const struct sockaddr *) &addr, sizeof(addr));
        if (ret < 0) 
            return ret;

        ret = listen(serverfd, 10);
        if (ret < 0) 
            return ret;

        return accept(serverfd, NULL, NULL);
    } else if (nfds == 1) {
        /* Exactly one socket descriptor is passed from systemd */
        int serverfd = SD_LISTEN_FDS_START;
        return accept(serverfd, NULL, NULL);
    } else {
        /* Failure */
        return -200;
    }
}

この場合でも一度sd_listen_fds()を呼ぶところは変わらないが、戻り値がゼロ、すなわちsystemdによってsocketのFDが渡されずに起動された場合には自分からソケットを作成する。この例では、あくまで例なのでストレートにacceptを実施するための処理しか記述していないので.socketの指定によりsystemdによって作成されて渡されてくるsocketと完全に動作性が同じとは限らない。また、この例では環境変数によってサーバプログラムが作成すべきソケットのパスをプログラムに渡しているが、あくまで一例であり例えば引数であってもよい。いうまでもなく引数で渡す場合には別途引数のパース処理を行う必要がある。

.socket[Socket]セクションではサーバーのソケットに関してたくさんの設定を行える。サーバープログラムはsocketの作成はsystemdによって行うと決め打ったほうが一般的に簡単になる。

まとめ

systemdによってsocket管理を行うには、.serviceに対応する.socketユニットを作成しそこにサーバーソケットの設定を書く。これによって、サーバープログラムが自身で行うべきサーバーソケットの設定部分の実装を大きく省略することができる。

リンク集