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

リソース設定(CPU) ~DPDK入門 第3回~

DPDKではCPU,メモリ, NICなどの低レベルリソースをユーザから設定を行う必要があります。今回はよく利用する設定方法をCPUを中心に解説します。

はじめに

こんにちは、NTTテクノクロス株式会社の寺尾です。

前回まででパケット処理の動かし方までを紹介させて頂きました。DPDKではCPU,メモリ, NICなどの低レベルリソースをユーザから設定を行う必要があります。今回はよく利用する設定方法をCPUを中心に解説します。よろしくお願いします。

イメージ画像

EALとは

DPDKはEALと呼ばれる低レベルリソースの抽象化層があります。主な役割としては、CPU、メモリ、NIC(PCIデバイス)といった低レベルリソースに対して環境に依存する情報を隠蔽し、DPDKアプリケーションに対してリソースのアクセスを容易にするためのインタフェースを提供します。DPDKを動かす際は、初期作業でアプリケーションが利用するリソースを設定します。EALに関して詳しく知りたい人は、DPDKのサイト(※1)にも記載がありますのでご覧下さい。

※1 Environment Abstraction Layer
https://doc.dpdk.org/guides/prog_guide/env_abstraction_layer.html

例えば DPDKのパケット処理 ~DPDK入門 第2回~ で紹介した L2 Forwarding Sample の場合では、-c 0x3 を指定することで、DPDKに対してCPUの0番と1番を利用するようにEALから設定しました。

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


メモリやNICに関しては何も設定がないため、基本的にはDPDKで使えるリソースは全て利用されます。尚、「--」 以降のコマンドパラメータはアプリケーション固有の設定になりEALの対象外です。通常、DPDKのリソース設定はmain関数の最初で実施します。以下がソースコードイメージになります。

DPDKの初期設定のソースコードイメージ
int main(int argc, char **argv)
{
・・・・
int ret
ret = rte_eal_init(argc, argv);
if (ret < 0)
rte_exit(EXIT_FAILURE, "Invalid EAL arguments\n");
argc -= ret;
argv += ret;
・・・・
}

rte_eal_init() 関数がDPDKの初期設定を行い、コマンドラインのパラメータをそのまま関数の引数として渡しています。先ほどのl2fwdのコマンドラインでは、-- までがEALで利用されるパラメータとなり、それ以降はDPDKアプリケーション側で利用するパラメータとなります。

この内容を確認するため、実際に以下のコードを埋め込み検証してみます。

l2fwdの追加コード
int
main(int argc, char **argv)
{
・・・・・・
int i=0;
for (i=0;i<argc;i++){
printf("main() argv[%d]=%s\n",i,argv[i]);
}
printf("\n");
/* init EAL */ ret = rte_eal_init(argc, argv); if (ret < 0) rte_exit(EXIT_FAILURE, "Invalid EAL arguments\n"); printf("\n"); printf("rte_eal_init() ret=[%d]\n",ret); for (i=0;i<argc;i++){ printf("main() argv[%d]=%s\n",i,argv[i]); } printf("\n");
argc -= ret; argv += ret; for (i=0;i<argc;i++){ printf("dpdk-apl argv[%d]=%s\n",i,argv[i]); } ・・・・・・ }

引数の内容に問題なければ rte_eal_init()の返り値に読み込まれたパラメータ数が返却されます。実際動かした結果は以下のようになりました。

$ sudo ./l2fwd -c 0x3 -- -p 0x3
main() argv[0]=./l2fwd
main() argv[1]=-c
main() argv[2]=0x3
main() argv[3]=--
main() argv[4]=-p
main() argv[5]=0x3

EAL: Detected 32 lcore(s)
EAL: No free hugepages reported in hugepages-1048576kB
EAL: Probing VFIO support...
・・・・・
rte_eal_init() ret=[3]
main() argv[0]=./l2fwd
main() argv[1]=-c
main() argv[2]=0x3
main() argv[3]=./l2fwd
main() argv[4]=-p
main() argv[5]=0x3

dpdk-apl argv[0]=./l2fwd
dpdk-apl argv[1]=-p
dpdk-apl argv[2]=0x3
・・・・


rte_eal_init()の返り値が3となり、-- は rte_eal_init() 後に argv[0]の内容が反映されていることが分かります。EALとして、3つのパラメータを正常に読み込めたことを表しています。この動きを利用すると、以下の2行を加えるとDPDKアプリケーション側ではDPDKのパラメータを意識することなく、アプリケーション側のパラメータに専念できます。

