實戰DeviceIoControl 之五:列舉已安裝的存儲設備

 
 
Q 前幾次我們討論的都是設備名比較清楚的情況,有了設備名(路徑),就可以直接調用CreateFile打開設備,進行它所支持的I/O操作了。如果事先並不能確切知道設備名,如何去訪問設備呢?
A 訪問設備必須用設備句柄,而得到設備句柄必須知道設備路徑,這個套路以你我之力是改變不了的。每個設備都有它所屬類型的GUID,我們順着這個GUID就能獲得設備路徑。
GUID是同類或同種設備的全球唯一識別碼,它是一個128 bit(16字節)的×××數,真實面目爲
typedef struct _GUID
{
    unsigned long  Data1;
    unsigned short Data2;
    unsigned short Data3;
    unsigned char  Data4[8];
} GUID, *PGUID;
例如,Disk類的GUID爲“53f56307-b6bf-11d0-94f2-00a0c91efb8b”,在我們的程序裏可以定義爲
const GUID DiskClassGuid = {0x53f56307L, 0xb6bf, 0x11d0, {0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b)};
或者用一個宏來定義
DEFINE_GUID(DiskClassGuid, 0x53f56307L, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b);
通過GUID找出設備路徑,需要用到一組設備管理的API函數
SetupDiGetClassDevs, SetupDiEnumDeviceInterfaces, SetupDiGetInterfaceDeviceDetail, SetupDiDestroyDeviceInfoList,
以及結構SP_DEVICE_INTERFACE_DATA, SP_DEVICE_INTERFACE_DETAIL_DATA。
有關信息請查閱MSDN,這裏就不詳細介紹了。
實現GUID到設備路徑的代碼如下:
// SetupDiGetInterfaceDeviceDetail所需要的輸出長度,定義足夠大
#define INTERFACE_DETAIL_SIZE    (1024)
  
// 根據GUID獲得設備路徑
// lpGuid: GUID指針
// pszDevicePath: 設備路徑指針的指針
// 返回: 成功得到的設備路徑個數,可能不止1個
int GetDevicePath(LPGUID lpGuid, LPTSTR* pszDevicePath)
{
    HDEVINFO hDevInfoSet;
    SP_DEVICE_INTERFACE_DATA ifdata;
    PSP_DEVICE_INTERFACE_DETAIL_DATA pDetail;
    int nCount;
    BOOL bResult;
  
    // 取得一個該GUID相關的設備信息集句柄
    hDevInfoSet = ::SetupDiGetClassDevs(lpGuid,     // class GUID 
        NULL,                    // 無關鍵字 
        NULL,                    // 不指定父窗口句柄 
        DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);    // 目前存在的設備
  
    // 失敗...
    if (hDevInfoSet == INVALID_HANDLE_VALUE)
    {
        return 0;
    }
  
    // 申請設備接口數據空間
    pDetail = (PSP_DEVICE_INTERFACE_DETAIL_DATA)::GlobalAlloc(LMEM_ZEROINIT, INTERFACE_DETAIL_SIZE);
  
    pDetail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
  
    nCount = 0;
    bResult = TRUE;
  
    // 設備序號=0,1,2... 逐一測試設備接口,到失敗爲止
    while (bResult)
    {
        ifdata.cbSize = sizeof(ifdata);
  
        // 枚舉符合該GUID的設備接口
        bResult = ::SetupDiEnumDeviceInterfaces(
            hDevInfoSet,     // 設備信息集句柄
            NULL,            // 不需額外的設備描述
            lpGuid,          // GUID
            (ULONG)nCount,   // 設備信息集裏的設備序號
            &ifdata);        // 設備接口信息
  
        if (bResult)
        {
            // 取得該設備接口的細節(設備路徑)
            bResult = SetupDiGetInterfaceDeviceDetail(
                hDevInfoSet,    // 設備信息集句柄
                &ifdata,        // 設備接口信息
                pDetail,        // 設備接口細節(設備路徑)
                INTERFACE_DETAIL_SIZE,    // 輸出緩衝區大小
                NULL,           // 不需計算輸出緩衝區大小(直接用設定值)
                NULL);          // 不需額外的設備描述
  
            if (bResult)
            {
                // 複製設備路徑到輸出緩衝區
                ::strcpy(pszDevicePath[nCount], pDetail->DevicePath);
  
                // 調整計數值
                nCount++;
            }
        }
    }
  
    // 釋放設備接口數據空間
    ::GlobalFree(pDetail);
  
    // 關閉設備信息集句柄
    ::SetupDiDestroyDeviceInfoList(hDevInfoSet);
  
    return nCount;
}
調用GetDevicePath函數時要注意,pszDevicePath是個指向字符串指針的指針,例如可以這樣
    int i;
    char* szDevicePath[MAX_DEVICE];        // 設備路徑
  
    // 分配需要的空間
    for (i = 0; i < MAX_DEVICE; i++)
    {
        szDevicePath[i] = new char[256];
    }
  
    // 取設備路徑
    nDevice = ::GetDevicePath((LPGUID)&DiskClassGuid, szDevicePath);
  
    // 逐一獲取設備信息
    for (i = 0; i < nDevice; i++)
    {
        // 打開設備
        hDevice = ::OpenDevice(szDevicePath[i]);
  
        if (hDevice != INVALID_HANDLE_VALUE)
        {
            ... ...        // I/O操作
  
            ::CloseHandle(hDevice);
        }
    }
  
    // 釋放空間
    for (i = 0; i & lt; MAX_DEVICE; i++)
    {
        delete []szDevicePath[i];
    }
