Сунь Лин, программист-стажер, IBM Ян И, программист-стажер, IBM
Если вы разрабатываете приложения управления устройствами для различных платформ, вы уже знаете, что Windows и Linux используют различные методы для управления аппаратными устройствами и перенос приложений с одной платформы на другую может быть связан со значительными сложностями. В этой статье мы рассматриваем подходы к работе с устройствами в этих операционных системах, начиная с архитектуры и заканчивая выполнением системных вызовов, уделяя особое внимание особенностям каждой из платформ. Также мы приводим пример переноса приложения (на C/C++), подробно иллюстрируя все возникающие особенности.
Предположения:
В данной статье термин "Windows" относится к Windows 2000 и более поздним версиям, также должна быть установлена среда разработки Microsoft Visual C++ версии 6 или выше. Для Linux используется ядро версии 2.6, также должен быть установлен GNU GCC.
Сравнение архитектур управления аппаратными устройствами
Методы управления устройствами в Windows и Linux различны
Архитектура управления устройствами в Windows
В Windows связь между пользовательским приложением и драйверами устройств осуществляется при помощи подсистемы ввода/вывода, которая также предоставляет инфраструктуру для поддержки драйверов устройств. Драйверы устройств обеспечивают интерфейс ввода/вывода для конкретных аппаратных устройств (см. рисунок 1).
Рисунок 1. Архитектура управления устройствами в Windows
При управлении устройствами операции ввода/вывода осуществляются при помощи IRP (I/O Request Packet - Пакет запроса ввода/вывода). Менеджер ввода/вывода создает IRP и отправляет его на вершину стека. После этого драйверы устройств получают местоположение в стеке пакета IRP, содержащего параметры для данного запроса ввода/вывода. В соответствии с требованиями, указанными в IRP (такими как create
, read
, write
, devioctl
, cleanup
, или close
), каждый драйвер выполняет свое задание при помощи аппаратных интерфейсов.
Архитектура управления устройствами в Linux
В Linux архитектура управления устройствами немного другая, и основное отличие заключается в том, что обычные файлы, директории, устройства и сокеты являются файлами -в Linux все является файлом. Чтобы обратиться к устройству, ядро Linux отображает вызов операции с устройством на драйвер устройства при помощи файловой системы. В Linux не существует менеджера ввода/вывода: все запросы ввода/вывода в начале поступают в файловую систему (см. рисунок 2).
Сравнение имен файлов устройств и имен путей к устройству
С точки зрения разработчика, для управления устройством необходимым условием является получение дескриптора (handle) для этого устройства. Так как архитектуры управления устройствами в Windows и Linux различны, то и получение дескриптора устройства в этих системах осуществляется по-разному.
В общем случае дескриптор устройства определяется по имени соответствующего драйвера устройства.
В Windows имя драйвера устройства отличается от имени обычного файла, вместо этого используется так называемое device pathname - имя пути к устройству. Оно имеет фиксированный формат наподобие \.DeviceName
. При программировании на C/C++ строка символов будет иметь следующий вид: \\.\DeviceName
. В коде программы следует использовать строку \\\\.\\DeviceName
. DeviceName
должно соответствовать имени, определенному в программе драйвера устройства.
Некоторые из имен устройств определены Microsoft и не могут быть изменены (см. таблицу 1).
Таблица 1. Имена устройств в Windows (x = 0, 1, 2 и т.д.)
Устройство |
Имя пути к устройству (Pathname) |
Флоппи-диск |
A: B: |
Логическая область жесткого диска |
C: D: E: . . . |
Физический жесткий диск |
PhysicalDrivex |
CD-ROM, DVD/ROM |
CdRomx |
Накопитель на магнитной ленте |
Tapex |
Последовательный порт |
COMx |
Например, в программе на C/C++ для имен пути к устройству (pathname) используются следующие обозначения: \\\\.\\PhysicalDrive1
, \\\\.\\CdRom0
и \\\\.\\Tape0
.
Поскольку в Linux все устройства описываются как файлы, то всем имеюшимся устройствам соответствуют файлы, находящиеся в директории ./dev. К числу таких драйверов устройств относятся:
- жесткие диски с интерфейсом IDE (Integrated Drive Electronics), например, /dev/hda и /dev/hdb
- дисководы CD-ROM, некоторые из них относятся к категории IDE-устройств, другие являются дисководами CD-RW (CD read/write), которые эмулируются как устройства SCSI (Small Computer Systems Interface), например, /dev/scd0
- последовательные порты, например, /dev/ttyS0 для последовательного порта COM1, /dev/ttyS1 для последовательного порта COM2 и так далее
- устройства управления, например, /dev/input/mice (мышь) и другие
- принтеры, например, /dev/lp0
Наиболее распространенные файлы устройств можно найти с помощью приведенного выше описания. Чтобы получить имена других файлов устройств и подробную информацию по по этим устройствам, воспользуйтесь командой dmesg
.
Сравнение основных системных вызовов
Основные системные вызовы для управления устройствами включают следующие операции: open (открыть), close (закрыть), I/O control (управление вводом/выводом), read/write (чтение/запись) и др. Соответствие между этими операциями в Windows/Linux показано в Таблице 2.
Таблица 2. Соответствие между функциями управления устройствами
Windows |
Linux |
CreateFile |
open |
CloseHandle |
close |
DeviceIoControl |
ioctl |
ReadFile |
read |
WriteFile |
write |
Давайте более подробно рассмотрим три наиболее часто используемые функции: create
, close
и devioctl
.
Открытие и закрытие устройства в Windows
В Windows для открытия и закрытия устройства используются CreateFile
и CloseHandle
. Для открытия устройства используется функция CreateFile
. Эта функция возвращает дескриптор, который затем может использоваться для доступа к объекту (см. листинг 1).
Листинг 1. Функция CreateFile в Windows
HANDLE CreateFile (LPCTSTR lpFileName, //Имя файла устройства
(Device Pathname)
DWORD dwDesiredAccess, //Способ доступа к объекту (чтение, запись
или одновременно чтение и запись)
DWORD dwShareMode, //Способ совместного использования объекта
(чтение, запись, одновременно чтение и запись или совместное использование запрещено)
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
//Атрибут безопасности, который определяет,
может ли возвращенный дескриптор наследоваться дочерними процессами
DWORD dwCreationDisposition, //Действие, которое предпринимается в случае
существования такого файла или его отсутствия
DWORD dwFlagsAndAttributes, //Атрибуты и флаги, относящиеся к файлу
HANDLE hTemplateFile);
//Дескриптор файла, используемого в качестве шаблона
|
Параметр lpFileName
представляет собой имя пути к устройству (device path name), которое уже было описано ранее. В общем случае, для открытия устройства достаточно задать 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
для получения дескриптора устройства.
Листинг 2. Функция open в Linux
int open (const char *pathname,
int flags,
mode_t mode);
|
При успешном вызове этой функции в качестве дескриптора файла возвращается дескриптор файла с наименьшим существующим номером, который еще не открыт в этом процессе. В случае неудачи возвращается значение -1. Дескриптор файла используется в качестве дескриптора устройства.
Параметр flags должен включать одно из следующих значений: O_RDONLY
, O_WRONLY
, или O_RDWR
. Другие флаги могут использоваться в качестве опций. Аргумент mode определяет разрешение использования в том случае, когда создается новый файл.
Функция close
используется для закрытия устройства в Linux аналогично закрытию обычного файла: int close(int fd);
.
Функция DeviceIoControl в Windows
Функция управления устройством (DeviceIoControl
в Windows и ioctl
в Linux) является наиболее часто используемой функцией в задачах управления устройствами, с ее помощью выполняется обращение к устройствам, получение информации, отправка команд и обмен данными. Пример использования функции DeviceIoControl
приведен в Листинге 3:
Листинг 3. Использование функции DeviceIoControl в Windows
BOOL DeviceIoControl (HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped);
|
Этот системный вызов отправляет указанному устройству код управления и прочие необходимые данные. Соответствующий драйвер устройства будет затем работать в соответствии с кодом управления, переданным при помощи параметра dwIoControlCode
. Например, с помощью IOCTL_DISK_GET_DRIVE_GEOMETRY
можно получить параметры, характеризующие структуру жесткого диска (тип устройства, количество цилиндров, количество дорожек для каждого цилиндра, количество секторов для каждой дорожки и так далее). Определения всех кодов управления, заголовочные файлы и подробную информацию по этой теме можно найти на Web-сайте MSDN.
Будут ли использоваться буферы ввода/вывода и какова их структура и размер - все это зависит от самого устройства и от операции ioctl
, которая выполняется процессом. Также они определяются параметром dwIoControlCode
, который указывается в вызове.
Если указатель на операцию overlapped устанавливается равным NULL, то операция DeviceIoControl
будет выполняться блокирующим (синхронным) способом. В противном случае операция будет выполняться асинхронно.
Операция ioctl в Linux
В Linux для передачи управляющей информации определенному устройству используется вызов ioctl
- int ioctl(int fildes, int request, /* arg */ ...);
-. Первым параметром fildes
в этом вызове является дескриптор открытого файла, который был возвращен функцией open()
и который описывает данное устройство.
В отличие от соответствующего системного вызова DeviceIOControl
, в функции ioctl
список входных параметров не является фиксированным. Он зависит от типа запроса, который выполняется с помощью ioctl
, а также от того, что указано в параметре запроса - аналогом может являться параметр dwIoControlCode
в используемой в Windows функции DeviceIOControl
. Однако при переносе приложений необходимо уделить внимание выбору правильного параметра request, так как параметр dwIoControlCode
в функции DeviceIOControl
и параметр request
в функции ioctl
принимают различные значения и не существует какого-либо списка, который обеспечивает отображение между dwIoControlCode
/request
. Обычно значение для параметра request выбирается исходя из его определения, которое содержится в заголовочном файле. Все определения кодов управления содержатся в файлах /usr/include/{asm,linux}/*.h.
Параметр arg
используется для передачи подробной информации, относящейся к команде, которая необходима данному устройству для выполнения заданной операции. Тип данных для arg
зависит от выполняемого запроса на управление. Мы можем использовать данный аргумент как для отправки подробной информации, связанной с данной командой, так и для получения возвращаемых данных.
Пример переноса приложения
Давайте рассмотрим процесс переноса приложения с Windows на Linux. Данный пример занимается чтением журнала SMART с главного жесткого диска (IDE) персонального компьютера.
Шаг 1. Определение типа устройства
Как мы уже знаем, в Linux любое устройство рассматривается как файл. На первом этапе необходимо понять, какое имя файла в Linux соответствует данному устройству. Только используя соответствующее имя файла мы сможем получить дескриптор устройства, который необходим для дальнейшего управления устройством.
В нашем примере объектом управления является жесткий диск с интерфейсом IDE. В Linux такие устройства описываются как /dev/hda, /dev/hdb и т.д. В первоначальном приложении (для Windows) имя пути к устройству (device path name) для жесткого диска было следующим: \\\\.\\PhysicalDrive0. В Linux этому устройству соответствует имя /dev/hda.
Шаг 2. Изменяем заголовочные файлы
Необходимо заменить включаемые при помощи директивы #include
заголовочные файлы их аналогамии в Linux (см. Таблицу 3):
Таблица 3. Заголовочные файлы, включаемые при помощи #include
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, and fcntl.h.
devioctl.h
в Windows используется для функции DeviceIoControl
, мы заменяем его файлом sys/ioctl.h, чтобы получить возможность работать с функцией ioctl
.
В заголовочном файле ntddscsi.h
(он относится к DDK) определяется набор кодов управления, которые служат для целей управления устройством. Так как в нашем примере мы будем работать только с жестким диском (интерфейс IDE), то в программе для Linux нам необходимо добавить файл linux/hdreg.h.
В других случаях необходимо убедиться, что включены заголовочные файлы со всеми необходимыми кодами управления. Например, если вместо жесткого диска необходимо обратиться к CD-ROM, то вместо указанного выше файла следует включить файл linux/cdrom.h.
Шаг 3. Изменить функции и параметры
Теперь давайте более подробно рассмотрим код. В листинге 4 приводится подробная информация по командам.
Листинг 4. Подробная информация по командам
unsigned char cmdBuff[7];
cmdBuff[0] = SMART_READ_LOG; // используется для определения SMART-команд
cmdBuff[1] = 1; // регистр счетчика секторов IDE
cmdBuff[2] = 1; // регистр номера сектора IDE
cmdBuff[3] = SMART_CYL_LOW; // нижнее значение для номера цилиндра IDE
cmdBuff[4] = SMART_CYL_HI; // верхнее значение для номера цилиндра IDE
cmdBuff[5] = 0xA0 / (((Dev->Id-1) & 1) * 16); // регистр диска/головки IDE
cmdBuff[6] = SMART_CMD; // действительная команда IDE
|
Информация о командах взята из спецификации по командам 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. Исходный код, показывающий использование DeviceIoControl в Windows
typedef struct _Buffer{
UCHAR req[8]; // Подробная информация, не относящаяся к
коду управления
ULONG DataBufferSize; // размер буфера данных Data Buffer, здесь равен 512
UCHAR DataBuffer[512]; // буфер данных Data Buffer
} Buffer;
Buffer regBuffer;
memcpy(regBuffer.req, cmdBuff, 7); //req[7] зарезервирован для использования
//в будущем, должен быть равен 0.
regBuffer.DataBufferSize = 512;
unsigned int size = 512+12; // размер regBuffer
// 8 - для req, 4 - для DataBufferSize, 512 - для данных
DWORD bytesRet = 0; // количество возвращенных данных
int retval; // возвращенное значение
retval = DeviceIoControl(devHandle,
IOCTL_IDE_PASS_THROUGH, //код управления
regBuffer, // входной буфер, включает размер подробной команды,
regBuffer, // выходной буфер, используется входной буфер
size,
&bytesRet, NULL);
if (!retval)
cout<<"DeviceIoControl failed."<<endl;
else
memcpy(data, retBuffer.DataBuffer, 512);
|
DeviceIoControl
имеет больше параметров по , чем ioctl
. В обеих платформах первым параметром является дескриптор устройства, который возвращается функцией CreateFile
(в Windows)/open()
(в Linux). Однако коды управления в 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 нет необходимости использовать все восемь байт - можно удалить информацию, связанную с размером буфера данных. В этом примере используются только 4 байта, относящихся к команде.
Итак, в Linux соответствующий код (листинг 7) выглядит намного проще, так как по сравнению с Windows функции имеют более простую структуру и аргументы.
Листинг 7. Исходный код, показывающий использование функции ioctl в Linux
int retval;
unsigned char req[4+512]; // Размер буфера позволяет сохранять
// возвращенные данные плюс 4 байта
// для хранения подробной информации о команде
req[0]= cmdBuff[6]; // В соответствии с требованиями данного
// примера используются только 4 байта
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 и среды, в которой выполняется компилирование.