背景知識

前篇文章已經建立了基於 Qemu 的 NVMe 用戶空間驅動環境,本文開始,基於 code 來分析此技術的細節。 在本文寫作的同時,提交了一個 Qemu NVMe 的 patch,修復在閱讀代碼中發現的問題,已經被作者 merge。

[PATCH] block/nvme: optimize the performance of nvme driver based on vfio-pci

patchwork.ozlabs.org/pa

為什麼要在用戶空間實現呢? 最主要的好處是用戶空間訪問設備,可以做輪詢(polling),響應更快

為了實現用戶空間 driver,第一步需要把設備從當前 OS 摘除,讓當前系統的內核驅動不去probe這個設備。

這個技術就是VFIO。有了它,用戶態就可以通過一個特殊的設備文件,直接訪問 PCI 設備了。

VFIO(Virtual Function IO)

又可以叫做:Versatile Framework for usespace IO,這個名字其實更準確些。 VFIO 是一個安全的用戶驅動框架。將PCI設備暴露到用戶態,使得用戶態可以操作DMA,寄存器,中斷等,這樣的好處是,可以在用戶態來支持新的設備,減少延遲,提高帶寬。

在VFIO之前,這些驅動程序必須要麼經歷整個開發周期,成為適當的上游 驅動程序,保持在upstream tree外,或使用UIO框架,它沒有IOMMU保護的概念,有限的中斷支持, 並需要root許可權才能訪問PCI配置等內容空間。

VFIO驅動程序框架打算統一這些,提供比UIO更安全,更有用的用戶空間驅動程序環境。

VFIO使用了 IOMMU,使得DMA的訪問相互隔離。

VFIO利用了容器類,可以包含一個或多個組。一個容器只需打開/dev/vfio/vfio 字元設備即可創建。

就其本身而言,容器提供的功能很少但鎖定了幾個版本和擴展查詢介面。

用戶需要將一個組添加到容器中以進行下一級別功能。為此,用戶首先需要識別與所需設備關聯的組。這可以使用以下示例中描述的sysfs鏈接。通過解除綁定來自主機驅動程序的設備並將其綁定到一個新的VFIO驅動程序VFIO組將在組中顯示為/dev/vfio/$GROUP,其中$GROUP是設備所屬的IOMMU組編號。如果IOMMU組包含多個設備,則每個設備都需要在操作之前綁定到VFIO驅動程序

$ lspci -nn
忽略其他的,只看 NVME 設備
0000:13:00.0 Non-Volatile memory controller [0108]: VMware Device [15ad:07f0]

0000:13:00.0 這個叫做 domain bus device func。
15ad:07f0 這個叫做 vendor device id。

$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
nvme0n1 259:0 0 96G 0 disk

$ echo "15ad 07f0" > /sys/bus/pci/devices/0000:13:00.0/driver/remove_id
$ echo "0000:13:00.0" > /sys/bus/pci/devices/0000:13:00.0/driver/unbind
$ lsblk

此時 lsblk 就看不到nvme 設備了。

SPDK裡面默認是使用 vfio-pci,當 iommu 不支持的話,就換成uio_pci_generic。 uio_pci_generic和 vfio-pci 一樣,都是透傳設備的。

可以通過判斷/sys/kernel/iommu_groups 是否為空來查看 iommu 支持情況。

下面是綁定uio_pic_generic的例子:

$ echo "15ad 07f0" > /sys/bus/pci/drivers/uio_pci_generic/new_id
$ echo "0000:13:00.0" > /sys/bus/pci/drivers/uio_pci_generic/bind

$ readlink -f /sys/bus/pci/devices/0000:13:00.0/iommu_group
/sys/devices/pci0000:00/0000:00:17.0/0000:13:00.0/iommu_group

查看 PCI(機器)的拓撲結構:

yum install hwloc -y
lstopo-no-graphics

IOMMU

IOMMU 其實是 Intel VT-d 和 AMD-Vi的更通用的一個名字。 VT-d 代表的是 Intel Virtualization Technology for Directed I/O 。

