BigAdmin System Administration Portal
特集記事: Solaris OS ネットワーキング -- マジックの種明かし
Print-friendly VersionPrint-friendly Version

Solaris OS ネットワーキング -- マジックの種明かし

Sunay Tripathi (上級スタッフエンジニア、Solaris Core Technology Group)、2006 年 1 月

概要: この記事では Solaris 10 OS におけるネットワークの進歩だけでなく、旧リリースにおけるネットワークの進化についても説明しています。 TCP、UDP、IP、デバイスドライバフレームワーク、パフォーマンス向けのチューニングなどのトピックを扱っています。

目次


1.0 背景

Solaris 1.x オペレーティングシステムのネットワークスタックは BSD 形式で、BSD Reno 実装によく似ていました。 BSD スタックはローエンドマシンには適していましたが、ローエンドの顧客だけでなく、企業顧客のニーズも満たすために、Solaris OS は AT&T SVR4 アーキテクチャーに移行し、これが Solaris 2.x プラットフォームになりました。

Solaris 2.x OS では、ネットワークスタックが大きく変更され、BSD スタイルのスタックから STREAMS ベースのスタックへと変わりました。 STREAMS フレームワークは簡単なメッセージ受け渡しインタフェースを備えており、STREAMS モジュール間での柔軟なやり取りが可能でした。 STREAMS の内部および外部境界を使用することで、モジュールの開発者は、実装を複雑にすることなく、相互排他を実現できました。 STREAM の設定にはコストがかかりましたが、1 秒あたりの接続数は重要な基準ではなく、通常、長時間の接続が行われていました。 NFS や FPT などのように接続時間が長くなるほど、全体の接続時間を通して見れば、新しいストリームの設定コストは償却されます。

1990 年代後半、サーバーは、多数の CPU を実行する SMP ベースになっていきました。 ミッドエンドマシンからハイエンドマシンが NUMA 主体となったため、CPU の切り替え処理のコストが高くなりました。 STREAMS は設計上 CPU と密接に関係していないため、特定の接続のパケットがさまざまな CPU の間を移動します。 Solaris 製品が、STREAMS アーキテクチャーから離れる必要があることは明らかでした。

1990 年代後半は、World Wide Web が爆発的な広がりを見せた時期でもありました。 処理能力の増加に伴い、短時間の接続が多数発生し、接続の設定時間も同じく重要になりました。 Solaris 10 プラットフォームでは、ネットワークスタックがさらに変化して、コア部分 (ソケットレイヤー、TCP、UPD、IP、デバイスドライバなど) で IP クラシファイアと直列化キューを使用することで、接続の設定時間、スケーラビリティー、およびパケット処理コストの向上を実現しました。 ISV が追加機能を実装するために必要な柔軟性を提供するために、STREAMS アーキテクチャーは引き続き使用されています。


2.0 Solaris 10 OS のスタック

新しいフレームワークとその主要なコンポーネントの動作の仕組みについて見ていきましょう。

2.1 概要

Solaris 10 OS 以前は、スタックは STREAMS 境界とカーネル適応型の相互排他を使用してマルチスレッドに対処しています。 TCP は STREAMS QPAIR 境界を使用し、UDP は STREAMS QPAIR と PUTSHARED を使用し、IP は PERMOD 境界と PUTSHARED を使用します。また、相互排他で保護される各種の TCP、UDP、および IP グローバルデータ構造が使用されます。 スタックは、さまざまなシステムコール、ネットワークデバイスドライバの読み込み側の割り込み、またはデバイスドライバのワークスレッドを実行するユーザー側のスレッドと、STREAMS フレームワークのワークスレッドの両方によって実行されます。 現在の境界は、モジュール単位、プロトコルスタックレイヤー単位、または水平境界を提供します。 Solaris 9 より以前の STREAMS ベースのスタックでは、個々のプロトコルレイヤーに対して水平境界が提供されるため、パケットが複数の CPU 上で、プロトコルレイヤー間でキューイングされる複数のスレッドによって処理されることがありました。 この結果、コンテキストの切り替えが頻繁に行われ、接続固有のデータ構造について、データの局所性が低下します。

「FireEngine」方式は、すべてのプロトコルレイヤーを、完全にマルチスレッド化された 1 つの STREAMS モジュールにマージします。 マージされたモジュール内では、データ構造単位のロックではなく、「垂直境界」と呼ばれる CPU 単位の同期メカニズムが使用されます。 「垂直境界」は、「squeue」と呼ばれる直列化キュー抽象化オブジェクトを使用して実装されます。 各 squeue は CPU にバインドされ、各接続は、接続固有のデータ構造にとって必要な同期と相互排他を提供する squeue にバインドされます。

インバウンドパケットの接続 (またはコンテキスト) ルックアップは、パケットが IP に達すると同時に、境界の外側で、IP 接続クラシファイアを使用して行われます。 格付けに基づいて、接続構造が識別されます。 ルックアップは境界の外部で発生するため、接続の初期化時に垂直境界または「squeue」のインスタンスに接続をバインドし、バインド先の squeue でその接続のすべてのパケットを処理することで、キャッシュの局所性が向上します。 垂直境界とクラシファイアの詳細については、あとのセクションで説明します。 クラシファイアは、すべてのインバウンドおよびアウトバウンドパケットに必要な一連の関数呼び出しを格納するデータベースにもなります。 これにより、Solaris ネットワークスタックは、現在のメッセージ受け渡しインタフェースから、BSD スタイルの関数呼び出しインタフェースへと変わります。 接続のパケット処理用のオンザフライで作成される関数の文字列 (イベントリスト) が最終的な新しいフレームワークの基礎であり、ほかのモジュールや Sun 以外の高パフォーマンスモジュールがこのフレームワークに参加できます。

2.2 垂直境界

squeue により、常に 1 つのスレッドのみが特定の接続を処理するため、マージされた TCP/IP モジュール内の複数のスレッド (読み取り側と書き込み側) による TCP 接続構造へのアクセスが直列化されます。 これは STREAMS QPAIR 境界に似ていますが、モジュールインスタンスを保護するだけでなく、IP から sockfs までの接続状態全体が保護されます。

垂直境界や squeue 自体は、パケットの直列化とデータ構造の相互排他を提供するだけですが、CPU 単位の境界を作成し、CPU 処理の割り込みに接続されたインスタンスに接続をバインドすることによって、データの局所性を高めることができます。

接続単位の境界または CPU 単位の境界という選択肢がありました。つまり、接続ごとのインスタンスまたは CPU ごとのインスタンスという選択肢です。 接続単位の境界に伴うオーバーヘッドとスレッドの競合によりパフォーマンスが低下することを考慮して、CPU 単位のインスタンスが採用されました。 CPU 単位のインスタンスの場合、接続構造をキューイングして処理する方法と、キューイングするのはパケットだけで、接続構造のポインタをパケットに格納するという方法が考えられました。 前者の方法の場合、接続用のパケットが絶えず到着する状況では、処理しきれなくなるおそれがあります。 このような状況を回避するためのオーバーヘッドは、パフォーマンスの低下をもたらしました。 パケットのキューイングでは順序が守られ、処理も非常に簡単なため、FireEngine ではこの方法が採用されました。

前述のように、各接続インスタンスは 1 つの squeue に割り当てられ、垂直境界の中でのみ処理されます。 squeue は一度に 1 つのスレッドによって処理されるため、境界内で特定の接続を処理するのに使用されるすべてのデータ構造は、ロックせずにアクセスできます。 この結果、接続メタデータ、パケットメタデータ、およびパケットペイロードデータのアクセスの、CPU とスレッドコンテキストのデータの局所性が向上します。 加えて、デバイス単位のドライバワークスレッド方式を使用せずに済みます。この方式では、システム全体のリソース問題を解決するのは困難です。また、新たな方針のアルゴリズムを実装して、ネットワークインタフェースのスループットおよびシステムスループットに基づいて、特定のネットワークインタフェースを適切に処理できます (たとえば、接続単位のパケット処理を CPU のグループにファンアウトします)。 squeue に入るスレッドは、すぐにパケットを処理することがあります。あるいは、それをキューイングして、あとで別のスレッドまたはワークスレッドで処理することもあります。 その選択は、squeue のエントリポイントと直列化の状態に依存します。 即時処理は、同じ squeue に別のスレッドが入っていない場合にのみ可能です。 squeue は、次の抽象化オブジェクトで表されます。

typedef  struct squeue_s {
    int_t     sq_flag;     /* Flags tells squeue status */
    kmutex_t sq_lock;      /* Lock to protect the flag etc */
    mblk_t     *sq_first;  /* First Packet */
    mblk_t     *sq_last;   /* Last Packet */
    thread_t sq_worker;    /* the worker thread for squeue */
} squeue_t;

squeue は、コアやハイパースレッドなど、H/W 実行パイプラインを基準に作成されることに注意してください。 直列化キュー (および H/W 実行パイプライン) のスタック処理は、一度に 1 つのスレッドに限られますが、新しいスタックがメモリーや垂直境界内のロックなどのリソースを待たなくて済むため、実際にパフォーマンスが改善されます。 また、複数のカーネルスレッドが H/W 実行パイプラインを時間共有する方が、1 つのスレッドだけが割り込みを受けずに実行するより、オーバーヘッドを伴います。

  • キューイングモデル: キューは、読み取り側と書き込み側の両方について厳密に FIFO (First In First Out) 方式なので、特定の接続が処理されないという状態は存在しません。 読み取り側または書き込み側のスレッドは、キューの末尾にパケットを入れます。 そのあとで、以下の処理モデルに基づいてパケットを処理したり、ワークスレッドに通知します。
  • 処理モデル: パケットがキューに入れられたあと、別のスレッドがすでに squeue を処理している場合は、キューイングスレッドが戻り、パケットはドレインモデルに基づいて、あとでドレインされます。 squeue が処理されておらず、かつ、パケットがキューイングされていない場合、スレッドは squeue に処理中のマークを付け (sq_flag で表現)、パケットを処理できます。 パケットの処理が完了すると、「処理中」のフラグが削除され、squeue は使用可能な状態になります。
  • ドレインモデル: パケットを適切に処理できたスレッドは、要求を処理中にキューに入れられたパケットをドレインすることもできます。 さらに、squeue は処理されていないが、すでにキューイングされているパケットがある場合、パケットをキューイングして戻るのではなく、キューをドレインし、自分のパケットを処理することができます。

ワークスレッドは、常にキュー全体をドレインできます。 適切なドレインモデルの選択は、非常に複雑です。 次のいずれかが考えられます。

  • 「常にキューイングする」
  • 「可能ならば自分のパケットを処理する」
  • 「期限付きの処理とドレイン」

