Вы находитесь на странице: 1из 5

Написание драйверов под Linux: рекомендации, типичные ошибки и ловушки.

Посвящается людям, которые несли в этот мир доброту, любовь, знания.

1. Аннотация.

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

2. Автор.

Mr.Nobody

3. Ключевые слова для поиска: драйвер разработка написание программирование Linux ошибки ловушки трудности
рекомендации советы практика

4. Введение.

Данная статья написана с целью помочь программистам, начинающим писать драйверы устройств для ОС Linux. Автор
понимает, что обычно люди берущиеся за написание драйверов обладают как правило достаточно высоким
интеллектом и серьёзными знаниями в области программировании, что позволяет им самостоятельно без чьей-либо
непосредственной помощи решить задачу любой сложности в плане программирования, в том числе и написание
правильно работающих драйверов под ОС Linux. Поэтому данная статья написана лишь с целью сократить время,
требуемое на решение проблем возникающих при написании драйверов под ОС Linux посредством указания типичных
ошибок (большей частью из личного опыта автора), потенциально опасных мест в коде, а также высказыванием ряда
рекомендаций, которых следует придерживаться при написании драйверов (и вообще при написании кода,
выполняющегося на уровне ядра).

Автор надеется и будет рад если данная статья окажется кому-либо полезной т.к. по мнению автора на данный момент
ощущается нехватка публикаций на данную тему. Приведённые в статье примеры относятся к ядрам версий 2.6.x, хотя
автор старался (по мере возможности) не привязываться к каким-либо конкретным версиям. Необходимо также
понимать, что несмотря на то, что приведены типичные симптомы, возникающие при указанных в данной статье
ошибках, поведение системы/ядра может отличаться (возможно незначительно) в разных версиях ядер ОС Linux.

5. Инструменты.

Необходимый минимум инструментария:


1. magic Sys Req-клавиши
2. отладочные сообщения
3. gdb и kdb

Использование п.1 по мнению автора обязательно. Настоятельно рекомендуется (по крайней мере до времени
получения правильно работающего kernelspace-кода) использовать журналируемые файловые системы. Отладочные
сообщения служат для определения области кода, приведшей к ошибке. Cледует избегать (по возможности)
использования отладчиков т.к. на работу с ними уходит значительное время. Хотя необходимо отметить, что как
правило совсем обойтись без них нельзя поэтому следует заранее побеспокоится например о поиске патча kdb под
отлаживаемое ядро и о свободном пространстве на жёстком диске (около 1Гб). Оба отладчика являются достаточно
полезными (kdb в большей степени), но имеют свою специфику например: gdb позволяет следить за данными, но не
может вмешиваться в работу ядра (например расставлять точки останова) при этом система функционирует как обычно;
kdb позволяет выставлять точки останова но при попадании в них вся система будет остановлена и нельзя будет
скажем переключится в другую консоль и посмотреть результаты какой-то программы, кроме того kdb плохо совместим с
X Window. Рекомендуется (для упрощения отладки) встраивать драйверы жёстко в ядро.

Ну и в целом при анализе кода смотрите на него глазами машины (т.е. не что он должен делать, а что делает).

6. Типичные ошибки.

1) неблокируемый вызов функций.


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

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

static int usb_device_open(struct inode *inode, struct file *file) {


int retval = 0;
...
// some initialization
retval = usb_device_init();
if ( retval < 0)
info("Failed initialization");
...
return retval;
}

Решение: проверка возвращаемых значений на допустимость:


static int usb_device_open(struct inode *inode, struct file *file) {
int retval = 0;
...
// some initialization
retval = usb_device_init();
if ( retval < 0)
info("Failed initialization");
...
// failed
if ( retval < 0)
return retval;
// success
else
return 0;
}

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

