linux平台实现虚拟磁盘驱动(通用的块设备驱动和基于SCSI的磁盘驱动)

by fanxiushu 2023-08-16 转载或引用请桌面原始作者。

实现linux平台的虚拟磁盘驱动,是为了要实现在linux远程无盘启动的。

linux平台下的无盘启动,现成的办法有许多,比如iSCSI,NFS,NBD等都可以,

不过我都没去试过,所以不清楚具体的细节。

但是可以肯定得是,比windows下实现无盘启动轻松,windows中也有现成的比如iSCSI办法,

不过 windows的中的iSCSI我也没有具体去实验过,

所以这里也得不出两个系统下的搭建无盘启动过程的难度对比情况。

上面说得iSCSI这些,都是操作系统本身集成的方案,

都是现成的,唯一需要去做的就是如何使用现成的东西,去搭建无盘启动的整个过程。

这中间不牵涉到程序的开发过程,顶多就是做一些脚本什么的方便维护。

而我这里说的是如何开发无盘启动的过程

(包括客户端驱动和服务器端,网络传输协议等都是自定义实现)。

在CSDN上前面的5,6篇文章,都是在阐述windows下的无盘启动开发内容,

其中以如何开发Legacy BIOS 和UEFI的引导程序讲述的比较详细,

其实最麻烦的还是windows下的驱动开发问题,

只是因为很早前讲述过windows中虚拟磁盘驱动开发,所以现在就省略了。

本章将介绍linux平台下的虚拟磁盘驱动开发过程。

为了增加一些兴趣,可以首先去查看我在windows平台下的无盘启动的演示视频:
win10系统的无盘启动过程-CSDN直播这是开发的虚拟驱动实现的win10平台下的无盘启动过程演示,在vmware中的演示效果。https://live.csdn.net/v/320536
无盘启动过程,演示的是win7平台和本地镜像启动-CSDN直播无盘启动的网络方式和本地镜像方式的演示视频https://live.csdn.net/v/314881

其中以win7的无盘启动最快,因为毕竟体积小(以前是觉得win7体积大,winxp小,现在是win7变小了)。

win10启动过程,我估计了一下,从UEFI引导程序调用bootmgr开始到进入到windows界面。

需要大约50秒时间。

当然这个时间跟具体的硬件配置有关,因为毕竟我这的电脑硬件配置,

在我的真实电脑上启动也得花个30-40秒的时间,现在网启也才多了10-20秒而已。

回到linux平台中的虚拟磁盘驱动开发上来。

其实linux中的磁盘驱动,本质上就是块设备驱动。

而在linux中分成三大类基本驱动类型:

1 字符设备驱动, 2 块设备驱动,3 网络设备驱动。

可想而知,磁盘驱动作为linux核心中基本类型,实现想必不会简单。

也确实,linux里边的关于块设备的核心代码确实比较复杂。

再回到windows,windows中关于磁盘相关的实现同样也是复杂的。

我只举个简单的层级例子: windows底层storport接口给程序员调用来实现具体的磁盘设备。

上层是disk.sys驱动实现底层磁盘设备的通用接口层,这个跟linux内核中 generic Block Layer 层基本是同一个概念。

再上层是partmgr.sys 实现对磁盘分区管理,再朝上是volmgr驱动负责每个分区卷的管理,

再然后就是对应的文件系统驱动,比如ntfs,fat等文件系统驱动。

linux内核中关于块设备也有同样的分层概念。

只是linux把这些一股脑儿的的塞进一个单一的linux内核中,而windows是以单独的模块来实现这些过程。

这里不阐述研究linux内核代码的事(这可是个相当绒长的,几百篇文章都可能研究不完),

而只是研究如何利用linux内核提供的接口,实现块设备驱动,从而达到实现我们的虚拟磁盘驱动的目的。

linux内核提供的块设备接口,那可真是太过于简单,

以至于总有种小孩子过家家一般的荒缪的感觉。

如下代码,就能实现一个内存磁盘的功能,代码却只有短短70多行。

(相同的简单功能,在windows中使用storport框架实现简单的内存磁盘,代码起码要到千行左右了,

而且还得编写inf配置文件,生成 cat签名文件,等等。反正不是在一个等级的)

int major;

int disk_size = 1024 * 1024;

char* memdisk = 0;

const char* DRIVER_NAME = "nbt_scsi";

spinlock_t queue_spinLock;

struct gendisk* disk = 0;

static const struct block_device_operations nbt_fops =

{

.owner = THIS_MODULE,

}

void disk_do_request(struct request_queue *q)

{

struct request *req;

while ((req = blk_fetch_request(q)) != NULL) {

BOOLEAN is_read;

int64_t offset;

long length;

struct req_iterator iter;

struct bio_vec *bvec;

///读写offset和length

offset = ((uint64_t)blk_rq_pos(req)) << 9;

length = blk_rq_bytes(req);

is_read = TRUE;

if (rq_data_dir(req) == WRITE) {

is_read = FALSE;

}

rq_for_each_segment(bvec, req, iter) { 查询request请求的所以数据块分段,

void *kaddr = kmap(bvec->bv_page);

char* buf = (char*)kaddr + bvec->bv_offset;

int len = bvec->bv_len;

///读写磁盘内容

if (is_read)memcpy(buf, memdisk + offset, len);

else memcpy(memdisk + offset, buf, len);

kunmap(bvec->bv_page);

offset += len;

}

__blk_end_request_all(req, 0); /// success complete request

}

}

