Теорія операційної системи

:: Меню ::

Головна
Представлення даних в обчислювальних системах
Машинні мови
Завантаження програм
Управління оперативною пам'яттю
Сегментна і сторінкова віртуальна пам'ять
Комп'ютер і зовнішні події
Паралелізм з точки зору програміста
Реалізація багатозадачності на однопроцесорних комп'ютерах  
Зовнішні пристрої
Драйвери зовнішніх пристроїв
Файлові системи
Безпека
Огляд архітектури сучасних ОС

:: Друзі ::

Карта сайту
 

:: Статистика ::

 

 

 

 

 

Архітектура драйвера

Типовий протокол роботи із зовнішнім пристроєм складається з аналізу запиту, передачі команди пристрою, чекання переривання після закінчення цієї команди, аналізу результатів операції і формування відповіді зовнішньому пристрою. Багато запитів не можуть бути виконані в одну операцію, тому аналіз результатів операції може привести до виводу про необхідність передати пристрою наступну команду.
Драйвер, що реалізовує цей протокол, природним чином розпадається на дві нитки: основну, яка здійснює власне обробку запиту, і обробник переривання. Залежно від ситуації, основна нитка може представляти собою самостійну нитку, або її код може виконуватися в рамках нитки, що сформувала запит.
У прикладі 10.1 приводиться скелет функції write () драйвера послідовного пристрою в системі Linux. Скелет спрощений (зокрема, ніяк не вирішується проблема реєнтерабельності функції foo_write. Використаний механізм синхронізації з обробником переривання також залишає бажати кращого), але має саме такий архітектуру, яка була описана раніше. Текст цитується по документу [HOWTO khg], переклад коментарів і додаткові коментарі автора.

Приклад 10.1. Скелет драйвера послідовного пристрою для ОС Linux

f* Основна нитка драйвера */
static int foo_write(struct inode * inode, struct file * file, char * buf, int count)
Щ
/* Отримати ідентифікатор пристрою: */
к/с в операційні систв
unsigned int minor = MINOR(inode->i_rdev); unsigned long сміттю size; unsigned long total_bytes_written = 0; unsigned long bytes__written;
/* Знайти блок змінних достатку пристрою */ struct foo_struct *foo = &foo_table[minor];
do { copy_size = (count <= Foo_buffer_size ?
count : Fooj3uffer_'size) ;
/* Передати дані з призначеного для користувача контексту */ memcpy_fromfs(foo->foo_buffer, buf, copy_size);
while (copy_size) {
/* Тут ми повинні ініціалізувати прериванія*/
if (some_error_has_occured) { /* Тут ми повинні обробити помилку */
current->timeout = jiffies + Foo_interrupt_timeout;
/* Встановити таймаут на випадок, якщо переривання буде пропущено */
interruptible_sleep_on (&f oo->foo_wait_queue) ;
if (some_error_has_occured) { /* Тут ми повинні обробити помилку */
bytes_written = foo->bytes_xfered; foo->bytes_written = 0;
if (current->signal H ~current->blocked) { if (total_bytes_written + bytes__written)
return total_bytes_written + bytes_written; else
return -eintr; /* Нічого не було записано, системний виклик був перерваний, потрібна повторна спроба */
O- Драйвери зовнішніх пристроїв
total_byr.c5_v;r:.i.U-.r. т= bytes_written; buf += bytes_written; count -= bytes_written;
) while (count > 0) ; return total_bytes_written;
/* Обробник переривання */ static void foo__interrupt (int irq)
{ struct foo_struct *foo = &foo__table [foo_irq[irq]] ;
/* Тут необхідно виконати всі дії, які мають бути виконані по перериванню.
Прапор в foo__table вказує, здійснюється операція читання або запису. */
/* Збільшити foo->bytes_xfered на кількість фактично переданих символів * /
if (буфер полон/пуст) wake_up_interruptible (&foo->foo_wait_queue) ;
}

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

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

  • включити мотор дисковода;
  • діждатися, поки диск розганятиметься до робочої швидкості (більшість контроллерів генерують з цієї нагоди переривання);
  • дати пристрою команду на переміщення прочитуючої голівки;
  • діждатися переривання по кінцю операції переміщення;
  • запрограмувати ПДП і ініціювати операцію запису;
  • діждатися переривання, що сигналізує про кінець операції.

Лише після цього можна буде передати дані програмі. Наївна реалізація таких багатокрокових операцій могла б виглядати так, як показано в прикладі 10.2.

Приклад 10.2. Простий драйвер контроллера гнучкого диска

/* Обробники переривань залежно від достатку */ void handle_spinup_interrupt(int irq, fdd_struct *fdd){
if (motor_speed_ok(fdd)) wake_up_interruptible((&fdd->fdd_wait_queue);
void handle_seek_interrupt(int irq, fdd_struct *fdd){
if (verify_track(fdd)) wake_up_interruptible((&fdd->fdd_wait_queue);
void handle_dma_interrupt(int irq, fdd_struct *fdd){
/* Збільшити fdd->bytes_xfered на кількість фактично переданих символів */
if (буфер полон/пуст) wake_up_interruptible(&fdd->fdd_wait_queue);
/* Основна нитка драйвера */
static int fdd_write(struct inode * inode, struct file * file, char * buf, int count)
10. Драйвери зовнішніх пристроїв
/* Отримати ідентифікатор пристрою: */ = MINOR ( inode->irdev) ;
unsigned long ccpy_size;
unsigned long total_bytes_written = 0;
unsigned long bytes_written;
int state;
/* Знайти блок змінних достатку пристрою */ struct fdd_struct *fdd = &fdd_table [minor] ;
do { copy_size = (count < = FDD__BUFFER_SIZE ?
count : Fdd_buffer_size) ;
/* Передати дані з призначеного для користувача контексту */ memcpy_f rornfs (fdd->fdd_buf fer, buf, copy_size) ;
while (copy_size) { if ( !motor_speed_ok (fdd) ) { fdd->handler = handle__spinup_interrupt; turn_motor_on (fdd) ;
current->timeout = jiffies + Fdd_interrupt_timeout; interruptible_sleep_on (&fdd->fdd_wait_queue) ; if (current->signal & -current->blocked) { if (total_bytes_written)
return total_bytes_written; else
return -eintr; /* Нічого не було записано, системний виклик був перерваний, потрібна повторна спроба */
if (fdd->current_track != Calculate_track(file)) { fdd->handler = handle_seek_interrupt; seek_head (fdd, Calcu1ate__TRACK (f ile) ) ; current->timeout = jiffies + Fdd_interruptjrimeout; interruptible_sleep_on(&fdd->fdd__wait_queue); if (current->signal & ~current->blocked) ( if (total bytes written)
Введення в операционниєсист^
return total_bytes_written; else
return -eintr; /* Нічого не було записано, системний виклик був перерваний, потрібна повторна спроба */
fdd->handler = handle_dma_interrupt;
setup_fdd_dma(fdd->fdd_buffer+bytes_xfered, copy_size) issue_write_command(fdd);
current->timeout = jiffies + Fdd_intekrupt_timeout; interruptible_sleep_on (Sfdd->fdd_wait_queue) ;
bytes_written = fdd->bytes_xfered; fdd->bytes_written = 0;
if (current->signal & ~current->blocked) { if (total_bytes_written + bytes_written)
*
return total_bytes_written + bytes_written; else
return -eintr; /* Нічого не було записано, системний виклик був перерваний, потрібна повторна спроба */
total_bytes_written += bytes_written; buf += bytes__written; count -= bytes_written;
} while (count > 0) ; return total bytes written;
/* Обробник переривання */ static void fdd_interrupt(int irq){ struct fdd_struct *fdd = &fdd_table[fdd_irq[irq]];
f (fdd->ha:idier != NULL) { fdd->handier(irq, fdd); fdd->handier=mull;
} else
{
/* He наше переривання? */
}
}

Видно, що пропонований драйвер здійснює обробку помилок і формування подальших команд в основній нитці драйвера. Велика спокуса перенести ці функції або їх частину обробник переривань. Таке рішення дозволяє скоротити інтервал між послідовними командами, і, таким чином, можливо, підвищити продуктивність роботи пристрою.
Проте дуже великий час, що проводиться в обробнику переривання, небажано з точки зору інших модулів системи, оскільки може збільшити реальний час реакції для них. Особливо поважно це для систем, які вимикають планувальник на час обслуговування переривань. Тому багато ОС накладають обмеження на час обслуговування переривань, і часто це обмеження виключає можливість формування команд і твору інших складних дій в обробнику.
Обробник, таким чином, повинен виконувати лише ті операції, які потрібно виконати негайно. Зокрема, багатьом пристроям потрібно так чи інакше пояснити, що переривання оброблене, щоб вони зняли сигнал запиту переривання. Якщо цього не сделать, після повернення з обробника і обумовленого цим зниження пріоритету ЦПУ, обробник буде викликаний знову.
Втім, нерідко пропонується дорога до обходу і цього обмеження: обробникам переривань дозволено створювати високопріоритетні нитки, які почнуть виконуватися відразу ж після того, як будуть обслужені всі переривання. Надалі ми називатимемо ці високопріоритетні нитки fork-процессами (цей термін використовується в VMS. Інші ОС, хоча і використовують аналогічні поняття, часто не мають виразної термінології для їх опису).

Fork-процессы в VMS
З точки зору планувальника VMS, fork-процесс є нитка з укороченим контекстом. Замість звичайного дескриптора процесу (РСВ — Process Control Block) використовується UCB — Unit Control Block, блок управління пристроєм. Укорочення полягає в тому, що ця нитка може працювати лише з одним банком віртуальної пам'яті з трьох, що є в процесора VAX, а саме з системним (повний список банків пам'яті VAX приведений в главі 5); таким чином, при перемиканні контексту задіюється менше регістрів диспетчера пам'яті. Fork-процесс має вищий пріоритет, ніж призначені для користувача процеси, і може бути витиснений лише більш пріорцтсб ним fork-процессом і обробником переривання.

При використанні fork-процессов, обслуговування переривання распадаєт на власне обробник (що викликається по сигналу переривання і виконуваний з відповідним пріоритетом) і код обробки поста, що виконується fork-процессом, на який не поширюються обмеження часу і який цілком може здійснити планерування наступних операцій (приклад 10.3).

Приклад 10.3. Складніший драйвер контроллера гнучкого диска

/* Обробники переривань залежно від достатку */ void schedule_seek (fdd__struct *fdd)
if ( !motor_speed_pk (fdd) ) {
fdd->handler = schedule_seek;
retry_spinup ( ) ; }
if (fdd->current_track != CALCULATEJTRACK (fdd->f ile) ) fdd->handler = schedule_command; seek_head(fdd, Calculate_track (f ile) } ; } else
/* Ми вже на потрібній доріжці */ schedule operation (fdd) ;
void schedule_operation(fdd_struct *fdd){
if (fdd->current_track != CALCULATEJTRACK(fdd->file)) { fdd->handler = schedule_operation; retry_seek(fdd); return; }
switch(fdd->operation)( case Fdd_write:
fdd->handler = handle_dma_write_interrupt; setup_fdd_dma(fdd->fdd_buffer+fdd->bytes__xfered, fdd->copy_size)
I issue_write_coromand (fdd) ; break; case Fdd_read:
fdd->handler = handle_dma_read_interrupt;
setup_fdd_dma (fdd->fdd_buf fer-t-fdd->bytes_xfered, fdd->copy_size)
issue_read_command (fdd) ;
break; /* Тут же ми повинні обробляти інші команди
що вимагають попереднього SEEK */
void handle_dma_write_interrupt (fdd_struct *fdd)
( /* Збільшити fdd->bytes_xfered на кількість фактично
переданих символів * /
if (буфер полон/пуст)
/* Тут ми не можемо передавати дані з призначеного для користувача
адресного простору . Треба будити основну нитку * /
wake_up_interruptible (&fdd->fdd_wait_queue) ; else {
fdd->handler = handle__dma__write_interrupt;
setup_fdd__dma (fdd->fdd_buf fer+fdd->bytes_xfered, fdd->copy_size)
issue_write_corranand(fdd);
/* Основна нитка драйвера */
static int fdd_write (struct inode * inode, struct file * file
char * buf, int count) (
/* Отримати ідентифікатор пристрою: */ unsigned int minor = MINOR ( inode->i_rdev) ; /* Звернете увагу, що майже всі змінні основної нитки
"переїхали" в описувач достатку пристрою */ /* Знайти блок змінних достатку пристрою */ struct fdd struct *fdd = &fdd table [minor] ;
fdd->total_bytes_written = 0; fdd->operation = Fdd_write;
do { fdd->copy_size = (count < = Fdd_buffer_size ?
count : Fdd_boffer_size);
/* Передати дані з призначеного для користувача контексту */ memcpy_fromfs(fdd->fdd_buffer, buf, copy_size);
if (!motor_5peed_ok()) (
fdd->handler = schedule_seek;
turn_motor_on(fdd); } else
schedule_seek(fdd);
current->timeout = jiffies + Fdd_interrupt__TIMEOUT; inte.rruptible_sleep_on(&fdd->fdd_wait_queue); if (current->signal & ~current->blocked) { if (fdd->total_bytes_written+fdd->bytes__written)'
return fdd->total_bytes_written+fdd->bytes_written; else
return -eintr; /* Нічого не було записано
системний виклик був перерваний, потрібна повторна спроба */
fdd->total_bytes_written += fdd->bytes_written; fdd~>buf += fdd->bytes_written; count -= fdd->bytes_written;
} while (count > 0) ; return total bytes written;
static struct tq_struct floppy_tq;
/* Обробник переривання */ static void fdd interrupt(int irq)
truct fdcl struct *fdd = &fdd_table [fdd_irq [irq] ] ;
Af (fdd->ha!,;;ier != NULL) {
void (Chandler)(int irq, fdd_struct * fdd) ;
f]_0ppy_tq. routine = (void *)(void *) fdd->handler;
floppy tq.parameter = (void *)fdd;
fdd->handler=null;
queue_task(sfloppy_tq &tq_immediate); } else
{ /* He наше переривання? */
}
}

Видно, що тепер нашим драйвером є послідовність функцій, переривань, що викликаються обробником. Звернете увагу, що якщо ми квапимося, чергову функцію можна викликати і безпосередньо в обробнику, а не створювати для неї fork-процесс queue_task . Але найголовніше, на що нам слід звернути увагу — послідовність цих функцій не задана жорстко: кожна з функцій сама визначає, яку операцію викликати наступною. У тому числі, вона може вирішити, що наступна операція може полягати у виклику тієї ж самої функції. У прикладі 10.3 ми використовуємо цю можливість для простий обробки помилок: повтору операції, яка не вийшла.
Для того, щоб зрозуміти, що ж у нас вийшло, які можливості нам відкриває така архітектура і як ними користуватися, нам слід зробити екскурс в одну з важливих областей теорії програмування.

 

:: Реклама ::

 

:: Посилання ::


 

 

 


Copyright © Kivik, 2017