实战DeviceIoControl:通过API访问设备驱动程序

Q 在MSDN的那个demo中,将设备名换成“A:”取A盘参数,先用资源 管理器读一下盘,再运行这个 程序可以成功,但换一张盘后就失败;换成“CDROM0”取CDROM参数,无论如何都不行。这个问题如何解决呢?

A 取软盘参数是从软盘上读取格式化后的信息,也就是必须执行读操作,这一点与硬盘不同。将CreateFile中的访问方式改为GENERIC_READ就行了。
IOCTL_DISK_GET_DRIVE_GEOMETRY这个I/O控制码,对软盘和硬盘有效,但对一些可移动媒介如CD/DVD-ROM、TAPE等就不管用了。要取CDROM参数,还得另辟蹊径。IOCTL_STORAGE_GET_MEDIA_TYPES_EX能够帮我们解决问题。


Q 使用这些I/O控制码,需要什么样的输入输出数据格式呢?


A DeviceIoControl使用这两个控制码时,都不需要输入数据。
IOCTL_DISK_GET_DRIVE_GEOMETRY直接输出一个DISK_GEOMETRY结构:

typedef struct _DISK_GEOMETRY {
  LARGE_INTEGER  Cylinders; // 柱面数
  MEDIA_TYPE  MediaType;  // 介质类型
  DWORD  TracksPerCylinder;  // 每柱面的磁道数
  DWORD  SectorsPerTrack;       // 每磁道的扇区数
  DWORD  BytesPerSector;  // 每扇区的字节数
} DISK_GEOMETRY;

IOCTL_STORAGE_GET_MEDIA_TYPES_EX输出一个GET_MEDIA_TYPES结构:

typedef struct _GET_MEDIA_TYPES {
  DWORD DeviceType;   // 设备类型
  DWORD MediaInfoCount;   // 介质信息条数
  DEVICE_MEDIA_INFO MediaInfo[1]; // 介质信息
} GET_MEDIA_TYPES;

让我们来看一下DEVICE_MEDIA_INFO结构的定义:

typedef struct _DEVICE_MEDIA_INFO {
    union {
        struct {
            LARGE_INTEGER Cylinders;  // 柱面数
            STORAGE_MEDIA_TYPE MediaType;  // 介质类型
            DWORD TracksPerCylinder;   // 每柱面的磁道数
            DWORD SectorsPerTrack;  // 每磁道的扇区数
            DWORD BytesPerSector;   // 每扇区的字节数
            DWORD NumberMediaSides;  // 介质面数
            DWORD MediaCharacteristics;  // 介质特性
        } DiskInfo;    // 硬盘信息
        struct {
            LARGE_INTEGER Cylinders;  // 柱面数
            STORAGE_MEDIA_TYPE MediaType;  // 介质类型
            DWORD TracksPerCylinder;   // 每柱面的磁道数
            DWORD SectorsPerTrack;  // 每磁道的扇区数
            DWORD BytesPerSector;   // 每扇区的字节数
            DWORD NumberMediaSides;  // 介质面数
            DWORD MediaCharacteristics;  // 介质特性
        } RemovableDiskInfo;   // “可移动盘”信息
        struct {
            STORAGE_MEDIA_TYPE MediaType;  // 介质类型
            DWORD   MediaCharacteristics;  // 介质特性
            DWORD   CurrentBlockSize;   // 块的大小
        } TapeInfo;    // 磁带信息
    } DeviceSpecific;
} DEVICE_MEDIA_INFO;

其中CD-ROM属于“可移动盘”的范围。请注意,GET_MEDIA_TYPES结构本身只定义了一条DEVICE_MEDIA_INFO,额外的DEVICE_MEDIA_INFO需要紧接此结构的另外的空间。


Q 调用方法我了解了,请用VC举个例子来实现我所期待已久的功能吧?

A 好,现在就演示一下如何取软盘/硬盘/光盘的参数。测试时,记得要有软盘/光盘插在驱动器里喔!
首先,用MFC AppWizard生成一个单文档的应用程序,取名为DiskGeometry,让它的View基于CEditView。
然后,添加以下的.h和.cpp文件。

//
// GetDiskGeometry.h
//

#if !defined(GET_DISK_GEOMETRY_H__)
#define GET_DISK_GEOMETRY_H__

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#include <winioctl.h>

BOOL GetDriveGeometry(const char* filename, DISK_GEOMETRY *pdg);

#endif // !defined(GET_DISK_GEOMETRY_H__)


//
// GetDiskGeometry.cpp
//

#include "stdafx.h"
#include "GetDiskGeometry.h"

// IOCTL_STORAGE_GET_MEDIA_TYPES_EX可能返回不止一条DEVICE_MEDIA_INFO,故定义足够的空间
#define MEDIA_INFO_SIZE  sizeof(GET_MEDIA_TYPES)+15*sizeof(DEVICE_MEDIA_INFO)

// filename -- 用于设备的文件名
// pdg -- 参数缓冲区指针
BOOL GetDriveGeometry(const char* filename, DISK_GEOMETRY *pdg)
{
 HANDLE hDevice;         // 设备句柄
 BOOL bResult;           // DeviceIoControl的返回结果
 GET_MEDIA_TYPES *pmt;   // 内部用的输出缓冲区
 DWORD dwOutBytes;       // 输出数据长度

 // 打开设备
 hDevice = ::CreateFile(filename,                           // 文件名
   GENERIC_READ,                              // 软驱需要读盘
   FILE_SHARE_READ | FILE_SHARE_WRITE,        // 共享方式
   NULL,                                      // 默认的安全描述符
   OPEN_EXISTING,                             // 创建方式
   0,                                         // 不需设置文件属性
   NULL);                                     // 不需参照模板文件

 if(hDevice == INVALID_HANDLE_VALUE)
 {
  // 设备无法打开...
  return FALSE;
 }

 // 用IOCTL_DISK_GET_DRIVE_GEOMETRY取磁盘参数
 bResult = ::DeviceIoControl(hDevice,                   // 设备句柄
   IOCTL_DISK_GET_DRIVE_GEOMETRY,         // 取磁盘参数
   NULL, 0,                               // 不需要输入数据
   pdg, sizeof(DISK_GEOMETRY),            // 输出数据缓冲区
   &dwOutBytes,                           // 输出数据长度
   (LPOVERLAPPED)NULL);                   // 用同步I/O

 // 如果失败,再用IOCTL_STORAGE_GET_MEDIA_TYPES_EX取介质类型参数
 if(!bResult)
 {
  pmt = (GET_MEDIA_TYPES *)new BYTE[MEDIA_INFO_SIZE];

  bResult = ::DeviceIoControl(hDevice,                 // 设备句柄
    IOCTL_STORAGE_GET_MEDIA_TYPES_EX,    // 取介质类型参数
    NULL, 0,                             // 不需要输入数据
    pmt, MEDIA_INFO_SIZE,                // 输出数据缓冲区
    &dwOutBytes,                         // 输出数据长度
    (LPOVERLAPPED)NULL);                 // 用同步I/O 

  if(bResult)
  {
   // 注意到结构DEVICE_MEDIA_INFO是在结构DISK_GEOMETRY的基础上扩充的
   // 为简化程序,用memcpy代替如下多条赋值语句:
   // pdg->MediaType = (MEDIA_TYPE)pmt->MediaInfo[0].DeviceSpecific.DiskInfo.MediaType;
   // pdg->Cylinders = pmt->MediaInfo[0].DeviceSpecific.DiskInfo.Cylinders;
   // pdg->TracksPerCylinder = pmt->MediaInfo[0].DeviceSpecific.DiskInfo.TracksPerCylinder;
   // ... ...
   ::memcpy(pdg, pmt->MediaInfo, sizeof(DISK_GEOMETRY));
  }

  delete pmt;
 }

 // 关闭设备句柄
 ::CloseHandle(hDevice);

 return (bResult);
}

然后,在Toolbar的IDR_MAINFRAME上添加一个按钮,ID为ID_GET_DISK_GEOMETRY。打开ClassWizard,在DiskGeometryView中

添加ID_GET_DISK_GEOMETRY的映射函数OnGetDiskGeometry。打开DiskGeometryView.cpp,包含头文件GetDiskGeometry.h。