static int __init blk_init(void)

{

spin_lock_init(&scsi->queue_spinLock);

major = register_blkdev(0, DRIVER_NAME);

///

memdisk = kmalloc(disk_size, GFP_KERNEL);

disk = alloc_disk(16);//创建

disk->major = major;

disk->first_minor = 0;

disk->fops = &nbt_fops;

set_capacity(disk, disk_size / 512); /// disk size

strcpy(disk->disk_name, "nbt-disk");

struct request_queue* q = blk_init_queue(disk_do_request, &queue_spinLock);

disk->queue = q;

add_disk(disk); 启动磁盘

return 0;

}

static void __exit blk_exit(void)

{

del_gendisk(disk);

blk_cleanup_queue(disk->queue);

put_disk(disk);

kfree(memdisk);

unregister_blkdev(scsi->major, DRIVER_NAME);

}

module_init(blk_init);

module_exit(blk_exit);

够简单的吧,不到100行代码,刚启蒙的小孩说不定都能做,哈哈。

大概的流程就是首先在初始化函数中调用 register_blkdev 注册块设备,

在退出函数中相应的使用un register_blkdev注销块设备。

然后就可以调用 alloc_disk 函数分配gendisk的结构体。

接着在gendisk中填充一些必须的参数,

包括上面代码中的 major,fops, queue,disk_name等参数。

其中 fops指向 block_device_operations 结构体,类似 字符设备中对应的操作回调函数,

不过呢,块设备的绝大部分操作函数都是linux内核帮我们实现了,

所以如果不是特别需求,都可以不填写,简单申明一个结构体就行。

然后就是设置 disk_name 块设备名字,这个名字对应 /dev目录下的名字,

再然后调用set_capacity 设置块设备的大小,

因为linux内核固定把 512字节当成一个块大小,所以实际磁盘大小除以512就行。

最后就比较重要的,磁盘读写问题,与字符设备不同,不是在操作回调函数中响应IO读写,

而是有一个专门的 queue来实现。

如上代码,使用 blk_init_queue 函数来创建一个队列,赋值给 gendisk的queue参数。

同时 blk_init_queue 函数需要提供一个回调函数,这个回调函数就会实现 磁盘的IO读写请求。

关于 queue问题,linux内核还实现一个读写方案,就是make request,

上面简单提到过, linux内核有个 generic Block Layer,处理通用的磁盘请求,

这一层,所有通信其实使用的是 bio 结构体传输数据的。

blk_init_queue 函数是传统的办法,会在generic block layer发送读写请求时候,

把一些相邻的bio请求合并起来,形成 request 请求,

然后再传输给 blk_init_queue提供的回调函数处理,

也就是相当于在 generic block layer 层和 block driver layer层的中间还有一个合并零散的bio的算法层。

而make request呢, 相当于略过了合并 bio请求的算法层,直接把bio请求发给 block driver。

这样做的好处也很明显,比如上面的内存磁盘,

其实到了 block driver layer也就是上面的代码disk_do_request回调函数中。

只需要简单内存拷贝(memcpy)就行,没必要浪费CPU再在算法层合并相邻 bio 请求。

所以make request 很适合 内存磁盘,SSD等响应非常快的磁盘系统。

至于make request如何调用,这里不再啰嗦,有兴趣可自行去查阅,反正也是很简单的几个函数而已。

到此,我们就轻轻松松的实现了linux系统下的磁盘设备了。

当然,严格来说,是实现了一个通用的块设备驱动,

作为通常的需求完全足够了,甚至作为linux的无盘启动来说,也已经足够了。

这个与windows平台完全不同,

linux下的这种 gendisk 通用的块设备驱动,其实在windows也有对应的实现方案:

windows驱动中,调用 IoCreateDevice 函数创建类型是 FILE_DEVICE_DISK 的设备,

然后响应 一些关于磁盘的特殊IOCTL, 同时响应 IRP_MG_READ和IRP_MJ_WRITE的读写磁盘扇区请求。

在应用层使用 DefineDosDevice等函数挂载,就能在应用层看到一个能被绝大部分程序访问的分区卷。

但是,在windows的这种做法,也就只能在应用层玩玩,在内核中基本不被承认,更不可能用它来作为启动磁盘了。

所以说,linux内核windows内核差别真的是非常大,驱动实现难度差别也是非常大。

通常来说,windows下的驱动开发更难。

上面说得linux通用块设备实现,

难道就没有更底层的接口,让我们的linux系统中的虚拟磁盘驱动看起来更像一块硬件磁盘,

而不是处于比较尴尬的通用块设备驱动的这一层。

当然是有的,自然最容易想到的就是基于 SCSI 接口的磁盘,

