情報畑でつかまえてロゴ
本サイトは NTTテクノクロスが旬の IT をキーワードに
IT 部門が今知っておきたい最新テクノロジーに関する情報をお届けするサイトです

DPDKアプリケーションの作成 初期化処理編 ~DPDK入門 第7回~

これまではDPDKのパケット処理を体験したり、DPDKの設定方法を解説するというDPDKを利用する側の連載でした。 今回からDPDKのアプリケーションの作成方法を解説します。

はじめに

こんにちは、NTTテクノクロス株式会社の寺尾です。
今回からはDPDKのアプリケーションの作成方法を解説していきます。よろしくお願いします。

イメージ画像

ポートについて

DPDKを利用したアプリケーションを開発する前に、「ポート」について解説します。通常「ポート」はケーブルや機器の差し込み口を指す場合が多いと思いますが、DPDKではパケットを送受信するNICのことを「ポート」と呼び、番号で表現しています。例えば、DPDKのパケット処理 ~DPDK入門 第2回~ではL2 Forwarding Sampleの起動時に「-pでDPDKドライバで割り当てたNICの0番及び1番を指定しています。」と説明していました。この番号をDPDKではポートと表現しています。

$ sudo ./build/l2fwd -c 0x3 -- -p 0x3

パケット処理を作成する場合はパケットを送受信するNICに対応するポート番号を指定します。NIC Aが受信したパケットを取得したい場合は、ポート番号:0を、NIC Bからパケットを送信したい場合はポート番号:1を指定してパケット処理用のインタフェースを利用するイメージです。

ポート番号は、初期化時にハードウェアのPCIアドレスが若い順に0番から割り振られます。例えばPCIアドレスが0000:04:00.0と0000:04:00.1のNICにDPDK用のドライバが割り当たっているなら、0000:04:00.0はポート番号:0、0000:04:00.1はポート番号:1となります。

DPDKアプリケーションの作成

パケット処理を行うアプリケーションの作成を題材にプログラム方法を解説します。今回作成するアプリケーションはDPDKとは? ~DPDK入門 第1回~で利用した「Hello World」のサンプルアプリケーションを参考に、"受信したパケットを折り返す"ことを行う簡単な機能を作ります。また、アプリケーションで利用するNICは、必ず0番のNIC(ポート番号:0のNIC)を扱うこととします。以下に作成するアプリケーションの動作イメージを示します。

なお、今回作成するDPDKアプリケーションは以下のソフトウェア条件で製造・動作確認しています。
- OS:Ubuntu 18.04.2 LTS
- Kernel:4.4.0-138-generic
- DPDK:18.11.1 (LTS)

「Hello World」のサンプルアプリケーションはDPDKが認識しているCPUのコア番号を表示するだけもので、パケットを処理する機能はありませんでした。パケット処理を実現するには、リソース設定(NIC) ~DPDK入門 第6回~で紹介したように、DPDKのNICを設定するとともに、プログラムではDPDK特有の初期化処理が必要になります。初期化処理の概要を図で記載します。

これから実際にソースコード例とともに初期化処理としてどのような処理が必要か、どのDPDKのAPIを利用するのかを紹介します。初期化処理では大まかに「EALの初期化」「パケットバッファ確保」「NIC初期化」の3つを以下に示す順番で行います。

今回はポート番号0のNICのみ利用するので、NIC初期化ではポート0を指定します。

EALの初期化

まずrte_eal_init()でEALを初期化します。EALについては、リソース設定(CPU) ~DPDK入門 第3回~の「EALとは」で解説していますので、参照ください。EALの初期化に失敗した場合、rte_exit()でプログラム異常をユーザに通知して終了します。

また、rte_eth_dev_count_avail()でDPDKアプリケーションが利用可能なポート数を取得することで、"利用可能なNICがあるか"をチェックしています。通常、DPDKのドライバでバインドされたNICなどがカウント対象となります。rte_eth_dev_count_avail()の戻り値が0(利用可能なNICがない)だとDPDKアプリケーションは動作できないため、プログラムを終了させます。