IOMMU 類似 MMU,MMU 的作用是將 CPU 看到的虛擬地址,映射成物理內存地址;IOMMU 的作用是將 device 的 IO 虛擬地址(IOVA),映射成內存地址。 如果沒有 IOMMU,那麼所有設備操作的是同一塊共享的低端的物理內存地址。

NVMe 的初始化

先來看一下驅動的數據結構:

static BlockDriver bdrv_nvme = {
.format_name = "nvme",
.protocol_name = "nvme",
.instance_size = sizeof(BDRVNVMeState),

.bdrv_parse_filename = nvme_parse_filename,
.bdrv_file_open = nvme_file_open,
.bdrv_close = nvme_close,
.bdrv_getlength = nvme_getlength,

.bdrv_co_preadv = nvme_co_preadv,
.bdrv_co_pwritev = nvme_co_pwritev,
.bdrv_co_flush_to_disk = nvme_co_flush,
.bdrv_reopen_prepare = nvme_reopen_prepare,

.bdrv_refresh_filename = nvme_refresh_filename,
.bdrv_refresh_limits = nvme_refresh_limits,

.bdrv_detach_aio_context = nvme_detach_aio_context,
.bdrv_attach_aio_context = nvme_attach_aio_context,

.bdrv_io_plug = nvme_aio_plug,
.bdrv_io_unplug = nvme_aio_unplug,

.bdrv_register_buf = nvme_register_buf,
.bdrv_unregister_buf = nvme_unregister_buf,
};

對於 QEMU 的塊驅動來說,有個BlockDriver的結構來描述塊設備的操作,實現了這一些函數指針,就可以支持或者模擬一個設備。

nvme_parse_filename: 解析 nvme://0000:00:04.0/1 這樣格式的地址。中間是pci地址,最後面的1是namespace id。

nvme_file_open: 通過調用nvme_init來初始化nvme設備,這個函數非常關鍵,NVMe設備通過PCI暴露給用戶態操作必須經過VFIO的初始化,才能被使用。

nvme_init 初始化VFIO包括以下操作: - VFIO的初始化; - NVMe spec 7.6.1 描述的初始化;

  1. 調用 qemu_vfio_open_pci 來打開設備。

這個device就是0000:00:04.0。最核心的調用在 qemu_vfio_init_pci 這個函數裡面。

簡化操作包括:
s->container = open("/dev/vfio/vfio", O_RDWR); // 創建容器
ioctl(s->container, VFIO_GET_API_VERSION) != VFIO_API_VERSION) // 檢查版本
ioctl(s->container, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU) // 檢查IOMMU

group_file = sysfs_find_group_file(device, errp); //查找device對應的group,這裡就是/dev/vfio/4
s->group = open(group_file, O_RDWR); // 打開/dev/vfio/4文件

ioctl(s->group, VFIO_GROUP_GET_STATUS, &group_status) // 測試group的狀態
ioctl(s->group, VFIO_GROUP_SET_CONTAINER, &s->container) // 添加group到container
ioctl(s->container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU) // enable IOMMU

ioctl(s->container, VFIO_IOMMU_GET_INFO, &iommu_info) // 獲取IOMMU 信息

// 獲取設備描述符,以後用來讀寫寄存器用,注意,這個fd不是在用戶空間通過open調用來獲取的。
s->device = ioctl(s->group, VFIO_GROUP_GET_DEVICE_FD, device)

ioctl(s->device, VFIO_DEVICE_GET_INFO, &device_info) //