これらのオプションは、読み取りスレッドと書き込みスレッドに個別に適用できます。

通常、割り込みスレッドによるドレインは常に期限付きの「ドレインと処理」であり、これに対して書き込みスレッドは、「自分のパケットを処理」または期限付きの「処理とドレイン」のいずれかです。 Solaris 10 リリースでは、書き込みスレッドの動作はチューニング可能な変数で指定され、デフォルトは「自分のパケットを処理」ですが、読み取り側は「期限付きの処理とドレイン」に固定されています。

これとは別に、ワークスレッドの通知は検討を要する重要なオプションです。 パケットの到着率が低く、スレッドが自分のパケットをキューイングすることが設定されている場合、実行する作業があれば、入力スレッドが squeue の処理を終了すると同時に、ワークスレッドの実行を許可する必要があります。

一方、パケットの到着率が高い場合は、ワークスレッドを起動するのを遅らせて、ドレインを完了後すぐにパケットが到着して割り込みが発生することを期待する方が望ましいことがあります。 パケットの到着率が高い状態でワークスレッドをすぐに起動すると、ワークスレッドと割り込みスレッドの間で不要な競合が生じます。

Solaris 10 OS のデフォルト設定では、ワークスレッドが遅れて起動するようになっています。 利用可能なサーバーで最初に試したところ、10 分遅らせてワークスレッドを起動したときに最適な結果が得られました。

squeue に要求を入れるには、squeue 単位のロックでキューの状態を保護する必要がありますが、CPU に分散されており、その時間も短いため、スケーラビリティーの問題は生じません。 最適化も利用しており、これによって、squeue 処理における単一スレッドのセマンティクスを保持したまま、コンテキストの切り替えを回避できます。 システム内の CPU ごとに squeue のインスタンスを作成し、その CPU にワークスレッドをバインドします。 そして、各接続は、特定の squeue、さらに特定の CPU にバインドされます。

squeue と CPU のバインドは変更できますが、squeue を保護する意味で、接続と squeue のバインドを変更することはできません。 マージされた TCP/IP の場合、垂直境界が各接続の TCP 状態を保護します。 各接続が使用する squeue のインスタンスは、アウトバウンド接続の場合は「オープン」、「バインド」、または「接続」時に選択され、インバウンド接続の場合は「イーガー接続作成時」に選択されます。

squeue インスタンスの選択は、システム内の CPU と NIC の相対速度で決まります。 2 つの場合があります。

  • CPU が NIC より高速な場合: 着信接続が、割り込みを受けた CPU の「squeue インスタンス」に割り当てられます。 アウトバウンドの場合、接続は、アプリケーションが実行されている CPU の squeue インスタンスに割り当てられます。
  • NIC が CPU より高速な場合: 1 つの CPU で NIC を処理することはできません。 接続は、利用可能なすべての squeue にランダムにバインドされます。

Solaris 10 OS の場合、NIC が CPU より高速であるか低速であるかの判断は、システム管理者が、グローバル変数 ip_squeue_fanout をチューニングすることによって行われます。 デフォルトは no fanout です。つまり、着信接続を、割り込みを受けた CPU に接続している squeue に割り当てます。 CPU をオフラインにする目的で、この CPU にバインドされているワークスレッドは、そのバインドを削除し、CPU がオンラインに戻るとバインドを復元します。 これにより、DR 機能が正しく動作します。 接続のパケットが複数の NIC に到着し、それによって複数の CPU が割り込みを受けると、これらは常に、もともと接続が確立された squeue 上で処理されます。 Solaris 10 OS では、垂直境界は TCP ベースの接続に対してのみ提供されます。 垂直境界へのインタフェースは、これが TCP 接続であることが決定されたあと、TCP レイヤーおよび IP レイヤーで行われます。 任意の用途の一般的な垂直境界を導入するため、Solaris 10 OS の更新がスケジュールされています。 squeue の API は、次のようになっています。

squeue_t  *squeue_create(squeue_t *, uint32_t, processorid_t, void (*)(), \
                                               void *, clock_t, pri_t);
void      squeue_bind(squeue_t *, processorid_t);
void      squeue_unbind(squeue_t *);
void      squeue_enter(squeue_t *, mblk_t *, void (*)(), void *);
void      squeue_fill(squeue_t *, mblk_t *, void (*)(), void *);

squeue_create は、新しい squeue をインスタンス化し、squeue_bind()/squeue_unbind() を使用して、特定の CPU とのバインドまたはアンバインドを行います。 いったん作成された squeue は破棄されません。 squeue_enter() を使用して squeue へのアクセスが試行され、入力スレッドは、前述のモデルに基づいて、squeue の処理およびドレインが許可されます。 squeue_fill() は、ワークスレッドまたはほかのスレッドによって処理される squeue 上のパケットをキューイングするために使用されます。

2.3 IP クラシファイア

IP 接続のファンアウトメカニズムは、3 つのハッシュテーブルから成ります。

  • 5 個組のハッシュテーブル {プロトコル、リモート IP アドレス、ローカル IP アドレス、リモートポート、ローカルポート}。完全修飾 TCP (ESTABLISHED) 接続を保持します。
  • プロトコル、ローカルアドレス、ローカルポートから成る 3 個組のルックアップ。リスナーを保持します。
  • プロトコルリスナー用の 1 個組のルックアップ。

ルックアップの一部として、接続構造 (すべての接続情報のスーパーセット) が返されます。 この接続情報は conn_t と呼ばれ、次の抽象オブジェクトで表されます。

typedef struct conn_s {
    kmutex_t conn_lock;        /* Lock for conn_ref */
    uint32_t conn_ref;         /* Reference counter */
    uint32_t conn_flags;       /* Flags */
    
    struct ill_s *conn_ill;    /* The ill packets are coming on */
    struct ire_s *conn_ire;    /* ire cache for outbound packets */
    tcp_t     *conn_tcp;       /* Pointer to tcp struct */
    void     *conn_ulp         /* Pointer for upper layer*/
    edesc_pf conn_send;        /* Function to call on read side */
    edesc_pf conn_recv;        /* Function to call on write side */
    squeue_t *conn_sqp;        /* Squeue for processing */
    
    
    /* Address and Ports */
    struct {
        in6_addr_t connua_laddr;     /* Local address */
        in6_addr_t connua_faddr;     /* Remote address. */
    } connua_v6addr;
#define     conn_src V4_PART_OF_V6(connua_v6addr.connua_laddr)
#define     conn_rem V4_PART_OF_V6(connua_v6addr.connua_faddr)
#define     conn_srcv6 connua_v6addr.connua_laddr
#define     conn_remv6 connua_v6addr.connua_faddr
    union {
        /* Used for classifier match performance */
        uint32_t conn_ports2;
        struct {
            in_port_t tcpu_fport;     /* Remote port */
            in_port_t tcpu_lport;     /* Local port */
        } tcpu_ports;
    } u_port;
#define     conn_fport u_port.tcpu_ports.tcpu_fport
#define     conn_lport u_port.tcpu_ports.tcpu_lport
#define     conn_ports u_port.conn_ports2
    uint8_t     conn_protocol;     /* protocol type */
    kcondvar_t     conn_cv;
} conn_t;

ここで注目すべきメンバーは、squeue、または垂直境界へのポインタです。 ルックアップは境界の外部で行われ、パケットは、接続先の squeue で処理またはキューイングされます。 また、conn_recv および conn_send は、読み取り側と書き込み側の関数をポイントします。 パケットが TCP 用の場合、読み取り側関数は tcp_input の可能性もあります。

また、接続ファンアウトメカニズムには、INADDR ANY など、ワイルドカードリスナーをサポートする機能が用意されています。 現在、接続テーブルとバインドテーブルは、主に TCP および UDP 専用です。 リスナーエントリは、listen() 呼び出しの中で作成されます。 TCP の 3 ウェイハンドシェークが完了したあとで、接続テーブルにエントリが作成されます。

IP クラシファイア API は、次のようになります。

conn_t     *ipcl_conn_create(uint32_t type, int sleep);
void       ipcl_conn_destroy(conn_t *connp);

int        ipcl_proto_insert(conn_t *connp, uint8_t protocol);
int        ipcl_proto_insert_v6(conn_t *connp, uint8_t protocol);
conn_t     *ipcl_proto_classify(uint8_t protocol);
int        *ipcl_bind_insert(conn_t *connp, uint8_t protocol, ipaddr_t src,
           uint16_t lport);
int        *ipcl_bind_insert_v6(conn_t *connp, uint8_t protocol,
           const in6_addr_t * src, uint16_t lport);
int        *ipcl_conn_insert(conn_t *connp, uint8_t protocol, ipaddr_t src,
           ipaddr_t dst, uint32_t ports);
int        *ipcl_conn_insert_v6(conn_t *connp, uint8_t protocol,
           in6_addr_t *src, in6_addr_t *dst, uint32_t ports);
void       ipcl_hash_remove(conn_t *connp);
conn_t     *ipcl_classify_v4(mblk_t *mp);
conn_t     *ipcl_classify_v6(mblk_t *mp);
conn_t     *ipcl_classify(mblk_t *mp);

関数の名前は、その内容をそのまま表しています。

2.4 同期メカニズム

垂直境界によって実現される CPU 単位の直列化を除けば、スタックは完全にマルチスレッド化されているため、参照ベースの方式を使用して、必要なときに接続インスタンスを使用できるようにしています。 参照カウントは conn_t メンバーの conn_ref で実装され、conn_lock で保護されています。 ロックの主な目的は conn_t の大部分ではなく、参照カウントを保護することです。 あるエンティティーがデータ構造を参照する (あとで処理するデータ構造へのポインタを格納する) たびに、CONN_INC_REF マクロを呼び出して参照カウントを増分し (基本的に conn_lock を取得)、conn_ref を増分し、conn_lock を廃棄します。 エンティティーが接続インスタンスへの参照を廃棄するたびに、CONN_DEC_REF マクロを使用してその参照を廃棄します。

確立された TCP 接続の場合は、3 つの参照があります。 各プロトコルレイヤーは、インスタンス (TCP と IP について 1 つずつ) への参照を持ちます。また、クラシファイア自体も確立された接続であるため、参照を持ちます。 パケットが接続に到着し、クラシファイアが接続インスタンスを検索するたびに、新たな参照が発生しますが、プロトコルレイヤーがそのパケットの処理を終了すると廃棄されます。 同様に、接続インスタンス上で実行するタイマーは、タイマーが起動したときにいつでもインスタンスが機能するように参照を備えています。 接続インスタンスに関連付けられているメモリーは、最後の参照が廃棄されると解放されます。


