FreeBSD kernel SOCKET I/F 探検

第7回 socket() システムコール (5)

2014.01.16

必要とする知識など:
  • UNIX (含む Linux) 上での C 言語と SOCKET によるプログラミング経験および TCP/IP に関する知識。

socket() システムコール、五回目です。socreate() で soalloc() を呼び出すところからです。

prison_check_af() の次は、soalloc() の呼び出しです。

so = soalloc(CRED_TO_VNET(cred));

CRED_TO_VNET は、/usr/src/sys/net/vnet.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/net/vnet.h?view=markup) で以下のように定義されています。

#define       CRED_TO_VNET(cr)        (cr)->cr_prison->pr_vnet

ここで渡している pr_vnet は soalloc(struct vnet *vnet) で受け取りますが、soalloc() 内で vnet を使用している処理は #ifdef VIMAGE ~ #endif で囲まれています。GENERIC kernel [1] のコンパイル時に VIMAGE は指定されていないようなので、この pr_vnet に関しては割愛します。また、今後 #ifdef VIMAGE ~ #endif で囲まれている部分が出現した場合、説明は割愛します。

soalloc() は、socreate() と同じ uipc_socket.c (http://svnweb.freebsd.org/base/release/9.1.0/sys/kern/uipc_socket.c?view=markup) で以下のように定義されています。

static struct socket *
soalloc(struct vnet *vnet)

soalloc() の処理を以下に示します。

struct socket *so;

so = uma_zalloc(socket_zone, M_NOWAIT | M_ZERO);
if (so == NULL)
        return (NULL);

uma_zmalloc() を使用して、struct socket 用の領域を確保します。struct socket は、/usr/src/sys/sys/socketvar.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/sys/socketvar.h?view=markup) で定義されています。

次の処理をまとめて掲載します。

  SOCKBUF_LOCK_INIT(&so->so_snd, "so_snd");
  SOCKBUF_LOCK_INIT(&so->so_rcv, "so_rcv");
  sx_init(&so->so_snd.sb_sx, "so_snd_sx");
  sx_init(&so->so_rcv.sb_sx, "so_rcv_sx");
  TAILQ_INIT(&so->so_aiojobq);
  mtx_lock(&so_global_mtx);
  so->so_gencnt = ++so_gencnt;
  ++numopensockets;
#ifdef VIMAGE
  VNET_ASSERT(vnet != NULL, ("%s:%d vnet is NULL, so=%p",
      __func__, __LINE__, so));
  vnet->vnet_sockcnt++;
  so->so_vnet = vnet;
#endif
  mtx_unlock(&so_global_mtx);
  return (so);

SOCKBUF_LOCK_INIT() は、/usr/src/sys/sockbuf.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/sys/sockbuf.h?view=markup) で以下のように定義されています。

#define SOCKBUF_LOCK_INIT(_sb, _name) \
        mtx_init(SOCKBUF_MTX(_sb), _name, NULL, MTX_DEF)

SOCKBUF_LOCKINIT() は、struct socket 内に含まれる送受信バッファ struct sockbuf so_recv/so_snd 内に含まれるミューテックス struct mtx sb_mtx の初期化を行なうために、mtx_init() を呼び出します。

mtx_init() は、/usr/src/sys/kern/kern_mutex.c (http://svnweb.freebsd.org/base/release/9.1.0/sys/kern/kern_mutex.c?view=markup) で定義されています。ミューテックスは、マルチスレッドプログラミングで扱うミューテックスと同義のもので、ここでは送受信バッファへのアクセス競合に対する排他制御に用いています。mtx_init() 内部の詳細は割愛します。

SOCKBUF_LOCKINIT() の次は sx_init() です。

sx_init(&so->so_snd.sb_sx, "so_snd_sx");
sx_init(&so->so_rcv.sb_sx, "so_rcv_sx");

sx_init() は、/usr/src/sys/sys/sx.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/sys/sx.h?view=markup) で以下のように定義されています。

#define sx_init(sx, desc)       sx_init_flags((sx), (desc), 0)

sx.h は sx lock という機能を提供するようです。以下は /usr/src/sys/sys/sx.h の先頭に書かれているコメントですが、

