本記事はDeNA 23 新卒 Advent Calendar 2022の25日目の記事です(ラスト ).
はじめに
本記事はRustでDPDKを使う苦行(チュートリアル)記事です.
本記事ではRustの文法等は取り上げず, RustからどのようにDPDKを扱うのかについて触れます.
DPDK
DPDK(Data Plane Development Kit)は, 高速なパケット処理を実現するためのライブラリです.
UIO(User Space I/O)という機能を用いて, カーネルをバイパスしユーザー空間で直接NICを触る機能をサポートしています. カーネルガン無視なので高速です.
また1CPUコア=1スレッドで固定化することでコンテキストスイッチを抑制しています.
メモリにはhugepageを利用することで高速アクセスを図っています. hugepageはメモリ空間のページサイズを大きくしたものです. 通常4KBのページサイズを2MB/1GBに拡張できます. ページフォールトの抑制に繋がります.
詳しい話はこれらのページが分かりやすいです(丸投げ).
開発準備
DPDKの用意
まずはインストール
今回はDPDK22.03を使います.
# cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.3 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.3 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal
# wget http://fast.dpdk.org/rel/dpdk-22.03.tar.xz
# tar xJf dpdk-22.03.tar.xz
# cd ./dpdk-22.03
# meson build
# cd ./build
# ninja
# ninja install
hugepageの準備をします.
DPDKをインストールすると, セットアップ用のスクリプトがインストールされるのでそれを使います.
-pはページ単位(2MB or 1GB), --setupは確保するメモリ容量です.
以下の例だ1GBのページを2つ用意し, 2GBのメモリ容量を確保します.
dpdk-hugepages.py -p 1G --setup 2G
最後にNICをDPDKの管理化に置きます.
こちらもスクリプトが用意されているのでそれを使います.
まず存在するNICを確認します.
# dpdk-devbind.py -s
Network devices using kernel driver
===================================
0000:01:00.0 'Virtio network device 1041' if=enp1s0 drv=virtio-pci unused=vfio-pci,uio_pci_generic *Active*
0000:06:00.0 'Virtio network device 1041' if=enp6s0 drv=virtio-pci unused=vfio-pci,uio_pci_generic
0000:07:00.0 'Virtio network device 1041' if=enp7s0 drv=virtio-pci unused=vfio-pci,uio_pci_generic
No 'Baseband' devices detected
==============================
No 'Crypto' devices detected
============================
No 'DMA' devices detected
=========================
No 'Eventdev' devices detected
==============================
No 'Mempool' devices detected
=============================
No 'Compress' devices detected
==============================
No 'Misc (rawdev)' devices detected
===================================
No 'Regex' devices detected
===========================
"Network devices using kernel driver"がカーネルで認識されているNICです.
これをUIOで利用できるように切り替えます.
切り替え後はカーネル側から認識できなくなるので, カーネルから使えません.
# modprobe uio_pci_generic
# dpdk-devbind.py -b uio_pci_generic 0000:06:00.0
# dpdk-devbind.py -s
Network devices using DPDK-compatible driver
============================================
0000:06:00.0 'Virtio network device 1041' drv=uio_pci_generic unused=vfio-pci
Network devices using kernel driver
===================================
0000:01:00.0 'Virtio network device 1041' if=enp1s0 drv=virtio-pci unused=vfio-pci,uio_pci_generic *Active*
0000:07:00.0 'Virtio network device 1041' if=enp7s0 drv=virtio-pci unused=vfio-pci,uio_pci_generic
No 'Baseband' devices detected
==============================
No 'Crypto' devices detected
============================
No 'DMA' devices detected
=========================
No 'Eventdev' devices detected
==============================
No 'Mempool' devices detected
=============================
No 'Compress' devices detected
==============================
No 'Misc (rawdev)' devices detected
===================================
No 'Regex' devices detected
===========================
0000:06:00.0が"Network devices using DPDK-compatible driver"に表示されました.
これでDPDKからNICが使えるようになります.
rust-dpdk
さてDPDKはCライブラリであるため, FFI(Foreign Function Interface)を使ってRustから叩きます.
FFIはRustから外部ライブラリを利用するための機能です.
FFIを使うためにはRust側でDPDKのインタフェースを作らないといけません.
手動で作ってもいいのですが(無理), 今回は自動生成ライブラリであるbindgenを組み込んでいるANLAB-KAIST/rust-dpdkを利用します.
DPDK関数をRust向けにラップしたラッパーライブラリが付属していますが, 今回は直接DPDKの関数を叩きます.
dependenciesに追加するだけで利用できます.
[dependencies]
rust-dpdk-sys = { git = "https://github.com/ANLAB-KAIST/rust-dpdk.git", rev = "ca6c36fad6160fde9679fcbe4c4effdf56ab23e6" }
HelloWorld
準備ができたのでHelloWorldします.
# cargo new helloworld
各スレッドでコアIDを出力するだけのコードです.
重要そうな関数だけ抜粋します.
-
dpdk_sys::rte_eal_init
DPDKの初期化を実行します.
引数にはコマンドライン引数のサイズとコマンドライン引数本体のポインタを与えます.
RustとCでは文字列の扱い方が異なるため, CStringにキャストし, CStringを*mut c_charにキャストする必要があります. -
dpdk_sys::rte_eal_remote_launch
指定したCPUコアで新規スレッドを立ち上げます.
今回はlcore_hello関数を新しいスレッドで呼び出します. RustとCではABIが異なるので, 引数で渡す関数にはextern "C"を付与して, 関数をCから叩けるようにしないといけません.
(今回はnullですが)第2引数で呼び出し関数の引数を, ポインタで渡すことができます. 当然構造体も渡すことができますが, #[repr(C)]を指定しメモリレイアウトをCに合わせる必要があります.
use std::env;
use std::ffi::c_void;
use std::ffi::c_char;
use std::ffi::CString;
use std::ptr::null_mut;
extern "C" fn lcore_hello(_: *mut c_void) -> i32 {
unsafe {
println!("hello from core {}", dpdk_sys::rte_lcore_id());
}
0
}
fn main() {
// コマンドライン引数の加工
let cargs: Vec<_> = env::args().map(|s| CString::new(s).unwrap()).collect();
let mut dpdk_cargs: Vec<_> = cargs.iter().map(|s| s.as_ptr() as *mut c_char).collect();
unsafe {
// DPDK初期化
let ret = dpdk_sys::rte_eal_init(dpdk_cargs.len() as i32, dpdk_cargs.as_mut_ptr());
if ret < 0 {
panic!("Cannot init EAL\n");
}
}
unsafe {
let mut lcore_id: u32 = dpdk_sys::rte_get_next_lcore(u32::MIN, 1, 0);
while lcore_id < dpdk_sys::RTE_MAX_LCORE {
// 指定したCPUコアで新規スレッドを立ち上げ
dpdk_sys::rte_eal_remote_launch(Some(lcore_hello), null_mut(), lcore_id);
lcore_id = dpdk_sys::rte_get_next_lcore(lcore_id, 1, 0);
}
}
lcore_hello(null_mut());
}
ビルドして実行します. 実行にはroot権限が必要です.
-cはDPDKから利用するコアIDをビットで表現し, それを16進数で指定します.
例えば0x7の場合, 2進数表記で"111"となるのでコアID0~2をDPDKで使います. 0x6だと"110"なのでコアID1~2になります.
# cargo build
# sudo ./target/debug/helloworld -c 0x7
EAL: Detected CPU lcores: 5
EAL: Detected NUMA nodes: 1
EAL: Detected static linkage of DPDK
EAL: Multi-process socket /var/run/dpdk/rte/mp_socket
EAL: Selected IOVA mode 'PA'
EAL: No free 1048576 kB hugepages reported on node 0
EAL: VFIO support initialized
TELEMETRY: No legacy callbacks, legacy socket not created
hello from core 1
hello from core 2
hello from core 0
パケット送受信
次はオウム返しするだけの簡単なパケット送受信を実装してみます.
がその前にrust-dpdkとDPDK本体に細工をします.
rust-dpdkを細工したものがx8xx/rust-dpdkになります(差分).
rte_debug.hをビルドに含めるようにしたのと, DPDKの構造体でDefaultトレイトを利用できるようにしました. DPDKの関数は参照渡しが多く, 構造体をいちいち手動で初期化するのは面倒なのでDefaultを使う戦略?です.
続いてDPDKの細工ですがこれは環境によって異なります. 結論から書くとリンクできていないドライバをリンクするための処置です.
rust-dpdkではDPDKとRustプログラムを静的リンクします. このときドライバ等がリンクされない問題(原因は書くと長くなりそうなので割愛)があるため応急処置として, ドライバ内に適当に関数を追記してRustから叩きます. するとドライバが適切にリンクされるようになります(あくまで応急処置なので褒められた行為ではない).
例) virtio用のドライバの場合
末尾に適当な関数を新規作成
void load_rte_virtio_pci_eth_dev() {
RTE_LOG(INFO, EAL, "Loading virtio_pci_ethdev\n");
}
ヘッダにも書く (rte_debug.hでなくてもいい)
void load_rte_virtio_pci_eth_dev();
追記後ビルドすれば完了です. Rustからdpdk_sys::load_rte_virtio_pci_eth_dev()を叩けば無事リンクされるようになります.
さて細工が終わったのでRustプログラムを見ていきます.
こちらも重要そうな関数だけ抜粋します.
- dpdk_sys::rte_pktmbuf_pool_create
DPDKではパケットをmbufというデータ構造で扱います. そのmbufを格納するバッファを作成します. - dpdk_sys::rte_eth_rx_burst / dpdk_sys::rte_eth_tx_burst
パケットの送受信関数です. mbufの受け渡しはすべてポインタでやり取りされます.
送受信はバッチ処理になっており, 一度に複数のmbufを取り扱えるので, mbufポインタの配列(のポインタ)を使っています.
use std::env;
use std::ffi::c_char;
use std::ffi::CString;
use std::ptr::null_mut;
fn main() {
// コマンドライン引数の加工
let cargs: Vec<_> = env::args().map(|s| CString::new(s).unwrap()).collect();
let mut dpdk_cargs: Vec<_> = cargs.iter().map(|s| s.as_ptr() as *mut c_char).collect();
unsafe {
// リンクできません問題の応急処置
dpdk_sys::load_rte_virtio_pci_eth_dev();
}
unsafe {
// DPDK初期化
let ret = dpdk_sys::rte_eal_init(dpdk_cargs.len() as i32, dpdk_cargs.as_mut_ptr());
if ret < 0 {
panic!("Cannot init EAL\n");
}
// 受信パケットを格納するバッファを作成
let cstr_mbuf_pool_name = CString::new("mbuf_pool").unwrap();
let buf = dpdk_sys::rte_pktmbuf_pool_create(
cstr_mbuf_pool_name.as_ptr() as *mut c_char,
8192,
256,
0,
dpdk_sys::RTE_MBUF_DEFAULT_BUF_SIZE.try_into().unwrap(),
dpdk_sys::rte_socket_id().try_into().unwrap()
);
// ここからポート初期化
let port_conf: dpdk_sys::rte_eth_conf = Default::default();
if dpdk_sys::rte_eth_dev_configure(0, 1, 1, &port_conf as *const _) < 0 {
panic!("Cannot configure device\n");
}
let dev_socket_id = dpdk_sys::rte_eth_dev_socket_id(0).try_into().unwrap();
if dpdk_sys::rte_eth_rx_queue_setup(0, 0, 1024, dev_socket_id, null_mut(), buf) < 0 {
panic!("Error rte_eth_rx_queue_setup\n");
}
if dpdk_sys::rte_eth_tx_queue_setup(0, 0, 1024, dev_socket_id, null_mut()) < 0 {
panic!("Error rte_eth_tx_queue_setup\n");
}
if dpdk_sys::rte_eth_dev_start(0) < 0 {
panic!("Error rte_eth_dev_start\n");
}
dpdk_sys::rte_eth_promiscuous_enable(0);
// ポート初期化ここまで
let mut pkts: [*mut dpdk_sys::rte_mbuf; 32] = [null_mut(); 32];
loop {
// 受信
let tap_rx = dpdk_sys::rte_eth_rx_burst(0, 0, pkts.as_ptr() as *mut *mut dpdk_sys::rte_mbuf, 32);
if tap_rx <= 0 {
continue;
}
// 送信
dpdk_sys::rte_eth_tx_burst(0, 0, pkts.as_ptr() as *mut *mut dpdk_sys::rte_mbuf, tap_rx);
}
}
}
ビルドして実行します.
対向からパケットを送るとそのまま返ってくると思います.
(無限ループなのでCtrl-cで終了してください)
# cargo build
# sudo ./target/debug/helloworld -c 0x1
EAL: Loading virtio_pci_ethdev
EAL: Detected CPU lcores: 5
EAL: Detected NUMA nodes: 1
EAL: Detected static linkage of DPDK
EAL: Multi-process socket /var/run/dpdk/rte/mp_socket
EAL: Selected IOVA mode 'PA'
EAL: No free 1048576 kB hugepages reported on node 0
EAL: VFIO support initialized
EAL: Probe PCI driver: net_virtio (1af4:1041) device: 0000:01:00.0 (socket 0)
eth_virtio_pci_init(): Failed to init PCI device
EAL: Requested device 0000:01:00.0 cannot be used
EAL: Probe PCI driver: net_virtio (1af4:1041) device: 0000:06:00.0 (socket 0)
EAL: Probe PCI driver: net_virtio (1af4:1041) device: 0000:07:00.0 (socket 0)
eth_virtio_pci_init(): Failed to init PCI device
EAL: Requested device 0000:07:00.0 cannot be used
TELEMETRY: No legacy callbacks, legacy socket not created
おわり
いかがでしたか?
本記事ではANLAB-KAIST/rust-dpdkを用いたRustでのDPDKプログラミングについて紹介しました.
本記事が日頃のパケットとの戯れに役に立てば幸いです.