Apple Silicon ハイパーバイザー Week 5: VirtIO VirtQueue 実装
VirtIO 1.2 仕様に基づいた Split Virtqueues の実装。ドライバーとデバイス間のデータ転送用リングバッファの詳細解説。
概要
Phase 2 Week 1 として、VirtIO Block デバイスの基盤となる VirtQueue(Split Virtqueues)を実装しました。VirtQueue は、ゲスト OS(ドライバー)とハイパーバイザー(デバイス)間で効率的にデータを転送するためのリングバッファ構造です。
実装内容:
- VirtIO 1.2 仕様に基づいた Split Virtqueues
- Descriptor Table, Available Ring, Used Ring の 3 つのリング構造
- ユニットテスト 8 件(カバレッジ 100%)
リポジトリ: Building-a-hypervisor PR: #16 feat: Phase 2 Week 1 - VirtQueue 実装
VirtIO とは
VirtIO の役割
VirtIO は、仮想化環境で使用される標準化されたデバイスインタフェースです。以下の特徴があります。
- 効率的な I/O 通信: ゲスト OS とハイパーバイザー間の高速なデータ転送
- 標準化: QEMU、KVM、Firecracker などで広く使用
- デバイス非依存: Block、Network、Console など様々なデバイスタイプをサポート
Split Virtqueues
VirtIO 1.2 仕様では、Split Virtqueues と Packed Virtqueues の 2 種類が定義されています。今回は、より一般的な Split Virtqueues を実装しました。
Split Virtqueues は以下の 3 つの部分から構成されます。
┌─────────────────────────────────────┐
│ Descriptor Table │ ← バッファを記述する記述子のテーブル
│ (16 bytes × queue_size) │
│ │
│ [0] addr, len, flags, next │
│ [1] addr, len, flags, next │
│ ... │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Available Ring │ ← ドライバーが利用可能にした記述子
│ (Driver → Device) │
│ │
│ flags, idx, ring[queue_size] │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Used Ring │ ← デバイスが処理完了した記述子
│ (Device → Driver) │
│ │
│ flags, idx, ring[queue_size] │
└─────────────────────────────────────┘
Descriptor Table
Descriptor 構造体
Descriptor は 16 bytes の固定サイズで、バッファの情報を保持します。
/// VirtQueue Descriptor (16 bytes)
#[derive(Debug, Clone, Copy, Default)]
pub struct Descriptor {
/// ゲスト物理アドレス
pub addr: u64,
/// バッファ長
pub len: u32,
/// フラグ(NEXT, WRITE, INDIRECT)
pub flags: u16,
/// 次の記述子のインデックス(NEXT フラグが立っている場合)
pub next: u16,
}
フラグ定義
/// Descriptor フラグ: 次の記述子へチェーン
const VIRTQ_DESC_F_NEXT: u16 = 1;
/// Descriptor フラグ: 書き込み専用バッファ
const VIRTQ_DESC_F_WRITE: u16 = 2;
/// Descriptor フラグ: 間接記述子
const VIRTQ_DESC_F_INDIRECT: u16 = 4;
記述子チェイン
複数の Descriptor を NEXT フラグでチェインすることで、大きなバッファを表現できます。
impl Descriptor {
/// NEXT フラグが立っているか
pub fn has_next(&self) -> bool {
(self.flags & VIRTQ_DESC_F_NEXT) != 0
}
/// WRITE フラグが立っているか(書き込み専用)
pub fn is_write(&self) -> bool {
(self.flags & VIRTQ_DESC_F_WRITE) != 0
}
/// INDIRECT フラグが立っているか
pub fn is_indirect(&self) -> bool {
(self.flags & VIRTQ_DESC_F_INDIRECT) != 0
}
}
Available Ring
構造
Available Ring は、ドライバー(ゲスト OS)がデバイスに対して「この記述子を処理してください」と通知するためのリングです。
struct AvailRing {
/// フラグ(将来の実装で使用予定)
#[allow(dead_code)]
flags: u16,
/// 次に書き込むインデックス
idx: u16,
/// 記述子インデックスのリング
ring: Vec<u16>,
}
push メソッド(テスト用)
#[cfg(test)]
fn push(&mut self, desc_idx: u16) {
let idx = self.idx as usize % self.ring.len();
self.ring[idx] = desc_idx;
self.idx = self.idx.wrapping_add(1);
}
idxはモノトニックに増加(wrapping add)- リングバッファなので
% self.ring.len()で循環
Used Ring
構造
Used Ring は、デバイスがドライバーに対して「この記述子の処理が完了しました」と通知するためのリングです。
struct UsedRing {
/// フラグ(将来の実装で使用予定)
#[allow(dead_code)]
flags: u16,
/// 次に書き込むインデックス
idx: u16,
/// Used Element のリング
ring: Vec<UsedElem>,
}
struct UsedElem {
/// 記述子チェーンの開始インデックス
#[allow(dead_code)]
id: u32,
/// 書き込まれた合計バイト数
#[allow(dead_code)]
len: u32,
}
push メソッド
fn push(&mut self, id: u32, len: u32) {
let idx = self.idx as usize % self.ring.len();
self.ring[idx] = UsedElem::new(id, len);
self.idx = self.idx.wrapping_add(1);
}
VirtQueue 実装
VirtQueue 構造体
pub struct VirtQueue {
/// キューサイズ(2 の累乗)
num: u16,
/// Descriptor Table
desc_table: Vec<Descriptor>,
/// Available Ring
avail_ring: AvailRing,
/// Used Ring
used_ring: UsedRing,
/// 次に処理する Available Ring のインデックス
last_avail_idx: u16,
}
new メソッド
pub fn new(num: u16) -> Self {
assert!(
num > 0 && num.is_power_of_two(),
"Queue size must be a power of 2"
);
Self {
num,
desc_table: vec![Descriptor::default(); num as usize],
avail_ring: AvailRing::new(num),
used_ring: UsedRing::new(num),
last_avail_idx: 0,
}
}
重要: VirtIO 仕様では、キューサイズは必ず 2 の累乗である必要があります。
pop_avail メソッド
デバイス側が Available Ring から次の記述子を取得します。
pub fn pop_avail(&mut self) -> Option<u16> {
if self.last_avail_idx == self.avail_ring.idx {
// 新しい記述子がない
return None;
}
let idx = self.last_avail_idx as usize % self.num as usize;
let desc_idx = self.avail_ring.ring[idx];
self.last_avail_idx = self.last_avail_idx.wrapping_add(1);
Some(desc_idx)
}
ポイント:
last_avail_idx == avail_ring.idxの場合、新しい記述子がないlast_avail_idxをデバイス側で管理(ドライバーはavail_ring.idxを更新)
push_used メソッド
デバイス側が処理完了を Used Ring に追加します。
pub fn push_used(&mut self, idx: u16, len: u32) {
self.used_ring.push(idx as u32, len);
}
ユニットテスト
テスト 1: VirtQueue の作成
#[test]
fn test_virtqueue_new() {
let queue = VirtQueue::new(16);
assert_eq!(queue.size(), 16);
assert_eq!(queue.desc_table.len(), 16);
}
テスト 2: 無効なキューサイズ
#[test]
#[should_panic(expected = "Queue size must be a power of 2")]
fn test_virtqueue_new_invalid_size() {
VirtQueue::new(15); // 2 の累乗でない
}
テスト 3: Descriptor フラグ
#[test]
fn test_descriptor_flags() {
let desc = Descriptor::new(
0x1000,
512,
VIRTQ_DESC_F_NEXT | VIRTQ_DESC_F_WRITE,
1,
);
assert!(desc.has_next());
assert!(desc.is_write());
assert!(!desc.is_indirect());
}
テスト 4: Available Ring の push/pop
#[test]
fn test_push_and_pop_avail() {
let mut queue = VirtQueue::new(16);
// Available Ring に記述子を追加
queue.push_avail(0);
queue.push_avail(1);
queue.push_avail(2);
// pop_avail で取得
assert_eq!(queue.pop_avail(), Some(0));
assert_eq!(queue.pop_avail(), Some(1));
assert_eq!(queue.pop_avail(), Some(2));
assert_eq!(queue.pop_avail(), None);
}
テスト 5: ラップアラウンド
循環バッファの境界条件をテストします。
#[test]
fn test_avail_ring_wrapping() {
let mut queue = VirtQueue::new(4); // 小さいサイズでテスト
// リングサイズと同じ数を追加
for i in 0..4 {
queue.push_avail(i);
}
// すべて順番に取得できる
for i in 0..4 {
assert_eq!(queue.pop_avail(), Some(i));
}
assert_eq!(queue.pop_avail(), None);
// さらに追加してラップアラウンドをテスト
for i in 4..8 {
queue.push_avail(i);
}
// ラップアラウンド後も順番に取得できる
for i in 4..8 {
assert_eq!(queue.pop_avail(), Some(i));
}
assert_eq!(queue.pop_avail(), None);
}
発見: 最初のテストでは、リングサイズを超える数を push していましたが、これは VirtIO の仕様に反していました。修正後、正しくラップアラウンドを検証できるようになりました。
テスト結果
$ cargo test
running 24 tests
test devices::virtio::queue::tests::test_avail_ring_wrapping ... ok
test devices::virtio::queue::tests::test_descriptor_flags ... ok
test devices::virtio::queue::tests::test_get_set_desc ... ok
test devices::virtio::queue::tests::test_pop_avail_empty ... ok
test devices::virtio::queue::tests::test_push_and_pop_avail ... ok
test devices::virtio::queue::tests::test_push_used ... ok
test devices::virtio::queue::tests::test_set_desc_invalid_index ... ok
test devices::virtio::queue::tests::test_virtqueue_new ... ok
test devices::virtio::queue::tests::test_virtqueue_new_invalid_size ... ok
test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
カバレッジ: VirtQueue の主要機能をすべてカバー。
CI 修正作業
1. cargo fmt エラー
エラー:
assert! macro line too long
修正:
cargo fmt
assert! マクロが自動的に複数行に分割されました。
2. clippy dead_code 警告
エラー:
warning: field is never read: `flags`
warning: field is never read: `id`
warning: field is never read: `len`
原因: 将来使用するフィールドだが、現時点では未使用。
修正:
struct AvailRing {
#[allow(dead_code)]
flags: u16,
// ...
}
struct UsedElem {
#[allow(dead_code)]
id: u32,
#[allow(dead_code)]
len: u32,
}
3. clippy derivable_impls 警告
エラー:
warning: manual implementation of Default for Descriptor could be derived
修正:
// Before
#[derive(Debug, Clone, Copy)]
pub struct Descriptor { /* ... */ }
impl Default for Descriptor {
fn default() -> Self {
Self {
addr: 0,
len: 0,
flags: 0,
next: 0,
}
}
}
// After
#[derive(Debug, Clone, Copy, Default)]
pub struct Descriptor { /* ... */ }
技術的発見
1. VirtIO Split Virtqueues の理解
Split Virtqueues は、以下の 3 つのリング構造で構成されます。
- Descriptor Table: バッファの記述子(全記述子を保持)
- Available Ring: ドライバーが利用可能にした記述子のインデックス
- Used Ring: デバイスが処理完了した記述子のインデックス
この分離により、ドライバーとデバイスが独立して動作できます。
2. 循環バッファの実装
idx はモノトニックに増加し、リングバッファとして動作させるために % ring.len() でインデックスを計算します。
let idx = self.idx as usize % self.ring.len();
self.ring[idx] = value;
self.idx = self.idx.wrapping_add(1);
3. Rust の #[cfg(test)]
テスト用メソッドを公開する際、#[cfg(test)] を使うことで、本番コードに不要なメソッドを含めないようにできます。
#[cfg(test)]
pub fn push_avail(&mut self, desc_idx: u16) {
self.avail_ring.push(desc_idx);
}
次のステップ
Phase 2 Week 2 では、VirtIO MMIO レジスタの実装に進みます。
目標:
- VirtIO MMIO レジスタマップを実装
- ゲストがデバイスを検出できるようにする
- Device Tree に VirtIO Block ノードを追加
参考資料:
まとめ
Phase 2 Week 1 では、VirtIO Block デバイスの基盤となる VirtQueue を実装しました。Split Virtqueues の 3 つのリング構造を理解し、ユニットテストで検証することで、確実に動作する実装を作成できました。
次回は、VirtIO MMIO レジスタを実装し、ゲストがデバイスを検出できるようにします。