かーねるさんとか

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

Intel NIC ドライバにおけるパケット送信について

LinuxNICデバイスドライバが、どのようにデータを送信するのかについてまとめました。特に Intel NIC のドライバについて見ていきます。

Intel の 1Gb NIC のデータシートが次の URL *1 で見つかりますので、それも参考にしながら見ていただければと思います。。

ソースコード

Intel NICデバイスドライバのコードは、Linuxソースコード内で、以下のディレクトリに配置されています。

今回は、e1000e を例に見ていきますが、重要なデータ構造は ixgbe, i40e にも共通しており、それぞれについても概ね同じ処理を行うことでデータの送信ができます。

NIC のデータ構造

データの送受信を行う場合の重要な構造体として、デスクリプタのリングがあります。これはハードウェアの仕様によるものです。デバイスドライバは、このデスクリプタリングを適切に扱うことで、データの送受信を行います。

データシートの 7.2.4 章に Transmit Descriptor Ring Structure というタイトルで転送用デスクリプタリングの構造の説明があります。また、転送用デスクリプタの各フィールドの説明が、データシートの 7.2.10 章に見つかります。

以下の図に、それらの情報をまとめます。

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

データシートによると、転送用デスクリプタリングは、4種類のレジスタによって表現されるとあります。

  • Transmit Descriptor Base Address register ( TDBA ) : ホストメモリ上での、デスクリプタリングの開始位置を指定する。
  • Transmit Descriptor Length register ( TDLEN ) : リングのために確保されているメモリの長さを指定する。
  • Transmit Descriptor Head register ( TDH ) : NIC によって処理されるべきデスクリプタの開始位置を示す。
  • Transmit Descriptor Tail register ( TDT ) : NIC が処理すべき最後のデスクリプタの位置を示す。

デスクリプタリングは NICレジスタによって表現されますが、デバイスドライバからは、直線的なメモリ領域の上にデスクリプタの構造体の配列が配置されているように見え、配列のインデックスをもとに各デスクリプタのフィールドへアクセスします。

NIC レジスタへのアクセス方法

デスクリプタリングを準備する際には、NICレジスタ、(TDBA, TDLEN, TDH, TDT)へ適切に値を入れていく必要があります。これらのレジスタは、デバイスドライバの初期化の段階でホストメモリにマップされており、データシートに記載されているオフセットを頼りにアクセスします。

データシートの 10.2.1 章 Register Summary Table の Table 83 には、NICレジスタのリストが掲載されており、ホストメモリにマップされた先頭のアドレスから、各レジスタへのオフセットが記載されています。

このテーブルによると、転送に関連する4種類のレジスタ TDBA、TDLEN、TDH、TDT は次のオフセットでアクセスできるようです。

  • TDBAL ( Transmit Descriptor Base Address Low ) : 0x03800
  • TDBAH ( Transmit Descriptor Base Address High ) : 0x03804
  • TDLEN ( Transmit Descriptor Length ) : 0x03808
  • TDH ( Transmit Descriptor Head ) : 0x03810
  • TDT ( Transmit Descriptor Tail ) : 0x03818

次にデバイスドライバのコードの e1000e/reg.h を見てみます。

#define E1000_TDBAL(_n) ((_n) < 4 ? (0x03800 + ((_n) * 0x100)) : \
                         (0x0E000 + ((_n) * 0x40)))
#define E1000_TDBAH(_n) ((_n) < 4 ? (0x03804 + ((_n) * 0x100)) : \
                         (0x0E004 + ((_n) * 0x40)))
#define E1000_TDLEN(_n) ((_n) < 4 ? (0x03808 + ((_n) * 0x100)) : \
                         (0x0E008 + ((_n) * 0x40)))
#define E1000_TDH(_n)   ((_n) < 4 ? (0x03810 + ((_n) * 0x100)) : \
                         (0x0E010 + ((_n) * 0x40)))
#define E1000_TDT(_n)   ((_n) < 4 ? (0x03818 + ((_n) * 0x100)) : \
                         (0x0E018 + ((_n) * 0x40)))

?マークは三項演算子と呼ばれるもので、" 条件 ? True の場合 : False の場合 "のような形式でプログラムを書くことができます。

この場合では、_n が 4 より小さければ、: の左側の値、_n が 4 以上なら、: の右の値が適用されます。e1000e のドライバでは常に _n に0を入れて利用されているので、常に左の値が適用されます。