在OnGetDiskGeometry中,添加以下代码

 const char *szDevName[]=
 {
  ".//A:",
  ".//B:",
  ".//PhysicalDrive0",
  ".//PhysicalDrive1",
  ".//PhysicalDrive2",
  ".//PhysicalDrive3",
  ".//Cdrom0",
  ".//Cdrom1",
 };
 DISK_GEOMETRY dg;
 ULONGLONG DiskSize;
 BOOL bResult;
 CString strMsg;
 CString strTmp;


 for(int i=0; i<sizeof(szDevName)/sizeof(char*); i++)
 {
  bResult = GetDriveGeometry(szDevName[i], &dg);
  
  strTmp.Format("/r/n%s  result = %s/r/n", szDevName[i], bResult ? "success" : "failure");
  strMsg+=strTmp;

  if(!bResult) continue;

  strTmp.Format("    Media Type = %d/r/n", dg.MediaType);
  strMsg+=strTmp;

  strTmp.Format("    Cylinders = %I64d/r/n", dg.Cylinders);
  strMsg+=strTmp;

  strTmp.Format("    Tracks per cylinder = %ld/r/n", (ULONG) dg.TracksPerCylinder);
  strMsg+=strTmp;

  strTmp.Format("    Sectors per track = %ld/r/n", (ULONG) dg.SectorsPerTrack);
  strMsg+=strTmp;

  strTmp.Format("    Bytes per sector = %ld/r/n", (ULONG) dg.BytesPerSector);
  strMsg+=strTmp;

  DiskSize = dg.Cylinders.QuadPart * (ULONG)dg.TracksPerCylinder *
   (ULONG)dg.SectorsPerTrack * (ULONG)dg.BytesPerSector;
  strTmp.Format("    Disk size = %I64d (Bytes) = %I64d (Mb)/r/n", DiskSize, DiskSize / (1024 *1024));
  strMsg+=strTmp;
 } 

 CEdit& Edit = GetEditCtrl();

 Edit.SetWindowText(strMsg);

 

Q 在NT/2000/XP中,我想用VC编写应用程序访问硬件设备,如获取磁盘参数、读写绝对扇区数据、测试光驱实际速度等,该从哪里入手呢?

A 在NT/2000/XP中,应用程序可以通过API函数DeviceIoControl来实现对设备的访问—获取信息,发送命令,交换数据等。利用该接口函数向指定的设备驱动发送正确的控制码及数据,然后分析它的响应,就可以达到我们的目的。

DeviceIoControl的函数原型为

BOOL DeviceIoControl(
  HANDLE hDevice,              // 设备句柄
  DWORD dwIoControlCode,       // 控制码
  LPVOID lpInBuffer,           // 输入数据缓冲区指针
  DWORD nInBufferSize,         // 输入数据缓冲区长度
  LPVOID lpOutBuffer,          // 输出数据缓冲区指针
  DWORD nOutBufferSize,        // 输出数据缓冲区长度
  LPDWORD lpBytesReturned,     // 输出数据实际长度单元长度
  LPOVERLAPPED lpOverlapped    // 重叠操作结构指针
);

设备句柄用来标识你所访问的设备。
发送不同的控制码,可以调用设备驱动程序的不同类型的功能。在头文件winioctl.h中,预定义的标准设备控制码,都以IOCTL或FSCTL开头。例如,IOCTL_DISK_GET_DRIVE_GEOMETRY是对物理驱动器取结构参数(介质类型、柱面数、每柱面磁道数、每磁道扇区数等)的控制码,FSCTL_LOCK_VOLUME是对逻辑驱动器的卷加锁的控制码。
输入输出数据缓冲区是否需要,是何种结构,以及占多少字节空间,完全由不同设备的不同操作类型决定。在头文件winioctl.h中,已经为标准设备预定义了一些输入输出数据结构。重叠操作结构指针设置为NULL,DeviceIoControl将进行阻塞调用;否则,应在编程时按异步操作设计。

 

Q 设备句柄是从哪里获得的?

A 设备句柄可以用API函数CreateFile获得。它的原型为

HANDLE CreateFile(
  LPCTSTR lpFileName,                         // 文件名
  DWORD dwDesiredAccess,                      // 访问方式
  DWORD dwShareMode,                          // 共享方式
  LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全描述符指针
  DWORD dwCreationDisposition,                // 创建方式
  DWORD dwFlagsAndAttributes,                 // 文件属性及标志
  HANDLE hTemplateFile                        // 模板文件的句柄
);

CreateFile这个函数用处很多,这里我们用它“打开”设备驱动程序,得到设备的句柄。操作完成后用CloseHandle关闭设备句柄。
与普通文件名有所不同,设备驱动的“文件名”形式固定为“//./DeviceName”(注意在C程序中该字符串写法为“.//DeviceName”),DeviceName必须与设备驱动程序内规定的设备名称一致。
一般地,调用CreateFile获得设备句柄时,访问方式参数设置为0或GENERIC_READ|GENERIC_WRITE,共享方式参数设置为FILE_SHARE_READ|FILE_SHARE_WRITE,创建方式参数设置为OPEN_EXISTING,其它参数设置为0或NULL。

 

Q 可是,我怎么知道设备名称是什么呢?

A 一些存储设备的名称是微软规定好的,不可能有什么变化。大体列出如下
软盘驱动器                                  A:, B:
逻辑驱动器                                  C:, D:, E:, ……
物理驱动器                                  PHYSICALDRIVEx
CD-ROM, DVD/ROM                             CDROMx
磁带机                                      TAPEx
其中,物理驱动器不包括软驱和光驱。逻辑驱动器可以是IDE/SCSI/PCMCIA/USB接口的硬盘分区(卷)、光驱、MO、CF卡等,甚至是虚拟盘。x=0,1,2 ……
其它的设备名称需通过驱动接口的GUID调用设备管理函数族取得,这里暂不讨论。

 

Q 请举一个简单的例子说明如何通过DeviceIoControl访问设备驱动程序

A 这里有一个从MSDN上摘抄来的demo程序,演示在NT/2000/XP中如何通过DeviceIoControl获取硬盘的基本参数。

/* The code of interest is in the subroutine GetDriveGeometry. The
   code in main shows how to interpret the results of the IOCTL call. */

#include <windows.h>
#include <winioctl.h>

BOOL GetDriveGeometry(DISK_GEOMETRY *pdg)
{
  HANDLE hDevice;               // handle to the drive to be examined
  BOOL bResult;                 // results flag
  DWORD junk;                   // discard results

  hDevice = CreateFile(".//PhysicalDrive0",  // drive to open
                    0,                // no access to the drive
                    FILE_SHARE_READ | // share mode
                    FILE_SHARE_WRITE,
                    NULL,             // default security attributes
                    OPEN_EXISTING,    // disposition
                    0,                // file attributes
                    NULL);            // do not copy file attributes

  if (hDevice == INVALID_HANDLE_VALUE) // cannot open the drive
  {
    return (FALSE);
  }

  bResult = DeviceIoControl(hDevice,  // device to be queried
      IOCTL_DISK_GET_DRIVE_GEOMETRY,  // operation to perform
                             NULL, 0, // no input buffer
                            pdg, sizeof(*pdg),     // output buffer
                            &junk,                 // # bytes returned
                            (LPOVERLAPPED) NULL);  // synchronous I/O

  CloseHandle(hDevice);

  return (bResult);
}

int main(int argc, char *argv[])
{
  DISK_GEOMETRY pdg;            // disk drive geometry structure
  BOOL bResult;                 // generic results flag
  ULONGLONG DiskSize;           // size of the drive, in bytes

  bResult = GetDriveGeometry (&pdg);

  if (bResult)
  {
    printf("Cylinders = %I64d/n", pdg.Cylinders);
    printf("Tracks per cylinder = %ld/n", (ULONG) pdg.TracksPerCylinder);
    printf("Sectors per track = %ld/n", (ULONG) pdg.SectorsPerTrack);
    printf("Bytes per sector = %ld/n", (ULONG) pdg.BytesPerSector);

    DiskSize = pdg.Cylinders.QuadPart * (ULONG)pdg.TracksPerCylinder *
      (ULONG)pdg.SectorsPerTrack * (ULONG)pdg.BytesPerSector;
    printf("Disk size = %I64d (Bytes) = %I64d (Mb)/n", DiskSize,
           DiskSize / (1024 * 1024));
  }
  else
  {
    printf ("GetDriveGeometry failed. Error %ld./n", GetLastError ());
  }

  return ((int)bResult);
}
DOS命令DISKCOPY给我很深的印象,现在也有许多“克隆”软件,可以对磁盘进行全盘复制。我想,要制作磁盘镜像文件,DeviceIoControl应该很有用武之地吧?