3.0 TCP

Solaris 10 OS における TCP の扱いは、これまでのリリースと同じです。つまり、TCP はクローンデバイスのように見えますが、実際には、TCP および IP コードが 1 つの D_MP STREAMS モジュールにマージされた複合物です。 マージされた TCP/IP モジュールのオープンとクローズの STREAMS エントリポイントは、IP のエントリポイント、つまり ip_open および ip_close と同じです。 オープン時に渡されるメジャー番号に基づき、IP は、オープンが TCP オープンに対応するのか IP オープンに対応するかを判断します。 TCP の put および service STREAMS エントリポイントは、tcp_wputtcp_wsrv、および tcp_rsrv です。 tcp_wput エントリポイントは、単にラッパールーチンとしての役割を果たすものであり、sockfs および上位からのほかのモジュールが STREAMS を使用して TCP とやり取りすることを実現します。 IP は TCP 関数を直接呼び出すため、tcp_rput がないことに注意してください。 IP の STREAMS エントリポイントは変わりません。

TCP の操作部分は、図 1 に示されているように、squeue_* プリミティブを通じて入力される垂直境界で完全に保護されています。 上位からやってくるパケットは、ラッパー関数 tcp_wput を通じて TCP に入ってきます。次に、対応する垂直境界に入ったあとで、本当の TCP 出力処理関数 tcp_output の実行を試みます。 同様に、下位からやってくるパケットは、垂直境界に入ったあと、本当の TPC 入力処理関数 tcp_input の実行を試みます。 垂直境界を経由する TCP へのエントリポイントは、複数存在します。

図 1

図 1

垂直境界が使用する TCP エントリポイント:

tcp_input - All inbound data packets and control messages
tcp_output - All outbound data packets and control messages
tcp_close_output - On user close
tcp_timewait_output - timewait expiry
tcp_rsrv_input - Flow control relief on read side
tcp_timer - All tcp timers

3.1 TCP と IP の間のインタフェース

FireEngine では、TCP と IP 間のインタフェースが、制御パスとデータパスの両方とも、STREAMS ベースのメッセージ受け渡しインタフェースから関数呼び出しベースのインタフェースへと変更されています。 アウトバウンド側では、垂直境界の内部にいながら、TCP は ip_output を呼び出すことによって、完全に準備済みのパケットを IP に直接渡します。

同様に、制御メッセージも関数引数として直接渡されます。 ip_bind_v{4, 6} は、バインドメッセージを引数として受け取り、要求された処理を実行し、呼び出し側に結果 mp を返します。 TCP は、connect()bind()、および listen() パスの中で、ip_bind_v{4, 6} を直接呼び出します。 IP は依然としてそのすべての STREAMS エントリポイントを保持していますが、TCP (/dev/tcp) が本当のデバイスドライバになります (つまり、ほかのデバイスドライバが取って代わることができません)。

基本的なプロトコル処理コードは変更されていません。 一般的なソケット呼び出し、およびフレームワークとのやり取りについて見てみましょう。

3.2 ソケット

TCP のソケットオープンまたは /dev/tcp のオープンは、最終的に ip_open を呼び出します。 オープンは次に IP 接続クラシファイアを呼び出し、すでに conn_t に統合されている TCP 単位のエンドポイント制御ブロックを割り当てます。 この接続の squeue が選択されます。 内部オープンの場合、アクセプタストリームの sockfs により、ほとんど何も行われず、有効な処理は受け入れ時まで遅延されます。

3.3 バインド

tcp_bind は、最終的に IP と対話して、渡されたアドレスが有効かどうかを把握する必要があります。 FireEngine TCP は、この要求を、通常の TPI メッセージ形式で用意しています。 しかし、このメッセージは、関数引数として ip_bind_v{4, 6} に直接渡され、この関数が結果を別のメッセージとして返します。 メッセージをパラメータとして使用することで、既存のコードを最小限の変更で活用できます。 TCP がバインドを検証するために使用するポートハッシュテーブルは、クラシファイアでは不要なため、TCP にそのまま残されています。

3.4 接続

tcp_connect における変更は、tcp_bind に似ています。 完全な bind() 要求は TPI メッセージとして準備され、関数引数として ip_bind_v{4, 6} に渡されます。 IP はクラシファイアを呼び出し、接続されたハッシュテーブルに接続を挿入します。 TCP 内の conn_ hash テーブルは使用されません。

3.5 待機

このパスは、tcp_bind の一部です。 tcp_bind はローカルのバインド TPI メッセージを準備し、それを関数引数として ip_bind_v{4, 6} に渡します。 IP はクラシファイアを呼び出し、バインドハッシュテーブルに接続を挿入します。 TCP の待機ハッシュテーブルはもはや存在しません。

3.6 受け入れ

Solaris 10 以前のリリースにおける accept 実装では、リスナーコンテキスト内での接続設定の処理が多く行われていました。 3 ウェイハンドシェークはリスナーの境界で完了し、接続の指示がリスナーの STREAM に送信されました。 受け入れを実行するのに必要なメッセージがリスナーの STREAM に送信され、リスナーは、T_CONN_RES メッセージを TCP に送信する時点から、sockfs が肯定応答を受信するまで、シングルスレッド化されていました。 Solaris 10 以前のリリースでは、接続の到着率が高い場合、新しい接続を受け入れるスタックの能力は、大きく低下しました。

さらに、TCP のオーバーヘッドが加わると、受け入れ率の低下を招きました。 sockfs がアクセプタの STREAM を TCP に対して開いて新しい接続を受け入れる場合、新しい接続に必要なデータ構造がすでに割り当てられていることを、TCP は認識しませんでした。 そのため、新しい構造を割り当て、初期化しました。 あとで受け入れ処理の中で、これらの構造は解放されました。 Solaris 10 以前のリリースでは、新しく作成された接続のパケットがリスナーの境界に到着するという大きな問題もありました。 すべての着信パケットに対して確認が必要であり、間違った境界に到着したパケットを正しい境界に送信する必要があるため、新たな遅延が生じました。

FireEngine モデルでは、SYN パケットが到着すると同時にその境界内で「イーガー」接続を確立することで (着信接続は、受け入れが完了するまで「イーガー」と呼ばれます)、そのパケットは常に正しい接続先に向かいます。 その結果、TCP グローバルキューを完全に除去することが可能です。 接続指示は引き続きリスナーの STREAM に送信されますが、新しく作成されたアクセプタの STREAM で受け入れが発生し (このため、この STREAM のデータ構造を割り当てる必要がありません)、肯定応答をアクセプタの STREAM に送信できます。 そのため、sockfs は、受け入れ処理のどの時点においても、シングルスレッド化される必要がありません。

新しい着信接続 (イーガー) が存在するのはそのリスナーがあるためだけであり、イーガーもリスナーも、イーガーがリセットを受信したりリスナーが閉じることによって受け入れ処理中の任意の時点で消滅する可能性があるため、新しいモデルは慎重に実装されました。

リスナーが閉じた場合でも、リスナーへのイーガー参照が常に有効となるように、リスナーに参照を配置することによって、イーガー接続は開始します。 3 ウェイハンドシェークの完了後に接続指示を送信する必要がある場合、イーガーは、リセットを受信して閉じても参照が引き続き有効となるように、自身に参照を配置します。 イーガーは、接続指示メッセージの一部として自身へのポインタを送信しますが、これは、リスナーが閉じていないことを確認してから、リスナーの STREAM を利用して送信されます。 T_CONN_RES メッセージが、新しく作成されたアクセプタの STREAM に到着すると、ふたたびイーガーの境界に入り、受け入れ処理を完了する前に、リセットの受信が原因でイーガーが閉じていないことを確認します。 TLI/XTI ベースアプリケーションの場合、T_CONN_RES メッセージはやはりリスナーの STREAM で処理され、肯定応答がリスナーの STREAMS に送信されるので、動作に変わりはありません。

3.7 クローズ

クローズするキューと TCP への参照が切り離されたため、TCP 内のクローズ処理は、参照カウントがゼロになるまで待つ必要がなくなりました。 クローズするキューへのすべての参照がなくなると同時に、クローズは終了します。 TCP データ構造自体は、ほとんどの場合、切り離された TCP として引き続き存在できます。 TCP への最後の参照が解除されると、TCP データ構造が解放されます。

ユーザーが起動するクローズは、ストリームのみを閉じます。 基になる TCP 構造は、引き続き存在します。 TCP は、すべてのユーザーデータの転送後にピアとの FIN/ACK 交換を経由して、一定の間、TIME_WAIT 状態に入ります。 これを、TCP の切り離しと呼びます。 切り離されたこの TCP でも、アウトバウンドおよびインバウンドの処理が、特定の切り離された TCP で同時に発生しないように保護が必要です。

3.8 データパス

TCP は、IRE にアクセスできるのであれば、ほとんどの場合、アウトバウンドパケットを送信するのに IP を呼び出す必要もありません。 マージされた TCP/IP では、接続用にキャッシュされた IRE にアクセスできるという利点があり、TCP は IRE 内の情報に基づいて、データをリンクレイヤードライバに直接プッシュできます。 FireEngine の処理は前述のとおりです。

3.9 TCP ループバック

TCP Fusion は、Solaris 10 OS におけるループバック TCP 接続用のプロトコルレスデータパスです。 2 つのローカル TCP エンドポイントの融合は、接続の確立時に起こります。 デフォルトでは、すべてのループバック TCP 接続が融合されます。 この動作は、チューニング可能なシステム変数 do tcp fusion を 0 に設定すると変更できます。 融合が行われるためには、両方のエンドポイントでいくつかの条件が満たされる必要があります。

  • 共通の squeue を共有する必要がある。
  • TCP であり、「生のソケット」ではない。
  • プロトコルレベルの処理を要求しない。つまり、接続に対して IPsec や IPQoS ポリシーが存在しない。

融合が失敗した場合は通常の TCP データパスを使用し、成功した場合は、両方のエンドポイントが送信パスとして tcp fuse output() を使用するようになります。 tcp fuse output() は、ピアの受信キューにアプリケーションデータを直接入れます。プロトコル処理は行われません。 データのキューイング後、送信側は次のいずれかの操作を実行できます。

  • putnext(9F) を呼び出して、データを受信側の読み取りキューにプッシュする。
  • そのまま戻り、同期 STREAMS エントリポイントを利用して受信側にキューイングデータを取得させる。

