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传输协议栈。

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
1 无盘工作站及其原理 1.1 无盘工作站 无盘工作站是没有硬盘的计算机终端。它本身不含有硬存储设备,机器引导时需要借助网络上的服务器来传输操作系统启动文件到本地内存,才可以完成启动。因此,无盘工作站必须具有CPU、主板、内存、网卡和远程启动设备。远程启动设备主要以芯片的形式固化或接插在网卡上,在系统启动时负责连接服务器,获取IP地址,指导服务器上的操作系统启动文件到本地内存的传输。目前,远程启动芯片主要有RPL和PXE两种,后者为前者的升级版,目前正在被广泛使用。 1.2 PXE芯片启动原理 PXE芯片在远程启动时,首先要进行一个DHCP会话过程来获取一个IP地址和相关信息,以便进行后续的文件传送;IP地址获取成功后,PXE芯片触发TFTP会话过程,根据DHCP过程获取的相关信息向服务器请求启动文件,服务器接到请求后,根据本地策略,发送相应的启动文件,工作站接收到启动文件后,开始本地启动过程。 以上是PXE芯片无盘工作站启动过程的简单描述,DHCP协议的相关内容可参考RFC2131标准化文档,TFTP协议的相关内容可参考RFC1350标准化文档。 对于Linux操作系统来说,PXE所需要下载的启动文件是配置文件和内核文件。其中,配置文件在DHCP应答报文中包含,通过TFTP会话过程获取;内核文件的名称和位置在配置文件中给出,同样需要通过TFTP会话过程获取;内核文件获取成功后,系统开始进行本地启动,当启动过程进行到需要装载文件系统时,PXE会根据配置文件,向服务器请求连接根文件系统(该文件系统为NFS网络根文件系统)所对应的服务器目录,若成功,则启动结束。下页图1-1给出了这一过程的交互模型。 Linux无盘工作站的详细启动流程如下: Step 1 工作站加电,PXE芯片进行自检; Step 2 准备启动,PXE芯片发送DHCP请求; Step 3 服务器收到工作站送出的请求,发送DHCP应答,内容包括客户端的IP地址,预设网关,DNS服务器及开机映象文件(配置文件和内核)位置; Step 4 工作站上的PXE芯片根据配置文件位置,开始TFTP会话请求下载该文件; Step 5 加载映象文件,开始本地启动,挂载NFS网络根文件系统; Step 6 出现Login行,启动成功。
锐起网吧无盘是锐起科技集多年的研发经验和深厚的技术功底,基于PXE启动方式,开发的用于远程启动的网络平台软件系统。最新一代的锐起无盘XP3.1系统独创性地采用多机热备技术,任一服务器宕机都不会影响客户机的正常使用,彻底解决了传统无盘服务器高风险的难题。为了减少服务器数据回写压力,锐起无盘XP3.1采用了客户机内存数据写缓存技术,可以将客户机运行过程中产生的临时文件保存在本地内存中,这样极大地提高了回写速度,增强了服务器的负载能力,让服务器运行更加高速稳定。锐起无盘XP3.1基于多机热备与写缓存两大技术优势已经成为网吧无盘方案的首选。 锐起网吧无盘主要功能 1、降低成本,节省投资 可以有效减少网吧硬盘投入,降低电费、网管人力成本及其他运营成本。 2、部署快捷,管理轻松 通过更新服务器镜像文件,就可以实现全部客户机的系统更新和维护,彻底解决网络管理费时费力的难题。 3、无盘架构,还原保护 告别病毒木马,完全免疫熊猫烧香、机器狗等网络病毒。 4、智能化PNP技术 确保网吧网络环境中的硬件升级简单易行。 5、更快的磁盘运行速度 千兆网络环境下可超越本地硬盘读写速度,带来更佳应用体验。 6、多机热备,负载均衡 通过多服务器热备设置,任一服务器宕机都不会影响客户机的正常运行。 7、客户机本地写缓存 通过设置客户机本地内存写缓存,提高了数据回写速度,降低服务器压力,让服务器运行更高速稳定。 锐起网吧无盘系统优势 1、独立版权:系统功能可根据市场需求持续升级,保证用户利益。 2、稳定高效:采用Windows SCSI Miniport为核心驱动模型程序,确保稳定可靠。 3、负载强劲:单台服务器负载能力可达100台。 4、兼容性强:满足主流网吧硬件配置和系统环境需求,支持Windows、Linux平台。 锐起网吧无盘 v4.3 Build 3204更新日志 01.修正主服务端有时会出现部分游戏未能自动更新的问题 02.修正3162版本重启服务器后客户机启动会出现卡DHCP的问题 03.修正主副服务器多网卡,客户机未设置首选服务器有时会出现卡DHCP的问题 04.修正64位客户端网吧桌面有时会出现快捷方式无法打开的问题 05.修正游戏拷贝过程中有时会出现网维中心服务崩溃的问题 06.优化网维中心服务,降低服务的CPU和内存占用 锐起网吧无盘截图

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值