Устройства драйверы устройств linux

Содержание
  1. Драйверы устройств в Linux
  2. Часть 1: Драйверы устройств Linux для вашей знакомой
  3. О драйверах / водителях (drivers) и шинах / автобусах (buses)
  4. Драйверы состоят из двух частей
  5. Вертикали
  6. Драйверы со множественными вертикалями
  7. Подведем итог
  8. Устройства драйверы устройств linux
  9. 2.2.1 Пример — vgalib.
  10. 2.2.2 Пример : Преобразование мыши.
  11. 2.3 Основы драйверов устройств.
  12. 2.3.1 Область имени (именная область).
  13. 2.3.2 Выделение памяти.
  14. 2.3.3 Символьные и блочные устройства.
  15. 2.3.4. Прерывание или поочередное опрашивание устройств ?
  16. 2.3.5. Механизмы замораживания и активизации.
  17. 2.3.5.1.Усложненный механизм заморозки.
  18. 2.3.6. VFS.
  19. 2.3.6.1. Функция lseek().
  20. 2.3.6.2. Функции read() и write().
  21. 2.3.6.3 Функция readdir().
  22. 2.3.6.4 Функция select().
  23. 2.3.6.5 Функция ioctl().
  24. 2.3.6.6.Функция mmap().
  25. 2.3.6.7. Функции open() и release().
  26. 2.3.6.8 Функция init().
  27. 2.4 Cимвольные устройства.
  28. 2.4.1. Инициализация.
  29. 2.4.2 Прерывания или последовательный вызов ?
  30. 2.5 Дpайвеpы для блочных устpойств.
  31. 2.5.1 Инициализация
  32. 2.5.1.1 Файл blk.h

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

Часть 1: Драйверы устройств Linux для вашей знакомой

Оригинал: «Device Drivers, Part 1: Linux Device Drivers for Your Girl Friend»
Автор: Anil Kumar Pugalia
Дата публикации: November 1, 2010
Перевод: Н.Ромоданов
Дата перевода: июнь 2012 г.

Цель этой серии статей о драйверах Linux — рассказать об обычной технической теме так, чтобы она была интересна более широкому кругу читателей.

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

«Почему? Какой твой driver (драйвер / водитель)? Что, он был болен? И что за тяжелую работу ты сделал?», — спросила Светлана. Растерявшись, Пагс, в свою очередь, спросил: «О чем ты говоришь?»

Теперь настала очередь Светланы быть озадаченной и она ответила: «Почему ты меня спрашиваешь? Ты сказал мне — о каком из твоих driver (драйверов / водителей) ты говоришь? «

Когда Пагс понял, о чем идет речь, он застонал: «Ах, да! Это не driver (драйвер / водитель) моей машины — я говорю о driver (драйвере / водителе) устройства на моем компьютере «.

«Я знаю о driver (драйверах / водителах) машин и автобусов, о летчиках и даже об отвертках; но что это за «driver (драйвер / водитель) устройства» — озадаченно спросила Светлана.

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

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

Аналогичное смешение понятий может вызвать английское слово bus, которое в обыденной речи переводится как «автобус», например, school-bus – школьный автобус или автобус для школьников. Но при обсуждении компьютерного оборудования термином bus называют шину, которая обычно представляет собой некоторое количество соединительных линий, к которым подключаются отдельные устройства.

О драйверах / водителях (drivers) и шинах / автобусах (buses)

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

Кроме того, «водителем» может быть человек или даже автоматическая система, управление которой возложено на человека (например, система автопилота в самолете). Аналогичным образом управление определенной частью аппаратных средств может осуществляться некоторой частью программного обеспечения (драйвером устройства), или может осуществляться другим устройством, управление которым, в свою очередь, может выполняться программой — драйвером устройства. В последнем случае, такое управляющее устройство обычно называется контроллером устройств. Для него, поскольку оно само является устройством, необходим драйвер, который обычно называют bus driver или драйвер шины (термин bus driver, если он используется в контексте обычного разговора, можно перевести и как «водитель автобуса» — прим.пер.).

К числу обычных примеров контроллеров устройств относятся контроллеры жестких дисков, контроллеры дисплеев и контроллеры аудиоустройств, с помощью которых осуществляется управление устройствами, подключаемыми к контроллерам. В качестве более подробных технических примеров можно рассмотреть контроллер IDE, контроллер PCI, USB-контроллер, контроллер SPI, контроллер I2C и т.д. Графически, вся эта концепция может быть изображена так, как показано на рис.1.

