banner
ekko

ekko's blog

时间不在于你拥有多少,而在于你怎样使用
github
xbox
email

ivshmem仮想デバイス通信原理

本篇主要讲解 ivshmem 仮想デバイスバックエンドとフロントエンドが共有メモリと割り込みを利用して通信する原理及びプロセス分析。
Pasted image 20250124101752
上図の左側は OEE 側、右側は RTOS で、通信プロセスは主に割り込みと共有メモリを通じて実現されます。割り込み処理と共有メモリの初期化部分は ivshmem pcie ドライバによって行われ、本文の後続内容は割り込みが相互に通知され、共有メモリが直接使用できることを前提としています。

後続内容においてdevice端は OEE を指し、driver端は RTOS を指します。

一、virtio_ring 構造体紹介#

#include <openamp/virtio_ring.h>
/**
 * @brief The virtqueue layout structure
 *
 * Each virtqueue consists of; descriptor table, available ring, used ring,
 * where each part is physically contiguous in guest memory.
 * 
 * 各virtqueueは、記述子リング、利用可能リング、使用済みリングで構成されており、各部分は共有メモリ内で連続しています。
 *
 * ドライバがデバイスにバッファを送信したい場合、記述子テーブルのスロットに記入し(またはいくつかを連結し)、記述子インデックスを利用可能リングに書き込みます。その後、デバイスに通知します。デバイスがバッファの処理を終えたら、記述子インデックスを使用済みリングに書き込み、割り込みを送信します。
 * 
 * ドライバ側がデバイス側にメッセージを送信したい場合、記述子リングの1つに記入し、利用可能リングのインデックスに書き込みます。
 * その後、割り込みでデバイス側に通知します。デバイスがメッセージの受信を完了したら、受信した記述子インデックスを使用済みリングに書き込み、ドライバ側に割り込みを通知します。  
 *
 * リングの標準レイアウトは、次のように連続したメモリのチャンクです。numは2の累乗であると仮定します。
 * vringは共有メモリ内で次のようにデータが配置されます:
 * struct vring {
 *      // 実際の記述子(各16バイト)
 *      struct vring_desc desc[num];
 *
 *      // 自由に走行するインデックスを持つ利用可能な記述子ヘッドのリング。
 *      __u16 avail_flags;
 *      __u16 avail_idx;
 *      __u16 available[num];
 *      __u16 used_event_idx;
 *
 *      // 次のアライン境界までのパディング。
 *      char pad[];
 *
 *      // 自由に走行するインデックスを持つ使用済み記述子ヘッドのリング。
 *      __u16 used_flags;
 *      __u16 used_idx;
 *      struct vring_used_elem used[num];
 *      __u16 avail_event_idx;
 * };
 *
 * 注:VirtIO PCIの場合、アラインは4096です。
 */
struct vring {
	/**
	 * virtqueue内のバッファ記述子の最大数。
	 * 値は常に2の累乗です。
	 */
	unsigned int num;//記述子の数、最大共有メモリが収容できるメッセージの数と理解できます

	/** 実際のバッファ記述子、各16バイト */
	struct vring_desc *desc;//共有メモリ内のnum個のメッセージを指す、記述子リング

	/** 自由に走行するインデックスを持つ利用可能な記述子ヘッドのリング */
	struct vring_avail *avail;//利用可能な記述子リング、ドライバ側が書き込み、デバイス側が読み込み

	/** 自由に走行するインデックスを持つ使用済み記述子ヘッドのリング */
	struct vring_used *used;//使用済み記述子リング、デバイス側が書き込み、ドライバ側が読み込み
};

struct vringは実際の通信プロセスでデータ転送を制御するデータ構造であり、バックエンドとフロントエンドのデータ通信はこのデータ構造によって実現されます。
その中の 3 つのポインタは初期化時に共有メモリ領域を指します。

