这是一个近期的项目需求功能点,参考相关资料,现在将研究所得的一些想法的实现分享一下。判断多个可移动磁盘卷是否在同一个USB HUB上,我们知道,每一个可移动磁盘卷都有一些唯一的标识,起先的想法是枚举USB可移动卷设备,总觉得设备信息里会有一些关于USB HUB的信息,不过后来没有找到就放弃这种想法了(如果我忽略了有牛们知道的话讨论下)。现在是从USB PORT入手,每个USB HUB上都有一个 NNumberOfPort来指示这个HUB有几个USB PORT,我们需要遍历这个。我们需要获取的是可移动磁盘的VID,PID和SerialNumber这三个标识。在USB PORT上挂接的设备中是可以找到这三个信息的,所以我们匹配这些信息就能找到指定的可移动磁盘对应的是哪一个USB HUB,从而达到标题所指的功能。
USB HUB设备是挂在HCD上的(控制总线),我们得先枚举这个。
一、枚举HCD( Host Controller)
这个很简单,HCD是这样的命名(\\.\HCD0,\\.\HCD1)
..
For( I = 0; ; i++ )
{
Wsprintf( szHcName,_T(\\\\.\\HCD%d),i );
HandleHcd = CreateFile( szHcName…. );
If( INVALID_HANDLE_VALUE != handleHcd )
{
//…..
CloseHandle( handleHcd );
}
Else
{
Break;
}
}
…
获取到HCD的设备句柄之后,我们需要获取它下面的首个HUB的设备
二、根据HCD的设备句柄获取HUB设备
用CreateFile找开时需要名字,首先获取该hub的名字,向hcd设备发IOCTL_USB_GET_ROOT_HUB_NAME就可以了。
typedef struct _USB_ROOT_HUB_NAME {
ULONG ActualLength; //指示长度
WCHAR RootHubName[1]; //名字的指针
} USB_ROOT_HUB_NAME, *PUSB_ROOT_HUB_NAME;
USB_ROOT_HUB_NAME hubNameTmp;
PUSB_ROOT_HUB_NAME pHubName;
//获取名字长度
DeviceIoControl( hHCDHandle,IOCTL_USB_GET_ROOT_HUB_NAME,&hubNameTmp,sizeof(USB_ROOT_HUB_NAME), &hubNameTmp,sizeof(USB_ROOT_HUB_NAME),&dwBytes,NULL );
dwBytes = hubNameTmp.ActualLength; //在结构的这个字段会返回所需的缓冲区长度
//获取名字
…
pHubName = (PUSB_ROOT_HUB_NAME)malloc( dwBytes );
if( pHubName )
{
pHubName->ActualLength = dwBytes;
if( DeviceIoControl( hHCDHandle,
IOCTL_USB_GET_ROOT_HUB_NAME,
NULL,0,
pHubName,dwBytes,
&dwBytes,NULL ) )
{
//获取成功,申请返回字符串内存
lpszHCDName = (TCHAR*)malloc( dwBytes );
if( lpszHCDName )
{
//获取到的是UNICODE字符串
WCharToMByte( pHubName->RootHubName,lpszHCDName,dwBytes );
//接下来可以根据这个名字打开这个句柄了
wsprintf( szName,_T("\\\\.\\%s"),lpszHubName );
hHubHandle = CreateFile( szName,GENERIC_WRITE,FILE_SHARE_WRITE,NULL,OPEN_EXISTING,NULL,NULL );
if( INVALID_HANDLE_VALUE != hHubHandle )
{
//成功获取到hub设备句柄
CloseHandle( hHubHandle );
}
}
}
//释放内存
free( pHubName );
}
…
有了HUB的句柄,那关于这个HUB的信息,比如我们关心的nNumberOfPorts也能获取到了,向这个设备发IOCTL_USB_GET_NODE_INFORMATION请求
….
USB_NODE_INFORMATION NodeInfo;
If( DeviceIoControl( hHubHandle,IOCTL_USB_GET_NODE_INFORMATION,NULL,0,&NodeInfo,sizeof(USB_NODE_INFORMATION),&dwBytes,NULL )
{
//这里就能获取到这个HUB上的USB端口数了,在hub的设备描述符中
NumberOfPorts = NodeInfo.u.HubInformation.HubDescriptor.bNumberOfPorts;
}
….
接下去可以去遍历这些端口了!
三、遍历HUB上的usb port获取一个指定HUB上的端口信息,向这个HUB设备发送IOCTL_USB_GET_NODE_CONNECTION_INFORMATION/EX,因为SerialNumber这个唯一标识是字符串值,而获取到的端口连接信息里只会有iSerialNumber这个指示offset的字段,所以我们还需要发送一个IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION去获取这个UB的字符串描述,然后根据iSerialNumber 这个offset来偏移到SerialNumber,而VID和PID则以USHORT类型直接在设备描述里出现。
信息结构如下:
#pragma pack(1)
typedef struct _USB_NODE_CONNECTION_INFORMATION_EX {
ULONG ConnectionIndex; //这个值是需要输入的,指定查询哪一个端口,从数字1开始
USB_DEVICE_DESCRIPTOR DeviceDescriptor; //如果有USB连接的话这里是连接的USB设备的描述符,也是我们用来比较两个可移动磁盘卷是不是相同的标识
UCHAR CurrentConfigurationValue;
UCHAR Speed;
BOOLEAN DeviceIsHub; //表示这个设备是不是一个HUB,也就是说如果是一个HUB的话我们需要递归下去
USHORT DeviceAddress;
ULONG NumberOfOpenPipes;
USB_CONNECTION_STATUS ConnectionStatus; //这个端口的连接状态
USB_PIPE_INFO PipeList[0];
} USB_NODE_CONNECTION_INFORMATION_EX, *PUSB_NODE_CONNECTION_INFORMATION_EX;
#pragma pack()
USB的设备描述结构
#pragma pack(1)
typedef struct _USB_DEVICE_DESCRIPTOR {
UCHAR bLength ;
UCHAR bDescriptorType ;
USHORT bcdUSB ;
UCHAR bDeviceClass ;
UCHAR bDeviceSubClass ;
UCHAR bDeviceProtocol ;
UCHAR bMaxPacketSize0;
USHORT idVendor ; //VID
USHORT idProduct ; //PID
USHORT bcdDevice ;
UCHAR iManufacturer ;
UCHAR iProduct ;
UCHAR iSerialNumber ; //SerialNumber,这个字段是指示在连接字符串信息中SerialNumber的offset
UCHAR bNumConfigurations ;
} USB_DEVICE_DESCRIPTOR, *PUSB_DEVICE_DESCRIPTOR ;
#pragma pack()
…..
PUSB_NODE_CONNECTION_INFORMATION_EX pNodeConnInfoEx;
For( int I = 0; I <= NumPorts; I ++ )
{
dwBytes = sizeof(USB_NODE_CONNECTION_INFORMATION_EX) + sizeof(USB_PIPE_INFO) * 32;
pNodeConnInfoEx = (PUSB_NODE_CONNECTION_INFORMATION_EX)malloc( dwBytes );
if( pNodeConnInfoEx )
{
Memset( pNodeConnInfoEx,0,dwBytes );
//这里是输入参数,指定要查高询哪一个端口的连接信息,从1开始
pNodeConnInfoEx->ConnectionIndex = I;
DeviceIoControl( hHubHandle,IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX,pNodeConnInfoEx,dwBytes,pNodeConnInfoEx,dwBytes,&dwBytes,NULL );
//这个端口有挂接设备
If( pNodeConnInfo->ConnectionStatus == DeviceConnected )
{
//这个设备有这三个主要信息
If( pNodeConnInfo->DeviceDescriptor.idVendor &&
pNodeConnInfo->DeviceDescriptor.idProduct &&
pNodeConnInfo->DeviceDescriptor.iSerialNumber )
{
//获取这个设备的字符串描述(IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION)
PUSB_DESCRIPTOR_REQUEST stringDescReq; //这里输入参数结构,需要填充
PUSB_STRING_DESCRIPTOR stringDesc;
PSTRING_DESCRIPTOR_NODE stringDescNode;
nBytes = sizeof(stringDescReqBuf);
stringDescReq = (PUSB_DESCRIPTOR_REQUEST)stringDescReqBuf;
stringDesc = (PUSB_STRING_DESCRIPTOR)(stringDescReq+1);
//填充输入查询信息参数
memset(stringDescReq, 0, nBytes);
//连接的端口索引
stringDescReq->ConnectionIndex = ConnectionIndex;
//这个wValume是一个WORD值,高8位指标查询类型,这里要查询字符串描述,所以是USB_STRING_DESCRIPTOR,低8位指定查询的描述符索引,比如要查询SerialNumber,这个值就是上面获取到的iSerialNumber
stringDescReq->SetupPacket.wValue = (USB_STRING_DESCRIPTOR_TYPE << 8)
| DescriptorIndex;
//语言ID,置为0即可
stringDescReq->SetupPacket.wIndex = LanguageID;
//指示整个输出缓冲的大小,用总大小nBytes减去查询结构大小,
stringDescReq->SetupPacket.wLength = (USHORT)(nBytes - sizeof(USB_DESCRIPTOR_REQUEST));
if( DeviceIoControl(hHubDevice,IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION,stringDescReq,nBytes,stringDescReq,nBytes,&nBytesReturned,NULL);
{
//获取到的是UNICODE字符串
TCHAR* pSerialNumberString = (TCHAR*)malloc( stringDesc->bLength / sizeof(WCHAR) + 1 );
//获取iSErialNumber在字符串描述中的偏移,stringDesc->bString + descChars就是SerialNumber的UNICODE字符串了
int descChars = ( (int)stringDesc->bLength - offsetof(USB_STRING_DESCRIPTOR,bString)) / sizeof(WCHAR);
if(pSerialNumberString)
{
//转为ansi字符串
stringDesc->bString[descChars] = 0;
WCharToMByte( stringDesc->bString, pSerialNumberString,stringDesc->bLength / sizeof(WCHAR) + 1 );
//现在有了这三个信息就可以匹配可移动磁盘了
// pNodeConnInfo->DeviceDescriptor.idVendor
// pNodeConnInfo->DeviceDescriptor.idProduct
// pSerialNumberString
……
}
}
}
}
…..
现在有了这三个唯一的标识,只要跟目标可移动磁盘匹配就可以了,一般我们实际有都不会指定一个可移动磁盘设备,而是指定一个盘符,接下来我们讨论如何通过盘符获取上面这三个唯一标识(SerialNumber,VID,PID)。
四、通过盘符获取磁盘设备的唯一标识
可移动磁盘,包括U盘,也包括移动硬盘,需要注意的分区。两个不同的分区,但是他们是处于同一个设备,所以Disk Number是一样的,我这里用这个来处理带有分区的可移动磁盘。
很简单,只要向磁盘卷设备发送IOCTL_STORAGE_GET_DEVICE_NUMBER就可以了,获取到的信息结构如下
typedef struct _STORAGE_DEVICE_NUMBER {
DEVICE_TYPE DeviceType; //这里是FILE_DEVICE_DISK
DWORD DeviceNumber; //这里就是我们需要的DiskNumber
DWORD PartitionNumber;
} STORAGE_DEVICE_NUMBER, *PSTORAGE_DEVICE_NUMBER;
….
Wsprintf( szName,_T(\\\\.\\%c:),cVolume );
HANDLE hDevice = CreateDevice( szName,…. );
….
DeviceIoControl( hDevice,IOCTL_STORAGE_GET_DEVICE_NUMBER,NULL,0,&sdNumber,sizeof(STORAGE_DEVICE_NUMBER),&dwBytes,NULL );
..
//sdNumber.DeviceNumber就是DISK NUMBER
获取到disk number之后我们可以用CM_Get_Device_ID函数获取这些信息,但是这个信息的第一个参数是DEVINST类型,可以通过上面获取到的disk number,用SetupDi函数枚举出来。
(网上现成代码如下)
DEVINST
GetDrivesDevInstByDiskNumber( long DiskNumber )
{
DEVINST devInstResult = 0;
GUID* guid = (GUID*)&GUID_DEVINTERFACE_DISK;
//GUID* guid = (GUID*)&UsbClassGuid;
HDEVINFO hDevInfo = SetupDiGetClassDevs( guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (hDevInfo == INVALID_HANDLE_VALUE){
return 0;
}
DWORD dwIndex = 0;
SP_DEVICE_INTERFACE_DATA devInterfaceData = {0};
devInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
BOOL bRet = FALSE;
BYTE Buf[1024];
PSP_DEVICE_INTERFACE_DETAIL_DATA pspdidd = (PSP_DEVICE_INTERFACE_DETAIL_DATA)Buf;
SP_DEVICE_INTERFACE_DATA spdid;
SP_DEVINFO_DATA spdd;
DWORD dwSize;
spdid.cbSize = sizeof(spdid);
while ( true )
{
bRet = SetupDiEnumDeviceInterfaces(hDevInfo, NULL, guid, dwIndex, &devInterfaceData);
if (!bRet)
{
break;
}
SetupDiEnumInterfaceDevice(hDevInfo, NULL, guid, dwIndex, &spdid);
dwSize = 0;
SetupDiGetDeviceInterfaceDetail(hDevInfo, &spdid, NULL, 0, &dwSize, NULL);
if ( dwSize!=0 && dwSize<=sizeof(Buf) )
{
pspdidd->cbSize = sizeof(*pspdidd);
ZeroMemory((PVOID)&spdd, sizeof(spdd));
spdd.cbSize = sizeof(spdd);
long res = SetupDiGetDeviceInterfaceDetail(hDevInfo, &spdid, pspdidd, dwSize, &dwSize, &spdd);
if ( res )
{
HANDLE hDrive = CreateFile(pspdidd->DevicePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, NULL, NULL);
if ( hDrive != INVALID_HANDLE_VALUE )
{
STORAGE_DEVICE_NUMBER sdn;
DWORD dwBytesReturned = 0;
res = DeviceIoControl(hDrive, IOCTL_STORAGE_GET_DEVICE_NUMBER, NULL, 0, &sdn, sizeof(sdn), &dwBytesReturned, NULL);
if ( res )
{
if ( DiskNumber == (long)sdn.DeviceNumber )
{
CloseHandle(hDrive);
SetupDiDestroyDeviceInfoList(hDevInfo);
devInstResult = spdd.DevInst;
break;
}
}
CloseHandle(hDrive);
}
}
}
dwIndex++;
}
return devInstResult;
}
通过CM_Get_Device_ID能获取到的只是SerialNumber,在获取到的字符串的最后一节(以‘\’)隔开
通过下面代码提取SerialNumber
if( CR_SUCCESS == CM_Get_Device_ID( devInst,szBuffer,200,0 ) )
{
p = _tcsrchr( szBuffer,TEXT('\\') );
if( p )
{
_tcscpy( pDid->SerialNumber,p+1 );
p = _tcschr( pDid->SerialNumber,TEXT('&') );
if( p )
{
p[0] = 0;
}
}
}
获取vid和pid也是通过setupdi函数枚举出来,在获取到DevicePath之后通过下面代码获取
Sscanf( pDetail->DevicePath, _T("\\\\?\\usb#vid_%04X&pid_%04X"),&Vid,&Pid );
注:关于SerialNumber的获取,DevicePath里也有SerialNumber,也可以通过上面的方法提取出来,而不用过CM_Get_Device_ID函数
这样就获取到指定盘符的三个唯一标识,去跟指定usb hub上挂接的设备进行比较,就可以知道两个卷是不是挂接在同一个usb hub上了。
上述有来自网上代码
代码USB View有相关实现,只是贴出讲解,如有错误请提出改正
参考:
http://blog.csdn.net/jakeyjia/archive/2006/12/01/1424111.aspx
WDK.7600.USb View
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课