▼EALの初期化とポートチェック
int ret = rte_eal_init(argc, argv);
if (ret < 0)
  rte_exit(EXIT_FAILURE, "Cannot init EAL \n");
if (rte_eth_dev_count_avail() <= 0)
  rte_exit(EXIT_FAILURE, "Cannot avail device \n");

【APIリファレンス】
rte_eal_init
引数 in/out パラメータ 補足
int input 起動パラメータ数 -
char** input 起動パラメータが格納されているバッファへのポインタ -
戻り値 説明
int 0以上 DPDKが読み込んだパラメータ数
負値 異常終了
ヘッダファイル rte_eal.h

rte_exit
引数 in/out パラメータ 補足
int input プログラム起動元に返す終了コード -
const char * input プログラム終了時に表示するエラーメッセージ printf()の書式で指定
戻り値 説明
void - -
ヘッダファイル rte_common.h

rte_eth_dev_count_avail
引数 なし
戻り値 説明
uint16_t 整数値 DPDKアプリケーションで利用可能なポート数
ヘッダファイル rte_ethdev.h

パケットバッファ確保

次にパケットを処理するためのバッファを作成します。 バッファの作成には、mbuf(※1)と呼ばれるパケットバッファライブラリ、mempool(※2)というメモリプールライブラリを利用し、rte_pktmbuf_pool_create()を使いメモリプールを作成します。mbufやmempoolに関して、詳しく知りたい人は公式ページの情報を参照下さい。

