Network programming on Linux - packet-socket -
2009年5月19日
Ethernetスイッチは現在ではネットワークを構築するためには欠かせない機器になりました。その種類は豊富で、家庭内向けの4ポートしかない小さな製品から企業LANやキャンパスLAN向けの100ポート以上装備できる大きな製品まであります。また、サポートされている機能も様々です。
1989年に米国Kalpana社がコンセプトを発表して以来20年が経った現在、Ethernetスイッチはとても高度な機能を備えています。代表的な機能は1台のスイッチの中に複数のブロードキャスト・ドメインを作成するVLAN機能です。中には、VLANに設けた仮想インターフェースにIPホストアドレスを割り当て、VLAN間でIPルーティングを行う製品もあります。(一般的にはこのような製品はEthernetスイッチとは呼ばず、L3スイッチと呼びます。)その他、1本のトランクリンクを複数のVLANが共有したり、複数の物理ポートを一つの論理的なポートに束ねたりすることが出来ます。
はなしをEthernetスイッチが発表されたころに戻しますが、当時のEthernetスイッチには現在のような豊富な機能は無く、ユーザーが製品を選ぶときは単にEthernetスイッチの転送性能に注目していました。現在の主流であるストアーアンドフォワードのスイッチング方式と、カットスルーまたはフラグメントフリーのスイッチング方式の違いなど、各メーカーはこぞって自社の製品がいかに高速であるかを主張していました。当時、主に使用されていたリピータ・ハブではEthernetフレームが通過する際に遅延は発生しないのですが、Ethernetスイッチでは必ず遅延が生じます。ユーザーは、最悪フレームが消失してしまうのではないかと危惧して、Ethernetスイッチを導入することに慎重でした。
すべてのEthernetスイッチはプログラム(ソフトウェア)を必要とします。このプログラムは、受信したEthernetフレームの宛先Ethernetアドレスを認識し、アドレスとポートの関係を管理するテーブルを検索して出力ポートを決定します。同時に、未登録の発信元Ethernetアドレスをテーブルに登録したり、長時間使用されていないエントリーをテーブルから削除したりします。このプログラムの実行時間がEthernetフレームをスイッチするときの遅延になります。プログラムを実行するハードウェアが汎用CPUであろうと、ASIC(特定アプリケーション用集積回路)であろうと、どのような製品であってもEthernetスイッチには必ずプログラムが存在します。そしてEthernetフレームがスイッチを通過するとき必ず遅延が生じます。
この記事では、EthernetフレームがEthernetスイッチを通過するときに生じる遅延を求めるための一般ユーザーが実行可能な手法を説明すると共に、実際にEthernetスイッチの遅延を計測したときの事例を紹介します。
なお、この記事を読むにあたっては、C言語によるプログラミングの経験とSocket APIに関する基本的な知識が必要です。
スイッチング処理に要する時間を求めたいとき、ネットワーク・エミュレータやネットワーク・タップなどの特別な機材を用いると非常に正確な値を得ることが出来ると思います。しかし、そのような機材の多くはとても高価で、ネットワーク機器販売代理店以外の一般ユーザーが保有していることはほとんどありません。この記事では特別な機材を使わずにEthernetスイッチの遅延を計測しています。自慢するようなことではありませんが、この記事の中で用いているEthernetスイッチの遅延を計測する為の機材は、60,000円程のパーソナルコンピュータです。
Ethernetスイッチの遅延を求めるために、用意した平凡なパーソナルコンピュータにネットワークインターフェースカード(NIC)を2つ以上装備し、プログラムを作成します。このプログラムは任意のEthernetフレームを生成した後、1つのインターフェースから送信して他のインターフェースで受信するだけです。プログラムが作成できたら、これから示す3つの段階の手順に従ってスイッチの遅延時間を算出します。
先ず、【図 2-1】に示す構成を作り、二つのインターフェース間でフレームを転送したとき何秒掛かったかを調べます。
+--------+ | |eth1 | +--------+ | Host | | (LAN Cable) | +--------+ | |eth2 +--------+ 【図 2-1】構成図1
次に、【図 2-2】に示す構成を作り、同じように二つのインターフェース間でフレームを転送したときに掛かる時間を調べます。
+--------+ +--------+ | |eth1 port1| | | +-------------+ | | Host | | Switch | | +-------------+ | | |eth2 port2| | +--------+ +--------+ 【図 2-2】構成図2
【図 2-1】の構成と【図 2-2】の構成の違いは、スイッチが1台増えたことと、LANケーブルが1本増えたことです。
100MbpsのEthernetの場合、媒体(LANケーブル)を通じて1ビットの信号を伝送する為に必要な時間は10ナノ秒(ns)です。例えば、64バイト(512ビット)のデータを伝送する場合は5120ナノ秒、別の単位で言うと5.12マイクロ(μ)秒必要です。Ethernetの仕様では、ハードウェアの立ち上がり損失によって有効ビットが欠落することを防ぎ、かつ信号同期を調整する為に、Ethernetフレームの前に8バイトのプリアンブルを付加してEthernetフレームを送信することになっています。従って、64バイトのEthernetフレームを伝送する場合、実際には512ビットに64ビットを加えた576ビットを伝送することになります。その所要時間は5.76μ秒です。
ステップ1とステップ2で得られた所要時間の差からLANケーブル1本分の伝送時間を差し引けば、Ethernetスイッチで費やされる処理時間が求められます。
今回は、計測プログラムのプラットホームにLinuxを採用し、packetソケットを使用してプログラムを作成します。なぜ、Linuxを採用したかについては後ほど説明します。
作成するプログラムの概要を以下に示します。
int main() { /* 送信用ソケットの作成 */ /* 受信用ソケットの作成 */ /* Ethernetフレームの作成 */ /* 現在時刻を送信時刻として取得 */ /* 送信 */ /* 受信 */ /* 受信時刻の取得 */ /* 時間差の算出 */ } 【図 3-1】プログラムの概要
送信用ソケットはpacketソケットを使用します。その理由は、プロトコルファミリPF_INETのソケット(以後inetソケットと略します)を使って自ホストが装備しているインターフェースに向けてデータを送信した場合、そのデータはホスト内部で折り返されてしまいLANケーブルには信号が流れないからです。
自ホストが装備している他のインターフェースに向けてLANケーブルを伝ってフレーム送る為には、目標のインターフェース宛の完全なEthernetフレームを作成し、packetソケットを通じてEthernetフレームを送出しなければなりません。
------------------------------------------------------------ int s_sd; s_sd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (s_sd == -1) { perror("ERROR socket() in main() "); return EXIT_FAILURE; } ------------------------------------------------------------ 【図 3-2】送信用ソケットを作成するコード
受信用ソケットにはpacketソケット以外に、rowソケットまたはinetソケットを使うことができます。受信用ソケットの設定で必要なことは特定のインターフェースでのみEthernetフレームを受信することです。そのためにbind関数を用います。受信用ソケットがpacketソケットの場合は、インターフェース番号をソケットに結びつけます。一方、受信用ソケットがinetソケットの場合は、IPホストアドレスまたはIPホストアドレスとポート番号をソケットに結び付けます。
受信用ソケットに関してはまだ作業があります。それはソケットオプションの設定です。今回は受信タイムアウトと受信タイムスタンプの二つのオプションを設定します。
パケットを受信するrecv関数やrecvfrom関数は、そのプロセスが完了するまでアプリケーションのプロセスをブロックします。万一、パケットの受信で問題が生じた場合、アプリケーションはいつまで経っても終わりません。アプリケーションのプロセスにシグナルを送って強制敵に終了させることはできますが、プログラムの中でmalloc関数を使っている場合、確保したメモリー領域をシステム資源に戻すことができません。このような事態を避けるためにソケットに受信タイムアウトを設定します。手順は先ず、構造体timeval型の変数を確保してタイムアウトする経過時間を設定します。次にsetsockopt関数を用いて受信用ソケットで受信タイムアウトを有効にします。以下に受信タイムアウトを5秒に設定する場合のプログラムコードの例を示します。
------------------------------------------------------------ struct timeval skopt_timeout; skopt_timeout.tv_sec = 5; skopt_timeout.tv_usec = 0; if (setsockopt(r_sd, SOL_SOCKET, SO_RCVTIMEO, &skopt_timeout, sizeof(struct timeval)) == -1) { perror("ERROR setsockopt(SO_RCVTIMEO) in main()"); return EXIT_FAILURE; } ------------------------------------------------------------ 【図 3-3】受信タイムアウトを設定するコード
受信タイムスタンプを設定する手順は、setsockopt関数を用いて受信用ソケットで受信タイムスタンプを有効にするだけです。ソケットで受信タイムスタンプが有効になっていると、受信したEthernetフレームがシステム・バッファに格納されたときの時刻をアプリケーション・プログラムで知ることが出来ます。プログラム・コードの例は後の節において、受信時刻をアプリケーション・プログラムで受け取る方法と合わせて示します。
setsockopt関数に与える引数は、良く知られているSO_TIMESTAMPの代わりにSO_TIMESTAMPNSを指定します。従来のSO_TIMESTAMPではEthernetフレームを受信したときの時刻をマイクロ(μ)秒単位でしか得ることができませんが、SO_TIMESTAMPNSを指定すればEthernetフレームを受信したときの時刻をナノ(nano)秒単位で得ることが出来ます。100Mbps Ethernetの場合、1ビットの信号をLANケーブルを通じて伝送するために必要な時間は10ナノ秒です。そのためEthernetフレームの受信時刻をナノ秒単位で得る方がより正確な計測ができます。但し、SO_TIMESTAMPNSは全てのLinuxディストリビューションでサポートされているとは限りませんので事前に確認する必要があります。sock(7)のオンライン・マニュアルに載っていなくても、ソースファイルを確認すれば、SO_TIMESTAMPNSが利用可能かどうか判ります。
先にも述べましたが、同一ホストの他のインターフェースに向けてLANケーブルを介してデータを送信したいときは、Ethernetヘッダを含めた完全なEthernetフレームを生成しなければなりません。
Ethernetヘッダに続くペイロードのフォーマットには特に制限はありませんが、受信用ソケットで受け取れるフォーマットでなければなりません。受信用ソケットがrowソケットならEthernetヘッダの後に完全なIPヘッダが必要です。また、受信用ソケットがSOCK_DGRAM型のinetソケットの場合はIPヘッダの後に完全なUDPヘッダが必要です。
プログラムが生成するフレームのフォーマットを以下に示します。
0 0 1 2 3 0 8 6 4 1 +--------+--------+--------+--------+ | Destination Ethernet address | + +-----------------+ | | | +-----------------+ + | Source Ethernet address | +-----------------+-----------------+ | Type (0x0101) | | +-----------------+ + | The time of packet sent | + +-----------------+ | | | +-----------------+ + | | // Padding (38 octets) // | | +-----------------------------------+ 【図 3-4】計測用独自フレーム
Ethernetスイッチは、通常、Ethernetヘッダの宛先アドレスをもとに転送すべき出力ポートを決定しますので、フレームの中にはEthernetヘッダがあれば十分です。IPヘッダやTCPヘッダがなくても問題ありません。Ethernetタイプの0x0101は実験のための番号です。 EthernetタイプにIP(0x0800)を指定して、完全なIPヘッダを含んだフレームを使うと、フレームを受信したときにカーネルのネットワークプロトコル層がパケットを処理するためにCPUパワーを無駄に使うことになります。
しかし、検査対象のネットワーク機器がEthernetスイッチではなくIPルーターである場合は、Ethernetヘッダに続けてIPヘッダが必要です。また、IPパケットフィルターなどのファイヤーウォール機器の遅延を調べたいときは、IPヘッダに続けてUDP/TCPヘッダ、あるいはICMPヘッダが必要になるでしょう。
UDP/TCPヘッダを書く場合は、宛先ポート番号に注意してください。宛先ポート番号にシステムで待ち受けていないポート番号を指定すると、カーネルは"ICMP Port unreachable"メッセージを生成して、発信元ホストに向けて送信してしまいます。
ICMPを使う場合はメッセージタイプに注意します。メッセージタイプに8(Echo Request)などの何らかの応答を求めるような値を使用すると、システムは、Ethernetフレームを受信した後、ICMPプログラムが応答パケット(Type 0: Echo Reply)を作成して発信元IPホストに向けて返信します。ですが、これを受信するプロセスはありません。ICMPのメッセージタイプには1または2などの未使用の(予約されている)値を指定します。受信パケットのIPヘッダのプロトコル番号が1のとき、ICMPのメッセージタイプが何であれICMPプログラムが起動しますが、このICMPプログラムはメッセージタイプの値が1のICMPメッセージに対しては何もしないで終了します。
フレームの送信時刻について説明する前に、先ず、フレームの受信時刻について説明します。 幸運なことに、LinuxでもFreeBSDでもEthernetフレームを受信した時刻をアプリケーション・プログラムで知ることができます。ただ、FreeBSDよりもLinuxの方がより詳細な時刻を得ることができます。
FreeBSDにおいて、アプリケーション・プログラムでEthernetフレームを受信するには、BPF (Berkely Packet Filter)デバイスをオープンし、特定のインターフェースに関係付けた後、read関数を用いてそのBPFデバイスからフレームを読み出します。読み出した個々のフレームには構造体bpf_hdr型のBPFヘッダが付いており、この構造体bpf_hdr型のメンバbh_tstampに受信時刻が記録されています。もう少し詳しく述べると、BPFヘッダのbh_tstampにある時刻はデータがBPFバッファに格納される直前の時刻です。
ネットワーク・インターフェースにEthernetフレームが到着するとEthernetコントローラがハードウェア割り込み要求を生成します。このハードウェア割り込み要求は割り込み制御装置を経てカーネルに渡ります。カーネルはハードウェア割り込み要求を受け取ると実行中の処理を中断し、割り込み要求を出したハードウェアに応じたデバイス・ドライバを起動します。(割り込み要求に応じたハンドラの登録などの詳細については他の文献を参照してください。)デバイス・ドライバはシステム・バッファ(mbuf)を準備して、インターフェースのキューからデータを取り出しmbufに移します。DMA(Direct Memory Access)がサポートされている場合、インターフェースのキューの実体はメモリ上のDMA領域です。次にデバイス・ドライバはsys/net/if_ethersubr.cの中のether_input関数を呼び出します。ether_input()はEthernetヘッダを検査し、異常がなければsys/net/ethernet.hの中で定義されているマクロETHER_BPF_MTAPを実行します。その後、sys/net/bpf.cの中のbpf_tap関数で現在時刻が保持され、bpf_tap()によって呼び出されるcatchpacket関数で保持されている時刻を構造体bpf_hdr型のメンバbh_tstampに格納します。この直後にmbufのデータがBPFのstoreバッファにコピーされます。ただ残念なことに、bh_tstampは構造体timeval型であり、格納されているフレームの受信時刻はマイクロ秒単位の時刻です。
Linuxにおけるパケットの受信時刻の記録は、FreeBSDのそれと比べて大きな違いが二つあります。一つはLinuxのシステム・バッファ(sk_buff)の中に受信時刻を格納する領域があること。もう一つは、格納される受信時刻がナノ秒単位であることです。 構造体sk_buff型はktime_t型のtstampというメンバを含んでおり、ここにフレームの受信時刻が記録されます。構造体sk_buff型の定義は/usr/src/linux-source-<version>/include/linux/skbuff.hの中に、そしてktime_t型の定義はinclude/linux/ktime.hの中にあります。構造体sk_buff型はソケット層の関数が扱うデータの基本単位ですので、ここに受信時刻が記録されることによりpacketソケットに限らず、rawソケットなどを使用する場合でもフレームの受信時刻を得ることが出来ます。
FreeBSDと同様に、ネットワーク・インターフェースにEthernetフレームが到着するとEthernetコントローラがハードウェア割り込み要求を生成し、割り込み要求を受け取ったカーネルが適切なデバイス・ドライバを起動します。Linux用のデバイス・ドライバはシステム・バッファ(sk_buff)を準備して、インターフェースのキューからデータを取り出しsk_buffに移します。この後、デバイス・ドライバはnetif_rx関数を呼び出すか、またはnetit_rx_schedule関数を呼び出します。どちらが実行されるかはEthernetフレームを受信したデバイスがNAPI(new API)をサポートしているか否かによります。NAPIをサポートしている場合、netit_rx_schedule()が呼び出され、netit_rx_schedule()はソフト割り込みを生成します。その結果として、netif_receive_skb関数が呼び出されます。このnetif_rx()またはnetif_receive_skb()の中の早い段階で現在時刻がsk_buffに記録されます。二つの関数の定義は/usr/src/linux-source-<version>/net/core/dev.cの中にあります。
NAPIは連続して到着するEthernetフレームを効率良く処理するための仕組みです。NAPIをサポートしていない場合、デバイスにEthernetフレームが到着する度にハードウェア受信割り込みが発生します。データ通信が頻繁に行われるシステムでは、この大量に発生するハードウェア割り込みがシステムのパフォーマンスを低下させてしまいます。NAPIでは、ハードウェア割り込みが発生すると一時的に同じハードウェア割り込みの発生を禁止します。そして直ちに受信処理を行わず、ソフトウェア割り込みを生成して少し後で受信処理を開始します。その間、後続フレームが到着してもハードウェア割り込みは発生しません。受信処理が開始されるときには複数のフレームが保留されていることになりますが、データの受信処理が多いシステムでは、一々、ハードウェア割り込み発生させて処理するよりも、溜まったデータを連続して処理するほうが効率的なのです。 ただし、NAPIをサポートしている場合はEthernetフレームが到着しても直ちに処理されないため、NAPIを無効にしている場合と比べるとEthernetフレームの受信時刻に少し遅れが生じます。今回使用したシステムの場合で約500ナノ秒の差がみられました。NAPIを無効にしたいときはカーネルのコンフィグレーションを編集してカーネルを再構築します。例えば、今回使用したRealtek 8169 Gigabit Ethernet NICの場合、make menuconfigで次のオプションを無効にしてカーネルを再コンパイルします。
Device Drivers ---> Network device support ---> Ethernet (1000 Mbit) ---> Realtek 9169 gigabit ethernet support [ ] Use Rx Polling (NAPI) (EXPERIMENTAL)
アプリケーション・プログラムでEthernetフレームの受信時刻を得るためにはソケットオプションのSO_TIMESTAMPまたはSO_TIMESTAMPNSをソケットに設定する必要があります。SO_TIMESTAMPを設定した場合は受信時刻をマイクロ秒の単位で、SO_TIMESTAMPNSを設定した場合はナノ秒の単位で得ることが出来ます。今回作成するプログラムのプラットフォームにLinuxを選んだ理由は、LinuxではソケットオプションのSO_TIMESTAMPNSをサポートしており、これを使うことでEthernetフレームの受信時刻をナノ秒の単位で得られるからです。
Ethernetフレームの受信時刻をアプリケーション・プログラムに渡す関数はnet/socket.cの中の__sock_recv_timestamp関数です。この関数は、先ず構造体sk_buff型のtstampの値をktime_t型の変数ktに代入し、その後構造体sock型のフラグを検査して処理を二通りに分岐します。もし、ktの値が0なら分岐した後それぞれのブロックで現在時刻をktに代入します。ソケットのフラグを検査した結果SOCK_RCVTSTAMPNSが無い場合は、先ず構造体timeval型の変数tvを用意し、次にktをktime_to_timeval関数に渡し、その戻り値をtvに代入します。一方、SOCK_RCVTSTAMPNSがある場合は、先ず構造体timespec型の変数tsを用意し、次にktをktime_to_timespec関数に渡し、その戻り値をtsに代入します。ktime_to_timeval()とktime_to_timespec()の定義はinclude/linux/ktime.hにあります。どちらのブロックでも最後にput_cmsg関数を呼び出しています。put_cmsg()はカーネルからユーザーに対して補助データを送る関数です。
------------------------------------------------------------ void __sock_recv_timestamp(struct msghdr *msg, struct sock *sk, struct sk_buff *skb) { ktime_t kt = skb->tstamp; if (!sock_flag(sk, SOCK_RCVTSTAMPNS)) { struct timeval tv; /* Race occurred between timestamp enabling and packet receiving. Fill in the current time for now. */ if (kt.tv64 == 0) kt = ktime_get_real(); skb->tstamp = kt; tv = ktime_to_timeval(kt); put_cmsg(msg, SOL_SOCKET, SCM_TIMESTAMP, sizeof(tv), &tv); } else { struct timespec ts; /* Race occurred between timestamp enabling and packet receiving. Fill in the current time for now. */ if (kt.tv64 == 0) kt = ktime_get_real(); skb->tstamp = kt; ts = ktime_to_timespec(kt); put_cmsg(msg, SOL_SOCKET, SCM_TIMESTAMPNS, sizeof(ts), &ts); } } ------------------------------------------------------------ 【図 3-1】__sock_recv_timestamp()
ユーザーは、アプリケーション・プログラム内の中でrecvmsg関数を使うことによって受信パケットに関する補助データを受け取ることが出来ます。補助データを受け取る場合、recvmsg()の第二引数に指定する構造体msghdr型の2つのメンバに補助データを格納するバッファの情報をセットします。一つはmsg_controlにバッファの先頭アドレスを、もう一つはmsg_controllenにそのバッファの大きさを設定します。以下に部分的なコードの例を示します。
------------------------------------------------------------ struct msghdr msg; struct iovec iov; char buff[1024]; char ctrl[CMSG_SPACE(sizeof(struct timespec))]; struct cmsghdr *cmsg = (struct cmsghdr *)&ctrl; msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = (caddr_t)ctrl; msg.msg_controllen = sizeof ctrl; iov.iov_base = buff; iov.iov_len = sizeof buff; ------------------------------------------------------------ 【図 3-2】msghdrを準備するコード
上の例では、補助データを受け取るバッファはchar型の配列ctrl[]です。ctrl[]の大きさを指定する際にCMSG_SPACEというマクロを用いています。このマクロはsys/socket.hの中にあります。
#define CMSG_SPACE(len) (CMSG_ALIGN(sizeof(struct cmsghdr)) + CMSG_ALIGN(len))
マクロに渡す値lenに構造体cmsghdr型のサイズを加算している点に注目してください。補助データは単独でバッファに格納されるのではなく、CMSGヘッダに続けてバッファに格納されます。
+-------------+----------------+ | CMSG Header | Ancillary Data | +-------------+----------------+ 【図 3-3】CMSGの構造
構造体cmsghdr型の定義はsys/socket.hにあります。
struct cmsghdr { __kernel_size_t cmsg_len; /* data byte count, including hdr */ int cmsg_level; /* originating protocol */ int cmsg_type; /* protocol-secific type */ }
先に紹介したnet/socket.cの中の__sock_recv_timestamp()のコードをもう一度見てみます。現在の時刻を補助データに格納するとき、以下のようにput_cmsg()を呼び出しています。
put_cmsg(msg, SOL_SOCKET, SCM_TIMESTAMPNS, sizeof(ts), &ts);
このときの第2引数、第3引数、そして第4引数が構造体cmsghdr型の各メンバに代入されます。put_cmsg()の定義はnet/core/scm.cの中にあります。以下にその一部を示します。
----------------------------------------------------------- int put_cmsg(struct msghdr *msg, int level, int type, int len, void *data) { struct cmsghdr __user *cm = (struct cmsghdr __user *)msg->msg_control; struct cmsghdr cmhdr; int cmlen = CMSG_LEN(len); ... cmhdr.cmsg_level = level; cmhdr.cmsg_type = type; cmhdr.cmsg_len = cmlen; ... if (copy_to_user(cm, &cmhdr, sizeof cmhdr)) goto out; if (copy_to_user(CMSG_DATA(cm), data, cmlen - sizeof(struct cmsghdr))) goto out; ... } ------------------------------------------------------------ 【図 3-4】put_cmsg()のコードの一部
プログラムの中にあるCMSG_LENもCMSG_DATAもsys/socket.hの中で定義されているマクロです。
最後にアプリケーション・プログラム側の部分的なコードを示します。先に示した【図 3-2】のプログラムコードと合わせて見てください。
------------------------------------------------------------ int recv_len, sockopt_on = 1; struct timespec recv_ts; struct msghdr msg; char ctrl[CSMG_SPACE(sizeof(struct timespec))]; struct cmsghdr *cmsg = (struct cmsghdr *)&ctrl; ... if (setsockopt(r_sd, SOL_SOCKET, SO_TIMESTAMPNS, &sockopt_on, sizeof(int)) == -1) { /* error */ } ... recv_len = recvmsg(r_sd, &msg, 0); if (recv_len == -1) { /* error */ } if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPNS && cmsg->cmsg_len == CMSG_LEN(sizeof(struct timesoec))) { memcpy(&recv_ts, CMSG_DATA(cmsg), sizeof(struct timespec)); } ------------------------------------------------------------ 【図 3-5】CMSGを受け取るコード
残念なことに、LinuxでもFreeBSDでも最後に送信したEthernetフレームの送信時刻を得るアプリケーション・インターフェースはありません。そこで、Ethernetフレームを送信する直前にシステムの現在の時刻を取得し、それを送信時刻とみなします。
ユーザーが作成するアプリケーション・プログラムにおいて、Ethernetフレームを送信する直前とはsendto関数を呼び出す直前のことです。
ユーザーアプリケーションの中で現在の時刻を取得するとき、良く知られているgettimeofday関数の代わりにclock_gettime関数を使います。gettimeofday()は、現在の時刻を引数で指定した構造体timeval型のポインタが指すメモリ領域に保存します。構造体timeval型の領域には現在の時刻がマイクロ秒単位で保存されます。一方clock_gettime()は、現在の時刻を引数で指定した構造体timespec型のポインタが指すメモリ領域に保存します。構造体timespec型の領域には現在の時刻がナノ秒で保存されます。
------------------------------------------------------------ struct timespec send_ts; if (clock_gettime(CLOCK_REALTIME, &send_ts) == -1) { perror("ERROR clock_gettime() in main() "); return EXIT_FAILURE; } ------------------------------------------------------------ 【図 3-6】ユーザーアプリケーションの中で現在時刻を取得するコード
clock_gettime()を使ったプログラムをgccでコンパイルしたとき、以下のようなメッセージが表示されるかもしれません。その場合はgccに-lrtを添えてコンパイルして下さい。
undefined reference to `clock_gettime'
ユーザーアプリケーションの中でsendto関数を呼び出す直前の時刻を送信時間とみなす場合、少し問題があります。時刻を取得したあと、sendto()を呼び出すと、カーネルによってシステム・バッファ領域が確保され、アプリケーション・バッファの送信データがシステム・バッファに写されます。送信データを含んだシステム・バッファはカーネルによってデバイス毎の送信キューの最後尾に加えられ、その後、デバイスドライバによってネットワークカードへデータが渡されます。このため、仮の送信時刻とEthernetフレームを実際に送信した時刻に差が生じます。
パケットの送信時刻をより正確に知るためにはカーネルの一部を変更する必要があります。FreeBSD信奉者の方には申し訳ありませんが、以下、Linuxの場合に限って説明します。
Linuxのカーネルの中で、送信データを含んだシステム・バッファ(sk_buff)をデバイス毎の送信キューに積むのはdev_queue_xmit関数の仕事です。この関数は/usr/src/linux-source-<version>/net/core/dev.cの中に記述されています。dev_queue_xmit()の中に、カーネルの実時間を取得して、それをバッファの中に直接埋め込むコードを加えます。
------------------------------------------------------------ ktime_t kt; if (*((unsigned short *)(skb->data + 12)) == 0x0101) { kt = ktime_get_real(); *((struct timespec *)(skb->data + 14)) = ktime_to_timespec(kt); } ------------------------------------------------------------ 【図 3-7】カーネルの中で現在時刻を取得するコード
dev_queue_xmit関数は送信データをデバイスのキューに積んだ後、dev_hard_start_xmit関数を呼び出します。そして、dev_hard_start_xmit()はデバイスを操作するデバイス・ドライバを呼び出します。デバイス・ドライバはデバイス毎に異なります。例えば、今回使用したRealtek 8169 Gigabit Ethernet NICの場合、データを送信するとき、最終的に、/usr/src/linux-source-<version>/drivers/net/r8169.cの中で定義されているrtl8169_start_xmit関数が呼び出されます。図3-7に示したコードをdev_queue_xmit()ではなく、デバイス・ドライバの中に書き加えるのも選択の一つです。
この項の初めに、ユーザーアプリケーションの中でsendto関数を呼び出す直前の時刻を送信時間とみなす場合、少し問題があると述べましたが、send()の直前で実時間を取得する場合と、dev_queue_xmit関数の中で実時間を取得する場合とを比べると、今回使用したシステムの場合で約2マイクロ秒(2000ナノ秒)の差が生じました。
この章では作成したプログラムを使って、実際にEthernetスイッチの遅延を計測した事例について述べます。なお、無用な誤解を招くことを避ける為にスイッチの製品名を明らかにすることは避けます。今回の実験に使用したスイッチは10/100Mbpsポートを8つ装備した小規模LAN向けの製品ですが、SNMPによる管理やIEEE802.1Dスパニングツリーをサポートし、さらにIEEE802.1Q VLANもサポートする少し高機能なスイッチです。
計測結果を示す前に、先ずホストのハードウェア仕様とOSのバージョンをを明らかにします。
CPU: Intel(R) Celeron(R) 2.66GHz Chipset: Intel(R) 865G + ICH5 FSB: 400MHz Memory: DDR 266MHz 512MB Onboard NIC: MARVELL 8001 NIC1: RealTek 8169S-32 Gigabit Ethernet for PCI (in PCI slot2) NIC2: RealTek 8169S-32 Gigabit Ethernet for PCI (in PCI slot3) NIC3: RealTek 8169S-32 Gigabit Ethernet for PCI (in PCI slot4) OS: Debian 5.0 (lenny) Original Kernel: Linux 2.6.26
これは特別なコンピュータではありません。パソコンを自分で組み立てることが好きな人は、DDR2以前のDDRを使用しているという点だけ見ても、これが個人利用向けの平凡な、しかもちょっと古いコンピュータだということが判るでしょう。
始めに、ホストに追加装備したNICから二つを選び、そのインターフェース間をクロス・ケーブルを用いて直接接続します。デバイスのハードウェアの違いによる影響を避けるため、今回オンボードのNICは使用しません。なお、udevが起動しているシステムではインターフェースの名前が変更されることがあるので、dmesgなどで確認しておく必要があります。
myhost$ dmesg | grep eth eth0: RTL8110s at 0xe080e000, 00:0a:79:98:d7:97, XID 04000000 IRQ 21 eth1: RTL8110s at 0xe0824000, 00:0a:79:98:da:38, XID 04000000 IRQ 22 eth2: RTL8110s at 0xe0882000, 00:0a:79:98:e2:d0, XID 04000000 IRQ 16 【図 4-1】dmesgの表示結果
追加装備したNICはギガビットEthernetに対応しており、さらにオートネゴシエーションをサポートしています。従って単に直接接続するとインターフェースのスピードが1Gbpsになります。検査対象のスイッチング・ハブが100Mbpsの場合は、二つのNICのスピードを100Mbpsに、そしてデュプレックスをfullに設定した後、計測します。NICのスピードとデュプレックスはethtoolコマンドで変更することが出来ます。但し、全てのNICがethtoolに対応している訳ではないので注意してください。
myhost$ ifconfig eth0 down myhost$ ifconfig eth1 down myhost$ sudo ethtool -s eth0 speed 100 duplex full autoneg off myhost$ sudo ethtool -s eth1 speed 100 duplex full autoneg off myhost$ ifconfig eth0 up myhost$ ifconfig eth1 up 【図 4-3】ethtoolの使用例
以下に今回作成したプログラム(以降、計測プログラムという)の実行結果について説明します。計測プログラムは、指定された発信元インターフェースから着信先インターフェースに向けて64バイトのEthernetフレームを送信し、発信時刻と着信時刻の差を計算します。計測プログラムを実際に実行してみると、実行する度に結果にかなりのバラツキ(揺らぎ)が生じました。これは、プロセス管理や仮想メモリ管理に費やされるOSのオーバーヘッドが影響していると考えられます。そこで、for文を使って、Ethernetフレームの送受信と時間差の算出を複数回繰り返し行い、その平均値を算出します。以下に、for文を使って10回送受信したときの結果を示します。
-------------------- Dir. eth0 -> eth1 -------------------- 1 24813 ns 2 22735 ns 3 16439 ns 4 16700 ns 5 16950 ns 6 16644 ns 7 16931 ns 8 16289 ns 9 16465 ns 10 16703 ns -------------------- ns: ナノ秒 【図 4-4】遅延を計測した結果
上に示した10個の計測結果の平均(小数点以下四捨五入)は 18067 nsですが、3番目から10番目までの結果だけに着目すると平均値は 16640 nsです。 このような問題を解決するために、母数(繰り返し回数)を十分に多くし、突発的に発生する大き過ぎる(または小さすぎる)値を無効にして平均値を求めます。
以下に、eth0から試験フレームを送信してeth1で受信した場合と、その逆で、eth1からeth0に試験フレームを伝送した場合の試験結果を示します。どちらの場合も1,000,000回以上、フレームを伝送して平均値を求めています。 また、計測プログラムを実行するとき、実行中、他のプロセスと切り替わることを極力避けるために、プロセスのリアルタイム属性を変更して計測プログラムを実行します。詳しくは、chrtのオンラインマニュアルを参照してください。
--------------------+-------------------- Dir. eth0 -> eth1 | Dir. eth1 -> eth0 --------------------+-------------------- Average 15551 ns | Average 15578 ns --------------------+-------------------- 【図 4-5】eth0とeth1を直接接続した場合の計測結果
次に、eth1とeth2間の実行結果を示します。
--------------------+-------------------- Dir. eth1 -> eth2 | Dir. eth2 -> eth1 --------------------+-------------------- Average 17086 ns | Average 15750 ns --------------------+-------------------- 【図 4-6】eth1とeth2を直接接続した場合の計測結果
この結果を見ると、eth1からeth2へEthernetフレームを送信する実行時間は、eth2からeth1への実行時間より長い時間を要していることがわかります。この差異について述べる前に、eth0とeth2間の実行結果を示します。
--------------------+-------------------- Dir. eth0 -> eth2 | Dir. eth2 -> eth0 --------------------+-------------------- Average 17123 ns | Average 15781 ns --------------------+-------------------- 【図 4-7】eth0とeth2を直接接続した場合の計測結果
計測の結果、eth0とeth2の組み合わせにおいても実行時間に差が生じました。何故このような現象が発生するのでしょうか。
実験の結果明かになった実行時間の差について、その原因が個々のNICにあるのか、それともシステムにあるのかを切り分ける為に、eth1とeth2のNICの装着位置を変えてみます。NICの装着位置を入れ替えて再起動した後のdmesgを以下に示します。
myhost$ dmesg | grep eth eth0: RTL8110s at 0xe080e000, 00:0a:79:98:d7:97, XID 04000000 IRQ 21 eth1: RTL8110s at 0xe0882000, 00:0a:79:98:e2:d0, XID 04000000 IRQ 22 eth2: RTL8110s at 0xe0824000, 00:0a:79:98:da:38, XID 04000000 IRQ 16 udev: renamed network interface eth2 to eth1 udev: renamed network interface eth1_rename to eth2 【図 4-8】NICを交換した後のdmesg
NICの交換前と交換後でどのように環境が変わったのか、重要な部分だけを比較して示します。
[NIC交換前] eth1: 00:0a:79:98:da:38, IRQ 22 eth2: 00:0a:79:98:e2:d0, IRQ 16 [NIC交換後] eth2: 00:0a:79:98:e2:d0, IRQ 22 eth1: 00:0a:79:98:da:38, IRQ 16
Ethernetアドレスと割り込み番号の組み合わせが変わっていることが確認できます。但し、udevの働きによって、Ethernetアドレスに基づいてインターフェース名が変更されている点に注意してください。 もし、実行時間の差が個々のNICに起因しているのなら、NICを交換した後の計測では図4-6の結果と同じ様に、eth1からeth2へのフレーム伝送にかかる時間は、eth2からeth1への伝送にかかる時間よりも長くなるはずです。 NIC交換後の計測結果を以下に示します。
--------------------+-------------------- Dir. eth1 -> eth2 | Dir. eth2 -> eth1 --------------------+-------------------- Average 15814 ns | Average 17152 ns --------------------+-------------------- 【図 4-9】NICを交換した後の計測結果
計測した結果、eth1からeth2へのフレーム伝送にかかる時間は、eth2からeth1への伝送にかかる時間よりも短くなりました。どうやらNIC固有の問題ではないようです。
次に、特定のPCI拡張スロットに問題があるのかと考え、NICの装着位置をいくつか変えて計測しましたが、特定のPCI拡張スロットに限った現象でもありませんでした。 ですが、試験を繰り返す過程で、受信NICのIRQ番号が小さいほど実行時間が長くなる傾向があることがわかりました。
今回使用したホストの場合、増設する3つのNICには、22, 21, 18, 16のいずれかのIRQ番号が割り当てられました。割り当てられるIRQ番号はPCI拡張スロットの位置に固定されていません。例えば、3番目のPCI拡張スロットにNICを装着したからといって、常に同じIRQ番号が割り当てられることはありません。その前後のPCI拡張スロットにNICが装着されているか否かでIRQ番号は変動します。
NICを装着する拡張スロットの位置を変更して何度も繰り返し試験を実行した結果、IRQ番号に22が割り当てられたNICに向けてフレームを送信した場合に実行時間が最も短く、IQR番号に16が割り当てられたNICに向けてフレームを送信した場合に実行時間が最も長くなることがわかりました。
割り込み番号の違いが試験プログラムの実行結果に影響を及ぼすことは確認できましたが、何故このようになるのか、その理由を具体的に知る為にはハードウェアに関する非常に詳しい知識が要求されると思います。これは興味深い話題なのですが、これ以上追求することはこの記事の本題から大きく外れることになりますので、この謎の解明は別の機会にしましょう。
次にEthernetスイッチを経由した場合の実行時間を計測します。以下に構成図を示します。
Host Switch +----------+ +----------+ | PCIslot2 |eth0 port1| 100Mbps | | [IRQ 21] +-------------+ Full-Dup.| | | | | | | | | | | | | | PCIslot3 +-------------+ 100Mbps | | [IRQ 22] |eth1 port3| Full-Dup.| +----------+ +----------+ 【図 4-8】Ethernetスイッチを介した構成
Ethernetスイッチを経由した場合の計測プログラムの実行結果を以下に示します。
--------------------+-------------------- Dir. eth0 -> eth1 | Dir. eth1 -> eth0 --------------------+-------------------- Average 23391 ns | Average 23874 ns --------------------+-------------------- 【図 4-9】eth1とeth2をスイッチ経由で接続した場合の計測結果
次に、Ethernetスイッチを経由した場合の計測結果と直接接続した場合の計測結果からそれぞれ平均値を選び、その差を求めます。
eth0 -> eth1の場合: 23391 - 15551 = 7840 (ns) eth1 -> eth0の場合: 23874 - 15578 = 8296 (ns)
最後にそれぞれの値からLANケーブル1本分の論理的な伝送時間5760ナノ秒を引きます。8バイトのプリアンブルとそれに続く64バイトのEthernetフレームを100Mbpsのレートで送信するためには5.76μ秒必要です。
eth0 -> eth1の場合: 7840 - 5760 = 2080 (ns) eth1 -> eth0の場合: 8296 - 5760 = 2536 (ns)
この結果が、スイッチングによる遅延時間と言えます。
ですが、二つの結果には差があります。同じ計測を何度も繰り返しましたが、結果は常に500?700ns程度の差が生じました。このことを詳しく調べるために、接続を【図 4-10】に示すように変更して、もう一度計測します。
Host Switch +----------+ +----------+ | PCIslot2 |eth0 port1| 100Mbps | | [IRQ 21] +---+ +----+ Full-Dup.| | | | / | | | | +--/--+ | | | | / | | | | PCIslot3 +----+ +---+ 100Mbps | | [IRQ 22] |eth1 port3| Full-Dup.| +----------+ +----------+ 【図 4-10】スイッチの遅延を計測する構成2
計測の結果を以下に示します。
--------------------+-------------------- Dir. eth0 -> eth1 | Dir. eth1 -> eth0 --------------------+-------------------- Average 23866 ns | Average 23344 ns --------------------+-------------------- 【図 4-11】スイッチの接続を変えて計測した結果
この結果から、一番初めに計測した二つのインターフェースをクロス・ケーブルで直接接続したときの計測値を引きます。
eth0 -> eth1の場合: 23866 - 15551 = 8315 (ns) eth1 -> eth0の場合: 23344 - 15578 = 7766 (ns)
最後にそれぞれの値からLANケーブル1本分の論理的な伝送時間5760ナノ秒を引きます。
eth0 -> eth1の場合: 8315 - 5760 = 2555 (ns) eth1 -> eth0の場合: 7766 - 5760 = 2006 (ns)
先に計測した結果と比較しやすいように並べて表示します。
* 構成 1 * 構成2 Host Switch Host Switch +----------+ +----------+ +----------+ +----------+ | PCIslot2 |eth0 port1| 100Mbps | | PCIslot2 |eth0 port1| 100Mbps | | [IRQ 20] +-------------+ Full-Dup.| | [IRQ 20] +---+ +----+ Full-Dup.| | | | | | | | / | | | | | | | | +--/--+ | | | | | | | | / | | | | PCIslot3 +-------------+ 100Mbps | | PCIslot3 +----+ +---+ 100Mbps | | [IRQ 21] |eth1 port3| Full-Dup.| | [IRQ 21] |eth1 port3| Full-Dup.| +----------+ +----------+ +----------+ +----------+ [スイッチの遅延] [スイッチの遅延] eth0 -> eth1の場合: 2080 (ns) eth0 -> eth1の場合: 2555 (ns) eth1 -> eth0の場合: 2536 (ns) eth1 -> eth0の場合: 2006 (ns) 【図 4-12】port1とport3の接続を変更したときの比較
二つの結果を見て言えることは、port1からport3にスイッチする方が、port3からport1へスイッチするより遅延が少ないということです。
同一ポート間でも方向の違いによってスイッチング遅延に違いが生じることについては、様々な組み合わせを計測することで、何らかの傾向を見出せるかもしれません。port1とport3の間のスイッチング遅延を比較したときと同じ手順で、port1とport8の間のスイッチング遅延を比較して見ます。
* 構成 1 * 構成2 Host Switch Host Switch +----------+ +----------+ +----------+ +----------+ | PCIslot2 |eth0 port1| 100Mbps | | PCIslot2 |eth0 port1| 100Mbps | | [IRQ 21] +-------------+ Full-Dup.| | [IRQ 21] +---+ +----+ Full-Dup.| | | | | | | | / | | | | | | | | +--/--+ | | | | | | | | / | | | | PCIslot3 +-------------+ 100Mbps | | PCIslot3 +----+ +---+ 100Mbps | | [IRQ 22] |eth1 port8| Full-Dup.| | [IRQ 22] |eth1 port8| Full-Dup.| +----------+ +----------+ +----------+ +----------+ [スイッチングに掛かる遅延] [スイッチングに掛かる遅延] eth0 -> eth1の場合: 2178 (ns) eth0 -> eth1の場合: 2424 (ns) eth1 -> eth0の場合: 2412 (ns) eth1 -> eth0の場合: 2192 (ns) 【図 4-13】port1とport8の接続を変更したときの比較
この結果から、port1からport8への遅延は、port8からport1への遅延より短いことがわかります。そして、先のport1とport3の結果と合わせて考えると次のような共通点があります。
・ 入力ポートの番号より大きい番号の出力ポートへのスイッチングは比較的早い。 ・ 入力ポートの番号より小きい番号の出力ポートへのスイッチングは比較的遅い。
もう一つ注目すべき点があります。port1からport3への遅延に比べてport1からport8への遅延は少し長くなっています。これに対して、port3からport1への遅延に比べてport8からport1への遅延は逆に少し短くなっています。
このような現象が確認できたのですが、残念ながら、これ以上探求することはできません。通常、メーカーはEthernetスイッチの中で実行されているプログラムのアルゴリズムを公開していません。そのため、今回計測したEthernetスイッチにおいて、なぜ同じポート間でも転送方向によって遅延に差が生じるのか、その理由を知ることはできません。
多くのEthernetスイッチでは、プロトコルの物理層はポート毎に独立しており、複数のポートが同時に(並列に)信号を受信をするとができます。しかし、Ethernetフレームがポートに到着したあと、直ちにスイッチング処理されるかはスイッチ・コントローラと呼ばれる集積回路の実装によります。スイッチ・コントローラは、受信したEthernetフレームをどのポートから出力すれば良いかを決定する重要な部品です。
スイッチ・コントローラが複数のEthernetフレームを同時に(全く平行に)処理できない場合、Ethernetスイッチに届いた複数のフレームはスケジューリングされて、一つずつ処理されることになります。
複数の入出力を制御する方法として、よく知られた方法が二つあります。一つは、インターフェースにデータが到着したときに、処理を開始する要求(通常、割り込みという)を生成し、早く到着したデータから先に処理する方法です。もう一つは、すべてのインターフェースを周期的に巡回し、そのインターフェースで処理すべきデータがあれば処理を実行する方法です。後者の制御方法は、ポーリング(Polling)またはラウンドロビン(Round-robin)方式と呼ばれています。
スイッチ・コントローラがポートを一つずつ巡回して処理を進める場合、今回の計測結果のような特徴が現れることがあります。
各ポートの処理は概ね次のようになります。
a. そのポートから送信すべきフレームがあれば、そのフレームを出力する。 b. そのポートにフレームが到着していれば、以下の作業を行う。 b-1. 宛先Ethernetアドレスを検出する。 b-2. アドレス・テーブルを検索し、出力ポートを決定する。 b-3. 出力ポートの順番が回って来たときにフレームが出力されるようスケジュールする。 c. 発信元Ethernetアドレスがアドレス・テーブルに無いなら、そのアドレスを登録する。
Ethernetスイッチのport1にport3から出力されるべきEthernetフレームが到着したとき、転送先ポートの判定のあと直ちにport3からEthernetフレームが送出される訳ではなく、port2の送受信処理を行った後、port3の順番が回って来たときにEthernetフレームが送出されます。逆に、port3にport1宛てのEthernetフレームが到着したときは、転送先ポートの判定後、port4、port5と処理を進めて行き、port8の処理後、再びport1の順番が回って来たときにEthernetフレームが送出されます。この結果、port3からport1への遅延は、port1からport3への遅延より長くなります。
port1からport3への転送とport1からport8への転送を比較すると、port1からport8へ転送する方が遅延は長くなります。これは、port1からport3の場合、port1の処理のあとport3の順番が回って来るまで他のportの処理が1つだけなのに対して、port1からport8の場合はport1の処理のあとport8の順番が回って来るまで6つのポートの処理を行わなければならないからです。一方、port3からport1への転送とport8からport1への転送を比較すると、port8からport1へ転送する方が遅延は短くなります。これは、port3からport1へ転送する場合、port3の処理のあとport4、port5と順番に進み、port1の順番が回って来るまで5つのポートの処理を行わなければならないのに対して、port8からport1の場合はport8の終了後すぐにport1に順番が回ってくるからです。
8ポートのEthernetスイッチの場合、port1とport5の組み合わせでは、二つのポートの距離は昇順と降順の両方で同じになります。port1からport5への転送するときに間にあるポートは3つで、port5からport1へ転送するときに間にあるポートも3つです。このように同じ距離のポート間で遅延に差が生じる場合は、1周の始まりに初期化ルーチンがあるか、1周の最後に後片付けルーチンがあると考えられます。例えば、一定時間参照されなかったエントリーをアドレス・テーブルから削除する処理などです。
さて、ここまで64バイト長のEthernetフレームを用いてEthernetスイッチの遅延を求めてきましたが、Ethernetフレームの長さを変えるとEthernetスイッチの遅延に変化が生じるのか調べてみます。
以下に示す結果はEthernetフレームの長さを1518バイトにしたときの実行結果です。
先ずは、eth0とeth1をクロス・ケーブルで直結した場合の実行結果を示します。
Host --------------------+-------------------- +----------+ Dir. eth0 -> eth1 | Dir. eth1 -> eth0 | PCIslot2 |eth0 --------------------+-------------------- | [IRQ 21] +--------+ Average 153676 ns | Average 153736 ns | | | --------------------+-------------------- | | | | | | | PCIslot3 +--------+ | [IRQ 22] |eth1 +----------+ 【図 4-14】1518バイト長のEthernetフレームを送信したときの結果
次は、Ethernetスイッチを接続した場合の実行結果です。
Host Switch --------------------+-------------------- +----------+ +----------+ Dir. eth0 -> eth1 | Dir. eth1 -> eth0 | PCIslot2 |eth0 port1| 100Mbps | --------------------+-------------------- | [IRQ 21] +-------------+ Full-Dup.| Average 277722 ns | Average 278433 ns | | | | --------------------+-------------------- | | | | | | | | | PCIslot3 +-------------+ 100Mbps | | [IRQ 22] |eth1 port3| Full-Dup.| +----------+ +----------+ 【図 4-15】1518バイトのEthernetフレームをスイッチングしたときの結果
この結果から、先に計測した二つのインターフェースをクロス・ケーブルで直接接続したときの計測値を引きます。
eth0 -> eth1の場合: 277722 - 153676 = 124046 (ns) eth1 -> eth0の場合: 278433 - 153736 = 124697 (ns)
最後にそれぞれの値からケーブル1本分の論理的な伝送遅延時間122080ナノ秒を引きます。
eth0 -> eth1の場合: 124046 - 122080 = 1966 (ns) eth1 -> eth0の場合: 124697 - 122080 = 2617 (ns)
この結果を64バイトのときの結果と比較します。
* 64バイトフレームの転送 * 1518バイトフレームの転送 eth0 -> eth1の場合: 2080 (ns) eth0 -> eth1の場合: 1966 (ns) eth1 -> eth0の場合: 2536 (ns) eth1 -> eth0の場合: 2617 (ns)
eth0からeth1へ、そしてeth1からeth0へ、どちらの方向についても約80nsの差が見られますが、1518バイトのフレームの伝送時間に比べるとかなり小さ値であり、フレームの大きさがスイッチングの遅延に大きな影響を与えることはないと言えます。
但し、これはスタンドアロン型の8ポートスイッチングハブの結果です。このスイッチにはスイッチング・コントローラと共有メモリが一つづつあるだけです。中規模以上のネットワーク向けのスイッチ製品には部品化されたインターフェース・モジュールを本体のスロットに装着して使用するような機器があります。このようなスイッチではバックプレーンを通じてモジュール間のメモリー転送が発生します。その場合、フレームの大きさがスイッチングの遅延に影響を及ぼす可能性があります。
この後、ポートの組み合わせを何通りにも変えて試験をした結果、今回使用したEthernetスイッチでは以下のような特徴が見られました。
・ 入力ポートのポート番号より2つ以上大きい番号の出力ポートに転送するとき、2.0 ? 2.2μ秒の遅延が発生する。 ・ 入力ポートのポート番号より小さい番号の出力ポートに転送するとき、2.4 ? 2.6μ秒の遅延が発生する。 ・ 入力ポートのポート番号より1つ大きい番号の出力ポートに転送するとき、2.6 ? 2.8μ秒の遅延が発生する。 但し、port4からport5への転送は除く。
3つ目の特徴は謎です。なぜこのような結果になるのか不明ですが、この特徴はハードウェアの構造によるものかもしれません。この特徴ではport4からport5への転送が例外になっています。Ethernetスイッチの基盤を見るとEthernetのTransformer(伝送装置)が二つあることがわかりました。例外的なポートの組み合わせ(port4とport5)の場合は伝送装置が異なるのに対して、他のポートの組み合わせの場合は同じ伝送装置を使うことになります。以下に今回使用したEthernetスイッチの内部構造のブロック図を示します。
+-----------------+ +---------------+ (Flash EEPROM 1) | Intel | | Advanced | | TE28F800 B3BA90 | | Comm. | (Switch controler) +-----------------+ | Devices | +-----------------+ | ACD83524 | (Flash EEPROM 2) | Intel | +---------------+ | TE28F800 B3BA90 | |||| |||| +-----------------+ +---------------+ +-----------------+ | MARVEL | (SDRAM) | Winbond | | | | 250WG | | 88E3081-RAF | +-----------------+ +---------------+ |||| |||| +-----------------+ +-----------------+ (Quad Trancformer) | YCL | | YCL | | PH406466 | | PH406466 | +-----------------+ +-----------------+ (port #1 to #4) (port #5 to #8) 【図 4-16】Ethernetスイッチの内部ブロック
最後に実行結果の有効性と性能評価を行うときの注意点について述べます。
今回主に使用したEthernetスイッチの他に、機能および価格が同程度の他社製Ethernetスイッチがあり、その箱の裏面に遅延は64バイト長のEthernetフレームのときで1.5μ秒(1500ナノ秒)と明記されていました。このEthernetスイッチの遅延を今回作成したプログラムを使って求めたところ、小さいポート番号への転送時は平均1.519μ秒、大きいポート番号への転送時は平均約1.640μ秒という結果が得られました。メーカーが公表した値と完全に一致しなかったことは残念ですが、この結果から、今回の計測方法を用いてメーカーが公表する値に近い値を得られることがわかりました。
最新のEthernetスイッチを見ると、カタログの製品仕様に遅延は64バイト長Ethernetフレームの場合で6μ秒と明記されている製品がありました。この値は今回使用したEthernetスイッチの遅延よりも大きな値ですが、この値だけをもってスイッチング性能が悪いと評価するは避けるべきです。最近のEthernetスイッチは、ポート密度が高く、また優先制御(QoS)などの複雑な制御を実行しており、遅延が大きくなることは必然であると考えます。ある製品のスイッチング性能の評価を行うときは、同じ数のポートを装備し、同じ機能をサポートした他の製品と比較して判断するべきです。
本記事は、特定のハードウェアおよびソフトウェアの性能を保証するものではありません。
著作権者の文書による承諾を得ずに、本記事の一部または全部を無断で複写・複製・転載することはできません。
著者 外薗智幸