by fanxiushu 2023-04-14/18 转载或引用请注明原始作者。
接上文。
上文提到了至少有两种办法制作UEFI的虚拟磁盘驱动。
本文只接收利用BlockIO的方式来构造一个临时用的虚拟磁盘。
之所以说是临时使用,是因为在作为引导程序中,一旦进入到 \EFI\boot\bootx64.efi之后,
控制权就交给了bootx64.efi以及之后的引导程序,我们的引导程序唯一需要在幕后扮演的就是提供虚拟磁盘的IO功能。
这跟传统BIOS下实现大型系统引导的基本规则是一样的。
所以在开发引导程序的时候,不必按照UEFI规范文档那样阐述的,制作符合UEFI规范的UEFI驱动程序,
我们只需要在程序入口里调用 InstallMultipleProtocolInterfaces 函数安装新的BlockIO的PROTOCOL,
并且之后调用ConnectController 函数,让我们新安装的PROTOCOL能被连接上,
能被别的引导程序(其实就是 EFI\Boot\bootx64.efi 等其他引导程序)识别到。
所以在UEFI中制作这么一个临时的BlockIO接口就会显得比较轻松:
定义:
EFI_BLOCK_IO_MEDIA Media;
EFI_BLOCK_IO_PROTOCOL BlkIo;
EFI_BLOCK_DEVICE_PATH DevPath;
EFI_BLOCK_DEVICE_PATH是我们自己定义的继承自 EFI_DEVICE_PATH 的路径,可直接定义成 MSG_VENDOR_DP 的方式。
设备路径(EFI_DEVICE_PATH)是必须要的,否则其他efi程序无法正确定位。
定义以上结构之后,然后填写磁盘相关的信息,当然得首先从服务器获取磁盘相关的信息。
具体做法其实在以前阐述开发传统BIOS的引导程序的时候就介绍过,这里不再啰嗦,有兴趣可以去查阅。
填写 EFI_BLOCK_IO_PROTOCOL相关的回调函数,其中两个核心的函数:
ReadBlocks,读磁盘扇区数据
WriteBlocks,写磁盘扇区数据。
这两个回调函数就是我们需要实现的。
我们需要在回调函数中,把对磁盘的读写请求通过网络传输,定位到服务器端的磁盘镜像中。
填写好相关参数,就可以调用BootSErvice的InstallMultipleProtocolInterfaces 函数安装这个PROTOCOL了。
再然后做些其他相关的初始化操作。
完成之后,调用 BootService的ConnectController函数,让我们的PROTOCOL正常运作起来。
再之后,就需要通过 BootService的LocateHandleBuffer函数查询
gEfiSimpleFileSystemProtocolGuid(EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID),
并进一步定位到属于我们刚才制作的BlockIO虚拟磁盘驱动里边的FAT文件系统。
然后调用 BS(BootService,以下都简称BS或bs)的 LoadImage 加载我们的虚拟磁盘驱动的 EFI\Boot\bootx64.efi
再然后,调用 BS的StartImage函数,将控制权交给bootx64.efi。
之后如果一切正常,StartImage 将不会返回,到UEFI退出舞台,被引导的系统运行起来。
接着我们来简单阐述一下网络通信部分。
本来按照正常思路,uefi是支持tcp,udp这些直接传输的。
也就是我们可以像使用类似bsd socket那样创建UDP的“套接口”。
但是实现了之后,才发现一个很大的问题,速度太慢。
举个例子吧,我是使用vmware虚拟机作为主要测试对象的。
以前开发的传统BIOS下的引导程序,16位下的引导程序,读取速度能达到 20-30MB(字节每秒),
也就相当于利用了千兆网卡 四分之一还多的带宽,其实在千兆网环境中,
单步读取(因为传统BIOS,UEFI等引导系统都是单CPU的,单步运行的,不存在多线程概念),
能达到 20-30MB这个速度,我还是很满意的,毕竟加载win10 前期阶段的数据,只需要3,4秒就完成了。
可是到了UEFI,直接使用UEFI提供的UDP方式传输,结果只能达到每秒 8MB-9MB左右的速度,
实在是差了两三倍。
先简单阐述UEFI下如何实现UDP通信,
正如前面在开发传统BIOS引导程序的时候说过的,整个过程都是使用UDP通信的,
因为在局域网环境中,丢包小的环境中,使用UDP反而更快,数据吞吐量更大,尤其是这种简单的一来一往的磁盘读写请求。
而且每个读写请求都包括偏移,数据长度等信息,根据这些信息,可以天然的保障读写数据不会丢失乱序(丢失了大不了重新发送请求)。
在UEFI中,首先创建一个UDP的句柄,用于通信:
调用bs的LocateProtocol定位gEfiUdp4ServiceBindingProtocolGuid(EFI_UDP4_SERVICE_BINDING_PROTOCOL_GUID),
获得 EFI_SERVICE_BINDING_PROTOCOL 之后,调用里边的CreateChild创建一个handle。
接着根据这个handle,再次调用LocateProtocol获取到 EFI_UDP4_PROTOCOL 指针,
这个EFI_UDP4_PROTOCOL里边存储的就是我们接下进行UDP传输的函数集合。
当然首先是调用 里边的Configure回调函数配置通信的本地地址和远端地址。
因为默认是么有配置的,所以很多时候,我们得手动配置本地地址。
而本地地址,我们可以再次使用DHCP协议发送请求,或者因为是PXE启动的,所以可以直接从缓存中查找。
显然从本地缓存中查找会更简单。
接着就是调用 Transmit 发送UDP数据包,调用Receive接收UDP数据包。
这就是简单的的UDP通信过程,但是正如上面所阐述的,实现了之后,才发现速度太慢。
一开始还以为是vmware虚拟机的问题,然后后来尝试在真机中测试,好像速度也快不到哪去。
也不太想去查找具体原因,反正调用UDP的办法UEFI规范中就是这么提供的,也没存在哪里优化不够的问题。
当然也不排除我上层定义的协议比较繁琐,但是在传统BIOS下都能达到20MB-30M的速度,
在windows甚至能达到100MB的速度,而在UEFI中表现惨淡,这就显得不太对了。
到后来回想起传统BIOS开发的引导程序速度为何比较快。那是因为在传统BIOS下,直接调用的底层接口,
直接自己封装的UDP数据包。
所以最后放弃使用UEFI的上层的UDP接口,转而寻找底层的接口。
结果找到了SNP(EFI_SIMPLE_NETWORK_PROTOCOL)。
SNP是直接对 UNDI的封装,但是已经足够接近底层。
于是利用这个接口,既然是底层接口,什么都得自己实现,包括ARP处理,如何封包UDP,
还好在开发传统BIOS引导程序的时候,就已经实现了一遍。所以算是在UEFI环境下重新开发了一遍。
然后这么折腾之后,速度终于达到了传统BIOS下的速度。
所以。。。UEFI的网络堆栈虽然灵活了许多,但是速度好像也打了折扣啊。。。
因为 UEFI的网络堆栈大致如下:
先是 UNDI,这个是网卡驱动接口,接着UNDI套上了唯一的SNP, SNP套上了唯一的NMP,
从NMP才开始分开各种协议的处理细节。比如 UDP归于UDP Protocol处理,TCP归于TCP PROTOCOL处理。
然后一层层往上,反正在整个过程中,速度有所降低也正常。毕竟套了这么多层,而且还是UEFI只能单步使用CPU情况下,
不存在多线程,只能依靠EVENT事件来达到异步的效果。
然后,这两天,有人提到一件事,说是类似无盘启动一类的系统,但具体又不严格是。
就是UEFI中虚拟磁盘驱动不从网络读取,而是读取本地磁盘中的镜像文件,
然后启动到windows或linux,也是读写对应的镜像文件。
也就是相当于本地磁盘不是用来安装操作系统的,而是存储一个一个的磁盘镜像文件的。
磁盘镜像文件里的才是操作系统。
也就是相当于从网络方式的无盘启动,转到本地磁盘的一个一个镜像文件中。
以前到没想到还能这么用,而且比起开发网络方式的无盘启动,还简单的多了。
当然,windows的虚拟磁盘驱动还是得自己开发,只不过读取数据不再是网络读取,
也不用考虑非常麻烦的各类网卡驱动在boot阶段运行的问题,
在boot阶段直接访问真实的硬盘镜像文件就可以了,这个时候NTFS等文件系统已经建立了。
然后开发UEFI的引导程序,替换掉 原来的 EFI\boot\bootx64.efi就可以,之后就能让整个系统按照读取镜像文件的方式运行。
这个应该是很早前就在使用的一类无盘启动了,只是最近才知道有这么一个应用。
虽然不知道这么个应用究竟有啥实际用处。
但是无疑让我对UEFI的引导类型又开了一次眼界。
这玩意还能这么用,感觉挺好玩的。故此简单写了出来。