如何在云电脑串流中实现本地存储的访问——磁盘映射(目录映射)开发
通常来说桌面云(VDI)都是基于远程资源的本地化使用方案,我们使用的存储,计算等资源都位于集中管理服务器上面。对于终端使用客户来说,许多情况都是瘦客户端(瘦客户端是指一些价格比较低廉,提供键鼠输入,显示输出等并不提供核心计算能力的哑终端),通过远程的VDI协议,共享使用服务器主机资源。如下图(核心资源集中在云服务器端):
不过存在很多情况下,我们的资源并不是全部集中在服务器,比如如果我们的瘦客户终端是PC的话,我们希望能够把本地的磁盘直接映射到远程VDI虚拟机里面使用,那么就需要使用到我们的磁盘映射技术了。
脱离VDI场景,其实磁盘映射在平时工作和生活中的应用的是非常普遍的;例如:你有一个公有云服务器,里面存储你自己的一些资料;当然你希望无论是在家里,还是在公司,或者出差,你都可以像在本地一样使用你公有云服务器上面的磁盘;这样磁盘映射就是一个比较好的选择方案了。
本文我们就来探讨一下基于桌面云环境下的磁盘映射的技术实现(当然除了映射整个磁盘之外我们也可以针对单个目录进行映射)。
1. 技术概览
有很多的技术方案我们都可以实现,这里大致介绍一下比较常用的三种方案:
方案1——基于文件过滤驱动,将所有文件IO请求进行封装和转发:
对于方案一,我们要创建新的目录,并对文件IRP进行监控。一般来说可以对IRP_MJ_CREATE
做处理,如果目标目录是我们目录,需要将IRP转向到目标处理(这里是本地客户端的磁盘目录),并保存相关FILE_OBJECT
信息到上下文结构中;并再IRP_MJ_CLOSE
删除上下文的关联信息。虽然改方案看起来似乎比较简单,但是对于文件的各种操作本来就是非常复杂的,因此整个过程是非常复杂的,如果IRP请求处理不恰当将会导致异常,至少需要对如下IRP进行处理:
IRP_MJ_CREATE
IRP_MJ_CLEANUP
IRP_MJ_CLOSE
IRP_MJ_READ
IRP_MJ_WRITE
IRP_MJ_QUERY_INFORMATION
IRP_MJ_SET_INFORMATION
IRP_MJ_DIRECTORY_CONTROL
IRP_MJ_QUERY_VOLUME_INFORMATION
IRP_MJ_QUERY_SECURITY
方案2——基于虚拟总线技术,在虚拟总线上面挂载磁盘设备,将对于虚拟机所有磁盘的请求全部转发到本地真实磁盘:
对于第二种虚拟总线技术方案,我们可以再虚拟总线挂载一个虚拟的磁盘设备,对于该磁盘的所有请求都通过网络协议发到客户端进行处理,该方案存在比较明显的缺陷就是客户端和虚拟机无法同时对于磁盘使用,而且实现难度也非常大。
方案3——基于文件系统技术,实现类似分布式文件系统:
对于基于分布式文件系统的方案是一种比较合适的方案,我们从文件系统层面,对磁盘(目录)来进行映射(共享)访问。
我们使用IoRegisterFileSystem
来注册一个自己实现的文件系统
void IoRegisterFileSystem(
__drv_aliasesMem PDEVICE_OBJECT DeviceObject
);
对于Windows下面常用的NTFS文件系统,FAT32文件系统,也是通过IoRegisterFileSystem
向系统注册的。
对于文件系统应该处理哪些请求时比较复杂的,但是非常友好的一点时我们有很多的资料可以参考,例如我们可以看到fastfat文件系统注册的回调函数有如下:
NTSTATUS
NTAPI
DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
//...
DriverObject->MajorFunction[IRP_MJ_CREATE] = (PDRIVER_DISPATCH)FatFsdCreate;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = (PDRIVER_DISPATCH)FatFsdClose;
DriverObject->MajorFunction[IRP_MJ_READ] = (PDRIVER_DISPATCH)FatFsdRead;
DriverObject->MajorFunction[IRP_MJ_WRITE] = (PDRIVER_DISPATCH)FatFsdWrite;
DriverObject->MajorFunction[IRP_MJ_QUERY_INFORMATION] = (PDRIVER_DISPATCH)FatFsdQueryInformation;
DriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = (PDRIVER_DISPATCH)FatFsdSetInformation;
DriverObject->MajorFunction[IRP_MJ_QUERY_EA] = (PDRIVER_DISPATCH)FatFsdQueryEa;
DriverObject->MajorFunction[IRP_MJ_SET_EA] = (PDRIVER_DISPATCH)FatFsdSetEa;
DriverObject->MajorFunction[IRP_MJ_FLUSH_BUFFERS] = (PDRIVER_DISPATCH)FatFsdFlushBuffers;
DriverObject->MajorFunction[IRP_MJ_QUERY_VOLUME_INFORMATION] = (PDRIVER_DISPATCH)FatFsdQueryVolumeInformation;
DriverObject->MajorFunction[IRP_MJ_SET_VOLUME_INFORMATION] = (PDRIVER_DISPATCH)FatFsdSetVolumeInformation;
DriverObject->MajorFunction[IRP_MJ_CLEANUP] = (PDRIVER_DISPATCH)FatFsdCleanup;
DriverObject->MajorFunction[IRP_MJ_DIRECTORY_CONTROL] = (PDRIVER_DISPATCH)FatFsdDirectoryControl;
DriverObject->MajorFunction[IRP_MJ_FILE_SYSTEM_CONTROL] = (PDRIVER_DISPATCH)FatFsdFileSystemControl;
DriverObject->MajorFunction[IRP_MJ_LOCK_CONTROL] = (PDRIVER_DISPATCH)FatFsdLockControl;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = (PDRIVER_DISPATCH)FatFsdDeviceControl;
DriverObject->MajorFunction[IRP_MJ_SHUTDOWN] = (PDRIVER_DISPATCH)FatFsdShutdown;
DriverObject->MajorFunction[IRP_MJ_PNP] = (PDRIVER_DISPATCH)FatFsdPnp;
DriverObject->FastIoDispatch = &FatFastIoDispatch;
RtlZeroMemory(&FatFastIoDispatch, sizeof(FatFastIoDispatch));
FatFastIoDispatch.SizeOfFastIoDispatch = sizeof(FAST_IO_DISPATCH);
FatFastIoDispatch.FastIoCheckIfPossible = FatFastIoCheckIfPossible; // CheckForFastIo
FatFastIoDispatch.FastIoRead = FsRtlCopyRead; // Read
FatFastIoDispatch.FastIoWrite = FsRtlCopyWrite; // Write
FatFastIoDispatch.FastIoQueryBasicInfo = FatFastQueryBasicInfo; // QueryBasicInfo
FatFastIoDispatch.FastIoQueryStandardInfo = FatFastQueryStdInfo; // QueryStandardInfo
FatFastIoDispatch.FastIoLock = FatFastLock; // Lock
FatFastIoDispatch.FastIoUnlockSingle = FatFastUnlockSingle; // UnlockSingle
FatFastIoDispatch.FastIoUnlockAll = FatFastUnlockAll; // UnlockAll
FatFastIoDispatch.FastIoUnlockAllByKey = FatFastUnlockAllByKey; // UnlockAllByKey
FatFastIoDispatch.FastIoQueryNetworkOpenInfo = FatFastQueryNetworkOpenInfo;
FatFastIoDispatch.AcquireForCcFlush = FatAcquireForCcFlush;
FatFastIoDispatch.ReleaseForCcFlush = FatReleaseForCcFlush;
FatFastIoDispatch.MdlRead = FsRtlMdlReadDev;
FatFastIoDispatch.MdlReadComplete = FsRtlMdlReadCompleteDev;
FatFastIoDispatch.PrepareMdlWrite = FsRtlPrepareMdlWriteDev;
FatFastIoDispatch.MdlWriteComplete = FsRtlMdlWriteCompleteDev;
//...
}
从上面我们可以也可以发现,一个分布式文件系统的实现也是非常复杂的。
实现了文件系统之后,我们需要对一个磁盘进行访问,那么我们就需要一个挂载点。对于所有文件操作的起点,我们都可以通过CreateFile
来进行跟踪,当我们打一个一个文件的时候,其实就是打开指定目录空间中的一个内核设备对象。
我们使用IoCreateDevice
来创建一个设备对象,这个函数声明如下:
NTSTATUS IoCreateDevice(
PDRIVER_OBJECT DriverObject,
ULONG DeviceExtensionSize,
PUNICODE_STRING DeviceName,
DEVICE_TYPE DeviceType,
ULONG DeviceCharacteristics,
BOOLEAN Exclusive,
PDEVICE_OBJECT *DeviceObject
);
对于这个函数有一个比较重要的参数就是DeviceType
,例如有如下值:
#define FILE_DEVICE_8042_PORT 0x00000027
#define FILE_DEVICE_ACPI 0x00000032
#define FILE_DEVICE_BATTERY 0x00000029
#define FILE_DEVICE_BEEP 0x00000001
#define FILE_DEVICE_BUS_EXTENDER 0x0000002a
#define FILE_DEVICE_CD_ROM 0x00000002
#define FILE_DEVICE_CD_ROM_FILE_SYSTEM 0x00000003
#define FILE_DEVICE_CHANGER 0x00000030
#define FILE_DEVICE_CONTROLLER 0x00000004
#define FILE_DEVICE_DATALINK 0x00000005
#define FILE_DEVICE_DFS 0x00000006
#define FILE_DEVICE_DFS_FILE_SYSTEM 0x00000035
#define FILE_DEVICE_DFS_VOLUME 0x00000036
#define FILE_DEVICE_DISK 0x00000007
#define FILE_DEVICE_DISK_FILE_SYSTEM 0x00000008
#define FILE_DEVICE_DVD 0x00000033
#define FILE_DEVICE_FILE_SYSTEM 0x00000009
#define FILE_DEVICE_FIPS 0x0000003a
#define FILE_DEVICE_FULLSCREEN_VIDEO 0x00000034
#define FILE_DEVICE_INPORT_PORT 0x0000000a
#define FILE_DEVICE_KEYBOARD 0x0000000b
#define FILE_DEVICE_KS 0x0000002f
#define FILE_DEVICE_KSEC 0x00000039
#define FILE_DEVICE_MAILSLOT 0x0000000c
当我们使用FILE_DEVICE_DISK
参数的时候,就会绑定文件系统,我们可以看一下这个设备对象的结构内容:
kd> dt nt!_DEVICE_OBJECT 87cd2520
+0x000 Type : 0n3
+0x002 Size : 0x268
+0x004 ReferenceCount : 0n3
+0x008 DriverObject : 0x86ea1e60 _DRIVER_OBJECT
+0x00c NextDevice : 0x86ea18c0 _DEVICE_OBJECT
+0x010 AttachedDevice : (null)
+0x014 CurrentIrp : (null)
+0x018 Timer : (null)
+0x01c Flags : 0x50
+0x020 Characteristics : 0
+0x024 Vpb : 0x87cd22b8 _VPB
+0x028 DeviceExtension : 0x87cd25d8 Void
+0x02c DeviceType : 7
+0x030 StackSize : 1 ''
+0x034 Queue : <unnamed-tag>
+0x05c AlignmentRequirement : 0
+0x060 DeviceQueue : _KDEVICE_QUEUE
+0x074 Dpc : _KDPC
+0x094 ActiveThreadCount : 0
+0x098 SecurityDescriptor : 0x99222d18 Void
+0x09c DeviceLock : _KEVENT
+0x0ac SectorSize : 0x200
+0x0ae Spare1 : 0
+0x0b0 DeviceObjectExtension : 0x87cd2788 _DEVOBJ_EXTENSION
+0x0b4 Reserved : (null)
kd> dx -id 0,0,863d1888 -r1 ((ntkrpamp!_VPB *)0x87cd22b8)
((ntkrpamp!_VPB *)0x87cd22b8) : 0x87cd22b8 [Type: _VPB *]
[+0x000] Type : 10 [Type: short]
[+0x002] Size : 88 [Type: short]
[+0x004] Flags : 0x1 [Type: unsigned short]
[+0x006] VolumeLabelLength : 0xa [Type: unsigned short]
[+0x008] DeviceObject : 0x87ce1398 : Device for "\FileSystem\Nanos" [Type: _DEVICE_OBJECT *]
[+0x00c] RealDevice : 0x87cd2520 : Device for "\FileSystem\Nanos" [Type: _DEVICE_OBJECT *]
[+0x010] SerialNumber : 0x19831116 [Type: unsigned long]
[+0x014] ReferenceCount : 0x2 [Type: unsigned long]
[+0x018] VolumeLabel : "NANOS" [Type: wchar_t [32]]
如果熟悉驱动开发就会知道,当解析设备名称的时候就会调用IopParseDevice
进行设备名称解析,这个函数就会对Vpb
结果进行处理,从而将相关操作交给我们文件系统驱动程序来处理。
2. 磁盘映射实现
上面我们大致描述了磁盘映射的实现技术概要,这里我们看一下具体实现架构图,如下:
在这里我们需要实现三大模块:
- NanoDisk.sys——这个是我们整个磁盘映射的核心,我们的磁盘映射文件系统在此模块中实现。
- NanoDiskAgent.exe——这个是我们远程云协议的封装的守护进程,主要是将服务端的请求通过协议封装,然后通过私有协议发送给远程客户端。
- NanoDiskClient.exe——这个是磁盘映射的客户端守护进程,该进程的主要作用就是连接NanoDiskAgent.exe,接收NanoDiskAgent.exe的数据包请求,然后将请求数据包解析操作真实文件,再将结果返回。
这里我们以文件读请求为例,当应用程序读取文件的时候产生一个IRP_MJ_READ
的请求,交给我们驱动程序处理
NTSTATUS
DispatchRead(__in PDEVICE_OBJECT DeviceObject, __in PIRP Irp)
{
//...
status = RegisterPendingIrp(DeviceObject, Irp);
//...
}
NTSTATUS
RegisterPendingIrp(__in PDEVICE_OBJECT DeviceObject, __in PIRP Irp)
{
PVCB_CONTEXT vcb = DeviceObject->DeviceExtension;
NTSTATUS status;
if (GetIdentifierType(vcb) != VCB)
{
return STATUS_INVALID_PARAMETER;
}
status = RegisterPendingIrp(DeviceObject, Irp);
//...
return status;
}
NTSTATUS
RegisterPendingIrp(__in PDEVICE_OBJECT DeviceObject, __in PIRP Irp)
{
//...
InitializeListHead(&irpEntry->ListEntry);
//...
IoMarkIrpPending(RequestContext->Irp);
InsertTailList(&IrpList->ListHead, &irpEntry->ListEntry);
//...
return STATUS_PENDING;
}
在RegisterPendingIrp
函数中,主要将IRP放入到处理队列中,当队列中的IRP在远端完成之后,再完成IRP。
VOID MainWorkerLoop(__in PIRP_LIST PendingIrp, __in PIRP_LIST NotifyEvent)
{
//...
RtlCopyMemory(buffer, &DriverContext->EventContext, eventLen);
//...
DokanCompleteIrpRequest(irp, irp->IoStatus.Status,
irp->IoStatus.Information);
//...
}
NanoDiskAgent.exe有一个专门的核心工作线程从队列中取出IRP,并解析IRP的数据包,将IRP包转发到客户端进行处理,如下是NanoDiskAgent.exe发起请求的过程:
VOID DispatchRead(HANDLE Handle, PEVENT_CONTEXT EventContext)
{
SendIoEvent(Handle, EventInfo, EventLength);
}
VOID SendIoEvent(HANDLE Handle, PEVENT_INFORMATION EventInfo, ULONG EventLength)
{
//...
status = DeviceIoControl(Handle,
IOCTL_EVENT_READ,
EventInfo,
EventLength,
NULL,
0,
&ReturnedLength,
NULL
);
//...
}
在我们的内核驱动中,就会对IOCTL_EVENT_READ
请求做处理,从文件系统驱动中取出IRP,并解析数据封包传递给用户层,完成此次IO请求。
3. 实现效果
通过上面技术实现,当我们使用客户机连接集中服务器的虚拟机的时候,我们就可以在虚拟机里面使用客户机的本地磁盘资源了。
当然如果你愿意,你也可以反向部署,在客户端直接使用远程虚拟机的资源。更加通用的场景,我们可以使用公有存储资源,统一共享映射磁盘,做到一个存储磁盘,给所有人共享使用,而且使用体验和本地磁盘一致。
例如,我们将本地客户机的C盘通过磁盘映射的方式映射到虚拟机里面(X盘),如下图所示:
我们可以对映射的磁盘做任何操作,体验跟本地磁盘完全一致,读取速度取决于网速,如下图所示: