首页
社区
课程
招聘
[分享]关于如何判断两个可移动磁盘卷是否在同一个USB HUB上
发表于: 2011-3-1 14:46 10810

[分享]关于如何判断两个可移动磁盘卷是否在同一个USB HUB上

2011-3-1 14:46
10810
这是一个近期的项目需求功能点,参考相关资料,现在将研究所得的一些想法的实现分享一下。判断多个可移动磁盘卷是否在同一个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直播授课

收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 120
活跃值: (160)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
学习一下代码。。。感谢。
2011-3-1 14:56
0
雪    币: 333
活跃值: (46)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
MARK  :磁盘、分区、卷标

thx~~
2011-11-23 16:52
0
游客
登录 | 注册 方可回帖
返回
//