同期 STREAMS が有効な場合は、後者のパスが取られます。 モジュールの挿入または削除が原因で TCP モジュールの上位に sockfs が直接存在しない場合は、自動的に無効です。

TCP Fusion 内のロックは、squeue と相互排他の tcp fuse lock によって処理されます。 融合が成功する条件の 1 つは、両方のエンドポイントが同じ squeue を使用することです。 これにより、一方の側がデータを送信し続けている間に、他方が消滅できなくなります。 同期 STREAMS が有効な場合、squeue だけでは安全なアクセスを実現するのに十分ではありません。 tcp fuse rrw() は squeue に入らず、tcp rcv リストとその他の融合関連のフィールドへのアクセスを送信側と同期する必要があるからです。 tcp fuse lock は、この目的で使用されます。

同期ストリームモードでの TCP Fusion において、小規模な書き込みフロー制御の速度制限は、それぞれ異なる制限に設定されている、受信バッファーのサイズとデータブロックの数を確認することによって実現されます。 これは、累積サイズのチェックがデータブロックカウントのチェックより優先される標準の STREAMS フロー制御とは異なります (STREAMS キューの高位境界値は、通常、バイトを表します)。 各キューイングによって、受信プロセスに通知が送られます。データブロックの増加は受信側が低速であることを表しており、送信側は、システムリソースをこれ以上無駄にしないために、できるだけ早い段階でブロックまたは通知される必要があります。 実際これは、システム内の未処理のセグメント数を制限することに相当します。

キューイング可能なデータブロックの最小数のデフォルト値は 8 です。この値は、チューニング可能なシステム変数 tcp_fusion_burst_min を、より大きな値または 0 に設定すると変更できます (後者の値を設定すると、バーストチェックが無効になります)。


4.0 UDP

Solaris 10 OS では、フレームワークの改善とは別に、スタック内での UDP パケットの移動に変更が加えられました。 プロジェクトの内部コード名は「Yosemite」でした。 Solaris 10 リリース以前の UDP 処理コストは、パケット単位の処理コストとバイト単位の処理コストに均等に分かれていました。 通常、パケット処理コストは、STREAMS、ストリームヘッド処理、およびスタックとドライバ内でのパケット廃棄によるものでした。 バイト単位の処理コストは、H/W チェックサムの欠如と、ネットワークスタック全体をとおしての最適化されていないコード分岐によるものでした。

4.1 スタック内での UDP パケットの廃棄

UDP は信頼性が低いと考えられていますが、ローカルエリアネットワークは信頼性が非常に高まってきており、アプリケーションは LAN 環境内でのパケットロスがないと想定する傾向にあります。 この想定はほぼ正しいのですが、Solaris 10 以前のバージョンでは、スタックは UDP オーバーロードの処理に関してあまり効率的ではなく、スタック自体の中でパケットを廃棄する傾向がありました。

インバウンドでは、受信パス全体で複数のレイヤーでパケットが廃棄されていました。 UDP の場合、パケットが廃棄されるもっとも一般的で明らかな場所は、パケットをキューイングするリソースが欠如している IP レイヤーでした。 パケットの廃棄が発生するもう 1 つの重要で明らかな場所は、ネットワークアダプタレイヤーです。 この種の廃棄は、通常、マシンが着信率が高いパケットを処理しているときに発生します。

UDP sockfs 拡張 (sockudp) は、ソケットベースの UDP アプリケーションの処理に使用される socktpi の代替パスです。 前者は、ストリームヘッドと TPI メッセージ受け渡しインタフェースを取り除くことによって、アプリケーションとネットワークスタックの間に、より直接的なチャネルを提供します。 これにより、ソケットとトランスポートレイヤーを通じて、直接的なデータおよび関数アクセスが可能になります。 また、スタックが効率的となり、UDP H/W チェックサムオフロードと組み合わせることで (断片化された UDP の場合でも)、UDP パケットがスタック内でほとんど廃棄されなくなります。

4.2 UDP モジュール

これは、IP と同じ保護ドメインのもとで実行する、完全なマルチスレッド UDP モジュールです。 このモジュールにより、トランスポート (UDP) が上下のレイヤーと密接に統合されます。 また、socktpi は、UDP を直接呼び出すことができます。 同様に、UDP もデータリンクレイヤーを直接呼び出すことができます。 GLDv3 が導入されている環境では、データリンクレイヤーもトランスポートを直接呼び出すことができます。 さらに、メッセージベースのインタフェースを使用せずに、ユーティリティー関数を直接呼び出すことができます。

エンドポイントの状態を変更する関数を実行する場合、UDP ではエンドポイント単位での排他的処理が必要です。 udp rput other() は、IP オプションを持つパケットと、そのパケットの処理を扱うことで、エンドポイントのオプション関連の状態を更新します。 udp wput other() は、エンドポイントの状態を更新する必要がある connect(3SOCKET) など、上位からの制御操作を扱います。 STREAMS の世界では、共有内部境界のエントリポイントを使用し、qwriter inner() を使用してエンドポイントに排他アクセスすることで、この同期が実現されていました。

Solaris 10 モデルは、STREAMS に依存しない内部境界を使用して前述の同期を実現します。その詳細を次に示します。

  • udp enter(): UDP エンドポイント境界に入ります。 つまり、udp become writer() が UDP エンドポイント上で排他的になります。 これは、即時に、または、境界があとで排他的に使用可能になったときに、排他的に呼び出される関数を指定します。
  • udp exit(): UDP エンドポイント境界を出ます。

上位または下位から UDP に入るには、udp enter() を使用します。 通常の場合のように、これらの境界ではロックは保持されません。 排他的モードが終了したら、udp exit() を呼び出して境界を出ます。

これをサポートするために、新しい UDP モデルは、UDP MT HOT モードおよび UDP SQUEUE モードという、2 つの操作モードを取り入れています。 UDP MT HOT モードでは、複数のスレッドが同時に UDP エンドポイントに入る可能性があります。 これは、標準データの送受信に使用され、putshared STREAMS エントリポイントに似ています。 制御操作とその他の特殊なケースでは udp become writer() を呼び出してエンドポイント単位で排他的となり、UDP SQUEUE モードへと移行します。 定義上、squeue は conn t へのアクセスを直列化します。 UDP 接続の squeue に保留中のメッセージがない場合、エンドポイントは MT HOT モードに戻ります。 その間、エンドポイントの MT スレッドがすべて終了していない場合は、メッセージがエンドポイントにキューイングされ、UDP は、UDP MT QUEUED モードまたは UDP QUEUED SQUEUE モードの、いずれかの一時モードになります。

安定したモードにある間、UDP はエンドポイントで動作するスレッドの数を追跡します。 udp reader count 変数は、UDP MT HOT モードで、読み取り側としてエンドポイントに入るスレッドの数を表します。 UDP SQUEUE への移行は、読み取り側のスレッドが 1 つだけになった場合、たとえば、このカウンタが 1 になった場合に発生します。 同様に、udp squeue count は、UDP SQUEUE モードで、エンドポイントの squeue で動作するスレッドの数を表します。 UDP MT HOT へのモードの移行は、最後のスレッドがエンドポイントを出たあとに発生します。

UDP と IP は同じ保護ドメインで実行しますが、これらは依然として別々の STREAMS モジュールです。 このため、STREAMS の plumb はそのまま保持され、UDP モジュールインスタンスは常に IP 上にプッシュされます。 これは、すべての UDP エンドポイントについて余分なオープンとクローズを引き起こしますが、ストリームに I POP を発行して IP9 に直接アクセスするなど、特定の処理をこのような plumb ジオメトリに依存して行うアプリケーションに対して、下位互換性を提供します。

実際の UDP 処理は、IP インスタンス内で行われます。 UDP モジュールインスタンスは、エンドポイントに関する状態を持っているわけではなく、ダミーモジュールとして動作するに過ぎません。その目的は、STREAMS plumb の外観を変えないことです。

Solaris 10 プラットフォームでは、次の plumb モードを使用できます。

  • 標準: 最初に IP がオープンされ、そのあとで UDP が直接上位にプッシュされます。 これは、UDP ソケットまたはデバイスがオープンされるときのデフォルトの動作です。
  • SNMP: UDP が、IP 以外のモジュールの上位にプッシュされます。 この場合、SNMP セマンティクスのみがサポートされます。

これらのモードは、IP と UDP の間の中間モジュールがサポートされていないことを意味しています。IP とトランスポートモジュールの間のレイヤー間通信のセマンティクスは非公開なので、実際、Solaris テクノロジでは、このようなシナリオをこれまでサポートしてきませんでした。

4.3 UDP とソケットの対話

socket(3SOCKET) システムコールの中で発生する重要なイベントに、ソケットのアドレスファミリおよびプロトコルタイプに関連付けられたモジュールの plumb があります。 TCP または UDP ソケットは、多くの場合、対応するトランスポートモジュールの直接上位に位置する sockfs を生じさせます。 Solaris 10 リリース以前は、ソケットレイヤーは STREAMS プリミティブを使用して UDP モジュールと通信していました。 Solaris 10 OS では、関数呼び出し可能なインタフェースが使用されており、sockfs から UDP への各送信時にメタデータの T UNITDATA REQ メッセージを使用する必要がありません。 代わりに、データとその付属情報 (リモートソケットアドレスなど) を代替の UDP エントリに直接提供し、余分な割り当てコストを回避できます。

トランスポートモジュールの場合、sockfs のすぐ下に位置していると、同期 STREAMS を使用できます。 このため、トランスポートレイヤーは着信データをバッファリングして、読み取り操作が発行されたときにアプリケーションがあとでデータを取得できるため (同期 STREAMS による)、受信処理の時間が短縮されます。

4.4 同期 STREAMS

同期 STREAMS は、メッセージの受け渡しと処理を目的とする、従来の STREAMS インタフェースの拡張機能です。 当初は、コピーとチェックサムを組み合わせた機能の一部として追加されました。 この機能により、モジュールまたはドライバのエントリポイントを、ユーザーの I/O 要求に関して、同期形式で呼び出すことができます。 従来の STREAMS では、このような要求に対して、ストリームヘッドが同期バリアーです。 同期 STREAMS は、このバリアーを、ストリームヘッドから下のモジュールへと移動するメカニズムを提供します。

同期 STREAMS の TCP 実装は、さまざまな要因で、Solaris 10 より以前のリリースでは複雑でした。 大きな要因は、チェックサムと copyin/copyout 処理の結合でした。

これに対して、Solaris 10 OS の場合、TCP は copyin/copyout 時にチェックサムに依存していないため、読み取り側でのループバック TCP および UDP で使用できるように、メカニズムが大幅に簡単化されています。 同期 STREAMS エントリポイントは、read(2)recv(3SOCKET) などの要求の中で呼び出されます。 これらのモジュールでは、putnext(9F) を使用してデータアップストリームを送信する代わりに、自分の内部受信キューにデータを入れるので、送信スレッドはすぐに戻ることができます。 strrput() を呼び出して、送信スレッドコンテキスト内からストリームヘッドでデータをキューイングしなくて済むため、より動的な動作が実現されます。 キューイングおよび受信側アプリケーションへのシグナル/ポーリング通知にかかる時間が短縮することで、送信スレッドはすぐに戻って別の作業を実行できます。そのため、直列化される操作もこれまでより少なくて済みます。

データが到着するたびに、トランスポートモジュールは、アプリケーションによるデータの取得をスケジュールします。 読み取り処理の際にアプリケーションがブロックされている場合 (スリープ状態) は、ブロックが解除されて、実行が再開されます。 これは、ストリーム上で STR WAKEUP SET() を呼び出すことによって行われます。 同様に、アプリケーション用のデータがない場合は、STR WAKEUP CLEAR() を呼び出すことによって、次の読み取り試行中にアプリケーションをふたたびブロックできます。 それより前に新しいデータが到着する場合はこの状態が無効となり、以降の読み取り処理が続行します。

アプリケーションは、読み取りイベントが発生するまで poll(2) でブロックされることがあります。また、使用されているソケットがブロックされていない場合は、SIGPOLL または SIGIO シグナルを待機することもあります。 このため、トランスポートモジュールは、データを受信するたびに、イベント通知を配信したり、アプリケーションに通知したりします。 これは、対応するストリーム上で STR SENDSIG() を呼び出すことによって行われます。

読み取り処理の一部として、トランスポートモジュールは、読み取り側の同期 STREAMS エントリポイントからデータを返すことにより、アプリケーションにデータを配信します。 ループバック TCP の場合、同期 STREAM の読み取りエントリポイントは、その受信キューの内容全体 (バイトストリーム) をストリームヘッドに返します。残りのデータは、次の読み取りを待つストリームヘッドでキューイングされます。 UDP の場合、読み取りエントリポイントは、一度に 1 つのメッセージ (データグラム) のみを返します。

4.5 STREAMS フォールバック

デフォルトでは、sockfs が対応するトランスポートモードのすぐ上にある場合、すべての UDP およびループバック TCP ソケットに対して、直接伝送と読み取り側の同期 STREAMS 最適化が有効になっています。 これらの機能を無効にすることが必要ないくつかの場合があります。この場合、sockfs とトランスポートモジュールの間のメッセージ交換を putnext(9F) を通じて行う必要があります。 次のような場合がこれに該当します。

  • 中間モジュール: モジュールは、autopush(1M) によりオープン時にトランスポートモジュールの上位に自動プッシュされるように構成されているか、ioctl(2) を通じてソケットに I PUSH されます。
  • ストリーム変換: 架空の sockmod モジュールがソケットから I POP され、ソケットエンドポイントからデバイスストリームに変換されます。

(ソケットエンドポイントでは I INSERT または I REMOVE ioctl は許可されないため、その処理にフォールバックは必要ないことに注意してください。)

フォールバックが要求される場合、sockfs は、直接モードが無効であることをトランスポートモジュールに通知します。 通知は、sockfs モジュールによって ioctl メッセージの形式で送信されます。これは、トランスポートモジュールに対して、データアップストリームを配信するには putnext(9F) を使用する必要があることを伝えます。 これによって、データは中間モジュールを移動し、デバイスストリームセマンティクスとの互換性が提供されます。


5.0 IP

前述のように、すべてのトランスポートレイヤーは、STREAMS モジュールと同様に、完全にマルチスレッド化され、擬似デバイスドライバとして動作する IP モジュールにマージされています。 IP における主要な変更は、IP クライアント機能の削除と、インバウンドパケットストリームの多重化でした。 新しい IP クラシファイア (依然として IP モジュールの一部) の役割は、インバウンドパケットを適切な接続インスタンスに分類することです。 IP モジュールは、引き続き、ネットワークレイヤープロトコルの処理と、ネットワークインタフェースの plumb および管理を扱います。

新しいスタック内でのネットワークインタフェース、マルチパス、およびマルチキャストの plumb の動作の仕組みついて見てみましょう。

5.1 NIC の plumb

plumb は、IP、ARP、およびデバイスドライバ間でのメッセージ交換に関係する一連の長い処理です。 IOCTL セットの大部分は、通常、plumb 処理に関係します。 普通のモデルでは、これらの IOCTL を ILL (IP Lower Level) ごとに 1 つずつ直列化します。 たとえば、hme0 および qfe0 の plumb は、互いに干渉することなく並列して進みます。 ただし、hme0 上のさまざまな IOCTL セットはすべて直列化されます。

さらにきめの細かい方法を使用し、IIL ごとではなく、IPIF ごとに処理を直列化するという方法も考えられます。 この方法は、多くの IPIF が ILL 上でホストされていて、異なる IPIF 上の処理が相互に干渉しない場合のみ効果があります。

さらに、標準の Solaris MT 手法を使用して、すべての IOCTL を完全にマルチスレッド化するという方法も考えられます。 しかし、この方法は必要以上に複雑で、それほどの付加価値はありません。 待機、およびドライバやほかのモジュールとのメッセージ交換が含まれる plumb シーケンス全体をとおしてロックを保持することは困難です。 純粋な反復的でない制御処理が行われるため、IPIF 上に複数の IOCTL セットを同時に許可することは、パフォーマンス的にも機能的にもメリットはありません。 ブロードキャスト IRE は、IPIF 単位ではなく IIL 単位に作成されます。 このため、IIL 上で複数の IPIF を同時に起動しようとすると、IRE 作成ロジックが非常に複雑になります。 一方、IIL 単位での plumb 処理の直列化は、既存の IP コードベースに簡単に役立ちます。 plumb の中で、IP はデバイスドライバおよび ARP とメッセージを交換します。 基になるデバイスドライバから受信したメッセージも、IP で排他的に処理されます。 書き込み側と読み取り側の処理の間で相互排他を提供する中で、putnext では標準の相互排他ロックを保持することはできないため、この方法は便利です。 全排他的な PERMOD syncq ではなく、ILL 単位の直列化キューを使用することで、この効果は簡単に実現できます。

5.2 IP ネットワークマルチパス (IPMP)

IPMP 処理はすべて、IPMP グループの概念に基づいて行われます。 フェイルオーバー処理とフェイルバック処理は、通常は同じ IPMP グループに含まれる 2 つの IIL の間で行われます。 IPIF と ILM は、IIL 間を移動します。 これには、発信元 ILL の下位への移動が含まれ、場合によっては送信先 ILL の上位への移動が含まれます。 IIL の上下への移動は、ブロードキャスト IRE に影響します。 ブロードキャスト IRE は、ブロードキャストパケットが重複して受信されないように、IPMP グループごとにグループ化する必要があります。 このため、ブロードキャスト IRE の操作は、IPMP グループのすべてのメンバーに影響します。 IFF_FAILED または IFF_STANDBY を設定すると、IPMP グループ内のすべての ILL が評価され、ブロードキャスト IRE が再グループ化されます。 このため、IPMP グループ単位の IPMP 処理の直列化は、既存のコードベースに簡単に役立ちます。 IPMP グループには、IPv4 IIL と IPv6 ILL の両方が含まれています。

5.3 マルチキャスト

マルチキャストジョインは、ILG 構造と ILM 構造の両方で動作します。 マルチキャストジョインを実行しようとする、IPC (ソケット) 上で動作している複数のスレッドは、ILG で動作するときには同期をとる必要があります。 マルチキャストジョインを実行しようとする、異なる IPC (ソケットエンドポイント) 上で動作している可能性がある複数のスレッドは、最終的に ILM を同時に操作する場合があり、ILM へのアクセスについて同期をとる必要があります。 両方の場合とも、標準的な Solaris MT 手法に従います。 これまでに述べた plumb、IPMP、およびマルチキャストのすべて考慮した場合、その共通点は、IPMP グループ単位ですべての排他処理を直列化することです。 IPMP が有効でない場合は、phyint が存在します。 たとえば、hme0 v4 ILL と hme0 v6 ILL は、一緒に phyint を共有します。 前述の例では、マルチキャストには、潜在的に高度なマルチスレッドが含まれています。 ただし、ほかの排他的操作と共存する必要があります。 たとえば、フェイルオーバー処理が進行中で、2 つの ILL 間で ILM を移動しようとしているときに、スレッドが ILM を作成または削除することは望ましくありません。 したがって、最低限の共通点は、物理インタフェースまたは IPMP グループごとにマルチキャストジョインを直列化することです。


6.0 Solaris 10 デバイスドライバフレームワーク

Solaris 10 OS 以前のネットワークデバイスドライバの実装方法と、これを新しい Solaris 10 スタックでなぜ変更しなければならなかったかについて、簡単に説明します。

6.1 GLDv2 およびモノリシックドライバ (Solaris 9 以前のリリース)

Solaris 10 より以前のリリースでは、ネットワークスタックは、通常は 2 つの方法のいずれかで実装される DLPI1 プロバイダを経由します。 次の図 (図 2) は、いわゆるモノリシック Data Link Provider Interface (DLPI) ドライバに基づくスタックと、Generic LAN Driver (GLDv2) モジュールを利用するドライバに基づくスタックを示しています。

図 2

図 2

GLDv2 モジュールは、基本的にライブラリとして動作します。 クライアントは、引き続きデバイスにバインドされているドライバインスタンスとやり取りしますが、DLPI プロトコル処理は GLDv2 モジュールを呼び出すことによって行われ、これが次にドライバを呼び出してハードウェアにアクセスします。 GLD モジュールを使用することで、ドライバの開発者に、ほとんどが汎用的な大量の DLPI プロトコル処理を再実装しなくて済むという明らかな利点がもたらされます。 802.1q 仮想 LAN (VLAN) などのレイヤー 2 (データリンク) 機能も主に GLD モジュールに実装できるため、すべてのドライバでこれを利用できます。 ただし、802.3ad リンク集積体 (トランキング) など、ネットワークインタフェースとデバイスが 1 対 1 で対応していない機能を実装する場合、アーキテクチャーには問題が生じます。

