Что такое код ioctl


Содержание

Управляющий код IOCTL_SERIAL_LSRMST_INSERT

Управляющий код IOCTL_SERIAL_LSRMST_INSERT разрешает или запрещает помещение значений состояния линии и состояния модема в обычный поток данных, который прикладная программа получает через посредство функции ReadFile.

Когда для этих данных о состоянии линии и состоянии модема включается режим их помещения, значениям статуса в потоке данных предшествует символ начала управляющей последовательности. Определяемый пользователем символ начала управляющей последовательности устанавливается управляющим кодом IOCTL_SERIAL_LSRMST_INSERT. Значения состояния состоят из 1 — 3 байтов (BYTE). Смотри раздел Замечания, в котором следует подробное описание значений статуса.

Чтобы выполнять эту операцию, вызовите функцию DeviceIoControl с ниже перечисленными параметрами.

Параметры

Если BYTE — ненулевое значение, он является требуемым значением символа начала управляющей последовательности состоянием линии и модема. Операция переключит режим LSRMST_INSERT помещения данных о состоянии линии и модема во включенное состояние.

Если BYTE равняется нулю, операция переключит режим LSRMST_INSERT размещения данных о состоянии линии и модема в выключенное состояние.

Символ начала управляющей последовательности не может быть символом XON или XOFF .

nInBufferSize [in] Размер буфера вывода данных, в байтах. Для этой операции это значение должно быть 1. lpOutBuffer

Не используется с этой операцией; устанавливается в ПУСТО (NULL).

Не используется с этой операцией; устанавливается в нуль.

lpBytesReturned [out] Указатель на переменную, которая получает размер данных, сохраненных в буфере вывода данных, в байтах. Если параметр lpOverlapped — ПУСТО (NULL), то параметр lpBytesReturned не может быть ПУСТО (NULL). Даже когда операция не возвращает выводимые данные, а lpOutBuffer — ПУСТО (NULL), функция DeviceIoControl использует параметр lpBytesReturned. После такой операции, значение lpBytesReturned не имеют смысла.

Если lpOverlapped — не ПУСТО (NULL), то lpBytesReturned может быть ПУСТО (NULL). Если этот параметр — не ПУСТО (NULL) и операция возвращает данные, lpBytesReturned не имеет смысла до тех пор, пока не завершиться асинхронная операция. Чтобы извлечь число байтов возвращаемых данных, вызовите функцию GetOverlappedResult. Если hDevice связан с портом завершения ввода-вывода данных (I/O), Вы можете извлечь число возвращаемых данных при помощи вызова GetQueuedCompletionStatus.

lpOverlapped [in] Указатель на структуру OVERLAPPED.

Если hDevice открывался без определения флажка FILE_FLAG_OVERLAPPED , то параметр lpOverlapped игнорируется.

Если hDevice открывался с флажком FILE_FLAG_OVERLAPPED , то операция выполняется как асинхронная операция. В этой ситуации, параметр lpOverlapped должен указать на допустимую структуру OVERLAPPED, которая содержит дескриптор объекта события. В противном случае, функция завершается ошибкой непредсказуемыми способами.

Для асинхронных операций, DeviceIoControl возвращает значение немедленно, и объект события подает сигнал, когда операция завершилась. В противном случае, функция не возвращает значение до тех пор, пока операция не завершиться или не произойдет ошибка.

Если операция завершается успешно, DeviceIoControl возвращает ненулевое значение.

Если операция завершается ошибкой, DeviceIoControl возвращает нуль. Чтобы получить дополнительную информацию об ошибке, вызовите GetLastError.

Замечания

Обратите внимание! на то, что приложение, которое использует эту схему, должно проверить каждый символ в потоке данных, чтобы выявить присутствие данных состояния линии или состояния модема.

Нижеследующие значения следуют за символом начала управляющей последовательности в потоке данных, если был включен режим LSRMST_INSERT .

Значение Предназначение
SERIAL_LSRMST_ESCAPE Обозначает прием символа начала управляющей последовательности непосредственно в поток данных.
SERIAL_LSRMST_LSR_DATA Указывает, что изменение состояния линии произошло и данные были доступны в аппаратном буфере приемника. Следующий байт (BYTE) — это значение BYTE зарегистрированного состояния линии, которое представлено в приемном аппаратном буфере, когда обрабатывалось изменение состояния линии.
SERIAL_LSRMST_LSR_NODATA Указывает, что изменение состояния линии произошло, но данные не были доступны в приемном аппаратном буфере.
SERIAL_LSRMST_MST Указывает, что изменение состояния модема произошло. Следующий байт (BYTE) — это BYTE, который является значением зарегистрированного состояния модема, когда обрабатывалось изменение состояния модема.

Размещение и совместимость IOCTL_SERIAL_LSRMST_INSERT

Что такое код ioctl

Итак, теперь давайте попробуем разобраться с тем что же там в этом драйвере происходит и как он работает.

В самом начале с помощью оператора #include подключаем файл htddk.h, в котором находятся необходимые определения констант, типов переменных, прототипов функций и макросов, используемых в исходных текстах программ драйверов. Некоторая доля этих определений входит в другие заголовочные файлы, на которые имеются ссылки в файле NTDDK.H. Два следующих предложения программы служат для задания символических имен (в данном случае NT_DEVICE_NAME и WIN32_DEVICE_NAME) текстовым строкам с именами объекта устройства, который будет создан нашим драйвером. Вопрос об объекте устройства и его именах будет подробнее рассмотрен ниже.

#define IOCTL_READ CTL_CODE (FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

позволяют определить коды действий, выполняемых драйвером по запросам приложения. Обращение приложения к драйверу, независимо от цели этого обращения, осуществляется с помощью единой функции DeviceIoControl(). Для того чтобы приложение могло запросить у драйвера выполнение конкретного действия (из числа предусмотренных в драйвере), в качестве одного из параметров этой функции выступает код действия (в нашем случае IOCTL_READ или IOCTL_WRITE). Процедура драйвера, вызываемая функцией Windows DeviceIoControl(), должна проанализировать поступивший в драйвер код действия и передать управление на соответствующий фрагмент драйвера. Коды действия, называемые в документации DDK NT управляющими кодами ввода-вывода (I/O control codes), строятся по определенным правилам. Каждый код представляет собой слово длиной 32 бита, в отдельных полях которого размещаются компоненты кода.

Файловый флаг устанавливается в случаях, когда пользователь создает новые коды действия для файловых устройств. Все наши устройства нефайловые, и этот бит должен быть сброшен. В поле «Тип устройства» помещается предопределенная константа, характеризующая устройство (FILE_DEVICE_CD_ROM, FILE_DEVICE_MOUSE и др.). В нашем случае можно использовать константу FILE_DEVICE_UNKNOWN, равную 0x22. Поле доступа определяет запрашиваемые пользователем права доступа к устройству (чтение, запись, чтение и запись). Мы будем использовать константу FILE_ANY ACCESS, равную нулю. Функциональный код может принимать произвольное значение в диапазоне 0x800. OxFFF (значения 0x000. 0x7FF зарезервированы для кодов Microsoft). В рассматриваемом примере используем два кода действия. Для первого из них выбран функциональный код, равный 0x800, для следующего 0x801. Кодов действий может быть больше и им будут присваиваться функциональные коды 0x802, 0x803 и т. д. Тип передачи определяет способ связи приложения с драйвером. Для драйверов физических устройств, выполняющих пересылки незначительных объемов данных без использования канала прямого доступа, в качестве типа передачи обычно используется константа METHOD_BUFFERED, равная нулю. Такой выбор константы определенным образом задает местоположение системного буфера, через который пересылаются данные. В дальнейшем этот вопрос будет рассмотрен подробнее.

Код действия можно сформировать «вручную», как это мы сделаем в будущем в нашем пользовательском приложении:

#define IOCTL_ADDR (0х800

Легко сообразить, что в этом случае предполагаются константы FILE_DEVICE_UNKNOWN=0x22, METHOD_BUFFERED=0 и FILE_ANY_ACCESS=O при значении функционального кода 0x800. В программе драйвера для формирования кода действия использован макрос CTL_CODE, который определен в файле NTDDK.H. Этот макрос позволяет обойтись без детального знания формата кода действия и значений конкретных констант.

Вслед за определением кода действия в тексте драйвера приведены прототипы используемых в нем функций. Этим функциям можно дать произвольные имена, однако их, как говорят, сигнатура, т.е. состав параметров вместе с типом возвращаемого значения, жестко заданы системой. Ключевое слово IN, с которого начинается описание каждого параметра, говорит о том, что этот параметр является для функции входным, т.е. передается в функцию при ее вызове. В других случаях может использоваться ключевое слово OUT, а также и комбинация IN OUT, если через данный параметр осуществляется как передача данного в функцию, так и возврат результата ее работы. По правилам языка Си, функция может изменять значения передаваемых в нее через параметры данных, если параметром является не само данное, а его адрес (указатель).

Программная часть драйвера начинается с обязательной функции с именем DriverEntry(), которая автоматически вызывается системой на этапе загрузки драйвера и должна содержать все действия по его инициализации. В первых строках функции определяются используемые в ней данные — указатель на объект устройства типа PDEVICE_OBJECT и две символьные строки типа UNICODE_STRING с именами устройства. В качестве первого параметра функция получает указатель еще на один объект, именно на объект драйвера типа PDRIVER_OBJECT. О каких объектах идет речь и почему объект устройства имеет два имени? Windows NT является объектно-ориентированной системой. Каждый компонент системы представляет собой объект, включающий в себя необходимые для его функционирования структуры данных и наборы функций. Некоторые из этих функций служат для внутреннего использования данным объектом, другие же являются экспортируемыми, т.е. доступными другим объектам. Системные компоненты общаются друг с другом не напрямую, а исключительно с помощью экспортируемых объектами функций. Типы объектов Windows, т.е. состав входящих в них структур данных и функций, известны заранее, однако сами объекты (или, как говорят, экземпляры объектов) создаются динамически по мере возникновения в них необходимости. При загрузке драйвера система создает объект драйвера (driver object), олицетворяющий для системы образ драйвера в памяти. С другой стороны, объект драйвера представляет собой структуру, содержащую необходимые для функционирования драйвера данные и адреса (указатели) функций.

В процессе инициализации драйвера (процедуру инициализации пишет программист-разработчик драйвера) создаются один или несколько объектов устройств (device object), олицетворяющих те устройства, с которыми будет работать данный драйвер. Объекты устройств существуют все время, пока драйвер находится в памяти; если мы не предусматриваем специальных средств динамической выгрузки драйвера, то объекты устройств будут уничтожены лишь при завершении работы системы Windows. Объект устройства (по крайней мере один) необходим для правильного функционирования драйвера и создается даже в том случае, если, как это имеет место в нашем примере, драйвер не имеет отношения к каким-либо реальным физическим устройствам.

Системные программы взаимодействуют с объектом устройства, созданным драйвером, посредством указателя на него. Однако для прикладной программы объект устройства представляется одним из файловых объектов и обращение к нему осуществляется по имени. Вот это-то имя, в нашем случае MYDRIVER, и следует определить в программе драйвера. Дело усугубляется тем, что объект устройства должен иметь два имени, одно в пространстве имен NT, другое — в пространстве имен Win32. Оба эти имени должны, во-первых, быть определены с помощью кодировки Unicode, в которой под каждый символ выделяется не 1, а 2 байта, и, во-вторых, представлять собой не просто символьные строки, а специальные структуры типа UNICODE_STRING, в которые входят помимо самих строк еще и их длины («структуры со счетчиками»). Кодировка Unicode задается с помощью символа L, помещаемого перед символьной строкой в кавычках, а преобразование строк символов в структуры типа UNICODE_STRING осуществляется вызовами функции RtlInitUnicodeString(), которые можно найти далее по тексту программы драйвера.

Имена объектов устройств составляются по определенным правилам. NT-имя предваряется префиксом \Device\, a Win32-имя- префиксом \??\ (или \DosDevice\). При указании имен в Си-программе знак обратной косой черты удваивается. Для того чтобы указанное в программе драйвера имя можно было использовать в приложении для открытия устройства, следует создать символическую связь между обоими заданными именами устройства. Эта связь создается функцией IoCreateSymbolicLink(), которой в качестве параметров передаются оба имени. Следующая обязательная операция — создание объекта устройства — осуществляется вызовом функции IoCreateDevice(), принимающей ряд параметров. Первый параметр, указатель на объект драйвера, поступает в функцию DriverEntry() при ее вызове из Windows (см. заголовок функции DriverEntry). Второй параметр определяет размер так называемого расширения устройства — области, служащей для передачи данных между функциями драйвера. В рассматриваемом драйвере расширение устройства не используется и на месте этого параметра указан 0. В качестве третьего параметра указывается созданное нами ранее NT-имя устройства. Наконец, последний параметр этой функции является выходным — через него функция возвращает указатель (типа DEVICE_OBJECT) на созданный объект устройства.

Последнее, что надо сделать на этапе инициализации драйвера, — это занести в объект драйвера адреса основных функций, включенных программистом в текст драйвера. Под основными функциями мы будем понимать те фрагменты драйвера, которые вызываются системой автоматически в ответ на определенные действия, выполняемые приложением или устройством. В наших примерах таких действий будет три: получение дескриптора драйвера функцией CreateFile(), запрос к драйверу на выполнение требуемого действия функцией DeviceIoControl() и закрытие драйвера функцией CloseHandle(). В более сложных драйверах основных функций может быть больше (вплоть до приблизительно трех десятков). Для хранения адресов основных функций в объекте драйвера предусмотрен массив (с именем MajorFunction) указателей на функции типа PDRIVERDISPATCH. В файле NTDDK.H определены символические смещения элементов этого массива. Так, в первом элементе массива (смещение IRP_MJ_CREATE=O) должен размещаться указатель на функцию, которая вызывается автоматически при выполнении в приложении функции CreateFile(). В элементе со смещением IRP_MJ_CLOSE=2 размещается указатель на функцию, вызываемую при закрытии устройства (функцией CloseHandle()). Наконец, в элементе со смещением IRP_MJ_DEVICE_CONTROL=0x0E должен находиться адрес функции диспетчеризации, которой система передает управление в ответ на вызов в выполняемом приложении Windows функции DeviceIoControl() с указанием кода требуемого действия. Назначение функции диспетчеризации — анализ кодов действий, направляемых в драйвер приложением, и осуществление переходов на соответствующие фрагменты драйвера. В рассматриваемом примере три упомянутые функции имеют (произвольные) имена CtlCreate, CtlClose и CtlDispatch. Структура нашего драйвера с указанием его функций точек входа приведена на рис. ниже.

Массив MajorFunction является одним из элементов структурной переменной. Если бы эта структура была объявлена в программе с указанием ее имени (пусть это имя будет DriverObject), то для обращения к элементу структуры с индексом 0 следовало бы использовать конструкцию с символом точки:

Однако у нас имеется не имя структурной переменной, а ее адрес pDriverObject, полученный в качестве первого параметра при активизации функции DriverEntry. В этом случае для обращения к элементу структуры следует вместо точки использовать обозначение->:

Разумеется, вместо численного значения индекса массива надежнее воспользоваться символическим. Функция DriverEntry(), как, впрочем, и все остальные функции, входящие в состав драйвера, завершается оператором return с указанием кода успешного завершения STATUS_SUCCESS (равного нулю). Как видно из прототипов функций CtlCreate(), CtlClose() и CtlDispatch(), все они принимают (из системы Windows) в качестве первого параметра указатель на объект драйвера, а в качестве второго — указатель на структуру типа IRP. Эта структура, так называемый пакет запроса ввода-вывода (in/out request packet, IRP), играет чрезвычайно важную роль в функционировании драйвера наряду с уже упоминавшимися объектами драйвера и устройства. Рассмотрим более детально создание и взаимодействие всех этих структур.

Как уже упоминалось выше, объект драйвера, олицетворяющий собой образ выполнимой программы драйвера в памяти, создается при загрузке драйвера на этапе запуска системы Windows. В этом объекте еще не заполнен массив MajorFunction, а также DeviceObject — указатель на объект устройства, поскольку сам объект устройства пока еще не существует. Загрузив драйвер, Windows активизирует его функцию инициализации DriverEntry(). Эта функция должна содержать вызов IoCreateDevice(), создающий объект устройства. В объекте устройства есть ссылка на объект драйвера, которому это устройство принадлежит, и, кроме того, адрес так называемого расширения устройства (device extension), поля произвольного размера, служащего для обеспечения передачи данных между запросами ввода-вывода. В настоящем примере драйвера расширение устройства не используется (и соответственно, не создается). Функция IoCreateDevice(), создав объект устройства, заносит его адрес в объект драйвера. Таким образом, обе эти структуры оказываются взаимосвязаны. Рассмотренные выше объекты существуют независимо от запуска и функционирования прикладной программы, работающей с драйвером. Уничтожены они будут только при закрытии всей системы или при динамической выгрузке драйвера из памяти, если такая возможность в драйвере предусмотрена, а она у нас предусмотрена в функции UnloadOperation(), которой мы воспользуемся в следующих статьях. Пакет запроса ввода-вывода создается заново при каждом обращении приложения к драйверу, т. е. при каждом вызове функции DeviceIoControl(). В терминологии драйверов Windows NT этот вызов носит название запроса ввода-вывода (I/O request). Выполнение функции IoCompleteRequest(), которой завершается любая активизируемая из приложения функция драйвера, приводит к уничтожению этого пакета, который, таким образом, существует лишь в течение времени выполнения активизированной функции драйвера. Обычно приложение за время своей жизни обращается к драйверу неоднократно; следующий запрос ввода-вывода снова создаст пакет IRP, который, разумеется, ничего не будет знать о предыдущем. Для того чтобы можно было передать данные, полученные в одном запросе ввода-вывода (например, данные, прочитанные из устройства), в другой запрос (например, с целью записи их в устройство), эти данные следует сохранить в расширении устройства. Пакет ввода-вывода IRP состоит из двух частей: фиксированной части и так называемой стековой области ввода-вывода (I/O stack location). Ссылка на стековую область ввода-вывода содержится в переменной CurrentStack Location, входящей в фиксированную часть IRP. Адрес же фиксированной части передается в качестве второго параметра в любую основную функцию драйвера при ее активизации. С другой стороны, адрес стековой области ввода-вывода можно получить с помощью специально предусмотренной функции IoGetCurrentIrpStackLocation().

Итак, в нашем драйвере имеются три основные функции: CtlCreate(), CtlClose() и CtlDispatch() и одна дополнительная UnloadOperation(), предназначенная для динамическом выгрузки драйвера из памяти. Это минимальный набор основных функций, при котором драйвер будет правильно функционировать. Активизация каждой из этих функций приводит к выделению системой (конкретно — диспетчером ввода-вывода I/O Manager) пакета запроса ввода-вывода со стековой областью, а завершение функции — к его возврату в систему. Для освобождения пакета запроса ввода-вывода необходимо заполнить в нем структуру блока состояния IO_STATUS_BLOCK и сообщить диспетчеру ввода-вывода вызовом функции IoCompleteRequest() о том, что мы завершили обработку этого пакета. В блок состояния входят две переменных: Status — для кода завершения и Information — для возврата в приложение некоторой числовой информации. В переменную Status естественно поместить код STATUS_SUCCESS, а переменная Information должна содержать число пересылаемых в приложение байтов. Функции CtlCreate() и CtlClose() ничего не пересылают в приложение, и значение этой переменной приравнивается нулю. Функция IoCompleteRequestQ требует указания двух параметров: указателя на текущий пакет запроса ввода-вывода и величины, на которую следует повысить приоритет вызывающей драйвер программы. В нашем случае запросы ввода-вывода обрабатываются очень быстро, за это время приоритет вызывающей программы снизиться не успевает, и нет необходимости его повышать. Поэтому в качестве второго параметра передается константа IO_NO_INCREMENT. Функции CtlCreate() и CtlClose() в нашем примере не выполняют никакой содержательный работы, и их тексты в результате оказались полностью совпадающими.

Перейдем к рассмотрению содержательной (с точки зрения прикладного программиста) части драйвера — функции диспетчеризации CtlDispatch(). Cистемный буфер, служащий для обмена информацией между драйвером и приложением, расположен в пакете запроса ввода-вывода IRP (переменная SystemBuffer). Таким образом, для организации взаимодействия пользовательского приложения и драйвера необходимо получить доступ к IRP, а через IRP — к SystemBuffer. С этой целью в функции CtlDispatch() объявляется переменная plrpStack типа указателя на стековую область ввода-вывода PIO_STACK_ LOCATION и, кроме того, переменная pIOBuffer, в которую будет помещен адрес системного буфера обмена. В структуре пакета запроса ввода-вывода этот адрес имеет тип PVOID — указатель на переменную произвольного типа. Действительно, тип передаваемых в приложение (или из приложения) данных может быть каким угодно: он определяется конкретными задачами данного запроса ввода-вывода. В нашем примере мы передаем через буфер обмена адреса портов, данные предназначенные для записи в порт и данные прочитанные из порта, поэтому для переменной pIOBuffer выбрали тип PUSHORT — указатель на short без знака. С помощью функции IoGetCurrentStackLocation() в переменную plrpStack помещается адрес стековой области ввода-вывода, а затем в переменную pIOBuffer заносится адрес системного буфера из структуры IRP. Системный буфер входит в объединение (union) с именем Associatedlrp, поэтому для доступа к переменной SystemBuffer использована конструкция pIrp->AssociatedIrp.SystemBuffer. Объединение можно рассматривать как эквивалент структурной переменной с тем отличием, что все члены объединения размещаются (альтернативно) в одной и той же области памяти. В синтаксическом плане обращения к объединению и к структуре выполняются одинаково. Конструкция switch-case анализирует содержимое ячейки IoControlCode, входящей в стековую область IRP и в зависимости от значения кода действия, содержащегося в этой ячейке, передает управление на тот или иной фрагмент программы драйвера. В рассматриваемом примере предусмотрены два кода действия:

  • IOCTL_READ — это действие возникает, когда пользовательское приложение хочет прочитать данные из указанного им порта
  • IOCTL_WRITE — это действие возникает, когда пользовательское приложение хочет записать данные в указанный им порт

Например, при поступлении в драйвер кода действия IOCTL_READ из системного буфера обмена в переменную драйвера Port читается адрес порта из которого надо прочесть данные. Этот адрес задает пользовательское приложение и передает его в драйвер. Далее в туже ячейку системного буфера, где раньше лежал адрес порта, записывается результат работы функции ядра Windows READ_PORT_UCHAR(), предназначенной для чтения байта данных из указанного порта. В переменную Irp->IoStatus.Information записывается число пересылаемых в пользовательское приложение байтов. Хоть мы и прочитали всего один байт, но т.к. pIOBuffer у нас определен как указатель на USHORT (т.е. там лежат указатели на USHORT, размер которых 2 байта) то и возвращаем мы в приложение два байта.

Последовательность действий при IOCTL_WRITE будет заключается в следующем: читаем первую ячейку буфера. Там лежит адрес порта, куда надо писать данные (что и как лежит в этом буфере определяем мы в пользовательском приложении. Когда доберемся до туда, станет понятнее). Читаем следующую ячейку и берем от туда байт данных, который надо записать в порт. Потом вызываем системную функцию WRITE_PORT_USHORT(), которая запишет данные по указанному адресу в порт. Т.к. в приложение мы никаких данных при этом не пересылаем, то Irp->IoStatus.Information присваиваем 0.

Ну что, голова распухла? Я вас отлично понимаю. Прежде чем я все это осознал, лично мне потребовалось несколько недель вдумчивого чтения. Поэтому, давайте займемся практикой и откомпилируем наш драйвер. Поэтому, если у Вас остались силы для дальнейшей борьбы с драверами перебираемся к следующей статье.

Большая часть материалов по описанию функционирования дравера для этой статьи были взяты из книги П.И. Рудакова и К.Г. Финогенова «Язык ассемблера: уроки программирования».

Использование функции DeviceIoControl с кодом IOCTL_SCSI_PASS_THROUGH

29.07.2020, 14:35

не работают системные функции (DeviceIoControl) в Windows 7
Здравствуйте, я только учусь, так что просьба не ругаться. Стоит рэйд контроллер и система Сервер.

Использование одного файла с кодом в проектах на C# и C
Может кто подскажет как решить задачу. Имею устройство сделанное на arduino. Управляется оно с.

