内容


从 Windows 向 Linux 迁移设备控制应用程序

通过认识 Windows 和 Linux 在设备控制方面的差异,克服迁移中的难题

如果读者开发过不同平台的设备控制应用程序,那么肯定了解 Windows 和 Linux 的设备控制方式的差别,从一个平台向另一个平台迁移应用程序相当复杂。本文分析两种操作系统的设备控制原理,探究从架构到系统调用的各个方面,重点比较二者差别。本文还给出一个迁移示例(用 C/C++ 编写),详细演示迁移过程。

工作条件:
根据本文的写作目的,“Windows” 是指 Windows 2000 或其后续版本,且安装有 Microsoft Visual C++® 6.0 或其后续版本。Linux应当基于 2.6 版内核,且安装有 GNU GCC。

比较设备控制的架构

Windows 和 Linux 设备控制的方式是不同的。

Windows 设备控制架构

Windows 的 I/O 子系统将用户应用程序和设备驱动程序联系起来,并定义基础结构支持设备驱动程序。设备驱动程序为具体设备提供 I/O 接口(参见图 1)。

图 1:Windows 设备控制架构
Windows 设备控制架构
Windows 设备控制架构

在设备控制过程中,I/O 操作封装为 IRP(I/O 请求数据包)。I/O 管理器创建 IRP,并将它发送到堆栈顶部。然后,设备驱动程序获取 IRP 的堆栈地址。IRP 包含着 I/O 请求的参数。根据 IRP 包含的请求(比如 创建读取写入设备 I/O 控制清除关闭),各驱动程序通过硬件接口工作。

Linux 设备控制架构

Linux 的设备控制架构有所不同。主要区别是,Linux 的普通文件、目录、设备和 socket 都是文件 —Linux 的所有东西都是文件。为了访问设备,Linux 内核将设备操作调用通过文件系统映射到设备驱动程序。Linux 没有 I/O 管理器。所有 I/O 请求从开始就进入文件系统(参见图 2)。

图 2. Linux 设备控制架构
Linux 设备控制架构
Linux 设备控制架构

比较设备文件名和路径名

从开发的角度来看,获取设备句柄是设备控制的先决条件。但是,由于设备控制架构的差异,获取设备句柄会根据所用平台不同(Windows 还是 Linux)而有不同的过程。

一般而言,设备句柄由具体设备驱动程序的名称决定。

Windows 设备驱动程序的文件名不同于普通文件,通常称为设备路径名。它具有固定格式,形如 \.DeviceName。在 C/C++ 编程中,这个字符串应当是 \\.\DeviceName。在代码中表示为 \\\\.\\DeviceNameDeviceName 应当与相应设备驱动程序定义的设备名称相同。

有些设备名称由 Microsoft 定义,因此不能修改(如表 1 所示)。

表 1. Windows 设备名称(x = 0,1,2 等)
设备路径名
软盘驱动器 A: B:
硬盘逻辑子区 C: D: E: . . .
物理驱动器PhysicalDrivex
CD-ROM、DVD/ROMCdRomx
磁带驱动器Tapex
COM 端口COMx

例如,我们在 C/C++ 编程中使用设备路径名,比如 \\\\.\\PhysicalDrive1\\\\.\\CdRom0\\\\.\\Tape0。 关于这个列表未收录的其他设备的详细情况,请查看本文后面的 参考资料 小节。

因为 Linux 将设备描述为文件,所以可以在目录 ./dev 中找到所有设备文件。这个目录的设备驱动程序包括:

  • IDE(Integrated Drive Electronics)硬盘驱动器,比如 /dev/hda 和 /dev/hdb
  • CD-ROM 驱动器,有些是 IDE;也有些是模拟 SCSI(Small Computer Systems Interface)设备的 CD-RW(CD 读/写)驱动器,比如 /dev/scd0
  • 串行口,例如 /dev/ttyS0 表示 COM1,/dev/ttyS1 表示 COM2,依此类推
  • 定位设备,包括 /dev/input/mice 等
  • 打印机,比如 /dev/lp0