A 是的。这里举一个制作软盘镜像文件,功能类似于“DISKCOPY”的例子。
本例实现其功能的核心代码如下:

// 打开磁盘
HANDLE OpenDisk(LPCTSTR filename)
{
 HANDLE hDisk;

 // 打开设备
 hDisk = ::CreateFile(filename,   // 文件名
  GENERIC_READ | GENERIC_WRITE,  // 读写方式
  FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式
  NULL,     // 默认的安全描述符
  OPEN_EXISTING,    // 创建方式
  0,     // 不需设置文件属性
  NULL);     // 不需参照模板文件

 return hDisk;
}

// 获取磁盘参数
BOOL GetDiskGeometry(HANDLE hDisk, PDISK_GEOMETRY lpGeometry)
{
    DWORD dwOutBytes;
 BOOL bResult;

 // 用IOCTL_DISK_GET_DRIVE_GEOMETRY取磁盘参数
 bResult = ::DeviceIoControl(hDisk,  // 设备句柄
  IOCTL_DISK_GET_DRIVE_GEOMETRY,  // 取磁盘参数
  NULL, 0,    // 不需要输入数据
  lpGeometry, sizeof(DISK_GEOMETRY), // 输出数据缓冲区
  &dwOutBytes,    // 输出数据长度
  (LPOVERLAPPED)NULL);   // 用同步I/O

 return bResult;
}

// 从指定磁道开始读磁盘
BOOL ReadTracks(HANDLE hDisk, PDISK_GEOMETRY lpGeometry, LPVOID pBuf, DWORD dwStartCylinder, DWORD

dwCylinderNumber)
{
    DWORD VirtBufSize;
    DWORD BytesRead;
               
 // 大小
 VirtBufSize =  lpGeometry->TracksPerCylinder * lpGeometry->SectorsPerTrack * lpGeometry->BytesPerSector;

 // 偏移
 ::SetFilePointer(hDisk, VirtBufSize*dwStartCylinder, NULL, FILE_BEGIN);

 return ::ReadFile(hDisk, pBuf, VirtBufSize*dwCylinderNumber, &BytesRead, NULL);
}

// 从指定磁道开始写磁盘
BOOL WriteTracks(HANDLE hDisk, PDISK_GEOMETRY lpGeometry, LPVOID pBuf, DWORD dwStartCylinder, DWORD

dwCylinderNumber)
{
    DWORD VirtBufSize;
    DWORD BytesWritten;

 // 大小
 VirtBufSize =  lpGeometry->TracksPerCylinder * lpGeometry->SectorsPerTrack * lpGeometry->BytesPerSector;

 // 偏移
 ::SetFilePointer(hDisk, VirtBufSize*dwStartCylinder, NULL, FILE_BEGIN);

    return ::WriteFile(hDisk, pBuf, VirtBufSize*dwCylinderNumber, &BytesWritten, NULL);
}

// 从指定磁道开始格式化磁盘
BOOL LowLevelFormatTracks(HANDLE hDisk, PDISK_GEOMETRY lpGeometry, DWORD dwStartCylinder, DWORD

dwCylinderNumber)
{
    FORMAT_PARAMETERS FormatParameters;
    PBAD_TRACK_NUMBER lpBadTrack;
    DWORD dwOutBytes;
    DWORD dwBufSize;
    BOOL bResult;

    FormatParameters.MediaType = lpGeometry->MediaType;
    FormatParameters.StartCylinderNumber = dwStartCylinder;
    FormatParameters.EndCylinderNumber = dwStartCylinder + dwCylinderNumber - 1;
    FormatParameters.StartHeadNumber = 0;
    FormatParameters.EndHeadNumber = lpGeometry->TracksPerCylinder - 1;

    dwBufSize = lpGeometry->TracksPerCylinder * sizeof(BAD_TRACK_NUMBER);

    lpBadTrack = (PBAD_TRACK_NUMBER) new BYTE[dwBufSize];
               
 // 用IOCTL_DISK_FORMAT_TRACKS对连续磁道进行低级格式化
 bResult = ::DeviceIoControl(hDisk,   // 设备句柄
  IOCTL_DISK_FORMAT_TRACKS,   // 低级格式化
  &FormatParameters, sizeof(FormatParameters), // 输入数据缓冲区
  lpBadTrack, dwBufSize,    // 输出数据缓冲区
  &dwOutBytes,     // 输出数据长度
  (LPOVERLAPPED)NULL);    // 用同步I/O

    delete lpBadTrack;

    return bResult;
}

// 将卷锁定
BOOL LockVolume(HANDLE hDisk)
{
    DWORD dwOutBytes;
    BOOL bResult;

 // 用FSCTL_LOCK_VOLUME锁卷
 bResult = ::DeviceIoControl(hDisk,  // 设备句柄
  FSCTL_LOCK_VOLUME,   // 锁卷
  NULL, 0,    // 不需要输入数据
  NULL, 0,    // 不需要输出数据
  &dwOutBytes,    // 输出数据长度
  (LPOVERLAPPED)NULL);   // 用同步I/O

    return bResult;
}

// 将卷解锁
BOOL UnlockVolume(HANDLE hDisk)
{
    DWORD dwOutBytes;
    BOOL bResult;

 // 用FSCTL_UNLOCK_VOLUME开卷锁
 bResult = ::DeviceIoControl(hDisk,  // 设备句柄
  FSCTL_UNLOCK_VOLUME,   // 开卷锁
  NULL, 0,    // 不需要输入数据
  NULL, 0,    // 不需要输出数据
  &dwOutBytes,    // 输出数据长度
  (LPOVERLAPPED)NULL);   // 用同步I/O

    return bResult;
}

// 将卷卸下
// 该操作使系统重新辨识磁盘,等效于重新插盘
BOOL DismountVolume(HANDLE hDisk)
{
    DWORD dwOutBytes;
    BOOL bResult;

 // 用FSCTL_DISMOUNT_VOLUME卸卷
 bResult = ::DeviceIoControl(hDisk,  // 设备句柄
  FSCTL_DISMOUNT_VOLUME,   // 卸卷
  NULL, 0,    // 不需要输入数据
  NULL, 0,    // 不需要输出数据
  &dwOutBytes,    // 输出数据长度
  (LPOVERLAPPED)NULL);   // 用同步I/O

    return bResult;
}

将软盘保存成镜像文件的步骤简单描述为:
1、创建空的镜像文件。
2、调用OpenDisk打开软盘。成功转3,失败转8。
3、调用LockVolume将卷锁定。成功转4,失败转7。
4、调用GetDiskGeometry获取参数。成功转5,失败转6。
5、将磁盘参数写入镜像文件作为文件头。调用ReadTracks按柱面读出数据,保存在镜像文件中。循环次数等于柱面数。
6、调用UnlockVolume将卷解锁。
7、调用CloseDisk关闭软盘。
8、关闭镜像文件。

将镜像文件载入软盘的步骤简单描述为:
1、打开镜像文件。
2、调用OpenDisk打开软盘。成功转3,失败转11。
3、调用LockVolume将卷锁定。成功转4,失败转10。
4、调用GetDiskGeometry获取参数。成功转5,失败转9。
5、从镜像文件中读出文件头,判断两个磁盘参数是否一致。不一致转6,否则转7。
6、调用LowLevelFormatTracks按柱面格式化软盘。循环次数等于柱面数。成功转7,失败转8。
7、从镜像文件中读出数据,并调用WriteTracks按柱面写入磁盘。循环次数等于柱面数。
8、调用DismountVolume将卷卸下。
9、调用UnlockVolume将卷解锁。
10、调用CloseDisk关闭软盘。
11、关闭镜像文件。