/**
 * @brief VirtIOリング記述子。
 *
 * 記述子テーブルは、ドライバがデバイスのために使用しているバッファを参照します。addrは物理アドレスで、バッファは\ref nextを介して連結できます。
 * 各記述子は、デバイスにとって読み取り専用(「デバイス読み取り可能」)またはデバイスにとって書き込み専用(「デバイス書き込み可能」)のバッファを説明しますが、記述子のチェーンにはデバイス読み取り可能とデバイス書き込み可能の両方のバッファを含めることができます。
 */
METAL_PACKED_BEGIN
struct vring_desc {
	/** アドレス(ゲスト物理) */
	uint64_t addr;//実際のメッセージアドレス、共有メモリ内

	/** 長さ */
	uint32_t len;//実際のメッセージの長さ

	/** 記述子に関連するフラグ */
	uint16_t flags;

	/** 使用されていない記述子をこれを介して連結します */
	uint16_t next;//0->1,1->2,環状インデックス
} METAL_PACKED_END;
/**
 * @brief デバイスにバッファを提供するために使用されます。
 *
 * 各リングエントリは、記述子チェーンのヘッドを参照します。これはドライバによってのみ書き込まれ、デバイスによって読み取られます。
 */
METAL_PACKED_BEGIN
struct vring_avail {
	/** デバイス通知が必要かどうかを決定するフラグ */
	uint16_t flags;

	/**
	 * ドライバがリング内の次の記述子エントリを置く場所を示します(キューサイズの剰余)
	 */
	uint16_t idx;//ドライバが次の記述子インデックスを書き込む

	/** 記述子のリング */
	uint16_t ring[0];//記述子インデックス配列
} METAL_PACKED_END;
/* uint32_tはパディングの理由でここで使用されています。 */
METAL_PACKED_BEGIN
struct vring_used_elem {
	union {
		uint16_t event;
		/* 使用済み記述子チェーンの開始インデックス。 */
		uint32_t id;
	};
	/* 書き込まれた記述子チェーンの合計長さ。 */
	uint32_t len;
} METAL_PACKED_END;
/**
 * @brief デバイスが処理を終えたバッファをこの構造体に返します
 *
 * この構造体はデバイスによってのみ書き込まれ、ドライバによって読み取られます。
 */
METAL_PACKED_BEGIN
struct vring_used {
	/** デバイス通知が必要かどうかを決定するフラグ */
	uint16_t flags;

	/**
	 * ドライバがリング内の次の記述子エントリを置く場所を示します(キューサイズの剰余)
	 */
	uint16_t idx;//デバイスが次の記述子インデックスを書き込む

	/** 記述子のリング */
	struct vring_used_elem ring[0];
} METAL_PACKED_END;

vring 初期化コード:

static inline void
vring_init(struct vring *vr, unsigned int num, uint8_t *p, unsigned long align)
{
	vr->num = num;
	vr->desc = (struct vring_desc *)p;//共有メモリの開始アドレスを渡す
	vr->avail = (struct vring_avail *)(p + num * sizeof(struct vring_desc));//記述子の終了アドレス
	vr->used = (struct vring_used *)
	    (((unsigned long)&vr->avail->ring[num] + sizeof(uint16_t) +
	      align - 1) & ~(align - 1));//availの終了アドレス+アラインオフセット
}

二、virtio_ring 通信フロー#

1. ドライバ側がメッセージを送信#

1.1 メッセージ内容を記入#

//現在の空いている記述子インデックス
u16 desc_index;
//空いている記述子ポインタを取得
struct vring_desc *curr_desc = vring.desc[desc_index];
//記入するアドレスは共有メモリアドレスのオフセットであり、各インデックスの記述子に対応するアドレスは自由に確認方法を定めることができます
//後続の転送データは共有メモリのこのメモリブロックに置かれます
curr_desc->addr = OFFSET_SHMEM + MSG_LENGTH * desc_index;
//転送データの長さ
curr_desc->len = msg_len;
//0はメッセージの最後の記述子、1は後続にメッセージ記述子があることを示します
curr_desc->flags = 0;
//メッセージ内容を記入
memcpy(shmem + curr_desc->addr, msg_buff, msg_len);

