Лабороторна робота №2_6 Написання драйверів в Linux
Драйвер пристрою це низькорівнева програма, яка містить специфічний код для роботи з пристроєм та дозволяє користувачу програмам(і самої ОС) керувати ним стандартним чином.
Пристрої можна розділити на:
Символьні. Читання і запис пристрою йде посимвольний. Приклади таких пристроїв: клавіатура, послідовні порти.
Блочні. Читання і запис пристрою можливі тільки блоками, зазвичай по 512 або 1024 байта. Приклад - жорсткий диск.
Мережеві інтерфейси. Приклад - мережева карта (eth0). Відрізняються тим, що не відображаються на файлову систему, тобто не мають відповідних файлів в директорії /dev, оскільки через специфіку цих пристроїв робота з мережевими пристроями як з файлами неефективна.
Для символьних і блочних пристроїв - взаємодія з драйвером реалізується через спеціальні файли, розташовані в директорії /dev. Кожен файл пристрою має два номери
- старший, який визначає тип пристрою
- молодший, який визначає конкретний номер пристрою(у системі може бути декілька пристроїв одного типу - наприклад, жорстких дисків). Багато з старших номерів пристроїв вже зарезервовані; їх можна подивитися в документації на ядро. Це файл /usr/share/doc/kernel-doc-2.4.2/devices.txt.
У перших версіях Linux драйвера пристроїв були "зашиті" в ядро. Недоліки такого рішення очевидні:
1. Драйвера, включені в ядро, завантажуються навіть за відсутності пристрою в системі - і споживають системні ресурси.
2. При підключенні нового пристрою (або нової версії драйвера) потрібна перекомпіляція ядра. Наявність цих проблем призвело до створення в ядрі механізму модулів що завантажуються динамічно. Механізм дає можливість встановлювати драйвери нових пристроїв "на льоту" - без перекомпіляції ядра і навіть перезавантаження системи. Встановлювати можна не всі драйвера, а лише драйвера для пристроїв, які реально присутні в системі. Динамічні модулі, на відміну від звичайних програм, є об'єктні файли, скомпільовані за певними правилами. Розглянемо створення такого модуля для символьного пристрою.
Найпростіший модуль. Компіляція та встановлення модуля в систему. Текст найпростішого модуля представлений нижче (файл module.c).
1 /*
2 ====================================
3 Приклад найпростішого модуля ядра
4 лаб.2_6_1
5 2014
6 ====================================
7 */
8 #define MODULE
9 #define __KERNEL__
10 #include <module.h>
11
12 int init_module()
13 {
14 return 0;
15 }
16
17 void cleanup_module()
18 {
19 return;
20 }
Як видно - є дві функції (вони обов'язкові):
int init_module() - викликається при завантаженні модуля ядром. Якщо повертається значення "0", все нормально; інакше - сталася помилка.
void cleanup_module() - Викликається при видаленні модуля з системи. Рядки 8-9 змушують компілятор генерувати код динамічно завантаження модуля. У заголовному файлі module.h містяться визначення, необхідні для створення динамічного модуля. Нижче наведено текст Makefile для збірки нашого модуля:
CC=gcc
MODFLAGS:= -O3 -Wall -DLINUX
module.o: module.c
$(CC) $(MODFLAGS) -c module.c
Директива - DLINUX каже компілятору про необхідність генерувати код під Linux. Ключ змушує компілятор генерувати саме об'єктний, а НЕ виконуваний файл. Збірка здійснюється командою make в директорії, де лежить вихідний файл module.c. Результат збірки - файл module.o . Для установки модуля в систему потрібні права суперкористувача root. Сама установка здійснюється командою insmod. Перегляд встановлених модулів доступний root -у по команді lsmod. Видалення модуля (теж з правами root ) - rmmod .
Удосконолимо модуль:
1. Непогано було б дати користувачеві можливість зрозуміти, що і як робить модуль, і як зв'язатися з автором. Для цього в module.h визначені макроси MODULE_DESCRIPTION і MODULE_AUTHOR. Отримати інформацію про автора можна командою modinfo - a, опис модуля - modinfo - d. Для виконання цих команд знову ж потрібні права root.
2. Модуль може виводити на консоль повідомлення; для цього є функція printk. Звичайно, для реальних модулів це зазвичай ні до чого, однак на етапі налагодженні такі повідомлення бувають дуже корисні.
3. Не завжди зручно називати функцію ініціалізації init_module, а функцію вивантаження cleanup_module. У файлі init.h визначено функції module_init() і module_exit(), що дозволяють зняти обмеження .
Новий код модуля наведено нижче:
/*
====================================
* Приклад найпростійшого модуля ядра
лаб.2_6_1
2014
====================================
*/
#define MODULE
#define __KERNEL__
#include <module.h> // визначення для модуля
#include <init.h> // module_init и module_exit
#include <kernel.h> // printk
MODULE_AUTHOR("Mike Goblin mgoblin@mail.ru");
MODULE_DESCRIPTION("Test module for linux kernel");
int module_start()
{
printk("This is a test module startup message\n");
return 0;
}
void module_stop()
{
printk("Module is dead\n");
return;
}
module_init(module_start);
module_exit(module_stop);
При завантаженні і вивантаженні модуля ви побачите в консолі тестові повідомлення модуля.
Реєстрація пристрою і захоплення ресурсів Наш модуль нічого не робить, та й взагалі недоступний для програм. Щоб зробити пристрій доступним - при завантаженні модуля його необхідно зареєструвати в системі і вказати в ресурси які використовуються.
Для реєстрації різних типів пристроїв в заголовному файлі fs.h визначено відповідні функції з префіксом register. Так, нам для реєстрації нашого "драйвера символьного пристрою" необхідно використовувати функцію register_chrdev. Функція оголошена так:
extern int register_chrdev(unsigned int, const char *, struct file_operations *);
Перший параметр - старший номер файлу пристрою (тип пристрою). Якщо цей параметр дорівнює 0 , то функція повертає вільний старший номер для нашого типу пристрою. Краще так і робити, тому що при цьому виключаються конфлікти старших номерів пристроїв. Другий параметр — ім'я пристрою. Під цим ім'ям пристрій буде відображатися в списку пристроїв. Третій параметр - структура з покажчиками на функції драйвера. Тут ми підходимо до питання про те, як система працює з драйвером. Драйвер зберігає таблицю доступних функцій, а система викликає ці функції, коли хтось намагається виконати деякі дії (відкриття, запис, читання) з файлом пристрою, який має старший номер нашого пристрою. Для символьного пристрою — таблиця функцій драйвера зберігається в структурі file_operations. Ця структура і передається при реєстрації пристрою. Поки що ми будемо передавати структуру незаповненою (адже функції роботи з пристроями що не написані ). У прикладі , наведеному нижче , структура оголошена в рядку 32. Драйвер реєструється в рядках 42-48. У рядку 31 оголошена змінна Major для зберігання старшого номера пристрою, одержуваного від ОС при реєстрації. Змінна оголошена як static, т.як. старший номер буде потрібен і при знятті реєстрації перед вивантаженням модуля. Після реєстрації драйвера , як правило , відбувається пошук присутніх у системі пристроїв даного типу та їх параметрів (номери переривань, порти вводу-виводу і т.д.). Методика пошуку — своя для кожного типу пристроїв, тому важко привести який-небудь код. У нашому прикладі - будемо вважати, що є два пристрої, що використовують один діапазон портів введення-виведення і одне переривання. Перше з наших пристроїв має молодший номер 0, а друге - 1. Для подальшої роботи - створимо файли даних пристроїв, набравши (з правами root) в директорії /dev команди:
mknod my_dev c 254 0
mknod my_dev c 254 1
Замість 254, можливо, знадобитися підставити інший старший номер(він видається модулем на консоль при завантаженні). Нижче приведений новий варіант нашого модуля.
1 /*
2 ===========================================
3 Приклад захоплення ресурсів модулем ядра
4 лаб.2_6_2
5 2014
6 ===========================================
7 */