Черги відкладених дій

(work queue) – це ще один спосіб реалізації відкладених операцій, який відрізняється від розглянутих раніше Черги дій дозволяють відкладати деякі операції для подальшого виконання потоком простору ядра – відкладені дії завжди виконуються в контексті процесу Тому код, виконання якого відкладено за допомогою постановки в чергу відкладених дій, отримує всі переваги, якими володіє код, що виконується в контексті процесу Найбільш важлива властивість – це те, що виконання черг дій управляється планувальником процесів і, відповідно, виконується код може переходити в стан очікування (sleep)

Зазвичай прийняти рішення про те, що необхідно використовувати: черги відкладених дій або відкладені переривання / тасклети, досить просто Якщо відкладеним діям необхідно переходити в стан очікування, то слід використовувати черги дій Якщо ж відкладені операції не можуть переходити в стан очікування, то скористайтеся тасклетамі або відкладеними перериваннями Зазвичай альтернатива використанню черг відкладених дій – це створення нових потоків простору ядра Оскільки при введенні нових потоків простору ядра розробники ядра зазвичай хмурить брови (а у деяких народів це означає смертельну образу), настійно рекомендується використовувати черги відкладених дій Їх дійсно дуже просто використовувати

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

7 Відсутність суворої послідовності виконання я добре позначається на продуктивності, але призводить до ускладнення програмування Конвертація я обробника в ВН в обробники тасклетов, наприклад, вимагає ретельного осмислення того, чи безпечно виконувати цей же код в той же самий час іншим тасклетом Однак після того як конвертація я виконана, це окупається підвищенням продуктивності

Реалізація черг відкладених дій

У своїй найбільш загальній формі підсистема черг відкладених дій – це інтерфейс для створення потоків простору ядра, які виконують деякі дії, десь поставлені в чергу Ці потоки ядра називаються робочими потоками (worker threads) Черги дій дозволяють драйверам створювати спеціальні робочі потоки ядра для того, щоб виконувати відкладені дії Крім того, підсистема черг дій містить робочі потоки ядра, які працюють за замовчуванням Тому в своїй загальній формі черги відкладених дій – це простий інтерфейс користувача для відкладання роботи, яка буде виконана потоком ядра

Робочі потоки, які виконуються за замовчуванням, називаються events / n, де п-Номер процесора Для кожного процесора виконується один такий потік Наприклад, в однопроцессорной системі виконується один потік events / 0 Б двухпроцессорной системи додається ще один потік- events / 1 Робочі потоки, які виконуються за замовчуванням, обробляють відкладені дії, які приходять з різних місць Багато драйверів, які працюють в режимі ядра, відкладають обробку своїх нижніх половин за допомогою потоків, що працюють за замовчуванням Якщо для драйвера або підсистеми немає суворої необхідності в створенні свого власного потоку ядра, то використання потоків, працюючих за замовчуванням, більш переважно

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

Структури даних для представлення потоків

Робочі потоки представлені за допомогою наступної структури workqueue_

struct

/*

* Зовні видима абстракція для представлення черг відкладених дій являє собою масив черг для кожного процесора:

*/

struct workqueue_struct {

struct cpu_workqueue_struct cpu_wq [NR_CPUS]

const char* name

struct list_head list

}

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

Структура cpu_workqueue_struct визначена у файлі kernel / workqueueс і є основною Ця структура показана нижче

/*

* Черга відкладених дій, повязана з процесором:

*/

struct cpu_workqueue_struct {

spinlock_t lock / * Черга для захисту даної структури * /

long rernove_sequence / * Останній доданий елемент

(Наступний для запуску) * /

long insert sequence / * Наступний елемент для додавання * / struct list_head worklist / * Список дій * / wait_queue_head_t more_work

wait_queue_head_t work_done

struct workqueue_struct * wq / * Відповідна структура workqueue_struct * /

task_t * thread / * Відповідний потік * /

int run_depth / * Глибина рекурсії функції run_workqueue () * /

}

Зауважимо, що кожентипробочих потоків має одну, повязану з цим типом структуру workqueue_struct Всередині цієї структури є по одному екземпляру структури cpu_workqueue_struc t для кожного робочого потоку і, отже, для кожного процесора в системі, так як існує тільки один робочий потік кожного типу на кожному процесорі

Структури для представлення дій