GLDv2 およびモノリシックドライバは両方とも DLPI メッセージに依存し、STREAMS フレームワークを通じて上位レイヤーと通信します。 このメカニズムは、リンク集積体や 10Gb NIC ではあまり効率的ではありませんでした。 新しいスタックでは、データの局所性を実現し、スタックがきめ細かくデバイスドライバを制御して割り込みを処理できる、優れたメカニズムが必要でした。

6.2 GLDv3 -- 新しいアーキテクチャー

Solaris 10 ソフトウェアには、新しいスタックとともに、GLDv3 (内部名「project Nemo」) という新しいデバイスドライバフレームワークが導入されました。 主要なデバイスドライバの大部分はこのフレームワークに移植され、今後のすべてのデバイスドライバや 10Gb デバイスドライバは、このフレームワークに基づくことになります。 このフレームワークには、(外部の非 IP モジュールが引き続き動作できるように) 下位互換性用に、STREAMS ベースの DLPI レイヤーも用意されています。

GLDv3 アーキテクチャーは、ネットワークスタックのレイヤー 2 を仮想化します。 ネットワークインタフェースとデバイスの間に、1 対 1 の関係はもはや存在しません。 次の図 (図 3) は、MAC サービス モジュール (MAC) に登録された複数のデバイスを示しています。 また、DLPI を通じてデータリンクドライバ (DLD) と通信する従来のクライアントと、カーネルベースで、データリンクサービスモジュール (DSL) に対して関数呼び出しを直接行うクライアントも示されています。

図 3

6.2.1 GLDv3 ドライバ

GLDv3 ドライバは、GLD ドライバに似ています。 ドライバは、misc/mac. および misc/dld. の依存関係にリンクしている必要があります。 次の構造のインスタンスへのポインタを指定して mac_register() を呼び出し、MAC モジュールに登録する必要があります。

typedef struct mac {
    const char       *m_ident;
    mac_ext_t        *m_extp;
    struct mac_impl  *m_impl;
    void             *m_driver;
    dev_info_t       *m_dip;
    uint_t           m_port;
    mac_info_t       m_info;
    mac_stat_t       m_stat;
    mac_start_t      m_start;
    mac_stop_t       m_stop;
    mac_promisc_t    m_promisc;
    mac_multicst_t   m_multicst;
    mac_unicst_t     m_unicst;
    mac_resources_t  m_resources;
    mac_ioctl_t      m_ioctl;
    mac_tx_t         m_tx;
} mac_t;

この構造は登録の有効期間中存続する必要があり、たとえば、mac_unregister() が呼び出されるまで解放できません。 GLDv3 ドライバ _init(9E) エントリポイントは、mod_install(9F) を呼び出す前に mac_init_ops() を呼び出す必要もあり、これらは、_fini(9E) から mod_remove(9F) を呼び出したあとに mac_fini_ops() を呼び出す必要があります。

この mac_t の重要なメンバーを、次に示します。

m_impl - MAC モジュールによって使用され、非公開データをポイントします。 ドライバが読み取りおよび変更することはできません。

m_driver -このフィールドは ドライバによって設定され、その非公開データをポイントします。 この値は、ドライバエントリポイントへの先頭の引数として指定されます。

m_dip - このフィールドは、mac_register() を呼び出すドライバインスタンスの dev_info_t ポインタに設定する必要があります。

m_stat -

typedef uint64_t    (*mac_stat_t)(void *, mac_stat_t);

このエントリポイントは、mac_stat_t 列挙 (下記) で定義されるいずれかの統計情報の値を取得するために、呼び出されます。 すべての値は、64 ビット符号なし整数で格納および返されます。 ドライバが明示的にサポートを宣言していない統計情報の値は要求されません。

m_start -

typedef    int (*mac_start_t)(void *);

インタフェースが登録されたときにリセット/休止状態であったデバイスを、その状態から解除するために、このエントリポイントは呼び出されます。 この呼び出しが行われるまで、MAC モジュールが伝送用のパケットを送信することはなく、ドライバは受信用のパケットを送信できません。 この関数が正常終了すると、ゼロが返されます。 異常終了すると、該当の errno 値が返されます。

m_stop -

typedef void (*mac_stop_t)(void *);

このエントリポイントは、インタフェースの登録を解除できるように、デバイスを停止し、リセット/休止状態にします。 この呼び出しが行われると、MAC モジュールが伝送用のパケットを送信することはなく、この呼び出しが完了すると、ドライバは受信用のパケットを送信できません。

m_promisc -

typedef int (*mac_promisc_t)(void *, boolean_t);

このエントリポイントは、デバイスの混在を設定するために使用されます。 2 番目の引数が B_TRUE の場合、デバイスはメディア上のすべてのパケットを受信します。 引数が B_FALSE に設定されている場合は、デバイスのユニキャストアドレスおよびメディアのブロードキャストアドレス宛てのパケットのみを受信します。

m_multicst -

typedef int (*mac_multicst_t)(void *, boolean_t, const uint8_t *);

このエントリポイントは、デバイスがパケットを受信するマルチキャストアドレスのセットにアドレスを追加または削除するために使用されます。 2 番目の引数が B_TRUE の場合、3 番目の引数でポイントされるアドレスがセットに追加されます。 2 番目の引数が B_FALSE の場合、3 番目の引数でポイントされるアドレスが削除されます。

m_unicst -

typedef int (*mac_unicst_t)(void *, const uint8_t *);

このエントリポイントは、新しいデバイスユニキャストアドレスを設定するために使用されます。 この呼び出しが行われると、デバイスが休止モードの場合を除いて、新しいアドレスおよびメディアブロードキャストアドレスを持つパケットのみを受信します。

m_resources -

typedef void (*mac_resources_t)(void *, boolean_t);

このエントリポイントは、ドライバが個々の受信リソースまたは Rx リングを登録することを要求するために呼び出されます。

m_tx -

typedef mblk_t *(*mac_tx_t)(void *, mblk_t *);

このエントリポイントは、デバイスが伝送用のパケットを送信するために使用されます。 2 番目の引数は、mblk_t 構造に含まれる 1 つ以上のパケットをポイントします。 同じパケットのフラグメントは、b_cont フィールドを使用してリンクされます。 個々のパケットは、先頭のフラグメント内の b_next フィールドによりリンクされます。 パケットは、チェーン内で出現する順番で伝送がスケジュールされます。 スケジュールできないパケットの残りのチェーンは返されます。 スケジュールできないパケットを m_tx() が返す場合、リソースが使用可能になったときに、ドライバは mac_tx_update() を呼び出す必要があります。 すべてのパケットが伝送用にスケジュールされている場合は、NULL が返されます。

m_info - これは、次のように定義されている組み込み構造です。

typedef struct mac_info {
    uint_t        mi_media;
    uint_t        mi_sdu_min;
    uint_t        mi_sdu_max;
    uint32_t      mi_cksum;
    uint32_t      mi_poll;
    boolean_t     mi_stat[MAC_NSTAT];
    uint_t        mi_addr_length;
    uint8_t       mi_unicst_addr[MAXADDRLEN];
    uint8_t       mi_brdcst_addr[MAXADDRLEN];
} mac_info_t;

mi_media は、メディアタイプのセットです。mi_sdu_min は、最小ペイロードサイズです。mi_sdu_max は、最大ペイロードサイズです。mi_cksum は、デバイス cksum 機能フラグの詳細です。mi_poll は、ドライバがポーリングをサポートしている場合の詳細です。mi_addr_length は、メディアが使用するアドレスの長さに設定されます。mi_unicst_addr は、mac_register() が呼び出された時点でのデバイスのユニキャストアドレスを使用して設定されます。mi_brdcst_addr は、メディアのブロードキャストアドレスに設定されます。mi_stat は、ブール値の配列です。

typedef enum {
        MAC_STAT_IFSPEED = 0,
        MAC_STAT_MULTIRCV,
        MAC_STAT_BRDCSTRCV,
        MAC_STAT_MULTIXMT,
        MAC_STAT_BRDCSTXMT,
        MAC_STAT_NORCVBUF,
        MAC_STAT_IERRORS,
        MAC_STAT_UNKNOWNS,
        MAC_STAT_NOXMTBUF,
        MAC_STAT_OERRORS,
        MAC_STAT_COLLISIONS,
        MAC_STAT_RBYTES,
        MAC_STAT_IPACKETS,
        MAC_STAT_OBYTES,
        MAC_STAT_OPACKETS,
        
        MAC_STAT_ALIGN_ERRORS,
        MAC_STAT_FCS_ERRORS,
        MAC_STAT_FIRST_COLLISIONS,
        MAC_STAT_MULTI_COLLISIONS,
        MAC_STAT_SQE_ERRORS,
        MAC_STAT_DEFER_XMTS,
        MAC_STAT_TX_LATE_COLLISIONS,
        MAC_STAT_EX_COLLISIONS,
        MAC_STAT_MACXMT_ERRORS,
        MAC_STAT_CARRIER_ERRORS,
        MAC_STAT_TOOLONG_ERRORS,
        MAC_STAT_MACRCV_ERRORS,
        
        MAC_STAT_XCVR_ADDR,
        MAC_STAT_XCVR_ID,
        MAC_STAT_XVCR_INUSE,
        MAC_STAT_CAP_1000FDX,
        MAC_STAT_CAP_1000HDX,
        MAC_STAT_CAP_100FDX,
        MAC_STAT_CAP_100HDX,
        MAC_STAT_CAP_10FDX,
        MAC_STAT_CAP_10HDX,
        MAC_STAT_CAP_ASMPAUSE,
        MAC_STAT_CAP_PAUSE,
        MAC_STAT_CAP_AUTONEG,
        MAC_STAT_ADV_CAP_1000FDX,
        MAC_STAT_ADV_CAP_1000HDX,
        MAC_STAT_ADV_CAP_100FDX,
        MAC_STAT_ADV_CAP_100HDX,
        MAC_STAT_ADV_CAP_10FDX,
        MAC_STAT_ADV_CAP_10HDX,
        MAC_STAT_ADV_CAP_ASMPAUSE,
        MAC_STAT_ADV_CAP_PAUSE,
        MAC_STAT_ADV_CAP_AUTONEG,
        MAC_STAT_LP_CAP_1000FDX,
        MAC_STAT_LP_CAP_1000HDX,
        MAC_STAT_LP_CAP_100FDX,
        MAC_STAT_LP_CAP_100HDX,
        MAC_STAT_LP_CAP_10FDX,
        MAC_STAT_LP_CAP_10HDX,
        MAC_STAT_LP_CAP_ASMPAUSE,
        MAC_STAT_LP_CAP_PAUSE,
        MAC_STAT_LP_CAP_AUTONEG,
        MAC_STAT_LINK_ASMPAUSE,
        MAC_STAT_LINK_PAUSE,
        MAC_STAT_LINK_AUTONEG,
        MAC_STAT_LINK_DUPLEX,
        MAC_STAT_LINK_STATE,
        MAC_NSTAT    /* must be the last entry */
} mac_stat_t;