3) Запрос памяти, чей размер не кратен размеру страниц, у низкоуровневых функций распределения памяти (напр.
__get_free_pages).
Симптомы: наиболее вероятно зависание системы вместе с отладчиком (в случае использования низкоуровневых
функций), менее вероятен вывод ошибки ядром (oops) или возращение кода ошибки (это связано с тем, что как правило
в низкоуровневых функциях отдаётся предпочтение производительности, чем проверкам на правильность/допустимость
(не везде, а как правило в некоторых частях ядра, особенно влияющих на производительность системы)), а также
возможно по причине некорректной работы низкоуровневых (вспомогательных) алгоритмов изначально не расчитанных
на работу с памятью, чей размер не кратен размеру страниц). Необходимо также отметить, что как правило число таких
функций невелико и обычно большинство функций из набора API, предоставляемого подсистемой памяти позволяет
работать с памятью не кратной размеру страницы (например, remap_pfn_range()).
Пример:
static inline void *mem_alloc(size_t size) {
void *mem;
// if size is not page aligned then system will die...
mem = (void *)__get_free_pages(GFP_ATOMIC, get_order(size));
...
return mem;
}

Решение: использование выравнивания там где это необходимо


static inline void *mem_alloc(size_t size) {
void *mem;
// align on page
mem = (void *)__get_free_pages(GFP_ATOMIC,
get_order((PAGE_ALIGN(size)));
...
return mem;
}

4) освобождение ресурсов во время их использования.


Симптомы: от зависания и выдачи ошибки ядром до полного отсутствия каких-либо ошибок непосредственно после
"досрочного" освобождения ресурсов (могут проявится позже).
Пример: отсоединение USB-устройства во время его работы, приводящее к "досрочному" освобождению памяти,
выделенной под его структуру.
Решение: осуществление освобождения ресурсов, только тогда, когда в них уже нет необходимости (т.н. deffered
freeing).

5) выделение больших объектов в стеке ядра.


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

6) Неправильное использование механизмов синхронизации из-за ошибок в коде или по причине неправильного
понимания их реализации (напр. непонимание отличия в приоритете захвата семафоров от rw-семафоров; захват
семафора с удерживаемой спин-блокировкой).
Симптомы: неправильная работа ПО (userspace), работающего с драйвером (как правило, выражающаяся в
блокировании на уровне ядра и создание т.н. "неубиваемых" процессов/потоков, расходующих ресурсы системы).
Пример: проявление некорректной работы USB-устройства в некоторых режимах (получение синхронно/асинхронно
данных с устройства) из-за (внесения) реализации сложного взаимодействия потоков ядра.
Решение: правильное использование наиболее подходящих механизмов синхронизации (для разных случаев могут
использоваться разные механизмы синронизации - для правильного выбора необходимо чётко представлять их
особенности и специфику; как правило очень полезна информация (директория /Documentation исходного кода ядра),
описывающая особенности реализации и специфику использования механизмов синхронизации). В случае сложного
взаимодйствия можно порекомендовать вынести код, отвечающий за синронизацию из kernelspace в userspace (если это
приемлимо/возможно).

7. Ловушки (потенциальные ошибки).

1) отсутствие барьеров между записями в регистры устройства.


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

2) неправильное указание флагов выделения памяти.


Симптомы: редко проявляющаяся некорректная работа или даже зависания системы.
Пример: использование GFP_KERNEL в kmalloc() в контексте прерывания.
Решение: использование GFP_ATOMIC.

