Storport is a welcome relief to storage driver writers wishing to develop a driver that exports a virtual device. This third and final article in a series, completes the development of the Virtual Storport Miniport Driver that we started in the earlier two issues of The NT Insider.
A Short Recap
In our previous articles we discussed that our example driver design was divided into 2 parts, the upper-edge which handled the Storport interaction, and the lower-edge that implemented the virtual SCSI Adapter and devices (see Figure 1). This article continues to discuss the specifics of our example driver’s implementation.
In our last article we ended our discussion mentioning 2 points:
- The Virtual Storport Miniport does not use IOCTL_SCSI_ MINIPORT requests for configuration since this handler would be called at IRQL DISPATCH_LEVEL, and
- To define a SCSI device our driver must respond to Storport with an INQUIRYDATA, when requested.
Given those 2 points we’ll continue by first describing how the driver handles user configuration requests and then describe how to respond to a Storport request for INQUIRYDATA.
Handling User Mode Requests
Because the example Virtual Storport Miniport driver that we’re developing needs to respond to user mode requests to configure devices, it needs some way to receive those user requests and also be at IRQL PASSIVE_LEVEL. Being at IRQL PASSIVE_LEVEL is required because our design needs to be able to interact with the Windows file systems. Since IOCTL_SCSI_MINPORT doesn’t meet the IRQL criteria, we use a new IOCTL that was added for Storport called IOCTL_MINIPORT_ PROCESS_SERVICE_IRP. The documentation for this IOCTL indicates that it is used by a user-mode application or kernel-mode driver that requires notification when something of interest happens in the virtual miniport. Our use of this might fall a bit outside of its intended use, but it works. Processing of these requests is handled by the HwProcessServiceRequest function that the driver set in the VIRTUAL_HW_INITIALIZATION_DATA before it called StorPort Initialize in its DriverEntry routine. The code for this function is shown in Figure 2.
VOID OsrHwProcessServiceRequest(IN PVOID PDevExt,IN PVOID PIrp) { PIRP pIrp = (PIRP) PIrp; PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(pIrp); NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST; POSR_DEVICE_EXTENSION pDevExt = (POSR_DEVICE_EXTENSION) PDevExt; OsrTracePrint(TRACE_LEVEL_VERBOSE,OSRVMINIPT_DEBUG_FUNCTRACE, ("OsrHwProcessServiceRequest Enter\n")); if(irpSp->MajorFunction == IRP_MJ_DEVICE_CONTROL) { __try { status = OsrUserProcessIoCtl(pDevExt->PUserGlobalInformation,pIrp); } __except(EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); } } if(status != STATUS_PENDING) { pIrp->IoStatus.Status = status; IoCompleteRequest(pIrp,IO_NO_INCREMENT); } OsrTracePrint(TRACE_LEVEL_VERBOSE,OSRVMINIPT_DEBUG_FUNCTRACE, ("OsrHwProcessServiceRequest Exit\n")); }
You will notice that the driver ensures that it has received an IRP_MJ_DEVICE_CONTROL request before passing it off to its lower-edge OsrUserProcessIoCtl routine. This routine processes user mode requests that have data in the buffer supplied in the AssociatedIrp.SystemBuffer field of the IRP. This input buffer contains a command code as well as the parameters needed for the command. The commands the example driver processes are:
- IOCTL_OSRVMPORT_SCSIPORT – used to confirm that this is indeed the OSRVMPORT driver.
- IOCTL_OSRVMPORT_CONNECT – used to connect a new SCSI device.
- IOCTL_OSRVMPORT_DISCONNECT – used to disconnect an existing SCSI device.
- IOCTL_OSRVMPORT_GETACTIVELIST – used to enumerate the list of active SCSI devices.
We’ll discuss IOCTL_OSRVMPORT_CONNECT and IOCTL_OSRVMPORT_DISCONNECT in this article. You can learn more about the implementation of the other IOCTLs by reading the code.
IOCTL_OSRVMPORT_CONNECT
This IOCTL is used by a caller to request the Virtual Storport Miniport Driver create a new SCSI device based upon provided input information. The caller initializes a CONNECT_IN structure and passes it to our driver as the input buffer of the IOCTL_OSRVMPORT_CONNECT IOCTL sent via a Win32 DeviceIoControl request. The CONNECT_IN structure is defined in Figure 3. The CONNECT_IN structure allows the user to specify the full path name of the file to be used to back the virtual SCSI device that’s being created. It also allows the user to indicate whether or not the device is to be CDROM, and if not, the driver treats the device as a disk. Finally, the user can specify whether or not the media is read-only (the example treats all CDROM devices as read-only by default).
#define MAX_NAME_LENGTH 256 typedef struct _CONNECT_IN { COMMAND_IN Command; WCHAR PathName[MAX_NAME_LENGTH]; BOOLEAN ReadOnly; BOOLEAN Cdrom; } CONNECT_IN, *PCONNECT_IN;
From this input information the driver, creates the CONNECTION_LIST_ENTRY structure, shown in Figure 4 , which is used to represent the connection to the media it is using to back the device. The code then calls OsrSPCreateScsiDevice, a routine in the “Virtual Storport Miniport Processing” part of our driver (what we refer to as the upper-edge), to create an OSR_VM_DEVICE structure, shown in Figure 6, which is used to internally represent the device. Notice that the OSR_VM_DEVICE structure contains a pointer to an INQUIRYDATA block, described in Part II of this series, which is used to describe our device to Storport. Once these are done, all the driver has to do is announce to Storport that our virtual storage bus has changed by calling the upper-edge routine OsrSPAnnounceArrival, which calls StorportNotification indicating BusChangeDetected. Sometime after doing this Storport will call back into our driver at the OsrHwStartIo handler with a SRB_FUNCTION_EXECUTE_SCSI request, which will in turn call our OsrVmExecuteScsi routine shown in Figure 5.
typedef struct _CONNECTION_LIST_ENTRY { LIST_ENTRY ListEntry; WCHAR FileName[MAX_NAME_LENGTH*2]; HANDLE FileHandle; UCHAR FileAttributes[sizeof(FILE_ALL_INFORMATION)+MAX_NAME_LENGTH*2]; ULONG FileType; PFILE_OBJECT FileObject; struct _USER_INSTANCE_INFO* PIInfo; ULONG BusIndex; ULONG TargetIndex; ULONG LunIndex; BOOLEAN HandleClosed; BOOLEAN Connected; BOOLEAN ContainingMediaRemoved; BOOLEAN UNCConnection; PVOID PnPNotificationEntry; ULONG IdentifierIndex; BOOLEAN Closing; /* Indicates connection is closing */ CONNECT_IN ConnectionInfo; } CONNECTION_LIST_ENTRY, *PCONNECTION_LIST_ENTRY;
UCHAR OsrVmExecuteScsi(IN POSR_DEVICE_EXTENSION PDevExt, IN PSCSI_REQUEST_BLOCK PSrb, IN PBOOLEAN PComplete) { POSR_LU_EXTENSION luExt; UCHAR srbStatus = SRB_STATUS_INVALID_REQUEST; NTSTATUS status; PCDB pCdb = (PCDB) &PSrb->Cdb; POSR_VM_DEVICE pOsrDevice; *PComplete = TRUE; luExt = (POSR_LU_EXTENSION) StorPortGetLogicalUnit(PDevExt, PSrb->PathId, PSrb->TargetId, PSrb->Lun ); if(!luExt) { return SRB_STATUS_NO_DEVICE; } pOsrDevice = FindOsrVmDevice(luExt,PDevExt,PSrb->PathId, PSrb->TargetId, PSrb->Lun,FALSE); if(pOsrDevice && pOsrDevice->PUserLocalInformation) { InterlockedIncrement(&pOsrDevice->OutstandingIoCount); status = OsrUserHandleSrb(pOsrDevice->PUserLocalInformation,PSrb); if(status == STATUS_PENDING) { *PComplete = FALSE; srbStatus = SRB_STATUS_PENDING; } else { InterlockedDecrement(&pOsrDevice->OutstandingIoCount); srbStatus = PSrb->SrbStatus; } } else { srbStatus = SRB_STATUS_NO_DEVICE; } return srbStatus; }
When OsrVmExecuteScsi is called it will call FindOsrVmDevice, shown in Figure 6. This routine will find the device, i.e. an OSR_VM_DEVICE structure corresponding to the input PathId, TargetId, and Lun (Which was created when we called OsrSpCreateDevice). Finding this structure will result in a call to OsrUserHandleSrb to handle the SRB targeted at the device. We will talk about OsrUserHandleSrb later in this article.
// // This represents a device that has been detected on a specific bus,target, // and lun. PUserLocalInformation represents the handle given to us // by the lower layer of our code that implements the adapter and scsi devices. // typedef struct _OSR_VM_DEVICE { ULONG MagicNumber; LIST_ENTRY ListEntry; PVOID PUserLocalInformation; ULONG PathId; ULONG TargetId; ULONG Lun; PINQUIRYDATA PInquiryData; BOOLEAN BReadOnlyDevice; BOOLEAN Missing; PVOID PDevExt; LONG OutstandingIoCount; BOOLEAN ReportedMissing; } OSR_VM_DEVICE, *POSR_VM_DEVICE;
IOCTL_OSRVMPORT_DISCONNECT
This IOCTL is used by a caller to request that the Virtual Storport Miniport Driver disconnects an existing SCSI device based upon supplied input information. To do this the caller initializes a CONNECT_IN structure and passes it to the driver as the input buffer of the IOCTL_OSRVMPORT_ DISCONNECT IOCTL sent via a Win32 DeviceIoControl request. The CONNECT_IN structure, previously discussed and shown in Figure 3, is also used for DISCONNECT processing. It allows the user to specify the full path name of the file used to back the SCSI device that is to be disconnected.
If the input information is valid, the IOCTL handler will search thru the list of connected devices and return the CONNECT_LIST_ENTRY of the device to be disconnected. Upon getting this entry, the code calls the OsrSPAnnounceDeparture in the upper-edge routines which will set the Missing field in the OSR_VM_DEVICE structure. The driver then calls StorportNotification indicating BusChangeDetected. As described in the previous section, calling the StorportNotification routine will result in Storport sending a request back into the driver at its OsrHwStartIo handler in attempt to identify all devices attached to the drivers’ virtual bus. As the driver responds to each call, it will call FindOsrVmDevice, shown in Figure 7, to determine if it has a valid device corresponding to the PathId, TargetId, and Lun being queried. As you can see below, if the driver finds a match in its list, it looks at the Missing field in the structure. If this is set to TRUE, then the driver knows that the device is being deleted and will indicate that to Storport. In addition, it will set the ReportedMissing field which indicates to DeleteDevicesThread that the structure can be removed from the list and deleted because Storport was notified that the device is no longer present.
POSR_VM_DEVICE FindOsrVmDevice(IN POSR_LU_EXTENSION LuExt, IN POSR_DEVICE_EXTENSION PDevExt, IN UCHAR PathId, IN UCHAR TargetId, IN UCHAR Lun, IN BOOLEAN ReturnMissing) { KIRQL lockHandle; POSR_VM_DEVICE pDevice = NULL; OsrTracePrint(TRACE_LEVEL_VERBOSE,OSRVMINIPT_DEBUG_FUNCTRACE,(__FUNCTION__": Entered\n")); OsrAcquireSpinLock(&PDevExt->DeviceListLock,&lockHandle); for(PLIST_ENTRY pEntry = PDevExt->DeviceList.Flink; pEntry != &PDevExt->DeviceList; pEntry = pEntry->Flink) { pDevice = (POSR_VM_DEVICE) CONTAINING_RECORD(pEntry,OSR_VM_DEVICE,ListEntry); OSR_VM_DEVICE_VALID(pDevice); if(pDevice->PathId == PathId && pDevice->TargetId == TargetId && pDevice->Lun == Lun) { if(!pDevice->Missing) { if(LuExt && !LuExt->OsrVmDevice) { LuExt->OsrVmDevice = pDevice; } } else if(!ReturnMissing) { if(!pDevice->ReportedMissing) { OsrTracePrint(TRACE_LEVEL_INFORMATION,OSRVMINIPT_DEBUG_PNP_INFO, (__FUNCTION__": %p Reported Missing, signaling DeleteDevices Thread\n", pDevice)); pDevice->ReportedMissing = TRUE; KeSetEvent(&PDevExt->DeleteDevicesThreadWorkEvent,8,FALSE); } pDevice = NULL; } break; } pDevice = NULL; } OsrReleaseSpinLock(&PDevExt->DeviceListLock,lockHandle); OsrTracePrint(TRACE_LEVEL_VERBOSE,OSRVMINIPT_DEBUG_FUNCTRACE,(__FUNCTION__": Exit\n")); return pDevice; }
So now that we know how a SCSI device is added and removed, the only thing we really need to talk about is how the driver will handle a SRB_FUNCTION_EXECUTE_ SCSI request for a device that is connected to the driver’s virtual bus. As mentioned previously, these requests are processed by the driver’s lower-edge in OsrUserHandleSrb, which we’ll talk about in the next section.
OsrUserHandleSrb
The OsrUserHandleSrb routine is the code in the lower-edge of the driver that performs the operation described by the CDB that’s contained in the received SRB. One issue to be aware of is that commands contained within the CDB come in different sizes. The size of the CDB is contained within the SRB. No matter what the size of the CDB is, the first byte of the CDB contains the operation code, and given the operation code, the code can determine how to interpret the rest of the CDB. Another issue to be aware of is that the operations received depend on the type of device being exported, and the completion statuses that the driver returns must be a SRB_STATUS_XXXXX and be returned in the SrbStatus field of the SRB.
Before continuing, we should note an important limitation of the example code presented. The example does not handle the necessary SCSI Operation codes for disks with capacities greater than 2.2 TB. Without this support, the example will handle virtual volumes less than or equal to 2.2TB in size, which we expect will be large enough for just about all purposes. Support for disks greater than 2.2TB first appeared in Windows (for secondary volumes only) starting in Windows Vista. For the example to handle a disk larger than 2.2TB it would have to handle at least the 16-byte variants of the Read, Write and Read Capacity commands. There may be others. In order to determine which 16-byte CDB commands need to be supported, the reader can examine the Disk and ClassPnP code contained with the Win 7 WDK “SRC\STORAGE\CLASS” directory.
Let’s take a look of some of the functions that we support in the driver implementation. For the functions not discussed here, please read the source code.
SCSIOP_INQUIRY (0x12)
As mentioned previously, a SCSI device is described by an INQUIRYDATA structure, and this structure is retrieved by Storport via a SCSIOP_INQUIRY request. The example driver code to handle this request is shown in Figure 8. As you can see, the data buffer which is used to return the INQUIRYDATA is obtained from the SRB by calling the upper-edge function OsrSpGetSrbDataAddress. The data that the driver returns in that buffer depends upon the device(s) the driver is exporting, which in the example driver is either a disk or cdrom. You may notice that the only real difference in the return data is the DeviceType, VendorId, and ProductId: The drive returns READ_ONLY_DIRECT_ ACCESS_DEVICE for cdrom versus DIRECT_ACCESS_ DEVICE for disk.
case SCSIOP_INQUIRY : {// 0x12 PCDB pCdb = (PCDB) &PSrb->Cdb; PUCHAR pBuffer = (PUCHAR) OsrSpGetSrbDataAddress(pIInfo->OsrSpLocalHandle,PSrb); PINQUIRYDATA pInquiryData; if(!pBuffer || PSrb->DataTransferLength < INQUIRYDATABUFFERSIZE) { status = STATUS_INSUFFICIENT_RESOURCES; PSrb->SrbStatus = SRB_STATUS_ERROR; goto completeRequest; } pInquiryData = (PINQUIRYDATA) pBuffer; // Fill in the correct Inquiry Data based on the type of device we are emulating. if(pIInfo->StorageType == OsrCdrom) { pInquiryData->DeviceType = READ_ONLY_DIRECT_ACCESS_DEVICE; pInquiryData->DeviceTypeQualifier = DEVICE_CONNECTED; pInquiryData->DeviceTypeModifier = 0; pInquiryData->RemovableMedia = TRUE; pInquiryData->Versions = 2; // SCSI-2 support pInquiryData->ResponseDataFormat = 2; // Same as Version?? according to SCSI book pInquiryData->Wide32Bit = TRUE; // 32 bit wide transfers pInquiryData->Synchronous = TRUE; // Synchronous commands pInquiryData->CommandQueue = FALSE; // Does not support tagged commands pInquiryData->LinkedCommands = FALSE; // No Linked Commands RtlCopyMemory((PUCHAR) &pInquiryData->VendorId[0],OSR_INQUIRY_VENDOR_ID_CDROM, strlen(OSR_INQUIRY_VENDOR_ID_CDROM)); RtlCopyMemory((PUCHAR) &pInquiryData->ProductId[0],OSR_INQUIRY_PRODUCT_ID_CDROM, strlen(OSR_INQUIRY_PRODUCT_ID_CDROM)); RtlCopyMemory((PUCHAR) &pInquiryData->ProductRevisionLevel[0],OSR_INQUIRY_PRODUCT_REVISION, strlen(OSR_INQUIRY_PRODUCT_REVISION)); } else { // The media is now either an OSR Disk or a regular disk, // either waywe return the same information. ASSERT(pIInfo->StorageType == OsrDisk); pInquiryData->DeviceType = DIRECT_ACCESS_DEVICE; pInquiryData->DeviceTypeQualifier = DEVICE_CONNECTED; pInquiryData->DeviceTypeModifier = 0; pInquiryData->RemovableMedia = FALSE; pInquiryData->Versions = 2; // SCSI-2 support pInquiryData->ResponseDataFormat = 2; // Same as Version?? according to SCSI book pInquiryData->Wide32Bit = TRUE; // 32 bit wide transfers pInquiryData->Synchronous = TRUE; // Synchronous commands pInquiryData->CommandQueue = FALSE; // Does not support tagged commands pInquiryData->LinkedCommands = FALSE; // No Linked Commands RtlCopyMemory((PUCHAR) &pInquiryData->VendorId[0],OSR_INQUIRY_VENDOR_ID, strlen(OSR_INQUIRY_VENDOR_ID)); RtlCopyMemory((PUCHAR) &pInquiryData->ProductId[0],OSR_INQUIRY_PRODUCT_ID, strlen(OSR_INQUIRY_PRODUCT_ID)); RtlCopyMemory((PUCHAR) &pInquiryData->ProductRevisionLevel[0],OSR_INQUIRY_PRODUCT_REVISION, strlen(OSR_INQUIRY_PRODUCT_REVISION)); } status = STATUS_SUCCESS; PSrb->SrbStatus = SRB_STATUS_SUCCESS; goto completeRequest; }
SCSIOP_MODE_SENSE (0x1A)
The SCSIOP_MODE_SENSE command is used by the Windows class drivers to retrieve more detailed information about a detected device. Since the device is either a generic disk or generic cdrom, we opted to support and return minimal information. We determined what that information was by looking at the Disk and Cdrom class driver implementations contained within the WDK’s “SRC\STORAGE\CLASS” directories. The driver’s processing for this command is shown in Figure 9. The important points to notice in this function are where the driver returns MODE_DSP_WRITE_ PROTECT, which tells the requestor that this media is read-only and the MODE_PAGE_CAPABILITIES handler where the driver returns capabilities information if the device is a cdrom.
case SCSIOP_MODE_SENSE : // 0x1A { // We have received a MODE_SENSE command. We need to // jury rig something up here.... We're returning // the bare MINIMUM (as I know it now) information // required. If we need more we'll add it here. // PCDB pCdb = (PCDB) &PSrb->Cdb; PMODE_PARAMETER_HEADER pModeHeader; PUCHAR pBuffer = (PUCHAR) OsrSpGetSrbDataAddress(pIInfo->OsrSpLocalHandle,PSrb); if(!pBuffer) { status = STATUS_INSUFFICIENT_RESOURCES; PSrb->SrbStatus = SRB_STATUS_ERROR; goto completeRequest; } pModeHeader = (PMODE_PARAMETER_HEADER) pBuffer; switch(pCdb->MODE_SENSE.PageCode) { case MODE_SENSE_CURRENT_VALUES: { pModeHeader->ModeDataLength = sizeof(MODE_PARAMETER_HEADER) + sizeof(MODE_PARAMETER_BLOCK); pModeHeader->MediumType = 0; __try { if(OsrUserIsDeviceReadOnly(pIInfo)) { if(pIInfo->ConnectionInformation->ConnectionInfo.Cdrom) { pModeHeader->DeviceSpecificParameter = MODE_DSP_WRITE_PROTECT; // readonly device } else { pModeHeader->DeviceSpecificParameter = MODE_DSP_WRITE_PROTECT; // readonly device } } else { pModeHeader->DeviceSpecificParameter = 0; // Writeable Device } } __except(EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); PSrb->SrbStatus = SRB_STATUS_ERROR; goto completeRequest; } pModeHeader->BlockDescriptorLength = sizeof(MODE_PARAMETER_BLOCK); PMODE_PARAMETER_BLOCK pModeBlock = (PMODE_PARAMETER_BLOCK) (pBuffer + sizeof(MODE_PARAMETER_HEADER)); RtlZeroMemory(pModeBlock,sizeof(MODE_PARAMETER_BLOCK)); } break; case MODE_PAGE_CAPABILITIES: if(pIInfo->ConnectionInformation->ConnectionInfo.Cdrom) { PCDVD_CAPABILITIES_PAGE pCapBlock = (PCDVD_CAPABILITIES_PAGE) (pBuffer +sizeof(MODE_PARAMETER_HEADER10)); pModeHeader->ModeDataLength = sizeof(MODE_PARAMETER_HEADER) + sizeof(CDVD_CAPABILITIES_PAGE); RtlZeroMemory(pCapBlock,sizeof(CDVD_CAPABILITIES_PAGE)); pCapBlock->PageCode = MODE_PAGE_CAPABILITIES; pCapBlock->PageLength = 0x18; pCapBlock->CDRRead = TRUE; pCapBlock->CDERead = TRUE; pCapBlock->Method2 = TRUE; break; } default: pModeHeader->ModeDataLength = sizeof(MODE_PARAMETER_HEADER); pModeHeader->MediumType = 0; __try { if(OsrUserIsDeviceReadOnly(pIInfo)) { if(pIInfo->ConnectionInformation->ConnectionInfo.Cdrom) { pModeHeader->DeviceSpecificParameter = MODE_DSP_WRITE_PROTECT; // readonly device } else { pModeHeader->DeviceSpecificParameter = MODE_DSP_WRITE_PROTECT; // readonly device } } else { pModeHeader->DeviceSpecificParameter = 0; // Writeable Device } } __except(EXCEPTION_EXECUTE_HANDLER) { NTSTATUS status = GetExceptionCode(); PSrb->SrbStatus = SRB_STATUS_ERROR; goto completeRequest; } pModeHeader->BlockDescriptorLength = 0; break; } status = STATUS_SUCCESS; PSrb->SrbStatus = SRB_STATUS_SUCCESS; goto completeRequest; }
SCSIOP_READ_CAPACITY (0x25)
This function is used by a caller to determine the capacity of the connected device. The caller must return the number of blocks on the device and the block size. As with every other command the information returned depends on the device. The drivers’ handling for this function is shown in Figure 10.
case SCSIOP_READ_CAPACITY : // 0x25 { ULONG numBlocks; ULONG bytesPerBlock; // // Someone has asked us to read the disk capacity of the device, // so here we need to return to the caller the information about // the SPECIAL disk we represent. Sooo here we go. // PREAD_CAPACITY_DATA pCapacityData = (PREAD_CAPACITY_DATA) PSrb->DataBuffer; __try { OsrUserGetDiskCapacity(pIInfo,&numBlocks,&bytesPerBlock); } __except(EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); PSrb->SrbStatus = SRB_STATUS_ERROR; goto completeRequest; } REVERSE_BYTES(&pCapacityData->LogicalBlockAddress,&numBlocks); REVERSE_BYTES(&pCapacityData->BytesPerBlock,&bytesPerBlock); // // Set status in Irp and in the SRB to indicate that the function was successful. // status = STATUS_SUCCESS; PSrb->SrbStatus = SRB_STATUS_SUCCESS; goto completeRequest; }
SCSIOP_READ (0x28) and SCSIOP_WRITE (0x2A)
The SCSIOP_READ and SCSIOP_WRITE functions, as the names imply, are the functions issued to perform read and write functions on the device, respectively. The caller specifies the logical block number from which to start the operation in the input CDB along with the number of blocks to read or write. The caller also specifies an MDL that describes a buffer used for the read/write data. It will then be the driver’s responsibility to translate the input information into something that makes sense for the device the driver is emulating. Whether the driver is emulating a disk or CD-ROM, this will entail translating the operation into a read from or a write to the file that the driver is using to back the device. We have shown only the SCSIOP_READ handler in Figure 11 since the write handler is almost exactly the same. We’ll discuss the actual implementation of the read and write code later in this article, but until then there is one point for you to notice: This function does not complete the SRB, but instead returns STATUS_PENDING, which will cause OsrUserHandleSrb to return SRB_STATUS_PENDING to Storport. This status indicates to Storport that the command has been accepted by the driver but is not yet complete. Any other return status would indicate that the command is complete.
case SCSIOP_READ : // 0x28 { // We have received a read request. Process the read. ULARGE_INTEGER startingLbn = {0,0}; ULONG readLength = 0; PCDB pReadCdb = (PCDB) &PSrb->Cdb[0]; ULONG bytesRead; ULONG numBlocks; ULONG bytesPerBlock; __try { OsrUserGetDiskCapacity(pIInfo,&numBlocks,&bytesPerBlock); // Convert the starting LBN back to little endian. // Convert the LBN to a byte offset instead of a block offset. REVERSE_BYTES(&startingLbn.LowPart,&pReadCdb->CDB10.LogicalBlockByte0) startingLbn.QuadPart *= bytesPerBlock; // Convert the read length back to little endian REVERSE_2BYTES(&readLength,&pReadCdb->CDB10.TransferBlocksMsb); readLength *= bytesPerBlock; // Issue the read to ScsiPortUser. PMDL readMdl = OsrSpGetSrbMdl(pIInfo->OsrSpLocalHandle,PSrb); if(!readMdl) { status = STATUS_INSUFFICIENT_RESOURCES; PSrb->SrbStatus = SRB_STATUS_ABORTED; } else { status = OsrUserReadData(pIInfo,PSrb,readMdl,startingLbn,readLength,&bytesRead); } } __except(EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); } // Oh, the user did not want to complete the request, but pended it. We'll // honor it and get out of here. It is up to the user to complete the request // later on. if(status == STATUS_PENDING) { return status; } } goto completeRequest;
Doing Real Work
The previous two articles discussed how to integrate the driver with Storport to become a Virtual Storport Miniport Driver and when integrated, the functions that the driver receives, and how the driver processes them. The current article has so far discussed how to programmatically request the driver to create a SCSI device (IOCTL_OSRVMPORT_CONNECT) and how the driver would respond to Storport requests to identify the device (SCSIOP_INQUIRY and SCSIOP_MODE_SENSE). In addition, we’ve talked about the preliminary handling of read and write requests via the SCSIOP_READ and SCSIOP_WRITE operations. So what we really need to do now is discuss where and how the real work is being done in the driver.
As we have mentioned many times, one of the issues of working in the storage stack is that many of the functions are called by the Storport driver at IRQL DISPATCH_LEVEL. We all know that if we are going to interact with the underlying file system containing the files that back the virtual SCSI devices, the driver can’t do it at elevated IRQL. This means that the real work in the driver will need to be done in worker threads, running at IRQL PASSIVE_LEVEL. When you look at the implementation of the driver, you’ll notice that functions like OsrUserWriteData and OsrUserReadData queue work items (Notice that when the driver specified the size of the SRB extension in the initialization code, the driver added the size of an OSR_WORK_ITEM) to a set of worker thread that the driver created on initialization (OsrUserAdapterStarted).
A worker thread in the driver will perform the operation specified in the work item. Check out the DoWorkThreadStart routine (available in the downloadable code to our driver). It handles work item commands, DO_CREATE, DO_CLOSE, DO_READ, and DO_WRITE. Let’s discuss each work item command.
DO_CREATE
This work item is used to open a file that is going to back a device that the driver is creating. You’ll notice a few things about this handler. The driver:
- Uses SeImpersonateClientEx to ensure that it is using the security credentials of the caller when opening the file, since the system thread may not have the same access rights as the requestor.
- Opens the file using ZwCreateFile for overlapped I/O, so that it can be doing multiple asynchronous operations against the file.
- Converts the file handle returned by ZwCreateFile to a pointer to the referenced File Object by calling ObReferenceObjectByHandle. This is done so that the driver can directly build and send Read and Write IRPS to the underlying file system.
- Calls ZwQueryInformationFile, so that it can get information on the file for commands like SCSIOP_READ_CAPACITY.
Once this command succeeds, the driver can mark the device as connected so that the software knows that the device is usable.
DO_CLOSE
This work item is used to close a file that backed a device that has been disconnected. Since the driver opened the file with ZwCreateFile, its closes the handle by calling ZwClose and dereferences the File Object that was returned by ObReferenceObjectByHandle.
DO_READ and DO_WRITE
These work items are used to read or write information to or from the file that backs the device. The code in OsrUserHandleSrb has converted the SCSIOP_READ or SCSIOP_WRITE logical block offset and block count into a file offset and byte count that the DO_READ or DO_WRITE code can use for the operation. Therefore all the DO_READ or DO_WRITE code has to do is issue a read/write to the underlying file system. Since the driver has the file object for the file and an MDL for the user data, the driver implements the read, via the DoRead function and the write via the DoWrite function. These functions build an IRP for the read or write operation, call the underlying file system directly, and wait for the response. Once the operation completes the driver code can call OsrSpCompleteSrb in the upper-edge of the driver to notify Storport that the SRB has completed.
Wrapping Up Processing
As you’ve seen, working with Storport to create a Virtual Storport Miniport is actually quite easy. We’ve covered how to create a device, notify Storport that a device has arrived, process I/Os to the device and make that device go away.
We’ve also covered, in detail, the process of actually performing I/O operations. This process is one of receiving SRB/CDB pairs, interpreting them, and then mapping them into some operation for the device the driver is emulating. Whether the driver is going to map operations to a file or some other target, with a virtual Storport Miniport, the concepts we have discussed will help you on your way.
So what we have shown through the 3 articles on “Writing a Virtual Storport Miniport Driver” is that the Storport environment is pretty easy to work in once you know the constraints.
Now let’s wrap up this series by talking about installing and building the included sample code.
Installation
Well, I suppose that since we now have discussed how the driver works we had better discuss how to install it. In the source code that we provide is the INF file used to install our Virtual Storport Miniport Driver. We’re going to assume that you’ve seen INF files before, so we’re not going to explain them. What we are going to do however is note some important points.
- The INF file defines its Class as SCSIAdapter which will indicate to the PnP Manager that this INF file is for a SCSI Adapter type of device.
- Since there is no hardware associated with this driver, the INF file indicates to the PnP Manager that this device is “ROOT” enumerated, in other words, the Pnp Manager must create a PDO for this device. This detail is defined in the INFs’ “Models” section where it specifies %rootstr%, which equates to “ROOT\OsrSVm”
- The INF file adds this service to the SCSI Miniport LoadOrderGroup
How you actually get Windows to install the software depends on which platform you are on. For Pre-Windows 7 systems, you can use the Add Hardware Manager from the control panel to perform this installation. For Windows 7 however, the Add Hardware Manager is missing from the control panel, so in order to install the driver, you must go to the control panel, Select Administrative Tools, then select Computer Management. When the Computer Management window opens select Device Manager, right click on your computer in the left window pane and then select Add Legacy Hardware.
Building
The sample source code comes with 3 directories which build 2 components. The directories are:
- OsrVmSample – which contains the OSR Virtual Storport Miniport Driver software
- OsrVmSampleMgmt – which contains the Win32 MFC application that manages the Driver
- OsrVmSampleInc – which contains the include files shared by the Driver and the Management code.
The Driver and the Management Application can all be built with the Windows 7 WDK and have been tested on Vista and Win7. The included project contains a “dirs” file which will build both components of the software and it also contains the INF file which can be used to install the software.
Summary
In the three articles on “Writing a Virtual Storport Miniport Driver” we have tried to cover all the important aspects of architecture, design, and implementation of the software. Hopefully with these articles and the downloaded software example, you will be able to fully understand our discussion. Included in the sample is the driver source, INF, and the controlling user mode application.
Download Sample code to OSR’s Virtual Storport Miniport Driver