▼パケットバッファ確保
struct rte_mempool *pktmbuf_pool = rte_pktmbuf_pool_create(
  "mbuf_pool", 8192U, 256, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
if (pktmbuf_pool == NULL)
  rte_exit(EXIT_FAILURE, "Cannot create mbuf pool \n");
この関数のパラメータに関して解説します。
- 第2パラメータは、メモリプールで確保するmbufのパケットバッファ数を指定します。大規模なアプリケーションになればなるほど大きな数を確保するケースが出てくると思いますが、今回のサンプルでは"8192"としています。最大8192のパケットが確保できることを意味します。
- 第3パラメータは、メモリプールではアクセス頻度が高い情報をキャッシュして保持する仕組みがあり、このパラメータではそのキャッシュのサイズを指定することができます。今回は"256"を指定しています。
- 第5パラメータは、パケットバッファのサイズは今回はDPDKが用意している定数RTE_MBUF_DEFAULT_BUF_SIZE (2048+128byte)を利用します。通常のパケットサイズは大きくても1500byte程度のため十分な確保量ですが、利用するネットワークの状況に応じて修正下さい。
- 第6パラメータは、rte_socket_id()を使いローカルメモリ(※3)からリソースを確保しています。

※1 Mbuf Library
https://doc.dpdk.org/guides/prog_guide/mbuf_lib.html

※2 Mempool Library
https://doc.dpdk.org/guides/prog_guide/mempool_lib.htm

※3 パフォーマンスに関して
ローカルメモリからメモリ確保することでパフォーマンスが良くなります。詳細は、
リソース設定(メモリ) ~DPDK入門 第5回~の「NUMAとは」を参照下さい。

【APIリファレンス】
rte_pktmbuf_pool_create
引数 in/out パラメータ 補足
const char* input mbuf名 マシン内でユニークである必要がある
unsigned input 確保するmbuf数 -
unsigned input mbufで使用する内部キャッシュサイズ -
uint16_t input プライベート領域サイズ mbuf管理用の構造体とパケットバッファ自体の間で確保しておくメモリ領域のサイズ(アプリケーション用の専用エリアとして利用可能)
uint16_t input データ領域サイズ パケットバッファのサイズ
int input ソケットID メモリ確保元のソケットを指定
戻り値 説明
struct rte_mempool* NULL以外 確保したmbufへのポインタ
NULL mbufの確保に失敗
ヘッダファイル rte_mbuf.h


rte_socket_id
引数 なし
戻り値 説明
unsigned 整数値 プログラムを実行している論理プロセッサが属するCPUソケット
ヘッダファイル rte_lcore.h

NICの初期設定

NICを利用するための設定を行います。まずはrte_eth_dev_configure()でNICの設定を行います。

▼NICの設定
static const struct rte_eth_conf port_conf = {0};
int ret = rte_eth_dev_configure(0, 1, 1, &port_conf);
if (ret < 0) {
  printf("Cannot configure device \n");
  return -1;
}
この関数のパラメータに関して解説します。
- 第1パラメータは、ポート番号を指定します。
- 第2パラメータにはNICの受信キュー数を、第3パラメータにはNICの送信キュー数を指定します。より効率よくNICを扱うには複数のキューでパケットを扱う作りとしますが、今回のサンプルでは"1"としています。
- 第4パラメータは、 NICのオフロード機能(※4)の利用設定です。今回は特にNICオフロード機能を利用しないため、0を設定しています。

次にNICのキューに対する設定を行います。rte_eth_rx_queue_setup()でNICの受信キューの設定を、rte_eth_tx_queue_setup()でNICの送信キューの設定を行います。

▼NICの受信キュー、送信キューの設定
int ret = rte_eth_rx_queue_setup(0, 0, 1024, rte_eth_dev_socket_id(0),
                             NULL, pktmbuf_pool);
if (ret < 0) {
  printf("error rte_eth_rx_queue_setup \n");
  return -1;
}
・・・
ret = rte_eth_tx_queue_setup(0, 0, 1024, rte_eth_dev_socket_id(0),
                             NULL);
if (ret < 0) {
  printf("error rte_eth_tx_queue_setup \n");
  return -1;
}
rte_eth_rx_queue_setup()とrte_eth_tx_queue_setup()のパラメータに関して解説します。
- 第1パラメータは、ポート番号を指定します。
- 第2パラメータは、設定対象のキュー番号を指定します。今回のサンプルでは使用するキュー数は1つなので、"0"としています。(キュー番号は0から始まります)
- 第3パラメータは、パケットのバッファ面数を指定します。パケットを受信した場合、PMDが受信パケットをパケットバッファに割り付けて確保します。パケットを送信した場合、送信データをパケットバッファに貯めて順次送信します。バッファ量はアプリケーションの特性にも依存しますが、今回のサンプルでは"1024"としています。
- 第4パラメータは、メモリ確保場所を指定します。rte_socket_id()を使いローカルメモリ(※3)からリソースを確保しています。
- 第5パラメータは、キュー毎のNICのオフロード機能の設定です。今回は特にNICオフロード機能を利用しないため、NULLを指定しています。
- 第6パラメータは、受信パケット格納先のパケットバッファを指定します。送信パケットの場合このパラメータは不要となります。

ここまで設定が出来たらrte_eth_dev_start()でNICを起動します。また、このままではNICが持つMACアドレスと合致するパケット以外は受信できないので、rte_eth_promiscuous_enable()でNICをプロミスキャスモードにしてます。プロミスキャスモードについてはDPDKのパケット処理 ~DPDK入門 第2回~で説明していますのでそちらを参照してください。

▼NICを起動しプロミスキャスモードにセット
int ret = rte_eth_dev_start(0);
if (ret < 0) {
  printf("error rte_eth_dev_start \n");
  return -1;
}
・・・
rte_eth_promiscuous_enable(0);
最後にNICが対向と通信できる(リンクアップするといいます)ことを確認します。rte_eth_link_get()関数でリンクアップ待ちを行います。rte_eth_link_get()関数をコールすると、NICがリンクアップするまで待ちます。対向NICがアップされていない場合などはNICがリンクアップできないため、タイムアウトになります。

▼NICのリンクアップ待ち
struct rte_eth_link link;
rte_eth_link_get(0, &link);
if (link.link_status == ETH_LINK_DOWN) {
  printf("timeout \n");
  return -1;
}
この関数のパラメータに関して解説します。
- 第1パラメータは、リンクアップ待ちを行うポート番号を指定します。
- 第2パラメータには、NICのステータスが返ってきます。今回はrte_eth_link構造体のメンバ:link_statusを確認して、NICがリンクアップしているかを判別しています。

ここまでで、NICの初期設定は終了です。

※4 NICのオフロード機能とは
チェックサム計算や分割されたTCPパケットの結合など、通常はドライバやアプリケーションが担う一部のパケット処理をNICが行う機能です。この機能をうまく活用することでアプリケーションの負荷が軽減され、高速化を狙うことができます。

【APIリファレンス】
rte_eth_dev_configure
引数 in/out パラメータ 補足
uint16_t input 設定対象のポート番号 -
uint16_t input 受信キュー数 NICの受信キューを何個使用するか指定
uint16_t input 送信キュー数 NICの送信キューを何個使用するか指定
const struct rte_eth_conf* input ハードウェア機能の設定 NICオフロード機能を利用する設定
戻り値 説明
int 0 正常終了
負値 異常終了
ヘッダファイル rte_ethdev.h

rte_eth_rx_queue_setup
引数 in/out パラメータ 補足
uint16_t input 設定対象のポート番号 -
uint16_t input 設定対象のキュー番号 -
uint16_t input 受信バッファサイズ キューから取得した受信パケットを貯めておく受信バッファの面数を指定
unsigned int input ソケットID メモリ確保元のソケットを指定
const struct rte_eth_rxconf* input ハードウェア機能の設定 NICオフロード機能を利用する設定
struct rte_mempool* input 受信パケットの格納先パケットバッファ -
戻り値 説明
int 0 正常終了
負値 異常終了
ヘッダファイル rte_ethdev.h

rte_eth_tx_queue_setup
引数 in/out パラメータ 補足
uint16_t input 設定対象のポート番号 -
uint16_t input 設定対象のキュー番号 -
uint16_t input 送信バッファサイズ DPDKアプリケーションが指定した送信パケットを貯めておく送信バッファの面数を指定
unsigned int input ソケットID メモリ確保元のソケットを指定
const struct rte_eth_txconf* input ハードウェア機能の設定 NICオフロード機能を利用する設定
戻り値 説明
int 0 正常終了
負値 異常終了
ヘッダファイル rte_ethdev.h

rte_eth_dev_socket_id
引数 in/out パラメータ 補足
uint16_t input ポート番号 -
戻り値 説明
int 整数値 ポートが紐づくNICが属するNUMAノード
ヘッダファイル rte_ethdev.h

rte_eth_dev_start
引数 in/out パラメータ 補足
uint16_t input 起動対象のポート番号 -
戻り値 説明
int 0 正常終了
負値 異常終了
ヘッダファイル rte_ethdev.h

rte_eth_promiscuous_enable
引数 in/out パラメータ 補足
uint16_t input プロミスキャスモードに設定する対象のポート番号 -
戻り値 説明
void - -
ヘッダファイル rte_ethdev.h

rte_eth_link_get
引数 in/out パラメータ 補足
uint16_t input リンクアップ待ち対象のポート番号 -
struct rte_eth_link* output リンクアップ状態 -
戻り値 説明
void - -
ヘッダファイル rte_ethdev.h

おわりに

今回はDPDKを利用したアプリケーションの基本的な作成方法を解説させていただきました。今まで紹介してきたDPDKのサンプルアプリケーションに対して今回解説したソースコードを書けば、簡単に動作確認することができると思いますので、ぜひ試してみてください。今回紹介した内容はNICの初期化までしか行っていないので、"受信したパケットを折り返す"ことは出来ません。次回は、この機能を作成し動作を確認するところまで紹介する予定です。ご覧頂きありがとうございました。

連載シリーズ
DPDK入門
著者プロフィール
寺尾 智之
寺尾 智之
長年NTTの通信基盤となるネットワーク関連の仕事に従事。 最近では仮想化技術が主流化しており、最新動向を追従する日々。 DPDKに関してはニッチな領域のためか仲間が増えないと感じており、ブログを通した仲間づくりを開始。 横浜生まれだが、巨人ファン。たまに東京ドームに観戦に行くことが楽しみ。