如何判断两个可移动磁盘卷是否在同一个USB HUB上?

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

来自: http://bbs.pediy.com/archive/index.php?t-130146.html
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值