Q 我注意到,磁盘读写和格式化是按柱面进行的,有什么道理吗?

A 没有特别的原因,只是因为在这个例子中可以方便地显示处理进度。
有一点需要特别提及,按绝对地址读写磁盘数据时,“最小单位”是扇区,地址一定要与扇区对齐,长度也要等于扇区长度的整数倍。比如,每扇区512字节,那么起始地址和数据长度都应能被512整除才行。


Q 我忽然产生了一个伟大的想法,用绝对地址读写的方式使用磁盘,包括U盘啦,MO啦,而不是用现成的文件系统,那不是可以将数据保密了吗?

A 当然,只要你喜欢。可千万别在你的系统盘上做试验,否则......可别怪bhw98没有提醒过你喔!


Q 我知道怎么测试光驱的传输速度了,就用上面的方法,读出一定长度数据,除以所需时间,应该可以吧?

A 可以。但取光盘参数时要用IOCTL_STORAGE_GET_MEDIA_TYPES_EX,我们已经探讨过的。

Q 用IOCTL_DISK_GET_DRIVE_GEOMETRY或IOCTL_STORAGE_GET_MEDIA_TYPES_EX只能得到很少的磁盘参数,我想获得包括硬盘序列号在内的更加详细的信息,有什么办法呀?

A 确实,用你所说的I/O控制码,只能得到最基本的磁盘参数。获取磁盘出厂信息的I/O控制码,微软在VC/MFC环境中没有开放,在DDK中可以发现一些线索。早先,Lynn McGuire写了一个很出名的获取IDE硬盘详细信息的程序DiskID32,下面的例子是在其基础上经过增删和改进而成的。
本例中,我们要用到ATA/APAPI的IDENTIFY DEVICE指令。ATA/APAPI是国际组织T13起草和发布的IDE/EIDE/UDMA硬盘及其它可移动存储设备与主机接口的标准,至今已经到了ATA/APAPI-7版本。该接口标准规定了ATA/ATAPI设备的输入输出寄存器和指令集。欲了解更详细的ATA/ATAPI技术资料,可访问T13的站点。
用到的常量及数据结构有以下一些:

// IOCTL控制码
// #define DFP_SEND_DRIVE_COMMAND   0x0007c084
#define  DFP_SEND_DRIVE_COMMAND   CTL_CODE(IOCTL_DISK_BASE, 0x0021, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS)
// #define DFP_RECEIVE_DRIVE_DATA   0x0007c088
#define  DFP_RECEIVE_DRIVE_DATA   CTL_CODE(IOCTL_DISK_BASE, 0x0022, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS)

#define  FILE_DEVICE_SCSI         0x0000001b
#define  IOCTL_SCSI_MINIPORT_IDENTIFY      ((FILE_DEVICE_SCSI << 16) + 0x0501)
#define  IOCTL_SCSI_MINIPORT 0x0004D008  //  see NTDDSCSI.H for definition

// ATA/ATAPI指令
#define  IDE_ATA_IDENTIFY    0xEC   //  ATA的ID指令(IDENTIFY DEVICE)

// IDE命令寄存器
typedef struct _IDEREGS
{
   BYTE bFeaturesReg;       // 特征寄存器(用于SMART命令)
   BYTE bSectorCountReg;    // 扇区数目寄存器
   BYTE bSectorNumberReg;   // 开始扇区寄存器
   BYTE bCylLowReg;         // 开始柱面低字节寄存器
   BYTE bCylHighReg;        // 开始柱面高字节寄存器
   BYTE bDriveHeadReg;      // 驱动器/磁头寄存器
   BYTE bCommandReg;        // 指令寄存器
   BYTE bReserved;          // 保留
} IDEREGS, *PIDEREGS, *LPIDEREGS;

// 从驱动程序返回的状态
typedef struct _DRIVERSTATUS
{
   BYTE  bDriverError;      // 错误码
   BYTE  bIDEStatus;        // IDE状态寄存器
   BYTE  bReserved[2];      // 保留
   DWORD  dwReserved[2];    // 保留
} DRIVERSTATUS, *PDRIVERSTATUS, *LPDRIVERSTATUS;

// IDE设备IOCTL输入数据结构
typedef struct _SENDCMDINPARAMS
{
   DWORD     cBufferSize;   // 缓冲区字节数
   IDEREGS   irDriveRegs;   // IDE寄存器组
   BYTE bDriveNumber;       // 驱动器号
   BYTE bReserved[3];       // 保留
   DWORD     dwReserved[4]; // 保留
   BYTE      bBuffer[1];    // 输入缓冲区(此处象征性地包含1字节)
} SENDCMDINPARAMS, *PSENDCMDINPARAMS, *LPSENDCMDINPARAMS;

// IDE设备IOCTL输出数据结构
typedef struct _SENDCMDOUTPARAMS
{
   DWORD         cBufferSize;   // 缓冲区字节数
   DRIVERSTATUS  DriverStatus;  // 驱动程序返回状态
   BYTE          bBuffer[1];    // 输入缓冲区(此处象征性地包含1字节)
} SENDCMDOUTPARAMS, *PSENDCMDOUTPARAMS, *LPSENDCMDOUTPARAMS;

