UNIX Domain Socket の SO_SNDBUF, SO_RCVBUF についての覚書 (2) - macOS XNU のnet.local.stream.sendspace 周辺のソースを読む

hiboma.hatenadiary.jp

このエントリの続きです。

このエントリの内容

macOSカーネル ( XNU ) をソースで sysctl net.local.stream.sendspace の周辺のコードを眺める内容です。

私はBSD系のカーネルに馴染みがないので、手探りで調べつつ内容を書き記した内容になります。詳細・正確な記述・説明を読みたい場合は、別のサイトや文献を当たってください。

macOSカーネルのソース

macOSカーネルは XNU として公開されています。ライセンスは Apple Public Source License 2.0

XNU is 何?

XNU kernel is part of the Darwin operating system for use in macOS and iOS operating systems. XNU is an acronym for X is Not Unix. XNU is a hybrid kernel combining the Mach kernel developed at Carnegie Mellon University with components from FreeBSD and a C++ API for writing drivers called IOKit. XNU runs on x86_64 for both single processor and multi-processor configurations.

https://github.com/apple/darwin-xnu の README.md から引用

以下は DeepL による翻訳

XNUカーネルは、macOSiOSのOSで使用されるDarwinオペレーティングシステムの一部です。XNUは、X is Not Unixの頭文字をとったものです。XNUは、カーネギーメロン大学で開発されたMachカーネルFreeBSDコンポーネント、IOKitというドライバを書くためのC++APIを組み合わせたハイブリッドカーネルである。XNU は x86_64 で動作し、シングルプロセッサとマルチプロセッサの両方の構成で動作します。

UNIX Domain Socket の実装は FreeBSDコンポーネントになっています。

ソースのダウンロード

tar.gz は下記からダウンロードできます

opensource.apple.com

ソースを github にミラーしているリポジトリもありました

github.com

net.local.stream.sendspace が定義されている場所を探す

sendspace あたりの適当なワードで grep (ag) をかけまくって調べました。

どうやら bsd/kern/uipc_usrreq.c が UNIX Domain Socket の実装を扱っているんだと絞り込めました。XNU のソースのうち、BSD 由来の実装は bsd ディレクトリ以下に収まってるんですね。

/*
 * Both send and receive buffers are allocated PIPSIZ bytes of buffering
 * for stream sockets, although the total for sender and receiver is
 * actually only PIPSIZ.
 * Datagram sockets really use the sendspace as the maximum datagram size,
 * and don't really want to reserve the sendspace.  Their recvspace should
 * be large enough for at least one max-size datagram plus address.
 */
#ifndef PIPSIZ
#define PIPSIZ  8192
#endif
static u_int32_t        unpst_sendspace = PIPSIZ; 👈
static u_int32_t        unpst_recvspace = PIPSIZ; 
static u_int32_t        unpdg_sendspace = 2 * 1024;       /* really max datagram size */
static u_int32_t        unpdg_recvspace = 4 * 1024;

static int      unp_rights;                     /* file descriptors in flight */
static int      unp_disposed;                   /* discarded file descriptors */

SYSCTL_DECL(_net_local_stream);
SYSCTL_INT(_net_local_stream, OID_AUTO, sendspace, CTLFLAG_RW | CTLFLAG_LOCKED,
    &unpst_sendspace, 0, ""); 👈
SYSCTL_INT(_net_local_stream, OID_AUTO, recvspace, CTLFLAG_RW | CTLFLAG_LOCKED,
    &unpst_recvspace, 0, "");

ユーザ空間で sysctl net.local.stream.sendspace として扱う値は、カーネル空間では u_int32_t unpst_sendspace に収まっているようです。sysctl の値が static 変数になっているのは Linux とおんなじ感じですね。

net.local.stream.sendspace のデフォルト値

unpst_sendspace のデフォルト値は PIPSIZ 8192 になっており、先のエントリで getsockopt(2) で読み出した値と一致しています。

sysctl の定義マクロ