マクロ MAC_MIB_SET()MAC_ETHER_SET()、および MAC_MII_SET() は、3 つの各グループのすべての値を B_TRUE に設定するために用意されています。

6.2.2 MAC サービス (MAC) モジュール

いくつかの重要なドライバサポート関数を、次に示します。

mac_resource_add -

extern mac_resource_handle_t mac_resource_add(mac_t *,    mac_resource_t *);

各種のメンバーは、次のように定義されています。

typedef void (*mac_blank_t)(void *, time_t, uint_t);
typedef mblk_t *(*mac_poll_t)(void *, uint_t);


typedef enum {
        MAC_RX_FIFO = 1
} mac_resource_type_t;


typedef struct mac_rx_fifo_s {
    mac_resource_type_t    mrf_type;    /* MAC_RX_FIFO */
    mac_blank_t            mrf_blank;
    mac_poll_t             mrf_poll;
    void                   *mrf_arg;
    time_t                 mrf_normal_blank_time;
    uint_t                 mrf_normal_pkt_cnt;
} mac_rx_fifo_t;



typedef union mac_resource_u {
    mac_resource_type_t    mr_type;
    mac_rx_fifo_t          mr_fifo;
} mac_resource_t;

この関数は、個々の受信リソース (一般には DMA 記述子のリングバッファー) を MAC モジュールに登録するために、m_resources() エントリポイントから呼び出されます。 返された mac_resource_handle_t 値は、mac_rx() への呼び出しに指定されます。 mac_resource_add() の 2 番目の引数は、追加されるリソースを指定します。 リソースは mac_resource_t 構造で指定されます。 現在は、MAC_RX_FIFO タイプのリソースだけがサポートされています。 MAC_RX_FIFO リソースは、mac_rx_fifo_t 構造で記述されます。

この mac_blank 関数は、デバイスの割り込み率を制御するために上位レイヤーにより使用されます。 1 番目の引数は、poll_blank の 1 番目の引数として使用されるデバイスコンテキストです。

その他のフィールドの mrf_normal_blank_time および mrf_normal_pkt_cnt は、デフォルトの割り込み間隔とパケットカウントのしきい値をそれぞれ指定します。 これらのパラメータは、上位レイヤーがデフォルトの割り込み率に戻す場合に、mac_blank の 2 番目および 3 番目の引数として使用される可能性があります。

割り込み率は、異なる引数を指定して poll_blank を呼び出すことによって、上位レイヤーにより制御されます。 割り込み率は、mac_blank の最後の 2 つの引数にこれらの値の倍数を渡すことによって、上位レイヤーにより増減できます。 これらの値をゼロに設定すると割り込みが無効になり、NIC はポーリングモードと見なされます。

mac_poll は、上位レイヤーによって使用されるドライバ独自の関数であり、Rx リングからパケットのチェーンを取得します (2 番目の引数で指定される最大カウントまで)。これは、mac_resource_add の中で以前に指定された mrf_arg に対応します (mac_poll の 1 番目の引数として指定)。

mac_resource_update -

extern void mac_resource_update(mac_t *);

使用可能なリソースが変更された場合にドライバによって呼び出されます。

mac_rx -

extern void mac_rx(mac_t *, mac_resource_handle_t, mblk_t *);

この関数は、mblk_t 構造に含まれる受信用のパケットのチェーンを配信するために呼び出されます。 同じパケットのフラグメントは、b_cont フィールドを使用してリンクされます。 個々のパケットは、先頭のフラグメント内の b_next フィールドによりリンクされます。 登録されたリソースがパケットチェーンを受信すると、適切な mac_resource_handle_t 値が、関数の 2 番目の引数として指定されます。 プロトコルスタックは、複数の CPU に負荷を分散させるときに、この値をヒントとして使用します。 同じフローに属するパケットは、常に同じリソースによって受信されることが想定されています。 リソースが不明または登録されていない場合は、2 番目の引数として NULL が渡されます。

6.2.3 データリンクサービス (DLS) モジュール

DLS モジュールは、DLPI に似たデータリンクサービスインタフェースを提供します。 DLPI で指定される STREAMS メッセージベースのインタフェースに対して、DLS インタフェースはカーネルレベルの関数インタフェースです。 このモジュールは、上位レイヤーがデータリンクサービスを作成および破棄するのに必要なインタフェースと、NIC を plumb および unplumb するのに必要なインタフェースも提供します。 GLDv3 ベースのデバイスドライバにおける NIC の plumb および unplumb は、以前の GLDv2 またはモノリシック DLPI デバイスドライバのときから変更されていません。 主要な変更は、直接呼出し、パケットチェーン、および NIC に対するきめ細かな制御が可能なデータパスにあります。

6.2.4 データリンクドライバ (DLD)

データリンクドライバは、DLS および MAC モジュールが提供するインタフェースを使用する DLPI をサポートします。 ドライバは、制御ノードに渡される IOCTL を使用して構成されます。 これらの IOCTL が、個々の DLPI プロバイダノードを作成および破棄します。 このモジュールは、NIC を plumb/unplumb するのに必要な DLPI メッセージを扱い、GLDv3 に対応していないクライアント用に STREAMS を経由するデータパスについて、下位互換性を提供します。

6.3 GLDv3 リンク集積体アーキテクチャー

GLDv3 フレームワークは、IEEE 802.3ad で定義されるリンク集積体をサポートします。 この機能を設計する上での主要な設計原則は、次のようなものでした。

  • コードを変更せずに GLDv3 MAC ドライバの集積を可能にする。
  • 集積されないデバイスのパフォーマンスを維持する。
  • 集積デバイスのパフォーマンスを、各メンバーの回線速度の累積とする。たとえば、集積によるオーバーヘッドを最小とする。
  • 手動構成と Link Aggregation Control Protocol (LACP) の両方をサポートする。

GLDv3 のリンク集積体は、aggr と呼ばれる擬似ドライバによって実装されます。 このドライバは、リンク集積体グループに対応する仮想ポートを、GLDv3 MAC レイヤーに登録します。 図 4 に示すように、MAC レイヤーで提供されるクライアントインタフェースを使用して、集積された MAC ポートを制御し、このポートと通信します。 また、dladm コマンドで使用される擬似 aggr コマンドをエクスポートして、リンク集積インタフェースを構成および制御します。 MAC ポートがリンク集積体グループの一部として構成されると、DLS レイヤーなどのほかの MAC クライアントが同時にアクセスできなくなります。 排他的アクセスは、MAC レイヤーによって適用されます。 LACP の実装は、個別の MAC ポートまたはリンクにアクセスできる aggr ドライバによって行われます。

図 4

GLDv3 aggr ドライバは、上位レイヤーに対して標準の MAC モジュールとして動作し、標準の NIC インタフェースとして表示されます。これは、dladm で作成されたあと、ifconfig によって構成および管理できます。 各 MAC ポートからのデータパスと割り込みを上位レイヤーが個別に管理できるように、aggr モジュールは、集積体を構成する各 MAC ポートを、mac_resource_add 関数を使用する上位レイヤーに登録します。 集積インタフェースは、ほとんどの場合 1 つの IP アドレスを持つ単一のインタフェースとして管理されます。 データパスは、一意の CPU/squeue ごとに個々の NIC として管理され、Solaris OS 集積体機能を実現します。 これによって、オーバーヘッドはゼロに近くなり、集積体を構成する MAC ポートの数に関して線形のスケーラビリティーが提供されます。

6.4 チェックサムオフロード

Solaris 10 プラットフォームでは H/W チェックサムオフロード機能が改善され、ほとんどアプリケーションで全体のパフォーマンスが向上しました。 ある時期、16 ビットの 1 の補数チェックサムオフロードフレームワークが、Solaris ソフトウェアに存在していました。 これは当初、Solaris 2.6 OS のゼロコピー TCP/IP の要件として追加されたものですが、最近になるまで、ほかのプロトコルを処理できるように拡張されませんでした。 Solaris テクノロジでは、2 つのクラスのチェックサムオフロードを次のように定義しています。

  • 完全: TCP および UDP パケットの擬似ヘッダーチェックサム計算を含む、ハードウェア内での完全なチェックサム計算。 ハードウェアが、プロトコルヘッダーを解析できることが前提です。
  • 部分: 開始、終了、およびスタッフオフセットに基づく、「完全でない」 1 の補数によるチェックサム。ハードウェア内の擬似ヘッダー計算機能を使用せず、チェックサムの対象となるデータのスパンとトランスポートチェックサムフィールドの場所を記述します。

現在の大部分のネットワークアダプタは、インタフェースにわずかな違いはありますが、いずれかのクラスのチェックサムオフロードをサポートするため、断片化されていない IPv4 のケース (ユニキャストまたはマルチキャスト) のサポートを追加することは、送信と受信の両方にとって重要ではありません。 IPv4/IPv6 上の TCP/UDP パケットについてチェックサム計算を処理できる、完全チェックサムネットワークアダプタはほとんどないため、IPv6 の場合はそれほど簡単ではありません。

断片化された IP の場合も、同様の制約があります。 送信時、チェックサムは断片化されていないデータグラムに適用されます。 アダプタがチェックサムオフロードをサポートするためには、最終的にチェックサムを計算し、回線上でフラグメントを送信する前に、すべての IP フラグメントをバッファリングできる (またはハードウェア内で断片化を実行できる) 必要があります。そのときまで、アウトバウンド IP フラグメントのチェックサムオフロードを実行することはできません。 これに対して、ほとんどの完全チェックサム (およびすべての部分チェックサム) ネットワークアダプタは、チェックサム値を計算し、その値をネットワークスタックに提供できるため、受信フラグメントの再アセンブリは柔軟に行われます。 フラグメントの再アセンブリの段階で、ネットワークスタックは、すべての値を結合することによって、断片化されていないデータグラムのチェックサム状態を求めることができます。

IP オプションがある場合は、チェックサムをオフロードしないことにより、処理が簡単化されました。 部分チェックサムオフロードの場合、一部のアダプタでは、開始オフセットを単純な IP パケットに十分な幅に制限します。 オプションがあるためにプロトコルヘッダーの長さがこの制限を超える場合、開始オフセットが折り返すため、計算が正しく行われません。 完全チェックサムオフロードの場合、該当のアダプタで、IPV4 ソースルーティングオプションを正しく処理できるものはありません。

