linux平台的无盘启动开发

by fanxiushu 2023-10-15 转载或引用请注明原始作者。

前一章节介绍的是linux平台下的虚拟磁盘驱动开发过程,主要讲述了 基于block的磁盘和基于SCSI接口的磁盘。

本文介绍的内容正是基于上文中的SCSI接口的虚拟磁盘实现的无盘启动。

同样的,linux系统下也有系统自己集成的无盘启动方案,

这点与windows类似,就连协议也能找到一样的,也就是windows和linux都可以使用iSCSI进行无盘启动。

但是linux系统可以选择的方案更多,除了大家都认可的基于底层磁盘块设备的无盘启动,

linux还能基于上层的文件系统进行无盘启动,比如linux可以配置NFS网络文件系统来无盘启动。

不过我认为还是基于底层块设备更好些,至少服务端对单个的镜像文件更好管理一些。

本文并不介绍linux系统自己集成的无盘启动方案,而是如前面文章阐述的windows平台的无盘启动系统一样。

全是自己开发的。

可以先看看下面演示视频:

linux的无盘启动

其实linux的无盘启动和windows的无盘启动的大体方向都是一致的:

1,在电脑引导阶段,需要开发 BIOS(UEFI)引导程序,

2,接着进入具体的操作系统阶段,需要开发对应的虚拟磁盘驱动来接管操作系统的启动和运行过程。

但是linux和windows的启动过程,有非常巨大的区别,这就造成了上面的第2个步骤中,开发的虚拟磁盘驱动的天差地别。

我们来回忆一下,前面章节讲述windows的启动阶段。

windows基本分成三个阶段:

1,boot-start阶段:运行 bios引导阶段winloader.efi(exe)程序加载到内存的windows基本内核和boot-start驱动程序,

2,initsystem阶段 :系统初始化

3,win logon阶段:系统登录

而且也说过,对于无盘启动来说,最重要的是boot-start阶段的处理问题,这是很麻烦的一件事。

具体都在以前的章节中讲述过。

而linux平台下的启动阶段,对无盘系统来说,就相对友好的多。

当然,同样的,bios引导阶段,会加载linux的引导程序,这个一般都是grub引导程序(当然可能也有其他引导程序,但是目前以grub为主),

而grub主要工作就是把 vmlinuz 基本内核文件 加载到内存中,

vmlinuz文件都可以在 /boot目录下找到,通常是vmlinuz为前缀加一些版本号什么的。

再然后就是加载 initramfs 虚拟文件系统到内存,

而 initramfs 就是我们能正常运行我们的虚拟磁盘驱动的关键,等下会讲述。

接着grub开始解压 initramfs等虚拟文件系统,并且挂载出一个虚拟文件系统,运行 vmlinuz 内核,把控制权力交接给linux内核。

现在我们来看看, linux为何会有 initramfs 这个虚拟文件系统。

linux和windows,或者说任何操作系统都一样,都存在一个尴尬的问题:

当BIOS引导程序把控制权交接给具体的操作系统,

操作系统不可避免的都需要访问磁盘,但是在最开始阶段,操作系统刚运行,磁盘驱动还没加载,

磁盘驱动文件不存在,需要从磁盘上加载磁盘驱动文件;但是这个时候操作系统无法访问磁盘。。。

这就陷入了一个荒诞的循环悖论中。

因此唯一的做法,就是在BIOS引导程序还在运行的阶段,把磁盘驱动文件首先加载到内存中。

windows的做法就是在BIOS运行阶段,winloader.exe程序把windows基本内核(ntoskrne.exe等基本文件),

以及处于 boot-start阶段(其中必然会包括磁盘驱动)加载到内存中。

因此到了windows的boot-start阶段,就开始运行加载到内存中的这些文件。

而linux的做法我们可以有一个非常简单的办法:

反正vmlinuz内核文件肯定首先被加载到内存中,我们把对应磁盘的驱动直接编译集成到vmlinuz内核文件中。

反正linux内核开源,而且是那种大一统的内核,不像windows的ntoskrnel内核,

但是这样弊端也非常明显,什么东西都朝vmlinuz内核文件中集成,会变得越来越臃肿和难以管理。

于是就诞生了 initramfs,顾名思义,

基于内存的文件系统,很像我们在内存中虚拟出一块磁盘,然后在上面加载对应的文件系统。

其实在2.6版本之前,是initrd,也就是直接生成内存磁盘。

2.6之后的版本,使用了initramfs来代替,因为initrd会直接开辟一块内存作为内存磁盘,这样会造成一些浪费。

而initramfs则是完全根据加载的文件来模拟虚拟文件系统,不存在initrd那样的浪费。

该如何解释initramfs展现出来的效果呢?

就是当grub把vmlinuz和initramfs加载到内存,并且initramfs初始化完成,vmlinuz接管控制权之后,

在没有访问真正的磁盘之前,

如果我们这时候登录到linux,会神奇的发现,像 /etc, /lib ,/bin ,/usr/lib 等目录就已经存在,而且里边有对应的文件了。