常见设备文件大多可以按照上述描述找到。有关其他设备文件名和设备的详细信息,请使用命令 dmesg

比较主系统调用

设备控制的主系统调用包括下列操作:打开、关闭、I/O 控制、读/写等。参见表 2 所示的 Windows/Linux 映射。

表 2. 设备控制函数的映射
WindowsLinux
CreateFileopen
CloseHandleclose
DeviceIoControlioctl
ReadFileread
WriteFilewrite

现在,我们深入探讨三个最常用的函数:createclosedevioctl

Windows 的设备打开和关闭

我们讨论 Windows 函数 CreateFileCloseHandle。函数 CreateFile 用于打开设备。该函数返回句柄,用以访问清单 1 所示的对象。

清单 1. Windows 的 CreateFile 函数
HANDLE CreateFile (LPCTSTR lpFileName,          //File name of the device 
                                                  (Device Pathname)
   DWORD dwDesiredAccess,                       //Access mode to the object (read, write, 
                                                  or both)
   DWORD dwShareMode,                           //Sharing mode of the object (read, 
                                                  write, both or none)
   LPSECURITY_ATTRIBUTES lpSecurityAttributes,  //Security attribute determining whether 
                                                  the returned handle can be inherited by 
                                                  child processes
   DWORD dwCreationDisposition,                 //Action taken on files that exist and 
                                                  do not exist
   DWORD dwFlagsAndAttributes,                  //File attributes and flags
   HANDLE hTemplateFile);                       //A handle to a template file

参数 lpFileName 是前面讲过的设备路径名。通常,打开设备需要将 dwDesiredAccess 设置为 0 或 GENERIC_READ|GENERIC_WRITE,将 dwShareMode 设置为 FILE_SHARE_READ|FILE_SHARE_WRITE,将 dwCreationDisposition 设置为 OPEN_EXISTING,以及将 dwFlagsAndAttributeshTemplateFile 设置为 0 或 NULL。返回句柄将用于后续设备控制操作。

关闭设备使用函数 CloseHandle。将参数 hObject 设置为设备打开时返回的句柄:BOOL WINAPI CloseHandle (HANDLE hObject);

Linux 的设备打开和关闭

在 Linux 中,我们讨论的是函数 openclose。 如前所述,打开设备就像打开普通文件一样。清单 2 显示如何使用 open 获取设备句柄。

清单 2. Linux 的 open 函数
int open (const char *pathname,
       int flags, 
       mode_t mode);

调用成功将返回文件描述符,它是进程尚未打开的序号最小的文件描述符。如果调用失败,将返回 -1。文件描述符用作设备句柄。

参数标志必须包含 O_RDONLYO_WRONLYO_RDWR 的其中之一。其他标志可选。参数模式在新文件创立时说明文件访问权。

在 Linux 中,函数 close 关闭设备就像关闭文件一样:int close(int fd);

Windows 的 DeviceIoControl

设备控制(Windows 的 DeviceIoControl 和 Linux 的 ioctl)是最常用的设备控制函数,可以完成设备访问、信息获取、命令发送和数据交换等任务。清单 3 举例说明了 DeviceIoControl

清单 3. Windows 的 DeviceIoControl 函数
BOOL DeviceIoControl (HANDLE hDevice,
      DWORD dwIoControlCode,
      LPVOID lpInBuffer,
      DWORD nInBufferSize,
      LPVOID lpOutBuffer,
      DWORD nOutBufferSize,
      LPDWORD lpBytesReturned,
      LPOVERLAPPED lpOverlapped);

这个系统调用向指定设备发送控制代码和其他数据。相应设备驱动程序按照控制代码 dwIoControlCode 的指示工作。例如,使用IOCTL_DISK_GET_DRIVE_GEOMETRY 可以从物理驱动器获取结构参数(介质类型、柱面数、每柱面磁道数、每磁道扇区数等)。可以在 MSDN 网站上找到所有控制代码定义、头文件和其他详细内容(参见 参考资料 获得相关链接)。