// IDE的ID命令返回的数据
// 共512字节(256个WORD),这里仅定义了一些感兴趣的项(基本上依据ATA/ATAPI-4)
typedef struct _IDINFO
{
 USHORT  wGenConfig;     // WORD 0: 基本信息字
 USHORT  wNumCyls;     // WORD 1: 柱面数
 USHORT  wReserved2;     // WORD 2: 保留
 USHORT  wNumHeads;     // WORD 3: 磁头数
 USHORT  wReserved4;        // WORD 4: 保留
 USHORT  wReserved5;        // WORD 5: 保留
 USHORT  wNumSectorsPerTrack;  // WORD 6: 每磁道扇区数
 USHORT  wVendorUnique[3];   // WORD 7-9: 厂家设定值
 CHAR    sSerialNumber[20];   // WORD 10-19:序列号
 USHORT  wBufferType;    // WORD 20: 缓冲类型
 USHORT  wBufferSize;    // WORD 21: 缓冲大小
 USHORT  wECCSize;     // WORD 22: ECC校验大小
 CHAR    sFirmwareRev[8];   // WORD 23-26: 固件版本
 CHAR    sModelNumber[40];   // WORD 27-46: 内部型号
 USHORT  wMoreVendorUnique;   // WORD 47: 厂家设定值
 USHORT  wReserved48;    // WORD 48: 保留
 struct {
  USHORT  reserved1:8;
  USHORT  DMA:1;     // 1=支持DMA
  USHORT  LBA:1;     // 1=支持LBA
  USHORT  DisIORDY:1;    // 1=可不使用IORDY
  USHORT  IORDY:1;    // 1=支持IORDY
  USHORT  SoftReset:1;   // 1=需要ATA软启动
  USHORT  Overlap:1;    // 1=支持重叠操作
  USHORT  Queue:1;    // 1=支持命令队列
  USHORT  InlDMA:1;    // 1=支持交叉存取DMA
 } wCapabilities;     // WORD 49: 一般能力
 USHORT  wReserved1;     // WORD 50: 保留
 USHORT  wPIOTiming;     // WORD 51: PIO时序
 USHORT  wDMATiming;     // WORD 52: DMA时序
 struct {
  USHORT  CHSNumber:1;   // 1=WORD 54-58有效
  USHORT  CycleNumber:1;   // 1=WORD 64-70有效
  USHORT  UnltraDMA:1;   // 1=WORD 88有效
  USHORT  reserved:13;
 } wFieldValidity;     // WORD 53: 后续字段有效性标志
 USHORT  wNumCurCyls;    // WORD 54: CHS可寻址的柱面数
 USHORT  wNumCurHeads;    // WORD 55: CHS可寻址的磁头数
 USHORT  wNumCurSectorsPerTrack;  // WORD 56: CHS可寻址每磁道扇区数
 USHORT  wCurSectorsLow;    // WORD 57: CHS可寻址的扇区数低位字
 USHORT  wCurSectorsHigh;   // WORD 58: CHS可寻址的扇区数高位字
 struct {
  USHORT  CurNumber:8;   // 当前一次性可读写扇区数
  USHORT  Multi:1;    // 1=已选择多扇区读写
  USHORT  reserved1:7;
 } wMultSectorStuff;     // WORD 59: 多扇区读写设定
 ULONG  dwTotalSectors;    // WORD 60-61: LBA可寻址的扇区数
 USHORT  wSingleWordDMA;    // WORD 62: 单字节DMA支持能力
 struct {
  USHORT  Mode0:1;    // 1=支持模式0 (4.17Mb/s)
  USHORT  Mode1:1;    // 1=支持模式1 (13.3Mb/s)
  USHORT  Mode2:1;    // 1=支持模式2 (16.7Mb/s)
  USHORT  Reserved1:5;
  USHORT  Mode0Sel:1;    // 1=已选择模式0
  USHORT  Mode1Sel:1;    // 1=已选择模式1
  USHORT  Mode2Sel:1;    // 1=已选择模式2
  USHORT  Reserved2:5;
 } wMultiWordDMA;     // WORD 63: 多字节DMA支持能力
 struct {
  USHORT  AdvPOIModes:8;   // 支持高级POI模式数
  USHORT  reserved:8;
 } wPIOCapacity;      // WORD 64: 高级PIO支持能力
 USHORT  wMinMultiWordDMACycle;  // WORD 65: 多字节DMA传输周期的最小值
 USHORT  wRecMultiWordDMACycle;  // WORD 66: 多字节DMA传输周期的建议值
 USHORT  wMinPIONoFlowCycle;   // WORD 67: 无流控制时PIO传输周期的最小值
 USHORT  wMinPOIFlowCycle;   // WORD 68: 有流控制时PIO传输周期的最小值
 USHORT  wReserved69[11];   // WORD 69-79: 保留
 struct {
  USHORT  Reserved1:1;
  USHORT  ATA1:1;     // 1=支持ATA-1
  USHORT  ATA2:1;     // 1=支持ATA-2
  USHORT  ATA3:1;     // 1=支持ATA-3
  USHORT  ATA4:1;     // 1=支持ATA/ATAPI-4
  USHORT  ATA5:1;     // 1=支持ATA/ATAPI-5
  USHORT  ATA6:1;     // 1=支持ATA/ATAPI-6
  USHORT  ATA7:1;     // 1=支持ATA/ATAPI-7
  USHORT  ATA8:1;     // 1=支持ATA/ATAPI-8
  USHORT  ATA9:1;     // 1=支持ATA/ATAPI-9
  USHORT  ATA10:1;    // 1=支持ATA/ATAPI-10
  USHORT  ATA11:1;    // 1=支持ATA/ATAPI-11
  USHORT  ATA12:1;    // 1=支持ATA/ATAPI-12
  USHORT  ATA13:1;    // 1=支持ATA/ATAPI-13
  USHORT  ATA14:1;    // 1=支持ATA/ATAPI-14
  USHORT  Reserved2:1;
 } wMajorVersion;     // WORD 80: 主版本
 USHORT  wMinorVersion;    // WORD 81: 副版本
 USHORT  wReserved82[6];    // WORD 82-87: 保留
 struct {
  USHORT  Mode0:1;    // 1=支持模式0 (16.7Mb/s)
  USHORT  Mode1:1;    // 1=支持模式1 (25Mb/s)
  USHORT  Mode2:1;    // 1=支持模式2 (33Mb/s)
  USHORT  Mode3:1;    // 1=支持模式3 (44Mb/s)
  USHORT  Mode4:1;    // 1=支持模式4 (66Mb/s)
  USHORT  Mode5:1;    // 1=支持模式5 (100Mb/s)
  USHORT  Mode6:1;    // 1=支持模式6 (133Mb/s)
  USHORT  Mode7:1;    // 1=支持模式7 (166Mb/s) ???
  USHORT  Mode0Sel:1;    // 1=已选择模式0
  USHORT  Mode1Sel:1;    // 1=已选择模式1
  USHORT  Mode2Sel:1;    // 1=已选择模式2
  USHORT  Mode3Sel:1;    // 1=已选择模式3
  USHORT  Mode4Sel:1;    // 1=已选择模式4
  USHORT  Mode5Sel:1;    // 1=已选择模式5
  USHORT  Mode6Sel:1;    // 1=已选择模式6
  USHORT  Mode7Sel:1;    // 1=已选择模式7
 } wUltraDMA;      // WORD 88:  Ultra DMA支持能力
 USHORT    wReserved89[167];   // WORD 89-255
} IDINFO, *PIDINFO;

// SCSI驱动所需的输入输出共用的结构
typedef struct _SRB_IO_CONTROL
{
   ULONG HeaderLength;  // 头长度
   UCHAR Signature[8];  // 特征名称
   ULONG Timeout;   // 超时时间
   ULONG ControlCode;  // 控制码
   ULONG ReturnCode;  // 返回码
   ULONG Length;   // 缓冲区长度
} SRB_IO_CONTROL, *PSRB_IO_CONTROL;

需要引起注意的是IDINFO第57-58 WORD (CHS可寻址的扇区数),因为不满足32位对齐的要求,不可定义为一个ULONG字段。Lynn McGuire的程序里正是由于定义为一个ULONG字段,导致该结构不可用。

以下是核心代码

// 打开设备
// filename: 设备的“文件名”
HANDLE OpenDevice(LPCTSTR filename)
{
 HANDLE hDevice;

 // 打开设备
 hDevice= ::CreateFile(filename,   // 文件名
  GENERIC_READ | GENERIC_WRITE,  // 读写方式
  FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式
  NULL,     // 默认的安全描述符
  OPEN_EXISTING,    // 创建方式
  0,     // 不需设置文件属性
  NULL);     // 不需参照模板文件

 return hDevice;
}

// 向驱动发“IDENTIFY DEVICE”命令,获得设备信息
// hDevice: 设备句柄
// pIdInfo:  设备信息结构指针
BOOL IdentifyDevice(HANDLE hDevice, PIDINFO pIdInfo)
{
 PSENDCMDINPARAMS pSCIP;  // 输入数据结构指针
 PSENDCMDOUTPARAMS pSCOP; // 输出数据结构指针
 DWORD dwOutBytes;   // IOCTL输出数据长度
 BOOL bResult;    // IOCTL返回值

 // 申请输入/输出数据结构空间
    pSCIP = (PSENDCMDINPARAMS)::GlobalAlloc(LMEM_ZEROINIT, sizeof(SENDCMDINPARAMS)-1);
    pSCOP = (PSENDCMDOUTPARAMS)::GlobalAlloc(LMEM_ZEROINIT, sizeof(SENDCMDOUTPARAMS)+sizeof(IDINFO)-1);

 // 指定ATA/ATAPI命令的寄存器值
// pSCIP->irDriveRegs.bFeaturesReg = 0;
// pSCIP->irDriveRegs.bSectorCountReg = 0;
// pSCIP->irDriveRegs.bSectorNumberReg = 0;
// pSCIP->irDriveRegs.bCylLowReg = 0;
// pSCIP->irDriveRegs.bCylHighReg = 0;
// pSCIP->irDriveRegs.bDriveHeadReg = 0;
 pSCIP->irDriveRegs.bCommandReg = IDE_ATA_IDENTIFY;

 // 指定输入/输出数据缓冲区大小
 pSCIP->cBufferSize = 0;
 pSCOP->cBufferSize = sizeof(IDINFO);

 // IDENTIFY DEVICE
 bResult = ::DeviceIoControl(hDevice,  // 设备句柄
  DFP_RECEIVE_DRIVE_DATA,   // 指定IOCTL
  pSCIP, sizeof(SENDCMDINPARAMS) - 1, // 输入数据缓冲区
  pSCOP, sizeof(SENDCMDOUTPARAMS) + sizeof(IDINFO) - 1, // 输出数据缓冲区
  &dwOutBytes,    // 输出数据长度
  (LPOVERLAPPED)NULL);   // 用同步I/O

 // 复制设备参数结构
 ::memcpy(pIdInfo, pSCOP->bBuffer, sizeof(IDINFO));

 // 释放输入/输出数据空间
 ::GlobalFree(pSCOP);
 ::GlobalFree(pSCIP);

 return bResult;
}