回到windows平台,windows的storport框架其实是从 winxp系统的scsiport框架升级发展而来,

storport可以实现任何底层协议的磁盘,不限于SCSI。

linux下也有对应的接口,毕竟SCSI是一个通用的接口层,比如U盘的上层协议就是基于SCSI的。

scsi接口比gendisk接口稍微麻烦点,但是总体来说还是很简单。

首先申明scsi_host_template 结构体。

类似下面这样,以下设置仅供参考,参数可以调整:

static struct scsi_host_template nbt_scsi_driver_template = {

///

.name = "Fanxiushu NetBoot Virtual SCSI Adapter",

.proc_name = "nbtscsi",

.info = nbt_scsi_info,

.queuecommand = nbt_scsi_queuecommand,

.change_queue_depth = nbt_scsi_change_queue_depth,

.eh_device_reset_handler = nbt_scsi_device_reset,

.bios_param = nbt_scsi_bios_parm,

.can_queue = 32,

.this_id = -1,

.sg_tablesize = SG_ALL,

.cmd_per_lun = 128,

.max_sectors = 8192,

.use_clustering = DISABLE_CLUSTERING,

.emulated = 1,

.module = THIS_MODULE,

};

其中 queuecommand 回调函数是最重要的。函数申明如下:

int nbt_scsi_queuecommand(struct Scsi_Host *sh, struct scsi_cmnd *sc);

它响应SCSI各种命令。

首先如下调用:

struct Scsi_Host *sh = scsi_host_alloc(&nbt_scsi_driver_template, sizeof(自己定义的私有结构体大小));

scsi_host_alloc 分配SCSI适配器对应的结构体。

接着调用 scsi_add_host(sh, dev ); 启动这个SCSI适配器,

这里dev对应着SCSI适配器总线设备,

但是我们创建的是一个虚拟磁盘驱动,没有真实的SCSI适配器硬件,所以是没有对应的device设备的。

该如何解决这个问题呢?

以前在讲述linux平台下实现虚拟USB控制器驱动的时候,就讲过,可以使用虚拟总线。

调用 platform_driver_register 注册一个平台总线,然后调用 platform_device_register注册一个总线设备。

这样在 probe回调函数中,就能获取到总线设备对应的device。

当scsi_host_alloc 和scsi_add_host调用之后,SCSI适配器就创建成功了。

接下来,就是创建具体的SCSI磁盘设备。

本来按照正常的硬件,我们接下来可以调用 scsi_scan_host 函数,从而触发系统扫描SCSI硬件适配器。

但是我们是虚拟驱动,所以没必要这么做,

直接调用 scsi_add_device 或者 __scsi_add_device 函数添加一个scsi磁盘设备就可以了。

调用scsi_add_device之后,linux内核就认为磁盘设备已经创建好。

这时候 scsi_host_template 里边对应的 queuecommand 回调函数就会被调用,用来处理具体的SCSI磁盘的SCSI请求。

至于queuecommand里边处理的SCSI命令,

这个就和windows下的驱动处理SCSI命令没啥本质区别(除了跟具体系统相关的之外)。

因为SCSI协议是跨平台通用的。

下图是linux下实现的scsi和通用块设备驱动演示图:

图中左边两个红色框里边对应的就是 SCSI接口的磁盘和基于gendisk的磁盘。

其中SCSI接口的磁盘在图中显示的跟底层硬件一样了。

除了vmware硬件设备之外,其它都显示是 Block Device 块设备。

可以与windows平台下对应的虚拟磁盘驱动做个对照:

在windows中,对网络通信做了非常多的工作,

如果只是普通的网络磁盘,没必要这么麻烦,直接使用WSK或TDI通信即可。

但是要作为启动磁盘,问题就比较大了,所以最终采用的是底层NDIS通信。

而linux的网络通信就没这么麻烦了,

非但不麻烦,可以说是非常简单,即便是作为linux启动盘也是一样的简单。

后面章节阐述我们自己的虚拟磁盘驱动实现 linux 下无盘启动的时候会讲述到。

相关推荐
pyliumy4 分钟前
rsync 全网备份
linux·运维·服务器
sorel_ferris33 分钟前
Ubuntu-24.04中Docker-Desktop无法启动
linux·ubuntu·docker
ggb199935 分钟前
【python的坑】vpn下,python request报错 check_hostname requires server_hostname
linux·运维·服务器
小O_好好学1 小时前
vi | vim基本使用
linux·编辑器·vim
-SGlow-1 小时前
Linux相关概念和重要知识点(4)(自举、vim)
linux·运维·vim
多多*1 小时前
OJ在线评测系统 登录页面开发 前端后端联调实现全栈开发
linux·服务器·前端·ubuntu·docker·前端框架
卑微的码蚁1 小时前
服务器相关问题
运维·服务器
博洋科技1 小时前
网站建设的服务器该如何选择?
运维·服务器·网站建设·保定响应式网站建设·保定h5网站建设·保定网站建设
人类群星闪耀时1 小时前
服务器管理:从零开始的服务器安装与配置指南
运维·服务器
王哲晓2 小时前
Linux通过yum安装Docker
java·linux·docker