/*
enum {
VFIO_PCI_BAR0_REGION_INDEX,
VFIO_PCI_BAR1_REGION_INDEX,
VFIO_PCI_BAR2_REGION_INDEX,
VFIO_PCI_BAR3_REGION_INDEX,
VFIO_PCI_BAR4_REGION_INDEX,
VFIO_PCI_BAR5_REGION_INDEX,
VFIO_PCI_ROM_REGION_INDEX,
VFIO_PCI_CONFIG_REGION_INDEX,
}
*/
s->config_region_info = (struct vfio_region_info) {
.index = VFIO_PCI_CONFIG_REGION_INDEX,
.argsz = sizeof(struct vfio_region_info),
};
// 獲取ROM REGIN的配置信息,每個設備都有一個256 B的配置地址空間,頭部的64B是一個統一的標準:
/*
#define PCI_STD_HEADER_SIZEOF 64
#define PCI_VENDOR_ID 0x00 /* 16 bits */
#define PCI_DEVICE_ID 0x02 /* 16 bits */
#define PCI_COMMAND 0x04 /* 16 bits */
#define PCI_COMMAND_IO 0x1 /* Enable response in I/O space */
#define PCI_COMMAND_MEMORY 0x2 /* Enable response in Memory space */
#define PCI_COMMAND_MASTER 0x4 /* Enable bus mastering */
#define PCI_COMMAND_SPECIAL 0x8 /* Enable response to special cycles */
#define PCI_COMMAND_INVALIDATE 0x10 /* Use memory write and invalidate */
#define PCI_COMMAND_VGA_PALETTE 0x20 /* Enable palette snooping */
#define PCI_COMMAND_PARITY 0x40 /* Enable parity checking */
#define PCI_COMMAND_WAIT 0x80 /* Enable address/data stepping */
#define PCI_COMMAND_SERR 0x100 /* Enable SERR */
#define PCI_COMMAND_FAST_BACK 0x200 /* Enable back-to-back writes */
#define PCI_COMMAND_INTX_DISABLE 0x400 /* INTx Emulation Disable */
*/
ioctl(s->device, VFIO_DEVICE_GET_REGION_INFO, &s->config_region_info)

// 初始化PCI的BAR0-5,獲取BAR0-5的信息
for (i = 0; i < 6; i++) {
ret = qemu_vfio_pci_init_bar(s, i, errp);
}

// enable bus master,就是修改上面的配置地址空間。
// 通過調用pread/pwrite(s->device),就在用戶空間操作PCI設備寄存器了。
ret = qemu_vfio_pci_read_config(s, &pci_cmd, sizeof(pci_cmd), PCI_COMMAND);

pci_cmd |= PCI_COMMAND_MASTER;
ret = qemu_vfio_pci_write_config(s, &pci_cmd, sizeof(pci_cmd), PCI_COMMAND);

qemu_vfio_pci_init_bar的內容如下:

s->bar_region_info[index] = (struct vfio_region_info) {
.index = VFIO_PCI_BAR0_REGION_INDEX + index,
.argsz = sizeof(struct vfio_region_info),
};
ioctl(s->device, VFIO_DEVICE_GET_REGION_INFO, &s->bar_region_info[index])

PCI BAR0-5的內容如下:

index: 0, offset: 0x0, size: 0x2000
index: 1, offset: 0x10000000000, size: 0x0
index: 2, offset: 0x20000000000, size: 0x0
index: 3, offset: 0x30000000000, size: 0x0
index: 4, offset: 0x40000000000, size: 0x1000
index: 5, offset: 0x50000000000, size: 0x0

