Анатомія драйвера

Знову повернемося в традиційну область розробки операційних систем (і програм для мікроконтролерів) — написання драйверів.

Я спробую виділити деякі загальні правила і канони в цій області. Як завжди — на прикладі Фантома.

Драйвер — функціональна компонента ОС, відповідальна за відносини з визначеною підмножиною апаратури комп'ютера.

З легкої руки того ж Юникса драйвера діляться на блокові і байт-орієнтовані. У минулі часи класичними прикладами були драйвер диска (операції — записати і прочитати сектор диска) і драйвер дисплея (прочитати і записати символ).

У сучасній реальності, звичайно, все складніше. Драйвер — типовий інстанси-об'єкт класу та класів цих до фіга і більше. В принципі, інтерфейс драйверів намагаються якось стиснути в прокрустове ложе моделі read/write, але це самообман. У драйвера мережевої карти є метод «прочитати MAC-адресу карти» (який, звичайно, можна реалізувати через properties), а у драйвера USB — ціла пачка USB-специфічних операцій. Ще веселіше у графічних драйверів — небудь bitblt( startx, starty, destx, desty, xsize, ysize, operation ) — звичайна справа.

Цикл життя драйвера, в цілому, може бути описаний так:

  • Ініціалізація: драйвер отримує ресурси (але не доступ до своєї апаратури)
  • Пошук апаратури: драйвер отримує від ядра або сам знаходить свої апаратні ресурси
  • Активація — драйвер починає роботу
  • Поява/вимикання пристроїв, якщо це доречно. См. той же USB.
  • Засинання/прокидання апаратури, якщо це доречно. У контролерах часто невживана апаратура вимикається для заощадження.
  • Деактивація драйвера — обслуговування запитів припиняється
  • Вивантаження драйвера — звільняються всі ресурси ядра, драйвер не існує.


(Взагалі я написав в минулому році чернетка відкритої специфікації інтерфейсу драйвера — див. репозиторій і документ.)

Мені відомі три моделі побудови драйвера:

  • Полінг
  • Переривання
  • Нитки (threads)

Драйвер на основі поллінг (циклічного опитування) пристрою

Такі драйвера застосовуються тільки з великого горя або з великої необхідності. Або якщо це проста вбудована мікроконтролерну систему, в якій і є всього два драйвера. Наприклад, перетворювач інтерфейсів serial port <-> TCP, у якому мережа працює по перериваннях, роботу з послідовним портом може, в принципі, виконувати і поллингом. Якщо не шкода надлишку тепла та витрат енергії.

Є і ще одна причина: такі драйвера практично неубиваемы. Тому, наприклад, в ОС Фантом налагоджувальна видача ядра в послідовний порт зроблена саме так.

У циклі перевіряємо готовність порту прийняти байт, передаємо байт, закінчили вправу.

#define PORT 0x3F8

int dbg_baud_rate = 115200;

void debug_console_putc(int c)
{
while(! (inb(PORT+5) & 0x20) )
;

if( c == '\n' ) outb(PORT, '\r' );
outb(PORT, c);
}

void arch_debug_console_init(void)
{
short divisor = 115200 / dbg_baud_rate;

outb( PORT+3, 0x80 ); /* set up to load divisor latch */
outb( PORT, divisor & 0xf ); /* LSB */
outb( PORT+1, divisor >> 8 ); /* MSB */
outb( PORT+3, 3 ); /* 8N1 */
}


Такий драйвер, як неважко бачити, пожирає процесор в очікуванні готовності пристрою. Це можна полагодити, якщо швидкість роботи самого драйвера некритична:

while(! (inb(PORT+5) & 0x20) )
yield(); // віддати процесор іншим ниткам, поки пристрій не готовий


Але, звичайно, в цілому це нікуди не годна (крім вищенаведеного випадку:) модель.

Драйвер на основі переривань

Загальна структура такого драйвера виглядає ось як:


strauct device_state dev;

dev_write( buf )
{
dev.buf = buf;
if( dev.started ) dev_start();
cond_wait( &dev.ready );
}

dev_interrupt()
{
dev_output();
}

dev_start() 
{
dev.started = 1;
dev_enable_interrups( &dev_interrupt );
dev_output();
}

dev_output()
{
if( buffer_empty() || (!dev.started) ) 
{
dev.started = 0;
dev_disable_interrupts();
cond_signal( &dev.ready ); // done
return;
}

// send to device next byte from buffer
out( DEV_REGISTER, *dev.buf++ );
}