// 向SCSI MINI-PORT驱动发“IDENTIFY DEVICE”命令,获得设备信息
// hDevice: 设备句柄
// pIdInfo:  设备信息结构指针
BOOL IdentifyDeviceAsScsi(HANDLE hDevice, int nDrive, PIDINFO pIdInfo)
{
 PSENDCMDINPARAMS pSCIP;  // 输入数据结构指针
 PSENDCMDOUTPARAMS pSCOP; // 输出数据结构指针
 PSRB_IO_CONTROL pSRBIO;  // SCSI输入输出数据结构指针
 DWORD dwOutBytes;   // IOCTL输出数据长度
 BOOL bResult;    // IOCTL返回值

 // 申请输入/输出数据结构空间
    pSRBIO = (PSRB_IO_CONTROL)::GlobalAlloc(LMEM_ZEROINIT, sizeof(SRB_IO_CONTROL)+sizeof(SENDCMDOUTPARAMS)+sizeof(IDINFO)-1);
    pSCIP = (PSENDCMDINPARAMS)((char *)pSRBIO+sizeof(SRB_IO_CONTROL));
    pSCOP = (PSENDCMDOUTPARAMS)((char *)pSRBIO+sizeof(SRB_IO_CONTROL));

 // 填充输入/输出数据
 pSRBIO->HeaderLength = sizeof(SRB_IO_CONTROL);
 pSRBIO->Timeout = 10000;
 pSRBIO->Length = sizeof(SENDCMDOUTPARAMS)+sizeof(IDINFO)-1;
 pSRBIO->ControlCode = IOCTL_SCSI_MINIPORT_IDENTIFY;
 ::strncpy ((char *)pSRBIO->Signature, "SCSIDISK", 8);

 // 指定ATA/ATAPI命令的寄存器值
// pSCIP->irDriveRegs.bFeaturesReg = 0;
// pSCIP->irDriveRegs.bSectorCountReg = 0;
// pSCIP->irDriveRegs.bSectorNumberReg = 0;
// pSCIP->irDriveRegs.bCylLowReg = 0;
// pSCIP->irDriveRegs.bCylHighReg = 0;
// pSCIP->irDriveRegs.bDriveHeadReg = 0;
 pSCIP->irDriveRegs.bCommandReg = IDE_ATA_IDENTIFY;
 pSCIP->bDriveNumber = nDrive;

 // IDENTIFY DEVICE
 bResult = ::DeviceIoControl(hDevice,  // 设备句柄
  IOCTL_SCSI_MINIPORT,   // 指定IOCTL
  pSRBIO, sizeof(SRB_IO_CONTROL) +sizeof(SENDCMDINPARAMS) - 1, // 输入数据缓冲区
  pSRBIO, sizeof(SRB_IO_CONTROL) +sizeof(SENDCMDOUTPARAMS) + sizeof(IDINFO) - 1, // 输出数据缓冲区
  &dwOutBytes,  // 输出数据长度
  (LPOVERLAPPED)NULL); // 用同步I/O
 
 // 复制设备参数结构
 ::memcpy(pIdInfo, pSCOP->bBuffer, sizeof(IDINFO));

 // 释放输入/输出数据空间
 ::GlobalFree(pSRBIO);

 return bResult;
}

// 将串中的字符两两颠倒
// 原因是ATA/ATAPI中的WORD,与Windows采用的字节顺序相反
// 驱动程序中已经将收到的数据全部反过来,我们来个负负得正
void AdjustString(char* str, int len)
{
 char ch;
 int i;

 // 两两颠倒
 for(i=0;i<len;i+=2)
 {
  ch = str[i];
  str[i] = str[i+1];
  str[i+1] = ch;
 }

 // 若是右对齐的,调整为左对齐 (去掉左边的空格)
 i=0;
 while(i<len && str[i]==' ') i++;

 ::memmove(str, &str[i], len-i);

 // 去掉右边的空格
 i = len - 1;
 while(i>=0 && str[i]==' ')
 {
  str[i] = '/0';
  i--;
 }
}

// 读取IDE硬盘的设备信息,必须有足够权限
// nDrive: 驱动器号(0=第一个硬盘,1=0=第二个硬盘,......)
// pIdInfo: 设备信息结构指针
BOOL GetPhysicalDriveInfoInNT(int nDrive, PIDINFO pIdInfo)
{
 HANDLE hDevice;   // 设备句柄
 BOOL bResult;   // 返回结果
 char szFileName[20]; // 文件名

 ::sprintf(szFileName,".//PhysicalDrive%d", nDrive);

 hDevice = ::OpenDevice(szFileName);

 if(hDevice == INVALID_HANDLE_VALUE)
 {
  return FALSE;
 }

 // IDENTIFY DEVICE
 bResult = ::IdentifyDevice(hDevice, pIdInfo);

 if(bResult)
 {
  // 调整字符串
  ::AdjustString(pIdInfo->sSerialNumber, 20);
  ::AdjustString(pIdInfo->sModelNumber, 40);
  ::AdjustString(pIdInfo->sFirmwareRev, 8);
 }

 ::CloseHandle (hDevice);

 return bResult;
}

// 用SCSI驱动读取IDE硬盘的设备信息,不受权限制约
// nDrive: 驱动器号(0=Primary Master, 1=Promary Slave, 2=Secondary master, 3=Secondary slave)
// pIdInfo: 设备信息结构指针
BOOL GetIdeDriveAsScsiInfoInNT(int nDrive, PIDINFO pIdInfo)
{
 HANDLE hDevice;   // 设备句柄
 BOOL bResult;   // 返回结果
 char szFileName[20]; // 文件名

 ::sprintf(szFileName,".//Scsi%d:", nDrive/2);

 hDevice = ::OpenDevice(szFileName);
     
 if(hDevice == INVALID_HANDLE_VALUE)
 {
  return FALSE;
 }

 // IDENTIFY DEVICE
 bResult = ::IdentifyDeviceAsScsi(hDevice, nDrive%2, pIdInfo);

 // 检查是不是空串
 if(pIdInfo->sModelNumber[0]=='/0')
 {
  bResult = FALSE;
 }

 if(bResult)
 {
  // 调整字符串
  ::AdjustString(pIdInfo->sSerialNumber, 20);
  ::AdjustString(pIdInfo->sModelNumber, 40);
  ::AdjustString(pIdInfo->sFirmwareRev, 8);
 }

 return bResult;
}


Q 我注意到ATA/ATAPI里,以及DiskID32里,有一个“IDENTIFY PACKET DEVICE”指令,与“IDENTIFY DEVICE”有什么区别?

A IDENTIFY DEVICE专门用于固定硬盘,而IDENTIFY PACKET DEVICE用于可移动存储设备如CDROM、CF、MO、ZIP、TAPE等。因为驱动程序的原因,实际上用本例的方法,不管是IDENTIFY DEVICE也好,IDENTIFY PACKET DEVICE也好,获取可移动存储设备的详细信息,一般是做不到的。而且除了IDE硬盘,对SCSI、USB等接口的硬盘也不起作用。除非厂商提供的驱动支持这样的功能。

Q ATA/ATAPI有很多指令,如READ SECTORS, WRITE SECTORS, SECURITY, SLEEP, STANDBY等,利用上述方法,是否可进行相应操作?

A 应该没问题。但切记,要慎重慎重再慎重!


Q 关于权限问题,请解释一下好吗?

A 在NT/2000/XP下,administrator可以管理设备,上述两种访问驱动的方法都行。但在user身份下,或者登录到域后,用户无法访问PhysicalDrive驱动的核心层,但SCSI MINI-PORT驱动却可以。目前是可以,不知道Windows以后的版本是否支持。因为这肯定是一个安全隐患。
另外,我们着重讨论NT/2000/XP中DeviceIoControl的应用,如果需要在98/ME中得到包括硬盘序列号在内的更加详细的信息,请参考DiskID32。

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<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 对这两个问题我也是心存疑惑,但又不敢妄加猜测,正琢磨着向微软请教呢。