3) Отсутствие проверок при захвате ресурсов (подключение большого числа устройств одного типа, интенсивно
использующих какой-либо ресурс системы - например полосу пропускания USB-шины).
Симптомы: деградация системы (в плане производительности) вплоть до отстуствия реакции, некорректная работа
некоторых подсистем ядра.
Пример: подключение и одновременная работа N высокоскоростных USB-устройств (где N, это максимально возможное
число одновременно подключённых USB-устройств, согласно стандарту (спецификации) Universal Serial Bus Specification
Revision 2.0; необходимо также учитывать, что степень реализации спецификации в ядре ОС Linux может изменяться в
разных версиях ядер, например реакция на превышение пропускной способности usb-шины существенно отличается в
ядрах серии 2.4.x и 2.6.x).
Решение: расчитать и ввести в драйвере ограничение на максимальное количество устройств одного типа с которыми
возможна одновременная работа (например, исходя из ограничения по пропускной способности USB-шины или объёмам
используемой памяти).
4) отсутствие защиты от некорректных действий (userspace-программы и пользователя напр. решившего во время
работы userspace-программы, получающей данные с USB-устройства отключить его (и возможно не только отключить, но
и успеть снова включить!) - т.н. "защита от дурака".
Симптомы: неправильная работа ПО (userspace), ошибки в работе ядра (например, некорректная работа USB-
подсистемы), зависание системы - зависит от значительности ошибки и степени её влияния на работу системы.
Пример: отключение устройства во время его работы.
Решение: реализация "защиты от дурака" (в том числе и достаточно маловероятных действий - система должна
функционировать надёжно при любых условиях).

8. Советы по оптимизации.

1) использование __attribute__((section("YOUR_SECTION_NAME"))) для размещения кода, к которому чаще всего


происходит доступ с целью ускорения доступа.
Пример:
static ssize_t __attribute__((section(".fire"))) usb_read(struct file *file,
char *buffer, size_t count, loff_t *ppos)
{
...
}

2) использование __attribute__((aligned(VALUE))) для задания выравнивания объектов в памяти, с целью увеличения


скорости доступа к ним.
Пример:
// Structure to hold all of our device specific stuff
struct usb_device {
// reorder of structure members to increase cache hits
// device's plug status
int plug __attribute__((aligned(L1_CACHE_BYTES)));
// the buffer for receiving data from the endpoint
unsigned char * kbuffer
__attribute__((aligned(L1_CACHE_BYTES)));
// the size of the requested frame from the endpoint
size_t kbuffer_size
__attribute__((aligned(L1_CACHE_BYTES)));
// the usb device for this device
struct usb_device * udev
__attribute__((aligned(L1_CACHE_BYTES)));
// the address of the bulk in endpoint
uint8_t bulk_in_endpointAddr
__attribute__((aligned(L1_CACHE_BYTES)));
// the interface for this device
struct usb_interface * interface;
// kernel usage count for this device
struct kref kref;
// usage counter of this device
int usage;
} __attribute__((aligned(L1_CACHE_BYTES)));

3) использование likely()/unlikely() для подсказки компилятору о возможных ветвлениях (улучшение branch prediction).
Пример:
if (unlikely(status != 0)) {
dbg("%s - nonzero read bulk status received: %d",
__FUNCTION__, status);
return;
}

9. Общие рекомендации.

1) использование стиля кодирования являющегося фактически стандартом при программировании на уровне ядра -
kernel coding style.
2) использование вместо специфичных для ядра типов данных типов имеющихся в стандарте языка С (вместо __u8
следует использовать uint8_t имеющийся в C99).
3) использование стандартных интерфейсов предоставляемых той или иной подсистемой ядра (не "изобретать колесо" в
случаях, когда без этого можно обойтись).
4) инициализация (обнуление) памяти в случае если её содержимое передаётся/используется в userspace.
5) после каждого изменения, вносимого в код, выполняющийся на уровне ядра необходимо провести тщательное
тестирование (на правильность функционирования, в различных режимах работы, в том числе и на защиту от
некорректных действий).
6) не усложняйте без необходимости код, выполняемый на уровне ядра - придерживайтесь принципа "чем проще - тем
лучше".

9. Список использованных источников.

1. Documentation/CodingStyle, исходный код ядра Linux (www.kernel.org)


2. Allessandro Rubini, Jonathan Corbet - "Linux Device Drivers", 2-nd Edition, O'Reilly
3. Arjan van de Ven - "How to NOT write kernel driver"
4. Роберт Лав - "Разработка ядра Linux", второе издание

Все статьи раздела "Система"

Вам также может понравиться