Всі робочі потоки реалізовані як звичайні потоки простору ядра, які виконують функцію worker_threa d () Після початкової ініціалізації ця функція входить в нескінченний цикл і переходить в стан очікування Коли-небудь дії ставляться в чергу, потік повертається до виконання і виконує ці дії Коли в черзі не залишається роботи, яку потрібно виконувати, потік знову повертається в стан очікування Кожна дія представлено за допомогою структури work_struct, визначеної у файлі Ця структура показана нижче

struct work_struct {

unsigned long pending / * Чи очікує це дія на виконання * / Struct list_head entry / * Звязаний список всіх дій * / void (* func) (void *) / * Функція-обробник * /

void * data / * Аргумент функції-обробника * /

void * wq_data / * Для внутрішнього використання * /

struct timer_list timer / * Таймер, який використовується для черг відкладених дій із затримками * /

}

Ці структури обєднані в звязаний список, по одному списку на кожен тип черги для кожного процесора Наприклад, для кожного процесора існує список відкладених дій, які виконуються потоками, які працюють за замовчуванням Коли робочий потік повертається до виконання, він починає виконувати всі дії, які знаходяться в його списку Після завершення роботи робочий потік видаляє відповідні структури work_struc t зі списку Коли список стає порожнім, потік переходить в стан очікування

Давайте розглянемо спрощену основну частину функції worker_threa d ()

for (;) {

set_task_state(current, TASK_INTERRUPTIBLE)

add_wait_queue(&ampcwq-&gtmore_work, &ampwait)

if (list_empty(&ampcwq-&gtworklist))

schedule()

else

set_task_state(current, TASK_RUNNING) remove_wait_queue (&ampcwq-&gtmore_work, &ampwait) if ( list_empty (&ampcwq-&gtworklist))

run_workqueue(cwq)

}

Ця функція виконує наступні дії в нескінченному циклі

• Потік переводить себе в стан очікування (прапор стану встановлюється в значення TASK_INTERRUPTIBLE), і поточний потік додається в чергу очікування

• Якщо звязаний список дій порожній, то потік викликає функцію schedule ()

і переходить в стан очікування

• Якщо список не порожній, то потік не переходить в стан очікування Замість цього він встановлює свій стан в значення TASK_RUNNING і видаляє себе з черги очікування

• Якщо список не порожній, то викликається функція run_workqueue () для виконання відкладених дій

Функція run_workqueue ()

Функція run_workqueue () у свою чергу виконує самі відкладені дії, як показано нижче

while (list_empty(&ampcwq-&gtworklist)) {

struct work_struct *work

void (*f) (void *)

void *data

work = list_entry(cwq-&gtworklistnext, struct work_struct, entry)

f = work-&gtfunc data = work-&gtdata list_del_init(cwq-&gtworklistnext) clear_bit(0, &ampwork-&gtpending) f(data)

}

Ця функція переглядає в циклі всі елементи списку відкладених дій і виконує для кожного елемента функцію, на яку вказує поле fun c відповідної структури workqueue_struct Послідовність дій така

• Якщо список не порожній, отримати наступний елемент списку

• Отримати покажчик на функцію (поле func), яку необхідно викликати, і аргумент цієї функції (поле data)

• Видалити отриманий елемент зі списку та обнулити біт очікування в структурі елемента

• Викликати отриману функцію

• Повторити зазначені дії

Вибачте, еслінепонятно

Взаємовідносини між різними, розглянутими в цьому розділі структурами досить заплутані На рис 71 показана діаграма, яка ці взаємини пояснює

На самому верхньому рівні знаходяться робочі потоки Може існувати кілька типів робочих потоків Для кожного типу робочих потоків існує один робочий потік для кожного процесора Різні частини ядра при необхідності можуть створювати робочі потоки За замовчуванням виконуються тільки робочі потокиevents  (Події) Кожен робочий потік представлений за допомогою структури cpu_workqueue_struct Структура workqueue_struc t представляє всі робочі потоки одного типу

Робочий потік Структура

cpu_workqueue_struct

Структура

workqueue_struct

По одному примірнику на процесор

Один примірник на кожен тип робочих потоків

Структура

work_struct

Один примірник

на кожну функцію відкладеного дії

Рис 71 Співвідношення між відкладеними діями, чергами, дій і робочими потоками

