Linux カーネルと FreeBSD カーネルのソケット構造

はじめに

縁あつて、 Linux カーネルと FreeBSD カーネルの双方を見る機会があり、 両者で、ソケットを表現する構造が大きく異なっていることに気付きました。 ここでは、それぞれのソケットの構造について、調べたことを書いてみたいと思います。

FreeBSD カーネルのソケット構造

FreeBSD では、ソケットの実体となるのは <sys/socketvar.h> で定義される socket 構造体 です。以下のようなものから構成されています:

  • ソケットの型 (SOCK_STREAM, SOCK_DGRAM など)
  • ソケットの状態 (接続済みかどうかなど)
  • 受信ソケットバッファを表す構造体 (struct sockbuf 型)
  • 送信ソケットバッファを表す構造体 (同上)
  • accept で使用される、接続のキュー
  • Protocol Control Block

ここで、 Procotol Control Block とは、プロトコルごとの情報を管理する構造体です。 TCP/IP ソケットなら、 TCP の情報を管理する tcpcb 構造体を指しています。 そして、この tcpcb 構造体は IP の情報を管理する inpcb 構造体へのポインタを持っています。

例えば、 TCP/IP ソケットと UDP/IP ソケットは、下図のように表現されます。

FreeBSD のソケット構造体定義は、 ネットワーク上位層を表現する構造体が下位層を表現する構造体を指すという形で構成されており、 ネットワークプロトコル構造をそのまま踏襲していることが分かります。

  • TCP/IP ソケット
  ソケット構造体     TCP control block     IP control block
 (struct socket)     (struct tcpcb)        (struct inpcb)
+---------------+   +--------------+
|               |   |              |      +--------------+
|           ----|-> |          ----|----> |              |
|               |   |              |      +--------------+
|+-------------+|   +--------------+
||accept queue ||
|+-------------+|
|               |
+---------------+
  • UDP/IP ソケット 1
  ソケット構造体     UDP control block
 (struct socket)     (struct udpcb)   
+---------------+   
|               |   +--------------+  
|           ----|-> |              |
|               |   +--------------+
|+-------------+|   
||accept queue ||
|+-------------+|
|               |
+---------------+

Linux カーネルのソケット構造

一方 Linux では、ソケットの実体となるのは <net/sock.h> で定義される sock 構造体 です。この構造体に含まれているのは、以下のようなものです:

  • ソケットの型 (SOCK_STREAM, SOCK_DGRAM など)
  • ソケットの状態
  • 受信ソケットバッファの使用量
  • 送信ソケットバッファの使用量
  • accept で使用される、接続のキューの大きさ (キューの実体は持っていない)

FreeBSD のソケット構造との一番の違いは、 FreeBSD にあった Protocol Control Block へのポインタを持つ領域が存在しない点です。 代わりに、 Linux では sock 構造体の 末尾 にプロトコル毎の領域を拡張するという方式を取っています。

例えば、 IPv4 ソケットを表す inet_sock 構造体 は、 sock 構造体を先頭に配置し、その後に続けて IPv4 プロトコルのためのフィールドを配置しています。 こうすることで、 sock 構造体から inet_sock 構造体へのポインタを得るには、 単にその sock 構造体を指すポインタをキャストするだけで済むようになっています。

さらに、 TCP/IP ソケットを表す tcp_sock 構造体 でも、 同様に前出の inet_sock 構造体を先頭に配置し、 それに続けて TCP のためのフィールドを配置しています。 このとき TCP では、 accept で必要となるキューも確保します ( inet_connection_sock 構造体。) Linux カーネルでは、 sock 構造体に、このキューの実体を持たせず、 プロトコル毎に実装することとなっています。このため、 UDP ソケットが小さくなっています。

また、 Linux では、 sock 構造体と別に socket 構造体も存在します。 これは sock 構造体に比べて小さく、 sock 構造体へのポインタの他には数個のフィールドしか持っていません。 sock 構造体とファイル記述子との結び付けのために使用されています。