1.2 メッセージを送信#

以下のすべてのインデックス関連の変数は記述子数の剰余操作を行っており、後続では個別に説明しません

//現在の利用可能インデックスは、ちょうど記入されたメッセージインデックスを指します
vring.avail->ring[vring.avail->idx] = desc_index;
//空いている記述子インデックスを更新、Nは前のステップで転送されたメッセージの数
desc_index += N;
//利用可能インデックスを更新
vring.avail->idx++;
//割り込み通知
ivshmem_notify();

2. デバイス側がメッセージを受信#

//受信割り込み通知
waitfor_notify();
//現在の利用可能記述子インデックス
u16 avail_index;
//利用可能記述子インデックスが更新されていない場合、新しいメッセージはありません
if(avail_index == vring.avail->idx)
	return;
//メッセージ記述子インデックスを取得
u16 desc_index = vring.avail->ring[avail_index];
//メッセージ記述子を取得
get:
struct vring_desc *curr_desc = vring.desc[desc_index];
//メッセージ内容を読み取る
memcpy(msg_buff, shmem + curr_desc->addr, curr_desc->len);
//後続にメッセージがあるかどうか
if(curr_desc->flags != 0)
{
	desc_index++;
	goto get;
}

3. デバイス側が返信を送信#

//使用済み内容を更新
vring.used->ring[vring.used->idx].id = desc_index;
vring.used->ring[vring.used->idx].len = curr_desc->len;
avail_index++;
vring.used->idx++;
//割り込み通知
ivshmem_notify();

4. ドライバ側が返信を受信#

//受信割り込み通知
waitfor_notify();
//現在の使用済み記述子インデックス
u16 used_index;
//使用済み記述子インデックスが更新されていない場合、返信内容はありません
if(used_index == vring.used->idx)
	return;
//返信内容を取得
u16 id = vring.used->ring[used_index].id;
u16 len = vring.used->ring[used_index].len;
used_index++;

三、両端の初期化 virtio_ring#

以下の構造体は linux 端で共通であり、共有メモリ内の開始アドレスに格納されます

struct virtio_ivshmem_common_header {
	uint32_t revision;
	uint32_t size;

	uint32_t write_transaction;// 更新オフセット

	uint32_t device_features;
	uint32_t device_features_sel;
	uint32_t driver_features;
	uint32_t driver_features_sel;

	uint32_t queue_sel;//vringインデックス

	uint16_t queue_size;//vring.num
	uint16_t queue_device_vector;
	uint16_t queue_driver_vector;
	uint16_t queue_enable;
	uint64_t queue_desc; //vring.desc
	uint64_t queue_driver;//vring.avail
	uint64_t queue_device;//vring.used

	uint8_t config_event;
	uint8_t queue_event;
	uint8_t __reserved[2];
	uint32_t device_status;//0xfに設定すると初期化完了

	uint32_t config_generation;
};

以下のプログラムは linux 端の仮想シリアルポートバックエンドアプリケーションの初期化部分であり、この構造体を利用して RTOS 側で初期化された vring のパラメータを linux 側に渡すことができます:

