かーねるさんとか

発言は個人の見解であり、所属する組織の公式見解ではありません。

高速パケット I/O フレームワークの NIC の I/O について

netmap のようなパケット I/O フレームワークが、どのように NIC からパケット転送を行っているのかについてまとめました。

以前のエントリー*1で、Intel NIC でパケットを転送するために、デバイスドライバがどのような処理をしているのかを見ました。

今回は、netmap が、それらのデバイスドライバをハックして、データ転送部分だけを切り出して使っている実装について見ていきます。

netmap のパケット転送

netmap を使ってパケットを送信する際には、以下の図のように、大きく3つのステップで行います。過去のエントリーにも説明がありますので、そちらも参考にしてください。

  1. アプリケーションがデータをパケットバッファに書き込む
  2. アプリケーションがカーネルにリクエストを送る
  3. カーネルNIC にパケット転送命令を送る

https://raw.githubusercontent.com/yasukata/asset/master/img/netmap_structure_201711/netmap_structure.png

以前のエントリー*2は、最初の2つの項目について説明しています。今回は3つ目についての説明で、カーネルデバイスドライバを経由して、NIC に転送のリクエストを送る部分を見ます。

netmap の NIC の転送部分の実装

下記の実装は、e1000e ドライバを利用する NIC 用で、netmap/LINUX/if_e1000e_netmap.h にあります。これと同じ類の実装が、それぞれの NIC ごとにあります。

以下の関数 e1000_netmap_txsync へは、図の赤い矢印(Request I/O)の先で、たどり着きます。

過去のエントリーでは、netmap_ring、netmap_slot 等の netmap のデータ構造について*3と、NIC のデスクリプタリングや、E1000_TX_DESC 等のマクロについて*4書いてありますので、参考にしてください。

/*
 * Reconcile kernel and user view of the transmit ring.
 */
static int
e1000_netmap_txsync(struct netmap_kring *kring, int flags)
{
	...
	struct netmap_ring *ring = kring->ring;
	...
	/* device-specific */
	// SOFTC_T は e1000_adapter のマクロ
	struct SOFTC_T *adapter = netdev_priv(ifp);
	// e1000_ring : 転送用デスクリプタリングの構造体
	struct e1000_ring* txr = &adapter->tx_ring[ring_nr];
	...
        if (nm_i != head) {     /* we have new packets to send */
                nic_i = netmap_idx_k2n(kring, nm_i);
                for (n = 0; nm_i != head; n++) {
			// パケットバッファのアドレスと、データ長の情報を持つスロットを取得
			struct netmap_slot *slot = &ring->slot[nm_i];
			u_int len = slot->len;
			uint64_t paddr;
			// paddr に netmap_slot が参照するパケットバッファの物理アドレスが格納される
			void *addr = PNMB(na, slot, &paddr);

			/* device-specific */
			// 転送用デスクリプタを E1000_TX_DESC マクロで取得
			struct e1000_tx_desc *curr = E1000_TX_DESC(*txr, nic_i);
			int flags = (slot->flags & NS_REPORT ||
				nic_i == 0 || nic_i == report_frequency) ?
				E1000_TXD_CMD_RS : 0;

			NM_CHECK_ADDR_LEN(na, addr, len);

			/* 既に、この段階ではデスクリプタの Buffer Address フィールドは、
			 パケットバッファを参照していますが、
			 slot->flags に NS_BUF_CHANGED というフラグを立てると、
			 デスクリプタが参照しているパケットバッファを変更することができます。*/
			if (slot->flags & NS_BUF_CHANGED) {
				...
				// デスクリプタの Buffer Address フィールドが参照しているアドレスを変更
				curr->buffer_addr = htole64(paddr);
			}
			...
			/* Fill the slot in the NIC ring. */
			curr->upper.data = 0;
			// デスクリプタの Length フィールドに送信するデータの長さを設定
			curr->lower.data = htole32(adapter->txd_cmd | len | flags |
				E1000_TXD_CMD_EOP);
			nm_i = nm_next(nm_i, lim);
			nic_i = nm_next(nic_i, lim);
			// 次の netmap_slot の保持する値を、次の転送デスクリプタに反映する
			/* アプリケーションが用意したデータ全てに処理が完了する、もしくは
			 転送デスクリプタを全て使い切るまで続ける */
		}
		...
		/* NIC に転送用データの準備が完了したことを伝える。
		 TDT レジスタの値を変更し、データ転送をリクエストする。*/
		NM_WR_TX_TAIL(nic_i);
		...
	}
	...
	return 0;
}

NIC からデータを送信するためには、デスクリプタのフィールドにパケットデータのアドレスとパケット長を指定し、TDT レジスタの値を更新することが必要で、上の e1000_netmap_txsync 関数では、まさにそれを行っています。

以下が、上記のプログラムの中で、パケットデータアドレスを設定する箇所です。

				curr->buffer_addr = htole64(paddr);

以下が、データの長さを設定するところです。

			curr->lower.data = htole32(adapter->txd_cmd | len | flags |
				E1000_TXD_CMD_EOP);

NM_WR_TX_TAIL は以下のようになっていて、TDT レジスタの値を更新をします。

#define NM_WR_TX_TAIL(_x)	writel(_x, txr->tail)

NIC からデータを発信するために重要なのは、以上の3点です。

これらを踏まえて、netmap と、NIC のデータ構造の結びつきを図示すると以下のようになります。

https://raw.githubusercontent.com/yasukata/asset/master/img/e1000_ring_20171127/netmap_nic.png

netmap_slot と、NIC の Transmit Descriptor は、どちらも同じくパケットバッファの位置( netmap_slot : buf_idx, Transmit Descriptor : Buffer Address )と、パケットデータの長さを保持するフィールド( netmap_slot : len, Transmit Descriptor : Length )を持っており、それらはリング状にして、netmap_ring と Transmit Descriptor Ring という構造で管理されています。

まとめ

  • NIC は転送デスクリプタのフィールドにパケットデータのアドレスとパケット長を指定し、TDT レジスタの値を更新するとデータを送信してくれる
  • netmap は netmap_slot に保持したパケットデータのアドレスと、パケット長を転送デスクリプタの各フィールドに設定することで、アプリケーションが用意したデータを送信する