6.1. ioctl | |
Большинству драйверов в дополнение к возможности чтения и записи устройства необходима возможность управления аппаратурой разными способами через драйвер устройства. Большинство устройств может выполнять операции за рамками простой передачи данных;
пользовательское пространство часто должно иметь возможность запросить, например, блокировку устройством своих шторок, извлечение носителя информации, сообщить об ошибке информации, изменение скорости передачи, либо самоликвидацию. Эти операции обычно поддерживаются через метод ioctl (команда управления вводом-выводом), который реализует системный вызов с тем же названием.
пользовательское пространство часто должно иметь возможность запросить, например, блокировку устройством своих шторок, извлечение носителя информации, сообщить об ошибке информации, изменение скорости передачи, либо самоликвидацию. Эти операции обычно поддерживаются через метод ioctl (команда управления вводом-выводом), который реализует системный вызов с тем же названием.
В пользовательском пространстве системный вызов ioctl имеет следующий прототип:
int ioctl(int fd, unsigned long cmd, ...);
Прототип выделяется в списке системных вызовов Unix из-за точек, которые обычно отмечают функцию с переменным числом аргументов. Однако, в реальной системе системный вызов в действительности не может иметь переменное количество аргументов. Системные вызовы должны иметь чётко определённые прототипы, потому что пользовательские программы могут получать к ним доступ только через аппаратные "ворота". Таким образом, точки в прототипе представляют не переменное количество аргументов, а один дополнительный аргумент, традиционно определяемый как char *argp. Точки просто не допускают проверку типа во время компиляции. Фактический характер третьего аргумента зависит от используемой команды управления (второй аргумент). Некоторые команды не требуют никаких аргументов, некоторым требуется целые значения, а некоторые используют указатель на другие данные. Использование указателя является способом передачи в вызов ioctl произвольных данных; устройство затем сможет обменяться любым количеством данных с пользовательским пространством.
Неструктурированный характер вызова ioctl вызвал его падение в немилость среди разработчиков ядра. Каждая команда ioctl является, по сути, отдельным, обычно недокументированным системным вызовом, и нет никакой возможности для проверки этих вызовов каким-либо всеобъемлющим образом. Кроме того, трудно сделать, чтобы неструктурированные аргументы ioctl работали одинаково на всех системах; учитывая, например, 64-х разрядные системы с пользовательским процессом, запущенном в 32-х разрядном режиме. В результате, существует сильное давление, чтобы осуществлять разные операции контроля только какими-либо другими способами. Возможные альтернативы включают вложения команд в поток данных (мы обсудим этот подход далее в этой главе) или использование виртуальных файловых систем, или sysfs, или специфичные драйверные файловые системы. (Мы будем рассматривать sysfs в Главе 14.) Тем не менее, остается фактом, что ioctl часто является самым простым и наиболее прямым выбором для относящихся к устройству операций. Метод драйвера ioctl имеет прототип, который несколько отличается от версии пользовательского пространства:
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
Указатели inode и filp являются значениями, соответствующими файловому дескриптору fd, передаваемого приложением, и являются теми же параметрами, передаваемыми в метод open. Аргумент cmd передаётся от пользователя без изменения, а необязательный аргумент arg передаётся в виде unsigned long, независимо от того, был ли он задан пользователем как целое или указатель. Если вызывающая программа не передаёт третий аргумент, значение arg, полученное операцией драйвера, не определено. Из-за отключённой проверки типа дополнительного аргумента компилятор не сможет предупредить вас, если в ioctl передаётся неправильный аргумент, и любая связанная с этим ошибка будет труднообнаруживаемой.
Как можно было себе представить, большинство реализаций ioctl содержат большой переключатель, который выбирает правильное поведение в зависимости от аргумента cmd. Разные команды имеют различные числовые значения, которые для упрощения кодирования обычно задаются символическими именами. Символическое имя присваивается через определение препроцессора. Заказные драйверы обычно объявляют такие символы в своих заголовочных файлах; для scull их объявляет scull.h. Чтобы иметь доступ к этим символам, пользовательские программы должны, конечно, подключить этот заголовочный файл.
6.1.1. Выбор команд ioctl
Прежде чем писать код для ioctl, необходимо выбрать цифры, которые соответствуют командам. Инстинктивно многие программисты выбирают небольшие числа, начиная с 0 или 1 и далее увеличивая значения. Есть, однако, веские причины не делать это таким образом. Номера команд ioctl должны быть уникальными в системе в целях предотвращения ошибок, вызванных правильной командой для неправильного устройства. Такое несоответствие не является маловероятным и программа могла бы обмануть сама себя, пытаясь изменить скорость передачи данных входного потока, не связанного с последовательным портом, такого, как FIFO или аудио устройства. Если каждый номер ioctl является уникальным, программа получает ошибку EINVAL раньше, чем успевает сделать что-то непреднамеренное.
Чтобы помочь программистам создавать уникальные коды команд ioctl, эти коды были разделены на несколько битовых полей. Первые версии Linux использовали 16-ти разрядные числа: верхние восемь "магических" чисел связывались с устройством, а нижние восемь были последовательными номерами, уникальными для данного устройства. Это произошло потому, что Линус был "невежественен" (его собственные слова); лучшее разделение битовых полей было придумано лишь позднее. К сожалению, довольно много драйверов всё ещё используют старое соглашение. Они вынуждены: изменение кодов команд нарушило бы работу многих бинарных программ и это не то, что разработчики ядра готовы сделать.
Для выбора номеров ioctl для вашего драйвера в соответствии с соглашением ядра Linux вы должны сначала проверить include/asm/ioctl.h и Documentation/ioctl-number.txt. Этот заголовок определяет битовые поля для использования: тип (системный номер), порядковый номер, направление передачи и размер аргумента. Файл ioctl-number.txt перечисляет системные номера, используемые в ядре (* Однако, поддержка этого файла была несколько ограниченной в последнее время.), поэтому вы сможете выбрать свой собственный номер в системе и избежать дублирования. В этом текстовом файле также перечислены причины, по которым должно быть использовано данное соглашение.
Утверждённый способ определения номеров команд ioctl использует четыре битовых области, которые имеют следующие значения. Новые символы, введённые в этом списке, определяются в .
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 могут быть извлечены при помощи логической операции.
size
Размер предполагаемых пользовательских данных. Ширина этого поля зависит от архитектуры, но, как правило, 13 или 14 бит. Вы можете найти это значение для вашей конкретной архитектуры в макросе _IOC_SIZEBITS. Не обязательно использовать поле size - ядро не проверяет его - но это хорошая идея. Правильное использование этого поля может помочь обнаружить ошибки программирования в пользовательском пространстве и позволит реализовать обратную совместимость, если вам когда-нибудь понадобится изменить размер соответствующего элемента данных. Однако, если вам требуются более крупные структуры данных, вы можете просто проигнорировать поле size. Как используется это поле, мы рассмотрим в ближайшее время.
Заголовочный файл , который подключается , определяет макрос, который поможет задать номера команд следующим образом: _IO(type,nr) (для команды, которая не имеет аргумента), _IOR(type,nr,datatype) (для чтения данных из драйвера), _IOW(type,nr,datatype) (для записи данных) и _IOWR(type,nr,datatype) (для двунаправленной передачи). Поля type и fields, передаваемые в качестве аргументов, и поле size получаются применением sizeof к аргументу datatype.
Заголовок определяет также макросы, которые могут быть использованы в вашем драйвере для декодирования номеров: _IOC_DIR(nr), _IOC_TYPE(nr), _IOC_TYPE(nr), _IOC_NR(nr) и _IOC_SIZE(nr). Мы не будем углубляться более подробно в эти макросы, так как заголовочный файл ясен, а ниже в этом разделе показан код примера.
Вот как в scull определяются некоторые команды ioctl. В частности, эти команды устанавливают и получают настраиваемые параметры драйвера.
/* Используем 'k' как системный номер */
#define SCULL_IOC_MAGIC 'k'
/* Пожалуйста, используйте в вашем коде другое 8-ми битовое число */
#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
/*
* S означает "Set" ("Установить") через ptr,
* T означает "Tell" ("Сообщить") прямо с помощью значения аргумента
* G означает "Get" ("Получить"): ответ устанавливается через указатель
* Q означает "Query" ("Запрос"): ответом является возвращаемое значение
* X означает "eXchange" ("Обменять"): переключать G и S автоматически
* H означает "sHift" ("Переключить"): переключать T и Q автоматически
*/
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, int)
#define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6, int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7)
#define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)
#define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10, int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12)
#define SCULL_IOC_MAXNR 14
Реальный файл исходника определяет несколько дополнительных команд, которые не были здесь показаны.
Мы решили реализовать оба способа получения целочисленных аргументов: по указателю и явным значением (хотя по принятому соглашению ioctl должна обмениваться значениями по указателю). Аналогичным образом, оба пути используются для возвращения целого числа: по указателю или устанавливая возвращаемое значение. Это работает, пока возвращаемое значение является положительным целым числом; как вы уже знаете, положительное значение сохраняется при возвращении из любого системного вызова (как мы видели для read и write), а отрицательное значение считается ошибкой и используется для установки errno в пользовательском пространстве. (* На самом деле, все реализации libc, использующиеся в настоящее время (включая uClibc), рассматривают как коды ошибок только значения диапазона от -4095 до -1. К сожалению, возможность возвращения больших отрицательных чисел, а не маленьких, не очень полезна.)
Операции "обменять" и "переключить" не являются особенно полезными для scull. Мы реализовали "обмен", чтобы показать, как драйвер может объединить отдельные операции в одну атомарную, и "переключить" для пары к "сообщить" и "запрос". Есть случаи, когда необходимы атомарные операции проверить-и-установить, как эти, в частности, когда приложениям необходимо установить или освободить блокировки.
Точный порядковый номер команды не имеет определённого значения. Он используется только, чтобы отличить команды друг от друга. На самом деле, вы можете даже использовать тот же порядковый номер для команды чтения и записи, поскольку фактический номер ioctl отличается в битах "направления", но нет никакой причины, почему бы вы не захотели сделать это. Мы решили не использовать порядковые номера команд нигде, кроме декларации, поэтому мы не назначили им символические значения. Вот почему в данном ранее определении появляются явные номера. Приведённый пример показывает один из способов использования номеров команд, но вы вольны делать это по-другому.
За исключением небольшого числа предопределённых команд (будет обсуждено в ближайшее время), значение аргумента cmd в ioctl в настоящее время не используется ядром и весьма маловероятно, что это будет в будущем. Таким образом, можно, если вы почувствовали себя ленивым, избегать сложных показанных ранее деклараций и явно декларировать набор скалярных чисел. С другой стороны, если бы вы сделали это, вы бы вряд ли выиграли от использования битовых полей и вы бы столкнулись с трудностями, если бы когда-то представили свой код для включения в основное ядро. Заголовок является примером этого старомодного подхода, использующего для определения команд ioctl 16-ти разрядные скалярные значения. Этот исходный файл полагался на скалярные значения, потому что он использовал конвенцию того времени, а не из-за лени. Изменение его сейчас вызвало бы неуместную несовместимость.
6.1.2. Возвращаемое значение
Реализация ioctl, как правило, использует команду switch, основанную на номере команды. Но что должно быть в варианте default, когда номер команды не совпадает с допустимыми операциями? Вопрос спорный. Некоторые функции ядра возвращают -EINVAL (“Invalid argument”, "Недопустимый аргумент"), что имеет смысл, поскольку этот командный аргумент действительно не является допустимым. Стандарт POSIX, однако, утверждает, что если была выдана неуместная команда ioctl, должно быть возвращено -ENOTTY. Данный код ошибки интерпретируется библиотекой Си как "несоответствующая команда ioctl для устройства", которая является обычно именно тем, что должен услышать программист. Хотя по-прежнему в ответ на недействительную команду ioctl очень распространено возвращение -EINVAL.
6.1.3. Предопределённые команды
Хотя системный вызов ioctl наиболее часто используется для воздействия на устройства, несколько команд распознаются ядром. Обратите внимание, что эти команды при применении к вашему устройству декодируются до того, как вызываются ваши собственные файловые операции. Таким образом, если вы выбираете тот же номер для одной из ваших команд ioctl, вы никогда не увидите запрос для этой команды, а также приложение получит нечто неожиданное из-за конфликта между номерами ioctl.
Предопределённые команды разделены на три группы:
• | Те, которые могут быть выполнены на любом файле (обычный, устройство, FIFO или сокет); |
• | Выполняемые только на обычных файлах; |
• | Зависящие от типа файловой системы; |
Команды последней группы выполняются реализацией главной файловой системы (так, например, работает команда chattr). Авторам драйверов устройств интересна только первая группа команд, чьим системным номером является "Т". Рассмотрение работы других групп остаётся читателю в качестве упражнения; ext2_ioctl является наиболее интересной функцией (и более лёгкой для понимания, чем можно было ожидать), поскольку она реализует флаг "append-only" ("только добавление") и флаг "immutable" ("неизменяемый").
Следующие команды ioctl предопределены для любого файла, включая специальные файлы устройств:
FIOCLEX
- Установить флаг "закрыть-при-выходе" (close-on-exec, File IOctl CLose on EXec). Установка этого флага вызывает закрытие дескриптора файла, когда вызывающий процесс выполняет новую программу.
FIONCLEX
- Очищает флаг "закрыть-при-выходе" (close-on-exec, File IOctl Not CLose on EXec). Команда восстанавливает общее поведение файла, отменяя то, что делает вышеприведённая FIOCLEX.
FIOASYNC
- Установить или сбросить асинхронные уведомления для файла (описывается далее в этой главе в разделе "Асинхронное сообщение"). Заметим, что ядро Linux до версии 2.2.4 неправильно использовало эту команду для модификации флага O_SYNC. Поскольку оба действия можно осуществить с помощью fcntl, никто на самом деле не использует команду FIOASYNC, о которой сообщается здесь только для полноты.
FIOQSIZE
- Эта команда возвращает размер файла или каталога; однако, при применении к файлу устройства она возвращает ошибку ENOTTY.
FIONBIO
- "Файловая IOctl НеБлокирующего Ввода/Вывода" (“File IOctl Non-Blocking I/O”) (описана в разделе "Блокирующие и неблокирующие операции"). Этот вызов изменяет флаг O_NONBLOCK в filp->f_flags. Третий аргумент системного вызова используется для обозначения, должен ли флаг быть установлен или очищен. (Мы будем рассматривать роль этого флага далее в этой главе.) Обратите внимание, что обычным способом изменить этот флаг является системный вызов fcntl, использующий команду F_SETFL.
Последним пунктом в списке представлен новый системный вызов, fcntl, который выглядит как ioctl. Фактически, вызов fcntl очень похож на ioctl тем, что он получает аргумент команды и дополнительный (необязательный) аргумент. Он сохраняется отдельным от ioctl в основном по историческим причинам: когда разработчики Unix столкнулись с проблемой контроля операций ввода/вывода, они решили, чтобы файлы и устройства отличались. В то время единственными устройствами с реализаций ioctl были телетайпы, что объясняет, почему -ENOTTY является стандартным ответом на неправильную команду ioctl. Всё изменилось, но fcntl остаётся отдельным системным вызовом.
6.1.4. Использование аргумента ioctl
Перед просмотром кода ioctl для драйвера scull необходимо сначала разобраться, как использовать дополнительный аргумент. Если это целое число, это несложно: его можно использовать напрямую. Однако, если он является указателем, об этом требуется позаботиться.
Когда указатель используется как ссылка в пространстве пользователя, мы должны гарантировать, что пользовательский адрес является действительным. Попытка доступа с непроверенным заданным пользователем указателем может привести к неправильному поведению, сообщению ядра Oops, повреждению системы, или проблемам с безопасностью. Обеспечение надлежащей проверки каждого используемого адреса пользовательского пространства и возвращение ошибки, если он является недействительным, является ответственностью драйвера.
В Главе 3 мы рассмотрели функции copy_from_user и copy_to_user, которые могут быть использованы для безопасного перемещения данных в и из пространства пользователя. Эти функции могут быть использованы так же и в методах ioctl, но вызовы ioctl часто связаны с небольшими объектами данных, которыми можно более эффективно манипулировать другими способами. Сначала проверка адреса (без передачи данных) осуществляется с помощью функции access_ok, которая объявлена в :
int access_ok(int type, const void *addr, unsigned long size);
Первый аргумент должен быть либо VERIFY_READ или VERIFY_WRITE, в зависимости от того, какое действие будет выполняться: чтение или запись памяти пользовательского пространства. Аргумент addr содержит адрес в пользовательском пространстве, а size является счётчиком байтов. Если, например, ioctl надо прочесть целое число из пользовательского пространства, size является sizeof(int). Если вам необходимы по данному адресу и чтение и запись, используйте VERIFY_WRITE, поскольку это расширенный вариант VERIFY_READ.
В отличие от большинства функций ядра, access_ok возвращает булево значение: 1 в случае успеха (доступ ОК) и 0 для ошибки (доступ не ОК). Если она возвращает ложь, то обычно драйвер должен вернуть вызывающему -EFAULT.
Необходимо обратить внимание на несколько интересных вещей относительно access_ok. Во-первых, она не делает полную работу проверки доступа к памяти; она лишь проверяет, что эта память по ссылке находится в области памяти, к которой этот процесс мог бы разумно иметь доступ. В частности, access_ok гарантирует, что адрес не указывает на память области ядра. Во-вторых, большинству кода драйвера фактически нет необходимости вызывать access_ok. Описанные ниже процедуры доступа к памяти позаботятся об этом за вас. Тем не менее, мы демонстрируем её использование, чтобы вы могли увидеть, как это делается.
Исходник scull проверяет битовые поля в номере ioctl, чтобы проверить аргументы перед командой switch:
int err = 0, tmp;
int retval = 0;
/*
* проверить тип и номер битовых полей и не декодировать
* неверные команды: вернуть ENOTTY (неверный ioctl) перед access_ok( )
*/
if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;
if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;
/*
* направление является битовой маской и VERIFY_WRITE отлавливает передачи R/W
* `тип' является ориентированным на пользователя, в то время как
* access_ok является ориентированным на ядро, так что концепции "чтение" и
* "запись" являются обратными
*/
if (_IOC_DIR(cmd) & _IOC_READ)
err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
else if (_IOC_DIR(cmd) & _IOC_WRITE)
err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));
if (err) return -EFAULT;
После вызова access_ok драйвер может безопасно выполнять фактическую передачу. В дополнение к функциям copy_from_user и copy_to_user программист может использовать набор функций, которые оптимизированы для наиболее часто используемых размеров данных (один, два, четыре и восемь байт). Эти функции описаны в следующем списке и определены в :
put_user(datum, ptr)
__put_user(datum, ptr)
- Эти макросы пишут данные в пользовательское пространство; они сравнительно быстрые и следует вызывать их вместо copy_to_user, когда передаются одинарные значения. Макросы были написаны для передачи в put_user любого типа указателя, пока он является адресом пользовательского пространства. Размер передаваемых данных зависит от типа аргумента ptr и определяется во время компиляции с помощью директив компилятора sizeof и typeof. В результате, если ptr является указателем на char, передаётся один байт, и так далее для двух, четырёх и, возможно, восьми байт. put_user проверяет, чтобы убедиться, что этот процесс может писать по данному адресу в памяти. Он возвращает 0 в случае успеха, и -EFAULT при ошибке. __put_user выполняет меньше проверок (он не вызывает access_ok), но всё ещё может не сработать, если указываемая память не доступна пользователю для записи. Таким образом, __put_user следует использовать только если область памяти уже была проверена access_ok. Как правило, вы вызываете __put_user для выигрыша нескольких циклов, когда вы реализуете метод read, или при копировании нескольких объектов и, таким образом, вызываете access_ok только один раз перед первой передачей данных, как показано выше для ioctl.
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.
6.1.5. Разрешения и запрещённые операции
Доступ к устройству управляется разрешениями на файл(ы) устройства и драйвер обычно не участвует в проверке разрешений. Однако, есть ситуации, когда любой пользователь получает права чтения/записи на устройство, но некоторые операции управления всё ещё должны быть запрещены. Например, не все пользователи ленточного накопителя должны иметь возможность установить размер блока по умолчанию и пользователю, которому был предоставлен доступ на чтение/запись дискового устройства, должно, вероятно, быть отказано в возможности его отформатировать. В подобных случаях драйвер должен выполнять дополнительные проверки, чтобы убедиться, что пользователь имеет право выполнять запрошенную операцию.
В Unix системах привилегированные операции были традиционно ограничены учётной записью суперпользователя. Это означало, что привилегия была вещью "всё или ничего" - суперпользователь может делать абсолютно всё, но все остальные пользователи сильно ограничены. Ядро Linux предоставляет более гибкую систему, названную capabilities (разрешения, мандаты). Система, базирующаяся на разрешениях, оставляет режим "всё или ничего" позади и разделяет привилегированные операции на отдельные подгруппы. Таким образом, каждый пользователь (или программа) могут быть уполномочены выполнять особые привилегированные операции, не предоставляя возможности выполнять другие, не связанные операции. Ядро использует разрешения исключительно для управления правами доступа и экспортирует два системных вызова, называемых capget и capset, чтобы предоставить возможность управлять ею из пространства пользователя.
Полный набор вариантов разрешений можно найти в . Они являются единственными вариантами, известными системе; для авторов драйверов или системных администраторов нет возможности определить новые без изменения исходного кода ядра. Часть этих средств, которые могли бы представлять интерес для авторов драйверов, включает в себя следующее:
CAP_DAC_OVERRIDE
- Возможность отменить ограничения доступа (контроль доступа к данным, data access control или DAC) к файлам и каталогам.
CAP_NET_ADMIN
- Возможность выполнять задачи администрирования сети, в том числе те, которые затрагивают сетевые интерфейсы.
CAP_SYS_MODULE
- Возможность загрузки или удаления модулей ядра.
CAP_SYS_RAWIO
- Возможность выполнять "сырые" операции ввода/вывода. Примеры включают доступ к портам устройств или прямое общение с устройствами USB.
CAP_SYS_ADMIN
- Всеобъемлющее разрешение, которое обеспечивает доступ ко многим операциям по администрированию системы.
CAP_SYS_TTY_CONFIG
- Возможность выполнять задачи конфигурации tty.
Перед выполнением привилегированных операций драйвер устройства должен проверить, имеет ли вызывающий процесс соответствующие разрешения; невыполнение этого может привести к выполнению пользовательским процессом несанкционированных операций с плохими последствиями для стабильности системы или безопасности. Проверки разрешений осуществляются функцией capable (определённой в ):
int capable(int capability);
В примере драйвера scull любой пользователь имеет право запрашивать квант и размер квантового набора. Однако, только привилегированные пользователи могут изменять эти значения, так как неподходящие значения могут плохо повлиять на производительность системы. При необходимости, реализация ioctl в scull проверяет уровень привилегий пользователя следующим образом:
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
В отсутствие более характерного разрешения для этой задачи, для этой проверки был выбран вариант CAP_SYS_ADMIN.
6.1.6. Реализация команд ioctl
Реализация ioctl в scull только передаёт настраиваемые параметры устройства и оказывается простой:
switch(cmd) {
case SCULL_IOCRESET:
scull_quantum = SCULL_QUANTUM;
scull_qset = SCULL_QSET;
break;
case SCULL_IOCSQUANTUM: /* Установить: arg указывает на значение */
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
retval = __get_user(scull_quantum, (int __user *)arg);
break;
case SCULL_IOCTQUANTUM: /* Сообщить: arg является значением */
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
scull_quantum = arg;
break;
case SCULL_IOCGQUANTUM: /* Получить: arg является указателем на результат */
retval = __put_user(scull_quantum, (int __user *)arg);
break;
case SCULL_IOCQQUANTUM: /* Запрос: возвращает его (оно положительно) */
return scull_quantum;
case SCULL_IOCXQUANTUM: /* Обменять: использует arg как указатель */
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
tmp = scull_quantum;
retval = __get_user(scull_quantum, (int __user *)arg);
if (retval == 0)
retval = __put_user(tmp, (int __user *)arg);
break;
case SCULL_IOCHQUANTUM: /* Переключить: как Tell + Query */
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
tmp = scull_quantum;
scull_quantum = arg;
return tmp;
default: /* избыточно, так как cmd была проверена на MAXNR */
return -ENOTTY;
}
return retval;
scull также включает в себя шесть элементов, которые действуют на scull_qset. Они идентичны таким же в scull_quantum и не стоят показа в распечатке.
Шесть способов передать и получить аргументы выглядят примерно следующим образом с точки зрения вызывающего (то есть из пространства пользователя):
int quantum;
ioctl(fd,SCULL_IOCSQUANTUM, &quantum); /* Установить по указателю */
ioctl(fd,SCULL_IOCTQUANTUM, quantum); /* Установить по значению */
ioctl(fd,SCULL_IOCGQUANTUM, &quantum); /* Получить по указателю */
quantum = ioctl(fd,SCULL_IOCQQUANTUM); /* Получить как возвращаемое значение */
ioctl(fd,SCULL_IOCXQUANTUM, &quantum); /* Обменять по указателю */
quantum = ioctl(fd,SCULL_IOCHQUANTUM, quantum); /* Обменять по значению */
Конечно, нормальный драйвер не будет реализовывать такое сочетание режимов вызова. Мы сделали это здесь только чтобы продемонстрировать различные способы, как это можно делать. Однако, обычно обмен данными будет выполняться последовательно либо с помощью указателей, или по значению, и смешения этих двух методов можно было бы избежать.
6.1.7. Управление устройством без ioctl
Иногда управление устройством лучше реализовать записью в него управляющих последовательностей. Например, эта техника используется в консольном драйвере, где так называемые управляющие последовательности используются для перемещения курсора, изменения цвета по умолчанию, или выполняют другие задачи настройки. Пользой от реализации управления устройством таким образом является то, что пользователь может управлять устройством просто записывая данные, без необходимости использовать (или иногда писать) программы, предназначенные только для настройки устройства. Когда устройства могут управляться таким образом, программе, выдающей команды, часто даже не требуется работать на той же системе, где находится контролируемое устройство.
Например, программа setterm воздействует на настройку консоли (или другого терминала) печатая управляющие последовательности.
Управляющая программа может жить на другом компьютере, потому что работу по конфигурации выполняет простое перенаправление потока данных. Это то, что происходит каждый раз, когда вы запускаете удалённый сеанс терминала: управляющие последовательности печатаются удаленно, но воздействуют на местный терминал; техника, однако, не ограничивается терминалами.
Управляющая программа может жить на другом компьютере, потому что работу по конфигурации выполняет простое перенаправление потока данных. Это то, что происходит каждый раз, когда вы запускаете удалённый сеанс терминала: управляющие последовательности печатаются удаленно, но воздействуют на местный терминал; техника, однако, не ограничивается терминалами.
Недостатком контроля печатанием является то, что это добавляет устройству ограничения политик; например, это жизнеспособно только если вы уверены, что управляющая последовательность не может появиться в данных, записываемых в устройство во время нормальной работы. Это только отчасти верно для терминала. Хотя текстовый дисплей предназначен для отображения только ASCII символов, иногда управляющие символы могут проскользнуть в записываемые данные и могут, следовательно, повлиять на установки консоли. Это может произойти, например, когда вы выполняете команду cat для бинарного файла на экран; беспорядок в результате может не содержать ничего и в итоге вы часто имеете на вашей консоли неправильный шрифт.
Управление записью является, определённо, способом, подходящим для тех устройств, которые не передают данные, а только реагируют на команды, таких, как автоматизированные устройства.
Например, драйвер, написанный для удовольствия одним из ваших авторов, передвигает камеру по двум осям. В этом драйвере "устройством" является просто пара старых шаговых двигателей, в которые в действительности нельзя писать или читать из них. Понятие "передача потока данных" в шаговый двигатель имеет мало или вообще не имеет смысла. В этом случае драйвер интерпретирует то, что было записано в виде ASCII команд и преобразует запросы в последовательности импульсов, которые управляют шаговыми двигателями. Отчасти, идея очень похожа на AT команды, которые вы посылаете в модем для установки связи, с главным отличием: последовательный порт, используемый для взаимодействия с модемом, должен так же передавать реальные данные. Преимущество прямого управления устройством в том, что можно использовать cat для перемещения камеры без написания и компиляции специального кода для выдачи вызовов ioctl.
При написании командно-ориентированного драйвера нет никаких причин для реализации метода ioctl. Дополнительные команды проще реализовать и использовать в интерпретаторе.
Иногда, впрочем, можно выбрать действовать наоборот: вместо включения интерпретатора в метод write и избегания ioctl, можно выбрать исключение write вообще и использовать исключительно команды ioctl и сопроводить драйвер специальной утилитой командной строки для отправки этих команд драйверу. Этот подход перемещает сложности из пространства ядра в пространство пользователя, где с ними может быть легче иметь дело, и помогает сохранить драйвер небольшим, запрещая использование простых команды cat или echo.
Комментариев нет:
Отправить комментарий
Примечание. Отправлять комментарии могут только участники этого блога.