static int process_write_transaction(void)
{
	unsigned int new_queue;

	switch (vc->write_transaction) {
	case 0:
		return 0;
	case VI_REG_OFFSET(device_features_sel):
		printf("device_features_sel: %d\n", vc->device_features_sel);
		if (vc->device_features_sel == 1) {
			vc->device_features =
				(1 << (VIRTIO_F_VERSION_1 - 32)) |
				(1 << (VIRTIO_F_IOMMU_PLATFORM - 32)) |
				(1 << (VIRTIO_F_ORDER_PLATFORM - 32));
		} else {
			vc->device_features = 1 << VIRTIO_CONSOLE_F_SIZE;
		}
		break;
	case VI_REG_OFFSET(driver_features_sel):
		printf("driver_features_sel: %d\n", vc->driver_features_sel);
		break;
	case VI_REG_OFFSET(driver_features):
		printf("driver_features[%d]: 0x%x\n", vc->driver_features_sel,
		       vc->driver_features);
		break;
	case VI_REG_OFFSET(queue_sel):
		new_queue = vc->queue_sel;
		printf("queue_sel: %d\n", new_queue);
		if (new_queue > 1)
			break;

		if (current_queue >= 0)
			memcpy(&queue_config[current_queue], &vc->queue_config,
			    sizeof(struct virtio_queue_config));

		current_queue = new_queue;
		memcpy(&vc->queue_config, &queue_config[current_queue],
		       sizeof(struct virtio_queue_config));
		break;
	case VI_REG_OFFSET(queue_config.size):
		printf("queue size: %d\n", vc->queue_config.size);
		break;
	case VI_REG_OFFSET(queue_config.driver_vector):
		printf("queue driver vector: %d\n",
		       vc->queue_config.driver_vector);
		break;
	case VI_REG_OFFSET(queue_config.enable):
		printf("queue enable: %d\n", vc->queue_config.enable);
		if (current_queue >= 0 && vc->queue_config.enable) {
			memcpy(&queue_config[current_queue], &vc->queue_config,
			    sizeof(struct virtio_queue_config));
			vring[current_queue].num = vc->queue_config.size;
			vring[current_queue].desc =
				shmem + vc->queue_config.desc;
			vring[current_queue].avail =
				shmem + vc->queue_config.driver;
			vring[current_queue].used =
				shmem + vc->queue_config.device;
			next_idx[current_queue] = 0;
		}
		break;
	case VI_REG_OFFSET(queue_config.desc):
		printf("queue desc: 0x%llx\n",
		       (unsigned long long)vc->queue_config.desc);
		break;
	case VI_REG_OFFSET(queue_config.driver):
		printf("queue driver: 0x%llx\n",
		       (unsigned long long)vc->queue_config.driver);
		break;
	case VI_REG_OFFSET(queue_config.device):
		printf("queue device: 0x%llx\n",
		       (unsigned long long)vc->queue_config.device);
		break;
	case VI_REG_OFFSET(device_status):
		printf("device_status: 0x%x\n", vc->device_status);
		if (vc->device_status == 0xf) {
			vc->config_event = 1;
			__sync_synchronize();
			mmio_write32(&regs->doorbell, peer_id << 16);
		}
		break;
	default:
		printf("unknown write transaction for %x\n",
		       vc->write_transaction);
		break;
	}

	__sync_synchronize();
	vc->write_transaction = 0;

	return 1;
}

//デバイス側のメインプログラムループ
while (state[peer_id] == VIRTIO_STATE_READY) {
			event = process_write_transaction();

			if (vc->device_status == 0xf) {
				event |= process_rx_queue();
				event |= process_tx_queue();
			}

			if (!event) {
				ret = poll(pollfd, 2, -1);
				if (ret < 0)
					error(1, errno, "poll failed");
				if (pollfd[1].revents & POLLIN)
					wait_for_interrupt(regs);
			}
		}

四、共有メモリデータ配置#

仮想シリアルポートの例

アドレスデータ機能
低アドレスstruct virtio_ivshmem_header設定初期化及びパラメータ同期用
vring[0]データ送信用
vring[1]データ受信用
高アドレス後続のアドレスは vring 記述子メッセージデータの保存に使用されます実際のデータ保存用

五、参考資料#

  1. https://kvm-forum.qemu.org/2019/KVM-Forum19_ivshmem2.pdf
  2. 深入浅出 vhostuser 伝送モデル | REXROCK
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。