Знание того, как написать драйвер для устройства, может стать отличным начальным этапом для знакомства с разработкой ядра Linux. Главной причиной этого являются чистые интерфейсы. В то время, как другие части ядра более тесно связаны, драйвера представляют собой самодостаточные закрытые черные ящики, использующие существующие интерфейсы и возможности, предоставляемые ядром, чтобы в свою очередь обеспечить интерфейс к устройству для ядра.
Введение
Драйвера устройств являются самой активно разрабатываемой частью ядра Linux. Более половины всех внесенных за последний год изменений относились к директории драйверов. Компании постоянно нанимают разработчиков в свои команды или прибегают к услугам независимых команд, предоставляющих профессиональные услуги по исследованию и разработке драйверов, поскольку новые устройства появляются каждый день, в то время как существующие драйвера требуют постоянного улучшения.
Знание того, как написать драйвер для устройства, может стать отличным начальным этапом для знакомства с разработкой ядра Linux. Главной причиной этого являются чистые интерфейсы. В то время, как другие части ядра более тесно связаны, драйвера представляют собой самодостаточные закрытые черные ящики, использующие существующие интерфейсы и возможности, предоставляемые ядром, чтобы в свою очередь обеспечить интерфейс к устройству для ядра.
Тем не менее, это не значит, что разработка драйвера Linux не требует знаний о других компонентах системы. Ядро Linux – это проект с открытым исходным кодом, что позволяет новым людям легко принять участие в разработке. Интересующие вас элементы ядра доступны для изучения, и написание драйверов для устройств Linux прекрасно подходит для того, чтобы посмотреть на ядро в действии.
Задачи драйверов устройств Linux
Пользовательские приложения делегируют управление аппаратной частью ядру операционной системы. Таким образом, приложениям не требуется заботиться о большом количестве деталей, таких как распределение ресурсов устройства, различия между разными типами устройств, аппаратные ограничения, и т.д. Работу с этими деталями берет на себя ядро системы. Механизм, который используется ядром для взаимодействия с аппаратными компонентами, называется драйвер.
Тем не менее, не весь код драйвера сосредоточен на конкретных аппаратных деталях. Например, возьмем USB устройства. USB камера и USB накопитель очевидно требуют разных драйверов, но эти драйвера могут использовать один и тот же код протокола USB. Жесткий диск, подключенный через SCSI, и флэш-накопитель, подключенный через USB, оба хранят данные пользователей, и оба этих устройства могут использовать одинаковый код управления файловой системой на этих устройствах.
Ядро Linux разделяет такие концепты, как интерфейсы, шины и файловые системы на отдельные модули, ответственные только за свои определенные задачи. Их иногда называют программными драйверами, потому что они не отвечают за управление конкретным аппаратным устройством.
Классы устройств
Различные устройства под управлением ядра можно разделить на ряд фундаментальных классов согласно принципиальному способу взаимодействия с ними. Каждый драйвер обычно имплементирует и поддерживает устройство только определенного класса.
Тем не менее, не существует жестких ограничений на то, какие задачи могут выполнять драйвера. Драйвер может зарегистрировать себя для поддержки стольких классов устройств, сколько требуется, и предоставить столько интерфейсов файловой системы, сколько этим устройствам необходимо.
Драйвера Linux могут поддерживать следующие типы устройства:
Символьные устройства (character devices)
Подобные устройства могут быть представлены как поток битов, и они распознаются системой как файл в директории /dev, который может быть последовательно прочитан. Примером таких устройств служат серийные порты и терминалы. Важным является тот факт, что эти устройства предоставляют поток байтов, к которому нельзя получать доступ в произвольном порядке. Помимо стандартных примитивов ядра, в драйвере такого устройства должны быть имплементированы запросы, такие как open(), close(), read(), write() для чтения последовательности байтов.Блочные устройства (block devices)
Блочные устройства представляют собой файловые системы. Они используются для того, чтобы считывать или записывать только блоки фиксированного размера (обычно кратного двум, например, 512 байт). Такие устройства также представлены в качестве файлов в директории /dev и выглядят одинаково на уровне пользователя. Пользователю не нужно волноваться о размере блоков и других деталях, потому что драйвер устройства сам сможет справится с этими деталями, реализовав привычный интерфейс системных вызовов с использованием различных примитивов для чтения и записи данных.
Сетевые интерфейсы (network interfaces)
Сетевые интерфейсы используются для передачи информации между несколькими компьютерами. Они считаются отдельным классом, поскольку такие устройства строятся вокруг приема и отправки пакетов данных, что не имеет отношения к сетевым подключениям, устанавливаемым пользовательскими приложениями. Эти устройства не представляются в виде файлов, но тем не менее, имеют уникальные имена, такие как eth0 или wlan1. Пользовательские приложения также взаимодействуют с сетевыми интерфейсами путем использования особых системных вызовов, таких как accept(), send(), and recv().
Модульные драйвера
Некоторая часть кода ядра является необходимой для его работы, в частности элементы, отвечающие за управление памятью. Эти элементы встроены в ядро и загружаются во время процесса загрузки системы. Тем не менее, существуют и опциональные части, которые могут сразу не понадобиться. Драйвера для устройств, которых у вас нет, или поддержка файловой системы, которую вы не используете, не являются чем-то необходимым. Загрузка этой информации в память просто приведет к потере ресурсов. И естественно, никому не хочется перекомпилировать ядро и перезапускать компьютер, когда новый драйвер все-таки понадобится.
Ядро Linux предлагает модульный механизм для того, чтобы динамично расширять свой код во время выполнения. Модули ядра по сути представляют собой плагины для ядра, которые могут быть установлены и удалены, когда требуется. Модули также можно собирать отдельно, используя заголовки ядра для связи с заранее скомпилированным ядром. Система сборки ядра Linux позволяет собирать драйвера устройств двумя способами – либо встраивать их в ядро, либо компилировать как отдельный модуль.
Драйвера пространства пользователя
Существует также третий вариант – вообще не писать драйвер на уровне ядра. В некоторых случаях можно управлять устройством с помощью пользовательского приложения с определенным уровнем привилегий, без обращения к драйверу внутри ядра.
Если устройство будет использоваться только одним приложением, например, сервер, который в одиночку управляет экраном, то можно написать для него процесс-демон. Ядро Linux предлагает доступ на уровне пользователя к физической памяти через файл /dev/mem, кроме того можно использовать порты посредством прямого обращения. Другие приложения смогут использовать процесс-демон для взаимодействия с устройством.
Другой подход – создание небольшого драйвера уровня ядра, который экспортирует стабильный интерфейс в ядро, и затем создать к нему расширяемую часть пользовательского уровня. Например, можно писать драйвера для USB устройств на уровне пользователя, используя libusb, или имплементировать поддержку файловой системы на уровне пользователя с помощью FUSE.
Разработка драйверов Linux в пользовательском пространстве предлагает ряд преимуществ:
Драйвер может быть написан с помощью любого языка и с использованием любых библиотек. Нет ограничений, заставляющих пользоваться только С и утилитами ядра.
Некоторые задачи, такие как обращение к файлам настройки, проще выполнить из пользовательского пространства.
Отладка приложений в пользовательском пространстве проще, чем отладка ядра.
Неправильно работающее приложение можно просто завершить и перезапустить, в то время как для перезапуска неправильно работающего драйвера скорей всего потребуется перезагрузка системы.
В пользовательских приложениях можно использовать вычисления с плавающей точкой. Плавающая точка запрещена в ядре исходя из соображений производительности.
Если пользовательское приложение активно не используется, оно не потребляет физической памяти. Загруженный модуль ядра всегда остается в физической памяти независимо от его использования.
Распространение пользовательских приложений с закрытым исходным кодом проще с точки зрения закона.
Тем не менее, некоторые вещи невозможно реализовать в пользовательском режиме:
В пользовательском режиме невозможно обрабатывать аппаратные прерывания.
Доступ к физической памяти и портам есть только у администратора системы.
Многим оборудованием, например, блочными устройствами или сетевыми интерфейсами, можно управлять только на уровне ядра.
Контекстные переключения между пользовательским режимом и режимом ядра могут привести к ухудшению производительности.
Выгруженные драйвера могут еще сильнее повлиять на производительность, что может оказаться недопустимым.
Потенциальные проблемы при разработке
Проблемы безопасности драйверов Linux
Ошибки ядра могут оказаться фатальными для системы
При написании драйверов уровня ядра важно учитывать безопасность. Возможности процессов пользовательского уровня ограничены. Вредоносный или некорректно работающий процесс не сможет навредить всей системе и может быть просто остановлен, если нужно. В то же время, кода уровня ядра имеет самую высокую степень привилегий в системе, и чтобы остановить его исполнение, необходимо перезагрузить систему.
Учитывайте память и ресурсы
При программировании на С можно очень легко совершить определенные ошибки, которые затем превращаются в проблемы с безопасностью, такие как переполнение целочисленных переменных, переполнение буфера, утечки памяти, двойное освобождение памяти, взаимные блокировки, и т.д. Тем не менее, большую часть этих проблем можно обнаружить во время процесса проверки кода. Такие проблемы реже возникают при написании драйверов, так как они имеют четко определенный интерфейс с ядром, который контролирует и предотвращает ряд проблем.
Никогда не доверяйте пользователю
Пользователи – это второй источник потенциальных угроз безопасности. Код на уровне ядра никогда не должен доверять полученным от пользователя данным и должен всегда проверять их валидность. Драйвер не должен предполагать, что введенные данные всегда верны, так как они могут идти от некорректно работающего или вредоносного процесса.
Следует также быть осторожным по поводу того, какую информацию возвращает драйвер, так как она может негативно отразится на безопасности пользователя. Если драйвер проецирует страницы памяти в пользовательский процесс, чтобы вернуть определенный набор данных, то эти страницы необходимо сначала обнулить, чтобы исключить возможность просмотра содержания физической памяти какого-либо другого процесса.
И хотя код драйвера не должен диктовать политики доступа и безопасности пользователю, будет полезным запретить действия, способные повлиять на всю систему (такие, как форматирование диска) для не привилегированных пользователей.
Лицензирование драйверов Linux
Ядро Linux распространяется под лицензией GNU General Public Licence (GPL). Эта лицензия предполагает сохранение прав (copyleft) при предоставлении доступа к исходному коду и разрешает распространение программного обеспечения с или без модификаций только под той же самой GPL лицензией. Идея лицензии заключается в том, чтобы дать все возможность изучать, модифицировать и улучшать программное обеспечение, таким образом, открытие исходного кода для общего доступа является обязательным.
Тем не менее, не все готовы открывать исходный код своих драйверов. Как указано выше, драйвера могут быть либо встроены в ядро, либо распространяться в качестве отдельных модулей. И хотя GPL запрещает распространение всего ядра в бинарной форме, она не запрещает распространение в закрытом виде отдельных модулей, которые только взаимодействуют с ядром. Подобная практика пока что принимается Linux Software Foundation, но это не означает, что распространять бинарные драйвера легко.
Как бы там ни было, при распространении программ с закрытым исходным кодом для Linux следует для начала получить юридический совет от профессионала.
Проблемы публикации драйверов Linux
При публикации драйвера у вас есть возможность включить его в дерево исходного кода ядра. Естественно, такой подход требует от вас открыть свой исходный код под лицензией GNU GPL, но у него есть ряд преимуществ:
Другие люди смогут улучшить ваш драйвер, добавить в него возможности, исправить ошибки, оптимизировать производительность.
Другие люди смогут обновить ваш драйвер, если интерфейсы ядра изменятся.
Стоимость поддержки драйвера будет распределена между разными участниками сообщества разработчиков ядра.
Ваш драйвер будет автоматически включен во все дистрибутивы Linux, не требуя от вас специально просить дистрибьютора сделать это.
Тем не менее, если в вашем случае GPL не подходит, то вы должны распространять свой драйвер в качестве отдельного модуля. Основная проблема подобного подхода заключается в том, что ядро Linux не имеет стабильного внутреннего API. Таким образом, вам необходимо будет вручную отслеживать изменения в ядре и обновлять свой исходный код самостоятельно с каждой новой версией.
Дополнительная литература
Надеемся, эта статья смогла дать вам представление о том, что нужно, чтобы разработать простой драйвер для Linux. Мы предлагаем список полезной литературы на эту тему, с которой вы также можете в дальнейшем ознакомиться:
Linux Device Drivers за авторством разработчиков Allessandro Rubini, Jonathan Corbet, and Greg Kroah-Hartman, издана O Reilly и доступна онлайн. Это полноценный обзор возможностей, предоставляемых ядром Linux для написания драйверов различных устройств.
Essential Linux Device Drivers за авторством инженера Sreekrishnan Venkateswaran, издана Prentice Hall. Еще одно хорошее руководство по возможностям Linux, похожее на Linux Device Drivers, но содержащее гораздо больше информации о низкоуровневых деталях и покрывающее больше тем.
Understanding the Linux Kernel за авторством Daniel P. Bovet and Marco Cesati, изданное O Reilly. Обзор архитектуры и подсистем ядра основанный на курсе по операционным системам, преподаваемом в University of Rome «Tor Vergata».
И хотя разработка драйверов является сложным процессом, практика позволит вам его покорить. Поэтому приступим к разработке!