Наприклад, давайте будемо вважати, що на додаток до звичайного типу робочих потоків events був створений ще один тип робочих потоків – falcon  Також є в розпорядженні чотирипроцесорний компютер Отже, виконується чотири потоки типуevents (Відповідно, визначено чотири примірники структури cpu_workqueue_struct) і чотири потоки типуfalcon  (Для яких теж визначено інші чотири примірники структури cpu_workqueue_struct) Для потоків типуevents визначений один екземпляр структури workqueue_struct, а для потоків типу falcon — другий примірник цієї структури

На самому нижньому рівні знаходяться відкладені дії Драйвер створює відкладене дію, якої має виконатися пізніше Дії представлені структурами work_struct Крім інших полів, ця структура містить покажчик на функцію, яка має опрацювати відкладене дію Відкладене дію відправляється на виконанняпевномупотоку Відповідний потік переводиться в стан виконання та виконує відкладену роботу

Більшість драйверів використовує існуючі за замовчуванням робочі потоки, які називаються events Вони прості в реалізації і у використанні Проте в деяких, більш серйозних ситуаціях необхідно створювати нові спеціальні робочі потоки Наприклад, драйвер файлової системи XFS створює два нових типи робочих потоків

Використання черг відкладених дій

Використовувати черги дій просто Спочатку ми розглянемо робочі потоки, використовувані за замовчуванням, – events,  а потім опишемо створення нових типів робочих потоків

Створення відкладених дій

Перший етап – це створення самої дії, яке має бути відкладено Для створення статичної структури на етапі компіляції необхідно використовувати наступний макрос

DECLARE_WORK(name, void (*func) (void *), void *data)

Це вираз створює структуру works_truc t з імям name, з функціейобработчіком fun c і аргументом функції-обробника data

Під час виконання відкладене дію можна створити за допомогою передачі покажчика на структуру, використовуючи наступний макрос

INIT_WORK(struct work_struct *work, void (*func)(void *),void *data)

Цей макрос динамічно инициализирует відкладене дію, на структуру якого вказує покажчик work, встановлюючи функцію-обробник fun c і аргумент data

Оброблювач відкладеного дії

Прототип обробника відкладеного дії має наступний вигляд

void work_handler (void *data)

Робочий потік виконує цю функцію, і, отже, ця функція виконується в контексті процесу За замовчуванням при цьому вага переривання дозволені і ніякі захоплені блокування не утримуються Їли це необхідно, то функція може переходити в стан очікування Слід зауважити, що незважаючи на те, що обробники відкладених дій і виконуються в контексті процесу, ці обробники не можуть переходити в простір користувача, так як у потоків простору ядра немає адресного простору користувача Ядро може звертатися в простір користувача, тільки коли воно виконується від імені користувача процесу, який має адресний простір користувача, відображене на память, як, наприклад, у разі виконання системного виклику

Блокування між чергами відкладених дій та іншими частинами ядра здійснюються також, як і у випадку будь-якого іншого коду, що працює в контексті процесу Це дозволяє зробити написання обробників відкладених дій досить простим У наступних двох розділах це розкривається більш детально

Планування дій на виконання

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

г

schedule_work(&ampwork)

Дія планується на виконання негайно і буде виконано, як тільки робочий потік events, що працює на даному процесорі, перейде в стан виконання

Іноді необхідно, щоб дія була виконана не негайно, а з деякою затримкою У цьому випадку робота може бути запланована на виконання в деякий момент часу в майбутньому Для цього використовується наступна функція

schedule_delayed_work (&ampwork, delay)

У цьому випадку дія, представлене структурою work_struct, з адресою

& Work, не буде виконано, поки не пройде хоча б заданий в параметрі dela y кількість імпульсів таймера Про те, як використовувати імпульси таймера для вимірювання часу, розповідається в чолі 10, Таймери та управління часом.

Очікування завершення дій

Дії, поставлені в чергу, виконуються, коли робочий потік повертається до виконання Іноді потрібно гарантувати, що, перед тим як рухатися далі, заданий пакет відкладених дій завершений Це особливо важливо для завантажуваних модулів, які, ймовірно, повинні викликати цю функцію, перед вивантаженням В інших місцях також може бути необхідно гарантувати, що немає очікують на виконання дій, для запобігання стану конкуренції