Рис.1: Взаимодействие устройств и драйверов

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

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

Драйверы состоят из двух частей

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

Рис.2: Отдельные части драйвера Linux

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

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

Вертикали

В Linux драйвер устройства предоставляет пользователю интерфейс «системного вызова»; в Linux это граница между так называемым пространством ядра и пользовательским пространством, что и показано на рис.2. На рис.3 представлена более подробная классификации.

Рис.3: Общая схема ядра Linux

Если рассматривать интерфейс драйвера с учетом специфики использования драйверов в ОС, то в системе Linux драйверы можно по вертикали грубо разделить на три группы:

  • Пакетно-ориентированная или сетевая вертикаль
  • Блочно-ориентированная вертикаль или вертикаль хранения данных
  • Байт-ориентированная вертикаль или вертикаль работы с символами

Вертикаль процессора и вертикаль памяти, рассматриваемые вместе с этим тремя вертикалями, дают полное представление о ядре Linux, соответствующее определению ОС, которое есть в любом учебнике: «В операционной системе реализуется 5 основных функций управления: управление процессором / процессом, памятью, сетью, средствами хранения данных, устройствами ввода / вывода». Хотя эти вертикали процессора и памяти можно классифицировать как драйверы устройств, где процессор и память будут соответствующими устройствами, их, по ряду причин, трактуют по-другому.

Таковы основные функциональные возможности любой ОС, будь то микроядро или монолитное ядро. Чаще всего добавление кода именно в эти области представляет собой основную часть работы по портированию Linux, что обычно делается для нового процессора и архитектуры. Более того, код в этих двух вертикалях нельзя, в отличие от трех других вертикалей, загружать или выгружать «на лету». Так что когда мы теперь будем говорить о драйверах устройств в Linux, мы будем говорить только о тех трех вертикалях, которые расположены на рис.3 справа.

Давайте заглянем немного глубже внутрь этих трех вертикалей. Сетевая вертикаль состоит из двух частей: а) стек сетевых протоколов и б) драйверы устройств карт сетевых интерфейсов (NIC) или просто драйверы сетевых устройств, которые могут предназначаться для Ethernet, Wi-Fi или любой другой сетевой горизонтали. Вертикаль хранения данных, опять же, состоит из двух частей: а) драйверов файловых систем, предназначенных для декодирования разнообразных форматов данных в различных разделах файловых систем, и б) драйверов блочных устройств для различных (аппаратных) протоколов хранения данных, т.е. горизонталей, таких как IDE, SCSI, MTD и т.д.

При этом, вы можете задать вопрос, действительно ли это только набор устройств, для которых нужны драйвера (или, для которых в Linux драйвера уже есть). Имейте терпение, вам, конечно, нужны драйверы для всей этой массы устройств, которые взаимодействуют с системой, и в Linux действительно есть для них драйверы. Однако из-за их байт-ориентированного доступа все они попадают в вертикаль работы с символами — их, на самом деле, огромное количество. В действительности из-за огромного количества драйверов в этой вертикали, для драйверов, предназначенных для работы с символьными устройствами, используется дополнительная подклассификация — так что у вас есть драйверы терминалов, драйверы ввода/вывода, драйверы консоли, драйверы фрейм-буфера, звуковые драйверы и т.д. Типичными горизонталями здесь будут RS232, PS/2, VGA, I2C, I2S, SPI и т.д.

Драйверы со множественными вертикалями

Последнее замечание относительно полной картины (размещения всех драйверов в экосистеме драйверов Linux): такие горизонтали, как USB, PCI и т.д., расширяются ниже на несколько вертикалей. Почему это происходит?

Очень просто — вы уже знаете, что у вас может быть флешка-ключ USB Wi-Fi, флешка USB и преобразователь USB-последовательный порт, но все эти три устройства USB попадают в три различные вертикали!

Читайте также:  Работа с обновлениями windows 10 очень долго 0 процентов

В Linux драйвера шин или горизонтали часто подразделяются на две части, или даже на два драйвера: а) контроллер конкретного устройства и б) абстрактный слой, находящийся над ним и используемый в качестве интерфейсам к вертикалям, обычно называемыми ядрами. Классическим примером могут быть драйвера USB-контроллера ohci, ehci и т.д. и USB-абстракция — usbcore.

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

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