argc -= ret;
argv += ret;

dpdk-apl argv[0]~argv[2] として、EALで利用するパラメータを意識することなくDPDKアプリケーションが利用するパラメータを読み取ることができました。

DPDKの多数のサンプルアプリケーションはこの方法で実装しています。新規にDPDKアプリケーションを開発する際もこの方法で実装すると良いと思います。

CPUの割り当て

DPDKでは、CPUの割り当ては「論理プロセッサ」単位で行います。論理プロセッサはOSが認識しているCPUになります。例えば、8コアのCPUを2つ搭載したマシンであれば論理プロセッサの数は16個となります(※)。論理プロセッサの確認は以下のコマンドでも確認できます。

※ 実際にはハイパースレッティングテクノロジなど物理的なコア数以上に見せる技術がありますが、ここでは簡単に説明しています。

$ cat /proc/cpuinfo | grep processor

processor : 0
processor : 1
processor : 2
・・・・
processor : 15
processor : 16


論理プロセッサ毎に番号が付与されていることが分かります。CPUを扱ういろいろなOSのコマンドでこの番号は利用されますが、DPDKの-cオプションもこの番号でCPUの割り当てを実施します。設定は16進数で行いますが、マスク値の考え方で設定されます。以下に図で記述します。

DPDKではCPUの割り当てもrte_eal_init() 関数で行います。CPUの割り当て処理のイメージを以下に図で記述します。

rte_eal_init()関数では、自プロセスに対して専用のCPUを割り当てるとともに、残りのCPUに対して専用のCPUを割り当てたスレッドを作成します。図のコマンドラインでは論理プロセッサの1番~3番を指定しているので、1番を自プロセスに割り当て、2番~3番を別のスレッドを作成して割り当てています。

DPDKではメインプロセスのことをmaster、それ以外のスレッドをslaveと呼びます。メインプロセスや各スレッドは固有の識別子(lcore)を付与するとともに、論理プロセッサを固定化するCPUアフェニティ(※)を設定しています。

CPUアフェニティはisolcpusの章で解説します。

今回のコマンドラインでは、rte_eal_init() ではlcoreの値は論理プロセッサの番号と同じ値が設定されます。またmaster lcore は指定された最若番の論理プロセッサ番号が割り当たります。

動作確認と解説

DPDKに対してのCPUの指定方法を DPDKとは? ~DPDK入門 第1回~ で紹介したHello Worldのサンプルプログラムを使って確認します。lcore_hello(NULL)の箇所は、今回の解説のため修正しています。

Hello Worldのソースコード(一部修正イメージ)
static int
lcore_hello(__attribute__((unused)) void *arg)
{
unsigned lcore_id;
lcore_id = rte_lcore_id();
printf("hello from core %u\n", lcore_id);
return 0;
}
int
main(int argc, char **argv)
{
int ret;
unsigned lcore_id;
ret = rte_eal_init(argc, argv);
if (ret < 0)
rte_panic("Cannot init EAL\n");
/* call lcore_hello() on every slave lcore */
RTE_LCORE_FOREACH_SLAVE(lcore_id) {
rte_eal_remote_launch(lcore_hello, NULL, lcore_id);
}
/* call it on master lcore too */
// lcore_hello(NULL);
lcore_id = rte_lcore_id();
printf("master-lcore hello from core %u\n", lcore_id);
rte_eal_mp_wait_lcore();
return 0;
}

サンプルプログラムを少し解説します。

rte_eal_init()の後、各スレッドをrte_eal_remote_launch() をlcoreを指定していますが、マクロ RTE_LCORE_FOREACH_SLAVE を使うことで、プログラマからは直接lcoreの値を意識することなくrte_eal_init()で割り当てたCPUの数だけスレッドを起動することができます。

各スレッドは、rte_eal_remote_launch()のパラメータで実行する関数を指定します。この例では、起動したすべてのスレッドは同じ関数lcore_hello()が呼び出されます。

main()関数の最後で自分自身のlcoreの値を出力しています。今回はmasterと分かりやすいように出力メッセージを変更しました。

それでは、-c 0x1e オプションで実行してみます。

$ sudo ./helloworld -c 0x1e
EAL: Detected 32 lcore(s)
・・・
hello from core 2
hello from core 3
hello from core 4
master-lcore hello from core 1