SYSCTL_DECL, SYSCTL_INT のマクロも、sysctl を定義しているであろうと、もなんとなく類推がつきます

  • SYSCTL_DECL(_net_local_stream) で名前の 階層? namespace? を区切ってる
  • SYSCTL_INT(_net_local_stream, ... &unpst_sendspace ) で sysctl の型、値を扱う変数の指定、属性の指定を行う
  • CTLFLAG_RW
    • sysctl の値はユーザ空間から読み書き可能
  • OID_AUTO
    • SNMP の OID = Object IDentifier の指定をよしなに任せる?
  • CTLFLAG_LOCKED
    • よくわからん。値を読み書きする際に、ロックを取った状態で扱う指定か?

という感じでしょうか。脱線するのでマクロの詳細までは見ません。

unpst はなんの略称か?

  • UNIX Protocol Stream か?
  • UNIX Process Steream か?

軽く調べた程度ではわからず

uipc_usrreq はなんの略称か?

  • UNIX IPC User Request か?

軽く調べた程度ではわからず

net.local.stream.sendspace ( unpst_sendspace ) が参照されるコード

unp_attach()という関数の中で soreserve(so, unpst_sendspace, unpst_recvspace); として参照しています。1️⃣ の部分です

static int
unp_attach(struct socket *so)
{
    struct unpcb *unp;
    int error = 0;

    if (so->so_snd.sb_hiwat == 0 || so->so_rcv.sb_hiwat == 0) {
        switch (so->so_type) {
        case SOCK_STREAM:
            error = soreserve(so, unpst_sendspace, unpst_recvspace); 1️⃣
            break;

        case SOCK_DGRAM:
            error = soreserve(so, unpdg_sendspace, unpdg_recvspace);
            break;

        default:
            panic("unp_attach");
        }
        if (error) {
            return error;
        }
    }
    unp = (struct unpcb *)zalloc(unp_zone);
    if (unp == NULL) {
        return ENOBUFS;
    }
    bzero(unp, sizeof(*unp));

    lck_mtx_init(&unp->unp_mtx, &unp_mtx_grp, &unp_mtx_attr);

    lck_rw_lock_exclusive(&unp_list_mtx);
    LIST_INIT(&unp->unp_refs);
    unp->unp_socket = so; 2️⃣
    unp->unp_gencnt = ++unp_gencnt;
    unp_count++;
    LIST_INSERT_HEAD(so->so_type == SOCK_DGRAM ?
        &unp_dhead : &unp_shead, unp, unp_link);
    lck_rw_done(&unp_list_mtx);
    so->so_pcb = (caddr_t)unp; 3️⃣
    /*
    * Mark AF_UNIX socket buffers accordingly so that:
    *
    * a. In the SOCK_STREAM case, socket buffer append won't fail due to
    *    the lack of space; this essentially loosens the sbspace() check,
    *    since there is disconnect between sosend() and uipc_send() with
    *    respect to flow control that might result in our dropping the
    *    data in uipc_send().  By setting this, we allow for slightly
    *    more records to be appended to the receiving socket to avoid
    *    losing data (which we can't afford in the SOCK_STREAM case).
    *    Flow control still takes place since we adjust the sender's
    *    hiwat during each send.  This doesn't affect the SOCK_DGRAM
    *    case and append would still fail when the queue overflows.
    *
    * b. In the presence of control messages containing internalized
    *    file descriptors, the append routines will not free them since
    *    we'd need to undo the work first via unp_dispose().
    */
    so->so_rcv.sb_flags |= SB_UNIX;
    so->so_snd.sb_flags |= SB_UNIX;
    return 0;
}

soreserve() の詳細は追いかけませんが、 struct sock, struct sockbuf に unpst_sendspace の値を突っ込んでる様子。

unp_attach is ?

unp_attach は struct socket (ソケットのインタフェース) と protocol ( プロトコル ) を結びつける実装のようです 2️⃣ 3️⃣

struct socket が ソケットのtインタフェースとなって抽象化を果たして、プロトコルを切り替えられる設計になっています。 ( インタフェースを変えずに裏の実装を変えるやり方は GoFデザインパターンでいうと Strategy パターン に相当するか? )

Linux でも struct socket, struct sock で同様の設計をしているので、類推して理解できます。

protocol-switch table

unp_attach() は上位の関数 uipc_attach()で呼び出されており、uipc_attach() はさらに uipc_usrreqs という構造体で関数テーブルになっている。