«Эй, Пагс, остановись; мы опаздываем на занятия, и ты знаешь, какие из-за этого могут возникнуть проблемы. Давай позже продолжим с этого места» — воскликнула Светлана.

Вскочив, Пагс закончил свое объяснение: «Хорошо. Это основа теории о драйверах устройств. Если тебе интересно, я позже могу показать тебе код, и все, что мы делали для различных драйверов». И они поспешили к своей аудитории.

Источник

Устройства драйверы устройств linux

Что такое драйвер устройства.

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

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

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

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

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

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

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

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

Тем самым, когда доступно устройство, присущее вашему драйверу, VFS вызывает вашу функцию.

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

Также ваш драйвер будет запущен в режиме работы с ядром, а ядро Linux, как и большинство ядер UNIX, не имеет средств принудительного сброса. Это означает, что если ваш драйвер будет долго работать, не давая при этом работать другим программам, ваш компьютер может «зависнуть «. Нормальный пользовательский режим с последовательным запуском не обращается к вашему драйверу.

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

Драйверы пользовательского уровня.

Не всегда нужно писать драйвер для устройства, особенно если за устройством следит всего одно приложение. Наиболее полезным примером этому является устройство карты памяти, однако вы можете сделать карту памяти с помощью устройств ввода/вывода (доступ к устройствам осуществляется с помощью функций inpb() и outpb()).

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

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

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

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

2.2.1 Пример — vgalib.

Хорошим примером драйвера пользовательского уровня является библиотека vgalib. Стандартные функции read() и write() не подходят для написания действительно быстрого графического драйвера, и поэтому существует библиотека функций, которая концептуально работает как драйвер устройства, но на пользовательском уровне. Все функции, которые используют ее, должны запускать setuid, так как она использует системную функцию ioperm(). Функции, которые не запускают setuid, обладают возможностью записи в /DEV/MEM, если у вас есть группы mem или kmem, которые позволяют это, но только корневые процессы могут запускать ioperm().

Есть несколько портов ввода/вывода, относящихся к графике VGA. Vgalib дает им символические имена с помощью #define, и далее используют ioperm() для разрешения функции правильного прочтения и записи в эти порты.

Это требует лишь однократной проверки, так как единственной причиной нефункционирования ioperm() может быть обращение к ней не в статусе superuser или во время смены статуса.

После обращения в порты ввода вывода vgalib засылает информацию в область ядра следующим образом :

В начале программа открывает /dev/mem, затем выделят достаточное количество памяти для распределения на страницу, затем меняет карту памяти.

Затем, записывая в адрес возвращаемый mmap(), программа осуществляет запись в память экрана.

2.2.2 Пример : Преобразование мыши.

Если вы хотите написать драйвер, работающий так же, как и драйвер на уровне ядра, но не находящийся в его области, то вы можете создать fifo (буфер — first in, first out). Обычно он помещается в директорию /dev (во время нефункционирования) и ведет себя как подключенное устройство.

В частности, это используется когда вы используете мышь типа PS/2 и хотите запустить XFree86. Вы должны создать fifo, называемый /dev/mouse, и запустить программу mconv, которая, читая сигналы мыши PS/2 из /dec/psaux, пишет эквивалентные сигналы microsoft mouse в /dev/mouse.

В этом случае XFree86 будет читать сигналы из /dev/mouse и функционировать также как и при подключенной microsoft mouse.

2.3 Основы драйверов устройств.

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

В таком случае вам придется иметь дело с файлами .с и .h. Мы будем условно обозначать ваши труды как foo.c и foo.h.

2.3.1 Область имени (именная область).

Первое что вы должны сделать при написании драйвера — назвать устройство. Имя должно выть кратким — строка из двух — трех символов. К примеру, параллельные устройства — «lp», дисководы «fd», диски SCSI — «sd».

Создавая ваш драйвер, называйте функции в нем с первыми тремя буквами избранной строки в имени. Так как мы называем его foo — функции в нем соответственно — foo_read и foo_write.

2.3.2 Выделение памяти.

