Как написать свой первый linux device driver

Обновлено: 03.07.2024

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

В настоящей статье описывается сетевой драйвер для сетевой платы RealTek 8139. Я выбрал чипсет RealTek по следующим двум причинам: Во-первых, компания RealTek бесплатно предоставляет технические спецификации на этот чипсет (спасибо, RealTek!). Во-вторых, он сравнительно дешев. В Индии его можно приобрести менее, чем за 300 рупий (приблизительно 7 долларов США).

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

Часть 2: Пишем в классе наш первый драйвер для Linux

Оригинал: "Device Drivers, Part 2: Writing Your First Linux Driver in the Classroom"
Автор: Anil Kumar Pugalia
Дата публикации: December 1, 2010
Перевод: Н.Ромоданов
Дата перевода: июнь 2012 г.

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

Пагс поспешно ответил, что они обсуждали именно ту тему, которую сегодня изучают в классе — драйверы устройств в Linux. Пагс был более, чем счастлив, когда профессор сказал: "Хорошо! Тогда что-нибудь скажите о динамической загрузке в Linux. Если вы справитесь, то я прощу вас обоих!". Пагс знал, что один из способов сделать профессора счастливым, это — покритиковать Windows.

Он объяснил: "Как известно, при обычной установке драйверов в Windows для того, чтобы их активировать, необходимо перезагрузить систему. А если это, предположим, действительно неприемлемо в случае, если это нужно делать на сервере? Вот где выигрывает Linux. В Linux можно загружать и выгружать драйверы на лету, и это активно используется сразу после загрузки системы. Кроме того, драйвер мгновенно отключается после его выгрузки. Это называется динамической загрузкой и выгрузкой драйверов в Linux ".

Это впечатлило профессора. "Хорошо! Идите на свои места, но больше не опаздывайте". Профессор продолжил лекцию: "Теперь, когда вы уже знаете, что такое динамическая загрузка и выгрузка драйверов, я, прежде, чем мы перейдем к написанию нашего первого драйверов, покажу вам, как загружать и выгружать драйвера".

Начинаем разработку драйвера

Рассмотрим разработку драйвера поэтапно по следующим пунктам:

  1. Обнаружение устройства
  2. Включение устройства
  3. Что такое сетевые устройства
  4. Доступ к устройству не через шину
  5. Что такое конфигурационное адресное пространство PCI
  6. Инициализация net_device
  7. Какой механизм передачи используется в RealTek8139
  8. Какой механизм приема используется в RealTek8139
  9. Делаем устройство готовым к передаче пакетов
  10. Делаем устройство готовым к приему пакетов

Обнаружение устройства

В качестве первого шага нам нужно обнаружить устройство, которое нас интересует. В ядре Linux представлен богатый набор API для обнаружения устройств, использующих шину PCI (Plug & Play), но мы будет использовать самое простое API - pci_find_device. Таблица 1: Обнаружение устройства

Каждый разработчик имеет уникальный, назначенный только ему идентификатор ID и назначает уникальный идентификатор ID каждому конкретному виду устройств. Макросы REALTEK_VENDER_ID и REALTEK_DEVICE_ID определяют эти идентификаторы ID. Вы можете найти эти значения в "PCI Configuration Space Table" в спецификациях RealTek8139.

Включение устройства