struct  uipc_usrreqs = {
    .pru_abort =            uipc_abort,
    .pru_accept =           uipc_accept,
    .pru_attach =           uipc_attach,
    .pru_bind =             uipc_bind,
    .pru_connect =          uipc_connect,
    .pru_connect2 =         uipc_connect2,
    .pru_detach =           uipc_detach,
    .pru_disconnect =       uipc_disconnect,
    .pru_listen =           uipc_listen,
    .pru_peeraddr =         uipc_peeraddr,
    .pru_rcvd =             uipc_rcvd,
    .pru_send =             uipc_send,
    .pru_sense =            uipc_sense,
    .pru_shutdown =         uipc_shutdown,
    .pru_sockaddr =         uipc_sockaddr,
    .pru_sosend =           sosend,
    .pru_soreceive =        soreceive,
};

accept(2) なら uipc_accept , listen(2) なら uipc_listen と行った感じで ソケットのシステムコール (とカーネル内部の処理 ) に対応するように、UNIX Domain Socket の実装が並んでいる。

さらに uipc_usrreqs は struct protosw に収まっています。 BSD カーネルでは protocol-switch table と呼ぶ構造体のようです AF_UNIX で SOCK_STREAM, SOCK_DGRAM を実装しているのが uipc_usrreqs なのだと理解できました。

static struct protosw localsw[] = {
    {
        .pr_type =      SOCK_STREAM,
        .pr_flags =     PR_CONNREQUIRED | PR_WANTRCVD | PR_RIGHTS | PR_PCBLOCK,
        .pr_ctloutput = uipc_ctloutput,
        .pr_usrreqs =   &uipc_usrreqs, 👈
        .pr_lock =      unp_lock,
        .pr_unlock =    unp_unlock,
        .pr_getlock =   unp_getlock
    },
    {
        .pr_type =      SOCK_DGRAM,
        .pr_flags =     PR_ATOMIC | PR_ADDR | PR_RIGHTS,
        .pr_ctloutput = uipc_ctloutput,
        .pr_usrreqs =   &uipc_usrreqs,
        .pr_lock =      unp_lock,
        .pr_unlock =    unp_unlock,
        .pr_getlock =   unp_getlock
    },
    {
        .pr_ctlinput =  raw_ctlinput,
        .pr_usrreqs =   &raw_usrreqs,
    },
};

Linux でも同様に struct proto_ops という関数テーブルがありますね。以下は LinuxUNIX Domain Socket の関数を集めた構造体です。

static const struct proto_ops unix_stream_ops = {
    .family =   PF_UNIX,
    .owner =    THIS_MODULE,
    .release =  unix_release,
    .bind =     unix_bind,
    .connect =  unix_stream_connect,
    .socketpair =   unix_socketpair,
    .accept =   unix_accept,
    .getname =  unix_getname,
    .poll =     unix_poll,
    .ioctl =    unix_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = unix_compat_ioctl,
#endif
    .listen =   unix_listen,
    .shutdown = unix_shutdown,
    .setsockopt =   sock_no_setsockopt,
    .getsockopt =   sock_no_getsockopt,
    .sendmsg =  unix_stream_sendmsg,
    .recvmsg =  unix_stream_recvmsg,
    .mmap =     sock_no_mmap,
    .sendpage = unix_stream_sendpage,
    .splice_read =  unix_stream_splice_read,
    .set_peek_off = unix_set_peek_off,
};

Linux には、BSD の protocol-switch table 相当の構造体はないんだったっかな?


ここらの設計は GoFデザインパターンでいうと Tempalte Method パターンに近いのかなと思う ( Strategy な気もするが )


.... socket 周りの実装を追いかけるとボリュームが大きくなり過ぎるので ここらでおしまい

感想

  • 慣れないソースコードだととっかかりを見つけるのが難しい
    • sendspace が特殊な名前で、grep で探しやすいことで助かった
  • Linux と似たような設計をしている箇所は勘で読み進められる

参考

Design and Implementation of the FreeBSD Operating Systemの古い版を持っていたので斜め読みしました ( ハードカバーの古本だと安いね )

Mach は以下の書籍でちょっと知識を得たことがあるくらい。でも忘れてしまった、手元にも書籍なかった。会社の本棚にあるんだったかな

バニビンくん 元気? 参考になったよ

feneshi.co