可是实际上我们还没真正访问本地磁盘。

这就是initramfs效果,它在内存中模拟出本地磁盘文件系统,vmlinuz内核像访问真正的目录文件那样访问这些目录中的文件。

于是,linux内核找到磁盘驱动文件位置,然后加载它,之后就能正常访问真正的本地磁盘系统了。

当然这个时候,linux内核会把真正的磁盘系统挂载上去,替换掉initramfs。

接下来我们需要做的,就是利用相关工具程序,把我们对应的磁盘驱动加到 initramfs 虚拟系统文件中。

当然,如果我们是在具体的电脑中安装对应的linux操作系统,安装程序就已经帮我们做好这一步了。

与windows做个对比,可以发现使用initramfs比起windows启动阶段显然简单得多,

这是不同系统内核的处理方式造成的,所以也不大可能搞得windows像linux那样的启动方式。

有了initramfs, 我们加载自己的无盘系统,就相对简单得多了。

我们主要在iniramfs中添加两个驱动:

1,网卡驱动,

2,我们的虚拟磁盘驱动,

网卡驱动则是对应网卡硬件厂商在linux下开发的驱动,

网卡驱动必须添加到initramfs中,否则虚拟磁盘无法访问网络,从而无法建立网络磁盘。

接着就是我们的虚拟磁盘驱动。

而虚拟磁盘驱动访问网络方式也不用像windows那样使用底层的NDIS协议,

可以直接使用TCP通信,因为TCP协议栈直接集成到vmlinuz内核中,

也不用担心像windows那样最早阶段连TCP协议栈都没建立起来。

这里以CentOS7,vmware虚拟机为例子,

简单阐述下如何把这两个驱动添加到 initramfs 中。

首先,假设我们开发的无盘启动的虚拟磁盘驱动 是 nbt_scsi.ko

把nbt_scsi.ko 复制到 /usr/lib/modules/$VERSION/kernel/drivers/block 目录中,

其中 $VERSION 对应的linux具体的内核版本,

然后使用 xz 命令 把 nbt_scsi.ko 压缩成 nbt_scsi.ko.xz 文件

运行 depmod -a 生成关联信息。

同时在 /etc/modules-load.d/ 目录中添加一个文件比如取名 nbt_scsi.conf

使用vi编辑,添加 如下两行:

nbt_scsi

e1000

其中e1000是vmware环境下,CentOS7系统对应的网卡驱动。

在 /etc/modules-load.d/ 目录下生成以上文本文件的目的是为了让linux内核自动加载上面两个驱动。

再然后则使用dracut ,如下运行:

dracut --force --add-drivers "nbt_scsi"

表示把 nbt_scsi 驱动添加到 initramfs 虚拟文件系统文件中。

(不同的linux发行版本,可能有不同的命令,但总体思路都是一样的)。

生成之后,如果不放心,

可以使用 lsinitrd |grep nbt_scsi 命令查看 initramfs中是否已经新添加了对应驱动文件。

同样的步骤,把 e1000 网卡驱动添加到 initramfs 中。

至此,CentOS7系统下的无盘配置就算完成,然后运行 nbt_scsi.ko 驱动,把服务端对应的镜像文件映射成本地scsi磁盘。

接着可以使用 dd 等命令,把磁盘数据copy到服务端镜像中。

再然后,我们就可以在别的也是同样支持e1000网卡的vmware虚拟机中无盘方式启动 centos7 了。

接着我们来看看nbt_scsi.ko 驱动中关于网络通信部分,

至于scsi接口的磁盘部分,上面一篇文章中已经阐述过,这里就不再重复了。

本文前面也说过,通信部分完全可以直接使用TCP通信,而且linux内核中socket与应用层的 BSD socket 接口一样的简单方便。

如下代码,我们就能封装成几乎跟应用层socket一样的接口方式:

kernel socket

struct socket* sock_create_v4(BOOLEAN is_tcp)

{

struct socket* sock = NULL;

#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 10, 0)

int ret = sock_create_kern(AF_INET, is_tcp ? SOCK_STREAM : SOCK_DGRAM , 0, &sock);

#else

int ret = sock_create_kern(&init_net, AF_INET, is_tcp ? SOCK_STREAM : SOCK_DGRAM, 0, &sock);

#endif

if (ret < 0) {

printk("sock_create_kern err=%d\n", ret);

return NULL;

}

if (is_tcp) {

int nodelay = 1;

#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 10, 0)

ret = kernel_setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&nodelay, sizeof(int));

#else

ret = sock_setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, KERNEL_SOCKPTR((char*)&nodelay), sizeof(int));

#endif

if (ret) {

printk("++ warnning: set tcp no delay err=%d\n", ret);

}

}

return sock;

}

void sock_destroy(struct socket* s)

{

if (s) {

sock_release(s);

}

}

typedef struct _sock_addr_v4

{

uint32_t ip;

uint16_t port;

} sock_addr_v4;

int sock_connect(struct socket* s, sock_addr_v4* svr_addr)

