Loading... ## Agenda - 需求 - 实现思路 - 具体实现 ## Requirements 我的操作系统(MayoOS)需要实现文件系统,在实现了*VFS*子系统后,需要一个真实的块设备并为其编写驱动来提供文件存储功能。因为这个系统纯粹是为了学习使用,所以我决定实现*VirtIO*的驱动程序。 首先可以使用*QEMU*创建一个*drive image*,这里的*image*我们通常称为镜像。使用指令`qemu-img create qwq.img 100M`,我们就创建了一个大小为`100M`的*image*。 随后就可以将其添加到我们的虚拟机中(可参照[man page of qemu_system_x86_64](https://manpages.debian.org/unstable/qemu-system-x86/qemu-system-x86_64.1.en.html)):`qemu-system-x86_64 our-bootimage.bin -drive format=raw,index=1,if=virtio,bus=0,file=qwq.img`。我们的*BootImage*已经占用了`index=0`的*drive*,所以这里设置为`1`。 现在*drive*已经被添加进虚拟机了,我需要为其编写一个驱动程序。 ## My thought 因为一些“传统观念”,我脑子里第一个想法是:BIOS是不是会生成一个*Device Tree*并传递给*Bootloader*。但是随后这个观点被[维基百科: Device Tree](https://en.wikipedia.org/wiki/Device_tree#:~:text=Personal%20computers%20with%20the%20x86%20architecture%20generally%20do%20not%20use%20device%20trees)否决了,上面写着——x86体系结构不使用*device tree*,它使用的是*various auto configuration protocols*。顺藤摸瓜可以发现*PCI*设备使用的是*[PCI configuration space](https://en.wikipedia.org/wiki/PCI_configuration_space)*。 ### PCI configuration space 在*[PCI spec](https://pcisig.com/specifications)*中,可以发现其提供了一般称为*配置空间*的协议。 首先,它规定了`0xCF8`端口为*CONFIG_ADDRESS(配置空间地址端口)*,`0xCFC`为*CONFIG_DATA(配置空间数据端口)*。下面给出*CONFIG_ADDRESS*寄存器的格式定义: ``` CONFIG_ADDRESS = 0x8000_0000 | bus << 16 | device << 11 | function << 8 | offset // 源自维基百科 ``` 其中的*Bus*、*Device*、*Function*是用来区分*PCI*设备的,简称为*BDF*。 在写入配置空间地址到端口后,就可以直接读取数据端口了。我们只需要读取一个*DWORD*。因为按照规范,无论是哪种*Header Type*,它的第一个字(WORD,等于二字节)都会是*Vendor ID*,而第二字则是*Device ID*。前者是厂商编号,后者是由厂商规定的设备编号。在获得这两个值后,我们可以对应厂商的定义来对PCI设备进行读取与操作。但是在这里我只关心*VirtIO*。 因此,通过[OSDev: VirtIO](https://wiki.osdev.org/Virtio)可以得到*VirtIO*设备的厂商编号是`0x1AF4`,而设备编号为`0x1000-0x103F`区间中的任意值。如下图。  可以看到第五个设备就是我的*VirtIO*设备。 上面其实还没有提到怎么“扫描”所有设备,这里给出一个简单思路—— QEMU可以自定义挂载的总线编号,所以确定*bus*值后只需要`for device_id in 1..32`,然后查询每个设备的*vendor id*,如果它为默认值`0xFFFF`,那么这个设备就不存在。查询时的*function*与*offset*置`0`即可。 ## Implemention ### 地址处理 ```rust pub fn new( bus_number: u8, device_number: u8, func_number: u8, offset: u8, ) -> Self { let bus_number = (bus_number as u32); // 总线编号是八位 let device_number = (device_number as u32) & 0b0001_1111; // 设备槽位是五位 let func_number = (func_number as u32) & 0b0000_0111; // 函数编号是三位 let offset = (offset as u32) & 0b1111_1100; // 屏蔽低2位 let mut tmp = 0x8000_0000 | bus_number << 16 | device_number << 11 | func_number << 8 | offset; PCIConfigAddr(tmp) } ``` 要注意的就是要屏蔽*offset*的低两位,原因可能是因为要对齐吧。 ### 扫描设备 ```rust pub fn scan_bus_devices(bus: u8) -> Vec<DeviceInfo> { let mut res: Vec<DeviceInfo> = Vec::with_capacity(32); for device_id in 0..32 { if check_device(bus, device_id) { let info = get_device_vendor_and_id(bus, device_id); res.push(DeviceInfo::new(bus, device_id, info.1, info.0)); } } res } ``` 以上。 最后修改:2021 年 05 月 09 日 04 : 31 PM © 允许规范转载 赞赏 请我喝杯咖啡 ×Close 赞赏作者 扫一扫支付 支付宝支付 微信支付
1 条评论
2021年5月9日修改记录:
- 修改了标题,更加符合文章主题
- 修改了一处表述错误:VirtIO的设备ID介于(0x1000,0x103F)区间而不是取两个端点的值