
101 Return;
102}
103 module_init(module_start);
104 module_exit(module_stop);
Захоплення портів введення -виведення здійснюється у функції module_start (рядки 49-60). У рядку 51 - перевіряється доступність діапазону портів (функцією check_region, оголошеної в заголовному файлі ioport.h).
Перший параметр - початок діапазону, другий - кількість запитуваних портів. Якщо даний діапазон портів введення-виведення не зайнятий, check_region повертає 0. Безпосередньо захоплення портів здійснюється в рядку 56, функцією request_region (яка також визначена в заголовному файлі ioport.h). Перші два параметри аналогічні параметрам check_region. Третій параметр - ім'я пристрою, за яким будуть закріплені порти.
Якщо ви не зареєструєте порти, які не використовуються пристроєм, нічого страшного не станеться: вони все одно будуть доступні з модуля (якщо ніякий інший модуль не зареєстрував ці порти - від чого, на жаль, ніхто не застрахований). Якщо ви зареєстрували діапазон портів — НЕ забудьте його звільнити при вивантаженні модуля; інакше система буде вважати даний діапазон зайнятим, і заборонить доступ до нього навіть при вивантаженому модулі. Природно, звільнення ресурсів відбувається в функції module_stop(звільнення діапазону портів введення-виведення виробляється в рядку 86 - викликом release_region). Багато хто з пристроїв має власну пам'ять(наприклад, відеокарти). Реєстрація блоків пам'яті пристрою проводиться за тим же принципом, що і реєстрація портів введення -виведення. Для реєстрації використовується check_mem_region, request_mem_region, для зняття реєстрації — release_mem_region.
Захоплення переривання (рядки 71-79) здійснюється викликом request_irq. Перший параметр функції - номер переривання, другий — обробник переривання, четвертий - ім'я пристрою, захоплюючого переривання. У разі вдалого захоплення - функція повертає 0. Звільняється переривання викликом free_irq. Перший параметр - номер переривання. При використанні інших параметрів функцій request_irq і free_irq ми поговоримо трохи пізніше. У нашому прикладі ми захоплюємо 11 - е переривання, т.як у мене на машині воно вільне.
У прикладі (для простоти) ми захоплюємо ресурси від імені одного пристрою - my_dev; в реальному житті потрібно було б захопити окремі діапазони портів, пам'яті і переривання для кожного з двох наших пристроїв.
Відкриття та закриття пристрою. Наступний крок - створення методів відкриття і закриття пристроїв. Попередньо слід оголосити структуру для зберігання інформації про стан пристрою і масив таких структур - для зберігання інформації про стан конкретних пристроїв, керованих драйвером. Індексами в масиві будуть молодші номери наших пристроїв. Для простоти ми вважаємо, що молодші номери починаються з 0, і максимальна кількість таких пристроїв в системі - 2. В реальності - де молодші номери можуть йти не підряд - частіше доводиться створювати не масив, а пов'язаний у списку.
// Структура для збереження стану пристрою
struct dev_state
{
int dev_open; // Чи відкритий пристрій
ssize_t byte_read; // Скільки байтів прочитано з пристрою
ssize_t byte_write; // Скільки байтів записано в пристрій
};
// Масив для збереження інформації про стан пристроїв
static struct dev_state state[MAX_INODE+1];
Код функції відкриття пристрою виглядає так (рядки пронумеровані):
1 static int device_open(struct inode *inode, struct file *filp)
2 {
3 struct dev_state *dev_state;
4 printk("Kernel: try opening device w/minor number %d\n", MINOR(inode->i_rdev));
5 dev_state = &state[MINOR(inode->i_rdev)];
6 if(dev_state->dev_open)
7 {
8 printk("Devise busy\n");
9 return -EBUSY;
10 }
11 dev_state->dev_open++;
12 dev_state->byte_read = 0;
13 dev_state->byte_write = 0;
14
15 MOD_INC_USE_COUNT;
16 return SUCCESS;
17 }
У рядку 5 ми отримуємо структуру стану пристрою. Зауважте, що молодший номер файлу пристрою(який вказує на те, яке з пристроїв даного типу в системі відкривається) ми отримуємо як MINOR(inode - > i_rdev). Потім, у рядках 6-10, ми перевіряємо, чи не відкрито вже даний пристрій(у нашому випадку - забороняємо повторне відкриття пристрою). Можливий і варіант, коли повторне відкриття дозволено, але його ми розглядати не будемо. Якщо пристрій ще не відкритий - ми збільшуємо лічильник відкриття пристрою і скидаємо статистику переданих та прийнятих байт. Далі (у рядку 15) ми інкрементуємо лічильник використання даного модуля.
Для цього використовується макрос MOD_INC_USE_COUNT, оголошений в заголовочному файлі module.h. Лічильник посилань використовується для заборони вивантаження модуля з пам'яті за наявності процесів, що працюють з функціями модуля. Тому необхідно уважно ставитися до управлінню лічильником використання модуля; якщо його значення більше 0 - модуль неможливо буде вивантажити(без перезавантаження комп'ютера). У випадку успішного відкриття пристрою - функція device_open повертає 0.
Функція закриття пристрою виглядає наступним чином:
static int device_close(struct inode *inode, struct file *filp)
{
struct dev_state *dev_state;
printk("Kernel: try to close device w/minor number %d\n", MINOR(inode->i_rdev));
dev_state = &state[MINOR(inode->i_rdev)];
if(!dev_state->dev_open)
{
printk("Device not open\n");
return SUCCESS;
}
dev_state->dev_open--;
MOD_DEC_USE_COUNT;
return SUCCESS;
}
Як видно з коду, закриття пристрою ми виконуємо завжди успішно;
ПРИ ЦЬОМУ зменшуємо на 1 кількість відкриттів даного пристрою і декрементуємо лічильник використання модуля.
Як приклад відкриття пристрою для користувача програмою розглянемо простий тестовий додаток:
/*Тестовий додаток*/
#include <module.h>
int main()
{
int fd;
printf("Test program\n");
fd = open("/dev/my_dev2",O_RDWR);
if (fd == -1) {
printf("open failed\n");
return -1;
}
printf("Device my_dev open\n");
close(fd);
printf("Device my_dev closed\n");
return 0;
}
Щоб вказати ядру на функції відкриття і закриття - заповнимо відповідні поля структури file_operations:
struct file_operations Fops =
{
open: device_open,
release: device_close
};
Розглянемо обмін даними з пристроєм, обробка переривань, керуючі коди. Читання і запис.
Отже - відкривати пристрій ми вміємо; тепер навчимо його читати і записувати дані. Наведемо код методів запису і читання повністю.
static ssize_t device_read(struct file *filp, char *buf, size_t buf_len, loff_t *offset){
struct inode* inode;
int count = buf_len;
struct dev_state *dev_state;
printk("Kernel: try to read\nKernel:
request reading %d bytes\n",buf_len);
inode = filp->f_dentry->d_inode;
printk("Kernel: minor number %d\n", MINOR(inode->i_rdev));
dev_state = &state[MINOR(inode->i_rdev)];
while(count--){
put_user(inb_p(PORT_START+10*MINOR(inode->i_rdev)), buf);
buf++;
}
dev_state->byte_read += buf_len;
printk("Kernel: read %d bytes\n", buf_len);
return buf_len;
}
static ssize_t device_write(struct file *filp, const char *buf, size_t buf_len,
off_t *offset){
struct inode* inode;
int count = buf_len;
unsigned char byte;
struct dev_state *dev_state;
printk("Kernel: try to write\nKernel:
request writing %d bytes\n",buf_len);
inode = filp->f_dentry->d_inode;
printk("Kernel: minor number %d\n", MINOR(inode->i_rdev));
dev_state = &state[MINOR(inode->i_rdev)];
while(count--){
get_user(byte, buf);
outb_p(byte, PORT_START+10*MINOR(inode->i_rdev));
buf++;
}
dev_state->byte_write +=buf_len;
printk("Kernel: written %d bytes\n", buf_len);
return buf_len;
}
Системі обов'язково треба дати знати про наявність функцій читання і запису;
для цього додамо в структуру file_operations посилання на ці функції:
struct file_operations Fops = {
open: devise_open,
release: device_close,
read: device_read,
write: device_write
};
При виклику операцій читання/запису нам передається буфер і його розмір. Зі структури file ми можемо отримати молодший номер нашого пристрою. Читанням з порту введення-виведення займається виклик inb_p. Записом порту - виклик outb_p (визначеного в asm/io.h).
Якщо ви уважно вивчили вищенаведений код, то напевно запитаєте: а навіщо для буфера виробляються виклики put_user при читанні і get_user при запису? Для вирішення цього питання згадаємо, що адресні простори ядра системи і користувацьких процесів різняться - значить покажчик на буфер коректний для користувача процесу, але для ядра його безпосереднє використання неприпустимо. Для вирішення цієї проблеми і призначені функції get_user і put_user. Функція get_user отримує байт з переданого користувачем буфера, put_user поміщає в цей буфер байт. Функції визначені в asm/uaccess.h. У даному прикладі ми припускали, що наш пристрій завжди готовий до читання або запису. На практиці це не так. Щоб "приспати" модуль на час, поки пристрій не готовий для читання або запису, застосовується виклик функції interruptible_sleep_on(). Ця функція переводить модуль в стан, коли він пропускає тимчасові слоти, відведені йому для роботи системою. Це дозволить підвищити швидкодію системи і знизити споживання ресурсів. "Пробуджується" модуль викликом wake_up_interruptible(), зазвичай з обробника переривань.
Обробка переривань
Обробка переривань - безсумнівно один з найцікавіших моментів написання драйвера. Повернемося трохи назад, до коду захоплення переривання у функції module_start.
// Захоплення переривання
if (request_irq(IRQ_NUM, irq_handler, 0, DEV_NAME, NULL)){
printk("Kernel: IRQ allocation failed\n");
release_mem_region(MEM_START, MEM_COUNT);
release_region(PORT_START, PORT_COUNT);
return -EBUSY;
}
Перший параметр - номер переривання, другий - функція обробник, а ось третій параметр - це прапори, найчастіше закінчения і їх комбінації:
SA_INTERRUPT - говорить системі про те, що переривання буде "швидким", або по іншому під час нього заборонена обробка всіх інших переривань. Відсутність цього прапора робить переривання повільним, під час його обробки можуть виникати і оброблятися інші переривання. Якщо немає гострої необхідності - краще обробляти переривання в "повільному режимі".
SA_SHIRQ - говорить про те, що дане переривання може розділяться декількома пристроями. Для забезпечення спільного доступу до перериванню всі пристрої повинні захоплювати його з прапором SA_SHIRQ.
Четвертий параметр - ім'я пристрою, яке захоплює переривання. П'ятий - ідентифікатор пристрою, передається обробнику переривання, щоб при наявності декількох пристроїв на одному перериванні можна було зрозуміти, яке з пристроїв видало переривання. Заготовка для обробника має наступний вигляд:
void irq_handler ( int irq , void * dev_id , struct pt_regs * regs )
{
return;
}
Перший параметр - номер переривання, другий — ідентифікатор пристрою, третій - регістри процесора. З першого погляду — все просто, заготовка для обробника є, переривання виникає частіше всього за готовності до запису/читання — потрібно перевірити , що з них доступно - і записати або читати. Але, дивимося уважно , що треба зробити в обробнику:
1. Перевірити dev_id на предмет, чи це наш пристрій;
2. Проаналізувати номер irq, на предмет наявності пристрою на цьому перериванні(можливо помилкове спрацьовування);
3. Перевірити, чи відкрито пристрій;
4. Перевірити готовність до прийому або передачі;
5. Перевірити, чи є в буфері дані для прийому або передачі;
6 . Надіслати або прийняти з пристрою інформацію.
Виникає питання, які з цих дій відносяться безпосередньо до обробки факту виникнення переривання? 1, ну може 2. А час на обробку кожного переривання витрачається, і немає гарантії, що під час обробки одного переривання не виникне інше. У Linux пропонується наступне рішення: розділити обробник на дві частини -безпосередньо обробник(top half) і функція, яка може бути викликана пізніше для аналізу переривання(bottom half). Таким чином, в top half ми виконуємо лише мінімальні дії з виявлення переривання і отриманню його параметрів, а в bottom half всі інші потрібні дії. Код top half повинен бути максимально компактним і швидким, щоб не пропустити виникнення нового переривання. Для виклику bottom half обробника використовуються черги завдань. Звичайно, можна створити і зареєструвати власну чергу завдань, але така черга вже напередвизначена в Linux - це так звана immediate queue. Таким чином, загальний алгоритм роботи top half наступний:
1. Виконати дії з виявлення достовірності переривання і з'ясувати факт приналежності переривання нашому пристрою;
2. Якщо є "гарячі дані", які можуть бути втрачені при виникненні наступного переривання, вважати їх;
3. Поставити в immediate - чергу bottom half;
4. "Позначити" immediate - чергу для виклику у вільний час процесора. В якості ілюстрації обробника переривання наведем приклад з книги Орі Померанца "Ядро Linux. Програмування модулів "даний приклад заміщає обробник переривання клавіатури. На жаль, немає способу відновити старий обробник без перезавантаження , тому даний код може бути потенційно небезпечний для вашої системи.
#define MODULE
#define __KERNEL__
#include <module.h>
// визначення для модуля
#include <init.h>
// module_init и mdule_exit
#include <kernel.h>
// printk
#include <sched.h>
// захоплення переривання
#include <tqueue.h>
// визначення черги
#include <interrupt.h>
// робота з перериванням
#include <io.h>
// введення та виведення портів
// ім'я нашого пристрою
#define DEV_NAME "my_dev";
// номер переривання
#define IRQ_NUM 1
// клавіатурне переривання
#define SUCCESS 0
MODULE_AUTHOR("Mike Goblin mgoblin@mail.ru");
MODULE_DESCRIPTION("Test module for linux kernel");
// bottom half обробники аналізує, яка клавіша
// була натиснута або відпущена і видає повідомлення про це на консоль
static void got_char(void *scancode){
printk("Scan code %x %s.\n",
(int)*((char *) scancode) & 0x7f,
*((char *) scancode) &0x80?
"released":"pressed");
return;
}
// обробник переривання -- top half
void irq_handler(int irq, void *dev_id, struct pt_regs *regs){
static unsigned char scancode;
static struct tq_struct task = {
{},0,got_char,&scancode
};
unsigned char status;
printk("Oops, IRQ\n");
status = inb(0x64);
scancode = inb(0x60);
queue_task(&task, &tq_immediate);
mark_bh(IMMEDIATE_BH);
return;
}
// Ініціалізація модуля
int module_start(){
printk("Kernel: this is a test module startup message\n");
//убираємо стандартний обробник переривання
free_irq(IRQ_NUM, NULL);
// захоплення переривання
if(request_irq(IRQ_NUM, irq_handler, 0, DEV_NAME, NULL)){
printk("Kernel: Allocation irq failed.\n");
return -EBUSY;
}
printk ("Kernel: IRQ allocated\n");
return SUCCESS;
}
// дії перед вивантаженням модуля
void module_stop(){
// знімаємо захоплення irq
free_irq(IRQ_NUM, NULL);
printk("Kernel: release irq\n");
printk("Kernel: module is dead \n");
return;
}
module_init(module_start);
module_exit(module_stop);
Виклик free_irq вивантажує стандартний обробник, функція irq_handler - містить код top half; в ньому queue_task ставить в чергу виклик got_char, що виконує функції bottom half. А активує чергу immediate виклик mark_bh.
Ioctl
Ми навчилися читати з пристрою і записивать в нього, але цим потреби роботи з пристроєм не обмежуються. А що, коли треба отримати будь-яку слжбову інформацію або виконати якісь маніпуляції з налаштуванням пристрою? Для цього використовують ioctl - керуючі коди. Передача таких кодів налаштування змушує його виконати запитані дії.
Щоб значення кодів були доступні з модулів, їх визначення винесене в окремий заголовний файл our_module.h. Його вміст приведено нижче:
#ifndef OUR_MODULE_H
#define OUR_MODULE_H 1
#endif <module.h>
#include <ioctl.h>
#define MAJOR 42
// отримання кількості зчитаних байтів
#define IOCTL_GET_RSTAT _IOR(MAJOR,0,ssize_t *)
// отримання кількості записаних байтів
#define IOCTL_GET_WSTAT _IOR(MAJOR,1,ssize_t *)
// скидання статистики считаних байтів
#define IOCTL_RESET_RSTAT _IOR(MAJOR,2,ssize_t *)
// скидання статистики записаних байтів
#define IOCTL_RESET_WSTAT _IOR(MAJOR,3,ssize_t *)
//Структура для зберігання стану нашого пристрою
struct dev_state{
int dev_open;
ssize_t byte_read;
ssize_t byte_write;
};
Припустимо, ми хочемо отримати дані про загальну кількість прочитаних і записаних байтів. Ці дані накопичуються при читанні - запису в структурі стану пристрою dev_state.
У нашому файлі визначено чотири керуючих коду IOCTL_GET_RSTAT,
IOCTL_GET_WSTAT, IOCTL_RESET_RSTAT, IOCTL_RESET_WSTAT.
код містить старший тип ioctl, неомер пристрої, код команди і тип параметра ioctl. Можливі чотири типи ioctl:
_IO - Ioctl з параметром;
_IOW - Параметри копіюються від користувача в модуль;
_IOR - Параметри заповнюються модулем і передаються користувачеві;
_IOWR - Параметри можуть передаватися в обидві сторони.
Так як в коді ioctl присутній старший номер пристрою, то динамічна реєстрація пристрою(з отриманням динамічного старшого номери пристроїв) неможлива. Реєструвати потрібно із заданим старшим номером, в нашому випадку 42, тобто при виклику функції register_char_dev в якості першого параметра передавати не 0, а старший номер. Нижче наведена змінена частина коду реєстрації пристрою:
int e;
//регістрація пристрою
printk("Kernel: this is a test module startup message\n");
if((e=register_chrdev(MAJOR, DEV_NAME,&Fops))){
printk("Kernel: Register failed\n");
return e;
}
printk("Kernel: Device registered. Major number is %d\n", MAJOR);
Функція обробки ioctl в модулі має наступний вигляд:
static int device_ioctl(struct inode *inode, struct file *filp, unsigned int ioctl, unsigned long param)
{
ssize_t *size = (ssize_t *)param;
printk("Kernel: ioctl request ");
switch(ioctl){
case IOCTL_GET_RSTAT:
printk("get read statistic \n");
*size = state[MINOR(inode->i_rdev)].byte_read;
break;
case IOCTL_GET_WSTAT:
printk("get write statistic \n");
*size = state[MINOR(inode->i_rdev)].byte_write;
break;
case IOCTL_RESET_RSTAT:
printk("reset read statistic \n");
state[MINOR(inode->i_rdev)].byte_read=0;
break;
case IOCTL_RESET_WSTAT:
printk("get write statistic \n");
state[MINOR(inode->i_rdev)].byte_write=0;
break;
}
return SUCCESS;
}
Не забудьте додати в структуру file_operations посилання на функцію ioctl:
struct file_operations Fops = {
open: device_open,
release: device_close,
read: device_read,
write: device_write,
ioctl: device_ioctl
};
Для ілюстрації застосування ioctl пропонується наступний тестовий додаток
/* тестовий додаток */
#include <module.h>
#include <ioctl.h>
#include "our_module.h";
int main(){
int fd;
size_t cnt = 0;
ssize_t cnt2 = 0;
char buffer[256];
printf("Test program\n");
fd = open("dev/mydev", O_RDWR);
if(fd == -1){
printf("open failed\n");
return -1;
}
printf("Device my_dev open\n");
cnt = read(fd, buffer, sizeof(buffer));
printf("Device my_dev read %d bytes\n", cnt);
cnt = write(fd, buffer, sizeof(buffer));
printf("Device my_dev write %d bytes\n", cnt);
if(ioctl(fd, IOCTL_GET_RSTAT, &cnt2)<0){
printf("ioctl failed\n");
}
printf("ioctl read stat is %d\n", cnt2);
if(ioctl(fd, IOCTL_GET_WSTAT, &cnt2)<0){
printf("ioctl failed\n");
}
printf("ioctl write stat is %d\n", cnt2);
close(fd);
printf("Device my_dev closed;\n");
return 0;
}