/*
 * In general, the sx locks and rwlocks use very similar algorithms.
 * The main difference in the implementations is how threads are
 * blocked when a lock is unavailable. For this, sx locks use sleep
 * queues which do not support priority propagation, and rwlocks use
 * turnstiles which do.

「Read/Write ロックと似ているが、ロックを獲得出来なかった場合スリープキューを使用する。Read/Write ロックでは turnstiles を使用する。」 turnstile は、英和辞典によると「回転ドア、回転式改札口」となっていますが、詳細は分かりかねます (http://ja.wikipedia.org/wiki/%E3%83%93%E3%82%B8%E3%83%BC%E3%82%A6%E3%82%A7%E3%82%A4%E3%83%88 これのことか?)。取り敢えず sx lock とはスリープキューを使用するロックのようです。Read/Write ロックに関しては http://www.soum.co.jp/misc/tatuhiko/multi-thread/index.html#read-write を参照してください。

sx_init() 内部の詳細は割愛します。

なお、struct sockbuf は mtx と sx の二つのロックを保持していますが、なぜ二つ必要なのかは現時点では分かりません。ソースの解析を進めていくと判明するのかもしれません。

次は、TAILQ_INIT() です。

TAILQ_INIT(&so->so_aiojobq);

TAILQ_INIT() に渡している so->so_aiojobq は、struct socekt のメンバ変数ですが、以下のように定義されています。

struct socket {
        ...
        TAILQ_HEAD(, aiocblist) so_aiojobq; /* AIO ops waiting on socket */

TAILQ_HEAD() は、/usr/src/sys/sys/queue.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/sys/queue.h?view=markup) で以下のように定義されています。

#define TAILQ_HEAD(name, type)                                          \
struct name {                                                           \
        struct type *tqh_first; /* first element */                     \
        struct type **tqh_last; /* addr of last next element */         \
        TRACEBUF                                                        \
}

TAILQ_HEAD() で使用している TRACEBUF も同じ queue.h で以下のように定義されています。

#ifdef QUEUE_MACRO_DEBUG
...
#else
#define TRACEBUF
#endif  /* QUEUE_MACRO_DEBUG */

これはデバッグ時に有効になるようなので、今回は QUEUE_MACRO_DEBUG が定義されていないものとみなします。

TAILQ_HEAD() を展開すると、上記の struct socket は以下のようになります。

struct socket {
        ...
        struct {
                struct aiocblist *fqh_first;
                struct aiocblist **fqh_last;
        } so_aiojobq;

困ったことに、struct aiocblist を定義しているヘッダーファイルが存在しません。が、/usr/src/sys/kern/vfs_aio.c (http://svnweb.freebsd.org/base/release/9.1.0/sys/kern/vfs_aio.c?view=markup) で定義されています。

これはどういうことかというと、簡単な例ですが...

$ cat tmp.c
#define NULL 0
struct {
        struct notdefined *first;
        struct notdefined **last;
} queue = {NULL, NULL};
$ cc -c tmp.c
$

このように未定義の構造体タグ名のポインタの定義は、コンパイルエラーにはなりません。が、当然「queue.first->メンバ変数名」と記述するとコンパイルエラーになります。struct aiocblist に関しては、実際に struct aiocblist を操作するであろう vfs_aio.c が struct aiocblist の詳細を把握してさえいれば、そのポインタを保持している側 (ここでは struct socket) は、struct aiocblist の詳細を知らなくても、何ら問題ありません。

長くなってしまいましたが、TAILQ_INIT() の説明に戻ります。

TAILQ_INIT(&so->so_aiojobq);

TAILQ_INIT() は、/usr/src/sys/sys/queue.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/sys/queue.h?view=markup) で以下のように定義されています。なお、TAILQ_INIT() が使用している TAIL_QUEUE_HEAD(), QMD_TRACE_HEAD() も同じ queue.h で定義されているので、合わせて掲載します。

#ifdef QUEUE_MACRO_DEBUG
...
#else
#define QMD_TRACE_HEAD(head)
#endif  /* QUEUE_MACRO_DEBUG */

#define TAILQ_FIRST(head)       ((head)->tqh_first)

#define TAILQ_INIT(head) do {                                           \
        TAILQ_FIRST((head)) = NULL;                                     \
        (head)->tqh_last = &TAILQ_FIRST((head));                        \
        QMD_TRACE_HEAD(head);                                           \
} while (0)