{

int ret;

struct sockaddr_in addr;

memset(&addr, 0, sizeof(addr));

addr.sin_family = AF_INET;

addr.sin_addr.s_addr = svr_addr->ip;

addr.sin_port = svr_addr->port;

ret = kernel_connect(s, (struct sockaddr*)&addr, sizeof(addr), 0);

if (ret < 0) {

printk("*** socket connect err=%d\n", ret);

}

return ret;

}

// 数据传输,包括接收或发送

int sock_transmit(

struct socket* sock, bool send, sock_addr_v4* addr,

char* buf, int size)

{

struct msghdr msg;

struct kvec iov;

struct sockaddr_in v4_addr;

int ret;

sock->sk->sk_allocation = GFP_NOIO;

iov.iov_base = buf;

iov.iov_len = size;

msg.msg_name = NULL;

msg.msg_namelen = 0;

msg.msg_control = NULL;

msg.msg_controllen = 0;

msg.msg_flags = MSG_NOSIGNAL;

if (addr) {

if (send) {

memset(&v4_addr, 0, sizeof(v4_addr));

v4_addr.sin_family = AF_INET;

v4_addr.sin_addr.s_addr = addr->ip;

v4_addr.sin_port = addr->port;

}

msg.msg_name = (struct sockaddr*)&v4_addr;

msg.msg_namelen = sizeof(struct sockaddr_in);

}

if (send) ret = kernel_sendmsg(sock, &msg, &iov, 1, size);

else {

ret = kernel_recvmsg(sock, &msg, &iov, 1, size, /*MSG_WAITALL*/ 0); ///完整接收size长度的数据,

if (ret > 0 && addr) {//收到数据了

addr->ip = v4_addr.sin_addr.s_addr;

addr->port = v4_addr.sin_port;

}

}

return ret;

}

显然,比起windows平台提供的TDI接口,或者WSK接口,不知道要简单多少倍了。

然后接下来就是磁盘数据的传输,这没啥好阐述的,按照SCSI接口提供的标准通信即可。

不过无盘启动的时候,依然会有问题,

其实主要就是grub加载内核之后,linux运行之后,并不会自动运行DHCP来配置本地IP地址,

如果没有对应的本地IP地址,TCP就无法路由,自然也无法通信。

至于这点,我们可以修改grub的启动参数,添加linux内核启动之后自动运行DHCP的支持,

也就是会多增加一些配置。

不过我自己开发的无盘系统,最主要的是基于windows平台的,

而在以前文章阐述过,底层网络通信基本都是直接使用NDIS通信,然后把链路层协议转成UDP来达到磁盘传输的目的。

所有协议的定义都和UDP有关,而且还会直接使用MAC地址来定位相关信息。

所以在linux平台的nbt_scsi驱动中,并没有使用TCP传输,或者只是作为辅助而已。

这样为了能跟windows中NDIS方式更好的结合。

一开始想在linux内核中寻找windows中类似NDIS的接口,甚至都想直接挂钩linux网卡驱动对应的通用接收函数。

到后来突然想到 PF_PACKET协议的套接口,它同样能接收和发送链路层数据包。

想到这一点,事情就好办了。

直接在nbt_scsi驱动使用 PF_PACKET协议的套接口。

同时为了加快接收速度,其实是替换了PF_PACKET 套接口的 sk_data_ready 回调函数。

这个实现过程就跟windows中使用NDIS协议驱动实现类似UDP套接口一样的复杂了。

有个好处就是:我不用再去理会本地IP地方分配问题,也不用去管DHCP,因为我在自己的协议中处理ip地址问题。

当然代价也是高昂的,等于自己利用链路层数据通信,自己完整实现了属于自己特色的UDP传输协议栈。

限于篇幅,这里不再赘述,有兴趣的同学可自行去研究。

相关推荐
许白掰1 小时前
Linux入门篇学习——Linux 工具之 make 工具和 makefile 文件
linux·运维·服务器·前端·学习·编辑器
longze_75 小时前
Ubuntu连接不上网络问题(Network is unreachable)
linux·服务器·ubuntu
Dirschs5 小时前
【Ubuntu22.04安装ROS Noetic】
linux·ubuntu·ros
qianshanxue115 小时前
ubuntu 操作记录
linux
AmosTian8 小时前
【系统与工具】Linux——Linux简介、安装、简单使用
linux·运维·服务器
这我可不懂11 小时前
Python 项目快速部署到 Linux 服务器基础教程
linux·服务器·python
车车不吃香菇11 小时前
java idea 本地debug linux服务
java·linux·intellij-idea
tan77º11 小时前
【Linux网络编程】Socket - TCP
linux·网络·c++·tcp/ip
kfepiza12 小时前
Linux的`if test`和`if [ ]中括号`的取反语法比较 笔记250709
linux·服务器·笔记·bash
CodeWithMe12 小时前
【Note】《深入理解Linux内核》 第十九章:深入理解 Linux 进程通信机制
linux·运维·php