プログラム内で宣言されている値 0x038XX が、データシートに掲載されているオフセットの値と対応していることがわかります。

レジスタへの値の書き込みは、以下のような関数で行われます。e1000_hw->hw_addr には、ホストメモリにマップされているレジスタの先頭のアドレスが、ドライバ初期化の際に代入されており、それに対して各レジスタのオフセットを足したアドレスへ値を書き込んでいきます。

/* e1000e/netdev.c */
void __ew32(struct e1000_hw *hw, unsigned long reg, u32 val)
{
        ...
        writel(val, hw->hw_addr + reg);
}
/* e1000e/e1000.h */
#define ew32(reg, val)  __ew32(hw, E1000_##reg, (val))

例えば、上記のマクロを使って以下のようにプログラムを書くと、TDH レジスタに 0 が書き込まれます。このときに、E1000_##reg は E1000_TDH(0) に展開され 0x03810 がオフセットの値として、__ew32 関数の unsigned long reg の引数として渡されます。

        ew32(TDH(0), 0);

転送用デスクリプタリングの初期化は、e1000e/netdev.c では、上記のマクロを使って、e1000_configure_tx 関数に以下のように実装されています。

/**
 * e1000_configure_tx - Configure Transmit Unit after Reset
 * @adapter: board private structure
 *
 * Configure the Tx unit of the MAC after a reset.
 **/