これらを元に

TAILQ_INIT(&so->so_aiojobq);

を展開すると、以下のようになります (struct socket も合わせて掲載します)。

struct socket {
        ...
        struct {
                struct aiocblist *fqh_first;
                struct aiocblist **fqh_last;
        } so_aiojobq;

do {
        (&so->so_aiojobq)->tqh_first = NULL;
        (&so->so_aiojobq)->tqh_last = &(&so->so_aiojobq)->tqh_first;
} while (0);

要するに、

  • キューの先頭を示すのポインタ fqh_first に NULL を代入する。
  • キューの末尾を示すポインタを格納するポインタ fqh_last に fqh_first のポインタを代入する。

という処理を行なっています。

ちょっと脱線

処理をマクロで定義する際に「do {...} while (0)」で括るのは定番なので、覚えておいてください。どういうことかと言うと、「do {...} while (0)」無しで if 文の直後で TAILQ_INIT() を使うと、

#define TAILQ_INIT(head)                                                \
        TAILQ_FIRST((head)) = NULL;                                     \
        (head)->tqh_last = &TAILQ_FIRST((head));                        \
        QMD_TRACE_HEAD(head);

if (条件)
        TAILQ_INIT(&so->so_aiojobq);

これは、

if (条件) {
        (&so->so_aiojobq)->tqh_first = NULL;
        (&so->so_aiojobq)->tqh_last = &(&so->so_aiojobq)->tqh_first;
}

を意図したコーディングですが、実際には以下のように展開されます。

if (条件)
        (&so->so_aiojobq)->tqh_first = NULL;
        (&so->so_aiojobq)->tqh_last = &(&so->so_aiojobq)->tqh_first;

インデントがあると間違えてしまいますが、tqh_last への代入は「条件」に関わらず実行されてしまいます。これを回避するために、「do {...} while (0)」で囲っています。

TAILQ_INIT() 以降の処理を再掲載します。

  mtx_lock(&so_global_mtx);
  so->so_gencnt = ++so_gencnt;
  ++numopensockets;
#ifdef VIMAGE
  VNET_ASSERT(vnet != NULL, ("%s:%d vnet is NULL, so=%p",
      __func__, __LINE__, so));
  vnet->vnet_sockcnt++;
  so->so_vnet = vnet;
#endif
  mtx_unlock(&so_global_mtx);
  return (so);

mtx_lock(&so_global_mtx) で使用している so_global_mtx は、uipc_socket.c で以下のように定義されています。

/*
 * so_global_mtx protects so_gencnt, numopensockets, and the per-socket
 * so_gencnt field.
 */
static struct mtx so_global_mtx;
MTX_SYSINIT(so_global_mtx, &so_global_mtx, "so_glabel", MTX_DEF);

コメントに書かれている通り、mtx_lock() の直後でインクリメントしている so_gencnt と numopensocets へのアクセスの競合の排他制御で使用します。「per-socekt」は、struct socket のことではないかと思います。

so_gencnt と numopensockets は、同じく uipc_socket.c で以下のように定義されています。

so_gen_t        so_gencnt;      /* generation count for sockets */

static int numopensockets;

so_gencnt のインクリメントとその値の so->so_gencnt への代入、numopensockets のインクリメントを行なった後 mtx_unlock(&so_global_mtx) でロックを解除し、本関数 (soalloc()) の最初で確保した struct socket へのポインタを返却して終了します。

soalloc() 関数終了直前のデータ構造を以下に示します。

今回の処理で確保した領域を緑、変更した変数を青、変更した値は赤で示しています。sb_mtx と sb_sx に関しては詳細は追っていないので、値は記述していません。

[1]/usr/src/sys/i386/conf に置かれている kernel のコンフィグレーションファイルの名称が GENERIC です。FreeBSD をインストールした直後のデフォルトの kernel (/boot/kernel/kernel) はこの GENERIC を元に生成されているので、BSD 界ではデフォルトのカーネルのことを GENERIC kernel と呼んでいます。

著者プロフィール

asou

好きな OS は、FreeBSD です。が、最近購入した Mac mini がなかなか快適なので、Mac でいいかなと思うようになってきました。

記事一覧Index