系列文章 : [6.1810] 跟著 MIT 6.1810 學習基礎作業系統觀念
{ 5.2.6 Device Operation }
// the format of the first descriptor in a disk request.
// to be followed by two more descriptors containing
// the block, and a one-byte status.
struct virtio_blk_req {
uint32 type; // VIRTIO_BLK_T_IN or ..._OUT
uint32 reserved;
uint64 sector;
};
想送給 VirtIO block device ( disk ) 的 request。
完整的 request 還需要包含 data, 以及 1 byte 的 status,在 xv6-riscv 實作中,因為 virtio_blk_req, data 跟 status 在 physical address 上不連續,所以拆分成三個 descriptor。
2 ( 1024 / 512 ) 個 sector。{ 2.4.5 The Virtqueue Descriptor Table }
// a single descriptor, from the spec.
struct virtq_desc {
uint64 addr;
uint32 len;
uint16 flags;
uint16 next;
};
描述要傳遞給 VirtIO device 的資料。
virtio_blk_req 的 data 進行寫入 ( 以 Guest OS 的角度而言,是從 disk 讀取資料 )。virtio_blk_req 的 data 進行讀取 ( 以 Guest OS 的角度而言,是對 disk 寫入資料 )descriptor chain。以 Guest OS 的角度而言,透過 desc 傳遞的資料在 physical address 可能是不連續的,但對於接收資料的 VirtIO device 而言,只要是在同一個 desctipor chain 的資料,會是連續的。
{ 2.4.6 The Virtqueue Available Ring }
// the (entire) avail ring, from the spec.
struct virtq_avail {
uint16 flags; // always zero
uint16 idx; // driver will write ring[idx] next
uint16 ring[NUM]; // descriptor numbers of chain heads
uint16 unused;
};
driver 把想要交給 VirtIO device 的 descriptor ( virtq_desc ) 的資訊放在這裡。
disk.desc 陣列的 index,而是自己的 ring ( disk.avail->ring ) 的 index。driver 寫入的,遵守嚴格遞增,不需要 modulo queue size。當我們將 idx + 1 時候,是告訴 VirtIO device,這邊有一個新的,可取用的 virtq_avail ring entry。disk.desc 陣列的 index。假如有使用 descriptor chain 的話,只需要放 descriptor chain 的 head 的 index。{ 2.4.8 The Virtqueue Used Ring }
struct virtq_used {
uint16 flags; // always zero
uint16 idx; // device increments when it adds a ring[] entry
struct virtq_used_elem ring[NUM];
};
當 VirtIO device 處理完 descriptor 之後,會把處理好的 descriptor 的資訊存放在這裡。
virtq_used_elem,放進 ring[idx],並將 idx + 1。{ 2.4.8 The Virtqueue Used Ring }
// one entry in the "used" ring, with which the
// device tells the driver about completed requests.
struct virtq_used_elem {
uint32 id; // index of start of completed descriptor chain
uint32 len;
};
#define VIRTIO_MMIO_MAGIC_VALUE 0x000 // 0x74726976
#define VIRTIO_MMIO_VERSION 0x004 // version; should be 2
#define VIRTIO_MMIO_DEVICE_ID 0x008 // device type; 1 is net, 2 is disk
#define VIRTIO_MMIO_VENDOR_ID 0x00c // 0x554d4551
#define VIRTIO_MMIO_DEVICE_FEATURES 0x010
#define VIRTIO_MMIO_DRIVER_FEATURES 0x020
#define VIRTIO_MMIO_QUEUE_SEL 0x030 // select queue, write-only
#define VIRTIO_MMIO_QUEUE_NUM_MAX 0x034 // max size of current queue, read-only
#define VIRTIO_MMIO_QUEUE_NUM 0x038 // size of current queue, write-only
#define VIRTIO_MMIO_QUEUE_READY 0x044 // ready bit
#define VIRTIO_MMIO_QUEUE_NOTIFY 0x050 // write-only
#define VIRTIO_MMIO_INTERRUPT_STATUS 0x060 // read-only
#define VIRTIO_MMIO_INTERRUPT_ACK 0x064 // write-only
#define VIRTIO_MMIO_STATUS 0x070 // read/write
#define VIRTIO_MMIO_QUEUE_DESC_LOW 0x080 // physical address for descriptor table, write-only
#define VIRTIO_MMIO_QUEUE_DESC_HIGH 0x084
#define VIRTIO_MMIO_DRIVER_DESC_LOW 0x090 // physical address for available ring, write-only
#define VIRTIO_MMIO_DRIVER_DESC_HIGH 0x094
#define VIRTIO_MMIO_DEVICE_DESC_LOW 0x0a0 // physical address for used ring, write-only
#define VIRTIO_MMIO_DEVICE_DESC_HIGH 0x0a4
各個 VirtIO memory-mapped register 的 offset。
詳情可以參考
#define VIRTIO_CONFIG_S_ACKNOWLEDGE 1
#define VIRTIO_CONFIG_S_DRIVER 2
#define VIRTIO_CONFIG_S_DRIVER_OK 4
#define VIRTIO_CONFIG_S_FEATURES_OK 8
可以參考 VirtIO spec 的 { 2.1 Device Status Field }
// device feature bits
#define VIRTIO_BLK_F_RO 5 /* Disk is read-only */
#define VIRTIO_BLK_F_SCSI 7 /* Supports scsi command passthru */
#define VIRTIO_BLK_F_CONFIG_WCE 11 /* Writeback mode available in config */
#define VIRTIO_BLK_F_MQ 12 /* support more than one vq */
#define VIRTIO_F_ANY_LAYOUT 27
#define VIRTIO_RING_F_INDIRECT_DESC 28
#define VIRTIO_RING_F_EVENT_IDX 29
可以參考 VirtIO spec 的 { 5.2.3 Feature bits }
// this many virtio descriptors.
// must be a power of two.
#define NUM 8
xv6-riscv 預期會使用的 virtioqueue entry 數量。
#define VRING_DESC_F_NEXT 1 // chained with another descriptor
#define VRING_DESC_F_WRITE 2 // device writes (vs read)
可以參考 VirtIO spec 的 { 2.4.5 The Virtqueue Descriptor Table }
virtio_blk_req 的 data 進行寫入 ( 以 Guest OS 的角度而言,是從 disk 讀取資料 )。virtio_blk_req 的 data 進行讀取 ( 以 Guest OS 的角度而言,是對 disk 寫入資料 )#define VIRTIO_BLK_T_IN 0 // read the disk
#define VIRTIO_BLK_T_OUT 1 // write the disk
可以參考 VirtIO spec 的 { 5.2.6 Device Operation }
總結一下 virtio_blk_req virtq_desc, virtq_avail, virtq_used, virtq_used_elem
virtq_desc, virtq_avail, virtq_used, virtq_used_elem 這四個 structure 代表 driver 該怎麼傳遞資訊給 VirtIO device ( 無關乎 Device Types, e.g. network card, block device … )
virtq_desc 本身會是一個陣列 ( 有多個 virtq_desc ),而 virtq_avail, virtq_used 則是每個 virtioqueue 會有一個,且裡面會有一個 ring buffer。
virtq_used_elem 是 virtq_used 的 ring buffer 的 element。
virtq_avail 跟 virtq_used 的 idx 欄位,並不是指 virtq_desc 陣列的 index,而是自己 ring buffer 的 index。
virtq_avail 的 ring buffer 的 element 是 virtq_desc 的 index
virtq_used 的 ring buffer 的 element 是 virtq_used_elem
virtq_used_elem 的 id 指的是 virtq_desc 陣列的 index
這邊簡述一下 driver 要傳資訊給 VirtIO device 的流程。
virtq_desc ( descriptor ) 會是多個 descriptor 組成一個陣列。
driver 把想要傳給 Virtio Device 的資料整理進一個或多個 virtq_desc 裡面。因為我們目前想使用的是 Virtio Block Device,所以要傳給 VirtIO Device 的資料是 virtio_blk_req ( 包含 type, sector, data, status)。
在使用 descriptor 之前,會需要先向系統 allocate 在 virtq_desc 陣列裡空閒的 descriptor。假如目前沒有空閒的 descriptor,我們可能需要陷入睡眠來等待。
以 xv6-riscv 的實作而言,type, sector 會包在第一個 descriptor, data 會在第二個 descriptor,status 會在第三個 descriptor,並用 VIRTQ_DESC_F_NEXT flag 把它們串接起來 ( descriptor chain )。
將 descriptor chain 的第一個 descriptor 在 virtq_desc 陣列裡面的 index,令其為 N。
把 N 放進 virtq_avail 的 ring 陣列裡面,並更新 idx。這表示正式將這些 virtq_desc 提供給 VirtIO device。
把 virtioqueue 的 index 寫入 QueueNotify register,以此來通知 VirtIO Device。
VirtIO device 收到來自 driver 的通知後,會去讀取 virtq_avail 得知有新的請求。 VirtIO device 會去 virtq_avail.ring 拿取 virtq_desc 的 index ( 假如有使用 descriptor chain 的話,就會去拿鏈上的第一個 virtq_desc ),並根據 descriptor 進行資料的讀寫。
當 VirtIO device 完成請求後,會把 virt_desc 陣列的 index,以及 VirtIO device 實際處理的資料的長度放在 virtq_used_elem,並把 virtq_used_elem 放進 virtq_used.ring,VirtIO device 會更新 virtq_used.idx。最後, VirtIO device 會發出中斷給 CPU 。
Driver 在收到來自 VirtIO device 的中斷之後,會去看 virtq_used.ring 裡面的 virtq_used_mem.id,這個 id 表示已經被 VirtIO device 處理完的 descriptor,在 virtq_desc 陣列裡的 index。取得這個 index 之後,就可以去釋放相對應的 virtq_desc,使其成為空閒的 descriptor,供後續的請求重複使用。
virtio_blk_req 則是 VirtIO block device 特有的 structure,當我們想從 block device 讀取或是寫入資料的時候,要給 VirtIO block device virtio_blk_req 這個資訊。
傳遞 virtio_blk_req 給 VirtIO block device 的方式,跟傳遞給任何 VirtIO device 的方式一樣, driver 都需要透過 virtq_desc, virtq_avail 給 VirtIO device 資訊,driver 並透過 virtq_used, virtq_used_elem 讀取來自 VirtIO device 的回覆。