启动流程分析
主要来看下当前内核中,相较于之前的部分,多了哪些东西,已经分析下他们的作用。
Makefile分析
首先来分析一下执行make qemu
后,对于网络部分,做了哪些工作。
在Makefile文件中,我们可以找到关于网络的一些特有qemu虚拟配置,代码如下:
ifeq ($(LAB),net)
QEMUOPTS += -netdev user,id=net0,hostfwd=udp::$(FWDPORT)-:2000 -object filter-dump,id=net0,netdev=net0,file=packets.pcap
QEMUOPTS += -device e1000,netdev=net0,bus=pcie.0
endif
查找qemu的手册,找到了关于其配置的讲解,其中qemu有设备前端和后端的概念,简单来说前端指的是模拟的硬件设备,后端指的是前端的数据应该被如何处理。前端通过-device来指定设备,后端通过-xxxdev来指定数据的处理方式。
在这里,
-
-device e1000
指定了前端设备是e1000网卡,它的后端设备是net0,在总线pcie.0上; -
-netdev user
配置了不需要管理员权限即可运行的用户模式主机网络后端。-
id
指定了可以在监视器命令中使用的符号名称,也就是net0。 -
hostfwd
表示将传入的 TCP 或 UDP 的数据从主机hostaddr的端口 hostport 转发到 访客 IP 地址为guestaddr的guestport 端口上的。如果未指定 guestaddr,则其值为 xxx15(内置 DHCP 服务器默认提供的第一个地址)。通过指定 hostaddr,可以将规则绑定到特定的主机接口。如果未设置连接类型,则使用 TCP。那么在这里,指定了将传入的UDP数据包从主机FWDPORT端口上转发到访客端口2000上。
-
-
-object filter-dump
表示将 netdev net0 上的网络流量转储到 packets.pcap文件中。每个数据包存储64k个字节(默认)。
main函数分析
在main函数中,添加了两个初始化的方法,如下所示
#ifdef LAB_NET
pci_init();
sockinit();
#endif
pci_init
方法是对pcie.0总线上的网卡设备的初始化,代码如下:
void
pci_init()
{
// we'll place the e1000 registers at this address.
// vm.c maps this range.
uint64 e1000_regs = 0x40000000L;
// qemu -machine virt puts PCIe config space here.
// vm.c maps this range.
uint32 *ecam = (uint32 *) 0x30000000L;
// look at each possible PCI device on bus 0.
for(int dev = 0; dev < 32; dev++){
int bus = 0;
int func = 0;
int offset = 0;
uint32 off = (bus << 16) | (dev << 11) | (func << 8) | (offset);
volatile uint32 *base = ecam + off;
uint32 id = base[0];
// 100e:8086 is an e1000
if(id == 0x100e8086){
// command and status register.
// bit 0 : I/O access enable
// bit 1 : memory access enable
// bit 2 : enable mastering
base[1] = 7;
__sync_synchronize();
for(int i = 0; i < 6; i++){
uint32 old = base[4+i];
// writing all 1's to the BAR causes it to be
// replaced with its size.
base[4+i] = 0xffffffff;
__sync_synchronize();
base[4+i] = old;
}
// tell the e1000 to reveal its registers at
// physical address 0x40000000.
base[4+0] = e1000_regs;
e1000_init((uint32*)e1000_regs);
}
}
}
在了解pci_init函数的功能之前,需要知道的储备知识是pci设备的配置空间,每个pic设备都有一个配置空间,大小为256B,实际上是一组连续的寄存器,其中头部64字节是PCI标准规定的,格式如下(图片来自维基百科)
剩余的部分是PCI设备自定义的。
其中DeviceID寄存器和VendorID寄存器标识设备ID和供应商ID;Status寄存器用于标识支持哪些功能以及是否发生了某些类型的错误。命令寄存器包含可以单独启用和禁用的功能的位掩码。
另外PCI配置空间头部有6个BAR(Base Address Registers),BAR记录了设备所需要的地址空间的类型(memory space或者I/O space),基址以及其他属性。PCI配置空间的初始值是由厂商预设在设备中的,于是设备需要哪些地址空间都是其自己定的,可能造成不同的PCI设备所映射的地址空间冲突,因此在PCI设备枚举(也叫总线枚举,由BIOS或者OS在启动时完成)的过程中,会重新为其分配地址空间,然后写入PCI配置空间中。
所以不难看出pci_init的前半部分功能是枚举总线上设备的配置空间,读取Device ID和Vendor ID从而确定e1000网卡设备,设置命令寄存器从而开启了设备可以响应I/O空间的访问,可以响应内存空间的访问,以及设备可以充当总线主控。
重点来讲一下对每个BAR设置0xffffffff的作用,这部分,查阅了很多,都没有讲,最后在维基百科中找到了答案,如下
也就是说,为了完成对BAR的配置,需要:
- 对BAR[x]写入全1
- 再从BAR[x]读出值,假设为y,通过~(0xffffff00 & y) + 1就是该BAR映射地址空间的大小
- 确定完大小后写入地址空间的起始地址就完成了对BAR的配置
我们来验证一下这个过程,在确定每个BAR大小的for循环中加入对大小的输出语句
再次执行 make qemu
得到输出
其中0x20000就是BAR0的大小,0x100就是BAR1的大小,在这里我们只需要使用BAR0,也就是e1000_regs,就只在BAR0写入了起始地址,并且可以在vm.c的kvmmake中得到验证
// pci.c maps the e1000's registers here.
kvmmap(kpgtbl, 0x40000000L, 0x40000000L, 0x20000, PTE_R | PTE_W);
这样,大概就可以理解这个函数的功能了,也就是设置了网卡中的一些寄存器值,并确定了BAR的大小,以及BAR0中的基址。这个BAR0存储的就是设备所需要空间的内存基址,在这里,也就是e1000_init函数中,我们可以看到,这段空间被用来设置网卡的各个控制位。