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

news2024/7/6 19:00:33

                                       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 下无盘启动的时候会讲述到。
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/887836.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JVM中释放内存的三种方法

判断是否需要垃圾回收可以采用分析。 1标记--清除算法 分为两个阶段&#xff0c;标记和清除&#xff0c;先利用可达性分型标记还存活的对象&#xff0c;之后将没有被标记的对象删除&#xff0c;这样容易生成空间碎片&#xff0c;而且效率不稳定 标记阶段&#xff1a; 标记阶段…

C#和Java的大端位和小端位的问题

C#代码里就是小端序,Java代码里就是大端序&#xff0c; 大端位:big endian,是指数据的高字节保存在内存的低地址中&#xff0c;而数据的低字节保存在内存的高地址中&#xff0c;也叫高尾端 小端位:little endian,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存…

vue : 无法加载文件 F:\nodejs\vue.ps1,因为在此系统上禁止运行脚本。

报错信息如下 在使用Windows PowerShell输入指令查看版本时或者用脚手架创建vue项目时都有可能报错&#xff0c;报错信息如下&#xff1a;vue : 无法加载文件 F:\nodejs\vue.ps1&#xff0c;因为在此系统上禁止运行脚本 解决方案&#xff1a; 原因&#xff1a;因为Windows Po…

问道管理:金叉死叉十句口诀?

随着越来越多人参加股票买卖&#xff0c;关于股票商场的了解也变得越来越重要。其中一项重要的概念就是金叉死叉&#xff0c;这是一种均线穿插的现象&#xff0c;而均线穿插是技能剖析的重点之一。在本文中&#xff0c;咱们将会从多个角度剖析金叉死叉&#xff0c;并给出十句口…

Qt应用开发(基础篇)——MDI窗口 QMdiArea QMdiSubWindow

一、前言 QMdiArea类继承于QAbstractScrollArea&#xff0c;QAbstractScrollArea继承于QFrame&#xff0c;是Qt用来显示MDI窗口的部件。 滚屏区域基类 QAbstractScrollAreahttps://blog.csdn.net/u014491932/article/details/132245486 框架类 QFramehttps://blog.csdn.net/u01…

sqlite3用法

完成数据库的插入。 程序如下&#xff1a; 运行结果如下&#xff1a;

Python学习笔记_基础篇(十)_socket编程

本章内容 1、socket 2、IO多路复用 3、socketserver Socket socket起源于Unix&#xff0c;而Unix/Linux基本哲学之一就是“一切皆文件”&#xff0c;对于文件用【打开】【读写】【关闭】模式来操作。socket就是该模式的一个实现&#xff0c;socket即是一种特殊的文件&…

返回数组中最大(最小)值的位置(索引值),查找范围可以是所有元素,或者指定行列numpy.argmax()numpy.argmin()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 返回数组中最大(最小)值的位置(索引值)&#xff0c; 查找范围可以是所有元素&#xff0c;或者指定行列 numpy.argmax() numpy.argmin() [太阳]选择题 关于以下代码说法错误的一项是? import…

管理类联考——逻辑——真题篇——按知识分类——汇总篇——一、形式逻辑——模态——-句式转换——逻辑转换

第一节 模态命题-句式转换-逻辑转换 题-模态命题-句式转换-逻辑转换&#xff1a;①不一定不可能&#xff1b;②不一定可能不未必。 1.唐代韩愈在《师说》中指出&#xff1a;“孔子曰&#xff1a;三人行&#xff0c;则必有我师。是故弟子不必不如师&#xff0c;师不必贤于弟子…

Java 反射出现的背景和简单概述

反射出现的背景 Java程序中&#xff0c;所有的对象都有两种类型&#xff1a;编译时类型和运行时类型&#xff0c;而很多时候对象的编译时类型和运行时类型不一致。 编译时类型是指在编译时期确定的对象类型&#xff0c;它是根据声明时所使用的类型来确定的。编译时类型是在编…

Error = [Microsoft][SQL Server Native Client 10.0]无法打开 BCP 主数据文件(已解决)

需求&#xff1a;分页方式查出表中数据存入excel中&#xff0c;并给excel标记不同的编号 前提&#xff1a; #all_4表中数据已准备好 版本&#xff1a; sql server 2008 declare n int set n 1 while n 100 begin print n:print nEXEC master.dbo.sp_configure show adva…

C#实现图像选择验证码

开发环境&#xff1a;C#&#xff0c;VS2019&#xff0c;.NET Core 3.1&#xff0c;ASP.NET Core 前几年使用12306购买火车票时使用过这种验证码&#xff0c;根据文字描述选择对应的图片&#xff0c;文字是随机的&#xff0c;图片也是随机的。 1、建立一个验证码控制器 新建两个…

2023年国赛数学建模思路 - 案例:最短时间生产计划安排

文章目录 0 赛题思路1 模型描述2 实例2.1 问题描述2.2 数学模型2.2.1 模型流程2.2.2 符号约定2.2.3 求解模型 2.3 相关代码2.4 模型求解结果 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 最短时…

保姆级别讲解Python数据处理,你绝对能会

名字&#xff1a;阿玥的小东东 学习&#xff1a;Python、C/C 主页链接&#xff1a;阿玥的小东东的博客_CSDN博客-python&&c高级知识,过年必备,C/C知识讲解领域博主 目录 1. 文件读取 2. 数据处理 3. 处理结果输出 总的来说 为了咱们让程序跑起来&#xff0c;我们需…

如何使用CSS实现一个瀑布流布局?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 使用CSS实现瀑布流布局⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚…

前端笔记2023

border-radius: 数值px;圆角 flex布局 小程序中 display:flex; //flex布局 flex-direction:row/column; //规定主轴的方向&#xff1a;row/column justify-content:space-around; //元素在主轴方向的排列方式&#xff1a;flex-start/flex-end/space-around/space-betwee…

【动画】p61Mixamo自动绑定骨骼

Mixamo自动绑定骨骼 Mixamo Mixamo 虚幻商城搜索Mixamo Animation Retargeting 有两个版本4.14-4.27和5.0-5.2 优化&#xff1a; 程序方面优化 素材方面优化 光照方面优化 模型面数优化 材质方面优化 先获得一个模型 打开mixamo网站 导入模型 自动进入自动绑骨界面&#xf…

工厂方法模式【Factory Method Pattern】

前言 1.工厂模式概念 实例化对象&#xff0c;用工厂方法代替new操作(重点) 工厂模式包括工厂方法模式和抽象工厂模式 抽象工厂模式是工厂方法模式的扩展 2.什么情况下适合工厂模式 有一组类似的对象需要创建 在编码时不能预见需要创建哪种类的实例 系统需要考虑扩展性&#xff…

前端(十二)——深入理解和使用 async和await

&#x1f61b;博主&#xff1a;小猫娃来啦 &#x1f61b;文章核心&#xff1a;深入理解和使用 async和await 在 JS中&#xff0c;异步操作是无法避免的&#xff0c;而处理异步操作最常用的方法是使用回调函数或者 Promise。然而&#xff0c;自 ES2017 引入了 async/await 之后…

分布式版本控制系统(一)

分布式版本控制系统(一) 目录 分布式版本控制系统(一) 1、Git、Github、Gitlab 的区别2、Git 与 SVN 区别3、Git工作流程4、Git基本概念5、Git 客户端安装使用 5.1 git-server安装配置5.2 git-client配置免密登录git服务器5.3 文本编辑器5.4 差异分析工具5.5 查看配置信息5.6 常…