通過認識 Microsoft Windows® 和 Linux® 操作系統設備控制的工作原理,本文將簡化從 Microsoft Windows® 向 Linux® 遷移設備控制應用程序。作者分析二者的差別,並給出 C/C++ 示例。
如果讀者開發過不同平台的設備控制應用程序,那麼肯定了解 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)。
在設備控制過程中,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)。
比較設備文件名和路徑名
從開發的角度來看,獲取設備句柄是設備控制的先決條件。但是,由於設備控制架構的差異,獲取設備句柄會根據所用平台不同(Windows 還是 Linux)而有不同的過程。
一般而言,設備句柄由具體設備驅動程序的名稱決定。
Windows 設備驅動程序的文件名不同於普通文件,通常稱為設備路徑名。它具有固定格式,形如 \.DeviceName。在 C/C++ 編程中,這個字元串應當是 \\.\DeviceName。在代碼中表示為 \\\\.\\DeviceName。DeviceName 應當與相應設備驅動程序定義的設備名稱相同。
有些設備名稱由 Microsoft 定義,因此不能修改(如表 1 所示)。
設備 | 路徑名 |
---|---|
軟盤驅動器 | A: B: |
硬碟邏輯子區 | C: D: E: . . . |
物理驅動器 | PhysicalDrivex |
CD-ROM、DVD/ROM | CdRomx |
磁帶驅動器 | Tapex |
COM 埠 | COMx |
例如,我們在 C/C++ 編程中使用設備路徑名,比如 \\\\.\\PhysicalDrive1、\\\\.\\CdRom0 和 \\\\.\\Tape0。 關於這個列表未收錄的其他設備的詳細情況,請查看本文後面的 參考資料 小節。
因為 Linux 將設備描述為文件,所以可以在目錄 ./dev 中找到所有設備文件。這個目錄的設備驅動程序包括:
常見設備文件大多可以按照上述描述找到。有關其他設備文件名和設備的詳細信息,請使用命令 dmesg。
比較主系統調用
設備控制的主系統調用包括下列操作:打開、關閉、I/O 控制、讀/寫等。參見表 2 所示的 Windows/Linux 映射。
Windows | Linux |
---|---|
CreateFile | open |
CloseHandle | close |
DeviceIoControl | ioctl |
ReadFile | read |
WriteFile | write |
現在,我們深入探討三個最常用的函數:create、close 和 devioctl。
Windows 的設備打開和關閉
我們討論 Windows 函數 CreateFile 和 CloseHandle。函數 CreateFile 用於打開設備。該函數返回句柄,用以訪問清單 1 所示的對象。
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,以及將 dwFlagsAndAttributes 和 hTemplateFile 設置為 0 或 NULL。返回句柄將用於後續設備控制操作。
關閉設備使用函數 CloseHandle。將參數 hObject 設置為設備打開時返回的句柄:BOOL WINAPI CloseHandle (HANDLE hObject);。
Linux 的設備打開和關閉
在 Linux 中,我們討論的是函數 open 和 close。 如前所述,打開設備就像打開普通文件一樣。清單 2 顯示如何使用 open 獲取設備句柄。
int open (const char *pathname, int flags, mode_t mode); |
調用成功將返迴文件描述符,它是進程尚未打開的序號最小的文件描述符。如果調用失敗,將返回 -1。文件描述符用作設備句柄。
參數標誌必須包含 O_RDONLY、O_WRONLY 或 O_RDWR 的其中之一。其他標誌可選。參數模式在新文件創立時說明文件訪問權。
在 Linux 中,函數 close 關閉設備就像關閉文件一樣:int close(int fd);。
Windows 的 DeviceIoControl
設備控制(Windows 的 DeviceIoControl 和 Linux 的 ioctl)是最常用的設備控制函數,可以完成設備訪問、信息獲取、命令發送和數據交換等任務。清單 3 舉例說明了 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 可以使用 ioctl — int ioctl(int fildes, int request, /* arg */ ...); — 向指定設備發送控制信息。第一個參數 fildes 是函數 open() 返回的文件描述符,用於指稱具體設備。
與對應的系統調用 DeviceIOControl 不同,ioctl 的輸入參數列表並不固定。它取決於 ioctl 進行何種請求,以及請求參數有何說明,正如 Windows 函數 DeviceIOControl 的參數 dwIoControlCode 一樣。但是,遷移期間需要注意何時選擇正確的請求參數,因為 DeviceIOControl 的 dwIoControlCode 和 ioctl 的 request 具有不同的取值。而且 dwIoControlCode 與 request 之間沒有顯式映射列表。通常可以在相關頭文件中查找請求參數值的定義來選擇參數值。所有控制代碼的定義在 /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):
Windows | Linux |
---|---|
#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 包含打開和關閉設備的函數(CreateFile 和 CloseHandle)。相應地,在 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 顯示命令的詳細信息。
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 主硬碟驅動器。
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 所示:
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); |
DeviceIoControl 比 ioctl 需要更多的參數。設備句柄在兩個平台上都是第一個參數,它從 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 簡單。
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 版本和編譯環境,可能需要另做修改。
(責任編輯:A6)
[火星人 ] 從 Windows 向 Linux 遷移設備控制應用程序已經有1249次圍觀