master core が 1番となり、slave core が2番~4番となりました。指定した最若番の論理プロセッサがmaster coreとなり、残りすべてがslave coreとして設定されたことが分かります。論理プロセッサの指定オプションとしては、以下のものが良く使われます。

-c オプション
16進数で論理プロセッサを指定します。

〇 -l オプション
10進数で論理プロセッサを指定します。-c 0x1e は -l 2,3,4,5 と同じ意味になります。-l 2-4,6-7 のような連番指定も可能です。

〇 --master-lcore オプション
再若番以外のlcoreをmasterにしたい場合、master lcoreの論理プロセッサを指定します。-c や -l オプション等で指定した範囲内のリソースに限り有効です。

-lオプションと--master-lcoreオプションで実行してみます。

$ sudo ./helloworld -l 2-4,7-9 --master-lcore 8
EAL: Detected 32 lcore(s)
・・・・・
hello from core 2
hello from core 3
hello from core 4
hello from core 7
hello from core 9
master-lcore hello from core 8

いろいろ試してみてください。

isolcpus

OSはプロセスに対してCPU(論理プロセッサ)をできるだけ公平に利用するようにスケジューリングする機能があります。プロセスの数はCPUよりも多く存在するため、特定のプロセスがCPUを占有することはせず、あるプロセスが一定時間CPUを使った後は別のプロセスにCPUを渡す必要があります。こういった切り替え処理はコンテキストスイッチと言われ、プロセスを処理するための情報の切り替えなどが必要になり、DPDKアプリケーションのような高速性を求めるアプリケーションにとっては好ましくありません。

しかしisolcpusという設定を行うことで、OSのCPUの割り当て候補となるリストから除外することができます。CPUの割り当て候補のリストのことをCPUアフェニティと言います。通常の状態ではプロセスは全てのCPUを公平に利用しようとしますが、CPUアフェニティを設定することで使用するCPUを制限することができます。

それでは、CPUアフェニティの確認方法やisolcpusの設定方法を解説していきます。今回は自身のシェルスクリプトプロセスを使って、CPUアフェニティの設定状況を確認してみます。

$ ps
PID TTY TIME CMD
102464 pts/6 00:00:00 bash
102728 pts/6 00:00:00 ps

$ taskset -pc 102464
pid 102464's current affinity list: 0-31


このマシンは論理プロセッサ数は32です。特にCPUの割り当て候補のリストから除外していないため、すべての論理プロセッサが割り当たる可能性があることが分かります。

次にisolcpusを設定します。設定はgrubファイルで行います。
今回は論理プロセッサの1番、2番、3番を除外することにします。以下のようにファイルを編集します。

sudo vi /etc/default/grub


grubファイルの中で、GRUB_CMDLINE_LINUX_DEFAULT の記載を追加します。もしこの行があるようでしたら、isolcpusの文字列だけを追加下さい。

/etc/default/grug

・・・・
GRUB_CMDLINE_LINUX_DEFAULT="isolcpus=1,2,3,5-7"
・・・

※ 複数の場合は「,」区切り、連番の場合は「-」で記載できる

設定が終われば、変更内容を反映してrebootすれば設定完了です。

$sudo update-grub
$sudo reboot

isolcpusの設定が有効になったかどうかは、CPUアフェニティの設定内容に変化があったかどうかで確認します。

$ ps
PID TTY TIME CMD
2991 pts/0 00:00:00 bash
3077 pts/0 00:00:00 ps

$ taskset -pc 2991
pid 2991's current affinity list: 0,4,8-31


isolcpusで設定した論理プロセッサが除外されていることが確認できれば設定成功です。isolcpusで除外した論理プロセッサはすべてのプロセスが対象となりますので、意図的に割り当てない限り使われることがありません。

DPDKアプリケーションに割り当てるCPUは上記で除外された論理プロセッサを指定することで、不必要なコンテキストスイッチを無くすことができます。

おわりに

DPDKではCPU,メモリ, NICなどの低レベルリソースを設定する方法をCPU観点を中心に紹介させて頂きました。DPDKアプリケーションを利用する際は、論理プロセッサやCPUアフェニティ、isolcpusなどの知識があると、より上手にDPDKを扱うことができるかと思います。

次回ですが、メモリやNICに関しての解説をしていきたいと考えています。2018年12月末に公開する予定ですので、よろしくお願いします。ご覧頂きありがとうございました。

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