Q 在NT/2000/XP中,如何读取CMOS数据?

Q 在NT/2000/XP中,如何控制speaker发声?

Q 在NT/2000/XP中,如何直接访问物理端口?

A 看似小小问题,难倒多少好汉!
NT/2000/XP从安全性、可靠性、稳定性上考虑,应用程序和操作系统是分开的,操作系统代码运行在核心态,有权访问系统数据和硬件,能执行特权指令;应用程序运行在用户态,能够使用的接口和访问系统数据的权限都受到严格限制。当用户程序调用系统服务时,处理器捕获该调用,然后把调用的线程切换到核心态。当系统服务完成后,操作系统将线程描述表切换回用户态,调用者继续运行。
想在用户态应用程序中实现I/O读写,直接存取硬件,可以通过编写驱动程序,实现CreateFile、CloseHandle、 DeviceIOControl、ReadFile、WriteFile等功能。从Windows 2000开始,引入WDM核心态驱动程序的概念。
下面是我写的一个非常简单的驱动程序,可实现字节型端口I/O。

#include <ntddk.h>

// 设备类型定义
// 0-32767被Microsoft占用,用户自定义可用32768-65535
#define FILE_DEVICE_MYPORT 0x0000f000

// I/O控制码定义
// 0-2047被Microsoft占用,用户自定义可用2048-4095
#define MYPORT_IOCTL_BASE 0xf00

#define IOCTL_MYPORT_READ_BYTE  CTL_CODE(FILE_DEVICE_MYPORT, MYPORT_IOCTL_BASE, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_MYPORT_WRITE_BYTE  CTL_CODE(FILE_DEVICE_MYPORT, MYPORT_IOCTL_BASE+1, METHOD_BUFFERED, FILE_ANY_ACCESS)

// IOPM是65536个端口的位屏蔽矩阵,包含8192字节(8192 x 8 = 65536)
// 0 bit: 允许应用程序访问对应端口
// 1 bit: 禁止应用程序访问对应端口

#define IOPM_SIZE 8192

typedef UCHAR IOPM[IOPM_SIZE];

IOPM *pIOPM = NULL;

// 设备名(要求以UNICODE表示)
const WCHAR NameBuffer[] = L"//Device//MyPort";
const WCHAR DOSNameBuffer[] = L"//DosDevices//MyPort";

// 这是两个在ntoskrnl.exe中的未见文档的服务例程
// 没有现成的已经说明它们原型的头文件,我们自己声明
void Ke386SetIoAccessMap(int, IOPM *);
void Ke386IoSetAccessProcess(PEPROCESS, int);

// 函数原型预先说明
NTSTATUS MyPortDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);
void MyPortUnload(IN PDRIVER_OBJECT DriverObject);


// 驱动程序入口,由系统自动调用,就像WIN32应用程序的WinMain
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
 PDEVICE_OBJECT deviceObject;
 NTSTATUS status;
 UNICODE_STRING uniNameString, uniDOSString;

 // 为IOPM分配内存
 pIOPM = MmAllocateNonCachedMemory(sizeof(IOPM));
 if(pIOPM == 0)
 {
  return STATUS_INSUFFICIENT_RESOURCES;
 }

 // IOPM全部初始化为0(允许访问所有端口)
 RtlZeroMemory(pIOPM, sizeof(IOPM));
   
 // 将IOPM加载到当前进程
 Ke386IoSetAccessProcess(PsGetCurrentProcess(), 1);
    Ke386SetIoAccessMap(1, pIOPM);

 // 指定驱动名字
 RtlInitUnicodeString(&uniNameString, NameBuffer);
 RtlInitUnicodeString(&uniDOSString, DOSNameBuffer);

 // 创建设备
 status = IoCreateDevice(DriverObject, 0,
     &uniNameString,
     FILE_DEVICE_MYPORT,
     0, FALSE, &deviceObject);

 if(!NT_SUCCESS(status))
 {
  return status;
 }

 // 创建WIN32应用程序需要的符号连接
 status = IoCreateSymbolicLink (&uniDOSString, &uniNameString);

 if (!NT_SUCCESS(status))
 {
  return status;
 }

 // 指定驱动程序有关操作的模块入口(函数指针)
 // 涉及以下两个模块:MyPortDispatch和MyPortUnload
    DriverObject->MajorFunction[IRP_MJ_CREATE]         =
    DriverObject->MajorFunction[IRP_MJ_CLOSE]          =
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyPortDispatch;
 DriverObject->DriverUnload = MyPortUnload;

    return STATUS_SUCCESS;
}


// IRP处理模块
NTSTATUS MyPortDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
  PIO_STACK_LOCATION IrpStack;
  ULONG              dwInputBufferLength;
  ULONG              dwOutputBufferLength;
  ULONG              dwIoControlCode;
  PULONG               pvIOBuffer;
  NTSTATUS           ntStatus;

  // 填充几个默认值
  Irp->IoStatus.Status = STATUS_SUCCESS; // 返回状态
  Irp->IoStatus.Information = 0;   // 输出长度
 
  IrpStack = IoGetCurrentIrpStackLocation(Irp);

  // Get the pointer to the input/output buffer and it's length

  // 输入输出共用的缓冲区
  // 因为我们在IOCTL中指定了METHOD_BUFFERED,
  pvIOBuffer = Irp->AssociatedIrp.SystemBuffer;

 switch (IrpStack->MajorFunction)
 {
  case IRP_MJ_CREATE:  // 与WIN32应用程序中的CreateFile对应
   break;
   
  case IRP_MJ_CLOSE:  // 与WIN32应用程序中的CloseHandle对应
   break;
  
  case IRP_MJ_DEVICE_CONTROL:  // 与WIN32应用程序中的DeviceIoControl对应
   dwIoControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode;
   switch (dwIoControlCode)
   {
    // 我们约定,缓冲区共两个DWORD,第一个DWORD为端口,第二个DWORD为数据
    // 一般做法是专门定义一个结构,此处简单化处理了
    case IOCTL_MYPORT_READ_BYTE:  // 从端口读字节
     pvIOBuffer[1] = _inp(pvIOBuffer[0]);
     Irp->IoStatus.Information = 8;  // 输出长度为8
     break;
    case IOCTL_MYPORT_WRITE_BYTE:  // 写字节到端口
     _outp(pvIOBuffer[0], pvIOBuffer[1]);
     break;
    default:  // 不支持的IOCTL
     Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
   }
 }

 ntStatus = Irp->IoStatus.Status;

 IoCompleteRequest (Irp, IO_NO_INCREMENT);

 return ntStatus;
}

// 删除驱动
void MyPortUnload(IN PDRIVER_OBJECT DriverObject)
{
 UNICODE_STRING uniDOSString;

 if(pIOPM)
 {
  // 释放IOPM占用的空间
  MmFreeNonCachedMemory(pIOPM, sizeof(IOPM));
 }

 RtlInitUnicodeString(&uniDOSString, DOSNameBuffer);

 // 删除符号连接和设备
 IoDeleteSymbolicLink (&uniDOSString);
 IoDeleteDevice(DriverObject->DeviceObject);
}


下面给出实现设备驱动程序的动态加载的源码。动态加载的好处是,你不用做任何添加新硬件的操作,也不用编辑注册表,更不用重新启动计算机。

// 安装驱动并启动服务
// lpszDriverPath:  驱动程序路径
// lpszServiceName: 服务名
BOOL StartDriver(LPCTSTR lpszDriverPath, LPCTSTR lpszServiceName)
{
 SC_HANDLE hSCManager;  // 服务控制管理器句柄
 SC_HANDLE hService;  // 服务句柄
 BOOL bResult = FALSE;  // 返回值

 // 打开服务控制管理
 hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

 // 打开失败...
 if(hSCManager)
 {
  // 创建服务
  hService = CreateService(hSCManager,
     lpszServiceName,
     lpszServiceName,
     SERVICE_ALL_ACCESS,
     SERVICE_KERNEL_DRIVER,
     SERVICE_DEMAND_START,
     SERVICE_ERROR_NORMAL,
     lpszDriverPath,
     NULL,
     NULL,
     NULL,
     NULL,
     NULL);

  if(hService)
  {
   // 启动服务
      bResult = StartService(hService, 0, NULL);
 
   // 关闭服务句柄
   CloseServiceHandle(hService);
  }
 
  // 关闭服务控制管理器句柄
  CloseServiceHandle(hSCManager);
 }

 return bResult;
}