上面的初始化有些複雜,但是這是基本操作,可以從 kernel 的docs裡面找到(kernel.org/doc/Document

  1. 調用 qemu_vfio_pci_map_bar 來映射寄存器。

size = 8192,這裡映射的只有BAR0的區域,也就是寄存器的地址。

s->regs = qemu_vfio_pci_map_bar(s->vfio, 0, 0, NVME_BAR_SIZE, errp);
實際的操作是:
p = mmap(NULL, MIN(size, s->bar_region_info[index].size - offset),
PROT_READ | PROT_WRITE, MAP_SHARED,
s->device, s->bar_region_info[index].offset + offset);

# map index: 0, offset: 0x0, size: 0x2000 map len: 0x2000, mapp return addr: 0x0x7fefd79f1000

這裡的bar_region_info是通過ioctl的VFIO_DEVICE_GET_REGION_INFO命令來獲取的。 regs的結構如下:

/* Memory mapped registers */
typedef volatile struct {
uint64_t cap;
uint32_t vs;
uint32_t intms;
uint32_t intmc;
uint32_t cc;
uint32_t reserved0;
uint32_t csts;
uint32_t nssr;
uint32_t aqa;
uint64_t asq;
uint64_t acq;
uint32_t cmbloc;
uint32_t cmbsz;
uint8_t reserved1[0xec0];
uint8_t cmd_set_specfic[0x100];
uint32_t doorbells[];
} QEMU_PACKED NVMeRegs;

  1. NVMe 硬體的初始化:

分為以下幾步:

1. Reset device to get a clean state.
s->regs->cc = cpu_to_le32(le32_to_cpu(s->regs->cc) & 0xFE);
2. 等待reset完成,s->regs->csts = 0。
while (le32_to_cpu(s->regs->csts) & 0x1) {
}
3. 創建admin queue pair。
4. 配置aqa,asq,acq。
s->regs->aqa = cpu_to_le32((NVME_QUEUE_SIZE << 16) | NVME_QUEUE_SIZE);
s->regs->asq = cpu_to_le64(s->queues[0]->sq.iova);
s->regs->acq = cpu_to_le64(s->queues[0]->cq.iova);
5. enable 設備
s->regs->cc = cpu_to_le32((ctz32(NVME_CQ_ENTRY_BYTES) << 20) |
(ctz32(NVME_SQ_ENTRY_BYTES) << 16) |
0x1);
6.等待enable完成。
while (!(le32_to_cpu(s->regs->csts) & 0x1)) {
}

7. 註冊中斷處理程序
通過註冊eventfd事件,來從內核態通知用戶態來處理。

ret = qemu_vfio_pci_init_irq(s->vfio, &s->irq_notifier,
VFIO_PCI_MSIX_IRQ_INDEX, errp);
// 設置IO read的處理handler,以及IO polling的處理handler,IO polling是Qemu支持的另一種IO處理形式,也是當前社區比較流行的方式,不通過事件驅動,而是busy polling,或者adaptive polling。
aio_set_event_notifier(bdrv_get_aio_context(bs), &s->irq_notifier,
false, nvme_handle_event, nvme_poll_cb);

qemu_vfio_pci_init_irq的操作如下:

// 查詢eventfd的支持情況
ioctl(s->device, VFIO_DEVICE_GET_IRQ_INFO, &irq_info)

// 將eventfd的fd傳遞到內核態
*irq_set = (struct vfio_irq_set) {
.argsz = irq_set_size,
.flags = VFIO_IRQ_SET_DATA_EVENTFD | VFIO_IRQ_SET_ACTION_TRIGGER,
.index = irq_info.index,
.start = 0,
.count = 1,
};

*(int *)&irq_set->data = event_notifier_get_fd(e);
r = ioctl(s->device, VFIO_DEVICE_SET_IRQS, irq_set);

8. 發送 Identify 命令。
發送命令需要以下幾步:
- 構造NvmeCmd對象;
- 分配response內存;
- response host內存需要經過vfio dma map到IO地址空間,然後一起發下去。
- cmd.prp1 = response 的IO地址

NvmeCmd cmd = {
.opcode = NVME_ADM_CMD_IDENTIFY,
.cdw10 = cpu_to_le32(0x1),
};
r = qemu_vfio_dma_map(s->vfio, resp, sizeof(NvmeIdCtrl), true, &iova);
cmd.prp1 = cpu_to_le64(iova);

if (nvme_cmd_sync(bs, s->queues[0], &cmd)) {
error_setg(errp, "Failed to identify controller");
goto out;
}

cmd.cdw10 = 0;
cmd.nsid = cpu_to_le32(namespace);
if (nvme_cmd_sync(bs, s->queues[0], &cmd)) {
error_setg(errp, "Failed to identify namespace");
goto out;
}

9. 創建IO queue pair.
q = nvme_create_queue_pair(bs, n, queue_size, errp);
if (!q) {
return false;
}
cmd = (NvmeCmd) {
.opcode = NVME_ADM_CMD_CREATE_CQ,
.prp1 = cpu_to_le64(q->cq.iova),
.cdw10 = cpu_to_le32(((queue_size - 1) << 16) | (n & 0xFFFF)),
.cdw11 = cpu_to_le32(0x3),
};
if (nvme_cmd_sync(bs, s->queues[0], &cmd)) {
error_setg(errp, "Failed to create io queue [%d]", n);
nvme_free_queue_pair(bs, q);
return false;
}
cmd = (NvmeCmd) {
.opcode = NVME_ADM_CMD_CREATE_SQ,
.prp1 = cpu_to_le64(q->sq.iova),
.cdw10 = cpu_to_le32(((queue_size - 1) << 16) | (n & 0xFFFF)),
.cdw11 = cpu_to_le32(0x1 | (n << 16)),
};
if (nvme_cmd_sync(bs, s->queues[0], &cmd)) {
error_setg(errp, "Failed to create io queue [%d]", n);
nvme_free_queue_pair(bs, q);
return false;
}
s->queues = g_renew(NVMeQueuePair *, s->queues, n + 1);
s->queues[n] = q;
s->nr_queues++;
return true;

nvme_create_queue_pair的實現如下:
// 分配requests 內存,NVME_QUEUE_SIZE=128
q->prp_list_pages = qemu_blockalign0(bs, s->page_size * NVME_QUEUE_SIZE);
// 分配IO地址空間,建立於host內存的prp對應的固定映射,只有映射之後,iova地址才能傳遞給VFIO。
r = qemu_vfio_dma_map(s->vfio, q->prp_list_pages,
s->page_size * NVME_QUEUE_SIZE,
false, &prp_list_iova);
if (r) {
goto fail;
}
// 每一個req,對應一個prp page。
for (i = 0; i < NVME_QUEUE_SIZE; i++) {
NVMeRequest *req = &q->reqs[i];
req->cid = i + 1;
req->prp_list_page = q->prp_list_pages + i * s->page_size;
req->prp_list_iova = prp_list_iova + i * s->page_size;
}

// 初始化submission queue,隊列長度是size,每一個entry的長度是64B
nvme_init_queue(bs, &q->sq, size, NVME_SQ_ENTRY_BYTES, &local_err);
if (local_err) {
error_propagate(errp, local_err);
goto fail;
}
// 每個queue pair的sq/cq都有一個doorbell,地址是寄存器經過PCI BAR0內存映射的地址。

q->sq.doorbell = &s->regs->doorbells[idx * 2 * s->doorbell_scale];

// 初始化completion queue,隊列長度是size,每一個entry的長度是16B
nvme_init_queue(bs, &q->cq, size, NVME_CQ_ENTRY_BYTES, &local_err);
if (local_err) {
error_propagate(errp, local_err);
goto fail;
}
q->cq.doorbell = &s->regs->doorbells[idx * 2 * s->doorbell_scale + 1];

nvme_init_queue的操作如下:
// 隊列空
q->head = q->tail = 0;
// 分配entry的內存
q->queue = qemu_try_blockalign0(bs, bytes);
// 分配IO地址空間,建立host內存的對應的固定映射,只有映射之後,iova地址才能傳遞給VFIO。
r = qemu_vfio_dma_map(s->vfio, q->queue, bytes, false, &q->iova);

至此,NVMe 用戶驅動的初始化工作就做完了。 接下來就是分析IO路徑了。 主要有: - 下發的讀寫 IO - IO完成的中斷處理

讀寫 IO

寫 IO 的入口:nvme_co_pwritev - 首先分配buf的host內存,將來傳給VFIO用來讀寫。 - 初始化 QEMUIOVector,並將這段buf,加入到vector里。 - 需要映射vector的buf到iova空間。 - 需要將用戶態的內容,拷貝到分配的buf里。 - 構造NVMe命令,並下發命令

// 只有一個IO queue,夠用
NVMeQueuePair *ioq = s->queues[1];
NVMeRequest *req;
// NVMe spec 定義
uint32_t cdw12 = (((bytes >> BDRV_SECTOR_BITS) - 1) & 0xFFFF) |
(flags & BDRV_REQ_FUA ? 1 << 30 : 0);
// 構造讀或者寫命令
NvmeCmd cmd = {
.opcode = is_write ? NVME_CMD_WRITE : NVME_CMD_READ,
.nsid = cpu_to_le32(s->nsid),
.cdw10 = cpu_to_le32((offset >> BDRV_SECTOR_BITS) & 0xFFFFFFFF),
.cdw11 = cpu_to_le32(((offset >> BDRV_SECTOR_BITS) >> 32) & 0xFFFFFFFF),
.cdw12 = cpu_to_le32(cdw12),
};
NVMeCoData data = {
.ctx = bdrv_get_aio_context(bs),
.ret = -EINPROGRESS,
};

assert(s->nr_queues > 1);
// 初始化的時候,每一個queue pair都分配並且映射好了reqs。
req = nvme_get_free_req(ioq);
assert(req);

qemu_co_mutex_lock(&s->dma_map_lock);
// 映射需要寫入的數據到iova空間,並打包成cmd
r = nvme_cmd_map_qiov(bs, &cmd, req, qiov);
qemu_co_mutex_unlock(&s->dma_map_lock);
if (r) {
req->busy = false;
return r;
}
// 提交命令,回調函數是 nvme_rw_cb,回調的功能是讓coroutine繼續走。
nvme_submit_command(s, ioq, req, &cmd, nvme_rw_cb, &data);

data.co = qemu_coroutine_self();
while (data.ret == -EINPROGRESS) {
qemu_coroutine_yield();
}

qemu_co_mutex_lock(&s->dma_map_lock);

// 完成IO之後,釋放臨時映射
r = nvme_cmd_unmap_qiov(bs, qiov);

nvme_get_free_req的定義如下
// 首先判斷是否滿了,慢的條件是q->inflight + q->need_kick >= NVME_QUEUE_SIZE - 1。
// 需要流出一個槽位,是因為head=tail表示空。
// 如果滿了,且在coroutine中,就可以yield了,否則返回空。
while (q->inflight + q->need_kick > NVME_QUEUE_SIZE - 2) {
/* We have to leave one slot empty as that is the full queue case (head
* == tail + 1). */
if (qemu_in_coroutine()) {
trace_nvme_free_req_queue_wait(q);
qemu_co_queue_wait(&q->free_req_queue, &q->lock);
} else {
qemu_mutex_unlock(&q->lock);
return NULL;
}
}
// 遍歷,找到空閑的req。
for (i = 0; i < NVME_QUEUE_SIZE; i++) {
if (!q->reqs[i].busy) {
q->reqs[i].busy = true;
req = &q->reqs[i];
break;
}
}

nvme_cmd_map_qiov 的功能就是將需要寫入的數據(可能很大),拆分成多個page,
進行dma映射,其實現如下:

BDRVNVMeState *s = bs->opaque;
uint64_t *pagelist = req->prp_list_page;
int i, j, r;
int entries = 0;

assert(qiov->size);
assert(QEMU_IS_ALIGNED(qiov->size, s->page_size));
assert(qiov->size / s->page_size <= s->page_size / sizeof(uint64_t));
//遍歷iovs
for (i = 0; i < qiov->niov; ++i) {
bool retry = true;
uint64_t iova;
try_map:
// 建立DMA 臨時映射。
// 注意,IO 數據是臨時映射,因為IO完成後,就不需要了;
// 而IO queue pair的entries,以及 prp_list_pages 這些需要固定映射,在生命周期里
// 都必須是有效的,不能釋放。
// req->prp_list_page 指向的是q->prp_list_pages 的一頁,
// 所以最大的page數量就是 s->page_size / sizeof(uint64_t),存放一個64地址.
// 相當於二級索引。可以計算出,但個寫IO的最大大小是 4k / 8 * 4k = 2M

r = qemu_vfio_dma_map(s->vfio,
qiov->iov[i].iov_base,
qiov->iov[i].iov_len,
true, &iova);
if (r == -ENOMEM && retry) {
retry = false;
trace_nvme_dma_flush_queue_wait(s);
if (s->dma_map_count) {
trace_nvme_dma_map_flush(s);
qemu_co_queue_wait(&s->dma_flush_queue, &s->dma_map_lock);
} else {
r = qemu_vfio_dma_reset_temporary(s->vfio);
if (r) {
goto fail;
}
}
goto try_map;
}
if (r) {
goto fail;
}

// 映射好後,需要將 iova 的地址,放到req的 page 裡面。
for (j = 0; j < qiov->iov[i].iov_len / s->page_size; j++) {
pagelist[entries++] = iova + j * s->page_size;
}
trace_nvme_cmd_map_qiov_iov(s, i, qiov->iov[i].iov_base,
qiov->iov[i].iov_len / s->page_size);
}

s->dma_map_count += qiov->size;

assert(entries <= s->page_size / sizeof(uint64_t));
switch (entries) {
case 0:
abort();
case 1:
cmd->prp1 = cpu_to_le64(pagelist[0]);
cmd->prp2 = 0;
break;
case 2:
// 如果兩頁可以放下
cmd->prp1 = cpu_to_le64(pagelist[0]);
cmd->prp2 = cpu_to_le64(pagelist[1]);;
break;
default:
// 需要多頁,由於cmd只有兩個變數可以放,這裡就需要使用到二級索引了。
// prp1存放第一個buf page的iova
// prp2存放指向第二個page的iova的地址
// xxxx prp1: 549754757120, prp2: 602112
// xxxxx prp 0 ... : 549754761216
// xxxxx prp 1 ... : 549754765312
// xxxxx prp 2 ... : 549754769408
// xxxxx prp 3 ... : 549754773504

cmd->prp1 = cpu_to_le64(pagelist[0]);
cmd->prp2 = cpu_to_le64(req->prp_list_iova);
for (i = 0; i < entries - 1; ++i) {
pagelist[i] = cpu_to_le64(pagelist[i + 1]);
}
pagelist[entries - 1] = 0;
break;
}

nvme_submit_command 提交命令的實現如下:

req->cb = cb;
req->opaque = opaque;
cmd->cid = cpu_to_le32(req->cid);

// 將 cmd 插入到queue pair的submission queue的隊列尾部。

qemu_mutex_lock(&q->lock);
memcpy((uint8_t *)q->sq.queue +
q->sq.tail * NVME_SQ_ENTRY_BYTES, cmd, sizeof(*cmd));
// 隊列tail++
q->sq.tail = (q->sq.tail + 1) % NVME_QUEUE_SIZE;
// 已經寫入++
q->need_kick++;
nvme_kick(s, q);
nvme_process_completion(s, q);
qemu_mutex_unlock(&q->lock);

數據放到了NVMe的submission queue之後,需要去kick NVMe 控制器去消費數據,
nvme_kick的實現如下:

if (s->plugged || !q->need_kick) {
return;
}
trace_nvme_kick(s, q->index);
assert(!(q->sq.tail & 0xFF00));
/* Fence the write to submission queue entry before notifying the device. */
smp_wmb();
// 每一個queue pair的submission/completion queue,都有一個doorbell,
// 這裡就是寫這個將新的submission queue的tail寫入doorbeel寄存器
*q->sq.doorbell = cpu_to_le32(q->sq.tail);
// 此時IO才相當於發出去了,inflight 統計發出去的IO數量;
q->inflight += q->need_kick;
// 已經kick了,清0
q->need_kick = 0;

讀 IO 邏輯同上,就不重複分析了。

IO 完成的處理,以及 IOVA空間的管理,見下篇。
推薦閱讀:
查看原文 >>
相关文章