本例的Project中除了要包含winioctl.h外,還要包含initguid.h,setupapi.h,以及連接setupapi.lib。
Q 得到設備路徑後,就可以到下一步,用CreateFile打開設備,然後用DeviceIoControl進行讀寫了吧?
A 是的。儘管該設備路徑與以前我們接觸的那些不太一樣。本是“\\.\PhysicalDrive0”,現在鳥槍換炮,變成了類似這樣的一副尊容:
“\\?\ide#diskmaxtor_2f040j0__________________________vam51jj0#3146563447534558202020202020202020202020#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}”。
其實這個設備名在註冊表的某處可以找到,例如在Win2000中這個名字可以位於
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Disk\Enum\0,
只不過“#”換成了“\”。分析一下這樣的設備路徑,你會發現很有趣的東西,它們是由接口類型、產品型號、固件版本、序列號、計算機名、GUID等信息組合而成的。當然,它是沒有規範的,不能指望從這裏面得到你希望知道的東西。
用CreateFile打開設備後,對於存儲設備,IOCTL_DISK_GET_DRIVE_GEOMETRY,IOCTL_STORAGE_GET_MEDIA_TYPES_EX等I/O控制碼照常使用。
今天我們討論一個新的控制碼:IOCTL_STORAGE_QUERY_PROPERTY,獲取設備屬性信息,希望得到系統中所安裝的各種固定的和可移動的硬盤、優盤和CD/DVD-ROM/R/W的接口類型、序列號、產品ID等信息。
// IOCTL控制碼
#define IOCTL_STORAGE_QUERY_PROPERTY   CTL_CODE(IOCTL_STORAGE_BASE, 0x0500, METHOD_BUFFERED, FILE_ANY_ACCESS)
// 存儲設備的總線類型
typedef enum _STORAGE_BUS_TYPE {
    BusTypeUnknown = 0x00,
    BusTypeScsi,
    BusTypeAtapi,
    BusTypeAta,
    BusType1394,
    BusTypeSsa,
    BusTypeFibre,
    BusTypeUsb,
    BusTypeRAID,
    BusTypeMaxReserved = 0x7F
} STORAGE_BUS_TYPE, *PSTORAGE_BUS_TYPE;
  
// 查詢存儲設備屬性的類型
typedef enum _STORAGE_QUERY_TYPE {
    PropertyStandardQuery = 0,          // 讀取描述
    PropertyExistsQuery,                // 測試是否支持
    PropertyMaskQuery,                  // 讀取指定的描述
    PropertyQueryMaxDefined             // 驗證數據
} STORAGE_QUERY_TYPE, *PSTORAGE_QUERY_TYPE;
  