// 停止服务并卸下驱动
// lpszServiceName: 服务名
BOOL StopDriver(LPCTSTR lpszServiceName)
{
 SC_HANDLE hSCManager;  // 服务控制管理器句柄
 SC_HANDLE hService;  // 服务句柄
 BOOL bResult = FALSE;  // 返回值
 SERVICE_STATUS ServiceStatus;

 // 打开服务控制管理
 hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

 // 打开失败...
 if(hSCManager)
 {
  // 打开服务
  hService = OpenService(hSCManager, lpszServiceName, SERVICE_ALL_ACCESS);

  if(hService)
  {
   // 停止服务
   bResult = ControlService(hService, SERVICE_CONTROL_STOP, &ServiceStatus);

   // 删除服务
   bResult = bResult && DeleteService(hService);

   // 关闭服务句柄
   CloseServiceHandle(hService);
  }

  // 关闭服务控制管理器句柄
  CloseServiceHandle(hSCManager);
 }

 return bResult;
}

应用程序实现端口I/O的接口如下:

// 全局的设备句柄
HANDLE hMyPort;

// 打开设备
// lpszDevicePath: 设备的路径
HANDLE OpenDevice(LPCTSTR lpszDevicePath)
{
 HANDLE hDevice;

 // 打开设备
 hDevice= ::CreateFile(lpszDevicePath,  // 设备路径
  GENERIC_READ | GENERIC_WRITE,  // 读写方式
  FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式
  NULL,     // 默认的安全描述符
  OPEN_EXISTING,    // 创建方式
  0,     // 不需设置文件属性
  NULL);     // 不需参照模板文件

 return hDevice;
}

// 打开端口驱动
BOOL OpenMyPort()
{
 BOOL bResult;

 // 设备名为"MyPort",驱动程序位于Windows的"system32/drivers"目录中
 bResult = StartDriver("system32//drivers//MyPort.sys", "MyPort");

 // 设备路径为"//./MyPort"
 if(bResult) hMyPort=OpenDevice(".//MyPort");

 return (bResult && hMyPort!=INVALID_HANDLE_VALUE);
}

// 关闭端口驱动
BOOL CloseMyPort()
{
    return (CloseHandle(hMyPort) && StopDriver("MyPort"));
}


// 从指定端口读一个字节
// port: 端口
BYTE ReadPortByte(WORD port)
{
 DWORD buf[2];   // 输入输出缓冲区   
 DWORD dwOutBytes;  // IOCTL输出数据长度

 buf[0] = port;   // 第一个DWORD是端口
// buf[1] = 0;   // 第二个DWORD是数据

 // 用IOCTL_MYPORT_READ_BYTE读端口
 ::DeviceIoControl(hMyPort,  // 设备句柄
  IOCTL_MYPORT_READ_BYTE,  // 取设备属性信息
  buf, sizeof(buf),  // 输入数据缓冲区
  buf, sizeof(buf),  // 输出数据缓冲区
  &dwOutBytes,   // 输出数据长度
  (LPOVERLAPPED)NULL);  // 用同步I/O 

 return (BYTE)buf[1];
}

// 将一个字节写到指定端口
// port: 端口
// data: 字节数据
void WritePortByte(WORD port, BYTE data)
{
 DWORD buf[2];   // 输入输出缓冲区   
 DWORD dwOutBytes;  // IOCTL输出数据长度

 buf[0] = port;   // 第一个DWORD是端口
 buf[1] = data;   // 第二个DWORD是数据

 // 用IOCTL_MYPORT_WRITE_BYTE写端口
 ::DeviceIoControl(hMyPort,  // 设备句柄
  IOCTL_MYPORT_WRITE_BYTE, // 取设备属性信息
  buf, sizeof(buf),  // 输入数据缓冲区
  buf, sizeof(buf),  // 输出数据缓冲区
  &dwOutBytes,   // 输出数据长度
  (LPOVERLAPPED)NULL);  // 用同步I/O
}

有了ReadPortByte和WritePortByte这两个函数,我们就能很容易地操纵CMOS和speaker了(关于CMOS值的含义以及定时器寄存器定义,请参考相应的硬件资料):

// 0x70是CMOS索引端口(只写)
// 0x71是CMOS数据端口
BYTE ReadCmos(BYTE index)
{
 BYTE data;

 ::WritePortByte(0x70, index);
 
 data = ::ReadPortByte(0x71);

 return data;
}

// 0x61是speaker控制端口
// 0x43是8253/8254定时器控制端口
// 0x42是8253/8254定时器通道2的端口
void Sound(DWORD freq )
{
 BYTE data;

 if(freq>=20 && freq<=20000)
 {
  freq = 1193181 / freq;

  data = ::ReadPortByte(0x61);

  if((data & 3) == 0)
  {
   ::WritePortByte(0x61, data | 3);
   ::WritePortByte(0x43, 0xb6);
  }

  ::WritePortByte(0x42, (BYTE)(freq%256));
  ::WritePortByte(0x42, (BYTE)(freq/256));
 }
}

void NoSound( void )
{
 BYTE data;

 data = ::ReadPortByte(0x61);

 ::WritePortByte(0x61, data & 0xfc);
}

// 读出CMOS 128个字节
 for(int i=0;i<128;i++)
 {
  BYTE data = ::ReadCmos(i);
  ... ...
 }

// 用C调演奏“多-来-米”

 // 1 = 262 Hz
 ::Sound(262);
 ::Sleep(200);
 ::NoSound();

 // 2 = 288 Hz
 ::Sound(288);
 ::Sleep(200);
 ::NoSound();

 // 3 = 320 Hz
 ::Sound(320);
 ::Sleep(200);
 ::NoSound(); 


Q 就是个简单的端口I/O,这么麻烦才能实现,搞得俺头脑稀昏,有没有简洁明了的办法啊?

A 上面的例子,之所以从编写驱动程序,到安装驱动,到启动服务,到打开设备,到访问设备,一直到读写端口,这样一路下来,是为了揭示在NT/2000/XP中硬件访问技术的本质。假如将所有过程封装起来,只提供OpenMyPort, CloseMyPort, ReadPortByte, WritePortByte甚至更高层的ReadCmos、WriteCmos、Sound、NoSound给你调用,是不是会感觉清爽许多?
实际上,我们平常做的基于一定硬件的二次开发,一般会先安装驱动程序(DRV)和用户接口的运行库(DLL),然后在此基础上开发出我们的应用程序(APP)。DRV、DLL、APP三者分别运行在核心态、核心态/用户态联络带、用户态。比如买了一块图象采集卡,要先安装核心驱动,它的“Development Tool Kit”,提供类似于PCV_Initialize, PCV_Capture等的API,就是扮演核心态和用户态联络员的角色。我们根本不需要CreateFile、CloseHandle、 DeviceIOControl、ReadFile、WriteFile等较低层次的直接调用。
Yariv Kaplan写过一个WinIO的例子,能实现对物理端口和内存的访问,提供了DRV、DLL、APP三方面的源码,有兴趣的话可以深入研究一下。

 

驱动程序源码MyPort.zip (3KB, 编译环境: VC6+2000DDK)
演示程序源码MyPortIo.zip (22KB, 含MyPort.sys, 该文件需复制到windows的system32/drivers目录中)
Yariv Kaplan的主页:http://www.internals.com


[作者后记]
“实战DeviceIoControl系列”,到此告一段落了。
所谓“实战DeviceIoControl”,其实名不副实,并不是一步一步地介绍一个大型应用的开发,限于篇幅,只是列举一些用到DeviceIoControl的场合的例子而已。对涉及硬件的开发人员,DeviceIoControl是一个非常重要的API,其基本用处是联络设备驱动和应用程序,象本例中编写设备的用户接口时用到的情形比较多。如果只是限于开发应用程序,可能永远都用不到,也可能被迫使用(如获取硬盘序列号)。

 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值