static void e1000_configure_tx(struct e1000_adapter *adapter)
{
        struct e1000_hw *hw = &adapter->hw;
        struct e1000_ring *tx_ring = adapter->tx_ring;
        u64 tdba;
        u32 tdlen, tctl, tarc;

        /* Setup the HW Tx Head and Tail descriptor pointers */
        tdba = tx_ring->dma;
        tdlen = tx_ring->count * sizeof(struct e1000_tx_desc);
        ew32(TDBAL(0), (tdba & DMA_BIT_MASK(32)));
        ew32(TDBAH(0), (tdba >> 32));
        ew32(TDLEN(0), tdlen);
        ew32(TDH(0), 0);
        ew32(TDT(0), 0);
        tx_ring->head = adapter->hw.hw_addr + E1000_TDH(0);
        tx_ring->tail = adapter->hw.hw_addr + E1000_TDT(0);
        ...

上記のプログラムでは、TDBA (デスクリプタリング開始位置指定用レジスタ)には、tx_ring->dma の値が書き込まれています。tx_ring->dma は、以下の関数、e1000_alloc_ring_dma でデスクリプタリング用に確保したメモリ領域の先頭アドレスが格納されています。

また、tx_ring->dma に格納される値は物理メモリアドレスで、tx_ring->desc には同じ領域を参照する仮想メモリアドレスが格納されます。NICレジスタへアドレスを指定する際は、物理メモリアドレス(tx_ring->dma)で指定し、デバイスドライバカーネルモジュールのプログラムからデスクリプタを参照する場合には、仮想メモリアドレス(tx_ring->desc)をもとに参照します。

/**
 * e1000_alloc_ring_dma - allocate memory for a ring structure
 **/
static int e1000_alloc_ring_dma(struct e1000_adapter *adapter,
                                struct e1000_ring *ring)
{
        ...
        ring->desc =
            dma_alloc_coherent(pci_dev_to_dev(pdev), ring->size, &ring->dma,
                               GFP_KERNEL);

デスクリプタのフィールド

データシートの転送用デスクリプタのフィールドの説明を見ていきます。データの送受信について特に重要なのが以下の2項目です。

  • 7.2.10.1.1 章 Buffer Address : Buffer Address は、メインメモリ上の転送すべきデータの位置(アドレス)を指定します。
  • 7.2.10.1.2 章 Length : Length は、Buffer Address で指定したアドレスから転送されるべきデータの長さをバイト単位で指定します。

Buffer Address のフィールドに転送したいデータのメインメモリ上のアドレス、Length に転送したいデータの長さを指定します。

デスクリプタへのアクセス

Intel の 1Gb NIC ドライバ e1000e では、転送デスクリプタを e1000e/hw.h 内部で以下のような構造体として定義しています。

/* Transmit Descriptor */
struct e1000_tx_desc {
        __le64 buffer_addr;     /* Address of the descriptor's data buffer */
        union {
                __le32 data;
                struct {
                        __le16 length;  /* Data buffer length */
                        u8 cso; /* Checksum offset */
                        u8 cmd; /* Descriptor control */
                } flags;
        } lower;
        union {
                __le32 data;
                struct {
                        u8 status;      /* Descriptor status */
                        u8 css; /* Checksum start */
                        __le16 special;
                } fields;
        } upper;
};

ある転送デスクリプタリングの、i 番目の転送デスクリプタへアクセスするためには、e1000.h で定義されている、以下の E1000_TX_DESC マクロを利用します。ここで、R には struct e1000_ring が入り、E1000_GET_DESC は (&(((struct e1000_tx_desc *)(tx_ring.desc))[i])) のように展開されます。

前述の通り、tx_ring.desc には、転送用デスクリプタリングの開始位置の仮想メモリアドレスが入っており、このマクロでは i 番目の struct e1000_tx_desc の配列のオブジェクトへのアドレスが得られます。

#define E1000_GET_DESC(R, i, type)      (&(((struct type *)((R).desc))[i]))
#define E1000_TX_DESC(R, i)             E1000_GET_DESC(R, i, e1000_tx_desc)

パケットの転送

1. デスクリプタのフィールドへ、Buffer Address と Length を指定する

e1000e では、以下の関数 e1000_tx_queue に実装されています。

static void e1000_tx_queue(struct e1000_ring *tx_ring, int tx_flags, int count)
{
        ...
        do {
                buffer_info = &tx_ring->buffer_info[i];
                tx_desc = E1000_TX_DESC(*tx_ring, i); // マクロを利用して i 番目のデスクリプタへの参照を得る
                tx_desc->buffer_addr = cpu_to_le64(buffer_info->dma); // Buffer Address フィールドへパケットのデータのアドレスを書き込む
                tx_desc->lower.data = cpu_to_le32(txd_lower | // Length フィールドへパケットの長さを書き込む
                                                  buffer_info->length);
                tx_desc->upper.data = cpu_to_le32(txd_upper);

                i++;
                if (i == tx_ring->count)
                        i = 0;
        } while (--count > 0);

上記の e1000_tx_queue 関数で、Buffer Address に指定している値、buffer_info->dma には、上記の処理にたどり着く前に、以下の関数 e1000_tx_map の中で、パケットデータの仮想アドレス(skb->data + offset)が参照する物理メモリアドレスが格納されています。

static int e1000_tx_map(struct e1000_ring *tx_ring, struct sk_buff *skb,
                        unsigned int first, unsigned int max_per_txd,
                        unsigned int nr_frags)
{
                ...
                buffer_info->length = size;
                ...
                buffer_info->dma = dma_map_single(pci_dev_to_dev(pdev),
                                                  skb->data + offset,
                                                  size, DMA_TO_DEVICE);
2. TDT レジスタNIC が処理すべき最後のデスクリプタを示す)の値を更新して、NIC に転送すべきパケットが用意されたことを伝える

先ほどと同じ関数 e1000_tx_map の中で、以下のようにして実装されています。

static int e1000_tx_map(struct e1000_ring *tx_ring, struct sk_buff *skb,
                        unsigned int first, unsigned int max_per_txd,
                        unsigned int nr_frags)
{
                ...
                writel(i, tx_ring->tail);

i には、転送されるべきデータを参照する最後尾の転送デスクリプタのインデックスが代入されています。これにより、NIC は、TDH から、新しく i が設定された TDT までのインデックスをもつ転送デスクリプタが Buffer Address のフィールドで参照しているパケットのデータを転送します。

tx_ring->tail は、転送用デスクリプタリングを設定する関数 e1000_configure_tx の中で、以下のようにして、NIC の TDT レジスタがマップされたアドレスが代入されています。

static void e1000_configure_tx(struct e1000_adapter *adapter)
{
        ...
        tx_ring->tail = adapter->hw.hw_addr + E1000_TDT(0);

まとめ

  1. デバイスドライバはデスクリプタリングを使ってパケットを送信する
  2. デスクリプタリングは、NICレジスタ4種類を設定することで、デバイスドライバからホストメモリ経由でアクセスできる
  3. デスクリプタのフィールドにパケットデータのアドレスとパケット長を指定し、TDT レジスタを更新することで NIC にパケット転送をリクエストできる