API DeviceIoControl
В оригинальном коде с примером (рабочий код): WINBASEAPI BOOL WINAPI DeviceIoControl( .

Функция DeviceIoControl
Мне нужно полное описание функции DeviceIoControl или подскажите книгу, где можно найти ее описание

Некомпелируеца программа на DeviceIoControl
Здраствуйте, помогите пожалуйсто, нашел в нете код, а он некомпелируеца, вот код: #include.

сети для гиков

12 нояб. 2012 г.

Ioctl: доступ к параметрам сетевого интерфейса

Озаботился как-то проблемой программного доступа к параметрам сетевых интерфейсов в Linux (в разрезе беспроводных сетей). И «нарыл» кое-каких интересных способов.

Итак, первый самый археологически ископаемый вариант — ioctl.

Гугл вытянул такую вот ссылку, где все про этот объяснено. Но я, конечно, не удержался и «сваял» свой вариант утилиты, печатающей информацию о сетевых интерфейсах системы.

Как гласит man, функция ioctl, объявленная в sys/ioctl.h, предназначена для манипулирования параметрами устройств, представленных в виде специальных файлов. Сетевые интерфейсы в Linux файлами быть давно уже перестали, но эта фишка для них продолжает работать через дескрипторы сокетов.

ОТ ТЕОРИИ.

У ioctl три параметра: дескриптор сокета, номер запроса и указатель на структуру, из которой извлекается имя интерфейса и записывается параметр, который нужно установить или получить.

Эта структура, используемая для запросов к сокетам, называется ifreq и определена в net/if.h:

Получить или установить за раз можно только один параметр, так как у ifreq два поля: имя интерфейса (ifr_name) и одно из перечисленных в определении полей (такой вот этот тип union).

Номера запросов определены толи в linux/sockios.h толи в i386-linux-gnu/bits/ioctls.h:

Алгоритм использования ioctl прост: предварительно открываем сокет, каждый раз очищаем структуру, потом записываем в нее имя интерфейса (а если хотим установить поле — заполняем его), выбираем сигнал из списка (в средине G — получить, S — установить), вызываем функцию, результат извлекаем по имени соответствующего поля.

Еще для получения списка интерфейсов нам потребуется функция if_nameindex(), определенная в net/if.h. Результат ее работы записывается в массив элементов одноименного типа. Каждый элемент содержит номер и имя интерфейса:

. К ПРАКТИКЕ!

Все, можно быдлокодить. Сначала заводим глобальные переменные (чтобы не таскать их из функции в функцию).

Структура — основной параметр:

Указатель на массив номеров и интерфейсов (последующие функции будут работать с текущим элементом):

Функция для инициализации sck и ifidx. Открывает сокет и генерирует список интерфейсов. В случае успеха возвращает 1, если не повезло — 0:

Ошибки функции socket (да и ioctl при желании) обрабатываются вполне стандартно:

Самодельный вызов ioctl. У него только один параметр — номер сигнала. Очищает содержимое структуры ifdat, копирует в ее поле ifr_name имя текущего интерфейса (поле if_name структуры if_nameindex):

В случае неуспеха ioctl возвращает -1. Этим и воспользуемся для дальнейших проверок.

Получение строкового значения параметра в формате адресов IPv4. Параметр — номер сигнала (SIOCGIFADDR — адрес, SIOCGIFNETMASK — маска, SIOCGIFBRDADDR — широковещательный адрес):

Соответственно, результат содержится в полях ifr_addr, ifr_netmask и ifr_broadaddr, имеющих формат sockaddr_in (linux/in.h), преобразуемый к удобночитаемому виду функцией inet_ntoa (arpa/inet.h).

Чтобы получить MAC-адрес, нужно использовать сигнал SIOCGIFHWADDR. Культурного способа преобразовать MAC-адрес в строку я не нашел, поэтому воспользовался грязным «хаком»:

Для MTU есть сигнал SIOCGIFMTU и возращаемое значение имеет тип int:

Флаги подключения тоже представлены целым числом (SIOCGIFFLAGS). Как положено, отдельные биты имеют имена (net/if.h). Их значения проверяются заковыристой логикой:

После всего этого кода main выглядит элементарно:

Если инициализация прошла, то с помощью while обрабатываются все элементы ifidx, и сокет закрывается.

Чтобы получилась работающая программка нужно все это собрать в один файл, добавить в начало необходимые includы:

IOCTL

Универсальный англо-русский словарь . Академик.ру . 2011 .

Смотреть что такое «IOCTL» в других словарях:

Ioctl — es una llamada de sistema en Unix que permite a una aplicación controlar o comunicarse con un driver de dispositivo, fuera de los usuales read/write de datos. Esta llamada se originó en la versión 7 del AT T Unix. Su nombre abrevia la frase… … Wikipedia Español

Ioctl — In computing, an ioctl (pronEng|aɪˈɒktəl or i o control ) is part of the user to kernel interface of a conventional operating system. Short for Input/output control , ioctls are typically employed to allow userspace code to communicate with… … Wikipedia

ioctl — ██████████5 % … Wikipédia en Français

IOCTL — I/O Control (Computing » Drivers) … Abbreviations dictionary

IOCTL — Input/Output Control … Acronyms

ioctl — ● ►en n. m. ►SYSTM I/O control. Contrôle des entrées/sorties. En général c est une fonction (ou plusieurs) du noyau qui s occupe de ça … Dictionnaire d’informatique francophone

IOCTL — Input/Output Control … Acronyms von A bis Z

IOCTL — abbr. I/O ConTroL (I/O) comp. abbr. Input/Output Control … United dictionary of abbreviations and acronyms

POSIX terminal interface — The POSIX terminal interface is the generalized abstraction, comprising both an Application Programming Interface for programs, and a set of behavioural expectations for users of a terminal, as defined by the POSIX standard and the Single Unix… … Wikipedia

Netlink — For the modem, see Sega NetLink. Netlink is a socket like mechanism for IPC between the kernel and user space processes, as well as between user space processes alone (like e.g., unix sockets) or a mixture of multiple user space and kernel space… … Wikipedia

TTY-абстракция — Стиль этой статьи неэнциклопедичен или нарушает нормы русского языка. Статью следует исправить согласно стилистическим правилам Википедии. У этого термина существуют и другие значения, см. Tty. Подсистема TTY, или TTY абстракция это одна из … Википедия

Глава 5. Дополнительные операции в драйвере символьного устройства.

В главе 3, «Драйверы символьных устройств», мы создали самодостаточный драйвер символьного устройства, с которым пользователь может производить операции чтения/записи. Но реальное устройство предлагает большую функциональность, чем синхронное чтение и запись. Теперь, когда мы овладели техникой отладки мы можем благополучно двигаться вперед и реализовывать новые, более сложные операции.

Обычно, в дополнении к операциям чтения и записи, существует необходимость выполнения различных операций по управлению оборудованием через драйвер устройства. Операции управления, обычно поддерживаются через метод ioctl(). Альтернативный способ управления заключается в передачи специальной управляющей последовательности в цикле записи в устройство. Тогда, через анализ этой последовательности можно будет выполнить действия аналогичные прямому вызову ioctl(). По возможности, следует избегать этой устаревшей техники управления устройством через передачу команд в цикле записи, т.к. это требует резервирования нескольких байт данных в потоке записи для управляющих команд. Кроме того, реализация управления устройством через цикл записи более сложно в реализации, чем управление через механизм ioctl(). Несмотря на неудобства, управление через цикл записи используется, например, для tty и других устройств. Мы опишем это более подробно в разделе «Управление устройствами без ioctl()».

Как мы упоминали в предыдущей главе, системный вызов ioctl() предлагает устройство-зависимые точки входа в драйвер для передачи команд управления. Коды передаваемые через механизм ioctl() устройство-зависимы, т.е. уникальны для каждого типа управляемого оборудования. Операции управления обычно недоступны через файловую абстракцию чтения/записи. Например, все, что вы пишете в последовательный порт воспринимается как коммуникационные данные, и вы не можете изменить скорость передачи данных записью кода в устройство. Таким образом, механизм ioctl() специально предназначен для управления каналами ввода/вывода.

Другая важная особенность реальных устройств (не таких как scull) заключается в том, что при обмене данными между устройствами требуется тот или иной механизм синхронизации. Концепции блочного ввода/вывода и асинхронного обмена поясняются в этой главе на примере модификации scull устройства. Драйвер использует взаимодействие между различными процессами для создания асинхронных событий. Как и в случае оригинального scull, нам не потребуется специального оборудования для тестирования драйвера. Мы не будем работать с реальным устройством до главы 8 «Управление оборудованием».

Системный вызов ioctl()

Вызов функции ioctl() в пользовательском процессе определяется следующим прототипом:

Благодаря неопределенному списку параметров, данный прототип выгодно выделяется в списке системных вызовов Unix, которые, обычно, представлены фиксированным числом формальных параметров. Однако, в реальной системе, системный вызов не может иметь переменное число параметров. Системные вызовы должны иметь строго определенное количество аргументов, так как пользовательские программы могут получить доступ к ним только через аппаратные «ворота» (hardware gates), как подчеркнуто в разделе «User Space and Kernel Space» главы 2 «Создание и запуск модулей». Поэтому, точки в прототипе, означают не переменное число параметров, а просто один необязательный (опциональный) аргумент, обычно определяемый как char *argp. Точки в прототипе используются только для предотвращения проверки типов в процессе компиляции. Действительный тип третьего аргумента зависит от передаваемой в процедуру команды, которая определяется вторым аргументом. Некоторые команды не имеют аргументов, некоторые используют целый аргумент, а некоторые берут в качестве аргумента указатель на структуру данных. Использование указателя позволяет передавать в системный вызов ioctl() произвольное количество требуемых данных. Таким образом, это одна из возможностей обмена произвольного объема данных между пользовательским процессом и процессом, работающем в ядре.


С другой стороны, функция ioctl() драйвера получает аргументы согласно следующему объявлению:

Указатели inode и filp представляют собой значения соответствующие файловому дескриптору fd, переданному пользовательским процессом, и полностью совпадают с параметрами, передаваемыми в системный вызов open(). Аргумент cmd передается от пользователя неизменным, а необязательный аргумент arg передается в форме unsigned long, что может соответствовать как целому значению, так и указателю. Если вызывающая программа не передает третий аргумент, то значение arg, полученное драйвером имеет неопределенное значение.

Так как проверка типов передаваемых аргументов запрещена, компилятор не сможет предупредить вас о возможной ошибке, и программист не узнает о ней до момента исполнения ошибочного кода. Этот недостаток есть небольшая плата за ту функциональность, которую обеспечивает системный вызов ioctl().

Как вы можете предположить, реализация метода ioctl(), в большинстве случаев строится на основе оператора switch, который обеспечивает ветвление, согласно значению аргумента cmd. Различные команды имеют различные числовые значения, которым, обычно, дают символические имена для улучшения читабельности кода. Символические имена задаются через директиву define препроцессора. Обычно, такие имена задаются в заголовочных файлах драйвера. Так для драйвера scull они описаны в файле scull.h. Если пользовательская программа, хочет использовать те же символические имена, то достаточно подключить к ней соответствующий заголовочный файл.

Выбор команд ioctl()

Перед реализацией функции ioctl() для нашего драйвера, мы должны определиться в кодах, которые будут соответствовать используемым командам. К сожалению, простой выбор номеров, начинающихся с единицы не всегда работает хорошо.

Нумерация команд, должна быть, по возможности, уникальна в пределах операционной системы, для того, чтобы предотвратить ошибки, которые могут возникнуть при ошибочном обращении к другому устройству. Такие ошибки не настолько невероятны, как может показаться на первый взгляд. Если нумерация кодов команд, передаваемых в ioctl() будет уникальна, то при обращении к «чужому» устройству вызывающая программа просто получит код ошибки EINVAL, что, конечно же, предпочтительнее, нежели непредсказуемая настройка ошибочно адресованного устройства.

В помощь программистам, создающим уникальные коды команд для ioctl() было предложено соглашение, согласно которому коды команд были разделены на несколько битовых полей. Первые версии Linux использовали 16-битовую нумерацию кодов. Старшие восемь бит являлись «магическими» числами связанными с устройством, а младшие восемь бит представляли коды команд для данного устройства. Так случилось, согласно собственным словам Линуса Торвальдса, по причине его невежественности («clueless»). Лучшее решение для использования битовых полей было найдено немного позднее. К несчастью, в некоторых драйверах еще используется старое соглашение. В наших программах мы будем использовать исключительно новое соглашение о нумерации кодов команд.

Для выбора кодов команд для передачи в ioctl() вашего драйвера, согласно новому соглашению, вы должны сначала просмотреть файлы include/asm/ioctl.h и Documentation/ioctl-number.txt. В заголовочном файле определяется использование битовых полей: тип («магический номер»), порядковый номер команды, направление передачи и размер аргумента. Файл ioctl-number.txt содержит список «магических номеров» используемых в ядре. Вам необходимо выбрать номер отличающийся от зарезервированных в файле, для избежания перекрытия номеров. Также, в этом текстовом файле содержится список причин, по которым, данное соглашение о нумерации кодов команд должно быть использовано.

Согласно старому, не рекомендуемому сейчас, соглашению выбор нумерации кодов для ioctl() был проще: автор выбирал «магический» восьмибитовый код для драйвера, например «k» (шестнадцатеричное значение – 0x6b) и добавлял порядковый номер команды.

Конечно же, и приложение, и драйвер должны использовать одно и тоже соглашение о нумерации кодов команд. Выбрав способ нумерации остается только организовать оператор switch в драйвере. Однако, способ нумерации, предложенный в этом примере, хоть и имеет свои основания в традициях Unix, не должен более использоваться. Приведенный пример – не более чем демонстрация того, как выглядит определение кодов команд в заголовочном файле драйвера.

Новый способ определения номеров команд использует четыре битовых поля, которые имеют описанное ниже значения. Все новые, используемые здесь, макросимволы определены в
.

type Магическое число. Выбор номера необходимо осуществлять после ознакомления с файлом ioctl-number.txt. Именно этот выбранный номер, необходимо, в дальнейшем, использовать везде, где он потребуется для вашего драйвера. Данное поле имеет емкость в восемь бит (_IOC_TYPEBITS). number Порядковый номер. Емкость – восемь бит (_IOC_NRBITS). direction Направление передачи данных, если данная команда вызывает пересылку данных. Возможные значения: _IOC_NONE (нет передачи данных), _IOC_READ, _IOC_WRITE, и _IOC_READ | _IOC_WRITE (данные передаются в обоих направлениях). Точкой наблюдения передачи данных является приложение. Таким образом _IOC_READ означает чтение данных из устройства, так, что драйвер должен писать в пользовательский процесс. Обратите внимание, что это поле представляет собой битовую маску, поэтому и _IOC_READ и _IOC_WRITE могут быть извлечены используя логическую операцию AND. size Размер передаваемых данных. Емкость этого поля архитектурно-зависима и, сейчас, лежит в диапазоне от 8 до 14 бит. Вы можете определить его значение для вашей архитектуры из макро _IOC_SIZEBITS. Если вы создаете портируемый драйвер, то значение этого поля не должно превышать 255. Использование данного поля не является обязательным. Если вы передаете больший размер данных, то вы можете просто игнорировать его. Скоро, мы увидим пример использования этого поля.

Ниже приведен пример некоторых ioctl-команд, определенных в драйвере scull. В частности команд, которые устанавливают и получают параметры конфигурирования драйвера.

Последняя команда, HARDRESET, используется для сброса счетчика использования модуля в 0, так, что модуль может быть выгружен при возникновении каких-нибудь проблем со счетчиком. В действительном коде драйвера, также определены команды между IOCHQSET и HARDRESET, которые не показаны здесь.

Мы выбрали для реализации оба способа передачи параметров – по указателю и явному значению, хотя по установившемуся соглашению ioctl() должен обмениваться значениями по указателю. Также, не имеет значения, какой способ будет использоваться для возврата целого значения – через указатель или через механизм возврата функции (return). Но последнее будет работать только для возврата положительных целых значений, т.к. отрицательное значение воспринимается ядром как ошибка и используется для передачи errno в пространство пользователя.

Операции «exchange» (обмен) и «shift» (смена) не являются необходимыми для нашего драйвера scull. Мы реализовали «exchange» для того, чтобы показать, как драйвер может соединять различную функциональность в одной атомарной операции. «shift» мы реализовали как пару «tell» (сказать) и «query» (запросить). Бывают случаи, когда атомарные операции типа test-and-set, наподобии этой, необходимы, в особенности, когда приложение должно установить или снять блокировки.

Фрагмент кода называется атомарным, когда он выполняется как одна инструкция, без каких-бы то ни было прерываний.

Порядковый номер команд не имеет какого-то специфического значения. Он используется только для различения команд. Вообще, вы можете даже использовать одинаковые номера команд для операций чтения и записи, так как, реальный вычисляемый ioctl-номер таких команд будет различаться битами направления передачи («direction» bits).

Значение параметра cmd в ioctl-вызове не анализируется ядром, и врядли такой анализ будет производиться в будущем. Поэтому, в принципе, вы можете не пользоваться таким сложным способом определения номеров, и просто явно определить набор скалярных номеров (скалярный, т.е. не состоящих из частей – битовых полей). С другой стороны, если вы выберите такой способ определения номеров команд, вы не получите выгоды от использования битовых полей. Заголовочный файл
представляет собой пример такого назначения номеров в старом стиле. Во время написания этого файла другой технологии определения номеров в Linux просто не было. Теперь, приведение этого файла к современному виду приведет к серьезным проблемам несовместимости.

Возвращаемое значение

В реализации ioctl() для анализа номеров команд обычно используется оператор switch. Возникает вопрос о реализации блока default при передаче в системный вызов некорректного кода команды. Данный вопрос является спорным. Некоторые функции ядра возвращают, в этом случае, вполне логичное значение -EINVAL («Invalid argument»), потому что переданный аргумент действительно некорректен. Однако, согласно стандарту POSIX, в этом случае, необходимо возвращать значение -ENOTTY. С даннной ошибкой должно быть связано текстовое сообщение «Not a typewriter». Данное правило распространялось на все ранние библиотеки до libc5 включительно. Только начиная с libc6 сообщение, связанное с этой ошибкой, было изменено на более корректное – «Inappropriate ioctl for device». Большинство современных Linux систем построены на основе libc6, и мы призываем вас придерживаться стандарта и использовать в качестве кода ошибки значение -EINVAL.

Предопределенные команды

Некоторые из команд системного вызова ioctl распознаются ядром, что означает декодирование номеров команд до вызова ioctl-функции вашего драйвера. Поэтому, в случае если вы выбрали такие номера для своих ioctl-команд, вызов вашей ioctl-функции драйвера не произойдет, и приложение получит неожидаемый ответ на системный запрос.

Предопределенные команды подразделяются на три группы:

  • Команды, которые могут быть использованы для любых типов файлов (обычных, файлов интерфейсов устройств, FIFO, или сокетов)
  • Команды, которые могут быть использованы только для обычных файлов
  • Команды, связанные с определенным типом файловых систем

Команды из последней группы реализуются драйверами файловых систем (см. команду chattr). Разработчикам драйверов наиболее интересны команды из первой группы, которые обозначаются магической литерой «T». Знакомство с другими предопределенными командами предоставляется интересующимся читателям в качестве упражнения (команда ext2_ioctl, реализующая append-only и immutable флаги, наверное, наиболее интересна).

Следующие ioctl команды предопределены для любых типов файлов:

FIOCLEX Установка флага close-on-exec (File Ioctl CLose on EXec). Установка этого флага приведет к закрытию файлового дескриптора, если вызывающий процесс выполняет новую программу. FIONCLEX Сброс флага close-on-exec. FIOASYNC Установка или сброс асинхронных уведомлений для файла (как описано в разделе «Асинхронное уведомление» позднее в этой главе). Заметьте, что ядра, до версии 2.2.4 некорректно использовали эту команду для модификации флага O_SYNC. И установка и сброс асинхронных уведомлений могут быть выполнены другими путями, поэтому, в действительности, никто не использует команду FIOASYNC. Объявление этой команды служит более целям полноты описания. FIONBIO «File IOctl Non-Blocking I/O» (описано позднее в этой главе, в разделе «Blocking and Nonblocking Operations»). Этот вызов изменяет значение флага O_NONBLOCK в поле filp->f_flags. Третий аргумент в этом системном вызове используется для передачи информации об установке или сбросе данного флага. Смысл этого флага будет объяснен в этой главе позднее. Заметьте, что данный флаг может быть также изменен системным вызовом fcntl(), используя команду F_SETFL в качестве его параметра.

Последний элемент в этом списке знакомит нас с новым системным вызовом fcntl(), который выглядит схожим с ioctl(). Схожесть заключается в том, что оба системных вызова получают, среди прочего, аргумент команду, и дополнительный (необязательный) аргумент. Различия этих вызовов имеют, главным образом, исторические причины. Когда разработчики Unix обдумывали проблему управления операциями ввода/вывода, то они решили, что файлы и устройства будут различаться. В то время, только устройства ttys управлялись через вызовы ioctl(), что и объясняет тот факт, почему код ошибки -ENOTTY был определен для некорректной ioctl() команды. С тех пор многое изменилось, но системный вызов fcntl() остался во имя обратной совместимости.

Использование аргумента команды в ioctl()

Перед тем, как мы рассмотрим реализацию кода вызова ioctl() для драйвера scull, необходимо обсудить использование дополнительного аргумента для команды, переданной в ioctl(). Если это целое число, то все просто – оно используется по прямому назначению. Однако, если это указатель, то мы должны учитывать различия в адресных пространствах ядра и пользовательских процессов.

Когда указатель используется для ссылки в пространство пользователя, то мы должны быть уверены в его корректности, и в том, что требуемая страница виртуальной памяти, в данный момент, является отображенной в пространство физической памяти. Если код пытается обратиться за пределы доступного адресного пространства, то процессор вырабатывает исключение. Исключения в коде ядра приводят к сообщению oops для всех ядер вплоть до версии 2.0.x. Версии 2.1 и более поздние, обрабатывают это исключение немного более изящно. Но, в любом случае, ответственность за использование некорректных указателей из адресного пространства пользователя должно лежать на коде ядра, который должен корректно обрабатывать эту ситуацию и возвращать ошибку, не доводя дело до исключительных ситуаций.

Илон Маск рекомендует:  Как сделать, чтобы фотография располагалась по правому краю окна браузера

Проверка корректности адреса в ядрах до версии 2.2.x реализуется через вызов функции access_ok(), которая описана в заголовочном файле :

Первый аргумент должен иметь значение либо VERIFY_READ, либо VERIFY_WRITE, в зависимости от того, собираемся мы читать или писать в память пользовательского адресного пространства. Аргумент addr указывает на адрес адресного пространства пользователя, а аргумент size – количество байт тестируемого диапазона адресов, начиная с адреса addr. Например, если в реализации ioctl() необходимо прочитать целое значение из адресного пространства пользователя, то значение size будет равно sizeof(int). Если вам требуется и чтение и запись по заданному диапазону адресов, то в качестве первого аргумента необходимо задавать VERIFY_WRITE, т.к. это значение является надмножеством для VERIFY_READ.

В отличии от большинства функций, access_ok() возвращает булево значение: 1 – в случае успешной проверки, и 0 – в случае неудачи. Если проверка адреса не удалась, то драйвер обычно возвращает код ошибки -EFAULT.

Имеется пара интересных вещей, которые необходимо знать о работе функции access_ok(). Прежде всего, функция не выполняет полной проверки для заданного диапазона адресов – определяется только принадлежность заданного диапазона тому множеству адресов, которые доступны для данного процесса. Таким образом, функция access_ok() может гарантировать, что заданный диапазон адресов не принадлежит пространству ядра. Кроме того, необходимо помнить, что в большинстве случаев вы не столкнетесь с необходимостью прямого вызова access_ok(), т.к. процедуры доступа к памяти, которые будут описаны позднее, выполняют эту проверку за вас. Тем не менее, мы продемонстрируем использование этой функции как в чисто учебных целях, так и по причинам обратной совместимости, о которых мы расскажем в конце этой главы.

Модуль scull использует битовые поля в номере команды, передаваемой в ioctl(), для проверки аргументов перед диспетчерезацией в операторе switch:

После вызова access_ok() драйвер может безопасно выполнять передачу данных из одного адресного пространства в другое (ядро и пользовательский процесс). В добавлении к функциям copy_from_user() и copy_to_user(), выполняющим такую передачу данных, программист может использовать множество функций, которые оптимизированы для наиболее часто используемых размеров данных – один, два, четыре, или восемь байт (для 64-х разрядной платформы). Приведем список этих функций, описанных в заголовочном файле .

put_user(datum, ptr) __put_user(datum, ptr) Данные макросы пишут datum в адресное пространство пользователя. Они относительно быстрее, и должны быть использованы вместо copy_to_user() везде, где требуется передача скалярного значения. Так как при расширении этих макроопределений не выполняется проверка типов, то вы можете передать любой тип указателя в put_user() из адресного пространства пользователя. Размер передаваемых данных зависит от типа аргумента ptr и определяется во время компиляции, используя специальные псевдофункции, определенные в gcc, и на которых не стоит сейчас акцентировать внимание. В результате, если ptr указатель на char, то будет передан один байт данных. То же для двух, четырех, и, возможно, восьми байт. put_user() проверяет возможность записи по данному адресу памяти. В случае успешного завершения возвращается 0, и -EFAULT в случае ошибки. __put_user() выполняет меньшее количество проверок (не выполняется вызов access_ok()), но определенные ошибки неправильной адресации могут быть определены данным вызовом. Таким образом, __put_user() должен быть использован только если регион памяти был уже проверен вызовом access_ok(). В общем, можно сказать, что при необходимости многократного обращения к одному региону памяти, вызов access_ok() следует производить только перед первым обращением к региону, и использовать __put_user() в дальнейшем. get_user(local, ptr) __get_user(local, ptr) Эти макросы используются для получения одного элемента данных из адресного пространства пользователя. Их поведение схоже с макросами put_user() и __put_user(), но передача данных производится в обратном направлении. Полученное из адресного пространства пользователя значение сохраняется в локальной переменной local. Возвращаемое макросом значение определяет успешность выполнения операции. Макрос __get_user() следует использовать только в том случае, если корректность адреса была уже проверена вызовом access_ok().

Если попытка выполнения передачи данных с помощью описанных выше макросов приводит, на этапе компиляции, к сообщению типа «conversion to non-scalar type requested», то размер передаваемого аргумента не соответствует размерам обрабатываемым макросом. В этом случае, необходимо воспользоваться функциями copy_to_user() и copy_from_user().

Концепция «мандатов» (capabilities) и привилегированные операции

Доступ к устройству определяется атрибутами прав доступа к файловому интерфейсу устройства, и драйвер, обычно, не содержит кода дополнительного контроля доступа. Однако, возможны ситуации, когда некоторые операции должны быть запрещены для части пользователей, имеющих разрешения чтения/записи на устройство. Например, не каждый пользователь накопителя на магнитной ленте должен иметь возможность изменять размер блока, установленного для этого устройства по умолчанию. Т.е. необходимо различать настройку и обычное использование устройства. В подобных случаях, драйверу необходимо осуществлять дополнительные проверки прав доступа к устройству различных пользователей на некоторые конкретные операции.

Традиционно, системы Unix, допускают привилегированные операции только для суперпользователя. Привилегии можно рассматривать как концепцию «все-или-ничего» (all-or-nothing) – суперпользователю можно абсолютно все, но остальные пользователи являются крайне ограниченными на права доступа к системе. Ядро Linux, начиная с версии 2.2, предоставляет более гибкую концепцию, которою можно называть мандатной (capabilities). Мне затруднительно подобрать точный перевод английского слова capabilities – это, например, можно попробовать перевести как концепция потенциальных возможностей. Выбирайте сами, а лучше используйте оригинальный английский вариант – capability-based system. Так вот, такие системы отклоняют правило “все-или-ничего”, и разделяют привилегированные операции по различным подгруппам. В этом случае, некоторые пользователи (или программы) могут получить права на выполнение некоторых специфических привилегированных операций, не получая прав на другие привилегированные операции. Такая концепция пока мало используется в пространстве пользователя, но широко используется в пространстве ядра.

Полный набор таких «мандатов» (capabilities) может быть найден в заголовочном файле
. Рассмотрим некоторые элементы, которые могут быть интересны разработчику драйверов:

CAP_DAC_OVERRIDE Дает возможность перекрытия ограничений доступа к файлам и каталогам. CAP_NET_ADMIN Позволяет выполнять сетевые задачи администрирования, включая те, которые определяют сетевой интерфейс. CAP_SYS_MODULE Дает возможность загружать или выгружать модули ядра. CAP_SYS_RAWIO Дает возможность выполнения операций «сырого» (raw) ввода/вывода. Например, обращение к портам устройства, или прямое взаимодействие с устройствами USB. CAP_SYS_ADMIN Особый мандат “catch-all” (владеть-всем), который обеспечивает доступ ко многим операциям системного администрирования. Прим. переводчика: Судя по всему, в оригинале какое-то недоразумение. Мандат CAP_SYS_ADMIN в моем текущем ядре 2.4.26, согласно комментариям в файле
дает возможность только на использование системной функции reboot(). Зато определение CAP_SYS_PACCT действительно позволяет получить доступ к нескольким десяткам операций системного администрирования. CAP_SYS_TTY_CONFIG Дает возможность выполнения задач конфигурирования tty.

Перед выполнением привилегированной операции, драйвер устройства должен проверить, что вызывающий процесс имеет соответствующий “мандат”. Такую проверку можно осуществить с помощью функции capable(), определенной в заголовочном файле :

В примере драйвера scull, любой пользователь имеет права на получение информации о размерах кванта или массива квантов. Однако, только привилегированный пользователь может изменять эти значения, т.к. неверный выбор значений для этих параметров может отрицательно сказаться на производительности системы. При необходимости, драйвер scull проверяет уровень привилегированности пользователя следующим способом:

Выбор мандата определяется из следующих соображений. Если в файле
отсутствует более специфический для данной привилегии мандат, то выбирается общий – CAP_SYS_ADMIN.

Реализация команд ioctl()

Реализация ioctl() в драйвере scull очень проста. Данный системный вызов используется для передачи и получения некоторых настроечных параметров драйвера. Опустив часть почти повторяющегося кода, приведем реализацию наиболее интересных команд метода ioctl():

Шесть способов передачи и приема аргументов выглядят, с точки зрения пользовательского процесса, следующим образом:

Конечно, в обычном драйвере, наверное, не реализуется такой калейдоскоп передачи параметров. Здесь, мы ставим целью только демонстрацию всех возможных способов передачи.

Управление устройством не используя ioctl()

Управление некоторыми устройствами удачнее реализуется через запись управляющей последовательности в само устройство. Такая техника, например, используется для драйвера консоли, в который передаются, так называемые эскейп-последовательности (esc-последовательности), используемые для перемещения курсора, изменения цвета, и выполнения других конфигурационных задач. Выгода такого способа управления заключается в том, что пользователи, имеющие права на запись в устройство могут участвовать в управлении им не приобретая специальных привилегий, и не используя специальных программ, реализующих ioctl() вызовы.

Например, программа setterm (см. man setterm) управляет конфигурацией консоли (или другого терминала) передачей esc-последовательностей. Дополнительным преимуществом такого управления является простота удаленного управления устройством. Управляющая программа и управляемое устройство могут размещаться на разных компьютерах, т.к. управление может осуществляться простым перенаправлением потока данных.

Неприятной стороной такого управления устройством являются дополнительные ограничения по управлению устройством. Например, вы должны быть уверены, что управляющие последовательности не окажутся внутри потока данных передаваемых в устройство. Это справедливо и для tty. Несмотря на то, что текстовый дисплей предназначен для отображения только символов ASCII, иногда, в потоке передаваемых данных, могут оказаться управляющие символы, влияющие на настройку консоли. Это может случиться, например, при выполнении операции grep над бинарными файлами — извлекаемые строки могут содержать все, что угодно, в том числе и коды, воспринимаемые консолью как управляющие. Так, например, это может закончиться изменением шрифтов в консоли.

Символ CTRL-N устанавливает альтернативный шрифт в консоли, состоящий из графических символов, не являющихся дружественными по отношению к вводу символов в вашей командной оболочке. Если вы столкнулись с этой проблемой, то передайте в консоль (командой echo) символ CTRL-O для восстановления исходного шрифта.

Управление посредством записи в устройство лучшим образом подходит для устройств, работа которых, в основном, состоит в отклике на принимаемые ими команды. К таким устройствам, прежде всего, следует отнести различные робототехнические устройства.

Например, от нечего делать, авторами оригинального английского материала этой книги, был написан драйвер, управляющий перемещением камеры по двум осям. «Устройством» для данного драйвера является пара старых шаговых двигателей, для которых, обычное понимание операций чтения/записи не имеет смысла. Поэтому, переданные в операции записи данные, драйвер интерпретирует как ASCII команды и преобразует их в электрические импульсы управления шаговыми двигателями. Эта идея схожа с концепцией управления модемом через последовательность AT-команд. Различие заключается лишь в том, что используемый для соединения с устройством последовательный порт используется не только для передачи команд, но и для передачи данных. Неоспоримым преимуществом такого способа управления является возможность использования стандартных команд типа cat для управления устройством, не утруждая себя написанием и использованием специальных программ реализующих системные вызовы ioctl.

При написании командно-ориентированных драйверов, не имеет смысла реализовывать метод ioctl(). Добавление команд в интерпретатор проще в реализации и использовании, нежели расширение функциональности драйвера через механизм ioctl(), прежде всего тем, что может не потребует исправление в пользовательской программе в обязательном порядке. Возможно, стоит напомнить, что при реализации управления драйвером через механизм ioctl(), вам потребуется специальная программа пользовательского уровня для предоставления интерфейса управления, в то время как реализация управления через метод write() драйвера, позволит использовать стандартный набор утилит Unix (echo, cat и пр.) для взаимодействия с устройством.

Блокировка ввода/вывода

В реализации отклика на чтение в драйвере может возникнуть проблема, когда, с одной стороны, данные еще не кончились (т.е. нет конца файла), но, в данный момент, еще не готовы.

Наверное вы уже догадались, что в этом случае, необходимо отправить вызывающий процесс в спящее состояние («go to sleep waiting for data»). В этом разделе мы покажем как перевести процесс в спящее состояние, как его разбудить, и как приложение может определить готовность данных. Затем мы применим эту же концепцию к операции записи.

Прежде чем продемонстрировать код, мы объясним некоторые концепции.

Уход в сон и пробуждение

Всегда, когда процесс должен ждать какого-либо события (ожидание данных, или завершение другого процесса), он должен быть погружен в спящее состояние. Это означает, что система приостанавливает исполнение процесса, освобождая процессор для исполнения других процессов. Позже, когда ожидаемое событие реализуется, процесс будет разбужен и продолжит свою работу. В этом разделе будет обсуждаться реализация этого механизма для ядра 2.4. Позднее, в разделе «Вопросы обратной совместимости» мы рассмотрим особенности более ранних версий.

Существует несколько способов управления переводом процесса в спящее состояние и его пробуждения. Каждый из этих способов используется в разных контекстах. Однако, все эти способы используют один и тот же тип данных – очередь ожидания (wait_queue_head_t). Очередь ожидания – это очередь процессов, ожидающих какого-либо события. Очередь ожидания описывается и инициализируется следующим образом:

Даже если очередь ожидания описана статически (static), т.е. она определена не как автоматическая переменная или часть динамически распределенной структуры данных, то, по прежнему, возможна инициализация очереди на этапе компиляции:

Наиболее общей ошибкой является игнорирование процедуры инициализации очереди ожидания. Возможно, это следствие того, что в ранних версиях ядра такая инициализации не требовалась. Если вы забудете выполнить такую инициализацию то результат работы с очередью будет непредсказуемым.

После того, как очередь ожидания описана и проинициализирована, ее можно использовать для перевода процесса в спящее состояние. Такой перевод осуществляется вызовом одной из sleep_on-функций:

sleep_on(wait_queue_head_t *queue); Функция выполняет перевод процесса в спящее состояние. Недостатком использования такого способа приостановки выполнения процесса является невозможность его прерывания. Т.е. процесс становится неубиваемым, если ожидаемое им событие не может случиться. interruptible_sleep_on(wait_queue_head_t *queue); Прерываемый вариант. Работает также как и sleep_on(), но переведенный в сон процесс может быть прерван сигналом. Разработчики драйверов использовали такую форму вызова достаточно долгое время до появления, описанной ниже, альтернативы wait_event_interruptible(). sleep_on_timeout(wait_queue_head_t *queue, long timeout); interruptible_sleep_on_timeout(wait_queue_head_t *queue, long timeout); Поведение этих двух функций сходно с поведением функций описанных выше, с той лишь разницей, что уснувший процесс может находится в состоянии сна не дольше заданного таймаута. Таймаут определен в джифисах (jiffies) – 0.01 сек. Подробнее об этом будет рассказано в главе 6 «Flow of Time». void wait_event(wait_queue_head_t queue, int condition); int wait_event_interruptible(wait_queue_head_t queue, int condition); Эти макросы предоставляют наиболее предпочтительный способ перевода процесса в сон до момента удовлетворения заданного условия. Макросы связывают ожидание события и проверку его возникновения способом, позволяющим избежать проблемы «race condition» (борьба за ресурсы). Код будет спать до момента возникновения условия, которое может быть задано любым логическим выражением языка Си, вычисляемым в true (истина). Данные макросы расширяются в цикл while, который перевычисляет условие перед каждым повторением тела цикла. Такое поведение отличается от вызова функции или простого макро, где аргументы вычисляются только в момент вызова. Последниее макро реализуется как выражение, которое возвращает ноль в случае успеха, и -ERESTARTSYS, если цикл прерывается сигналом.

Стоит заметить, что в большинстве случаев разработчики драйверов должны всегда использовать прерываемые варианты этих функций/макросов. Непрерываемые версии предоставляются для случаев, когда обработка сигналов нежелательна, либо опасна. Например, при ожидании страницы данных из пространства свопа. В большинстве драйверов не возникает подобных ситуаций.

Понятно, что перевод в состояние сна только половина проблемы. Пробуждение — вторая часть этой проблемы. Как правило, если одна часть драйвера спит, то существует другая его часть, которая может выполнить пробуждение, при возникновении определенного события. Как правило, драйвер пробуждает спящие куски кода в своем обработчике прерываний при получении новой порции данных. Однако, возможен и другой сценарий.

Поскольку существует несколько способов перевода в спящее состояние, то существует и более одного способа пробуждения. Рассмотрим список высокоуровневых фукнций, предоставляемых ядром, для пробуждения процесса.

wake_up(wait_queue_head_t *queue); Данная функция будит все процессы, которые ожидают данной очереди. wake_up_interruptible(wait_queue_head_t *queue); Данная функция будит только те процессы, которые находятся в прерываемом сне. Все остальные процессы, которые заснули по заданной очереди событий, но были переведены в сон с помощью «непрерываемых» функций, останутся в спящем состоянии. wake_up_sync(wait_queue_head_t *queue); wake_up_interruptible_sync(wait_queue_head_t *queue); Обычно, вызов wake_up() может привести к немедленному переупорядочиванию очереди, означающему, что другие процессы могут быть запущены до того, как вызов wake_up() завершится. «Синхронный» вариант вызова помечает разбуженные процессы запускаемыми, но не переупорядочивает очередь. Это используется для предотвращения переупорядочивания очереди, в случае, если текущему процессу известно о переводе в сон, что позволяет использовать принудительное переупорядочивание. Заметьте, что разбуженные процессы могут быть немедленно запущены на другом процессоре, и использование этих функций должно предотвращать ситуацию mutual exclusion («взаимное исключение»). Примечание переводчика: Перевод последнего абзаца, наверное, неверен. Привожу оригинал. «Normally, a wake_up call can cause an immediate reschedule to happen, meaning that other processes might run before wake_up returns. The «synchronous» variants instead make any awakened processes runnable, but do not reschedule the CPU. This is used to avoid rescheduling when the current process is known to be going to sleep, thus forcing a reschedule anyway. Note that awakened processes could run immediately on a different processor, so these functions should not be expected to provide mutual exclusion.»

Если ваш драйвер использует interruptible_sleep_on(), то имеется небольшое отличие между вызовами wake_up() и wake_up_interruptible(). Однако, чтобы быть последовательным, используя interruptible_sleep_on() для перевода в сон, следует использовать wake_up_interruptible() для пробуждения.

В качестве примера использования очереди ожидания, вообразите, что вы хотите перевести процесс в сон, при чтении вашего устройства, и разбудить его, когда кто-нибудь еще осуществляет запись в устройство. Следующий код реализовывает описанную функциональность:

Вы можете использовать этот код в качестве примера управления переводом в сон и пробуждением процесса. Для тестирования можно использовать обычную команду cat с перенаправлением ввода/вывода.

Вы должны помнить, что пробуждение процесса может быть связано не с тем событием, которого он ожидал. Процес может быть разбужет по другим причинам, например, в результате получения сигнала. Поэтому, спящий код должен проверять необходимое условие после пробуждения. Подробнее это рассмотрено в разделе «Простая реализация: scullpipe» в этой главе позднее.

Углубленный взгляд на очередь ожидания

Ранее мы обсудили все то, что должны знать большинство разработчиков драйверов для управления переводов процессов в сон и их пробуждением. Однако, некоторым читателям захочется узнать об этом немного больше. В этом разделе мы попытаемся, в общих чертах, удовлетворить их любопытство. Все остальные могут пропустить этот раздел не боясь упустить что-либо важное.

Тип wait_queue_head_t, в действительности, очень простая структура, определенная в

  • . Она содержит только переменную lock и связанный список спящих процессов. Индивидуальные элементы данных списка представлены типом wait_queue_t, который представляет собой обычный список, определенный в
  • , и описан в разделе «Связанные списки» в главе 10 «Judicious Use of Data Types». Обычно, структуры wait_queue_t распределяются в стеке такими функциями, как interruptible_sleep_on(). Эти структуры оказываются в стеке просто потому, что они описаны как автоматические переменные в соответствующих функциях. Как правило, у программиста не возникает необходимости использования этих структур.

    Однако, в некоторых приложениях, может потребоваться прямая работа с переменными wait_queue_t. Для этого, имеет смысл рассмотреть то, что происходит внутри таких функций, как interruptible_sleep_on(). Ниже приведена упрощенная версия реализации interruptible_sleep_on() для перевода процесса в сон:

    Представленный здесь код создает новую переменную wait типа wait_queue_t, которая будет рассположена в стеке, и инициализирует ее. Для текущего процесса, current->state устанавливается в значение TASK_INTERRUPTIBLE, означающем, что процесс уходит в прерываемый сон. Затем, элемент очереди ожидания добавляется к очереди queue (аргумент wait_queue_head_t *). Затем вызывается диспетчер schedule(), который переключает процессор на какую-либо другую задачу. Функция schedule() завершится только тогда, когда кто-нибудь еще разбудит данный процесс и установит его состояние current->state в значение TASK_RUNNING. После этого, элемент очереди wait удаляется из очереди queue, и сон завершается.

    На рисунке 5-1 показаны элементы структур данных очередей ожидания, и их использование процессами.

    Беглый анализ кода ядра показывает, что огромное количество процедур уходит в сон самостоятельно, используя код, похожий на тот, что показан в предыдущем примере. Большая часть этих реализаций относит нас к эпохе ядра 2.2.3, еще до появления макроса wait_event(). Как уже говорилось, использование макроса wait_event() является предпочтительным способом перевода процесса в сон до возникновения нужного события, потому что использование функции interruptible_sleep_on() может привести к нежелательной проблеме борьбы за ресурсы (race condition). В разделе «Going to Sleep Without Races» главы 9 «Interrupt Handling» мы подробно остановимся на причинах, приводящих к race condition при засыпании. Кратко можно сказать лишь о том, что проблема может возникнуть за время между тем, как код драйвер решил заснуть по interruptible_sleep_on() и заснул.

    Есть еще одна причина, по которой необходимо реализовывать явный вызов диспетчера (scheduler). Возможна ситуация, когда, во время сна, несколько процессов ждут одного и того же события (exclusive sleep). При вызове wake_up() все эти процессы будут пытаться продолжить выполнение. Предположим, что ожидаемое этими процессами событие состоит в получении некоего атомарного куска данных. Тогда, только один процесс может обратиться к этим данным. Все остальные процессы, должны просто проснуться, проверить доступность данных и вернуться в спящее состояние.

    Такая ситуация иногда называется проблемой «громоподобного стада» (thundering herd problem). В условиях высокой высокой производительности системы, такая проблема приводит к заметным и бесполезным затратам вычислительных ресурсов. Создание большого количества готовых к запуску процессов, которые не могут выполнять полезной работы приводят к пустой перегрузки процессора из-за затрат на переключение контекстов процесса. Ситуация будет намного проще, если такие процессы останутся в спящем состоянии.

    По этим причинам, во время разработки ядра серии 2.3 была добавлена концепция эксклюзивного сна (exclusive sleep). Если процессы спят в таком эксклюзивном режиме, то они сообщают ядру о необходимости будить только один из них. В результате, в таких ситуациях, улучшается производительность системы.

    Приведем пример кода, демонстрирующего переход в эксклюзивный сон. Приводимый код очень похож на пример ухода в обычный сон.

    Добавление флага TASK_EXCLUSIVE в поле current->state текущей задачи уже показывает, что процесс находится в эклюзивном ожидании. Однако, также необходимо вызвать функцию add_wait_queue_exclusive(). Эта функция добавляет процесс в конец очереди ожидания, после всех остальных процессов в очереди. В результате, все остальные процессы, находящиеся в неэксклюзивном сне оказываются в начале очереди, где они всегда будут разбужены. Как только wake_up() встречает первый эксклюзивно спящий процесс, то его работа по пробуждению процессов останавливатся.

    Внимательный читатель может предположить и другие причины для явного управления очередью ожидания и диспетчером (explicitly). В то время как функции, похожие на sleep_on(), будут блокировать процессы только по одной очереди ожидания, прямая работа с очередями позволяет установить сон по нескольким очередям одновременно. В большинстве случаев это не нужно. Однако, при необходимости, вы можете воспользоваться приведенным выше способом.

    Для более глубокого ознакомления с кодом по очередям ожидания, ознакомтесь с файлами
    и kernel/sched.c.

    Написание реентерабельного кода

    Когда процесс уходит в спящее состояние, драйвер продолжает работать и может быть вызван другим процессом. Давайте рассмотрим пример консольного драйвера. Пока приложение ожидает ввода с клавиатуры на tty1, пользователь может переключиться на tty2 и запустить новый shell. Теперь оба командных интерпретатора ожидают ввода с клавиатуры в драйвере консоли, хотя они спят по различным очередям ожидания: одна очередь связана с tty1, а другая — с tty2. Каждый процесс блокируется функцией interruptible_sleep_on(), но драйвер может принимать и отвечать на запросы с других tty-устройств.

    Конечно, на системах SMP, множественные одновременные обращения к драйверу могут возникнуть даже при неспящих процессах.

    Такие ситуации могут быть безболезненно обработаны реентерабельным кодом. Реентерабельным называется код, который не содержит информацию о своем состоянии в глобальных переменных, и, таким образом, способен управлять данными от разных задач, не смешивая их. Если вся информация о текущем статусе будет специфична для текущего обрабатываемого процесса, то взаимовлияния процессов ни когда не случится.

    Если вам потребуется статусная информации, то вы можете сохранить ее либо в локальных переменных функции драйвера (каждый процесс имеет свою собственную страницу стека в пространстве ядра, в которой будут сохранены локальные переменные), либо вы можете воспользоваться указателем private_date в структуре, на которую указывает filp. Использование локальных переменных предпочтительнее, потому что иногда, одни и теже указатели filp могут быть разделены между двумя процессами, обычно между родительским процессом (parent) и ребенком (child).

    Если вам необходимо сохранить большое количество статусных данных, то вы можете удержать их под локально описанным указателем, и использовать kmalloc() для распределения необходимого пространства памяти. В этом случае, вы не должны забывать освобождать эту память через вызов kfree(), потому что работая в пространстве ядра мы не можем полагаться на правило «при завершении процесса, вся связанная с ним память автоматически освобождается». Использование локальных переменных для распределения множества элементов данных не рекомендуется, потому что данные могут не уместиться в одной странице памяти, распределенной под пространство стека.


    Реентерабельной должна быть любая функция удовлетворяющая одному из двух условий. Во-первых, если она вызывает диспетчер (schedule), например, неявно, через обращение к sleep_on() или wake_up(). Во-вторых, если она копирует данные в, или из пространства пользователя, потому что обращение в пространство пользователя может привести к page-fault, и процесс уйдет в сон до тех пор, пока ядро не подгрузит требуемую страницу из стека. Каждая функция, которая вызывает другие такие функции, также должна быть реентерабельной. Например, если функция sample_read() вызывает функцию sample_getdata(), которая может быть блокирована, то sample_read(), также как и sample_getdata() должна быть реентерабельна, потому что ничто не запрещает вызвать ее какому-нибудь другому процессу, во время ее обработки спящего процесса.

    Кроме того, конечно, код который спит всегда должен помнить, что состояние системы может измениться во время его сна. Поэтому, драйвер должен проверить все элементы своего окружения, которые могли измениться.

    Блокируемые и неблокируемые операции

    Прежде чем мы рассмотрим реализацию полнофункциональных методов чтения и записи, необходимо ознакомиться со значеним флага O_NONBLOCK в поле filp->f_flags. Этот флаг определен в заголовочном файле

  • , который автоматически включен в
  • .

    Название флага просходит от «open-nonblock», потому что он может быть определено во время открытия драйвера, и первоначально, мог быть определен только там. Если вы просмотрите код источника, то вы найдете несколько ссылок на флаг O_NDELAY. Это альтернативное имя для флага O_NONBLOCK, принятое для совместимости с кодом System V. Флаг, по умолчанию, сброшен, потому что сон, является нормальным состоянием процесса ожидающего данные. В случае разрешения блокировки, для поддержания стандартной семантики, следует реализовывать следующее поведение:

    • Если процесс вызывает системный вызов read(), но данные еще не доступы, то процесс должен быть блокирован. Процесс пробуждается, как только появляются какие-нибудь данные, которые и передаются процессу, даже если количество доступных данных меньше, чем запрошено аргументом count.
    • Если процесс вызывает системный вызов write(), но буфер для данных еще не готов, то процесс должен быть блокирован, и для блокировки должна быть использована другая очередь ожидания, нежели для чтения. Когда данные будут переданы в устройство, и выходной буфер опустеет, то процесс просыпается и системный вызов write() успешно завершается, хотя данные могут быть записаны только частично, если в буфере недостаточно места для тех count байт, которые были запрошены.

    Оба эти утверждения предполагают, что присутствуют как входной, так и выходной буфера. Большинство драйверов устройств, действительно, имеют их. Входной буфер требуется для того, чтобы избежать потери данных тогда, когда их никто не читает. Также, данные не могут быть потеряны и при записи, потому что если системный вызов не принимает данные, то они остаются в буфере пространства пользователя. Кроме того, выходной буфер почти всегда полезен для повышения производительности передачи данных в устройство.

    Выигрыш в производительности от реализации выходного буфера в драйвере происходит от уменьшения количества переключений контекста и переходов между уровнями ядра/приложение. Для медленных устройств без выходного буфера, за один системный вызов могут быть переданы только один или несколько символов. Рассмотрим работу двух процессов с драйвером. Пока один процесс заснул по write(), другой процесс работает (первое переключение контекста). Когда первый процесс просыпается, он возобновляет свою работу (второе переключение контекста). Когда вызов write() завершается (переход из пространства ядра в пространство пользователя), то процесс вызывает write() повторно для передачи следующей порции данных (переход из пространства пользователя в пространство ядра). Вызов блокируется и все начинается сначала. И так до тех пор, пока не будут переданы все данные. Если выходной буфер достаточно большой, то передача всех данных производится за один вызов write(). Буферизованные данные будут переданы в устройство позднее, в «свободное» время, без необходимости возврата в пространство пользователя для второго и последующих вызовов. Выбор подходящего размера выходного буфера представляет собой задачу, специфическую для каждого конкретного устройства.

    Мы не используем входной буфер в драйвере scull, потому что данные уже доступны при вызове read(). Также, мы не используем выходного буфера, потому что данные просто копируются в область памяти связанной с устройством. Наше устройство, по определению, уже является буфером, поэтому дополнительный буфер будет явно излишним. Мы увидим использование буферов в главе 9 «Interrupt Handling», в разделе «Interrupt-Driven I/O».

    Поведение вызовов read() и write() различаются при определении флага O_NONBLOCK. В этом случае, системные вызовы просто возвращают -EAGAIN если процесс вызвал read() при отсутствии входных данных, или если он вызвал write() в то время, когда в системе недостаточно памяти для распределения выходного буфера.

    Как вы можете предположить, неблокируемые операции вовращают управление вызывающему процессу немедленно, позволяя приложению периодически опрашивать готовность системы принять, или передать данные. Приложения должны быть внимательны при использовании функций из stdio, работая с неблокируемыми файлами, потому что они могут легко ошибиться, приняв неблокируемый возврат за EOF. Необходимо всегда проверять errno.

    В действительности, флаг O_NONBLOCK имеет большое значение и для системного вызова open(). Это может быть полезным, когда системный вызов может быть блокирован на долгое время. Например, при открытии FIFO, в который еще не было записи, или при получении доступа к файлу с незаконченной блокировкой (with a pending lock). Обычно, открытие устройства заканчивается либо удачно, либо неудачно, и не требует ожидания внешних событий. Однако, иногда, открытие устройства требует продолжительной инициализации, и вы можете добавить поддержку O_NONBLOCK для вызова open(), для немедленного получения результата -EAGAIN («try it again» — попытайся снова). Мы рассмотрим одну такую реализацию в разделе «Блокировка системного вызова open() как альтернатива EBUSY» позднее в этой главе.

    Некоторые драйвера могут, также, реализовать специальную семантику для O_NONBLOCK. Например, открытие устройства накопителя на магнитной ленте, обычно блокируется до тех пор, пока лента не будет вставлена. Если такое устройство открывается с использованием флага O_NONBLOCK, то оно открывается немедленно, независимо от того, вставлена магнитная лента или нет.

    Надо заметить, что флаг O_NONBLOCK влияет только на операции read(), write() и open().

    Пример реализации: scullpipe

    Четыре устройства /dev/scullpipe являются частью scullmodule и используются для демонстрации реализации блочного ввода/вывода.

    Процесс блокированный по системному вызову read() просыпается при появлении данных. Обычно, из устройства поступает прерывание, сообщающее об этом событии, и драйвер будит ожидающий процесс при обработке этого прерывания. Драйвер scull работает немного иначе. Ему не требуется какое-либо специальное оборудование или обработчик прерываний. Мы будем использовать один процесс передающий данные в драйвер, что будет приведет к пробуждению другого процесса, читающего данные из этого драйвера. Такая реализация очень похожа на работу FIFO (или «named pipe» — именованной трубы). Отсюда и название драйвера — scullpipe.

    Драйвер устройства использует структуру, которая включает две очереди ожидания и буфер. Размер буфера конфигурируется обычными способами (на этапе компиляции, во время загрузки или исполнения).

    Предствленная ниже реализация системного вызова read() управляет как блокируемым, так и не блокируемым вводом. Позднее, в разделе «Seeking a Device» мы объясним начальную строчку кода, которая может привести читателей в замешательство.

    Обратите внимание на оператор PDEBUG. При компиляции вы можете разрешить выдачу драйвером отладочных сообщений, которые упростят наблюдение за работой устройства.

    Также, обратите внимание на использование семафоров для защиты критических кусков кода. Код драйвера scull должен избегать погружения в сон при удержании семафора. В противном случае, процесс передающий данные в драйвер не сможет этого сделать, и произойдет взаимная блокировка — deadlock. Приведенный код, при ожидании требуемых данных, использует wait_event_interruptible() для перевода процесса в сон. Однако, код должен проверить доступность данных после пробуждения. Может оказаться так, что кто-нибудь еще захватит данные между тем, как произойдет пробуждение и получение семафора.

    Имеет смысл напомнить, что процесс может уйти в сон либо вызывая, явно или неявно, диспетчер (scheduler), либо при передаче данных в/из пользовательского пространства. В последнем случае, процесс может заснуть, если пользовательские данные, в текущий момент, не представлены в оперативной памяти. Если scull засыпает при копировании данных между пространствами ядра и пользовательского процесса, то он заснет при удержании семафора устройства. Удержание семафора в этом случае оправдано, так как предотвращает взаимную блокировку системы (deadlock), и препятствует изменению передаваемого блока данных во время сна драйвера.

    Оператор if, за которым следует interruptible_sleep_on() ответственнен за обрабокту сигнала. Этот оператор гарантирует правильную и ожидаемую реакцию на сигналы, которые могут быть ответственны за пробуждение процесса (т.к. мы находимся в прерываемом сне). Если сигнал пришел, и не был блокирован процессом, то правильное поведение позволит верхним слоям ядра обработать это событие. В помощь этому, драйвер возвращает -ERESTARTSYS. Это значение используется слоем виртуальной файловой системы VFS (Virtual File System), который либо вызовет системный вызов повторно, либо возвратит -EINR пользовательскому процессу. Мы будем использовать тот же самый оператор для обработки сигнала в каждой read() и write() реализации. Так как signal_pending() появился только в ядре версии 2.1.57, то мы, в нашем заголовочном файле sysdep.h определили его для более ранних версий ядра, для сохранения совместимости нашего кода с такими ядрами.

    Реализация для вызова write() похожа на реализацию вызова read(). И так же как для read() мы объясним значение первой строки кода позднее. Особенностью этого кода является то, что он никогда полностью не заполняет буфер, всегда оставляя «дырку» хотя бы для одного байта. Таким образом, когда буфер пуст, то wp и rp эквиваленты, когда же в буфере появляются данные, эти элементы всегда различаются.

    По нашему замыслу, рассматриваемый драйвер не реализует блокирование в системном вызове open(). Эта деталь отличает его от реализации FIFO. Если вам интересно познакомиться с реальным положением вещей, то вы можете рассмотреть код из файла fs/pipe.c в каталоге источников ядра.

    Для того, чтобы протестировать блокировку операций в драйвере scullpipe, вы можете задействовать драйвер из нескольких программ, используя обычное перенаправление ввода/вывода. Тестирование неблокируемой активности значительно сложнее, потому что обычные программы не используют неблокируемые операции. В нашем каталоге источников misc-progs, вы найдете простую программу nbtest, которую можно использовать для тестирования таких операций. Все, что делает эта программа, это копирование своего ввода на свой вывод, используя неблокируемый ввод/вывод и выполняя задержку между повторами. Время задержки передается в программу через командную строку, и, по умолчанию, равно одной секунде.

    Системные вызовы poll() и select()

    Приложения, которые используют неблокируемый ввод/вывод, также, часто используют системные вызовы poll() и select(). Эти системные вызовы, во многом, равнофункциональны. И тот и другой позволяют процессам определить возможность открытия файлов на чтение или запись без блокировки. Таким образом, они часто используются приложениями, которые должны использовать множество входных или выходных потоков без блокировки какого либо из них. Одна и та же функциональность предлагается двумя различными функциями, потому что они были реализованы в Unix практически одновременно двумя различными группами. select() был реализован в BSD Unix, а poll() — в Systev V.

    Поддержка каждого из этих системных вызовов требует реализации в драйвере соответвующей функции. В версии 2.0 ядра был реализован только интерфейс для метода select(), и системный вызов poll() не был доступен из пользовательского пространства. Однако начиная с версии 2.1.23, ядро предоставляло интерфейс для обоих методов, который был основан на реализации системного вызова poll(), который предоставлял более обширные возможности управления, нежели select().

    Реализация метода poll(), обслуживающего системные вызовы poll() и select(), имеет следующий прототип:

    Этот метод драйвера будет вызван пользовательским процессом как при обращении к системному вызову poll(), так и при обращении к select(). В метод передается файловый дескриптор, связанный с драйвером. Реализация этого метода драйвера строится на выполнении следующих двух шагов:

    1. Вызов poll_wait() на основе одной или нескольких очередей ожидания (wait queues), который может показать изменения в статусе опроса (in the poll status).
    2. Возвращение битовой маски, описывающей операции, которые могут быть выполнены без блокировки в данный момент времени (на момент опроса).

    Обе эти операции достаточно просты, и выглядят очень схожим образом во всех драйверах. Однако, они опираются на информацию, которая может получена только из конкретного драйвера, поэтому метод должен быть реализован индивидуально для каждого драйвера.

    Структура poll_table, второй аргумент в методе poll(), используется в ядре для реализации вызовов poll() и select(). Структура описана в
    , которая предоставляется источниками ядра. Разработчикам драйверов нет необходимости знать что-либо о ее внутренностях. Эту структуру можно рассматривать как совершенно &qout;прозрачный» объект. Она передается в метод драйвера, так, что каждый элемент очереди событий, который может разбудить процесс и измениь статус операции poll(), может быть добавлен в структуру poll_table с помощью вызова poll_wait():

    Вторая задача, выполняемая методом poll() заключается в возвращении битовой маски, описывающей операции, которые могут быть выполнены немедленно. Например, если устройство имеет доступные данные, и метод read() может быть выполнен без ухода в спящее состояние, то метод poll() должен отразить этот факт в битовой маске. Для определения возможных операций можно использовать некоторые флаги, определенные в
    :

    POLLIN Этот бит должен быть установлен, если устройство может быть прочитано без блокировки. POLLRDNORM Этот бит должне быть установлен, если данные («normal» data) доступны для чтения. Доступное для чтения устройство возвращает (POLLIN | POLLRDNORM). POLLRDBAND Этот бит показывает, что на чтение из устройства доступно out-of-band данных (т.е. некий избыток данных). В данный момент это используется только в одном месте Linux ядра — в коде сетевого транспорта протокола DECnet. Этот бит, в общем, не применим к драйверам устройств. POLLPRI Флаг указывает на то, что высокоприоритетные данные (out-of-band) могут быть прочитаны без блокировки. Данные бит воспринимается вызовом select() как возникновение в файле условия исключения (exception condition). POLLHUP Когда процесс, читающий это устройство, достигает конца файла (end-of-file), то драйвер должен устрановить POLLHUP (hang-up). Процесс, вызывающий select(), будет уведомлен, что устройство доступно для чтения, как продиктовано функциональностью метода select(). POLLERR Флаг указывает на возникновение ошибки в устройстве. При вызове poll() устройство определяется как готовое и к чтению, и к записи, т.к. и при чтении и при записи устройство возвратит код ошибки без блокировки. POLLOUT Этот бит устанавливается в том случае, если устройство может быть прочитано без блокировки. POLLWRNORM Этот бит имеет тот же самый смысл, что и POLLOUT, и, в действительности, имеет тот же самый номер. Устройство, готовое к запими возвращает (POLLOUT | POLLWRNORM). POLLWRBAND Как и POLLRDBAND, этот бит означает, что данные с ненулевым приоритетом могут быть записаны в устройство. Этот бит используется только в реализации poll() для дейтаграмм (datagram), т.к. только дейтаграммы могут передавать out-of-band данные.

    Наверное, нет смысла уточнять, что флаги POLLRDBAND и POLLWRBAND имеют смысл только для файловых дескрипторов связанных с сокетами. Другие драйвера обычно не используют эти флаги.

    Описание метода poll() весьма обширное, и имеет смысл просто познакомиться с одним из практических примеров его реализации. Рассмотрим реализацию метода poll() для драйвера scullpipe:

    Этот код просто добавляет две очереди событий scullpipe в таблицу poll_table, и устанавливает соответствующие биты в маске, в зависимости от того, доступно ли устройство на чтение или на запись.

    Как вы можете видеть, приведенный код метода poll() не поддерживает end-of-file. Метод poll() должен возвращать флаг POLLHUP, когда устройство достигло конца файла. Если же программа пользователя обратится к системному вызову select(), то файл, в этом случае, определится как доступный для чтения. В обоих случаях, приложение пользователя будет знать, что оно может вызвать системный вызов read() без риска быть заблокированным. В случае же действительного конца файла, вызов read() вернет 0 символизируя end-of-file.

    Если обратиться к настоящей реализации FIFO (очередь), то читающий процесс увидит конец файла тогда, когда все пишущие в файл процессы, закроют файл. В отличии от этого, процесс читающий драйвер scullpipe никогда не увидит конца файла. Поведение различно потому, что FIFO намеревается организовать коммуникационный канал между двумя процессами, в то время как scullpipe является «мусорным ведром» в которое кто угодно может положить данные, и хотя бы один процесс может их прочитать.

    Реализация end-of-file тем же способом, как это сделано для FIFO будет означат необходимость проверки поля dev->nwriters как в методе read(), так и в методе poll(). При этом, если нет процессов, которые открыли бы устройство для записи, то методы должны будут сообщить о конце файла, как было описано выше. К несчастью, если читающий процесс откроет устройство scullpipe раньше, то он сразу увидит конец файла без шансов дождаться каких-либо данных. Лучшим способом устранения этой проблемы является реализация блокировки в методе open(). Эту задачу мы оставим читателю в качестве упражнения.

    Взаимодействие с методами read() и write()

    Назначение системных вызовов poll() и select() заключается в предоставлении информации о блокировке операций ввода/вывода. В этом отношении, они дополняют методы read() и write(). Более важным является то, что методы poll() и select() позволяют приложению одновременно обрабатывать несколько потоков данных. Данную функциональность мы не реализовали в драйвере scull.

    Корректная реализация этих трех системных вызовов будет залогом корректно работающего приложения. Попробуем подвести итог всему изложенному по этому вопросу.

      Чтение данных из устройства
    • Если во входном буфере присутствуют данные, то системный вызов read() может быть выполнен без какой-либо ощутимой задержки, даже, если объем доступных данных меньше того, что запрашивает приложение.
    • Если во входном буфере нет данных, то, по умолчанию, системный вызов read() будет блокирован до тех пор, пока в буфере не появиться по меньшей мере одного байта данных. С другой стороны, если установлен флаг O_NONBLOCK, то метод read() выполнится без задержки, но с кодом ошибки -EAGAIN (некоторые старые вырсии System V возвратят, в этом случае, 0). Поэтому, при запрещении блокировки имеет смысл опрашивать готовность устройства с помощью системного вызова poll()
    • При достижении конца файла, метод read() должен без задержки возвратить значение 0, независимо от того установлен ли флаг O_NONBLOCK, или нет. Метод poll() в этом случае должен возвращать флаг POLLHUP.
      Запись данных в устройство
    • Если в выходном буфере имеется свободное место, то системный вызов write() должен быть выполнен без какой-либо ощутимой задержки. Буфер может принять меньше данных, чем передается приложением, но он должен быть готов принять как минимум один байт данных. В этом случае, системный вызов poll() должен определить, что устройство готово для записи.
    • Если выходной буфер полон, то по умолчанию, системный вызов write() будет блокирован до тех пор, пока в буфере не освободится сколько нибудь места. Если установлен флаг O_NONBLOCK, то метод write() в этом случае, завершится немедленно с кодом ошибки -EAGAIN (некоторые старые версии System V возвратят, в этом случае, 0). Метод poll(), при полном буфере, должен возвратить, что устройство не готово для записи. Если же устройство не может принять больше данных, то метод write() возвращает -ENOSPC («No space left on device» — нет осталось места на устройстве), независимо от того, установлен ли флаг O_NONBLOCK, или нет.
    • Никогда не выполняйте ожидание передачи данных перед возвратом в системном вызове write(), даже если флаг O_NONBLOCK не установлен. Дело в том, что многие приложения используют метод select() для определения возможной блокировки записи. Поэтому, если сообщается, что устройство готово к записи, то запись, соответственно, не должна блокироваться. Если же программа, использующая устройство, хочет убедиться что данные переданные в выходной буфер были в действительности отправлены в устройство, то драйвер должен предоставить для такой проверки метод fsync(). Например, сменные устройства должны иметь реализацию метода fsync().

    Несмотря на то, что мы сформулировали несколько удачных основных правил обработки чтения записи, необходимо помнить, что каждое устройство, в своем роде, уникально, и в некоторых случаях необходимо немного отходить от намеченных правил. Например, такое запись-ориентированное устройство как накопитель на магнитной ленте не может выполнять частичную запись данных.

    Flushing — принудительный сброс выходного буфера

    Как мы видели, метод write() не производит полное слежение за выходным потоком данных. Функция ядра fsync(), вызываемая одноименным системным вызовом заполняет эту прореху. Прототип этого метода выглядит следующим образом:

    Илон Маск рекомендует:  Что такое код equalrgn

    Если некоторому приложению потребуется уверенность в том, что данные были действительно переданы в устройство, то необходимо будет воспользоваться системным вызовом fsync(), и соответствующий метод драйвера должен быть реализован. Метод fsync() завершает свою работу только тогда, когда данные из буфера будут полностью сброшены на устройство. Такая операция носит название flush. Операция может занять какое-то время, независимо от того, установлен флаг O_NONBLOCK, или нет. Аргумент datasync представлен только в ядрах серии 2.4, и используется для различия между системными вызовами fsync() и fdatasync(). Аргумент представляет интерес только для кода файловых систем, и может быть проигнорирован другими драйверами.

    Метод fsync() не содержит ничего необычного. Исполнение данного системного вызова не критично во времени, поэтому его реализация в драйвере может быть выполнена в произвольном авторском вкусе. Как правило, этот метод не реализовывается для драйверов символьных устройств. Драйвера блочных устройств, в другой стороны, всегда реализовывают метод общего назначения block_fsync(), который сбрасывает все блоки данных в устройство ожидая завершения ввода/вывода.

    Структуры данных для poll() и select()

    Действительная реализация системных вызовов poll() и select() достаточно проста. Всегда, когда приложение пользователя вызывает любую из этих функций, ядро вызывает метод poll() для всех файлов, связанных с данным системным вызовом, передаваю каждому из них одну и ту же таблицу poll_table. Это массив структур poll_table_entry общего назначения, связанный с каждым определенным вызовом poll() или select(). Каждый элемент poll_table_entry содержит указатель на структуру файл для открытого устройства, указатель на wait_queue_head_t, и элемент wait_queue_t. Когда драйвер вызывает poll_wait, один из этих элементов заполняется информацией из драйвера, и элемент очереди ожидания укладывается в очередь драйвера. Указатель на wait_queue_head_t используется для получения текущей очереди ожидания, в которой зарегистрирован элемент текущей таблицы опроса (poll table). Это необходимо для того, чтобы free_wait() мог удалить элемент из очереди перед пробуждением.

    Примечание переводчика: В последнем абзаце я не совсем уверен, поэтому приведу оригинальный текст. «. Whenever a user application calls either function, the kernel invokes the poll method of all files referenced by the system call, passing the same poll_table to each of them. The structure is, for all practical purposes, an array of poll_table_entry structures allocated for a specific poll or selectcall. Each poll_table_entry contains the struct file pointer for the open device, a wait_queue_head_t pointer, and a wait_queue_t entry. When a driver calls poll_wait, one of these entries gets filled in with the information provided by the driver, and the wait queue entry gets put onto the driver’s queue. The pointer to wait_queue_head_t is used to track the wait queue where the current poll table entry is registered, in order for free_wait to be able to dequeue the entry before the wait queue is awakened.»

    Если ни один из опрошенных драйверов не выдал готовность операций ввода/вывода к исполнению без блокировки, то вызов poll() просто засыпает до тех пор, пока одна, или несколько, очередей не будут готовы проснуться.

    В реализации метода poll() интересно то, что файловые операции могут быть вызваны с NULL-указателем для аргумента poll_table. Такая ситуация может произойти по паре причин. Если приложение вызвавшее poll() установила значение таймаута в 0 (т.е никаких ожиданий), то нет причин для накопления очередей ожиданий, и система просто ничего не делает. Также, poll_table_pointer может быть установлен в NULL сразу после того как какой-нибудь опрошенный драйвер показал возможность операции ввода/вывода. Так как ядро знает, где и какое неожидаемое событие возникло, то оно не выстраивает список очередей ожидания. Прим. переводчика: Опять что то не совсем понятное «Since the kernel knows at that point that no wait will occur, it does not build up a list of wait queues.»

    Когда вызов poll() завершается, то структура poll_table уничтожается из памяти, и все элементы очереди ожидания, предварительно добавленные в таблицу опроса, удаляются из таблицы и соответствующих очередей ожиданий.

    В действительности, рассмотренный только что механизм, чуть более сложен, потому что таблица опроса poll_table не совсем массив, а скорее множество одной или нескольких страниц, каждая из которых содержит массив. Это усложнение позволяет избегать ограничения на максимальное количество дескрипторов, обрабатываемых системными вызовами poll() или select(), и связанное с небольшим размером самой страницы.

    На рисунке 5-2 изображены структуры данных учавствующие в механизме опроса. Вообще, на рисунке представлена упрощенная схема реальных структур данных, потому что здесь проигнорирована многостраничная природа таблицы опросы, и неучтен файловый указатель, который является частью каждого элемента poll_table_entry. Те читатели, которых заинтересовало реальное положение дел могут познакомиться с файлами
    и fs/select.c из каталога источников ядра.

    Асинхронные уведомления (Asynchronous Notification)

    Несмотря на то, что в большинстве случаев для управления устройством достаточно механизмов блокируемых/неблокируемых операций и метода select(), все же, возникают ситуации когда такая техника управления становится недостаточной. Скоро мы рассмотрим такие случаи подробнее.

    Например, давайте вообразим, что некий процесс выполняет цикл продолжительных вычислений с ниским приоритетом, требуя, по возможности, подкачки новых данных. Если входным каналом данных являются данные с клавиатуры, то вы можете послать приложению сигнал — символ «INTR» (обычно, комбинация клавиш CTRL+C). Такая возможность передачи сигнала является частью tty-абстракции, являющейся программным слоем, который не используется для общего класса символьных устройств. Поэтому, для асинхронных уведомлений, нам требуется что-то еще. Кроме того, входные данные должны имет возможность генерировать прерывания — не только CTRL+C.

    Программы пользователя должны выполнить следующие два шага для разрешения асинхронных уведомлений из входного файла. Во-первых, они должны определить процесс как «владелец» файла. Для этого процесс должен выполнить системный вызов fcntl() с передачей в него команды F_SETOWN. В результате, идентификатор процесса (process ID) владельца сохранится в поле filp->f_owner для дальнейшего использования. Этот шаг необходим только для того, чтобы ядро знало о том какой процесс требует уведомления. Для того, чтобы разрешить ассинхронное уведомление, пользовательские программы должны установить в устройстве флаг FASYNC. Установка этого флага производится передачей команды F_SETFL в системный вызов fcntl().

    После выполнения этих двух системных вызовов, входной файл может потребовать доставку сигнала SIGIO при получении новой порции данных. Сигнал посылается процессу сохраненному в filp->f_owner (или группе процессов, если значение отрицательно).

    Например, следующие строки кода в пользовательской программе разрешают асинхронные уведомления текущего процесса при получение новой порции данных на входной файл stdin:

    Программа под именем asynctest, в источниках к данной книге, является простой программой демонстрирующей чтение stdin. Она может быть использована для проверки асинхронных возможностей драйвера scullpipe. Программа во многом похожа на стандартную программу cat, но не завершает свою работу по достижении конца файла (end-of-file).

    Заметьте однако, что не все устройства поддерживают асинхронное уведомление, и вы можете ошибиться в выборе устройства. Приложения обычно предполагают, что возможности асинхронного уведомления доступны только на сокетах и устройствах tty. Например, pipes (трубы) и FIFO (очереди) не поддерживают эту возможность, по крайней мере, на текущем ядре 2.4.х. Мышь, наоборот, поддерживает асинхронное уведомление, потому что некоторые программы ожидают, что мышь может послать сигнал SIGIO, как это делают устройства tty.

    Однако имеется одна старая проблема с входным уведомлением. Когда процесс получает SIGIO, он не знает, в каком из входных файлов появились новые данные. Т.е. если некий процесс может получить асинхронные уведомления от нескольких файлов, то приложение должно будет еще проанализировать результаты poll() или select() запросов, чтобы разобраться в том, что случилось.

    Взгляд со стороны драйвера

    Для нас, более уместным, будет являться рассмотрение того, как драйвер устройства может реализовать асинхронную передачу сигнала. В следующем списке представлена детальная последовательность операций с точки зрения ядра.

    1. При выполнении команды F_SETOWN системным вызовом fcntl() ничего не происходит, кроме передачи значения полю filp->f_owner.
    2. При выполнении системным вызовом fcntl() команды F_SETFL для установки флага FASYNC, вызывается метод драйвера fasync(). Этот метод вызывается свякий раз, когда флаг FASYNC добавляется или убирается из поля filp->f_flags. Таким образом, код драйвера уведомляется об установке или сбросе данного флага, что необходимо для правильного функционирования драйвера. Флаг очищается по умолчанию при открытии файла. Скоро мы рассмотрим стандартную реализацию этого метода в драйвере.
    3. При получении новых данных, сигнал SIGIO должен быть послан всем процессам, которые зарегистрировали асинхронное уведомление для данного файла.

    В то время как реализация первого шага тривиальна — драйвер не должен что-либо делать — другие шаги связаны с обработкой динамических структур данных для поддержки приложений-клиентов, запросивших асинхронное уведомление. Причем, таких приложений может быть сколько угодно. Однако, эти динамические структуры не завися то особенностей конкретного устройства, и ядро предлагает походящую реализацию общего назначение, так что вы не должны переписывать однотипный код для каждого драйвера.

    Общая реализация предлагаемая Linux основана на одной структуре данных и двух функциях, которые вызываются на втором и третьем шагах, описанных выше. В заголовочном файле
    , содержатся необходимые описания. Интересующая нас структура данных носит название facync_struct. Так же, как мы работали с очередями ожидания, нам необходимо вставить указатель на эту структуру в структуру данных специфическую для устройства. В действительности, мы уже видели такое поле в разделе «Простая реализация scullpipe»

    Описанные выше функции, вызываемые драйвером, соответствуют следующим прототипам:

    Функция fasync_helper() вызывается для добавления или извлечения файлов из списка, который интересен процессам при установке или сбросе флага FASYNC для открытого файла. Все аргументы этой функции, за исключением последнего, предоставляются для передачи в метод fasync() и могут быть переданы в него напрямую. Функция kill_async() используется для передачи сигнала списку интересующихся процессов при получении новых данных. Вторым аргументом идентифицируется передаваемый сигнал (обычно SIGIO). Третий аргумент определяет способ передачи (band), который, как правило, равен POLL_IN, но который может быть использован для указания срочности («urgent») или отложенности (out-of-band) передачи данных в сетевом коде.

    Рассмотрим реализацию метода fasync() в драйвере scullpipe:

    Ясно, что вся работа выполняется в вызове fasync_helper(). Однако, реализовать в fasync_helper() всю функциональность было бы невозможно, потому что данная функция требует корректного указателя на структуру fasync_struct *, представленную в данном примере как &dev->async_queue. Данный указатель может быть предоставлен только каждым конкретным драйвером устройства.

    При появлении новых данных должен быть вызван следующий оператор для передачи асинхронного сигнала читающему процессу. Так как новые данные для процесса, читающего драйвер scullpipe, передаются из процесса записывающего в этот драйвер, то данный оператор размещают в методе write() драйвера scullpipe.

    В описании данного механизма мы опустили еще одну деталь. Мы должны вызвать наш метод fasync() при закрытии файла для удаления файла из списка активных процессов ждущих асинронное уведомление на чтение данного файла. И хотя этот вызов требуется только в случае, если поле filp->f_flags имеет установленным бит FASYNC, но не будет никакого вреда, если вызов fasync() будет произведен в любом случае, что обычно и делается. Следующие строки, например, являются частью метода close() для драйвера scullpipe:

    Структура данных, лежащая в основе операций асинхронного уведомления во многом идентична структуре wait_queue, потому что обе ситуации связаны с ожиданием некоторого события. Различие состоит в том, что структура file используется вместо структуры task_struct. Структура file используется для получения f_owner определяющей процес, которому необходимо передать уведомляющий сигнал.

    Seeking a Device — перемещение по данным в устройстве

    Сложная часть данной главы закончилась. Теперь мы рассмотрим метод llseek(), который полезен для получения общей файловой картины устройства и, к тому же, прост в реализации.

    Реализация метода llseek()

    На методе llseek() построена реализация двух системных вызовов: llseek() и lseek(). Мы уже говорили, что если метод llseek() не реализован в драйвере, то реализация по умолчанию, используемая в этом случае ядром, выполняет перемещение от начала в файла в нужную позицию простым изменением поля filp->f_pos — текущей позицией чтения/записи в файле. Заметьте, что для правильной работы системного вызова lseek() необходимо, чтобы методы read() и write() корректно изменяли текущее значение смещения в файле, которое они принимают в качестве аргумента — этот аргумент обычно является указателем на filp->f_pos.

    Вам может понадобиться собственная реализация метода llseek() для вашего драйвера, если операция перемещения по файлу устройства связана с некой реальной физической операцией над устройством, или, если перемещение за пределы конца файла, которое не учтено в методе по умолчанию, может оказаться для вашего устройства нежелательным. Простой пример реализации данного метода можно увидеть в драйвере scull:

    Единственной устройство-специфической операцией в данном коде является получение текущего размера файла устройства. В драйвере scull работа методов read() и write() взаимосвязана, и показана в разделе «Методы read() и write()» главы 3 «Драйверы символьных устройств».

    Показанная только что реализация имеет смысл для драйвера scull, который управляет простой областью данных из оперативной памяти. Однако, большая часть драйверов управляют, скорее, потоком данных. Представьте себе последовательный порт или клавиатуру. Для таких устройств, операция перемещения файлового указателя не имеет смысла. В этом случае вам также потребуется определение собственного метода llseek() в драйвере, так как функция предоставляемая ядром по умолчанию позволяет перемещение указателя. Код, запрещающий перемещение, будет выглядеть следующим образом:

    Именно эта функция используется в драйвере scullpipe, который управляет областью данных, произвольное перемещение по которой невозможно. Код возвращаемой этим методом ошибки воспринимается как «illegal seek» (перемещение невозможно), хотя символическое определение идентификатора ошибки происходит от «is a pipe». Позиция файлового указателя для таких устройств бессмыслена, и ни метод read() ни метод write() не утруждают себя его изменением при передаче данных.

    Интересно заметить, что с тех пор, как в множество поддерживаемых системных вызовов были добавлены вызовы pread() и pwrite(), то вызов lseek() перестал быть единственным способом, которым может воспользоваться программа пользователя для произвольного перемещения файлового указателя. Правильная реализация драйвера устройства с неперемещаемым файловым указателем должна обрабатывать, по возможности, нормальные операции read() и write(), запрещая, при этом, операции pread() и pwrite(). При этом, первой строкой в методах read() и write() должна быть следующая стока, которая не была сразу объяснена при ознакомлении с этими методами для scullpipe:

    Дополнительно управление доступом к файлу устройства

    Управление доступом для некоторых устройств может оказаться жизнено важной задачей. Использование устройства должно быть запрещено неавторизованным пользователям (такие ограничения накладываются битовой маской разрешений доступа предоставляемой файловой системой), и, кроме того, доступ должен быть ограничен только одним из авторизованных пользователей за раз (в данный момент времен).

    Проблема схожа с использованием tty-устройств. В случае с tty, процесс входа пользователя в систему изменяет владельца устройства для того, чтобы предотвратить взаимовлияние или слежение за течением данных в tty. Однако, использование привилегированной программы для изменение владельца устройства при каждом его открытии непрактично, за исключением случая получения уникального доступа к нему.

    Ни один из рассмотренных выше примеров не реализовывал дополнительное управление доступом к устройству кроме того, что определяла битовая маска разрешений, предоставляемая файловой системой. Если системный вызов open() перенаправлял запрос к драйверу, то операция завершалась успешно. Теперь, мы рассмотрим несколько техник, позволяющих реализовать дополнительные проверки для разрешения доступа к устройству.

    Каждое из устройств, описанных в этом разделе имеет схожее поведение с основополагающим устройством scull, т.е. неким устройством общего применения, просто реализующем распределение памяти. Разница заключается только в дополнительном управлении доступом, реализованном в методах open() и close().


    Устройства single-open — одно открытие за раз

    Один из грубых способов дополнительного управления доступом к устройству состоит в запрещении его открытия в случае, если оно уже открыто одним из процессов. Такие устройства носят название single-open devices. Следует избегать такой техники, так как она тормозит активность пользователя. Возможно, пользователь захочет запустить два различных процесса, один из которых пишет, другой читает одно и то же устройство. Например, одни процесс пишет данные, а другой читает статус-информацию о состоянии устройства. В некоторых случаях, пользователь может разрешить такую задачу с помощью shell-скрипта, запуская требуемые процессы поочередно. Другими словами, реализация такого single-open поведения приводит к созданию политики, которая может определить способ получения того, что требуется пользователю устройства.

    Устройства позволяющие одновременное открытие только одним процессом имеют нежелательные свойства, но легки в реализации ограничения доступа. Продемонстрируем данную реализацию на примере методов open() и close() для драйвера scullsingle.

    Системный вызов open() ограничивает доступ на основе глобального флага занятости устройства:

    С другой стороны, системный вызов close() снимает флаг занятости с устройства.

    Обычно, мы рекомендуем размещеть флаг открытия scull_s_count устройства внутри структуры устройства, которая в данном коде представлена структурой Scull_Dev. Такой подход концептуален, так как данная структура принадлежит устройству. Такой же подход к размещению рекомендуется и для флага scull_s_lock, который будет объяснен в следующем подразделе. Однако, драйвер scull использует для этих целей отдельные глобальные переменные, что позволяет минимизировать повторяющийся код для разных драйверов семейства scull.

    Отступление в проблему Race Conditions (борьба за ресурсы)

    Рассмотрим внимательнее только что показанную обработку переменной scull_s_count. Здесь производится два различных действия: (1) — проверяется значение переменной и операция открытия запрещается, если ее значение не равно 0, и (2) — значение переменной инкрементируется для указания занятости устройства. На однопроцессорной системе такая проверка безопасна, так как другие процессы не могут быть запущены между выполнением этих двух операций.

    Проблема возникает в мире систем SMP. Если два процесса на двух разных процессорах пытаются одновременно открыть устройство, то возможно, что они оба проверят значение scull_s_count перед тем, как оно будет изменено. При этом, в лучшем случае для вашего устройства не сработает концепция single-open, а в худшем — неожидаемый одновременный доступ может разрушить структуру данных, что может привести к краху системы.

    Другими словами — налицо проблема Race Conditions (одновременного доступа к ресурсам). Подробно данная проблема была рассмотрена в главе 3 «Драйвера символьных устройств», где было показано что защита разделяемой структуры данных может быть реализована с помощью семафоров. В общем случае, семафоры могут оказаться слишком дорогим решением, потому что могут привести переводу вызывающего процесса в спящее состояние. Имеется более дешевое решение этой проблемы, состоящее в быстрой проверке переменной статуса.

    Вместо этого, драйвер scullsingle использует другой механизм блокировки, называемый спинлоком (spinlock). Данный механизм не приведет к погружению процесса в сон. Вместо этого, если блокировка не доступна, то код спинлока будет просто повторен снова и снова, пока блокировка не освободится. Отсюда и название — одним из значений английского слова «spin» является значение «волчок». Таким образом, накладные расходы на такой способ блокировки невелики, но потенциально они могут привести к пустой и длительной загрузке процессора в цикле ожидания, если блокировка уже запущена на другом процессоре. Другим преимуществом спинлоков перед семаформами является тот факт, что на однопроцессорной системе, где подобная проблема просто не может возникнуть, можно воспользоваться директивами компиляции для устранения спинлоков, хотя перерасход на обработку спинлока в однопроцессорной системе будет совсем незначителен. Семафоры используются для более глобальной защиты ресурсов, которая необходима даже на однопроцессорной системе.

    Спинлоки могут быть идеальным механизмом для защиты небольших критически важных элементов данных. Процессы должны удерживать спинлоки на минимально возможное время, и никогда не должны уходить в сон во время удержания такой блокировки. Таким образом, основной драйвер scull, который обменивается данными с пользовательским пространством, и может, поэтому, запросто заснуть, не является подходящим местом для использования спинлоков. Но, с другой стороны, спинлоки замечательно работают для реализации механизма ограничения доступа в драйвере scull_s_single, хотя в главе 9 «Обработка прерываний» мы рассмотрим, возможно, более оптимальное решение для этой проблемы.

    Спинлоки описываются типом spinlock_t, который определен в заголовочном файле
    . Перед использованием, спинлоки должны быть инициализированы:

    Начало критической секции кода должно быть предварено блокировкой с помощью функции spin_lock():

    Освобождение блокировки реализуется с помощью функции spin_unlock():

    Спинлоки могут быть более сложными, чем в рассмотреном нами случае. Такие варианты использования спинлоков будут нами детально рассмотрены в главе 9 «Обработка прерываний». Однако, в рассмотренном примере, простой вариант использования спинлоков достаточен, и во всех наших примерах по дополнительному управлению доступом мы будем использовать спинлоки именно таким простым способом.

    Внимательный читатель может заметить, что использование scull_s_count предохраняется спинлоком только при открытии драйвера, т.е в функции scull_s_open(). При закрытии драйвера, в методе scull_s_close() такая предосторожность не используется, так как этот код совершенно безопасен по причине того, что не может существовать второго процесса уменьшающего значение флага scull_s_count.

    Ограничение доступа по принципу «Один пользователь за раз»

    Следующий способ ограничения доступа, который мы сейчас рассмотрим, позволяют одному пользователю открывать устройство много раз из разных процессов, но устройство может быть открыто только одним пользователем за раз. Т.е. до тех пор пока один пользователь использует устройство хотя бы в одном из своих процессов, любой другой пользователь не сможет получить доступ к этому устройству. Такое устройство может быть легко протестировано, так как пользователь может читать и писать данные в устройство одновременно из нескольких процессов. Единственное, нужно понимать, что пользователь несет некоторую ответственность за целостность данных при организации множественного доступа к устройству. Такое управление доступом реализуется добавлением специальной проверки в метод open() драйвера. Понятно, что такая проверка будет выполнена после всех проверок выполняемых файловой системой на основе битов разрешения доступа к файлу устройства. Именно такая политика доступа используется для устройств tty, но реализуется не за счет обращения к внешним привелигированным программам.

    Такая политика доступа несколько сложнее в реализации, нежели политика single-open устройства. В этом случае нам необходимо слежение как за счетчиком использования модуля, так и за uid («user ID» — идентификатор пользователя) владельца («owner») устройства. Как уже говорилось раньше, лучшим местом для хранения такой информации является структура устройства, однако для минимизации повторяемого кода в наших примерах мы будем использовать для ее хранения глобальную переменную. Обсуждаемое здесь устройство называется sculluid.

    Вызов open() разрешает доступ при первом открытии файла, и запоминает владельца устройства. Теперь этот пользователь может открыть данное устройство произвольное количество раз, используя разные процессы для параллельного доступа к этому устройству. В то же самое время, никакой другой пользователь не сможет открыть данное устройство. Вариант метода open() для данного случая во многом похож на тот код, который был приведен ранее, для устройства с политикой single-open, поэтому здесь мы приведем только измененную часть кода, а не весь метод целиком:

    В качестве кода возврата при невыполнении приведенного условия мы используем значение -EBUSY, а не -EPERM. Это подчеркивает тот факт, что пользователь имеет право доступа к устройству, но на данный момент оно занято. «Permition denied» используется, обычно, как ошибка при проверке прав доступа к /dev-файлу устройства, в то время как «Device busy» корректно отображает тот факт, что устройство занято другим пользователем.

    Приведенный код проверяет не только идентификатор пользователя (uid), но и эффективный идентификатор пользователя (euid), что позволяет открывать устройство процессам использующим требуемую замену идентификатора пользователя. Проверка возможности перекрытия прав доступа к файлам и каталогам (CAP_DAC_OVERRIDE), дополнительно позволяет ослабить жесткую политику доступа к устройству.

    Код метода close() не показан для этого случая, потому что все, что он делает сводится к декрементированию счетчика открытия файла.

    Блокировка метода open() как альтернатива EBUSY

    В случае, когда доступ к устройству запрещен, наиблее осмысленным является возврат ошибки занятости устройства, но возможны ситуации, когда вы можете предпочесть ожидание устройства.

    Например, если коммуникационный канал передачи данных используется одновремено как для периодической посылки отчетов, согласно диспетчеру crontab, и для случайного использования пользователем, то, наверное, при занятости канала, имеет смысл немного задержать передачу периодического отчета, нежели пропустить его вовсе.

    Вообще, правильный выбор сильно зависит от назначения устройства, и определяется на этапе разработки его драйвера.

    Альтернативой передачи значения занятости (-EBUSY), как вы уже догадались, является блокировка метода open().

    Устройство scullwuid представляет собой вариант sculluid, который ожидает устройство при открытии, вместо того, чтобы возвратить код занятости (-EBUSY). Отличие кода заключено только в следующей части:

    Как вы могли догадаться, реализация снова построена на использовании очереди ожидания. Очереди ожидания создаются для составления списка процессов, которые должны спать при ожидании некоторого события.

    Метод release() драйвера, выполняет пробуждение всех, еще не проснувшихся, процессов:

    Проблема реализации блокировки в методе open() состоит в том, что пользователь должен учитывать предположение о том, что происходит все не совсем гладко. Взаимодействие пользователя с драйвером может быть построено на использовании готовых команд, таких как cp и tar, которые не могут добавить флаг O_NONBLOCK при вызове open(). Кто-то, кто выполняет резервное копирование на накопитель на магнитной ленте, стоящий в другой комнате, предпочтет получить сообщение «device or resource busy», вместо того, чтобы ломать голову над тем, почему безмолствует жесткий диск при выполнении резервного копирования.

    Этот сорт проблем, связанный с различием фукнциональности и политики для одного и того же устройства, лучшим образом решается созданием множества файловых интерфейсов, реализующих различную политику для одного устройства. В качестве примера такой практики можно привести реализацию драйвера для накопителя на магнитной ленте в Linux, который предоставляет множество файловых интерфейсов для одного и того же устройства. Различные файловые интерфейсы позволяют, в последнем случае, производить запись с компрессией, или без компрессии данных, или использовать автоматическую перемотку при закрытии устройства.

    Клонирование устройства в методе open()

    Другой техникой управления уточненным доступом состоит в создании различных копий устройства, в зависимости от процесса, который его открывает.

    Ясно, что реализация такой техники возможна только в том случае, если устройство не зажато рамками физического устройства. Так, устройство scull, как раз является отличным примером такого «программного» устройства. Внутренняя реализация /dev/tty использует похожую технику, для того чтобы предоставить процессам различную точку зрения на элемент /dev-интерфейса. Когда копии устройства создается программным драйвером, мы называем из виртуальными устройствами — как виртуальные консоли, использующие одно физическое устройство tty.

    Хотя необходимость в таком способе управления доступом достаточно редка, рассмотрение его реализации хорошо демонстрирует факт того, как код ядра может изменить точку зрения приложения на окружающий мир (т.е. на компьютер). Данная тема достаточно экзотична, поэтому, если она вам не интересна, то вы можете смело приступить к изучению следующего раздела.

    Файловый интерфейс /dev/scullpriv реализует виртуальное устройство из пакета scull. Реализация scullpriv использует младший номер процесса, управляющего tty, как ключ доступа к виртуальному устройству. Конечно, вы можете легко изменить код источника драйвера для использования любого другого целого значения в качестве ключа. Каждый способ определения ключа является следствием различной политики. Например, использование в качестве ключа идентификатора пользователя (uid) приведет к различным виртуальным устройствам для каждого пользователя, в то время как использование в качестве ключа идентификатора процесса (pid) позволит создать для каждого процесса свое виртуальное устройство.

    Использование в качестве ключа код управляющего терминала позволит упростить процесс тестирования устройства используя обычное перенаправление ввода/вывода. Устройство будет разделяться между всеми комаднами запущенными на одном виртуальном терминале, и закроет доступ для команд запущенных на другом терминале.

    Приведем код метода open(). Он должен найти соответсвующее виртуальное устройство, и, возможно, создать его. Заключительная часть функции не показана, потому что она совпадает с кодом общим для основного драйвера scull, который уже был приведен выше.

    Метод release() не делает ничего особенного. Мы бы могли просто «уничтожить» или «освободить» устройство при закрытии последнего, связанного с ним файлового интерфейса, но в данном коде мы не поддержали счетчик открытия устройства для того, чтобы упростить его тестирование. Если устройство будет освобождаться по закрытию последнего файлового интерфейска, связанного с устройством, то вы не сможете прочитать теже самые данные, которые были записаны в устройство, до тех пор пока вы не будете держать некий фоновый процесс, который будет удерживать его открытым на время записи с последующим чтением. Поэтому, данный пример драйвера просто удерживает данные, так, что при следующем открытии устройства вы сможете найти в нем последние данные. Устройство освобождается при вызове scull_cleanup().

    Приведем реализацию метода release() для /dev/scullpriv, который закрывает обсуждение методов устройства в данной книге.

    Вопросы обратной совместимости

    В процессе развития ядра, с изменением номера его ветки, могут измениться некоторые элементы набора API ядра. Поэтому, если вы заботитесь о том чтобы ваш драйвер написанный для ядра 2.4 мог бы исполняться и в ветках 2.2 и 2.0, то в этом разделе вы сможете найти необходимую информацию по вопросам совместимости для тех элементов, которые обсуждались в этой главе.

    Очереди ожидания в Linux 2.2 и 2.0

    Относительно небольшая часть материала этой главы подверглась изменениям на этапе разработки ядра 2.3. Значительное изменение касается очередей ожидания. Ядро 2.2 имело другую и значительно более простую реализацию очередей ожидания, но в ней не хватало некоторых важных характеристик, таких как возможности эксклюзивного сна. Новая реализация очередей ожидания была представлена в ядре 2.3.1.

    Реализация очереди ожидания в ядре 2.2 использовала переменные типа struct wait_queue *, вместо wait_queue_head_t. Этот указатель должен был быть проинициализирован в NULL перед первым использованием. Типичное описание и инициализация очереди ожидания выглядела следующим образом:

    Различные функции для перевода в сон и пробуждения выглядят аналогично, за исключением типа переменной для указания самой очереди ожидания. В результате, написания кода, который бы мог работать как на ядре 2.4, так и на ядрах 2.2 и 2.0, можно упростить, если использовать следующий кусок кода, который представлен в нашем заголовочном файле sysdep.h, и используемый нами для компиляции наших примеров.

    Синхронные версии для набора функций wake_up() были добавлены в ядре 2.3.29, поэтому в заголовочном файле sysdep.h мы предоставили макросы с этими именами, которые вы можете использовать для поддержки этой функциональности в портируемом коде. Эти макросы расширяются в обычные вызовы wake_up(), так как в ранних версиях ядра отсутствовали другие механизмы. Версии sleep_on() с поддержкой таймаута были добавлены в ядро на момент выпуска версии 2.1.127. Другие элементы интерфейса очереди ожидания остались практически без изменений. Предоставленный нами заголовочный файл sysdep.h определяет необходимые макросы, для того, чтобы вы могли компилировать и запускать ваши модули на ядрах 2.4 2.2 и 2.0 не загрязняя код модуля множеством директив условной компиляции.

    Макро wait_event() не был представлен в ядре 2.0, поэтому, для тех кому это может понадобиться, мы представили его реализацию в заголовочном файле sysdep.h.

    Асинхронные уведомления

    В работе асинхронных уведомлений были сделаны небольшие изменения в версиях ядра 2.2 и 2.4.

    В ядре 2.3.21, функция kill_fasync() получила свой третий аргумент. До этого, функция kill_fasync() имела следующую сигнатуру вызова:

    Предоставляемый нами заголовочный файл sysdep.h позаботился об этом изменении.

    В ядре 2.2 изменился тип первого аргумента для метода fasync(). В ядре 2.0, вместо целого значения файлового дескриптора передавался указатель на структуру inode:

    Для решения этой проблемы несовместимости мы используем тот же самый прием, что и для методов read() и write(): использование оберточной функции, при компиляции для ядра 2.0.

    To solve this incompatibility, we use the same approach taken for read and write: use of a wrapper function when the module is compiled under 2.0 headers.

    Аргумент inode также передается в метод fasync(), когда он вызывается из метода release(). Это предпочтительнее передачи значения -1, используемое в более поздних версиях ядра. Примечание переводчика: попробуйте сами прочитать последнее предложение — «. The inode argument to the fasync method was also passed in when called from the release method, rather than the -1 value used with later kernels.»

    Метод fsync()

    Третий аргумент (целое значение datasync) был добавлен в метод fsync() при разработке серии ядра 2.3, означая, что для написания портируемого кода нам необходимо написать функцию-обертку для старых версия ядра. Однако, разработчики, которые попытаются написать портируемый код для метода fsync(), могут попасться в ловушку: по меньшей пере один из дистрибьютеров, имя которого нам не известно, пропатчил свое ядро 2.2 fsync()-API из ядра 2.4. Разработчики ядра обычно пытаются избежать изменений API в стабильных ветках ядра, но они не могут проконтролировать действия дистрибьютеров.

    Доступ к пространству пользователя в ядре Linux 2.0

    Способ управления памятью имел отличия в ядрах 2.0. Система управления виртуальной памятью уже была реализована в Linux к тому времени, но само управление памятью несколько отличалось. Ключевые изменения в системе управления памятью произошли во время открытия ветки ядра 2.1, что принесло значительное улучшение производительности, но, к несчастью, сопровождалось множеством проблем совместимости, которые добавили головной боли разработчикам драйверов.

    Функции, используемые для доступа к памяти в ядре 2.0 выглядели следующим образом:

    verify_area(int mode, const void *ptr, unsigned long size); Работа этой функции схоже с работой access_ok(), но выполняла более обширные проверки и работала медленнее. Функция возвращает 0 в случае успеха, и -EFAULT при обнаружении ошибки. Еще в недавних версиях заголовков ядра данная функция была определена, но уже являлась оберткой над access_ok(). При работе в ядре 2.0, вызов verify_area() не был опциональным (необязательным). Безопасный доступ к пространству пользователя не мог быть произведен без явной, предварительной проверки указателя. put_user(datum, ptr) Макро put_user() выглядит во многом похоже на его современный эквивалент. Различие заключалось в том, что не производилась проверка адреса, и не было возвращаемого значения. get_user(ptr) Это макро получало значение по заданному адресу, и отдавало его в качестве возвращаемого значения. Проверки корректности передаваемого в макро указателя не производилось.

    Вызов фукции verify_area() должен был вызываться явно, потому что функции копирования в/из пространства пользователя не выполняли такой проверки. Однако, ядро 2.1, к счастью, форсировала проблемы несовместимости для функций get_user() и put_user(), тем, что задачу проверки адресов пользовательского пространства оставила процессору и его окружению, потому что ядро потому, что ядро было не готово управлять ловушками и процессорными исключениями, генерируемыми во время копирования данных в пространство пользователя.

    В качестве примера использования старых вызовов, рассмотрим еще раз код драйвера scull. Версия scull использующая API ядра 2.0 может вызывать verify_area() следующим способом:

    Теперь, можно использовать get_user() и put_user() следующим образом:

    Здесь показана только маленькая часть кода оператора switch из метода ioctl(), т.к. отличие между версиями 2.2 и более поздними небольшие.

    Жизнь разработчиков драйверов была бы относительно проще при решении вопросов совместимости, если бы put_user() и get_user() не были бы реализованы как макросы во всех версиях ядра Linux, и их интерфейс не был бы изменен. В результате, прямое решение с использованием макросов не может быть реализовано.

    Одно из решений состоит в определении нового множества версии-независимых макросов. Так, в нашем заголовочном файле sysdep.h определены макросы с использованием в имени символов верхнего регистра, т.е. GET_USER(), __GET_USER() и пр. Аргументы совпадают с аргументами для макросов, определенных в ядре 2.4, но вызывающие программы должны предварительно использовать проверку адреса с помощью verify_area(), как это требуется в ядре 2.0.

    «Мандаты» (Capabilities) в ядре 2.0

    Ядро 2.0 не поддерживает концепцию мандатов (capabilities) вообще. Вся проверка прав выглядела достаточно просто — суперпользователю разрешены все операции. Для этой цели использовалась функция suser(). Она не имела аргументов, и возвращала ненулевое значение если процесс имел привилегии суперпользователя.

    Функция suser() существует в последующих версиях ядер, но ее использование выглядет несколько необычно. Лучшим решением для данной проблемы совместимости является определение для ядра 2.0 макроса capable(), что мы и сделали в нашем заголовочном файле sysdep.h:

    Код, написанный с использованием этого макроса, будет портируемым, и сможет работать на современных, мандатно-ориентированных системах.

    Метод select() в ядре Linux 2.0

    Ядро 2.0 не поддерживает системный вызов poll(), и доступен только метод select() в стиле BSD. Поэтому, соответсвующий метод драйвера назывался select(), и работал несколько иначе, хотя выполняемые действия были почти идентичны.

    В метод select передается указатель на select_table, который должен быть передан в select_wait(), в случае, если вызывающий процесс должен дождаться одного из затребованных условий SEL_IN, SEL_OUT или SEL_EX.

    Драйвер scull решает данную несовместимость объявлением специального метода select(), который используется при компиляции для ядра 2.0:

    Используемый здесь препроцессорный символ __USE_OLD_SELECT__ устанавливается в нашем заголовочном файле sysdep.h согласно версии ядра.

    Перемещение по файлу в ядре Linux 2.0

    До версии ядра 2.1 метод llseek() назывался lseek() и имел другие аргументы, в отличии от текущей реализации. По этой причине, работая под ядром 2.0 мы не могли перемещаться по файлу размером более 2 ГБт, несмотря на то, что системный вызов llseek() уже поддерживался.

    Прототип этой файловой операции в ядре 2.0 имел следующий вид:

    Поэтому решение данного вопроса совместимости для ядер 2.0 и 2.2 обычно заключается в определении различных реализаций метода перемещения по файлу для этих двух интерфейсов.

    Ядро 2.0 и SMP

    Ядро Linux 2.0 практически не поддерживала SMP системы, поэтому вопросы борьбы за ресурсы (race conditions), упомянутые в данной главе, обычно не актуальны для этой версии ядра. Так, ядро 2.0 не имело реализации spinlock, но так как, на данном ядре в данный момент времени только один процессор обрабатывал код ядра, то необходимости в такой блокировке не существовало.

    Краткий справочник определений

    Рассмотрим вкратце заголовочные файлы и символы представленные в данной главе.

    indbooks

    Читать онлайн книгу

    Глава 17. Операции функции ioctl

    17.1. Введение

    Функция ioctl традиционно являлась системным интерфейсом, используемым для всего, что не входило в какую-либо другую четко определенную категорию. POSIX постепенно избавляется от функции ioctl , создавая заменяющие ее функции-обертки и стандартизуя их функциональность. Например, доступ к интерфейсу терминала Unix традиционно осуществлялся с помощью функции ioctl , но в POSIX были созданы 12 новых функций для терминалов: tcgetattr для получения атрибутов терминала, tcflush для опустошения буферов ввода или вывода, и т.д. Аналогичным образом POSIX заменяет одну сетевую функцию ioctl : новая функция sockatmark (см. раздел 24.3) заменяет команду SIOCATMARK ioctl . Тем не менее прочие сетевые команды ioctl остаются не стандартизованными и могут использоваться, например, для получения информации об интерфейсе и обращения к таблице маршрутизации и кэшу ARP (Address Resolution Protocol — протокол разрешения адресов).

    В этой главе представлен обзор команд функции ioctl , имеющих отношение к сетевому программированию, многие из которых зависят от реализации. Кроме того, некоторые реализации, включая системы, происходящие от 4.4BSD и Solaris 2.6, используют сокеты домена AF_ROUTE (маршрутизирующие сокеты) для выполнения многих из этих операций. Маршрутизирующие сокеты мы рассматриваем в главе 18.

    Обычно сетевые программы (как правило, серверы) используют функцию ioctl для получения информации обо всех интерфейсах узла при запуске программы, с тем чтобы узнать адрес интерфейса, выяснить, поддерживает ли интерфейс широковещательную передачу, многоадресную передачу и т.д. Для возвращения этой информации мы разработали нашу собственную функцию. В этой главе мы представляем ее реализацию с применением функции ioctl , а в главе 18 — другую реализацию, использующую маршрутизирующие сокеты.

    17.2. Функция ioctl

    Эта функция работает с открытым файлом, дескриптор которого передается через аргумент fd .

    int ioctl(int fd , int request , . /* void * arg */ );

    Возвращает: 0 в случае успешного выполнения, -1 в случае ошибки

    Третий аргумент всегда является указателем, но тип указателя зависит от аргумента request .

    ПРИМЕЧАНИЕ

    В 4.4BSD второй аргумент имеет тип unsigned long вместо int, но это не вызывает проблем, поскольку в заголовочных файлах определены константы, используемые для данного аргумента. Пока прототип функции подключен к программе, система будет обеспечивать правильную типизацию.

    Некоторые реализации определяют третий аргумент как неопределенный указатель (void*), а не так, как он определен в ANSI С.

    Не существует единого стандарта заголовочного файла, определяющего прототип функции для ioctl, поскольку он не стандартизован в POSIX. Многие системы определяют этот прототип в файле , как это показываем мы, но традиционные системы BSD определяют его в заголовочном файле .

    Мы можем разделить аргументы request , имеющие отношение к сети, на шесть категорий:

    в– операции с сокетами;

    в– операции с файлами;

    в– операции с интерфейсами;


    в– операции с кэшем ARP;

    в– операции с таблицей маршрутизации;

    в– операции с потоками (см. главу 31).

    Помимо того, что, как показывает табл. 7.9, некоторые операции ioctl перекрывают часть операций fcntl (например, установка неблокируемого сокета), существуют также некоторые операции, которые с помощью функции ioctl можно задать более чем одним способом (например, смена групповой принадлежности сокета).

    В табл. 17.1 перечислены аргументы request вместе с типами данных, на которые должен указывать адрес arg . В последующих разделах эти вызовы рассматриваются более подробно.

    Таблица 17.1. Обзор сетевых вызовов ioctl

    Категория request Описание Тип данных
    Сокет SIOCATMARK Находится ли указатель чтения сокета на отметке внеполосных данных int
    SIOCSPGRP Установка идентификатора процесса или идентификатора группы процессов для сокета int
    SIOCGPGRP Получение идентификатора процесса или идентификатора группы процессов для сокета int
    Файл FIONBIO Установка/сброс флага отсутствия блокировки int
    FIOASYNC Установка/сброс флага асинхронного ввода-вывода int
    FIONREAD Получение количества байтов в приемном буфере int
    FIOSETOWN Установка идентификатора процесса или идентификатора группы процессов для файла int
    FIOGETOWN Получение идентификатора процесса или идентификатора группы процессов для файла int
    Интерфейс SIOCGIFCONF Получение списка всех интерфейсов struct ifconf
    SIOCSIFADDR Установка адреса интерфейса struct ifreq
    SIOCGIFADDR Получение адреса интерфейса struct ifreq
    SIOCSIFFLAGS Установка флагов интерфейса struct ifreq
    SIOCGIFFLAGS Получение флагов интерфейса struct ifreq
    SIOCSIFDSTADDR Установка адреса типа «точка-точка» struct ifreq
    SIOCGIFDSTADDR Получение адреса типа «точка-точка» struct ifreq
    SIOCGIFBRDADDR Получение широковещательного адреса struct ifreq
    SIOCSIFBRDADDR Установка широковещательного адреса struct ifreq
    SIOCGIFNETMASK Получение маски подсети struct ifreq
    SIOCSIFNETMASK Установка маски подсети struct ifreq
    SIOCGIFMETRIC Получение метрики интерфейса struct ifreq
    SIOCSIFMETRIC Установка метрики интерфейса struct ifreq
    SIOC xxx (Множество вариантов в зависимости от реализации)
    ARP SIOCSARP Создание/модификация элемента ARP struct arpreq
    SIOCGARP Получение элемента ARP struct arpreq
    SIOCDARP Удаление элемента ARP struct arpreq
    Маршрутизация SIOCADDRT Добавление маршрута struct rtentry
    SIOCDELRT Удаление маршрута struct rtentry
    Потоки I_ xxx (См. раздел 31.5)
    17.3. Операции с сокетами

    Существует три типа вызова, или запроса (в зависимости от значения аргумента request ) функции ioctl , предназначенные специально для сокетов [128, с. 551–553]. Все они требуют, чтобы третий аргумент функции ioctl был указателем на целое число.

    в– SIOCATMARK . Возвращает указатель на ненулевое значение в качестве третьего аргумента (его тип, как только что было сказано, — указатель на целое число), если указатель чтения сокета в настоящий момент находится на отметке внеполосных данных (out-of-band mark), или указатель на нулевое значение, если указатель чтения сокета не находится на этой отметке. Более подробно внеполосные данные (out-of-band data) рассматриваются в главе 24. POSIX заменяет этот вызов функцией sockatmark , и мы рассматриваем реализацию этой новой функции с использованием функции ioctl в разделе 24.3.

    в– SIOCGRP . Возвращает в качестве третьего аргумента указатель на целое число — идентификатор процесса или группы процессов, которым будут посылаться сигналы SIGIO или SIGURG по окончании выполнения асинхронной операции или при появлении срочных данных. Этот вызов идентичен вызову F_GETOWN функции fcntl , и в табл. 7.9 мы отмечали, что POSIX стандартизирует функцию fcntl .

    в– SIOCSPGRP . Задает идентификатор процесса или группы процессов для отсылки им сигналов SIGIO или SIGURG как целое число, на которое указывает третий аргумент. Этот вызов идентичен вызову F_SETOWN функции fcntl , и в табл. 7.9 мы отмечали, что POSIX стандартизирует функцию fcntl .

    17.4. Операции с файлами

    Следующая группа вызовов начинается с FIO и может применяться к определенным типам файлов в дополнение к сокетам. Мы рассматриваем только вызовы, применимые к сокетам [128, с. 553].

    Следующие пять вызовов требуют, чтобы третий аргумент функции ioctl указывал на целое число.

    в– FIONBIO . Флаг отключения блокировки при выполнении операций ввода-вывода сбрасывается или устанавливается в зависимости от третьего аргумента функции ioctl . Если этот аргумент является пустым указателем, то флаг сбрасывается (блокировка разрешена). Если же третий аргумент является указателем на единицу, то включается неблокируемый ввод-вывод. Этот вызов обладает тем же действием, что и команда F_SETFL функции fcntl , которая позволяет установить или сбросить флаг O_NONBLOCK , задающий статус файла.

    в– FIOASYNC . Флаг, управляющий получением сигналов асинхронного ввода-вывода ( SIGIO ), устанавливается или сбрасывается для сокета в зависимости от того, является ли третий аргумент функции ioctl пустым указателем. Этот флаг имеет то же действие, что и флаг статуса файла O_ASYNC , который можно установить и сбросить с помощью команды F_SETFL функции ioctl .

    в– FIONREAD . Возвращает число байтов, в настоящий момент находящихся в приемном буфере сокета, как целое число, на которое указывает третий аргумент функции ioctl . Это свойство работает также для файлов, каналов и терминалов. Более подробно об этом вызове мы рассказывали в разделе 14.7.

    Илон Маск рекомендует:  Что такое код getrvalue

    в– FIOSETOWN . Эквивалент SIOCSPGRP для сокета.

    в– FIOGETOWN . Эквивалент SIOCGPGRP для сокета.

    17.5. Конфигурация интерфейса

    Один из шагов, выполняемых многими программами, работающими с сетевыми интерфейсами системы, — это получение от ядра списка всех интерфейсов, сконфигурированных в системе. Это делается с помощью вызова SIOCGIFCONF , использующего структуру ifconf , которая, в свою очередь, использует структуру ifreq . Обе эти структуры показаны в листинге 17.1 [1] .

    Листинг 17.1. Структуры ifconf и ifreq, используемые в различных вызовах функции ioctl, относящихся к интерфейсам

    int ifc_len; /* размер буфера, «значение-результат» */

    caddr_t ifcu_buf; /* ввод от пользователя к ядру */

    struct ifreq *ifcu_req; /* ядро возвращает пользователю */

    #define ifc_buf ifc_ifcu.ifcu_buf /* адрес буфера */

    #define ifc_req ifc_ifcu.ifcu_req /* массив возвращенных структур */

    #define IFNAMSIZ 16

    char ifr_name[IFNAMSIZ]; /* имя интерфейса, например «le0» */

    struct sockaddr ifru_addr;

    struct sockaddr ifru_dstaddr;

    struct sockaddr ifru_broadaddr;

    #define ifr_addr ifr_ifru.ifru_addr /* адрес */

    #define ifr_dstaddr ifr_ifru.ifru_dstaddr /* другой конец линии передачи, называемой

    #define ifr_broadaddr ifr_ifru.ifru_broadaddr /* широковещательный адрес */

    #define ifr_flags ifr_ifru.ifru_flags /* флаги */

    #define ifr_metric ifr_ifru.ifru_metric /* метрика */

    #define ifr_data ifr_ifru.ifru_data /* с использованием интерфейсом */

    Прежде чем вызвать функцию ioctl , мы выделяем в памяти место для буфера и для структуры ifconf , а затем инициализируем эту структуру. Мы показываем это на рис. 17.1, предполагая, что наш буфер имеет размер 1024 байта. Третий аргумент функции ioctl — это указатель на нашу структуру ifconf .

    Рис. 17.1. Инициализация структуры ifconf перед вызовом SIOCGIFCONF

    Если мы предположим, что ядро возвращает две структуры ifreq , то при завершении функции ioctl мы можем получить ситуацию, представленную на рис. 17.2. Затененные области были изменены функцией ioctl . Буфер заполняется двумя структурами, и элемент ifc_len структуры ifconf обновляется, с тем чтобы соответствовать количеству информации, хранимой в буфере. Предполагается, что на этом рисунке каждая структура ifreq занимает 32 байта.

    Рис. 17.2. Значения, возвращаемые в результате вызова SIOCGIFCONF

    Указатель на структуру ifreq также используется в качестве аргумента оставшихся функций ioctl интерфейса, показанных в табл. 17.1, которые мы описываем в разделе 17.7. Отметим, что каждая структура ifreq содержит объединение ( union ), а директивы компилятора #define позволяют непосредственно обращаться к полям объединения по их именам. Помните о том, что в некоторых системах в объединение ifr_ifru добавлено много зависящих от реализации элементов.

    17.6. Функция get_ifi_info

    Поскольку многим программам нужно знать обо всех интерфейсах системы, мы разработаем нашу собственную функцию get_ifi_info , возвращающую связный список структур — по одной для каждого активного в настоящий момент интерфейса. В этом разделе мы покажем, как эта функция реализуется с помощью вызова SIOCGIFCONF функции ioctl , а в главе 18 мы создадим ее другую версию, использующую маршрутизирующие сокеты.

    ПРИМЕЧАНИЕ

    BSD/OS предоставляет функцию getifaddrs, имеющую аналогичную функциональность.

    Поиск по всему дереву исходного кода BSD/OS 2.1 показывает, что 12 программ выполняют вызов SIOCGIFCONF функции ioctl для определения присутствующих интерфейсов.

    Сначала мы определяем структуру ifi_info в новом заголовочном файле, который называется unpifi.h , показанном в листинге 17.2.

    Листинг 17.2. Заголовочный файл unpifi.h

    1 /* Наш собственный заголовочный файл для программ, которым требуется

    2 информация о конфигурации интерфейса. Включаем его вместо «unp.h». */

    3 #ifndef __unp_ifi_h

    4 #define __unp_ifi_h

    7 #define IFI_NAME 16 /* то же, что и IFNAMSIZ в заголовке */

    8 #define IFI_HADDR 8 /* с учетом 64-битового интерфейса EUI-64 в будущем */

    9 struct ifi_info <

    10 char ifi_name[IFI_NAME]; /* имя интерфейса, заканчивается

    символом конца строки */

    11 short ifi_index; /* индекс интерфейса */

    12 short ifi_mtu; /* MTU для интерфейса */

    13 u_char ifi_haddr[IFI_HADDR]; /* аппаратный адрес */

    14 u_short ifi_hlen; /* количество байтов в аппаратном адресе: 0, 6, 8 */

    15 short ifi_flags; /* константы IFF_xxx из */

    16 short if_myflags; /* наши флаги IFI_xxx */

    17 struct sockaddr *ifi_addr; /* первичный адрес */

    18 struct sockaddr *ifi_brdaddr; /* широковещательный адрес */

    19 struct sockaddr *ifi_dstaddr; /* адрес получателя */

    20 s truct ifi_info *ifi_next; /* следующая из этих структур */

    22 #define IFI_ALIAS 1 /* ifi_addr — это псевдоним */

    23 /* прототипы функций */

    24 struct ifi_info *get_ifi_info((int, int);

    25 struct ifi_info *Get_ifi_info(int, int);

    26 void free_ifi_info(struct ifi_info*);

    27 #endif /* _unp_ifi_h */

    9-21 Связный список этих структур возвращается нашей функцией. Элемент ifi_next каждой структуры указывает на следующую структуру. Мы возвращаем в этой структуре информацию, которая может быть востребована в типичном приложении: имя интерфейса, индекс интерфейса, MTU, аппаратный адрес (например, адрес Ethernet), флаги интерфейса (чтобы позволить приложению определить, поддерживает ли приложение широковещательную или многоадресную передачу и относится ли этот интерфейс к типу «точка-точка»), адрес интерфейса, широковещательный адрес, адрес получателя для связи «точка-точка». Вся память, используемая для хранения структур ifi_info вместе со структурами адреса сокета, содержащимися в них, выделяется динамически. Следовательно, мы также предоставляем функцию free_ifi_info для освобождения всей этой памяти.

    Перед тем как представить реализацию нашей функции ifi_info , мы покажем простую программу, которая вызывает эту функцию и затем выводит информацию. Эта программа, представленная в листинге 17.3, является уменьшенной версией программы ifconfig .

    Листинг 17.3. Программа prifinfo, вызывающая нашу функцию ifi_info

    1 #include «unpifi.h»

    3 main(int argc, char **argv)

    5 struct ifi_info *ifi, *ifihead;

    6 struct sockaddr *sa;

    8 int i, family, doaliases;

    10 err_quit(«usage: prifinfo «);

    11 if (strcmp(argv[1], «inet4») == 0)

    12 family = AF_INET;

    13 else if (strcmp(argv[1], «inet6») == 0)

    14 family = AF_INET6;

    17 doaliases = atoi(argv[2]);

    18 for (ifihead = ifi = Get_ifi_info(family, doaliases);

    19 ifi ! = NULL; ifi = ifi->ifi_next) <

    20 printf(«%s: ifi_name);

    21 if (ifi->ifi_index != 0)

    22 printf(«%d) «, ifi->ifi_index);

    30 if ((i = ifi->ifi_hlen) > 0) <

    31 ptr = ifi->ifi_haddr;

    33 printf(«%s%x», (i == ifi->ifi_hlen) ? » » : «:», *ptr++);

    37 if (ifi->ifi_mtu != 0)

    38 printf(» MTU: %d\n». ifi->ifi_mtu);

    39 if ((sa = ifi->ifi_addr) != NULL)

    40 printf(» IP addr: %s\n», Sock_ntop_host(sa, sizeof(*sa)));

    41 if ((sa = ifi->ifi_brdaddr) != NULL)

    42 printf(» broadcast addr, %s\n»,

    43 Sock_ntop_host(sa, sizeof(*sa)));

    44 if ((sa = ifi->ifi_dstaddr) != NULL)

    45 printf(» destination addr %s\n\»,

    46 Sock_ntop_host(sa, sizeof(*sa)));

    18-47 Программа представляет собой цикл for , в котором один раз вызывается функция get_ifi_info , а затем последовательно перебираются все возвращаемые структуры ifi_info .


    20-36 Выводятся все имена интерфейсов и флаги. Если длина аппаратного адреса больше нуля, он выводится в виде шестнадцатеричного числа (наша функция get_ifi_info возвращает нулевую длину ifi_hlen , если адрес недоступен).

    37-46 Выводится MTU и те IP-адреса, которые были возвращены.

    Если мы запустим эту программу на нашем узле macosx (см. рис. 1.7), то получим следующий результат:

    macosx % prifinfo inet4 0

    IP addr: 172.24.37.78

    broadcast addr: 172.24.37.95

    Первый аргумент командной строки inet4 задает адрес IPv4, а второй, нулевой аргумент указывает, что не должно возвращаться никаких псевдонимов, или альтернативных имен (альтернативные имена IP-адресов мы описываем в разделе А.4). Обратите внимание, что в MacOS X аппаратный адрес интерфейса Ethernet недоступен.

    Если мы добавим к интерфейсу Ethernet ( en1 ) три альтернативных имени адреса с идентификаторами узла 79, 80 и 81 и изменим второй аргумент командной строки на 1, то получим:

    macosx % prifinfo inet4 1

    IP addr: 172.24.37.78 первичный IP-адрес

    broadcast addr: 172.24.37.95

    IP addr: 172.24.37.79 первый псевдоним

    broadcast addr: 172.24.37.95

    IP addr: 172 24.37.80 второй псевдоним

    broadcast addr: 172.24 37.95

    IP addr: 172 24.37.81 третий псевдоним

    broadcast addr: 172.24.37 95

    Если мы запустим ту же программу под FreeBSD, используя реализацию функции get_ifi_info , приведенную в листинге 18.9 (которая может легко получить аппаратный адрес), то получим:

    freebsd4 % prifinfo inet4 1

    IP addr: 135.197.17.100

    broadcast addr: 135.197.17.255

    IP addr: 172.24.37.94 основной IP-адрес

    broadcast addr: 172.24.37.95

    IP addr: 172.24.37.93 псевдоним

    broadcast addr: 172.24.37.93

    В этом примере мы указали программе выводить псевдонимы, и мы видим, что один из псевдонимов определен для второго интерфейса Ethernet ( de1 ) с идентификатором узла 93.

    Теперь мы покажем нашу реализацию функции get_ifi_info , использующую вызов SIOCGIFCONF функции ioctl . В листинге 17.4 показана первая часть этой функции, получающая от ядра конфигурацию интерфейса.

    Листинг 17.4. Выполнение вызова SIOCGIFCONF для получения конфигурации интерфейса

    1 #include «unpifi.h»

    2 struct ifi_info*

    3 get_ifi_info(int family, int doaliases)

    5 struct ifi_info *ifi, *ifihead, **ifipnext;

    6 int sockfd, len, lastlen, flags, myflags, >

    7 char *ptr, *buf, lastname[IFNAMSIZ], *cptr, *haddr, *sdlname;

    8 struct ifconf ifc;

    9 struct ifreq *ifr, ifrcopy;

    10 struct sockaddr_in *sinptr;

    11 struct sockaddr_in6 *sin6ptr;

    12 sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    14 len = 100 * sizeof(struct ifreq); /* начальное приближение к нужному размеру буфера */

    16 buf = Mallос(len);

    17 ifc.ifc_len = len;

    18 ifc.ifc_buf = buf;

    19 if (ioctl(sockfd, SIOCGIFCONF, &ifc)

    20 if (errno != EINVAL || lastlen != 0)

    21 err_sys(«ioctl error»);

    23 if (ifc.ifc_len == lastlen)

    24 break; /* успех, значение len не изменилось */

    25 lastlen = ifc.ifc_len;

    27 len += 10 * sizeof(struct ifreq); /* приращение */

    30 ifihead = NULL;

    32 lastname[0] = 0;

    33 sdlname = NULL;

    Создание сокета Интернета

    11 Мы создаем сокет UDP, который будет использоваться с функциями ioctl . Может применяться как сокет TCP, так и сокет UDP [128, с. 163].

    Выполнение вызова SIOCGIFCONF в цикле

    12-28 Фундаментальной проблемой, связанной с вызовом SIOCGIFCONF , является то, что некоторые реализации не возвращают ошибку, если буфер слишком мал для хранения полученного результата [128, с. 118–119]. В этом случае результат просто обрезается так, чтобы поместиться в буфер, и функция ioctl возвращает нулевое значение, что соответствует успешному выполнению. Это означает, что единственный способ узнать, достаточно ли велик наш буфер, — сделать вызов, сохранить возвращенную длину, снова сделать вызов с большим размером буфера и сравнить полученную длину со значением, сохраненным из предыдущего вызова. Только если эти две длины одинаковы, наш буфер можно считать достаточно большим.

    ПРИМЕЧАНИЕ

    Беркли-реализации не возвращают ошибку, если буфер слишком мал [128, с. 118-199], и результат просто обрезается так, чтобы поместиться в существующий буфер. Solaris 2.5 возвращает ошибку EINVAL, если возвращаемая длина больше или равна длине буфера. Но мы не можем считать вызов успешным, если возвращаемая длина меньше размера буфера, поскольку Беркли-реализации могут возвращать значение, меньшее размера буфера, если часть структуры в него не помещается.

    В некоторых реализациях предоставляется вызов SIOCGIFNUM, который возвращает число интерфейсов. Это позволяет приложению перед выполнением вызова SIOCGIFCONF выделить в памяти место для буфера достаточного размера, но такой подход не является широко распространенным.

    Выделение в памяти места под буфер фиксированного размера для результата вызова SIOCGIFCONF стало проблемой с ростом Сети, поскольку большие веб-серверы используют много альтернативных адресов для одного интерфейса. Например, в Solaris 2.5 был предел в 256 альтернативных адресов для интерфейса, но в версии 2.6 этот предел вырос до 8192. Обнаружилось, что на сайтах с большим числом альтернативных адресов перестают работать программы с буферами фиксированного размера для размещения информации об интерфейсе. Хотя Solaris возвращает ошибку, если буфер слишком мал, эти программы размещают в памяти буфер фиксированного размера, запускают функцию ioctl, но затем перестают работать при возвращении ошибки.

    12-15 Мы динамически размещаем в памяти буфер начиная с размера, достаточного для 100 структур ifreq . Мы также отслеживаем длину, возвращаемую последним вызовом SIOCGIFCONF в lastlen , и инициализируем ее нулем.

    19-20 Если функция ioctl возвращает ошибку EINVAL и функция еще не возвращалась успешно (то есть lastlen все еще равно нулю), значит, мы еще не выделили буфер достаточного размера, поэтому мы продолжаем выполнять цикл.

    22-23 Если функция ioctl завершается успешно и возвращаемая длина равна lastlen , значит, длина не изменилась (наш буфер имеет достаточный размер), и мы с помощью функции break выходим из цикла, так как у нас имеется вся информация.

    26-27 В каждом проходе цикла мы увеличиваем размер буфера для хранения еще 10 структур ifreq .

    Инициализация указателей связного списка

    29-31 Поскольку мы будем возвращать указатель на начало связного списка структур ifi_info , мы используем две переменные ifihead и ifipnext для хранения указателей на список по мере его создания.

    Следующая часть нашей функции get_ifi_info , содержащая начало основного цикла, показана в листинге 17.5.

    Листинг 17.5. Конфигурация интерфейса процесса

    34 for (ptr = buf; ptr

    35 ifr = (struct ifreq*)ptr;

    36 #ifdef HAVE_SOCKADDR_SA_LEN

    37 len = max(sizeof(struct sockaddr), ifr->ifr_addr.sa_len);

    39 switch (ifr->ifr_addr.sa_family) <

    41 case AF_INET6:

    42 len = sizeof(struct sockaddr_in6);

    45 case AF_INET:

    47 len = sizeof(struct sockaddr);

    50 #endif /* HAVE_SOCKADDR_SA_LEN */

    51 ptr += sizeof(ifr->ifr_name) + len; /* для следующей строки */

    52 #ifdef HAVE_SOCKADDR_DL_STRUCT

    53 /* предполагается, что AF_LINK идет перед AF_INET и AF_INET6 */

    54 if (ifr->ifr_addr.sa_family == AF_LINK) <

    55 struct sockaddr_dl *sdl = (struct sockaddr_dl*)&ifr->ifr_addr;

    56 sdlname = ifr->ifr_name;

    58 haddr = sdl->sdl_data + sdl->sdl_nlen;

    59 hlen = sdl->sdl_alen;

    62 if (ifr->ifr_addr.sa_family != family)

    63 continue; /* игнорируется, если семейство адреса не то */

    65 if ((cptr = strchr(ifr->ifr_name, ‘:’)) != NULL)

    66 *cptr = 0; /* замена двоеточия нулем */

    67 if (strncmp(lastname, ifr->ifr_name, IFNAMSIZ) == 0) <

    68 if (doaliases == 0)

    69 continue; /* этот интерфейс уже обработан */

    70 myflags = IFI_ALIAS;

    72 memcpy(lastname, ifr->ifr_name, IFNAMSIZ);

    73 ifrcopy = *ifr;

    74 Ioctl(sockfd, SIOCGIFFLAGS, &ifrcopy);

    75 flags = ifrcopy.ifr_flags;

    76 if ((flags & IFF_UP) == 0)

    77 continue; /* игнорируется, если интерфейс не используется */

    Переход к следующей структуре адреса сокета

    35-51 При последовательном просмотре всех структур i freq ifr указывает на текущую структуру, а мы увеличиваем ptr на единицу, чтобы он указывал на следующую. Необходимо предусмотреть особенность более новых систем, предоставляющих поле длины для структур адреса сокета, и вместе с тем учесть, что более старые системы этого поля не предоставляют. Хотя в листинге 17.1 структура адреса сокета, содержащаяся в структуре ifreq , объявляется как общая структура адреса сокета, в новых системах она может относиться к произвольному типу. Действительно, в 4.4BSD структура адреса сокета канального уровня также возвращается для каждого интерфейса [128, с. 118]. Следовательно, если поддерживается элемент длины, то мы должны использовать его значение для переустановки нашего указателя на следующую структуру адреса сокета. В противном случае мы определяем длину, исходя из семейства адресов, используя размер общей структуры адреса сокета (16 байт) в качестве значения по умолчанию.

    ПРИМЕЧАНИЕ

    В системах, поддерживающих IPv6, не оговаривается, возвращается ли адрес IPv6 вызовом SIOCGIFCONF. Для более новых систем мы вводим оператор case, в котором предусмотрена возможность возвращения адресов IPv6. Проблема состоит в том, что объединение в структуре ifreq определяет возвращаемые адреса как общие 16-байтовые структуры sockaddr, подходящие для 16-байтовых структур sockaddr_in IPv4, но для 24-байтовых структур sockaddr_in6 IPv6 они слишком малы. В случае возвращения адресов IPv6 возможно некорректное поведение существующего кода, созданного в предположении, что в каждой структуре ifreq содержится структура sockaddr фиксированного размера. В системах, где структура sockaddr имеет поле sa_len, никаких проблем не возникает, потому что такие системы легко могут указывать размер структур sockaddr.

    52-60 Если система возвращает структуры sockaddr семейства AF_LINK в SIOCGIFCONF , мы копируем индекс интерфейса и данные об аппаратном адресе из таких структур.

    62-63 Мы игнорируем все адреса из семейств, отличных от указанного вызывающим процессом в аргументе функции get_ini_info .

    Обработка альтернативных имен

    64-72 Нам нужно обнаружить все альтернативные имена (псевдонимы), которые могут существовать для интерфейса, то есть присвоенные этому интерфейсу дополнительные адреса. Обратите внимание в наших примерах, следующих за листингом 17.3, что в Solaris псевдоним содержит двоеточие, в то время как в 4.4BSD имя интерфейса в псевдониме не изменяется. Чтобы обработать оба случая, мы сохраняем последнее имя интерфейса в lastname и сравниваем его только до двоеточия, если оно присутствует. Если двоеточия нет, мы игнорируем этот интерфейс в том случае, когда имя эквивалентно последнему обработанному интерфейсу.

    Получение флагов интерфейса


    73-77 Мы выполняем вызов SIOCGIFFLAGS функции ioctl (см. раздел 16.5), чтобы получить флаги интерфейса. Третий аргумент функции ioctl — это указатель на структуру ifreq , содержащую имя интерфейса, для которого мы хотим получить флаги. Мы создаем копию структуры ifreq , перед тем как запустить функцию ioctl, поскольку в противном случае этот вызов перезаписал бы IP-адрес интерфейса, потому что оба они являются элементами одного и того же объединения из листинга 17.1. Если интерфейс не активен, мы игнорируем его.

    В листинге 17.6 представлена третья часть нашей функции.

    Листинг 17.6. Получение и возвращение адресов интерфейса

    78 ifi = Calloc(1, sizeof(struct ifi_info));

    79 *ifipnext = ifi; /* prev указывает на новую структуру */

    80 ifipnext = &ifi->ifi_next; /* сюда указывает указатель на

    81 ifi->ifi_flags = flags; /* значения IFF_xxx */

    82 ifi->ifi_myflags = myflags; /* значения IFI_xxx */

    83 #if defined(SIOCGIFMTU) && defined(HAVE_STRUCT_IFREQ_IFR_MTU)

    84 Ioctl(sockfd, SIOCGIFMTU, &ifrcopy);

    85 ifi->ifi_mtu = ifrcopy.ifr_mtu;

    87 ifi->ifi_mtu = 0;

    89 memcpy(ifi->ifi_name, ifr->ifr_name, IFI_NAME);

    90 ifi->ifi_name[IFI_NAME-1] = ‘\0’;

    91 /* если sockaddr_dl относится к другому интерфейсу, он игнорируется */

    92 if (sdlname == NULL || strcmp(sdlname, ifr->ifr_name) != 0)

    94 ifi->ifi_index = idx;

    95 ifi->ifi_hlen = hlen;

    96 if (ifi->ifi_hlen > IFI_HADDR)

    97 ifi->ifi_hlen = IFI_HADDR;

    99 memcpy(ifi->ifi_haddr, haddr, ifi->ifi_hlen);

    Выделение памяти и инициализация структуры ifi_info

    78-99 На этом этапе мы знаем, что возвратим данный интерфейс вызывающему процессу. Мы выделяем память для нашей структуры ifi_info и добавляем ее в конец связного списка, который мы создаем. Мы копируем флаги и имя интерфейса в эту структуру. Далее мы проверяем, заканчивается ли имя интерфейса нулем, и поскольку функция callос инициализирует выделенную в памяти область нулями, мы знаем, что ifi_hlen инициализируется нулем, a ifi_next — пустым указателем.

    В листинге 17.7 представлена последняя часть нашей функции.

    Листинг 17.7. Получение и возврат адреса интерфейса

    100 switch (ifr->ifr_addr.sa_family) <

    101 case AF_INET:

    102 sinptr = (struct sockaddr_in*)&ifr->ifr_addr;

    103 ifi->ifi_addr = Calloc(1, sizeof(struct sockaddr_in));

    104 memcpy(ifi->ifi_addr, sinptr, sizeof(struct sockaddr_in));

    105 #ifdef SIOCGIFBRDADDR

    106 if (flags & IFF_BROADCAST) <

    107 Ioctl(sockfd, SIOCGIFBRDADDR, &ifrcopy);

    108 sinptr = (struct sockaddr_in*) &ifrcopy.ifr_broadaddr;

    109 ifi->ifi_brdaddr = Calloc(1, sizeof(struct sockaddr_in));

    110 memcpy(ifi->ifi_brdaddr, sinptr, sizeof(struct sockaddr_in));

    113 #ifdef SIOCGIFDSTADDR

    114 if (flags & IFF_POINTOPOINT) <

    115 Ioctl(sockfd, SIOCGIFDSTADDR, &ifrcopy);

    116 sinptr = (struct sockaddr_in*) &ifrcopy.ifr_dstaddr;

    117 ifi->ifi_dstaddr = Calloc(1, sizeof(struct sockaddr_in));

    118 memcpy(ifi->ifi_dstaddr, sinptr, sizeof(struct sockaddr_in));

    122 case AF_INET6:

    123 sin6ptr = (struct sockaddr_in6*)&ifr->ifr_addr;

    124 ifi->ifi_addr = Calloc(1, sizeof(struct sockaddr_in6));

    125 memcpy(ifi->ifi_addr, sin6ptr, sizeof(struct sockaddr_in6));

    126 #ifdef SIOCGIFDSTADDR

    127 if (flags & IFF_POINTOPOINT) <

    128 Ioctl(sockfd, SIOCGIFDSTADDR, &ifrcopy);

    129 sin6ptr = (struct sockaddr_in6*)&ifrcopy.ifr_dstaddf;

    130 ifi->ifi_dstaddr = Calloc(1, sizeof(struct sockaddr_in6));

    131 memcpy(ifi->ifi_dstaddr, sin6ptr,

    132 sizeof(struct sockaddr_in6));

    141 return(ifihead); /* указатель на первую структуру в связной списке */

    102-104 Мы копируем IP-адрес, возвращенный из нашего начального вызова SIOCGIFCONF функции ioctl , в структуру, которую мы создаем.

    106-119 Если интерфейс поддерживает широковещательную передачу, мы получаем широковещательный адрес с помощью вызова SIOCGIFBRDADDR функции ioctl . Мы выделяем память для структуры адреса сокета, содержащей этот адрес, и добавляем ее к структуре ifi_info , которую мы создаем. Аналогично, если интерфейс является интерфейсом типа «точка-точка», вызов SIOCGIFBRDADDR возвращает IP-адрес другого конца связи.

    123-133 Обработка случая IPv6 — полная аналогия IPv4 за тем исключением, что вызов SIOCGIFBRDADDR не делается, потому что IPv6 не поддерживает широковещательную передачу.

    В листинге 17.8 показана функция free_ifi_info , которой передается указатель, возвращенный функцией get_ifi_info . Эта функция освобождает всю динамически выделенную память.

    Листинг 17.8. Функция free_ifi_info: освобождение памяти, которая была динамически выделена функцией get_ifi_info

    144 free_ifi_info(struct ifi_info *ifihead)

    146 struct ifi_info *ifi, *ifinext;

    147 for (ifi = ifihead; ifi != NULL; ifi = ifinext) <

    148 if (ifi->ifi_addr != NULL)

    150 if (ifi->ifi_brdaddr != NULL)

    152 if (ifi->ifi_dstaddr != NULL)

    154 ifinext = ifi->ifi_next; /* невозможно получить ifi_next

    после вызова freed */

    17.7. Операции с интерфейсами

    Как мы показали в предыдущем разделе, запрос SIOCGIFCONF возвращает имя и структуру адреса сокета для каждого сконфигурированного интерфейса. Существует множество других вызовов, позволяющих установить или получить все остальные характеристики интерфейса. Версия get этих вызовов ( SIOCGxxx ) часто запускается программой netstat , а версия set ( SIOCSxxx ) — программой ifconfig . Любой пользователь может получить информацию об интерфейсе, в то время как установка этой информации требует прав привилегированного пользователя.

    Эти вызовы получают или возвращают структуру ifreq , адрес которой задается в качестве третьего аргумента функции ioctl . Интерфейс всегда идентифицируется по имени: le0 , lo0 , ppp0 , — то есть по имени, заданному в элементе ifr_name структуры ifreq .

    Многие из этих запросов используют структуру адреса сокета, для того чтобы задать или возвратить IP-адрес или маску адреса. Для IPv4 адрес или маска содержится в элементе sin_addr из структуры адреса сокета Интернета. Для IPv6 они помещаются в элемент sin6_addr структуры адреса сокета IPv6.

    в– SIOCGIFADDR . Возвращает адрес направленной передачи в элементе ifr_addr .

    в– SIOCSIFADDR . Устанавливает адрес интерфейса из элемента ifr_addr . Также вызывается функция инициализации для интерфейса.

    в– SIOCGIFFLAGS . Возвращает флаги интерфейса в элементе ifr_flags . Имена различных флагов определяются в виде IFF_xxx в заголовочном файле . Флаги указывают, например, включен ли интерфейс ( IFF_UP ), является ли он интерфейсом типа «точка-точка» ( IFF_POINTOPOINT ), поддерживает ли широковещательную передачу ( IFF_BROADCAST ) и т.д.

    в– SIOCSIFFLAGS . Устанавливает флаги из элемента ifr_flags .

    в– SIOCGIFDSTADDR . Возвращает адрес типа «точка-точка» в элементе ifr_dstaddr .

    в– SIOCSIFDSTADDR . Устанавливает адрес типа «точка-точка» из элемента ifr_dstaddr .

    в– SIOCGIFBRDADDR . Возвращает широковещательный адрес в элементе ifr_broadaddr . Приложение сначала должно получить флаги интерфейса, а затем сделать корректный вызов: SIOCGIFBRDADDR для широковещательного интерфейса или SIOCGIFDSTADDR — для интерфейса типа «точка-точка».

    в– SIOCSIFBRDADDR . Устанавливает широковещательный адрес из элемента ifr_broadaddr .

    в– SIOCGIFNETMASK . Возвращает маску подсети в элементе ifr_addr .

    в– SIOCSIFNETMASK . Устанавливает маску подсети из элемента ifr_addr .

    в– SIOCGIFMETRIC . Возвращает метрику интерфейса в элементе ifr_metric . Метрика поддерживается ядром для каждого интерфейса, но используется демоном маршрутизации routed . Метрика интерфейса добавляется к счетчику количества переходов.

    в– SIOCSIFMETRIC . Устанавливает метрику интерфейса из элемента ifr_metric .

    В этом разделе мы описали наиболее типичные операции интерфейсов. Во многих реализациях появились дополнительные операции.

    17.8. Операции с кэшем ARP

    Операции с кэшем ARP также осуществляются с помощью функции ioctl . В этих запросах используется структура arpreq , показанная в листинге 17.9 и определяемая в заголовочном файле .

    Листинг 17.9. Структура arpreq, используемая с вызовами ioctl для кэша ARP

    struct sockaddr arp_pa; /* адрес протокола */

    struct sockaddr arp_ha; /* аппаратный адрес */

    int arp_flags; /* флаги */

    #define ATF_INUSE 0x01 /* запись, которую нужно использовать */

    #define ATF_COM 0x02 /* завершенная запись */

    #define ATF_PERM 0x04 /* постоянная запись */

    #define ATF_PUBL 0x08 /* опубликованная запись (отсылается другим узлам) */

    Третий аргумент функции ioctl должен указывать на одну из этих структур. Поддерживаются следующие три вызова:

    в– SIOCSARP . Добавляет новую запись в кэш ARP или изменяет существующую запись. arp_pa — это структура адреса сокета Интернета, содержащая IP-адрес, a arp_ha — это общая структура адреса сокета с элементом ss_family , равным AF_UNSPEC , и элементом sa_data , содержащим аппаратный адрес (например, 6-байтовый адрес Ethernet). Два флага ATF_PERM и ATF_PUBL могут быть заданы приложением. Два других флага, ATF_INUSE и ATF_COM , устанавливаются ядром.

    в– SIOCDARP . Удаляет запись из кэша ARP. Вызывающий процесс задает интернет-адрес удаляемой записи.

    в– SIOCGARP . Получает запись из кэша ARP. Вызывающий процесс задает интернет-адрес, и соответствующий адрес Ethernet возвращается вместе с флагами.

    Добавлять или удалять записи может только привилегированный пользователь. Эти три вызова обычно делает программа arp .

    ПРИМЕЧАНИЕ

    Запросы функции ioctl, связанные с ARP, не поддерживаются в некоторых более новых системах, использующих для описанных операций ARP маршрутизирующие сокеты.

    Обратите внимание, что невозможно с помощью функции ioctl перечислить все записи кэша ARP. Большинство версий команды arp при использовании флага -a (перечисление всех записей кэша ARP) считывают память ядра ( /dev/kmem ), чтобы получить текущее содержимое кэша ARP. Мы увидим более простой (и предпочтительный) способ, основанный на применении функции sysctl , описанной в разделе 18.4.

    Пример: вывод аппаратного адреса узла

    Теперь мы используем нашу функцию my_addrs для того, чтобы возвратить все IP-адреса узла. Затем для каждого IP-адреса мы делаем вызов SIOCGARP функции ioctl , чтобы получить и вывести аппаратные адреса. Наша программа показана в листинге 17.10.

    Листинг 17.10. Вывод аппаратного адреса узла

    1 #include «unpifi.h»

    4 main(int argc, char **argv)

    7 struct ifi_info *ifi;

    8 unsigned char *ptr;


    9 struct arpreq arpreq;

    10 struct sockaddr_in *sin;

    11 sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    12 for (ifi = get_ifi_info(AF_INET, 0); ifi != NULL; ifi = ifi->ifi_next) <

    13 printf(«%s: «, Sock_ntop(ifi->ifi_addr, sizeof(struct sockaddr_in)));

    14 sin = (struct sockaddr_in*)&arpreq.arp_pa;

    15 memcpy(sin, ifi->ifi_addr, sizeof(struct sockaddr_in));

    16 if (ioctl(sockfd, SIOCGARP, &arpreq)

    17 err_ret(«ioctl SIOCGARP»);

    Получение списка адресов и проход в цикле по каждому из них

    12 Мы вызываем функцию get_ifi_info , чтобы получить IP-адреса узла, а затем выполняем цикл по всем адресам.

    Вывод IP-адреса

    13 Мы выводим IP-адреса, используя функцию inet_ntop . Мы просим функцию get_ifi_info возвращать только адреса IPv4, так как ARP с IPv6 не используется.

    Вызов функции ioctl и проверка ошибок

    14-19 Мы заполняем структуру arp_pa как структуру адреса сокета IPv4, содержащую адрес IPv4. Вызывается функция ioctl , и если она возвращает ошибку (например, указанный адрес относится к интерфейсу, не поддерживающему ARP), мы выводим сообщение и переходим к следующему адресу.

    Вывод аппаратного адреса

    20-22 Выводится аппаратный адрес, возвращаемый ioctl .

    При запуске этой программы на нашем узле hpux мы получаем:

    hpux % prmac

    127.0.0.1: ioctl SIOCGARP: Invalid argument

    17.9. Операции с таблицей маршрутизации

    Для работы с таблицей маршрутизации предназначены два вызова функции ioctl . Эти два вызова требуют, чтобы третий аргумент функции ioctl был указателем на структуру rtentry , которая определяется в заголовочном файле . Обычно эти вызовы исходят от программы route . Их может делать только привилегированный пользователь. При наличии маршрутизирующих сокетов (глава 18) для выполнения этих запросов используются именно они, а не функция ioctl .

    в– SIOCADDRT . Добавить запись в таблицу маршрутизации.

    в– SIOCDELRT . Удалить запись из таблицы маршрутизации.

    Нет способа с помощью функции ioctl перечислить все записи таблицы маршрутизации. Эту операцию обычно выполняет программа netstat с флагом -r . Программа получает таблицу маршрутизации, считывая память ядра ( /dev/kmem ). Как и в случае с просмотром кэша ARP, в разделе 18.4 мы увидим более простой (и предпочтительный) способ, предоставляемый функцией sysctl .

    17.10. Резюме

    Команды функции ioctl , используемые в сетевых приложениях, можно разделить на шесть категорий:

    1. Операции с сокетами (находимся ли мы на отметке внеполосных данных?).

    2. Операции с файлами (установить или сбросить флаг отсутствия блокировки).

    3. Операции с интерфейсами (возвратить список интерфейсов, получить широковещательный адрес).

    4. Операции с кэшем ARP (создать, изменить, получить, удалить).

    5. Операции с таблицей маршрутизации (добавить или удалить).

    6. Операции с потоками STREAMS (см. главу 31).

    Мы будем использовать операции с сокетами и файлами, а получение списка интерфейсов — это настолько типичная операция, что для этой цели мы разработали собственную функцию. Мы будем применять ее много раз в оставшейся части книги. Вызовы функции ioctl с кэшем ARP и таблицей маршрутизации используются лишь несколькими специализированными программами.

    Упражнения

    1. В разделе 17.7 мы сказали, что широковещательный адрес, возвращаемый запросом SIOCGIFBRDADDR, возвращается в элементе ifr_broadaddr . Но на с. 173 [128] сказано, что он возвращается в элементе ifr_dstaddr . Имеет ли это значение?

    2. Измените программу get_ifi_info так, чтобы она делала первый вызов SIOCGIFCONF для одной структуры ifreq , а затем каждый раз в цикле увеличивайте длину на размер одной из этих структур. Затем поместите в цикл операторы, которые выводили бы размер буфера при каждом вызове независимо от того, возвращает функция ioctl ошибку или нет, и при успешном выполнении выведите возвращаемую длину буфера. Запустите программу prifinfo и посмотрите, как ваша система обрабатывает вызов, когда размер буфера слишком мал. Выведите также семейство адресов для всех возвращаемых структур, семейство адресов которых не совпадает с указанным в первом аргументе функции get_ifi_info , чтобы увидеть, какие еще структуры возвращает ваша система.

    3. Измените функцию get_ifi_info так, чтобы она возвращала информацию об адресе с альтернативным именем, если дополнительный адрес находится не в той подсети, в которой находится предыдущий адрес для данного интерфейса. Таким образом, наша версия из раздела 17.6 будет игнорировать альтернативные имена в диапазоне от 206.62.226.44 до 206.62.226.46, и это вполне нормально, поскольку они находятся в той же подсети, что и первичный адрес интерфейса 206.62.226.33. Но если альтернативное имя находится в другой подсети, допустим 192.3.4.5, возвратите структуру ifi_info с информацией о дополнительном адресе.

    4. Если ваша система поддерживает вызов SIOCGIGNUM функции ioctl , измените листинг 17.4 так, чтобы запустить этот вызов, и используйте возвращаемое значение как начальный размер буфера.

    Операции с кэшем ARP также осуществляются с помощью функции ioctl . В этих запросах используется структура arpreq , показанная в листинге 17.9 и определяемая в заголовочном файле .

    Листинг 17.9. Структура arpreq, используемая с вызовами ioctl для кэша ARP

    struct sockaddr arp_pa; /* адрес протокола */

    struct sockaddr arp_ha; /* аппаратный адрес */

    int arp_flags; /* флаги */

    #define ATF_INUSE 0x01 /* запись, которую нужно использовать */

    #define ATF_COM 0x02 /* завершенная запись */

    #define ATF_PERM 0x04 /* постоянная запись */

    #define ATF_PUBL 0x08 /* опубликованная запись (отсылается другим узлам) */

    Третий аргумент функции ioctl должен указывать на одну из этих структур. Поддерживаются следующие три вызова:

    в– SIOCSARP . Добавляет новую запись в кэш ARP или изменяет существующую запись. arp_pa — это структура адреса сокета Интернета, содержащая IP-адрес, a arp_ha — это общая структура адреса сокета с элементом ss_family , равным AF_UNSPEC , и элементом sa_data , содержащим аппаратный адрес (например, 6-байтовый адрес Ethernet). Два флага ATF_PERM и ATF_PUBL могут быть заданы приложением. Два других флага, ATF_INUSE и ATF_COM , устанавливаются ядром.

    в– SIOCDARP . Удаляет запись из кэша ARP. Вызывающий процесс задает интернет-адрес удаляемой записи.

    в– SIOCGARP . Получает запись из кэша ARP. Вызывающий процесс задает интернет-адрес, и соответствующий адрес Ethernet возвращается вместе с флагами.

    Добавлять или удалять записи может только привилегированный пользователь. Эти три вызова обычно делает программа arp .

    ПРИМЕЧАНИЕ

    Запросы функции ioctl, связанные с ARP, не поддерживаются в некоторых более новых системах, использующих для описанных операций ARP маршрутизирующие сокеты.

    Обратите внимание, что невозможно с помощью функции ioctl перечислить все записи кэша ARP. Большинство версий команды arp при использовании флага -a (перечисление всех записей кэша ARP) считывают память ядра ( /dev/kmem ), чтобы получить текущее содержимое кэша ARP. Мы увидим более простой (и предпочтительный) способ, основанный на применении функции sysctl , описанной в разделе 18.4.

    Операции с кэшем ARP также осуществляются с помощью функции ioctl . В этих запросах используется структура arpreq , показанная в листинге 17.9 и определяемая в заголовочном файле .

    Листинг 17.9. Структура arpreq, используемая с вызовами ioctl для кэша ARP

    struct sockaddr arp_pa; /* адрес протокола */

    struct sockaddr arp_ha; /* аппаратный адрес */

    int arp_flags; /* флаги */

    #define ATF_INUSE 0x01 /* запись, которую нужно использовать */

    #define ATF_COM 0x02 /* завершенная запись */

    #define ATF_PERM 0x04 /* постоянная запись */

    #define ATF_PUBL 0x08 /* опубликованная запись (отсылается другим узлам) */

    Третий аргумент функции ioctl должен указывать на одну из этих структур. Поддерживаются следующие три вызова:

    в– SIOCSARP . Добавляет новую запись в кэш ARP или изменяет существующую запись. arp_pa — это структура адреса сокета Интернета, содержащая IP-адрес, a arp_ha — это общая структура адреса сокета с элементом ss_family , равным AF_UNSPEC , и элементом sa_data , содержащим аппаратный адрес (например, 6-байтовый адрес Ethernet). Два флага ATF_PERM и ATF_PUBL могут быть заданы приложением. Два других флага, ATF_INUSE и ATF_COM , устанавливаются ядром.

    в– SIOCDARP . Удаляет запись из кэша ARP. Вызывающий процесс задает интернет-адрес удаляемой записи.

    в– SIOCGARP . Получает запись из кэша ARP. Вызывающий процесс задает интернет-адрес, и соответствующий адрес Ethernet возвращается вместе с флагами.

    Добавлять или удалять записи может только привилегированный пользователь. Эти три вызова обычно делает программа arp .

    ПРИМЕЧАНИЕ

    Запросы функции ioctl, связанные с ARP, не поддерживаются в некоторых более новых системах, использующих для описанных операций ARP маршрутизирующие сокеты.

    Обратите внимание, что невозможно с помощью функции ioctl перечислить все записи кэша ARP. Большинство версий команды arp при использовании флага -a (перечисление всех записей кэша ARP) считывают память ядра ( /dev/kmem ), чтобы получить текущее содержимое кэша ARP. Мы увидим более простой (и предпочтительный) способ, основанный на применении функции sysctl , описанной в разделе 18.4.

    Включение ioctl-кодов в программу пользовательского пространства

    Существует ли распространенная практика для программ пользовательского пространства включать коды ioctl используемые в модуле ядра.

    mydev.h:

    Обычно я помещаю свои коды ioctl в заголовок, который я включаю в свой код модуля ядра. Я просто включил этот заголовок в свои приложения для пользовательского пространства, но я понял, что путь к файлу linux/ioctl.h может отсутствовать в большинстве систем (например, в системах без экспортированных заголовков ядра).

    По-видимому, решение состоит в том, чтобы изменить строку #include на: #include ; но тогда я не мог использовать этот заголовок для моего модуля ядра.

    Есть ли лучшее решение этой проблемы, или, как правило, есть два отдельных, но почти идентичных файла заголовка?

    Вы можете использовать макрос _KERNEL_.

    Возможно, вам придется абстрагироваться от фактических значений ioctl, но вы получите эту идею.

    Что такое sys / ioctl.h? Где / как я могу добавить это для установки libtecla?

    Я пытаюсь использовать libtecla с bladeRF в Windows 10. Однако после попытки установки я вижу следующую ошибку. Я не имею понятия, что не так. Я вижу, что там написано, что нет файла или каталога с именем sys / ioctl.h, но почему это так? Я использую MinGW для бинарных файлов GCC 5.3.0.

    Разве эта библиотека не должна иметь ссылку на эту библиотеку? Если нет, что я могу сделать, чтобы это исправить?

    Что такое код ioctl

    До появления ядра 1.1.54 индикаторы состояния отражали только состояние флагов клавиатуры, которые также могли быть изменены KDGETLED/KDSETLED. После появления 1.1.54 они могли отображать уже произвольную информацию, но по умолчанию показывали флаги клавиатуры. Последующие две команды ioctl используются для доступа к флагам клавиатуры.

    KDGKBLED Получить флаги клавиатуры: CapsLock, NumLock, ScrollLock (не индикаторы). argp указывает на ячейку типа char, в которой сохраняется состояния флагов. Младшие три бита (маска 0x7) содержат текущее состояние флагов, следующие три бита (маска 0x70) содержат устанавливаемое состояние флагам по умолчанию. (Начиная с ядра 1.1.54). KDSKBLED Установить флаги клавиатуры: CapsLock, NumLock, ScrollLock (не индикаторы). argp содержит требуемые состояния флагов. Три младших бита (маска 0x7) содержат состояния флагов, следующие три бита (маска 0x70) содержат состояния флагов по умолчанию. (Начиная с 1.1.54). KDGKBTYPE Получить тип клавиатуры. Возвращается значение KB_101 равное 0x02. KDADDIO Добавить порт ввода/вывода (I/O) как допустимый. Эквивалентно вызову ioperm(arg,1,1). KDDELIO Удалить порт ввода/вывода (I/O) как недопустимый. Эквивалентно вызову ioperm(arg,1,0). KDENABIO Включить ввод/вывод на видеокарту. Эквивалентно вызову ioperm(0x3b4, 0x3df-0x3b4+1, 1). KDDISABIO Выключить ввод/вывод на видеокарту. Эквивалентно вызову ioperm(0x3b4, 0x3df-0x3b4+1, 0). KDSETMODE Установить текстовый/графический режим. argp содержит одно из двух значений:

    KD_TEXT 0x00
    KD_GRAPHICS 0x01

    KDGETMODE Получить тип режима (текстовый/графический). argp указывает на ячейку типа long, которой присваивается одно из вышеперечисленных устанавливаемых значений. KDMKTONE Генерировать тон заданной длительности. Младшие 16 битов argp задают частоту тона (период в тактах), старшие 16 битов устанавливают его длительность в мсек. Если длительность равна нулю, звук выключается. Управление возвращается немедленно. Например, argp = (125 KIOCSOUND Запустить и остановить генерацию звука. Младшие 16 битов argp определяют период в тактах (то есть argp = 1193180/частота). Значение argp = 0 выключает звук. В обоих случаях управление возвращается немедленно. GIO_CMAP Получить из ядра текущую цветовую карту по умолчанию. argp указывает на массив из 48 байтов. (Начиная с 1.3.3.) PIO_CMAP Изменить цветовую карту по умолчанию для текстового режима. argp указывает на массив из 48 байтов, который содержит, по порядку, значения красного, зеленого и синего для 16-и доступных экранных цветов: 0 — составляющая выключена, 255 — полная интенсивность. Цвета по умолчанию, по порядку: чёрный, тёмно-красный, тёмно-зелёный, коричневый, тёмно-синий, тёмно-сиреневый, тёмно-голубой, светло-сервый, тёмно-серый, ярко-красный, ярко-зелёный, жёлтый, ярко-синий, ярко-сиреневый, ярко-голубой и белый. (Начиная с 1.3.3). GIO_FONT Получить экранный шрифт (256 символов) в расширенном виде. argp указывает на массив из 8192 байтов. Возвращает код ошибки EINVAL, если текущий загруженный шрифт содержит 512 символов или консоль находится не в текстовом режиме. GIO_FONTX Получить экранный шрифт и связанную с ним информацию. argp указывает на struct consolefontdesc (см. PIO_FONTX). При вызове значение поля charcount должно быть равно максимальному числу символов, которое помещается в буфер, указываемый chardata. При возврате charcount и charheight содержат информацию о текущем загруженном шрифте, а массив chardata содержит данные шрифта, если согласно начальному значению charcount для этого достаточно места; в противном случае буфер остаётся неизменным и errno присваивается значение ENOMEM. (Начиная с 1.3.1). PIO_FONT Установить экранный шрифт из 256 символов. Шрифт загружается в знакогенератор EGA/VGA. argp указывает на карту размером 8192 байта (32 байта на символ). Только первые N из них используются для шрифта 8xN (0 PIO_FONTX Установить экранный шрифт и соответствующую информацию для изображения. argp указывает на структуру:

    Если требуется, соответственно изменяются размеры экрана и соответствующим процессам посылается сигнал SIGWINCH. Этот вызов также отменяет перекодировку в Юникод. (Начиная с 1.3.1).

    PIO_FONTRESET Сбросить экранный шрифт, размер и перекодировку в Юникод в начальные значения, использованные при загрузке. Аргумент argp не используется, но его значение должно быть NULL, чтобы эта версия была совместима с будущими версиями Linux. (Начиная с 1.3.28). GIO_SCRNMAP Получить разметку экрана из ядра. argp указывает на область размером E_TABSZ, которая заполняется позициями символов шрифта, используемыми при отображении. Вызов возвращает бесполезную информацию, если текущий загруженный шрифт содержит более 256 символов. GIO_UNISCRNMAP Получить полную экранную перекодировку в Юникод из ядра. argp указывает на область размером E_TABSZ*sizeof(unsigned short), которая заполняется представлением в Юникоде каждого символа. Специальный набор Юникода, начинающийся с U+F000, используется для перекодировки «напрямую в шрифт» (начиная с 1.3.1). PIO_SCRNMAP Загрузить «определяемую пользователем» (четвёртую) таблицу в ядро, по которой перекодируются байты в символы экрана консоли. argp указывает на область размером E_TABSZ. PIO_UNISCRNMAP Загрузить «определяемую пользователем» (четвёртую) таблицу в ядро, перекодирующую байты в значения Юникода, которые затем транслируются в экранные символы согласно текущей загруженной карте соответствия символов Юникода и шрифта. Специальные коды Юникода, начинающиеся с U+F000, могут использоваться для непосредственного перевода байтов в символы шрифта. (Начиная с 1.3.1). GIO_UNIMAP Получить соответствие символов Юникода шрифту из ядра. argp указывает на структуру

    где entries указывает на массив структур

    PIO_UNIMAP Поместить соответствие символов Юникода и экранного шрифта в ядро. argp указывает на struct unimapdesc (начиная с 1.1.92). PIO_UNIMAPCLR Очистить таблицу, возможно с помощью алгоритма хэширования. argp указывает на структуру

    KDGKBMODE Получить текущий режим клавиатуры. argp указывает на ячейку типа long, в которой может быть одно из значений:

    K_RAW 0x00
    K_XLATE 0x01
    K_MEDIUMRAW 0x02
    K_UNICODE 0x03

    KDSKBMODE Установить текущий режим клавиатуры. argp указывает на ячейку типа long, значением которой может быть любое из представленных чуть ранее. KDGKBMETA Получить режим обработки метаклавиш. argp указывает на ячейку типа long, в которой может быть одно из значений:

    K_METABIT 0x03 установлен старший бит
    K_ESCPREFIX 0x04 экранирующий префикс

    KDSKBMETA Установить режим обработки метаклавиш. argp указывает на ячейку типа long, значением которой может быть любое из представленных чуть ранее. KDGKBENT Получить один элемент из таблицы трансляции клавиш (код клавиши для кода действия). argp указывает на структуру

    значения двух первых полей представляют собой: kb_table — выбранную таблицу клавиш (0 KDSKBENT Создать элемент в таблице трансляции клавиш. argp указывает на struct kbentry. KDGKBSENT Получить значение строки функциональной клавиши. argp указывает на структуру

    kb_string равна (заканчивающейся null) строке, соответствующей коду действия функциональной клавиши kb_func.

    KDSKBSENT Создать элемент строки функциональной клавиши. argp указывает на struct kbsentry. KDGKBDIACR Получить таблицу акцентов из ядра. argp указывает на структуру

    где kb_cnt — число элементов массива, каждый из которых является структурой

    KDGETKEYCODE Получить элемент таблицы кодов клавиш ядра (сканкод в код клавиши). argp указывает на структуру

    keycode устанавливается в соответствии с заданным scancode. (Допускается 89 KDSETKEYCODE Записать элемент таблицы кодов клавиш ядра. argp указывает на struct kbkeycode. (Начиная с 1.1.63). KDSIGACCEPT Вызывающий процесс показывает свою готовность к приёму сигнала argp, если он генерируется нажатием соответствующей комбинации клавиш (1 VT_OPENQRY Получить первую доступную (не открытую) консоль. argp указывает ячейку типа int, устанавливаемое значение которой равно номеру vt (1 VT_GETMODE Считывает режим активного vt. argp указывает на структуру

    которая задаёт режим активного vt. mode имеет одно из значений:

    VT_AUTO автоматическое переключение vt
    VT_PROCESS обрабатывать управление переключением
    VT_ACKACQ подтверждающий переключатель

    VT_SETMODE Установить режим активного vt. argp указывает на struct vt_mode. VT_GETSTATE Получить общую информацию о состоянии vt. argp указывает на структуру

    Для каждого используемого vt устанавливается соответствующий бит в поле v_state. (В версиях с 1.0 до 1.1.92).

    VT_RELDISP Освободить дисплей. VT_ACTIVATE Переключиться на виртуальный терминал argp (1 VT_WAITACTIVE Подождать, пока виртуальный терминал argp не станет активным. VT_DISALLOCATE Освободить память, выделенную виртуальному терминалу argp. (Начиная с 1.1.54.) VT_RESIZE Установить представление о размере экрана в ядре. argp указывает на структуру

    Заметим, что этот вызов не изменяет видеорежим. Смотрите resizecons(8). (Начиная с 1.1.54.)

    VT_RESIZEX Установить значение различных параметров экрана в ядре. argp указывает на структуру

    Любому параметру может быть присвоено нулевое значение, указывающее «оставить без изменений», но, если задано несколько параметров, то они должны быть согласованы. Этот вызов не изменяет видеорежим. Смотрите resizecons(8). (Начиная с 1.3.3).

    Действие следующих ioctl зависит от первого байта структуры, указываемой argp, далее называемой subcode. Доступны только суперпользователю или владельцу текущего терминала.

    TIOCLINUX, subcode=0 Сделать дамп экрана. Удалено в 1.1.92 (c ядром 1.1.92 и более поздним используйте чтение из /dev/vcsN или /dev/vcsaN). TIOCLINUX, subcode=1 Получить информацию о задании. Удалено в 1.1.92. TIOCLINUX, subcode=2 Произвести выделение. argp указывает на структуру xs и ys — начальные столбец и строка. xe и ye — конечные столбец и строка (у левого верхнего угла строка=столбец=1). Значение sel_mode равно 0 для выделения «символ за символом», 1 для выделения «слово за словом» или 2 для выделения «строки за строкой». Выделенные символы подсвечиваются и сохраняются в статическом массиве sel_buffer из devices/char/console.c. TIOCLINUX, subcode=3 Вставить выделение. Символы буфера выделения записываются в fd. TIOCLINUX, subcode=4 Включить погашенный ранее (blank) экран. TIOCLINUX, subcode=5 Установить содержимое 256-битной таблицы поиска определения символов в «word» для выделения «слово за словом». (Начиная с 1.1.32). TIOCLINUX, subcode=6 argp указывает ячейку типа char, которая устанавливает значение переменной ядра shift_state. (Начиная 1.1.32.) TIOCLINUX, subcode=7 argp указывает ячейку типа char, которая устанавливает значение переменной ядра report_mouse. (Начиная с 1.1.33.) TIOCLINUX, subcode=8 Сделать дамп значений ширины и высоты экрана, позиции курсора и всех пар символ-атрибут (только в версиях с 1.1.67 по 1.1.91. С ядром 1.1.92 и более поздних версий используйте чтение /dev/vcsa*). TIOCLINUX, subcode=9 Восстановить ширину и высоту экрана, позицию курсора и все пары символ-атрибут (только в версиях с 1.1.67 по 1.1.91. С ядром 1.1.92 и более поздних версий используйте запись в /dev/vcsa*). TIOCLINUX, subcode=10 Обработчик функций энергосбережения для нового поколения мониторов. Режим гашения (blanking) экрана VESA устанавливается равным argp[1], который определяет тип гашения: 0: Гашение экрана выключено. 1: Текущие установки регистров видеоадаптера сохраняются, затем контроллер программируется на отключение вертикальной синхронизации. Происходит перевод монитора в режим «ожидания» (standby). Если в мониторе есть таймер Off_Mode, то он может в итоге сам выключить питание. 2: Текущие настройки сохраняются, а затем вертикальная и горизонтальная синхронизации отключаются. Происходит перевод в режим «выключен» (off). Если в мониторе нет таймера Off_Mode или вы хотите отключить питание сразу же по истечении времени blank_timer, то можете выбрать это значение. Внимание: частое выключение питания может повредить монитор. (Начиная с 1.1.76)

    ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ

    ОШИБКИ

    ЗАМЕЧАНИЯ

    Очень часто ioctl вводятся для обмена между ядром и какой-то определённой известной программой (fdisk, hdparm, setserial, tunelp, loadkeys, selection, setfont и т.д.), и их поведение изменяется по требованию этой программы.

    Программы, использующие такие ioctl, не могут быть перенесены в другие версии UNIX, не будут работать в старых версиях Linux и могут не работать в будущих версия Linux.

  • Понравилась статья? Поделиться с друзьями:
    Кодинг, CSS и SQL