Linux カーネルに於いて例えば、 TCP/IP ソケットと UDP/IP ソケットは、 下図のように表現されます。このように、 Linux のソケットは、 階層を持つのではなく、一つの大きな構造体を使用するという構造になっています。

  • TCP/IP ソケット
struct socket    struct tcp_sock
  +-----+       +------------------------+
  |   --|-----> |                        |
  +-----+       |                        |-- sock 構造体
                |                        |
                +------------------------+
                |                        |-- IPv4 用追加フィールド
                |                        | (inet_sock 構造体による)
                +------------------------+
                |+----------------------+|
                || inet_connection_sock ||
                ||   (accept queue)     ||-- TCP 用追加フィールド
                |+----------------------+|  (tcp_sock 構造体による)
                |                        |
                |                        |
                +------------------------+
  • UDP/IP ソケット2
struct socket    struct udp_sock
  +-----+       +----------------+
  |   --|-----> |                |
  +-----+       |                |-- sock 構造体
                |                |
                +----------------+
                |                |-- IPv4 用追加フィールド
                |                | (inet_sock 構造体による)
                +----------------+
                |                |-- UDP 用追加フィールド
                +----------------+  (udp_sock 構造体による)

Linux カーネルの Listen ソケット

Listen ソケットでは、要求された接続をキューに繋いでおき、 accept コールが呼ばれた時点でその接続を返すのですが、 この部分の実装にも、 FreeBSD と Linux で、大きな違いがあります。

FreeBSD カーネルでは、接続をキューに繋ぐときに、完全な socket 構造体 を割り当てます ( sonewconn() 関数)。 accept キューの実体は、 socket 構造体のリストであり、 accept コールではそのキューから socket 構造体を一つ取り出してユーザに返しています。

一方、 Linux カーネルは、この部分について思い切ったことをしています。 接続の状態に応じて、割り当てる構造体の大きさを変えてしまうのです。例えば、 TCP での accept は、以下のような仕組みになっています。

  1. 接続要求が届くと、 request_sock 構造体を作り、 inet_connection_sock 構造体の持つハッシュ表へと登録する。
    この request_sock 構造体は、 接続要求を識別するための情報のみを含んでおり、 sock 構造体よりもずっと小さい。
  2. 接続が確立すると、ここで初めて tcp_sock 構造体を割り当て、 request_sockinet_connection_sock 構造体の持つキューへと移動させる。
  3. accept コールは、 inet_connection_sock 構造体のキューから tcp_sock 構造体を取り出し、 socket 構造体と関連づけて返す。
  4. 接続が切れて、 TIME_WAIT 状態になると、 tcp_sock 構造体から、 inet_timewait_sock 構造体 に切り替える。 この構造体は、 TIME_WAIT 状態を管理する情報だけを含んでおり、 sock 構造体よりもずっと小さい。

このように、接続の状態に応じて構造体そのものを差し替えることで、 省メモリ化を実現しています。

まとめ

二つのソケット構造を比べてみると、 FreeBSD カーネルのソケットは明瞭な構造ですが Linux カーネルのものと比べると少し無駄が目立ち、 Linux カーネルのソケットは無駄を削った一方で複雑になっているという印象です。

ここからは私見ですが、 Linux のソケット実装は性能重視の一方で汎用性が失われていると感じました。 例えば、 TCP ソケットで accept のために使われる inet_connection_sock 構造体ですが、 コードを見る限り TCP に強く順応した構造になっており、 他のプロトコルでは使いにくくなってしまっているように見えます。 実例として、 Linux カーネルには、他の接続指向のプロトコルとして SCTP (Stream Control Transmission Protocol) が含まれているのですが、 こちらでは inet_connection_sock 構造体は使われておらず、 SCTP 内部で独自の仕組みを実装しているという状態になっています。 一方で FreeBSD では、TCP や SCTP も含め、 同じコードを使用して accept のキューの管理を実装しており、 柔軟に対応できる仕組みになっていることが分かります。 柔軟性と高効率とはある程度トレードオフの関係にあるという一例になっていると感じました。

References

Author: YOKOTA Yuki

Date: 2014-04-09

Generated by Org version 7.8.11 with Emacs version 24

© Mathematical Systems Inc. 2014