Для цього є наступна функція, яка дозволяє чекати, поки черга дій events не очистите

void  flush_scheduled_work(void)

Дана функція очікує, поки всі дії в черзі дій events не будуть виконані В очікуванні завершення всіх завдань черзі, ця функція переводить викликає процес в стан очікування Тому її можна викликати тільки з контексту процесу

Зауважимо, що ця функція не скасовує ніяких відкладених дій із затримками Будь-які дії, які заплановані на виконання за допомогою функції schedule_delayed_wor k () і затримки яких ще не закінчені – не очищаються за допомогою функцій flush_scheduled_wor k () Для скасування відкладених дій із затримками слід використовувати функцію

int cancel_delayed_work(struct work_struct *work)

Ця функція скасовує відкладене дія, яка повязана з даною структурою work_struct, якщо воно заплановано

Створення нових черг відкладених дій

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

Нова черга дій і повязані з нею робочі потоки створюються за допомогою простого виклику функції

struct workqueue_struct *create_workqueue(const char *name)

Параметр name використовується для того, щоб присвоювати імена потокам ядра Наприклад, чергу events, яка використовується за замовчуванням, створюється за допомогою наступного виклику

structworkqueue_struct*keventd_wq=create_workqueue(&quotevents&quot)

При цьому також створюються всі робочі потоки (по одному на кожен процесор), які підготовляються до виконання роботи

Створення відкладених дій виконується однаково, незалежно від тина черги Після того як дії створені, можуть бути використані функції, аналогічні функціям schedule_work () і schedule_delayed_work (), які відрізняються тим, що працюють із заданою чергою дій, а не з чергою, яка використовується за замовчуванням

int queue_work struct workqueue_struct *wq, struct work_struct *work)

intqueue_delayed_work(structworkqueue_struct*wq,

struct wesrk_struct *work, unsigned long delay)

І нарешті, очікування завершення дій в заданій черзі може бути виконано за допомогою функції

flush_workqueue(struct workqueue_struct *wq)

Ця функція працює за аналогією з функцією flush_scheduled_wor k (), як описувалося раніше, за винятком того, що вона очікує, поки задана чергу не стане порожньою

Старий механізм черг завдань

Так само як і у випадку інтерфейсу ВН, який дав початок інтерфейсам відкладених переривань (softirq) і тасклетов (tasklet), інтерфейс черг дій виник завдяки недолікам інтерфейсу черг завдань (task queue) Інтерфейс черг завдань (який ще називають простоtq), так само як і тасклети, не має нічого спільного з завданнями (task), в сенсі з процессамі8 Усі підсистеми, які використовували механізм черг завдань, були розбиті на дві групи ще в часи розробки серії ядер 25 Перша група була переведена на використання тасклетов, а друга-продовжувала використовувати інтерфейс черг завдань Все, що залишилося від інтерфейсу черг завдань, перейшло в інтерфейс черг відкладених дій Короткий розгляд черг завдань, яким користувалися протягом деякого часу, – це хороша вправа з історії

Інтерфейс черг завдань дозволяв визначати набір черг Черги мали імена, такі як scheduler queue (черга планувальника), immediate queue (негайна чергу) або timer queue (черга таймера) Кожна чергу виконувалася в певних місцях в ядрі Потік простору ядра keventd виконував роботу, повязану з чергою планувальника Ця черга була попередником інтерфейсу черг відкладених дій Черга таймера виконувалася при кожному імпульсі системного таймера, а негайна чергу виконувалася в декількох місцях, щоб гарантувати негайне виконання Були також і інші черги Крім того, можна було динамічно створювати нові черги

Все це може здатися корисним, але на практиці інтерфейс черг завдань приносив тільки неприємності Всі черги були, по суті, відірвані від дійсності Єдиною цінною чергою виявилася чергу планувальника, яка надавала єдину можливість, щоб виконувати відкладені дії в контексті процесу

Ще однією перевагою механізму черг завдань була простота інтерфейсу Незважаючи на велику кількість черг і різноманітність правил, за якими вони виконувалися, інтерфейс був максимально простий Все інше, що стосується черг завдань, необхідно було прибрати

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

Джерело: Лав, Роберт Розробка ядра Linux, 2-е видання : Пер з англ – М: ТОВ «ІД Вільямс »2006 – 448 с : Ил – Парал тит англ

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*