Add 08-doc and modified the device driver (On going...)

This commit is contained in:
ianchen0119
2021-06-24 17:53:37 +08:00
parent 07ab6ae0b3
commit 90199d7967
5 changed files with 380 additions and 91 deletions

View File

@@ -0,0 +1,252 @@
# 08-BlockDeviceDriver -- RISC-V 的嵌入式作業系統
在實現外部中斷的機制以後,我們已經在先前的 Lab 中加入了 UART 的 ISR ,為了讓作業系統能夠讀取磁碟資料,我們必須加入 Virtio 的 ISR :
```c=
void external_handler()
{
int irq = plic_claim();
if (irq == UART0_IRQ)
{
lib_isr();
}
else if (irq == VIRTIO_IRQ)
{
virtio_disk_isr();
}
else if (irq)
{
lib_printf("unexpected interrupt irq = %d\n", irq);
}
if (irq)
{
plic_complete(irq);
}
}
```
當然,在開始之前我們仍需要認識一下 VirtIO 協定。
## 先備知識: Virtio
### Descriptor
Descriptor 包含這些訊息: 地址,地址長度,某些 flag 和其他信息。
使用 Descriptor ,我們可以將設備指向 RAM 中任何緩衝區的內存位址。
```c=
struct virtq_desc
{
uint64 addr;
uint32 len;
uint16 flags;
uint16 next;
};
```
- addr: 我們可以在 64-bit 內存地址內的任何位置告訴設備存儲位置。
- len: 讓 Device 知道有多少內存可用。
- flags: 用於控制 descriptor 。
- next: 告訴 Device 下一個描述符的 Index 。如果指定了 VIRTQ_DESC_F_NEXT Device 僅讀取該字段。否則無效。
### AvailableRing
用來存放 Descriptor 的索引,當 Device 收到通知時,它會檢查 AvailableRing 確認需要讀取哪些 Descriptor 。
> 需要注意的是: Descriptor 和 AvailableRing 都存儲在 RAM 中。
```c=
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;
};
```
### UsedRing
UsedRing 讓 Device 能夠向 OS 發送訊息,因此, Device 通常使用它來告知 OS 它已完成先前通知的請求。
AvailableRing 與 UsedRing 非常相似,差別在於: OS 需要查看 UsedRing 得知哪個 Descriptor 已經被服務。
```c=
struct virtq_used_elem
{
uint32 id; // index of start of completed descriptor chain
uint32 len;
};
struct virtq_used
{
uint16 flags; // always zero
uint16 idx; // device increments when it adds a ring[] entry
struct virtq_used_elem ring[NUM];
};
```
## 發送讀寫請求
為了節省閱讀 Virtio Spec 的時間,本次的 Block Device Driver 大量閱讀並參考了 xv6-riscv 的實作,不過在 xv6 的檔案系統實作中有非常多層:
```
+------------------+
| File descriptor |
+------------------+
| Pathname |
+------------------+
| Directory |
+------------------+
| Inode |
+------------------+
| Logging |
+------------------+
| Buffer cache |
+------------------+
| Disk |
+------------------+
```
為了精簡,我們會將 Buffer 抽離,完成 Device Driver 後會更方便我們在日後實現檔案系統。
### 指定寫入的 Sector
```c=
uint64_t sector = b->blockno * (BSIZE / 512);
```
### 分配 descriptor
因為 [qemu-virt](https://github.com/qemu/qemu/blob/master/hw/block/virtio-blk.c) 會一次讀取 3 個 descriptor ,所以在發送請求之前我們要先分配好這些空間。
```c=
static int
alloc3_desc(int *idx)
{
for (int i = 0; i < 3; i++)
{
idx[i] = alloc_desc();
if (idx[i] < 0)
{
for (int j = 0; j < i; j++)
free_desc(idx[j]);
return -1;
}
}
return 0;
}
```
### 發送 Block request
宣告 req 的結構:
```c=
struct virtio_blk_req *buf0 = &disk.ops[idx[0]];
```
因為磁碟有讀寫操作之分,為了讓 qemu 知道要讀還是要寫,我們要在請求中的 `type` 成員中寫入 **flag** :
```c=
if(write)
buf0->type = VIRTIO_BLK_T_OUT; // write the disk
else
buf0->type = VIRTIO_BLK_T_IN; // read the disk
buf0->reserved = 0; // The reserved portion is used to pad the header to 16 bytes and move the 64-bit sector field to the correct place.
buf0->sector = sector; // specify the sector that we wanna modified.
```
### 填充 Descriptor
到了這一步,我們已經分配好 Descriptor 與 req 的基本資料了,接著我們可以對這三個 Descriptor 做資料填充:
```c=
disk.desc[idx[0]].addr = (uint64_t)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];
disk.desc[idx[1]].addr = (uint64_t)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];
disk.info[idx[0]].status = 0xff; // device writes 0 on success
disk.desc[idx[2]].addr = (uint64_t)&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;
// record struct buf for virtio_disk_intr().
b->disk = 1;
disk.info[idx[0]].b = b;
// tell the device the first index in our chain of descriptors.
disk.avail->ring[disk.avail->idx % NUM] = idx[0];
__sync_synchronize();
// tell the device another avail ring entry is available.
disk.avail->idx += 1; // not % NUM ...
__sync_synchronize();
*R(VIRTIO_MMIO_QUEUE_NOTIFY) = 0; // value is queue number
// Wait for virtio_disk_intr() to say request has finished.
while (b->disk == 1)
{
}
disk.info[idx[0]].b = 0;
free_chain(idx[0]);
```
## 實作 VirtIO 的 ISR
```c=
void virtio_disk_isr()
{
lock_acquire(&disk.vdisk_lock);
// 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;
__sync_synchronize();
// the device increments disk.used->idx when it
// adds an entry to the used ring.
while (disk.used_idx != disk.used->idx)
{
__sync_synchronize();
int id = disk.used->ring[disk.used_idx % NUM].id;
if (disk.info[id].status != 0)
panic("virtio_disk_intr status");
struct buf *b = disk.info[id].b;
b->disk = 0; // disk is done with buf
wakeup(b);
disk.used_idx += 1;
}
lock_free(&disk.vdisk_lock);
}
```
## Reference
- [xv6-riscv](https://github.com/mit-pdos/xv6-riscv)
- [Lecture: Virtual I/O Protocol Operating Systems Stephen Marz](https://web.eecs.utk.edu/~smarz1/courses/cosc361/notes/virtio/)