系列文章 : [6.1810] 跟著 MIT 6.1810 學習基礎作業系統觀念
這個 function 會對 virtio disk 進行初始化,所以我們可以先看看 spec 上怎麼定義初始化的順序。
{ 4.2.3.1.1 Driver Requirements: Device Initialization }
{ 3.1.1 Driver Requirements: Device Initialization }
驅動程式需要依照下列的順序來初始化一個 VirtIO device
Status register 寫入 0,可以 reset 這個 deviceDeviceFeatures register ( device feature bits ),並且寫入 Guest OS 可以支援的,該群 bits 的 subset。在過程中,guest OS 的 driver 可能會去讀取 ( 但絕不能寫入 ) device-specific configuration ( Configuration space )。network card, block device, console … 等等假如上述的步驟有錯的話,guest OS driver 必須要設定 FAILED status bits { 2.1 Device Status Field },表示 guest OS 已經放棄這個 VirtIO device 了, driver 不可以繼續初始化這個 device。
{ 4.2.3.2 Virtqueue Configuration }
The driver will typically initialize the virtual queue in the following way:
QueueSel register,藉此來選擇想要設定的 queue。QueueReady, and expect a returned value of zero (0x0).QueueNumMax. If the returned value is zero (0x0) the queue is not available.QueueNum.Descriptor Table, Available Ring and Used Ring to (respectively) the QueueDescLow/QueueDescHigh, QueueAvailLow/QueueAvailHigh and QueueUsedLow/QueueUsedHigh register pairs.QueueReady.對於 VirtIO device 的 memory-mapped register 的模擬,可以參考 QEMU 的程式碼。對照 VirtIO device 的 spec 以及 QEMU 的程式碼,可能也是個有趣的學習經驗。
https://github.com/qemu/qemu/blob/stable-10.0/hw/virtio/virtio-mmio.c
void
virtio_disk_init(void)
{
uint32 status = 0;
這個變數的意義是 VirtIO 裡的 Device Status。
( VirtIO spec : 2.1 Device Status Field )
initlock(&disk.vdisk_lock, "virtio_disk");
確保每次只有一個 CPU 可以控制,送 command 給這個 VirtIO blk device。
if(*R(VIRTIO_MMIO_MAGIC_VALUE) != 0x74726976 ||
*R(VIRTIO_MMIO_VERSION) != 2 ||
*R(VIRTIO_MMIO_DEVICE_ID) != 2 ||
*R(VIRTIO_MMIO_VENDOR_ID) != 0x554d4551){
panic("could not find virtio disk");
}
這邊會去讀取一些 MMIO ( memory-mapped I/O address ),跟這個裝置溝通,並確定我們是在跟正確的 device 對話。
0x74726976
virt
block device ( a hard disk ),所以會是 2。 // reset device
*R(VIRTIO_MMIO_STATUS) = status;
Device Initialization 步驟 1 : Reset the device
// set ACKNOWLEDGE status bit
status |= VIRTIO_CONFIG_S_ACKNOWLEDGE;
*R(VIRTIO_MMIO_STATUS) = status;
Device Initialization 步驟 2 : Set the ACKNOWLEDGE status bit: the guest OS has noticed the device.
// set DRIVER status bit
status |= VIRTIO_CONFIG_S_DRIVER;
*R(VIRTIO_MMIO_STATUS) = status;
Device Initialization 步驟 3 : Set the DRIVER status bit: the guest OS knows how to drive the device.
// negotiate features
uint64 features = *R(VIRTIO_MMIO_DEVICE_FEATURES);
Device Initialization 步驟 4 : 閱讀 DeviceFeatures register ( device feature bits )
features &= ~(1 << VIRTIO_BLK_F_RO);
features &= ~(1 << VIRTIO_BLK_F_SCSI);
features &= ~(1 << VIRTIO_BLK_F_CONFIG_WCE);
features &= ~(1 << VIRTIO_BLK_F_MQ);
features &= ~(1 << VIRTIO_F_ANY_LAYOUT);
features &= ~(1 << VIRTIO_RING_F_EVENT_IDX);
features &= ~(1 << VIRTIO_RING_F_INDIRECT_DESC);
這邊會把 xv6-riscv 不支援的 features 關掉。
*R(VIRTIO_MMIO_DRIVER_FEATURES) = features;
Device Initialization 步驟 4 : 寫入 Guest OS 可以支援的,該群 bits 的 subset。
// tell device that feature negotiation is complete.
status |= VIRTIO_CONFIG_S_FEATURES_OK;
*R(VIRTIO_MMIO_STATUS) = status;
Device Initialization 步驟 5 : Set the FEATURES_OK status bit. The driver MUST NOT accept new feature bits after this step.
// re-read status to ensure FEATURES_OK is set.
status = *R(VIRTIO_MMIO_STATUS);
if(!(status & VIRTIO_CONFIG_S_FEATURES_OK))
panic("virtio disk FEATURES_OK unset");
Device Initialization 步驟 6 : Re-read device status to ensure the FEATURES_OK bit is still set: otherwise, the device does not support our subset of features and the device is unusable.
// initialize queue 0.
*R(VIRTIO_MMIO_QUEUE_SEL) = 0;
Device Initialization 步驟 7.
Virtqueue Configuration 步驟 1 : Select the queue writing its index (first queue is 0) to QueueSel.
一個 VirtIO device 可能會有多個 queue。而這個 register 就是來讓我們可以選擇目前我們想設定的是哪一個 queue。
在這裡,我們想設定的是第 0 個 queue ( queue 0 )
// ensure queue 0 is not in use.
if(*R(VIRTIO_MMIO_QUEUE_READY))
panic("virtio disk should not be ready");
Device Initialization 步驟 7.
Virtqueue Configuration 步驟 2 : Check if the queue is not already in use: read QueueReady, and expect a returned value of zero (0x0)
因為我們剛 reset 這個 device,所以 ready bit 必須要是 0。
// check maximum queue size.
uint32 max = *R(VIRTIO_MMIO_QUEUE_NUM_MAX);
if(max == 0)
panic("virtio disk has no queue 0");
if(max < NUM)
panic("virtio disk max queue too short");
Device Initialization 步驟 7.
Virtqueue Configuration 步驟 3 : Read maximum queue size (number of elements) from QueueNumMax. If the returned value is zero (0x0) the queue is not available.
VIRTIO_MMIO_QUEUE_NUM_MAX 讀取出來的值是 0,表示該 queue 不存在,需要發 panic // allocate and zero queue memory.
disk.desc = kalloc();
disk.avail = kalloc();
disk.used = kalloc();
if(!disk.desc || !disk.avail || !disk.used)
panic("virtio disk kalloc");
memset(disk.desc, 0, PGSIZE);
memset(disk.avail, 0, PGSIZE);
memset(disk.used, 0, PGSIZE);
Device Initialization 步驟 7.
Virtqueue Configuration 步驟 4 : Allocate and zero the queue pages, making sure the memory is physically contiguous. It is recommended to align the Used Ring to an optimal boundary (usually the page size).
把一些跟 Guest OS 以及 VirtIO device 共享的資料歸 0,每一個 virtqueue 包含以下三個 data structure。
// set queue size.
*R(VIRTIO_MMIO_QUEUE_NUM) = NUM;
Device Initialization 步驟 7.
Virtqueue Configuration 步驟 5 : Notify the device about the queue size by writing the size to QueueNum.
告訴 VirtIO device,xv6-riscv ( guest OS ) 會使用 virtqueue 裡面的 NUM 個 entry。
// write physical addresses.
*R(VIRTIO_MMIO_QUEUE_DESC_LOW) = (uint64)disk.desc;
*R(VIRTIO_MMIO_QUEUE_DESC_HIGH) = (uint64)disk.desc >> 32;
*R(VIRTIO_MMIO_DRIVER_DESC_LOW) = (uint64)disk.avail;
*R(VIRTIO_MMIO_DRIVER_DESC_HIGH) = (uint64)disk.avail >> 32;
*R(VIRTIO_MMIO_DEVICE_DESC_LOW) = (uint64)disk.used;
*R(VIRTIO_MMIO_DEVICE_DESC_HIGH) = (uint64)disk.used >> 32;
Device Initialization 步驟 7.
Virtqueue Configuration 步驟 6 : Write physical addresses of the queue’s Descriptor Table, Available Ring and Used Ring to (respectively) the QueueDescLow/QueueDescHigh, QueueAvailLow/QueueAvailHigh and QueueUsedLow/QueueUsedHigh register pairs.
// queue is ready.
*R(VIRTIO_MMIO_QUEUE_READY) = 0x1;
Device Initialization 步驟 7.
Virtqueue Configuration 步驟 7 : Write 0x1 to QueueReady.
寫 1 給 virtqueue 0 的 QUEUE_READY 是在告訴 VirtIO device, queue 0 已經設定好了,並且 ready to use。
// all NUM descriptors start out unused.
for(int i = 0; i < NUM; i++)
disk.free[i] = 1;
這部分跟 VirtIO device 硬體本身無關,是 guest OS 用自己的方法去追蹤哪一個 descriptor 是閒置的。
// tell the device we're completely ready.
status |= VIRTIO_CONFIG_S_DRIVER_OK;
*R(VIRTIO_MMIO_STATUS) = status;
// plic.c and trap.c arrange for interrupts from VIRTIO0_IRQ.
}
Device Initialization 步驟 8 : Set the DRIVER_OK status bit. At this point the device is “live”.
這表示 guest OS 的對 VirtIO device 的 driver 已經初始化完畢了,現在 VirtIO block device 可以開始接受 read/write requests 了。
// find a free descriptor, mark it non-free, return its index.
static int
alloc_desc()
{
for(int i = 0; i < NUM; i++){
if(disk.free[i]){
disk.free[i] = 0;
return i;
}
}
return -1;
}
找尋有沒有空閒的 disk.desc
// mark a descriptor as free.
static void
free_desc(int i)
{
if(i >= NUM)
panic("free_desc 1");
if(disk.free[i])
panic("free_desc 2");
disk.desc[i].addr = 0;
disk.desc[i].len = 0;
disk.desc[i].flags = 0;
disk.desc[i].next = 0;
disk.free[i] = 1;
釋放一個 descriptor,供後續的 request 使用。
wakeup(&disk.free[0]);
假如有人想要 alloc descriptor 失敗的話,就會陷入睡眠。
這邊會把陷入睡眠的 process 叫醒。
https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/virtio_disk.c#L186
從 desciprtor chain 的頭開始 free 掉一整個 descriptor chain。
alloc3_desc(int *idx)
因為 xv6-riscv 的設計是每次對 VirtIO block disk 的操作都需要三個 descriptor,所以這個 function 就是一次 alloc 三個 descriptor,並把取得的 index 放進 idx 陣列。
if(idx[i] < 0){
for(int j = 0; j < i; j++)
free_desc(idx[j]);
return -1;
}
這邊有個小巧思,就是當我們 alloc 失敗的時候,要把之前 alloc 成功的 descriptor 還回去。
void
virtio_disk_rw(struct buf *b, int write)
{
uint64 sector = b->blockno * (BSIZE / 512);
把 block number 轉換成 sector number。
acquire(&disk.vdisk_lock);
代表每次只有一個 CPU 可以進行 VirtIO block device 的操作。
// the spec's Section 5.2 says that legacy block operations use
// three descriptors: one for type/reserved/sector, one for the
// data, one for a 1-byte status result.
// allocate the three descriptors.
int idx[3];
while(1){
if(alloc3_desc(idx) == 0) {
break;
}
sleep(&disk.free[0], &disk.vdisk_lock);
}
嘗試 alloc 三個 descriptor,失敗的話就去睡覺。
當我們在 free descriptor 的時候,會被叫醒。
// format the three descriptors.
// qemu's virtio-blk.c reads them.
struct virtio_blk_req *buf0 = &disk.ops[idx[0]];
每次對 VirtIO block device 請求都會需要有一個 virtio_blk_req。
會將 virtio_blk_req 放在第一個 descriptor。
要注意的是,在 VirtIO spec 裡面,virtio_blk_req 還包含了 data 以及 status,但在 xv6-riscv 裡面,virtio_blk_req 沒有那兩樣東西。
xv6-riscv 會把 data 放在第二個 descriptor,並把 status 放在第三個 descriptor。
if(write)
buf0->type = VIRTIO_BLK_T_OUT; // write the disk
else
buf0->type = VIRTIO_BLK_T_IN; // read the disk
buf0->reserved = 0;
buf0->sector = sector;
把相對應的資訊放進 virtio_blk_req。
disk.desc[idx[0]].addr = (uint64) buf0;
disk.desc[idx[0]].len = sizeof(struct virtio_blk_req);
disk.desc[idx[0]].flags = VRING_DESC_F_NEXT;
disk.desc[idx[0]].next = idx[1];
第一個 descriptor 會放 xv6-riscv 的 virtio_blk_req。
disk.desc[idx[1]].addr = (uint64) b->data;
disk.desc[idx[1]].len = BSIZE;
if(write)
disk.desc[idx[1]].flags = 0; // device reads b->data
else
disk.desc[idx[1]].flags = VRING_DESC_F_WRITE; // device writes b->data
disk.desc[idx[1]].flags |= VRING_DESC_F_NEXT;
disk.desc[idx[1]].next = idx[2];
第二個 descriptor 會放 data
disk.info[idx[0]].status = 0xff; // device writes 0 on success
disk.desc[idx[2]].addr = (uint64) &disk.info[idx[0]].status;
disk.desc[idx[2]].len = 1;
disk.desc[idx[2]].flags = VRING_DESC_F_WRITE; // device writes the status
disk.desc[idx[2]].next = 0;
第三個 descriptor 會放 status
// record struct buf for virtio_disk_intr().
b->disk = 1;
disk.info[idx[0]].b = b;
disk.info[idx[0]].b = b
struct buf *b 記錄下來,讓我們可以在將來的 interrupt handler 中使用這個指標,並利用這個指標,把相對應的 process 喚醒 ( wake up )。 // tell the device the first index in our chain of descriptors.
disk.avail->ring[disk.avail->idx % NUM] = idx[0];
把 chain of descriptors 裡的第一個 descriptor 在 disk.desc 陣列裡的 index,放進 disk.avail 裡面,以此來通知 VirtIO block device,chain of descriptors 第一個 descriptor 是哪一個。
__sync_synchronize();
這個 memory barrier 可以防止 compiler 或是 CPU 把這些記憶體的寫入重新排序。它保證了對 disk.avail 的寫入,可以在 index 更新 ( 下一行 ) 之前發生,確保我們是寫入正確的 index。
// tell the device another avail ring entry is available.
disk.avail->idx += 1; // not % NUM ...
更新 disk.avail->idx,根據 VirtIO Spec 的描述,我們不可以對其取模 ( modulo )。
__sync_synchronize();
這個 memory barrier 可以保證,在我們 notify the queue 之前,我們已經完成了對 disk.avil->idx 的寫入。
*R(VIRTIO_MMIO_QUEUE_NOTIFY) = 0; // value is queue number
0 代表要通知 virtqueue 0,叫 virtqueue 0 起來做事了。看 disk.avail->ring 裡面,有沒有新的 descriptor 需要處理 ( 可以藉由 disk.avail->idx 得到這個資訊 ) 。有的話,就處理相對應的 descriptor。
// Wait for virtio_disk_intr() to say request has finished.
while(b->disk == 1) {
sleep(b, &disk.vdisk_lock);
}
等待 virtio_disk_intr() 的通知,也就是等待 VirtIO block device 處理相對應的請求。
假如 b->disk = 1 ( 表示 VirtIO Block Device 還在處理 ),就讓該 process 陷入睡眠。
disk.info[idx[0]].b = 0;
free_chain(idx[0]);
release(&disk.vdisk_lock);
}
一旦醒來了,表示 VirtIO block device 的請求已經被完成了,可以把資源釋放掉。
void
virtio_disk_intr()
{
acquire(&disk.vdisk_lock);
代表每次只有一個 CPU 可以進行 VirtIO block device 的操作。
// the device won't raise another interrupt until we tell it
// we've seen this interrupt, which the following line does.
// this may race with the device writing new entries to
// the "used" ring, in which case we may process the new
// completion entries in this interrupt, and have nothing to do
// in the next interrupt, which is harmless.
*R(VIRTIO_MMIO_INTERRUPT_ACK) = *R(VIRTIO_MMIO_INTERRUPT_STATUS) & 0x3;
當我們沒有 acknowledge interrupt 的話,這個 device 就不會發新的一發 interrupt。
這裡就是在做 acknowledge interrupt 的動作。
VIRTIO_MMIO_INTERRUPT_STATUS 可以知道目前發生 interrupt 的原因
VIRTIO_MMIO_INTERRUPT_ACK,表示我們 acknowledge 這個 interrupt 了,讓 device 可以發起下一發 interrupt 。 __sync_synchronize();
這個 memory barrier 確保我們對 interrupt 的 acknowledgement 已經傳給硬體之後,CPU 才嘗試對 disk 相關的資料進行讀取 ( disk.used, disk.used_idx … )。
// the device increments disk.used->idx when it
// adds an entry to the used ring.
while(disk.used_idx != disk.used->idx){
__sync_synchronize();
確保我們在使用 disk.used->ring 的資訊之前,其他的記憶體操作都已經完成了。
int id = disk.used->ring[disk.used_idx % NUM].id;
if(disk.info[id].status != 0)
panic("virtio_disk_intr status");
假如 VirtIO Block Device 對 status 寫入的值不是 0,代表這次的 request 是失敗的。
struct buf *b = disk.info[id].b;
從 disk.desc 陣列的 index ( id ) 取得相對應的 struct buf *b。
b->disk = 0; // disk is done with buf
這代表 VirtIO Block Device 已經完成了這個請求。
wakeup(b);
喚醒因 b ( channel 為 b ) 而睡覺的 process。
disk.used_idx += 1;
}
處理下一個 VirtIO Block Device 已經完成,但 driver 還沒處理的 request。