送信のチェックサムオフロードが発生すると、ネットワークスタックは、該当のパケットを、ドライバがチェックサム計算をハードウェアにオフロードするのに必要な付属情報に関連付けます。

インバウンドの場合、ドライバは、ハードウェアで計算されるチェックサム値に関連付けられているパケットを完全に制御できます。 ドライバが DL CAPAB HCKSUM を通じてその機能を通知すると、ネットワークスタックは、IPv4 および IPv6 パケットの完全または部分チェックサム情報を受け取ります。 このプロセスは、断片化されていないペイロードと断片化されたペイロードの両方について発生します。

チェックサム検証は完全に再アセンブリされたデータグラムに対して発生するため、断片化されたパケットは、最初に、再アセンブリプロセスを経由する必要があります。 再アセンブリの中で、ネットワークスタックは、ハードウェアで計算された各フラグメントのチェックサム値を結合します。

6.4.1 dladm -- データリンク管理用の新しいコマンド

ifconfig は、スタック内のさまざまなレイヤーを管理しようとして、あまりに機能が膨れ上がってしまいました。 Solaris 10 OS では、データリンクサービスを管理し、ifconfig の負担を軽減するために、dladm コマンドが導入されています。 dladm コマンドは、3 種類のオブジェクトを処理します。

  • link: 名前で特定されるデータリンク
  • aggr: キーで特定されるネットワークデバイスの集積体
  • dev: ドライバ名とインスタンス番号の組み合わせで特定されるネットワークデバイス

集積体のキーは、1 ~ 65535 の整数です。 デバイスによっては、構成可能なデータリンクまたは集積体をサポートしません。 このようなデバイスで提供される固定のデータリンクは、dladm を使用して表示できますが、構成することはできません。

GLDv3 フレームワークでは、ユーザーは、集積体を構成する一方で、集積体の各種メンバーにわたるアウトバウンド負荷分散ポリシーを選択できます。 ポリシーでは、パケットの送信に使用される dev オブジェクトを指定します。 ポリシーは、1 つ以上のレイヤー指定子をカンマで区切ったリストで構成されます。 レイヤー指定子は、次のいずれかです。

  • L2: パケットの発信元および送信先 MAC アドレスに従ってアウトバウンドデバイスを選択します。
  • L3: パケットの発信元および送信先 IP アドレスに従ってアウトバウンドデバイスを選択します。
  • L4: パケットに格納されている上位レイヤーのプロトコル情報に従ってアウトバウンドデバイスを選択します。 TCP と UDP の場合、これには発信元ポートと送信先ポートが含まれます。 IPsec の場合は、SPI (セキュリティーパラメータインデックス) が含まれます。

たとえば、上位レイヤーのプロトコル情報を使用するには、次のポリシーを使用します。

-P L4

発信元および送信先 IP アドレスに加えて、発信元および送信先 MAC アドレスを使用するには、次のポリシーを使用します。

-P L2,L3

フレームワークは、lacp-mode および lacp-timer サブコマンドを通じて dladm で制御できる GLDv3 ベースの集積体に対して、Link Aggregation Control Protocol (LACP) もサポートします。 lacp-mode は、offactive、または passive に設定できます。

新しいデバイスがシステムに挿入されると、再構成ブートまたは DR の際に、そのデバイスに対してデフォルトの非 VLAN データリンクが作成されます。 すべてのオブジェクトの構成は、リブートを通じて持続されます。

将来的には、dladm と、すべての持続的情報が格納される専有ファイル (/etc/datalink.conf) を使用して、デバイス固有のパラメータを管理する予定です。現在、このパラメータは、ndd ドライバ固有の構成ファイルと /etc/system で管理されています。


7.0 パフォーマンスのチューニング

Solaris 10 スタックのチューニングは、使用されているハードウェアにかかわらず、優れた革新的なパフォーマンスを実現するように工夫されています。 その秘訣は、割り込みモードとポーリングモードの動的な切り替えや、負荷が非常に高い場合にポーリングモードに切り替えてスループットを高めたり適切な待ち時間を定めるなどの手法を使用することにあります。NIC によるパケット単位の割り込みを許可することで負荷が管理可能な場合、割り込みモードとポーリングモードの動的な切り替えにより、待ち時間が大きく改善されます。 デフォルト値も、ハードウェア構成に基づいて慎重に選択されています。 たとえば、Solaris 10 以前のリリースでは、チューニング可能な変数 tcp_conn_hash_size は非常に控えめな値でした。 デフォルト値の 512 ハッシュバケットは、サポートされる中でメモリーに関して最低の構成に基づいて選択されていました。 Solaris 10 OS は、ブート時に空きメモリーを調べて tcp_conn_hash_size の値を選択します。 同様に、時間待ち状態から接続が「取得」されると、接続インスタンスに関連付けられているメモリーがすぐに解放されるのではなく (この場合も使用可能な総システムメモリーに基づきます)、free_list に設定されます。 特定の時間内に新しい接続が到着すると、free_list のメモリーの再使用が試みられます。新しい接続が到着しない場合、free_list は定期的にクリーンアップされます。

これらの機能にもかかわらず、場合によっては、チューニング可能ないくつかの変数に手を加えて、極端な場合や特定のワークロードに対処する必要があります。 次のセクションでは、スタックの動作を制御するチューニング可能変数について説明します。 その影響を十分に理解することが重要です。十分に理解しないままに操作すると、システムが不安定になることがあります。 ほとんどのアプリケーションとワークロードでは、デフォルト値で最適な結果が得られることに留意してください。

ip_squeue_fanout: 1 つの NIC からの着信接続をすべての CPU にファンアウトするかどうかを制御します。 値 0 は、着信接続を、割り込みを受けた CPU に接続している squeue に割り当てることを意味します。 値 1 は、接続をすべての CPU にファンアウトすることを意味します。 NIC が CPU より高速で (たとえば 10Gb NIC)、複数の CPU で NIC を処理する必要がある場合は、後者が必要です。 /etc/system で次の行を追加することにより設定します。

set ip:ip_squeue_fanout=1

ip_squeue_bind: ワークスレッドが特定の CPU にバインドするかどうかを制御します。 バインドしている場合 (デフォルト)、局所性が向上します。 プロセッサセットがシステム上に作成される場合のみ、デフォルト以外の値 (バインドなし) を選択してください。 /etc/system で次の行を追加することにより設定を解除します。

set ip:ip_squeue_bind=0

tcp_squeue_wput: 書き込み側の squeue のドレイン動作を制御します。

  1. 自分のパケットを処理し、squeue をドレインしない。
  2. 自分のパケットおよびキューイングされているパケットも処理する。

デフォルト値は 2 ですが、/etc/system で次の行を追加することにより変更できます。

set ip:tcp_squeue_wput=1

CPU の数がアクティブな NIC の数よりはるかに多く、プラットフォームのメモリー待ち時間が本質的に長いため、アプリケーションスレッドが squeue のドレインを行い、動きが取れなくなる可能性が高い場合は、この値を 1 に設定します。

ip_squeue_wait: キューイングされたパケットを処理するまでのワークスレッドの待ち時間を ms 単位で制御します (割り込みまたは書き込み側のスレッドがパケットを処理することが前提です)。 トラフィックが十分なサーバーの場合は、デフォルト値の 10ms が適切です。しかし、デスクトップのように対話型のトラフィックが多いマシンでは待ち時間が問題となるため、/etc/system で次の行を追加することにより、この値を 0 に設定する必要があります。

ip:ip_squeue_wait=0

また、特に大量のメモリーを搭載したシステムでは、プロトコルレベルのチューニング (max_buf、高位境界値、低位境界値の変更など) が効果的です。


8.0 今後の展望

Solaris ネットワークスタックは、引き続き、レイヤー間の垂直統合を基礎とすることで、局所性とパフォーマンスのさらなる改善が期待されます。 チップマルチスレッドおよびマルチコア CPU の出現に伴い、ローエンドシステムでも、並列実行パイプライン数が増加し続けることが期待されます。 現代の一般的な 2 CPU マシンはデュアルコアであり、実行パイプライン数は 4 で、多くの場合ハイパースレッドにも対応しています。

NIC も高度になり、MSI-X による複数の割り込み、細かい分類機能、複数の DMA チャネル、大規模セグメントオフロードのような各種ステートレスオフロードなどを提供します。

今後、TCP オフロードエンジン、Remote Direct Memory Access (RDMA)、iSCSI のサポートを含めて、このようなハードウェアの動向を活用していくことが期待されます。 現在取り組んでいるその他の特別な分野は、次のとおりです。

  • ネットワークスタックの仮想化: サーバー統合、および同じ物理インスタンス内で複数の仮想マシンを実行するという業界全体の動向に伴い、Solaris スタックが効率的に仮想化できることが重要です。
  • B/W リソース制御: ネットワークの仮想化をもたらした動向が、同じボックス上の各種アプリケーションと仮想マシンの帯域幅使用を効率的に制御する必要性ももたらしています。
  • 高パフォーマンスの Sun 以外のモジュールのサポート: 現在の Solaris 10 フレームワークは、依然として Sun のモジュール専用です。 ISV にとっては STREAMS ベースモジュールが唯一のオプションであるため、新しいフレームワークの潜在能力をすべて使用することができません。
  • 転送パフォーマンス: Solaris OS の転送パフォーマンスを改善するための作業が行われています。
  • パフォーマンスに優れたネットワークセキュリティー: 世界は複雑になり、敵意に溢れています。 パフォーマンスとセキュリティーのどちらかを選択するのはもはや不可能です。 どちらも欠かせません。 Solaris ソフトウェアは常に非常に強力なセキュリティーを誇ってきましたが、Solaris 10 OS はパフォーマンスを犠牲にすることなくセキュリティーを実現した点で、大きな前進を遂げています。 IP フィルタのパフォーマンスと機能の強化、およびサービス拒否攻撃の検出とその対処に関するまったく新しい方法の提供に、引き続き取り組んでいます。

9.0 謝辞

この記事の一部を寄稿してくれた Thirumalai Srinivasan、Adi Masputra、Nicolas Droux、および Eric Cheng に感謝の意を表します。 Solaris ネットワークコミュニティーのすべてのメンバーにも、その支援に対して感謝の意を表します。


ここで取り上げたすべてのテクニカルマニュアル内のコード (記事、FAQ、サンプルを含む) は、特にほかのライセンス供与がないかぎり、このライセンスのもとに提供されています。


BigAdmin
  
 
BigAdmin Upgrade Hub