文章目录
前言
本文基于xv6源码和虚拟硬盘标准来对xv6的文件系统最底层----磁盘层做详细解析,内容会涉及较多偏底层和虚拟磁盘交互的内容。最简单概括来讲,就是向设备对应寄存器写入相应的值以便实现设备控制和数据交换,相信耐心看完后你不会对xv6中这方面再有任何疑问。
xv6文件系统简介
xv6文件系统分7层,其中最底层Disk负责和虚拟硬盘交互,其余层次之后的文章中介绍,所有层次如图所示:
本文将详细从xv6源码以及标准协议出发解析这最后一层中对于虚拟硬盘的一系列操作。
virtio虚拟设备简介
Virtual I/O Device (VIRTIO) 虚拟输入输出设备是由OASIS制定的标准控制的一系列虚拟设备,具体包括虚拟网络设备,块设备(其中就包括虚拟硬盘virtio_disk),虚拟GPU设备,虚拟控制台等。标准当前最新版本为1.2。本文按照xv6遵循的1.1标准展开。
在qemu启动时,可以通过如下命令:qemu ... -device virtio-blk-device,bus=virtio-mmio-bus.0
来给qemu指定硬盘为虚拟硬盘同时总线指定为virtio-mmio-bus.0,以便再后续驱动中正确通过memory mapped IO方式控制虚拟硬盘。
虚拟设备通过特定总线方法发现并识别,具体有以下三种:PCI总线配置空间,memory mapped IO(将设备寄存器映射到内存中,通过访问内存的方式直接控制设备),channel IO(通过特定指令如IN/OUT写端口的方式实现控制)
虚拟设备整体结构
每一个符合标准规定的虚拟设备都包括以下五个部分:
1:Device status field(设备状态,通过状态寄存器设置)
2:Feature bits(控制设备详细工作状态)
3:Notifications(设备和驱动之间的通知)
4:Device Configuration space(设备配置空间,其中包含所有控制以及数据寄存器)
5:One or more virtqueues(一个或多个虚拟队列用于驱动和设备之间传递数据)
virtio_disk虚拟硬盘设备的各部分详细介绍
以下内容将针对虚拟硬盘来详细展开每一具体的部分
Device status field(用于通知设备状态):
在虚拟硬盘中通过向设备配置空间的0x70偏移位置的寄存器写入值来控制。具体见下图:
标准规定status的取值中有效位一共6个,分别是
1:ACKNOWLEDGE (=1第0位)
表示OS已经发现有效虚拟设备
2:DRIVER (=2第1位)
表示OS已经知道如何驱动设备
3:FAILED (=128)
表示出错,OS放弃设备
4:FEATURES_OK (=8)
表示设备特征/具体工作方式协商完毕
5:DRIVER_OK (=4)
表示驱动已经准备好
6:DEVICE_NEEDS_RESET (=64)
表示出现不可恢复错误,需要设备重置。(0一般用于初始化时重置设备)
所以xv6源码中对于设备寄存器的操作为:*R(VIRTIO_MMIO_STATUS)
,其中有#define VIRTIO_MMIO_STATUS 0x070
Feature bits(控制设备详细工作状态)
控制设备和驱动feature主要通过设备配置空间的四个寄存器。如下图所示:
其中,DeviceFeatures
只读,DeviceFeaturesSel
和DriverFeaturesSel
用于分别给设备和驱动指定一套feature,xv6中并未使用。xv6中并未对原始设备feature修改,只是对驱动的feature在设备feature基础上做部分禁用处理并写入DriverFeatures
。
feature bits主要用于控制设备详细工作状态,且位数较多,就不一一介绍。本文仅列出xv6中使用的feature(xv6把他们都禁用掉了)。具体所有的feature见标准5.2.3和第6章
1:VIRTIO_BLK_F_RO (5-->第5位)
表示块设备只读
2:VIRTIO_BLK_F_SCSI (7)
表示虚拟块设备支持SCSI(Small Computer System Interface)包命令
3:VIRTIO_BLK_F_CONFIG_WCE(11)
表示允许虚拟块设备在写回和写穿模式之间切换其缓存
4:VIRTIO_BLK_F_MQ(12)
表示设备支持多个虚拟队列,xv6中只使用了一个,默认的队列0
5:VIRTIO_F_ANY_LAYOUT(27)
出于对旧版的兼容,表示设备和驱动之间对消息帧不做任何协商
6:VIRTIO_RING_F_INDIRECT_DESC(28)
用于扩展descriptor数组到更大容量,由原本的一个队列struct virtq_desc *desc;
变为多个队列struct virtq_desc desc[len / 16];
其中的descriptor将在后面详细解释
7:VIRTIO_RING_F_EVENT_IDX(29)
用于指示设备和驱动程序之间是否支持使用事件索引机制来提高通知性能。如果没有协商这个特性,驱动程序可以通过available ring中的flags字段来通知设备,不需要在使用缓冲区时发送通知。当然,性能相应就不高。
总体上,xv6只是一个简单的OS,在文件系统的组织上也尽量简单,一些虚拟硬盘的高级特性读者有需要可以参看标准。
Notification(设备和驱动之间的通知/信号)
标准规定虚拟设备中共三种类型的通知,分别是:
1:配置改变
2:有空闲buffer
3:buffer已经使用
其中,1和3是设备通知驱动,设备到驱动信号实现机制多为中断,因此在旧版标准中这些通知也常称为中断。只有第2种是驱动主动通知设备虚拟队列中有可用buffer。
xv6中用到的信号有中断和VIRTIO_MMIO_QUEUE_NOTIFY
通知设备虚拟队列一个buffer待用的信号。使用VIRTIO_MMIO_QUEUE_NOTIFY
只需要向该寄存器写入目标虚拟队列号即可。标准中该内容如下图:
MMIO Device Register Layout(设备寄存器分布,其中包含所有控制以及数据寄存器)
MMIO方式下设备配置空间起始位置为virtio-mmIO基址,在qemu-riscv中也就是0x10001000
,具体来源下方会解释。在此基址下便是所有用户和设备交互的寄存器,其中就包括上方提及的Status设备状态
,QueueNotify队列buffer可用通知
,DriverFeature驱动特征选择
等等。在此列出部分标准中的寄存器如下图,左边为寄存器+偏移+读写权限,右边为寄存器功能概述:
其中,只针对设备的配置空间位于0x100位置(配置扇区相关内容等),xv6未使用,就不详细展开。
同时给出xv6源码中使用的寄存器并做注释:
#define VIRTIO_MMIO_MAGIC_VALUE 0x000 // 只读且必须为0x74726976
#define VIRTIO_MMIO_VERSION 0x004 // 只读必须为2
#define VIRTIO_MMIO_DEVICE_ID 0x008 // 只读,1 是网络设备, 2 是块设备
#define VIRTIO_MMIO_VENDOR_ID 0x00c // 子系统ID,必须为0x554d4551
#define VIRTIO_MMIO_DEVICE_FEATURES 0x010 //只读,设备feature
#define VIRTIO_MMIO_DRIVER_FEATURES 0x020 //控制驱动feature
#define VIRTIO_MMIO_QUEUE_SEL 0x030 // 队列选择,只写
#define VIRTIO_MMIO_QUEUE_NUM_MAX 0x034 // 当前队列最大容量,只读
#define VIRTIO_MMIO_QUEUE_NUM 0x038 // 只写,控制当前队列容量
#define VIRTIO_MMIO_QUEUE_READY 0x044 // 队列是否已经准备好,只读
#define VIRTIO_MMIO_QUEUE_NOTIFY 0x050 // 只写,通知设备队列有buffer可用
#define VIRTIO_MMIO_INTERRUPT_STATUS 0x060 // 只读,设备中断通知
#define VIRTIO_MMIO_INTERRUPT_ACK 0x064 // 只写,中断知晓
#define VIRTIO_MMIO_STATUS 0x070 // 设备状态
#define VIRTIO_MMIO_QUEUE_DESC_LOW 0x080 //只写,descriptor table物理低32地址
#define VIRTIO_MMIO_QUEUE_DESC_HIGH 0x084 //只写,descriptor table物理高32地址
#define VIRTIO_MMIO_DRIVER_DESC_LOW 0x090 // 只写,available ring物理低32地址
#define VIRTIO_MMIO_DRIVER_DESC_HIGH 0x094 //只写,available ring物理高32地址
#define VIRTIO_MMIO_DEVICE_DESC_LOW 0x0a0 // 只写,used ring物理低32地址
#define VIRTIO_MMIO_DEVICE_DESC_HIGH 0x0a4 //只写,used ring物理高32地址
其余寄存器读者可参考标准4.2.2 MMIO Device Register Layout
virtio-mmio基址来源
首先给出qemu对于riscv架构下的地址空间映射,github详细地址,关键部分如下图:
所以xv6源码中有如下定义:#define VIRTIO0 0x10001000
One or more virtqueues(一个或多个虚拟队列用于驱动和设备之间传递数据)
在标准1.0之前只支持Split Virtqueues
,xv6中也采用这种方式,本文就按它展开。事实上虚拟队列的操作十分复杂,标准1.1之后还有 Packed Virtqueues
,这里尽量简化其中的内容方便理解,读者想要更详细的虚拟队列信息可以参考标准。
虚拟队列组成
1:Descriptor Table
,描述符表/缓冲区,作为buffer存储待存入磁盘的数据/待磁盘写入的数据
标准规定的Descriptor Table
的格式:
struct virtq_desc {
le64 addr; //待存入磁盘的数据/待磁盘写入的数据物理地址
le32 len; //待存入磁盘的数据长度/待磁盘写入的数据长度
#define VIRTQ_DESC_F_NEXT 1 //表示next有效,后面还有数据
#define VIRTQ_DESC_F_WRITE 2 //表示设备只能写入addr,否则设备只能读取addr
#define VIRTQ_DESC_F_INDIRECT 4 //不可用,feature中禁用了VIRTIO_RING_F_INDIRECT_DESC
le16 flags; //该数据的权限,可以为VIRTQ_DESC_F_NEXT和VIRTQ_DESC_F_WRITE的组合
le16 next; //指向下一个descriptor下标
};
2:Available Ring
,Descriptor Table作为缓冲,而Available Ring用于传递数据在缓冲中的具体位置到设备。
标准规定的Available Ring
的格式:
struct virtq_avail {
#define VIRTQ_AVAIL_F_NO_INTERRUPT 1
le16 flags; //一直是0,xv6中使用中断
le16 idx; //驱动向ring[idx]中写已经就绪的descriptor下标
le16 ring[ /* Queue Size */ ]; //存储一系列descriptor下标,可以同一时间传递多个缓冲
le16 used_event; /* Only if VIRTIO_F_EVENT_IDX feature已经禁用,无效*/
};
Available Ring
只能由驱动写,设备只读。
3:Used Ring
,设备完成对buffers的操作时写Used Ring,只设备写,驱动读。
标准规定的Used Ring
的格式:
struct virtq_used {
#define VIRTQ_USED_F_NO_NOTIFY 1
le16 flags;//一直为0,使用notify
le16 idx; //仅设备写,每次完成buffers操作后递增
struct virtq_used_elem ring[ /* Queue Size */];
le16 avail_event; /* Only if VIRTIO_F_EVENT_IDX,无效,已禁用*/
};
/* le32 is used here for ids for padding reasons. */
struct virtq_used_elem {
/* 已经完成的descriptor下标 */
le32 id;
/* 整个操作的全部长度 */
le32 len;
};
标准中虚拟队列的构成:
struct virtq {
// descriptor队列
struct virtq_desc desc[ Queue Size ];
// avail ring
struct virtq_avail avail;
// used ring
struct virtq_used used;
};
一个完整的读写请求过程
标准规定,在读写操作向Descriptor Table
写具体的内容之前,必须先向第一个Descriptor
中写一个请求头阐述具体的操作类型,扇区号等信息,同时向next
域指定的Descriptor
中写入数据。
标准规定的virtio_blk_req
如下:
struct virtio_blk_req {
#define VIRTIO_BLK_T_IN 0
#define VIRTIO_BLK_T_OUT 1
le32 type; //描述操作类型,写磁盘还是读
le32 reserved;//保留
le64 sector;//扇区号
u8 data[];//xv6未使用,用于写0命令等操作
u8 status;//设备写,xv6未使用
};
总的来说,按照5.2节描述的传统操作步骤如下:
1:用户上层负责传递buffer,也就是之后文章会解析的buffer cache层,驱动负责找到可用3个descriptor
并向descriptor table
第一个位置写入请求头,第二个位置写入目标数据,第三个位置负责让磁盘向请求头的status写1,表示操作结束。
2:写入完毕通知设备有可用buffer,也就是上文中的VIRTIO_MMIO_QUEUE_NOTIFY
信号。
3:随后设备处理该buffers,并在处理完成时写Used Ring触发中断,驱动通过中断处理程序读取Used Ring来实现全部的交互。
至此,真相大白,沉冤昭雪,程序猿欢呼雀跃,奔走相告(狗头保命)。
(在开始解析xv6源码之前先给出基本的启动流程,方便后边的理解)
虚拟设备基本启动流程
接下来先给出标准规定的一般虚拟设备启动流程(见标准3.1.1位置),xv6源码严格按照这个流程组织。
1:重置设备,设置状态寄存器为0即可。
2:设置状态寄存器Acknowladge
状态位,OS识别到设备
3:设置状态寄存器Driver
状态位,OS知道如何驱动设备
4:读取设备features,做设备状态协商。
5:设置Features_OK
状态位,驱动不再接收新的工作特性
6:再次读取设备状态,确保已经设置Features_OK
状态位。
7:执行特定设备的设置,包括虚拟队列等(虚拟队列配置流程后方详细展开)
8:设置DRIVER_OK
状态位,此时设备就活起来了,是的,非常优雅,不愧是标准。(原话:At this point the device is “live"
)
设备启动中虚拟队列基本配置流程
1:选择要使用的队列,将它的下标写入QueueSel
,xv6使用默认的0号队列
2:检查QueueReady
寄存器判断队列是否就绪
3:读取QueueNumMax
寄存器,得到设备支持的最大队列大小
4:给队列分配内存空间,确保物理上连续
5:通过写入QueueNum
通知设备驱动使用的队列大小
6:写寄存器组:QueueDescLow/QueueDescHigh, QueueDriverLow/QueueDriverHigh and QueueDeviceLow/QueueDeviceHigh
分别写入Descriptor Table
Available Ring
和Used Ring
的64位地址。
7:向QueueReady
寄存器写1。准备完毕。
xv6源码解析
有了上边的知识储备,直接开始降维打击,我们勇往直前
virtio_disk_init虚拟磁盘初始化函数
uint32 status = 0;
// 初始化锁
initlock(&disk.vdisk_lock, "virtio_disk");
// 判断只读寄存器的值,确定设备正确
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");
}
// 第1步,重置设备
*R(VIRTIO_MMIO_STATUS) = status;
// 第2步,OS识别到设备
status |= VIRTIO_CONFIG_S_ACKNOWLEDGE;
*R(VIRTIO_MMIO_STATUS) = status;
// 第3步,知道如何驱动设备
status |= VIRTIO_CONFIG_S_DRIVER;
*R(VIRTIO_MMIO_STATUS) = status;
// 第4步,协商feature,每个feature均有说明
uint64 features = *R(VIRTIO_MMIO_DEVICE_FEATURES);
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);
*R(VIRTIO_MMIO_DRIVER_FEATURES) = features;
// 第5步,协商完成
status |= VIRTIO_CONFIG_S_FEATURES_OK;
*R(VIRTIO_MMIO_STATUS) = status;
// 第6步,再次读取状态,确定OK
status = *R(VIRTIO_MMIO_STATUS);
if(!(status & VIRTIO_CONFIG_S_FEATURES_OK))
panic("virtio disk FEATURES_OK unset");
// 第7步,初始化要用的队列号
*R(VIRTIO_MMIO_QUEUE_SEL) = 0;
// 确保队列准备好
if(*R(VIRTIO_MMIO_QUEUE_READY))
panic("virtio disk should not be ready");
// 检查队列最大容量
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");
// 分配物理地址连续空间
disk.desc = kalloc();
disk.avail = kalloc();
disk.used = kalloc();
if(!disk.desc || !disk.avail || !disk.used)
panic("virtio disk kalloc");
// 空间置0
memset(disk.desc, 0, PGSIZE);
memset(disk.avail, 0, PGSIZE);
memset(disk.used, 0, PGSIZE);
//设置驱动所用队列大小
*R(VIRTIO_MMIO_QUEUE_NUM) = NUM;
// 写一系列寄存器组
*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;
// 虚拟队列OK
*R(VIRTIO_MMIO_QUEUE_READY) = 0x1;
// 记录descriptor的可用情况,为1,可用,0不可用
for(int i = 0; i < NUM; i++)
disk.free[i] = 1;
// 最后一步,设备活起来喽
status |= VIRTIO_CONFIG_S_DRIVER_OK;
*R(VIRTIO_MMIO_STATUS) = status;
virtio_disk_rw虚拟磁盘读写函数
uint64 sector = b->blockno * (BSIZE / 512);//先计算出扇区号
acquire(&disk.vdisk_lock);//同一时间只能一个进程操作磁盘
// 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.
// 申请3个空闲描述符,不够则睡眠等待
int idx[3];
while(1){
if(alloc3_desc(idx) == 0) {
break;
}
sleep(&disk.free[0], &disk.vdisk_lock);
}
// 按标准格式化三个描述符
// qemu virtio-blk.c 负责读取
// 初始化请求头,使用disk的ops域存储,ops存储位置和第一个描述符的位置一致
struct virtio_blk_req *buf0 = &disk.ops[idx[0]];
if(write)
buf0->type = VIRTIO_BLK_T_OUT; // 写磁盘,初始化请求头
else
buf0->type = VIRTIO_BLK_T_IN; // 读磁盘,初始化请求头
buf0->reserved = 0;
buf0->sector = sector;
// 用请求头初始化第一个descriptor
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
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];
//第三个描述符的特殊操作,xv6把待设备写入的status放在disk.info域
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;
// 记录struct buf信息,方便中断处理程序处理
b->disk = 1;
disk.info[idx[0]].b = b;
// 把有效index写入avail ring中
disk.avail->ring[disk.avail->idx % NUM] = idx[0];
__sync_synchronize();
// index递增
disk.avail->idx += 1; // not % NUM ...
__sync_synchronize();
// 给设备发送通知消息
*R(VIRTIO_MMIO_QUEUE_NOTIFY) = 0; // value is queue number
// 等待驱动处理程序处理完毕并响应,睡眠锁,sleep和中断响应程序中的wakeup对应
while(b->disk == 1) {
sleep(b, &disk.vdisk_lock);
}
// 清除信息和descriptor链
disk.info[idx[0]].b = 0;
free_chain(idx[0]);
// 释放锁
release(&disk.vdisk_lock);
virtio_disk_intr中断处理程序
acquire(&disk.vdisk_lock);
// 响应中断,方便下次中断产生
// 中断0位表示buffer处理通知,第1位表示配置改变通知,保持不变写入ack则表示已经处理该中断
*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.
// 设备会增加used->idx并向该ring位置写入buffer开头index,当处理过该buffer时
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");
//清空该位置的disk标识,表示已经处理完毕
struct buf *b = disk.info[id].b;
b->disk = 0; // disk is done with buf
wakeup(b);// 唤醒进程
// 该index处理完毕
disk.used_idx += 1;
}
release(&disk.vdisk_lock);
结束语
所有设备无外乎写控制寄存器,而后数据交互。感谢耐心看完
后面关于源码详解略粗糙,如有问题,欢迎私信,必虚心受教。