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

转载:http://m.blog.csdn.net/article/details?id=21602051


DeviceIoControl的其实和ReadFile和WriteFile是一样的, 不过这个功能更强, 一次交互能够输入数据, 也可以输出数据.

DeviceIoControl内部创建的IRP是IRP_MJ_DEVICE_CONTROL类型的IRP, 然后操作系统会将这个IRP转发给驱动程序的分发函数中. 就是类似Win32下面的发送自定义消息了.. 但是又有点区别, 如果发送自定义消息, 如果跨进程, 那是不可以传递指针的, 但是和驱动那就无所谓了, 可以传递一块缓冲区过去。

利用该接口函数向指定的设备驱动发送正确的控制码及数据,然后分析它的响应,就可以达到我们的目的。

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                        // 模板文件的句柄
);
参数 类型及说明
hDeviceLong,设备句柄
dwIoControlCodeLong,带有 FSCTL_ 前缀的常数。参考设备控制选项的部分列
lpInBufferAny,具体取决于dwIoControlCode参数。参考设备控制选项的部分列表
nInBufferSizeLong,输入缓冲区的长度
lpOutBufferAny,具体取决于dwIoControlCode参数。参考设备控制选项的部分列表
nOutBufferSizeLong,输出缓冲区的长度
lpBytesReturned Long,实际装载到输出缓冲区的字节数量
lpOverlappedOVERLAPPED,这个结构用于重叠操作。针对同步操作,请用ByVal As Long传递零值
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/ROMCDROMx
磁带机TAPEx
其中,物理驱动器不包括软驱和光驱。逻辑驱动器可以是IDE/SCSI/PCMCIA/USB接口的硬盘分区(卷)、光驱、MO、CF卡等,甚至是虚拟盘。x=0,1,2 ……
其它的设备名称需通过驱动接口的GUID调用设备管理函数族取得,这里暂不讨论。

访问设备必须用设备句柄,而得到设备句柄必须知道设备路径,这个套路以你我之力是改变不了的。每个设备都有它所属类型的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 对这两个问题我也是心存疑惑,但又不敢妄加猜测,正琢磨着向微软请教呢。

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>
#include "MyPort.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;          // 服务句柄
    DWORD dwLastError;           // 错误码
    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 == NULL)
        {
            if (::GetLastError() == ERROR_SERVICE_EXISTS)
            {
                hService = ::OpenService(hSCManager, lpszServiceName, SERVICE_ALL_ACCESS);
            }
        }
  
        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;                // 返回值
    SERVICE_STATUS ServiceStatus;
  
    bResult = FALSE;
  
    // 打开服务控制管理器
    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三方面的源码,有兴趣的话可以深入研究一下。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值