Фактично, такий драйвер породжує для себе псевдо-нитка: потік управління, який живе тільки на вступі переривань від пристрою.

Як тільки драйвер отримує черговий запит на запис, він включає переривання і «вручну» ініціює відправку в пристрій першого байта даних. Після чого входить нитка засинає, чекаючи кінця передачі. А може і повернутися, якщо потрібна асинхронна робота. Тепер драйвер буде чекати переривання від пристрою. Коли пристрій «прожует» отриманий байт, вона згенерує переривання, при обслуговуванні якого драйвер або відправить черговий байт (і буде чекати чергового переривання), або закінчить роботу, відключити переривання і «відпустить» чекає всередині dev_write() нитку.

Що забуто

Перш ніж ми перейдемо до останньої моделі драйвера, перерахуємо речі, які я (навмисно) пропустив у попередньому оповіданні.

Обробка помилок
У нашому псевдокоде ніяк не перевіряється успішність введення-виведення. Реальний пристрій може відмовити або повідомити про несправності носія. Вийняли кабель з порту локалки, на диску стався поганий блок. Драйвер повинен виявити та обробити.

Таймаут
Пристрій може зламатися і просто не відповісти на запит переривання, або ніколи не виставити біт готовності. Драйвер повинен запросити таймерное подія, яка б вивело його зі ступору на такий випадок.

Смерть запиту
Якщо навколишня нас ОС це дозволяє, то треба бути готовим до того, що ввійшла в драйвер нитка, в рамках якої «прийшов» запит вводу-виводу, може бути просто вбита. Це не повинно призводити до фатальних наслідків для драйвера.

Синхронізація
Для простоти я вказую як примітиву синхронізації cond. У реальному драйвері це неможливо — cond вимагає охоплюючого mutex в точці синхронізації, а в перериванні який вже mutex — не можна! Ось в останній моделі, драйвері з власної ниткою, можна застосовувати cond як засіб синхронізації нитки користувача та нитки драйвера. А ось синхронізація з перериванням — тільки spinlock і семафор, причому реалізація семафора повинна бути готова до можливості активувати (відкрити) семафор з переривання. (Фантоми це так і є)

Драйвер на основі нитки

Від попереднього він відрізняється тим, що має власну нитку, яка виконує ввід-висновок.


dev_thread()
{
while(dev.active)
{
while(!dev_buffer_empty())
cond_wait(&dev.have_data);

while( /* device busy */ )
cond_wait(&dev.interrupt);

dev_enable_interrupts();
// send to device next byte from buffer
out( DEV_REGISTER, *dev.buf++ );
}
}

dev_interrupt()
{
dev_disable_interrupts();
cond_signal(&dev.interrupt);
}


Перевага такого драйвера в тому, що нитки можна дозволити собі куди більше, ніж хендлера переривання — можна виділяти пам'ять, управляти таблицями сторінок і взагалі кликати будь-яку функцію ядра. З переривання не можна дозволити собі довгі і, тим більше, що блокують операції.

Зазначимо, що є третя, проміжна модель, в якій драйвер не має своєї нитки, а виконує все те ж саме з нитки запиту вводу-виводу. Але, по-перше, див. пункт про те, що її можуть вбити, по-друге це жлобство :), а в третіх — не завжди вона (нитка) такого хоче. Іншим би хотілося асинхронного обслуговування.

Блоковий введення-виведення, сортування і паркани

Дискові драйвера зазвичай мають на вході чергу запитів — ОС генерує запити на введення-виведення пачками, і всі запити на рівні драйвера асинхронны. При цьому хороший драйвер має власну стратегію обслуговування запитів, причому — обслуговування не в порядку надходження.

Дійсно, якщо на звичайному дисковому пристрої виконувати запити в тому порядку, в якому вони прилетіли, голівці накопичувача доведеться здійснювати хаотичні руху по диску, сповільнюючи введення-виведення.

Зазвичай драйвер просто сортує чергу запитів за номером блоку і обслуговує їх так, щоб головка диска послідовно рухалася від зовнішньої доріжки до внутрішньої, або навпаки. Це сильно допомагає.

Але не кожну пару запитів можна поміняти місцями. Якщо файлова система (або її аналог) вирішила, що їй треба гарантувати цілісність даних на диску, вона б дуже хотіла переконатися в тому, що певна група запитів завершена. Для цього в чергу запитів вставляється спеціальний запит вводу-виводу, який забороняє перемішувати запити до себе з запитами після себе.

Крім того, погана ідея переставляти місцями запит на запис блоку N і запит на читання того ж блоку. Втім, це питання домовленостей.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.