// 查詢存儲設備還是適配器屬性
typedef enum _STORAGE_PROPERTY_ID {
    StorageDeviceProperty = 0,          // 查詢設備屬性
    StorageAdapterProperty              // 查詢適配器屬性
} STORAGE_PROPERTY_ID, *PSTORAGE_PROPERTY_ID;
  
// 查詢屬性輸入的數據結構
typedef struct _STORAGE_PROPERTY_QUERY {
    STORAGE_PROPERTY_ID PropertyId;     // 設備/適配器
    STORAGE_QUERY_TYPE QueryType;       // 查詢類型 
    UCHAR AdditionalParameters[1];      // 額外的數據(僅定義了象徵性的1個字節)
} STORAGE_PROPERTY_QUERY, *PSTORAGE_PROPERTY_QUERY;
  
// 查詢屬性輸出的數據結構
typedef struct _STORAGE_DEVICE_DESCRIPTOR {
    ULONG Version;                    // 版本
    ULONG Size;                       // 結構大小
    UCHAR DeviceType;                 // 設備類型
    UCHAR DeviceTypeModifier;         // SCSI-2額外的設備類型
    BOOLEAN RemovableMedia;           // 是否可移動
    BOOLEAN CommandQueueing;          // 是否支持命令隊列
    ULONG VendorIdOffset;             // 廠家設定值的偏移
    ULONG ProductIdOffset;            // 產品ID的偏移
    ULONG ProductRevisionOffset;      // 產品版本的偏移
    ULONG SerialNumberOffset;         // 序列號的偏移
    STORAGE_BUS_TYPE BusType;         // 總線類型
    ULONG RawPropertiesLength;        // 額外的屬性數據長度
    UCHAR RawDeviceProperties[1];     // 額外的屬性數據(僅定義了象徵性的1個字節)
} STORAGE_DEVICE_DESCRIPTOR, *PSTORAGE_DEVICE_DESCRIPTOR;
  
// 取設備屬性信息
// hDevice -- 設備句柄
// pDevDesc -- 輸出的設備描述和屬性信息緩衝區指針(包含連接在一起的兩部分)
BOOL GetDriveProperty(HANDLE hDevice, PSTORAGE_DEVICE_DESCRIPTOR pDevDesc)
{
    STORAGE_PROPERTY_QUERY Query;    // 查詢輸入參數
    DWORD dwOutBytes;                // IOCTL輸出數據長度
    BOOL bResult;                    // IOCTL返回值
  
    // 指定查詢方式
    Query.PropertyId = StorageDeviceProperty;
    Query.QueryType = PropertyStandardQuery;
  
    // 用IOCTL_STORAGE_QUERY_PROPERTY取設備屬性信息
    bResult = ::DeviceIoControl(hDevice, // 設備句柄
        IOCTL_STORAGE_QUERY_PROPERTY,    // 取設備屬性信息
        &Query, sizeof(STORAGE_PROPERTY_QUERY),    // 輸入數據緩衝區
        pDevDesc, pDevDesc->Size,        // 輸出數據緩衝區
        &dwOutBytes,                     // 輸出數據長度
        (LPOVERLAPPED)NULL);             // 用同步I/O    
  
    return bResult;
}
Q 我用這個方法從IOCTL_STORAGE_QUERY_PROPERTY返回的數據中,沒有得到CDROM和USB接口的外置硬盤的序列號、產品ID等信息。但從設備路徑上看,明明是有這些信息的,爲什麼它沒有填充到STORAGE_DEVICE_DESCRIPTOR中呢?再就是爲什麼硬盤序列號本是“D22P7KHE            ”,爲什麼它填充的是“3146563447534558202020202020202020202020”這種形式呢?
A 對這兩個問題我也是心存疑惑,但又不敢妄加猜測,正琢磨着向微軟請教呢。

[相關資源]

  • 本文Demo源碼:StorageEnum.zip (23KB)
  • bhw98的專欄:[url]http://www.csdn.net/develop/author/netauthor/bhw98/[/url]
  • 0

    收藏

    hello_world

    97篇文章,146W+人氣,0粉絲

    Ctrl+Enter 發佈

    發佈

    取消

    掃一掃,領取大禮包

    0

    分享
    hello_world
    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章