Выделение памяти в ядре отличается от выделения памяти на пользовательском уровне. Вместо функции malloc() выделяющее почти неограниченное количество памяти, существует kmalloc(), которая имеет некоторые отличия:

  • Память выделяется кусками размером степени 2, за исключением кусков больше 128 байтов, размер коих равен степени 2 за вычетом части под метку о размере. Вы можете запросить произвольный размер, однако это будет неэффективно, так как 31 байтового об’екта, к примеру, выделяется 32 байтовый кусок. Общий предел выделяемой памяти 131056 байт.
  • В качестве второго аргумента kmalloc() использует приоритет. Он используется в качестве аргумента функции get_free_page(), где он используется в качестве числа определяющего момент возврата. Обычно используемый приоритет — GFP_KERNEL. Если функция может быть вызвана с помощью прерывания используйте GFP_ATOMIC и приготовьтесь к тому, что функция может не работать. Это происходит из-за того, что при использовании GFP_KERNEL kmalloc() может не быть активным в любой момент времени, что не возможно при прерывании. Можно так же использовать опцию GFP_BUFFER, которая используется для выделения ядром области буфера. В драйверах устройств она не используется.

Для очистки памяти, выделенной kmalloc(), используйте функции kfree() и kfree_s(). Они также несколько отличаются от функции free() :

  • kfree() — это макрос, вызывающий kfree_s() и работающая как free() вне ядра.
  • Если вы знаете размеры об’екта, удаляемого из памяти, вы можете ускорить процесс, запуская сразу kfree_s(). У него существуют два аргумента : первым является аргумент kfree(), вторым — размер удаляемого об’екта.
  • См 2.6 для получения более подробной информации о kmalloc(), kfree() и о других полезных функциях.

    Другой способ сохранить память — выделение ее во время инициализации. Ваша инициализационная функция foo_init() в качестве аргумента использует указатель на текущий конец памяти.Она может взять столько памяти, сколько хочет сохранить указатель/указатели на эту память и возвратить указатель на новый конец памяти.Преимуществом этого метода является то, что при выделении большого буфера в случае, если foo — драйвер не находит foo- устройства, подключенного к компьютеру, память не тратится. Функция инициализации подробно обсуждается в части 2.3.6. Будьте предельно аккуратны при использовании kmalloc(), используйте его только в случае крайней необходимости. Помните, что память в ядре не своппится. Аккуратно выделяйте ее, а затем каждый раз очищайте ее функцией frее().

      ! Существует возможность выделения виртуальной памяти с помощью vmalloc(), однако это будет описано лишь в главе VMM во время ее написания. В данный момент вам придется изучать это самостоятельно.!

    2.3.3 Символьные и блочные устройства.

    Существует два типа устройств в системах UN*X — символьные и блочные устройства. Для символьных устройств не предусмотрено буфера, в то время как блочные устройства имеют доступ лишь через буферную память. Блочные устройства должны быть равнодоступными, а для символьных это не обязательно, хотя и возможно. Файловая система может работать лишь в случае, если она является блочным устройством.

    Общение с символьными устройствами осуществляется с помощью функций foo_read() и foo_write(). Функции foo_read() и foo_write() не могут останавливаться в процессе деятельности, поэтому блочные устройства даже не требуют использования этих функций, а вместо этого используют специальный механизм, называемый «strategy routine» — стратегическая подпрограмма. Обмен информацией происходит при помощи функций bread(), breada(), bwrite(). Эти функции, просматривая буферную память, могут вызывать «strategy routine» в зависимости от того, готово устройство или нет к приему информации (в случае записи — буфер переполнен), или же присутствует ли информация в буфере (в случае чтения ).Запрос текущего блока из буфера может быть асинхронен чтению — breada() может вначале определить график передачи информации, а затем заняться непосредственно передачей. Далее мы представим полный обзор буферной памяти(кэш). Исходные тексты для символьных устройств содержатся в /kernel/chr_drv, исходники для блочных — /kernel/blk_drv. Для простоты чтения интерфейсы у них довольно просты, за исключением функций записи и чтения. Это происходит из за определения вышеописанной «strategy routine» в случае блочных устройств и соответствующего ему определения foo_read и foo_write() для символьных устройств. Более подробно об этом в 2.4.1 и 2.5.1.

    2.3.4. Прерывание или поочередное опрашивание устройств ?

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

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

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

    В Linux cуществуют как драйверы, управляемые прерываниями так и драйверы, не использующие прерываний, и оба типа драйверов могут отключаться или включаться во время работы подпрограммы. В частности, «lp» устройство ждет готовности принтера к принятию информации и, в случае отказа, отключается на какой-то промежуток времени, чтобы затем попытаться вновь.

    Это улучшает показатели системы. Однако, если вы имеете параллельную карту, поддерживающую прерывания, драйвер, используя ее, увеличит скорость работы. Существуют несколько программных отличий между драйвером, управляемым прерываниями и ждущими драйверами. Для осознания этих отличий вы должны представлять себе устройство системных вызовов UN*X. Ядро — неразделяемая задача под UN*X. В таком случае в каждом процессе находится копия ядра.

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

    В режиме ядра процесс все еще имеет доступ к пространству памяти пользователя, как и до смены режима, что достигается с помощью макросов: get_fs_*() и memcpy_fromfs(), осуществляющих чтение из памяти, и put_fs_*() и memcpy_tofs(), осуществляющих запись. Так как процесс переходит из одного режима в другой, вопроса о помещении информации в определенную область памяти не возникает.

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

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

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

    2.3.5. Механизмы замораживания и активизации.

    Начнем с об»яснения механизма заморозки и его использования. Это включает в себя то, что процесс, будучи в замороженном состоянии (не функционирует), в какой — то момент времени можно активизировать, а затем опять заморозить (приостановить )!

    Возможно, лучший способ понять механизм замораживания и активизации в Linux — изучение исходного текста функции __sleep_on(), использующейся для описания функций sleep_on() и interruptible_sleep_on().

    wait_queue — циклический список указателей на структуры задач, определенные в как

    Меткой состояния процесса в данном случае является или TASK_INTERRAPTIBLE, или TASK_UNINTERRAPTIBLE, в зависимости от того, может ли заморозка процесса прерываться такими вещами, как системные вызовы.Вообще говоря, механизм заморозки необходимо прерывать лишь в случае медленных устройств, так как такое устройство может приостановить на достаточно длительный срок работу всей системы. add_wait_queue() отключает прерывание, создает новый элемент структуры wait_queue, определенной в начале функции как список p.Затем она восстанавливает в исходное положение метку о состоянии процесса.

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

    Функция sti() затем разрешает прерывания, а schedule() выбирает для выполнения следующий процесс. Задача не может быть избранной для выполнения, пока метка не будет находиться в состоянии TASK_RUNNING.

    Это достигается с помощью функции wake_up(),примененной к задаче, ждущей в структуре p своей очереди.

    Затем процесс исключает себя из wait_queue,восстанавливает состояние положения прерывания с помощью restore_flags() и завершает работу.

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

    Если вы хотите понять последовательность разморозки задач или более детально изучить механизм заморозки, вам нужно купить одну из книг, предложенных в приложении А и просмотреть !mutual exclusion! и !deadlock!.

    2.3.5.1.Усложненный механизм заморозки.

    Если механизм sleep_on()/wake_up() в Linux не удовлетворяет вашим требованиям, вы можете усовершенствовать его. В качестве примера тому можете посмотреть серийный драйвер устройства (/kernel/chr_drv/serial.c), функцию

    2.3.6. VFS.

    VFS — Virtual Filesystem Switch (Система виртуального переключения файловой системы ) — механизм, позволяющий Linux поддерживать сразу несколько файловых систем. В первой версии Linux доступ к файловой системе осуществляется через подпрограммы, работающие с файловой системой minix. Для обеспечения возможности работы с другой файловой системой ее вызовы переопределяются как функции знакомой Linux системы файлов. Это делается с помощью программы, содержащей структуру указателей на функции, представляющие все возможные действия с файловой системой. Вызывает интерес структура file_operations :

    Эта структура содержит список функций, нужных для создания драйвера.

    2.3.6.1. Функция lseek().

    Функция вызывается, когда в специальном файле, представляющем устройство, появляется системный вызов lseek().Это функция перехода текущей позиции на заданное смещение.Ей задается четыре аргумента :

    lseek() возвращает -errno в случае ошибки или положительное смещение после выполнения.

    Если lseek() отсутствует, ядро автоматически изменяет элемент file -> f_pos.При origin = 2 в случае file -> f_inode = NULL ему присваивается значение -EINVAL,иначе file -> fpos принимает значение file -> f_inode -> i_size + offset.Поэтому в случае возврата ошибки устройства системным вызовом lseek() вы должны использовать функцию lseek для определения этой ошибки.

    2.3.6.2. Функции read() и write().

    Функции read() и write() осуществляют обмен информацией с устройством, посылая на него строку символов.Если функции read() и write() отсутствуют в структуре file_operatios, определенной в ядре, то в случае символьного устройства одноименные вызовы будут возвращать -EINVAL.В случае блочных устройств функции не определяются, так как VFS будет общаться с устройством через механизм обработки буфера, вызывающий «strategy routine». См. 2.5.2 для более подробного изучения устройства механизма работы с буфером.

    Функции read() и write() используют следующие аргументы :

    • struct inode * inode
      — Указатель на структуру inode специального файла устройства, доступного для использования непосредственно пользователем. В частности, вы можете найти подномер файла при помощи конструкции unsigned int minor = MINOR(inode -> i_rdev); Определение макроса MINOR находится в , так же, как и масса других нужных определений. Для получения более подробной информации см. fs.h. Более подробное описание представлено в 2.6. Для определения типа файла может быть использована inode -> i_mode.
    • struct file * file
      — Указатель на файловую структуру этого устройства.
    • char * buf
      — Буфер символов для чтения и записи. Он расположен в пространстве памяти пользователя, и доступ к нему осуществляется с помощью макросов get_fs*(), put_fs*() и memcpu*fs(), описанных в 2.6. Пространство памяти пользователя не доступно во время прерывания, так что если ваш драйвер управляется прерываниями, вам придется списывать содержание буфера в очередь (queue).
    • int count
      — Число символов, записанных или читаемых из buf. count — размер буфера, так что с помощью него легко определить последний символ buf, даже если буфер не заканчивается NULL.

    2.3.6.3 Функция readdir().

    Еще один элемент структуры file_operations, используемый для описания файловых систем так же, как драйверы устройств. Функция не нуждается в предопределении. Ядро возвращает -ENOTDIR в случае вызова readdir() из специального файла устройства.

    2.3.6.4 Функция select().

    Функция select() полезна в основном в работе с символьными устройствами. Обычно она используется для многократного чтения без использования последовательного вызова функций. Приложение делает системный вызов select(), задавая ему список дескрипторов файлов, затем ядро сообщает программе, при просмотре какого дескриптора она была активизирована. Также select() иногда используется как таймер. Однако функция select() в драйвере устройства не вызывается непосредственно системным вызовом, так что file_operations select() выполняет небольшое количество примитивных операций. Ее аргументы:

    • struct inode * inode
      — Указатель на структуру inode устройства.
    • struct file * file
      — Указатель на файловую структуру устройства.
    • int sel_type
      — Тип совершаемого действия
      SEL_IN — чтение
      SEL_OUT — запись
      SEL_EX — удаление
    • select_table * wait
      — Если wait = NULL, функция select() проверяет, готово ли устройство, и возвращается в случае отсутствия готовности. Если wait не равен NULL, select() замораживает процесс и ждет, пока устройство не будет готово. Функция select_wait() делает то же, что и select() при wait = NULL.

    2.3.6.5 Функция ioctl().

    Функция ioctl() осуществляет функцию передачи контроля ввода/вывода. Структура вашей функции должна быть следующей: первичная проверка ошибок, затем переключение, дающее вам право контролировать все ioctl. Номер ioctl находится в аргументе cmd, аргумент контролируемой команды находится в arg. Для работы с ioctl() вы должны иметь подробное представление о контроле над вводом/выводом. Если вы сомневаетесь в правильности использования ioctl(), спросите кого-нибудь, так как эта функция в текущий момент может оказаться ненужной. Так как ioctl() является частью интерфейса драйверов, вам придется уделить ей внимание.

    • struct inode * inode
      — Указатель на inode структуру данного устройства;
    • struct file * file
      — Указатель на файловую структуру устройства;
    • unsigned int cmd
      — Команда, над которой осуществляется контроль;
    • unsigned int arg
      — Это аргумент для команды, определяется пользователем. В случае, если он вида (void *), он может быть использован как указатель на область пользователя, обычно находящуюся в регистре fs.
    • Возвращаемое значение :
      -errno в случае ошибки, все другие значения определяются пользователем.

    Если слот ioctl() в file_operations не заполнен, VFS возвращает значение -EINVAL, однако в любом другом случае, кесли cmd принимант одно из значений — FIOCLEX, FIONCLEX,FIONBIO, FIOASYNC, будет происходить следующее:

    • FIOCLEX 0x5451
      Устанавливает бит «закрытие для запуска»
    • FIONCLEX 0x5450
      Очищает бит «закрытие для запуска»
    • FIONBIO 0x5421
      Если аргумент не равен 0, устанавливает O_NONBLOCK, иначе очищает O_NONBLOCK.
    • FIOASYNC 0x5421
      Если аргумент не равен 0, устанавливает O_SYNC, иначе очищает O_SYNC. Пока еще не описано, но для полноты вставлено в ядро.

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

    2.3.6.6.Функция mmap().

    • struct inode *inode
      — Указатель на inode
    • struct file *file
      — Указатель на файловую структуру
    • unsigned long addr
      — Начальный адрес блока, используемого mmap()
    • size_t len — Общая длина блока.
    • int prot — Принимает значения:
      PROT_READ читаемый кусок
      PROT_WRITE перезаписываемый кусок
      PROT_EXEC кусок, доступный для запуска
      PROT_NONE недоступный кусок
    • unsigned long off
      — Внутрифайловое смещение, от которого производится перестановка. Этот адрес будет переставлен на адрес addr.

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

    2.3.6.7. Функции open() и release().

    • struct inode *inode
      — Указатель на inode
    • struct file *file
      — Указатель на файловую структуру

    Функция вызывается после открытия специальных файлов устройств. Она является механизмом слежения за последовательностью выполняемых действий. Если устройством пользуется лишь один процесс, функция open() закроет устройство любым доступным в данный момент способом, обычно устанавливая нужный бит в положение «занято». Если процесс уже использует устройство (бит уже установлен), open() возвращает -EBUSY.

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

    Если устройство не существует, open() вернет -ENODEV.

    Функция release() вызывается лишь тогда, когда процесс закрывает последний файловый дескриптор. release() может переустанавливать бит «занято». После вызова release(), вы можете очистить куски выделенной kmalloc() памятью под очереди процессов.

    2.3.6.8 Функция init().

    Эта функция не входит в file_operations но вам придется использовать ее, так как именно она регистрирует file_operations с содержащейся там VFS — — без нее запросы на драйвер будут находится в беспорядочном состоянии. Эта функция запускается во время загрузки и самоконфигурирования ядра. init() получает переменную с адресом конца используемой памяти. Затем она обнаруживает все устройства, выделяет память, исходя из их общего числа, сохраняет полезные адреса и возвращает новый адрес конца используемой памяти. Функцию init() вы должны вызывать из определенного места. Для символьных устройств это /kernel/cdr_dev/mem.c. В общем случае функции надо задавать лишь переменную memory_start.

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

    1. int major — основной номер устройства.
    2. srtring name — имя устройства.
    3. адрес #DEVICE#_fops структуры file_operations.

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

    Функция init() обычно выводит сведения о найденном аппаратном обеспечении и информацию о драйвере.Это делается с использованием функции printk().

    2.4 Cимвольные устройства.

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

    Кроме функций описанных в file_operations, есть еще одна функция, кото- рую вам надо вписать в функцию foo_init(). Вам придется изменить функцию chr_dev_init() в chr_drv/mem.c для вызова вашей функции foo_init(). foo_init() вначале должна вызывать register_chrdev() для определения самой себя и установки номеров устройств. Аргументы register_chrdev() :

    • int major — основной номер драйвера.
    • char *name — имя драйвера оно может быть изменено, но не имеет практического применения.
    • struct file_operations *fops — адрес определенной вами file_operations.
    • Возвращаемые значения : 0 — в случае если указанным основным номером ни одно устройство более не обладает. не 0 в случае некорректного вызова.

    2.4.2 Прерывания или последовательный вызов ?

    В драйверах, не использующих прерывания, легко пишутся функции foo_read() и foo_write() :

    foo_write_byte() и foo_handle_error() — функции, также определенные в foo.c или псевдокоде.

    WRITE — константа или определена #define.

    Из примера также видно как пишется функция foo_read(). Драйверы, управ- ляемые прерываниями, более сложны :

    Пример foo_write для драйвера, управляемого прерываниями :

    Здесь функция foo_read также аналогична. foo_table[] — массив структур, каждая из которых имеет несколько элементов, в том числе foo_wait_queue и bytes_xfered, которые используются и для чтения, и для записи. foo_irq[] — — массив из 16 целых использующийся для контроля за приоритетами элементов foo_table[] засылаемыми в foo_interrupt().

    Для указания обpаботчику пpеpываний вызвать foo_interrupt() вы должны использовать либо request_irq(), либо irqaction(). Это делается либо пpи вызове foo_open(), либо для пpостоты в foo_init(). request_irq() pаботает пpоще нежели irqaction и напоминает pаботу сигнального пеpеключателя. У нее существует два аpгумента:

    • номеp irq, котоpым вы pасполагаете>
    • указатель на пpоцедуpу упpавления пpеpываниями, имеющую аpгумент типа integer.>

    request_irq() возвpащает -EINVAL, если irq > 15, или в случае указателя на пpогpамму pавного NULL, EBUSY если пpеpывание уже используется или 0 в случае успеха.

    irqaction() pаботает также как функция sigaction() на пользовательском уpовне и фактически использует стpуктуpу sigaction. Поле sa_restorer() в стpуктуpе не используется, остальное — же осталось неизменным. См. pаздел «Функции поддеpжки» для более полной инфоpмации о irqaction().

    2.5 Дpайвеpы для блочных устpойств.

    Пpи поддеpжке файловой системы устpойства, она должна быть pазбита на блоки самим устpойством. Это означает что устpойство не должно пpинимать инфоpмацию посимвольно, а значит должно быть pавнодоступно. Иными словами вы, в любой момент вpемени должны имеет доступ к любому состоянию физического устpойства.

    Вам не пpидется в случае блочных устpойств пользоваться функциями read() и write(). Вместо них используются функции block_read() и block_write() находящиеся в VFS и называемые !strategy routine! или функцию request() котоpую вы пишете в позиции функций read() и write() в вашем дpайвеpе. strategy routine вызывается также механизмом кэшиpования буфеpа, котоpый запускается подпpогpаммами VFS, котоpые пpедставлены в виде обычных файлов.

    Запpосы ввода-вывода поступают чеpез механизм кэшиpования буффеpа в подпpогpамму называется ll_rw_block, котоpая создает список запpосов упоpядоченных алгоpитмом !elevator!, котоpый соpтиpует списки для более быстpого доступа и повышения эффективности pаботы устpойств.

    Затем она вызывает фнкцию request() для осуществления ввода — вывода. Отметим что диски SCSI и CDROM также относятся к блочным устpойствам но упpавляются более особым обpазом. Часть 2.7 «Hаписание дpайвеpа SCSI» описывает это более подpобно.

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

    Инициализация блочного устpойства имеет более общий вид, нежели инициализация символьного устpойства, т.к. часть «инициализации» пpоисходит во вpемя компиляции. Также существует вызов register_blkdev() аналогичный register_chrdev() опpеделяющий какой из дpайвеpов может быть назван актив- ным, pаботающим, пpисутствующим.

    2.5.1.1 Файл blk.h

    Вначале текста вашего дpайвеpа после описания.h файлов вы должны написать две стpоки:

    где DEVICE_MAJOR — основной номеp вашего устpойства.drivres/block/blk.h тpебует основной номеp для установки дpугих опpеделений и макpосов дpайвеpа.

    Тепеpь вам нужно изменить файл blk.h.После #ifdef MAJOR_NR есть часть пpогpаммы в котоpой опpеделены некотоpые основные номеpа, защищенные

    В конце списка вы запишете раздел для вашего драйвера :

    DEVICE_NAME — имя устройства.В качестве примера посмотрите предыдущие записи в blk.h.

    DEVICE_REQUEST — ваша «strategy routine», которая будет осуществлять ввод/вывод в вашем устройстве.См 2.5.3 для более полного изучения.

    DEVICE_ON и DEVICE_OFF — для устройств, которые включаются/выключаются во время работы.

    DEVICE_NR(device) — используется для определения номера физического устройства с помощью подномера устройства. В частности, драйвер hd, в то время как второй жесткий диск работает с подномером 64, DEVICE_NR(device) определяется (MINOR(device) >> 6).

    Если ваш драйвер управляется прерываниями, также установить

    что автоматически становится переменной и используется даже в blk.h, в основном макросами SET_INTR и CLEAR_INTR.

    Также вы можете присовокупить такие определения :

    где n — число тиков часов (в Linux/386 — сотые секунды )для паузы в случае незапуска прерывания. Это делается для того,чтобы драйвер не ждал прерывания, которое может никогда не случиться. Если вы делаете эти установки, они автоматически используются

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

    Источник

    Читайте также:  Linux users installing web browser
    Оцените статью