このエントリの続きです。
このエントリの内容
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カーネルは、macOSやiOSのOSで使用されるDarwinオペレーティングシステムの一部です。XNUは、X is Not Unixの頭文字をとったものです。XNUは、カーネギーメロン大学で開発されたMachカーネルとFreeBSDのコンポーネント、IOKitというドライバを書くためのC++APIを組み合わせたハイブリッドカーネルである。XNU は x86_64 で動作し、シングルプロセッサとマルチプロセッサの両方の構成で動作します。
UNIX Domain Socket の実装は FreeBSD のコンポーネントになっています。
ソースのダウンロード
tar.gz は下記からダウンロードできます
ソースを github にミラーしているリポジトリもありました
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 はなんの略称か?
軽く調べた程度ではわからず
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 という関数テーブルがありますね。以下は Linux の UNIX 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 周りの実装を追いかけるとボリュームが大きくなり過ぎるので ここらでおしまい
感想
参考
Design and Implementation of the FreeBSD Operating Systemの古い版を持っていたので斜め読みしました ( ハードカバーの古本だと安いね )
Mach は以下の書籍でちょっと知識を得たことがあるくらい。でも忘れてしまった、手元にも書籍なかった。会社の本棚にあるんだったかな
バニビンくん 元気? 参考になったよ