是否需要输入/输出缓冲,以及它们结构和大小怎样,都取决于实际 ioctl 过程涉及的设备和操作,并由该调用指定的 dwIoControlCode 确定。

如果重叠操作的指针设为 NULL,那么 DeviceIoControl 将以阻塞(同步)方式工作。否则,它以异步方式工作。

Linux 函数 ioctl

Linux 可以使用 ioctlint ioctl(int fildes, int request, /* arg */ ...); — 向指定设备发送控制信息。第一个参数 fildes 是函数 open() 返回的文件描述符,用于指称具体设备。

与对应的系统调用 DeviceIOControl 不同,ioctl 的输入参数列表并不固定。它取决于 ioctl 进行何种请求,以及请求参数有何说明,正如 Windows 函数 DeviceIOControl 的参数 dwIoControlCode 一样。但是,迁移期间需要注意何时选择正确的请求参数,因为 DeviceIOControldwIoControlCodeioctlrequest 具有不同的取值。而且 dwIoControlCoderequest 之间没有显式映射列表。通常可以在相关头文件中查找请求参数值的定义来选择参数值。所有控制代码的定义在 /usr/include/{asm,linux}/*.h 文件中。

参数 arg 为具体设备的运转提供详细的命令信息。arg 的数据类型取决于特定控制请求。这个参数可以用于发送详细命令和接收返回数据。

迁移示例

我们查看一个从 Windows 向 Linux 迁移的过程的示例。这个示例涉及从个人电脑主 IDE 硬盘驱动器读取 SMART 日志。

步骤 1. 识别设备类型

如前所述,Linux 的各个设备被当作文件。首先要描述设备在 Linux 上的文件名。只有使用这个文件名,才能获取设备控制需要的设备句柄。

在这个示例中,对象是 IDE 硬盘驱动器。Linux 将其描述为 /dev/hda、/dev/hdb 等。本例将要迁移的硬盘设备路径名是 \\\\.\\PhysicalDrive0。/dev/hda 是该设备对应的 Linux 文件名。

步骤 2. 改变包含头文件

必须将 #include 头文件改为 Linux 形式(参见表 3):

表 3. #include 头文件
WindowsLinux
#include <windows.h> #include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <devioctl.h> #include <sys/ioctl.h>
#include <ntddscsi.h> #include <linux/hdreg.h>

windows.h 包含打开和关闭设备的函数(CreateFileCloseHandle)。相应地,在 Linux 中用于 open()close() 的函数应当包含头文件 sys/types.h、sys/stat.h 和 fcntl.h。

Windows 的 devioctl.h 用于函数 DeviceIoControl,我们将其改为 sys/ioctl.h 以确保该函数 ioctl 能够工作。

ntddscsi.h(它是来自 DDK 的头文件)定义了一组用于设备控制的控制代码。因为本例只处理 IDE 硬盘驱动器,所以只需将 linux/hdreg.h 添加到 Linux 程序。

对于其他情况,应当确保包含所有头文件(它们带有所需的控制代码的定义)。例如,如果访问 CD-ROM 而非硬盘驱动器,那么应当包含 linux/cdrom.h。

步骤 3. 改正函数和参数

现在我们详细查看代码。清单 4 显示命令的详细信息。

清单 4. 命令详解
unsigned char cmdBuff[7];
cmdBuff[0] = SMART_READ_LOG;  // Used for specifying SMART "commands"
cmdBuff[1] = 1;               // IDE sector count register
cmdBuff[2] = 1;               // IDE sector number register
cmdBuff[3] = SMART_CYL_LOW;   // IDE low order cylinder value
cmdBuff[4] = SMART_CYL_HI;    // IDE high order cylinder value
cmdBuff[5] = 0xA0 | (((Dev->Id-1) & 1) * 16); // IDE drive/head register
cmdBuff[6] = SMART_CMD;       // Actual IDE command

命令信息来自 ATA 命令说明书。因为将此代码移植到 Linux 不需要修改,所以没有必要进一步分析。

清单 5 所示代码打开 Windows 主硬盘驱动器。

清单 5. 打开 Windows 主硬盘驱动器
HANDLE devHandle = CreateFile("\\\\.\\PhysicalDrive0",           //pathname
                             GENERIC_WRITE|GENERIC_READ,         //Access Mode
                             FILE_SHARE_READ|FILE_SHARE_WRITE,   //Sharing Mode
                             NULL,OPEN_EXISTING,0,NULL);

从有关设备打开和关闭的讲解可知,我们需要两个参数(文件路径名和设备访问模式)来打开 Linux 设备。根据前面的原始代码,第一个参数应当是 /dev/hda,第二个是 O_RDONLY|O_NONBLOCK。修改过的代码如下所示:HANDLE devHandle = open("/dev/hda", O_RDONLY | O_NONBLOCK);。相应将 CloseHandle(devHandle); 更改为 close(devHandle);

移植的主要部分是如何使用 ioctl 访问特定设备和获取需要的信息。原始 Windows 代码如清单 6 所示:

清单 6. Windows 上 DeviceIoControl 的源代码
typedef struct _Buffer{
       UCHAR   req[8];              // Detailed command information other than 
                                       control code
       ULONG   DataBufferSize;      // Size of Data Buffer, here is 512
       UCHAR   DataBuffer[512];     // Data Buffer
} Buffer;

Buffer regBuffer;
memcpy(regBuffer.req, cmdBuff, 7);  //req[7] is reserved for future use. Must be zero.
regBuffer.DataBufferSize = 512;
unsigned int size = 512+12;         // Size of regBuffer
                                    // 8 for req, 4 for DataBufferSize, 512 for data
DWORD bytesRet = 0;                 // Number of bytes returned
int retval;                         // Returned value

retval = DeviceIoControl(devHandle,
                         IOCTL_IDE_PASS_THROUGH,  //Control code
                         regBuffer, // Input Buffer, including detailed command
                         size, 
                         regBuffer, // Output Buffer, use the same buffer here
                         size, 
                         &bytesRet, NULL);
if (!retval)
	cout<<"DeviceIoControl failed."<<endl;
else
memcpy(data, retBuffer.DataBuffer, 512);

DeviceIoControlioctl 需要更多的参数。设备句柄在两个平台上都是第一个参数,它从 CreateFile 和 Linux 的 open() 返回。但是 Windows 的控制代码和 Linux 的请求在定义上差别很大,以致没有固定规则能够找出这两个参数的映射关系,如前文所述。 IOCTL_IDE_PASS_THROUGH 在头文件 ntddscsi.h 中定义为 CTL_CODE (IOCTL_SCSI_BASE, 0x040a, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS)。通过在头文件 /usr/include/linux/hdreg.h 中查找定义,可以选用相应 Linux 控制代码 HDIO_DRIVE_CMD

另外,设备要完成具体任务需要详细的命令信息。该命令存放在缓存中,与返回数据的内存空间在进程中交换数据。我们使用同一缓存来发送命令和获取所需日志信息。Linux 的缓存大小可以改变;不一定用完八个字节。本例只用了命令的四个字节。

对应的 Linux 代码(清单 7)看起来简单很多,因为它的结构和函数参数比 Windows 简单。

清单 7. Linux 函数 ioctl 的源代码
int retval;
unsigned char req[4+512]; // Enough for returned data and the 4 byte detailed 
                             command information
req[0]= cmdBuff[6];       // Consider the requirement in this sample, only 4 bytes 
                             are used
req[1]= cmdBuff[2];
req[2]= cmdBuff[0];
req[3]= cmdBuff[1];

retval = ioctl(devHandle, HDIO_DRIVE_CMD, &req);
if(ret)
	cout<<"ioctl failed."<<endl;
else 
memcpy(data, &req[4], 512);

步骤 4. Linux 环境下的测试

在改正头文件、函数和参数之后,该程序准备在 Linux 上运行。现在的任务是在 Linux 平台上编译该程序并纠正剩余的语法错误。根据 Linux 版本和编译环境,可能需要另做修改。


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=320251
ArticleTitle=从 Windows 向 Linux 迁移设备控制应用程序
publish-date=07142008