После того, как устройство обнаружено, то, прежде чем как-то с ним взаимодействовать, нам нужно его включить. Продолжим фрагмент кода, приведенный в таблице 1, который теперь обнаруживает устройство и включает его. Таблица 2: Обнаружение и включение устройства
  • Проверяет, что мы работаем на PCI-совместимой системе
    • pci_present вернет NULL, если система не поддерживает PCI

    Давайте на время приостановим процесс изучения кода драйвера; вместо этого мы рассмотрим несколько важных тем, чтобы понять, чем с точки зрения Linux является сетевое устройство. Мы рассмотрим сетевые устройства и разницу между вводом-выводом с отображением в память (memory-mapped I/O), вводом-выводом с отображением по портам (port-mapped I/O) и конфигурационным адресным пространством PCI (PCI configuration space).

    Что такое сетевые устройства

    Мы обнаружили устройство PCI и включили его, но в Linux сетевые устройства рассматриваются как интерфейсы в сетевом стеке. Для этого используется структура net_device. В таблице 3 перечисляются некоторые важные поля структуры net_device, которая будут использоваться далее в настоящей статье. Таблица 3: Структура net_device

    Хотя полей в этой структуре значительно больше, для нашего минимального драйвера вполне достаточно перечисленных. Рассмотрим их подробнее:

    Хотя мы не упомянули все поля структуры net_device, пожалуйста, обратите внимание на то, что нет никаких ссылок на функцию, принимающую пакеты. Это делается обработчиком прерываний устройства, что мы также рассмотрим далее в настоящей статье.

    Доступ к устройству не через шину

    Ввод-вывод с отображением в память (Memory-Mapped I/O)

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

    После того, как Вы закончите использовать устройство (скажем, в вашей подпрограмме выхода из модуля), вызовите iounmap для того, чтобы вернуть ядру адресное пространство. Архитектура большинства систем позволяет выделять новое адресное пространство каждый раз, когда Вы вызываете ioremap, и использовать его до тех пор, пока Вы не вызовете iounmap.

    Доступ к устройству

    Часть интерфейса, наиболее используемая драйверами, это чтение из регистров устройства, отображаемых в память, и запись в них. Linux предоставляет интерфейс для чтения и записи блоков размером 8, 16, 32 или 64 бита. Исторически сложилось так, что они называются доступом к байту (byte), к слову (word), к длинному числу (long) и к двойному слову или четверке слов (quad). Названия функций следующие - readb, readw, readl, readq, writeb, writew, writel и writeq.

    Для некоторых устройств (работающих, например, с буферами кадров) было бы удобнее за один раз передавать блоки, значительно большие чем 8 байтов. Для этих устройств предлагается использовать функции memcpy_toio, memcpy_fromio и memset_io. Не используйте memset или memcpy для работы с адресами ввода/вывода; они не гарантируют копирование данных в правильном порядке.

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

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

    Доступ к пространству портов

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

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

    Доступ к пространству портов или устройства с отображением ввода-вывода

    Доступ к этому пространству обеспечивается с помощью набора функций, в которых допускается доступ к 8, 16 и 32 битам, известных как байт (byte), слово (word ) и длинное слово (long). Это следующие функции - inb, inw, inl, outb, outw и outl.

    Эти функции имеют несколько вариантов. Для некоторых устройств требуется, чтобы доступ к их портам происходил со сниженной скоростью. Эта функциональность обеспечивается при помощи добавления _p в конце команды. Имеются также эквиваленты команды memcpy. Функции ins и out копируют байты, слова и длинные слова в заданный порт и из него.

    Что такое конфигурационноеадресное пространство PCI (PCI Configuration Space)

    В этом разделе мы рассмотрим конфигурационное адресное пространство PCI. Устройства PCI имеют 256 байтное адресное пространство. Первые 64 байта используются стандартным образом, тогда как использование оставшихся байтов зависит от устройства. На рис.1. показано стандартное конфигурационное адресное пространство PCI

    Конфигурационное адресное пространство

    Рис.1: Конфигурационное адресное пространство

    Поля "Vendor ID" и "Device ID" содержат уникальные идентификаторы, присвоенные разработчику устройств и устройству соответственно. Мы уже обсуждали их в разделе "Обнаружение устройства". Отметим еще поле - "Base Address Registers" (базовые адресные регистры), известное еще как BAR. Мы кратко расскажем, как использовать регистры BAR.

    Инициализация net_device

    Теперь можно вернуться к разработке драйвера. Перед этим я напомню вам о поле priv в структуре net_device. Мы объявим структуру, в которой будут храниться приватные данные вашего устройства, а указатель на эту структуру должен храниться в поле priv. В структуре должны быть следующие поля (по мере разработки мы будет добавлять в структуру новые компоненты).

    Таблица 4: Структура rtl8139_private

    В остальной части функции init_module мы теперь определяем указатель net_device и инициализируем его.

    Таблица 5: Инициализация net_device

    Теперь давайте объясним, что мы сделали в таблице 5. Функцию probe_for_realtek8139 мы уже рассматривали. Функция rtl8139_init распределяет память для локального указателя rtl8139_dev, который мы должны использовать как net_device. Вдобавок эта функция заполняет компоненту pci_dev of rtl8139_private для обнаруженного устройства.

    В оставшейся части кода, приведенного в таблице 5, выполняется обычная инициализация структуры net_device. Заметьте, что теперь мы читаем аппаратный адрес из устройства и записываем его в dev_addr. Если Вы смотрели раздел "Описания регистров" в спецификации RealTek8139, то знаете, что первые 6 байтов являются аппаратным адресом устройства. Также мы должны иметь проинициализированные компоненты указателя на функцию, но не должны определять какую-либо соответствующую функцию. Теперь для компиляции модуля мы добавим фиктивные функции.

    Таблица 6: Фиктивные функции

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

    Таблица 7: Функция cleanup_module

    Теперь мы имеем готовый фиктивный драйвер или драйвер-шаблон. Откомпилируйте модуль и вставьте его в ядро так, как показано в таблице 8 (предполагается, что исходный код ядра - /usr/src/linux-2.4.18 ).

    Таблица 8: Компиляция драйвера

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

    Какой механизм передачи используется в RealTek 8139

    В этом разделе описывается механизм передачи данных в устройстве RealTek8139; однако рекомендуется загрузить руководство по программированию "RTL8139 (A/B) Programming Guide", в котором приведены все подробности. В RealTek8139 имеется 4 дескриптора передачи, каждый дескриптор имеет фиксированное смещение адреса ввода-вывода. Четыре дескриптора используются циклически. Это означает, что для передачи четырех пакетов драйвер будет использовать в циклическом порядке дескриптор 0, дескриптор 1, дескриптор 2 и дескриптор 3. Для передачи следующего пакета драйвер снова будет использовать дескриптор 0 (при условии, что он свободен). В спецификациях RealTek8139 в разделе "Описание регистров" указывается, что регистры TSAD0, TSAD1, TSAD2 и TSAD3 имеют смещения 0x20, 0x24, 0x28, 0x2C, соответственно. В этих регистрах хранится "начальный адрес дескрипторов передачи ", т.е. в них хранится стартовый адрес (в памяти) пакетов, которые должны быть переданы. Позже устройство считает содержимое пакетов из этих адресов DMA, перепишет их в свой собственный стек FIFO, а затем выполнит передачу данных в сеть.

    Мы скоро увидим, что этот драйвер выделяет память прямого доступа (доступ DMA), где будут храниться пакеты, и записывает адрес этой памяти в регистры TSAD.

    Какой механизм приема используется в RealTek 8139

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

    Делаем устройство готовым к передаче пакетов

    Компонента tx_flag должна содержать флаги передачи, уведомляющие устройство о некоторых параметрах, о которых мы скоро расскажем. Поле cur_tx должно хранить текущий дескриптор передачи, а dirty_tx указывает на первый из дескрипторов передачи, для которых передача еще не завершена (это означает, что мы можем использовать эти занятые дескрипторы для следующей передачи пакетов до тех пор, пока предыдущий пакет не будет полностью передан). В массиве tx_buf хранятся адреса четырех "дескрипторов передачи". Как мы скоро увидим, поле tx_bufs используется для тех же самых целей. В tx_buf и в tx_bufs должен храниться именно виртуальный адрес, который может использоваться драйвером, но устройство не может использовать такие адреса. Устройству для доступа нужен физический адрес, который запоминается в поле tx_bufs_dma. Ниже приведен список смещений регистров, используемых в исходном коде. Более подробную информацию об этих значениях Вы можете получить из спецификаций RealTek8139.

    Таблица 10: Определения регистров RTL 8139

    Рассмотрим функцию rtl8139_open с учетом сделанного выше определения:

    Таблица 11: Пишем функцию открытия устройства

    Таблица 12: Пишем функцию start_xmit

    Функция rtl8139_start_xmit, показанная в таблице 12, исключительно тривиальная. Сначала она ищет имеющийся дескриптор передачи, а затем проверяет, чтобы размер пакета был, по меньшей мере, 60 байтов (поскольку размер пакетов Ethernet не может быть меньше 60 байтов). Как только это будет сделано, будет вызвана функция skb_copy_and_csum_dev, которая скопирует содержимое пакетов в имеющуюся память DMA. Следующей функцией writel мы информируем устройство о длине пакета. После этого пакеты передаются в сеть. Затем мы определяем имеющиеся в наличии следующие дескрипторы передачи и, если так случится, что он будет равен дескриптору, для которого передача еще не завершена, то мы остановим устройство; в противном случае мы просто выйдем из функции.

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

    Делаем устройство готовым к приему пакетов

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

    Таблица 13: Расширяем структуру rtl8139_private

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

    Таблица 14: Расширяем функцию rtl8139_open

    Теперь, когда мы рассмотрели функцию rtl8139_open, рассмотрим функцию rtl8139_hw_start, в которой сконфигурируем устройство для приема пакетов.

    Таблица 15: Расширяем функцию rtl8139_hw_start

    Таблица 16: Обработчик прерываний

    Последняя функция, которую мы хотим добавить - rtl8139_get_stats, она просто возвращает tp->stats.

    Таблица 17: Функция rtl8139_get_stats

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

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

    Как написать свой первый Linux device driver

    Цель данной статьи — показать принцип реализации драйверов устройств в системе Linux, на примере простого символьного драйвера.

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

    Это моя первая статья, пожалуйста не судите строго!

    Получилось слишком много букв, поэтому я принял решение разделить статью на три части:

    Часть 1 — Введение, инициализация и очистка модуля ядра.
    Часть 2 — Функции open, read, write и trim.
    Часть 3 — Пишем Makefile и тестируем устройство.

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

    Инициализация

    Теперь давайте посмотрим на функцию инициализации устройства.


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


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

    Выделяем память, делая вызов функции kmalloc и обязательно проверяем указатель на NULL.

    Стоит упомянуть, что вместо вызова двух функций kmalloc и memset, можно использовать один вызов kzalloc, который выделят область памяти и инициализирует ее нулями.


    Продолжаем инициализацию. Главная здесь функция — это scull_setup_cdev, о ней мы поговорим чуть ниже. MKDEV служит для хранения старший и младших номеров устройств.


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


    Выше были представлены структуры scull_dev и cdev, которые реализуют интерфейс между нашим устройством и ядром. Функция scull_setup_cdev выполняет инициализацию и добавление структуры в систему.

    Удаление

    Функция scull_cleanup_module вызывается при удалении модуля устройства из ядра.
    Обратный процесс инициализации, удаляем структуры устройств, освобождаем память и удаляем выделенные ядром младшие и старшие номера.

    С удовольствием выслушаю конструктивную критику и буду ждать feedback'a.

    Если вы нашли ошибки или я не правильно изложил материал, пожалуйста, укажите мне на это.
    Для более быстрой реакции пишите в ЛС.

    Подведем итог

    Как только у нас будет файл ofd.ko , мы в роли пользователя root или с помощью команды sudo выполним обычные действия.

    Команда lsmod должна вам сообщить о том, что драйвер ofd загружен.

    Пока студенты экспериментировали со своим первым модулем, прозвенел звонок, сообщивший об окончании урока. Профессор Гопи подвел итог: "В настоящий момент мы не увидели ничего, кроме того, что модуль lsmod сообщил о загрузке драйвера. Куда выводит информацию команда printk ? Найдите это самостоятельно на лабораторных занятиях и познакомьте меня с своими выводами. Также учтите, что наш первый драйвер будет шаблоном для любого драйвера, который можно написать для Linux. Написание специализированных драйверов это всего лишь вопрос о том, чем будет заполнен конструктор и деструктор драйвера. Поэтому дальнейшее изучение будет представлять собой расширение данного драйвера с целью получить драйвер с конкретными функциональными возможностями".

    Русские Блоги

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

    1. Простой пример драйвера

    Файл драйвера hello.c

    Файл драйвера в основном включает в себя функции hello_open, hello_write, hello_init, hello_exit. Тестовый пример не дает модулю драйвера практическую функцию, а только печатает журнал, чтобы сообщить консоли некоторую отладочную информацию, чтобы мы могли понять выполнение драйвера ,

    При печати с использованием printk добавление «KERN_EMERG» к параметрам может обеспечить вывод информации для печати на консоль. Поскольку printk print разделен на 8 уровней, верхний уровень выводится на консоль, а нижний уровень выводится в файл журнала.

    Makefile требуется для компиляции драйвера

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

    Тестовый верхний код приложения hellotest.c

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

    Во-вторых, пример теста диска

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

    1. Скомпилируйте драйвер

    Создайте команду, напрямую вызовите Makefile для компиляции hello.c и, наконец, сгенерируйте «hellomodule.ko».

    2. Скомпилируйте приложение верхнего уровня

    С помощью этой команды вы можете скомпилировать приложение hellotest верхнего уровня.

    3. Загрузите драйвер

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


    Кроме того, вы можете увидеть загруженные модули в "/ proc / devices".


    4. Создайте узел

    Хотя драйвер hellomodule.ko был загружен, и загруженный модуль HelloModule также отображается в файле / proc / devices, этот модуль не может использоваться, поскольку в каталоге устройств / dev нет соответствующего файла устройства. Поэтому вам необходимо создать узел устройства.

    Номер основного устройства модуля HelloModule в / proc / devices равен 231. Когда узел создается, файл устройства / dev / hellodev подключается к номеру основного устройства. Таким образом, когда приложение манипулирует файлом / dev / hellodev, оно обнаружит модуль HelloModule.


    • Устройство в / proc / devices генерируется драйвером, оно может генерировать мажор для mknod в качестве параметра. Содержимое этого файла показывает модули, которые в настоящее время смонтированы в системе. Когда загружается драйвер HelloModule, соответствующий файл устройства не генерируется для абстрактной инкапсуляции устройства для доступа к приложениям верхнего уровня.
    • Устройство в / dev добавляется mknod, и пользователь получает доступ к драйверу через это имя устройства. Я думал, что файлы в / dev можно рассматривать как абстрактный пакет аппаратных модулей, и все устройства в Linux были упакованы в виде файлов.

    5. Вызов драйвера приложения верхнего уровня

    Приложение hellotest сначала открывает файл "/ dev / hellodev", а затем записывает переменную val в этот файл. В течение этого периода будут вызваны функции hello_open и hello_write в базовом драйвере. Ниже приведены результаты работы hellotest.


    6. Удалите драйвер

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


    Суммируйте последовательность операций модуля:

    (1) Зарегистрируйте модуль с помощью команды insmod

    (2) Создайте файл устройства «xxx» в каталоге / dev с помощью команды mknod и установите соединение с модулем через основной номер устройства.

    (3) Прикладной уровень управляет базовым модулем через файл устройства / dev / xxx

    Три, диск шаблон

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

    1. Заголовочные файлы

    init.h определяет функции, связанные с инициализацией и выходом драйвера
    kernel.h определяет часто используемые прототипы функций и определения макросов
    module.h определяет функции, переменные и макросы, связанные с модулем ядра

    2. Функция инициализации

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

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


    3. Выход из функции

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

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

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

    4. Информация об авторских правах

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

    5. Функциональная функция

    В-четвертых, процесс выполнения от приложения верхнего уровня до драйвера нижнего уровня

    1. Иерархическая структура системы Linux


    Многоуровневая структура системы Linux: прикладной уровень ----> библиотека ----> ядро ​​----> драйвер ----> аппаратное устройство.

    2. Процесс выполнения от приложения верхнего уровня до драйвера нижнего уровня

    Возьмем функцию "open (" / dev / hellodev ", O_RDWR)" в качестве примера для иллюстрации.

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

    (2) Библиотека выполняет swi-инструкцию в соответствии с параметрами, переданными функцией open, что приведет к сбоям в работе процессора и попаданию в ядро.

    (3) Функция обработки исключений ядра находит соответствующий драйвер в соответствии с этими параметрами.

    (4) Выполните соответствующий драйвер.

    (5) Верните дескриптор файла в библиотеку, а затем в приложение.

    3. Характеристики исполнения водителя

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

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

    «Полное руководство по разработке приложений для встроенного Linux»

    Динамическая загрузка драйверов

    Эти динамически загружаемые драйвера чаще всего называют модулями, которые собираются в виде отдельных модулей с расширением .ko (объект ядра). В каждой системе Linux в корне файловой системы (/) есть стандартное место для всех предварительно собранных модулей. Они организованы аналогично древовидной структуре исходных кодов ядра и находятся в директории /lib/modules/<kernel_version>/kernel , где <kernel_version> результат вывода системной команды uname -r (см.рис.1).


    Рис.1: Предварительно собранные модули Linux

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

    • lsmod — список модулей, загруженных в текущий момент
    • insmod <module_file> — добавление / загрузка указанного файла модуля
    • modprobe <module> — добавление / загрузка модуля вместе со всеми его зависимостями
    • rmmod <module> — удаление / выгрузка модуля

    Давайте в качестве примера рассмотрим соответствующие драйвера файловой системы FAT. На рис.2 показан весь процесс нашего эксперимента. Файлы с модулями будут fat.ko , vfat.ko и т.д., находящиеся в директории fat (в vfat для старых версий ядра) в /lib/modules/`uname -r`/kernel/fs . Если они представлены в сжатом формате .gz , вам нужно будет распаковать их с помощью команды gunzip , прежде чем вы сможете выполнить операцию insmod .


    Рис.2: Операции с модулями Linux

    Модуль vfat зависит от модуля fat , так что первым должен быть загружен модуль fat.ko . Чтобы автоматически выполнить распаковку и загрузку зависимостей, воспользуйтесь командой modprobe . Обратите внимание, что когда вы пользуетесь командой modprobe , вы не должны в имени модуля указывать расширение .ko . Команда rmmod используется для выгрузки модулей.

    Подготовительные работы

    Спасибо Kolyuchkin за уточнения.

    Символьный драйвер (Char driver) — это, драйвер, который работает с символьными устройствами.
    Символьные устройства — это устройства, к которым можно обращаться как к потоку байтов.
    Пример символьного устройства — /dev/ttyS0, /dev/tty1.

    К вопросу про проверсию ядра:

    Драйвер представляет каждое символьное устройство структурой scull_dev, а также предостовляет интерфейс cdev к ядру.


    Устройство будет представлять связный список указателей, каждый из которых указывает на структуру scull_qset.


    Для наглядности посмотрите на картинку.

    Для регистрации устройства, нужно задать специальные номера, а именно:

    MAJOR — старший номер (является уникальным в системе).
    MINOR — младший номер (не является уникальным в системе).

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

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


    В ядре есть специальные макросы module_init/module_exit, которые указывают путь к функциям инициализации/удаления модуля. Без этих определений функции инициализации/удаления никогда не будут вызваны.


    Здесь будем хранить базовую информацию об устройстве.


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

    Драйверы устройств в Linux

    Сборка нашего первого драйвера

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

    Чтобы собрать драйвер для Linux, у вас в системе должен быть исходный код ядра (или, по крайней мере, заголовки ядра). Предполагается, что исходный код ядра будет находиться в директории /usr/src/linux . Если в вашей системе он находится в каком-нибудь другом месте, то укажите это место в переменной KERNEL_SOURCE в файле Makefile .

    Когда есть код на языке C ( ofd.c ) и готов файл Makefile , то все, что нам нужно сделать для сборки нашего первого драйвера ( ofd.ko ), это вызвать команду make .

    Подготовка к разработке драйвера

    Прежде, чем начать разработку драйвера, нам нужно для этого подготовить систему. Настоящая статья была написана и проверена для ядра Linux 2.4.18, в котором содержится исходный код драйвера чипсета RealTek8139. Может быть в ядре, с которым Вы работаете, драйвер включен в состав ядра, либо скомпилирован как модуль. Для того, чтобы избавиться от каких-либо сюрпризов, желательно собрать ядро, в котором не будет драйвера RealTek8139 ни в каком из вариантов. Если Вы не знаете, как откомпилировать ядро, я рекомендую обратиться по следующей ссылке.

    Настоятельно рекомендуется иметь книгу Rubini Linux Device Drivers в качестве справочника по API. В настоящий момент это лучший известный мне источник сведений для разработки драйверов устройств под Linux.

    Наш первый драйвер для Linux

    Перед тем, как написать наш первый драйвер, давайте рассмотрим некоторые понятия. Драйвер никогда не работает сам по себе. Он похож на библиотеку, загружаемую из-за функций, которые будут вызваны из работающего приложения. Он написан на языке C, но в нем отсутствует функция main() . Кроме того, он будет загружаться / компоноваться с ядром, поэтому он должен компилироваться аналогично тому, как было откомпилировано ядро, и вы можете в качестве заголовочных файлов использовать только те, что есть в исходном коде ядра, а не из стандартного директория /usr/include .

    Интересный факт, касающийся ядра, это то, что оно, как мы видим даже на примере нашего первого драйвера, представляет собой объектно-ориентированную реализацию на языке C. В любом драйвере есть конструктор и деструктор. Когда модуль успешно загружается в ядро, то вызывается конструктор модуля, а дескруктор модуля вызывается, когда команде rmmod удается успешно выгрузить модуль. Это в драйвере две обычные функции, разве что они называются init и exit, соответственно, и вызываются с помощью макросов module_init() и module_exit() , которые определены в заголовков ядра module.h .

    С учетом вышесказанного это полный код нашего первого драйвера; назовем его ofd.c. Обратите внимание, что отсутствует заголовок stdio.h (заголовок пользовательского пространства), вместо него мы используем аналог kernel.h (заголовок пространства ядра). Функция printk() эквивалентна функции printf() . Кроме того, для обеспечения совместимости версии модуля с ядром, в которое будет загружен модуль, добавлен заголовок version.h . С помощью макроса MODULE_* заполняется информация, относящаяся к модулю, которая будет использована как "подпись" модуля.

    Читайте также: