Принципи роботи в UNIX-подібних ОС з прикладу Linux. Як Linux створює легковажні процеси

Один з найбільш дратівливих моментів при переході від середовища на базі Windowsдо використання командного рядка- Втрата легкої багатозадачності. Навіть у Linux, якщо ви використовуєте X Window system, ви можете використовувати мишу, щоб просто натиснути на нову програмута відкрити її. У командному рядку, однак, ви значно застрягли з монозадачею. У цій статті ми покажемо вам як перейти до багатозадачності в Linux за допомогою командного рядка.

Фонове та пріоритетне управління процесами

Тим не менш, у Linux все ще є способи багатозадачності, і деякі з них є всеосяжними, ніж інші. Один вбудований спосіб, який не вимагає будь-якого додаткового програмного забезпечення, – це просто переміщення процесів у фоновому режимі та на передньому плані. Ми про це. Однак вона має деякі недоліки.

Незахищений

По першеЩоб надіслати процес у фоновому режимі, спочатку його потрібно призупинити. Неможливо відправити вже запущену програмуу фоновому режимі та підтримувати її в один час.

По-другеВам потрібно розбити робочий процес, щоб почати нову команду. Ви повинні вийти з того, що ви зараз робите, і набрати більше команд в оболонку. Це працює, але це незручно.

По-третєВи повинні стежити за висновками з фонових процесів. Будь-який висновок з них з'явиться в командному рядку і заважатиме тому, що ви робите зараз. Таким чином, фонові завдання повинні або перенаправити свій висновок у окремий файл, або потрібно повністю відключити.

Через ці недоліки виникають величезні проблеми з управлінням процесом фону та переднього плану. Краще рішення– використовувати утиліту командного рядка «screen», як показано нижче.

Але спочатку ви відкриєте новий сеанс SSH

Не забувайте, що ви просто відкриваєте новий сеанс SSH.

Це може бути незручно відкрити нові сесії весь час. І саме тоді вам потрібний “screen”

Утиліта screenдозволяє створювати кілька робочих процесів, відкритих у той самий час – найближчий аналог “windows”. За замовчуванням він доступний у звичайних репозиторіях Linux. Встановіть його в CentOS/RHEL за допомогою наступної команди:

Sudo yum install screen

Відкриття нового екрану

Тепер почніть сеанс, набравши "screen".

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

Наш екран має номер “0”, як показано. На цьому скріншоті ми використовуємо фіктивну команду “read”, щоб заблокувати термінал та змусити його чекати на введення. Тепер скажемо, ми хочемо зробити щось ще, доки ми чекаємо.

Щоб відкрити новий екран і зробити щось інше, ми друкуємо:

Ctrl+a c

“ctrl+a” є комбінацією клавіш за промовчанням для керування екранами у програмі екрана. Те, що ви вводите після нього, визначає дію. Так наприклад:

  • ctrl+a c – Cактивує новий екран
  • ctrl+a [число]– перехід до певного номера екрану
  • ctrl+a k – Kвідключає поточний екран
  • ctrl+a n – Перехід до екрану n
  • ctrl+a “ – відображає всі активні екрани у сеансі

Якщо натиснути “ctrl+a c”, ми отримаємо новий екран із новим номером.

Ви можете використовувати клавіші курсору для навігації за списком та перейти до екрана, який ви хочете.
Екрани – це найближче, що ви отримаєте до “windows”, як система у командному рядку Linux. Звичайно, це не так просто, як клацання мишею, але графічна підсистема дуже ресурсомістка. З екранами ви можете отримати майже таку ж функціональність і включити повну багатозадачність!

ЛАБОРАТОРНА РОБОТА №3

БАГАТОЗАДАЧНЕ ПРОГРАМУВАННЯ ВLINUX

1. Мета роботи:Ознайомитись з компілятором gcc, методикою налагодження програм, функціями роботи з процесами.

2. Короткі теоретичні відомості.

Мінімальним набором ключів компілятора gcc є - Wall (виводити всі помилки та попередження) та - o (output file):

gcc - Wall - print_pid print_pid. c

Команда створить виконуваний файл print_pid.

Стандартна бібліотека C (libc, реалізована в Linux glibc), використовує можливості багатозадачності Unix System V (далі SysV). У libc тип pid_t визначений як ціле, здатне вмістити у собі pid. Функція, яка повідомляє pid поточного процесу, має прототип pid_t getpid (void) і визначена разом з pid_t unistd. h та sys/types. h).

Для створення нового процесу використовується функція fork:

pid_t fork(void)

Вставляючи затримку випадкової довжини за допомогою функцій sleep та rand, можна наочніше побачити ефект багатозадачності:

це змусить програму заснути на випадкове число секунд: від 0 до 3.

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

// якщо виконується дочірній процес, то викличемо функцію

pid = proces (arg);

// вихід із процесу

Часто як дочірній процес необхідно запускати іншу програму. Для цього застосовується функція сімейства exec:

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


if (execl("./file","file",arg, NULL)<0) {

printf("ERROR while start process\n");

else printf("process started (pid=%d)\n", pid);

// вихід із процесу

Часто батьківському процесу необхідно обмінюватися інформацією з дочірніми або хоча б синхронізуватися з ними, щоб виконувати операції у потрібний час. Один із способів синхронізації процесів - функції wait і waitpid:

#include

#include

pid_t wait(int *status) - зупиняє виконання поточного процесу до завершення якогось із його процесів-нащадків.

pid_t waitpid (pid_t pid, int *status, int options) - зупиняє виконання поточного процесу до завершення заданого процесу або перевіряє завершення заданого процесу.

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

status = waitpid (pid, status, WNOHANG);

if (pid == status) (

printf("PID: %d, Result = %d\n", pid, WEXITSTATUS(status)); )

Для зміни пріоритетів породжених процесів використовуються функції setpriority і. Пріоритети задаються в діапазоні від -20 (вищий) до 20 (нижчий), нормальне значення - 0. Зауважимо, що підвищити пріоритет вище за нормальний може тільки суперкористувач!

#include

#include

int process(int i) (

setpriority(PRIO_PROCESS, getpid(),i);

printf("Process %d ThreadID: %d working with priority %d\n",i, getpid(),getpriority(PRIO_PROCESS, getpid()));

return(getpriority(PRIO_PROCESS, getpid()));

Для знищення процесу служить функція kill:

#include

#include

int kill (pid_t pid, int sig);

Якщо pid > 0, він задає PID процесу, якому посилається сигнал. Якщо pid = 0, сигнал посилається всім процесам тієї групи, до якої належить поточний процес.

sig – тип сигналу. Деякі типи сигналів у Linux:

SIGKILL Цей сигнал призводить до негайного завершення процесу. Цей процес сигнал не може ігнорувати.

SIGTERM Цей сигнал є запитом на завершення процесу.

SIGCHLD Система надсилає цей сигнал процесу при завершенні одного з його дочірніх процесів. Приклад:

if (pid[i] == status) (

printf("ThreadID: %d finished with status %d\n", pid[i], WEXITSTATUS(status));

else kill (pid [i], SIGKILL);

3. Методичні вказівки.

3.1. Для ознайомлення з опціями компілятора gcc, описом функцій мови C використовуйте вказівки man та info.

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

3.3. У файловому менеджері Midnight Commander є буфер команд, що викликається поєднанням клавіш - H, переміщення яким здійснюється стрілками управління курсором (вгору і вниз). Для вставки команди з буфера в командний рядок використовується клавіша для редагування команди з буфера - клавіші<- и ->, і .


3.4. Пам'ятайте, що поточна директорія не міститься в path, тому з командного рядка потрібно запускати програму як "./print_pid". У MC достатньо навести курсор на файл та натиснути .

3.5. Щоб переглянути результат виконання програми, використовуйте клавіші - O. Вони працюють і в режимі редагування файлу.

3.6. Для протоколювання результатів виконання програм доцільно використовувати перенаправлення виведення з консолі файл: ./test > result. txt

3.7. Для доступу до файлів, створених на сервері Linux, використовуйте протокол ftp, клієнтська програмаякого є в Windows 2000 і вбудована в файловий менеджер FAR. При цьому обліковий записі пароль ті ж, що і при підключенні протоколу ssh.

4.1. Ознайомитись з опціями компілятора gcc, методикою налагодження програм.

4.2. Для варіантів завдань із лабораторної роботи №1 написати та налагодити програму, що реалізує породжений процес.

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

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

5. Варіанти завдань.варіанти завдань з лабораторної роботи №1

6. Зміст звіту.

6.1. Мета роботи.

6.2. Варіант завдання.

6.3. Лістинги програм.

6.4. Протоколи виконання програм.

7. Контрольні питання.

7.1. Особливості компіляції та запуску С-програм у Linux.

7.2. Що таке pid, як його визначити в операційній системі та програмі?

7.3. Функція fork - призначення, застосування, значення, що повертається.

7.4. Як запустити виконання в породженому процесі функцію? Програму?

7.5. Способи синхронізації батьківського та дочірніх процесів.

7.6. Як дізнатися стан породженого процесу при його завершенні та повернене їм значення?

7.7. Як керувати пріоритетами процесів?

7.8. Як знищити процес в операційній системі та програмі?

Linux- багатозадачна та розрахована на багато користувачів операційна система для освіти, бізнесу, індивідуального програмування. Linux належить до сімейства UNIX-подібних операційних систем.

Linux спочатку був написаний Лінусом Торвальдсом, а потім покращувався незліченною кількістю народу у всьому світі. Він є клоном операційної системи Unix, однією з перших потужних операційних систем, що розробляються для комп'ютерів, але не є безкоштовною. Але ні Unix System Laboratories, творці Unix, ні Університет Берклі, розробники Berkeley Software Distribution (BSD), не брали участь у його створенні. Один з найбільш цікавих фактівз історії Linux"а - це те, що в його створенні брали участь одночасно люди з усіх куточків світу - від Австралії до Фінляндії - і продовжують це робити досі.

Спочатку Linux розроблявся до роботи на 386 процесорі. Одним із перших проектів Лінуса Торвальдса була програма, яка могла перемикатися між процесами, один із яких друкував АААА, а інший – ВВВВ. Згодом ця програма зросла у Linux. Правильніше, правда буде сказати, що Лінус розробив ядро ​​ОС, і за його стабільність він відповідає.

Linux підтримує більшу частину популярного Unix"івського програмного забезпечення, включаючи графічну систему X Window, - а це величезна кількість програм, але варто підкреслити, що Linux поставляється АБСОЛЮТНО БЕЗКОШТОВНО. Максимум, за що доводиться платити, так це за упаковку та CD, на яких записаний дистрибутив Linux.Дистрибутив - це сама ОС + набір пакетів програм для Linux.Стодіє також згадати, що все це поставляється з вихідними текстами, і будь-яку програму, написану під Linux, можна переробити під себе.Це ж дозволяє перенести будь-яку програму на будь-яку платформу - Intel PC, Macintosh До речі, все вищеописане вийшло завдяки Free Software Foundation, фонду безкоштовних програм, що є частиною проекту GNU. І саме для цих цілей була створена GPL – General Public License, виходячи з якої Linux – безкоштовний, як і весь софт під нього, причому комерційне використанняпрограмне забезпечення для Linux або його шматків заборонено. конфігурація система unix linux

Крім вищеописаного, Linux - дуже потужна та стабільна ОС. Використання його в Мережі виправдовує себе, та й зламати його не так вже й легко.

На сьогоднішній день розвиток Linux йде по двох гілках. Перша з парними номерами версій (2.0, 2.2, 2.4) вважається більш стабільною, надійною версією Linux. Друга, чиї версії нумеруються непарними номерами (2.1, 2.3), є зухвалішою і швидше розвивається і, отже (на жаль), багатшою помилками. Але це вже справа смаку.

У Linux немає поділу на диски С,D, та процес спілкування з пристроями дуже зручний. Усі пристрої мають власний системний файл, всі диски підключаються до однієї файловій системіі виглядає все це як би монолітно, єдино. Чітка структура каталогів дозволяє знаходити будь-яку інформацію миттєво. Для файлів бібліотек - свій каталог, для файлів - свій, для файлів з налаштуваннями - свій, для файлів пристроїв - свій, і так далі.

Модульність ядра дозволяє підключати будь-які послуги ОС без перезавантаження комп'ютера. Крім того, ви можете переробити саме ядро ​​ОС, добре вихідні текстиядра також є у будь-якому дистрибутиві.

У ОС Linux дуже вміло, якщо можна висловитися, використовується ідея багатозадачності, тобто. будь-які процеси в системі виконуються одночасно (порівняйте з Windows: копіювання файлів на дискету і спроба слухати музику не завжди сумісні).

Але не все так просто. Linux трохи складніший, ніж Windows, і не всім так просто перейти на нього після використання вікон. На перший погляд, може навіть здатися, що він дуже незручний і важко налаштовується. Але це не так. Вся особливість Linux"a в тому, що його можна налаштувати під себе, налаштувати так, що від користування цієї ОС ви відчуватимете величезне задоволення. Величезна кількість налаштувань дозволяє змінити зовнішній (та й внутрішній) вигляд ОС, причому жодна Linux-система не буде схожа на вашу.У Linux у вас є вибір у використанні графічної оболонки, є кілька офісних пакетів, програми-сервери, фаєрволи… Просто ціла купа різноманітних програм на будь-який смак.

У 1998 Linux була операційною системою для серверів, що швидко розвивається, поширення якої збільшилося в тому ж році на 212%. Сьогодні користувачів Linux налічується понад 20 000 000. Під Linux існує безліч додатків, призначених як домашнього використання, так повністю функціональних робочих станцій UNIX і серверів Internet.

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

  • * безкоштовний (вірніше, вільно поширюваний) клон Юнікс;
  • * Операційна система з істинною багатозадачністю;
  • * ОС, яку кожен її "користувач" може модифікувати, тому що можна знайти вихідні кодипрактично для будь-якої складової її частини;
  • * яка налаштовується саме так, як вам хочеться, а не як воліє виробник.

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

В результаті таких особливостей свого створення та розвитку Linux набув дуже специфічних "рис характеру". З одного боку, це типова UNIX-система, розрахована на багато користувачів і багатозадачна. З іншого боку - типова система хакерів, студентів і взагалі будь-яких людей, яким подобається безперервно вчитися і розбиратися в усьому до найдрібніших подробиць. У гнучкості налаштування та застосування Linux, напевно, просто немає рівних. Ви можете користуватися нею на рівні, на якому працює win95, - тобто мати графічний десктоп з усіма ознаками під Windows: значками, панеллю завдань, контекстним меню, і т. д. Мало того - ви можете встановити десктоп, який взагалі не відрізнятиметься за зовнішньому виглядута функцій від "Windows". (Взагалі кажучи, варіантів віконних менеджерів під Linux просто неміряно, від суперспартанського icewm до супернавороченого Enlightment + Gnome). З іншого боку, Linux дає вам безпрецедентні можливості наближення до заліза на будь-якому рівні доступності. Щоправда, для цього вже мало вміти плескати правою кнопкою миші, доведеться вивчити СІ та архітектуру комп'ютера. Але людина, якось відчув цей запах думки, це натхнення програміста, коли ти тримаєш машину "за вуха" і можеш зробити з нею буквально все, на що вона здатна - така людина вже ніколи не зможе повернутися в м'які лапи "віндози".

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

Відповідей на запитання "А що таке Linux?" можна знайти безліч. Дуже багато хто вважає, що Linux - це тільки ядро. Але одне тільки ядро ​​марне для користувача. Хоча ядро, безперечно, є основою ОС Linux, користувачеві весь час доводиться працювати з прикладними програмами. Ці програми є не менш важливими, ніж ядро. Тому Linux - це сукупність ядра та основних прикладних програм, які зазвичай бувають встановлені кожному комп'ютері з цією операційною системою. Об'єднання ядра та прикладних програм у єдине ціле проявляється і в назві системи: GNU/Linux. GNU - це проект створення комплексу програм, подібного до того, що зазвичай супроводжує Unix-подібну систему.

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

Продовжуємо тему багатопоточності в ядрі Linux. Минулого разу я розповідала про переривання, їх обробку та tasklet'и, і оскільки спочатку передбачалося, що це буде одна стаття, у своїй розповіді про workqueue я посилатимуся на tasklet'и, вважаючи, що читач уже з ними знайомий.
Як і минулого разу, я постараюся зробити мою розповідь максимально докладною та детальною.

Статті циклу:

  1. Багатозадачність у ядрі Linux: workqueue

Workqueue

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

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

У цій темній справі замішано кілька сутностей.
По перше, work item(для стислості просто work) - це структура, що описує функцію (наприклад, обробник переривання), яку ми хочемо запланувати. Його можна сприймати як аналог структури tasklet. Tasklet'и при плануванні додавалися в черги, приховані від користувача, тепер нам потрібно використовувати спеціальну чергу - workqueue.
Tasklet'и розгрібаються функцією-планувальником, а workqueue обробляється спеціальними потоками, які звуться worker'ами.
Worker'и забезпечують асинхронне виконання work'ів з workqueue. Хоча вони викликають work'і в порядку черги, загалом про суворе, послідовне виконання не йдеться: все-таки тут мають місце витіснення, сон, очікування тощо.

Взагалі, worker'и – це kernel-потоки, тобто ними керує основний планувальник ядра Linux. Але worker'и частково втручаються у планування для додаткової організації паралельного виконання work'ів. Про це детальніше піде нижче.

Щоб окреслити основні можливості механізму роботи, я пропоную вивчити API.

Про чергу та її створення

alloc_workqueue(fmt, flags, max_active, args...)
Параметри fmt та args - це printf-формат для імені та аргументи до нього. Параметр max_activate відповідає за максимальну кількість work'ів, які з цієї черги можуть виконуватись паралельно на одному CPU.
Чергу можна створити з такими прапорами:
  • WQ_HIGHPRI
  • WQ_UNBOUND
  • WQ_CPU_INTENSIVE
  • WQ_FREEZABLE
  • WQ_MEM_RECLAIM
Особливу увагу слід приділити прапору WQ_UNBOUND. За наявності цього прапора черги діляться на прив'язані та неприв'язані.
У прив'язаних чергах work'і при додаванні прив'язуються до поточного CPU, тобто в таких чергах work'і виконуються на тому ядрі, яке його планує. У цьому плані прив'язані черги нагадують tasklet'и.
У неприв'язаних чергах work’и ​​можуть виконуватися на будь-якому ядрі.

Важливою властивістю реалізації workqueue в ядрі Linux є додаткова організація паралельного виконання, яка є у прив'язаних черг. Про неї докладніше написано нижче, зараз скажу, що здійснюється таким чином, щоб використовувалося якнайменше пам'яті, і щоб при цьому процесор не простоював. Реалізовано це все з припущенням, що одна робота не використовує занадто багато тактів процесора.
Для неприв'язаних черг такого немає. По суті такі черги просто надають work'ам контекст і запускають їх якомога раніше.
Таким чином, неприв'язані черги слід використовувати, якщо очікується інтенсивне навантаження на процесор, тому що в такому випадку планувальник потурбується про паралельне виконання на кількох ядрах.

За аналогією з tasklet'ами, work'ам можна надавати пріоритет виконання, нормальний чи високий. Пріоритет загальний всю чергу. За умовчанням черга має нормальний пріоритет, а якщо задати прапор WQ_HIGHPRI, То, відповідно, високий.

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

Прапори WQ_FREEZABLEі WQ_MEM_RECLAIMспецифічні і виходять за межі теми, тому докладно на них зупинятися не будемо.

Іноді є сенс не створювати власні черги, а використовувати спільні. Основні з них:

  • system_wq - прив'язана черга для швидких work'ів
  • system_long_wq - прив'язана черга для work'ів, які, ймовірно, будуть виконуватися довго
  • system_unbound_wq - неприв'язана черга

Про work'и та їх планування

Тепер розберемося із work'ами. Спочатку поглянемо на макроси ініціалізації, декларації та підготовки:
DECLARE(_DELAYED)_WORK(name, void (function)(struct work_struct *work)); /* на етапі компіляції */ INIT(_DELAYED)_WORK(_work, _func); /* під час виконання */ PREPARE(_DELAYED)_WORK(_work, _func); /* для зміни виконуваної функції */
У черзі work'і додаються за допомогою функцій:
bool queue_work (struct workqueue_struct * wq, struct work_struct * work); bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay); /* work буде додано в чергу тільки після закінчення delay */
Ось на цьому варто зупинитися докладніше. Хоча ми як параметр вказуємо чергу, насправді, work'і кладуться не в самі workqueue, як це може здатися, а в зовсім іншу сутність - у список-черг структури worker_pool. Структура worker_pool, по суті, найголовніша сутність в організації механізму праці, хоча для користувача вона залишається за лаштунками. Саме з ними працюють worker'и, і саме в них міститься вся основна інформація.

Тепер побачимо, які пули є в системі.
Для початку пули для прив'язаних черг (на малюнку). Для кожного CPU статично виділяються два worker pool: один для високопріоритетних work'ів, інший - для work'ів із нормальним пріоритетом. Тобто, якщо ядра у нас чотири, то прив'язаних пулів буде всього вісім, не дивлячись на те, що workqueue може бути скільки завгодно.
Коли ми створюємо workqueue, у нього для кожного CPU виділяється службовий pool_workqueue(Pwq). Кожен такий pool_workqueue асоційований з worker pool, який виділено на тому ж CPU і відповідає за пріоритетом типу черги. Через них workqueue взаємодіє з worker pool.
Worker'и виконують work'и з worker pool без розбору, не розрізняючи, до якого workqueue вони належали спочатку.

Для неприв'язаних черг worker pool'и виділяються динамічно. Усі черги можна розбити на класи еквівалентності за їх параметрами, і для кожного такого класу створюється свій worker pool. Доступ до них здійснюється за допомогою спеціальної хеш-таблиці, де ключем служить набір параметрів, а значенням відповідно worker pool.
Насправді у неприв'язаних черг все трохи складніше: якщо у прив'язаних черг створювалися pwq та черги для кожного CPU, то тут вони створюються для кожного вузла NUMA, але це вже додаткова оптимізація, яку в деталях не розглядатимемо.

Будь-які дрібниці

Ще наведу кілька функцій з API для повноти картини, але докладно про них не говоритиму:
/* Примусове завершення */ bool flush_work(struct work_struct *work); bool flush_delayed_work(struct delayed_work *dwork); /* Скасувати виконання work */ bool cancel_work_sync(struct work_struct *work); bool cancel_delayed_work(struct delayed_work *dwork); bool cancel_delayed_work_sync(struct delayed_work *dwork); /* Видалити чергу */ void destroy_workqueue(struct workqueue_struct *wq);

Як worker'и справляються зі своєю роботою

Тепер, як ми познайомилися з API, давайте спробуємо докладніше розібратися, як це все працює та керується.
Кожен пул має набір worker'ів, які розгрібають завдання. Причому кількість worker'ів змінюється динамічно, підлаштовуючись під поточну ситуацію.
Як ми вже з'ясували, worker'и – це потоки, які в контексті ядра виконують work'и. Worker дістає їх по порядку один за одним із асоційованого з ним worker pool, причому work'і, як ми вже знаємо, можуть належати до різних вихідних черг.

Worker'и умовно можуть перебувати у трьох логічних станах: вони можуть бути простоювальними, запущеними або керуючими.
Worker може простоюватиі нічого не робити. Це, наприклад, коли всі work'і вже виконуються. Коли worker переходить у цей стан, він засинає і, відповідно, не буде виконуватися доти, доки його не розбудять;
Якщо не потрібно керувати пулом і список запланованих work'ів не порожній, worker починає виконувати їх. Такі worker'и умовно називатимемо занедбаними.
Якщо ж потрібно, worker бере на себе роль управителяпулом. У пула може бути або тільки один керуючий worker, або не бути взагалі. Його завдання – підтримувати оптимальну кількість worker'ів на пул. Як це він робить? По-перше, видаляються worker'и, які досить довго простоюють. По-друге, створюються нові worker'и, якщо виконуються одразу три умови:

  • ще є завдання на виконання (work'і в пулі)
  • немає простоюючих worker'ів
  • немає працюючих worker'ів (тобто активних і при цьому не сплячих)
Проте в останній умові є свої нюанси. Якщо черги пулу неприв'язані, то облік запущених worker'ів не здійснюється, для них ця умова завжди є істинною. Те саме справедливо і у разі виконання worker'ом завдання з прив'язаної, але з прапором WQ_CPU_INTENSIVE, черги. При цьому, у разі прив'язаних черг, оскільки worker'и працюють із work'ами із загального пулу (який один із двох на кожне ядро ​​на зображенні вище), то виходить, що деякі з них враховуються як працюючі, а деякі - ні. З цього ж випливає, що виконання work'ів з WQ_CPU_INTENSIVEчерги може розпочатися не відразу, зате самі вони не заважають виконуватись іншим work'ам. Тепер має бути зрозуміло, чому це прапор так називається, і чому він використовується, коли ми очікуємо, що work’і будуть виконуватися довго.

Облік працюючих worker'ів здійснюється безпосередньо з основного планувальника ядра Linux. Такий механізм управління забезпечує оптимальний рівень паралельності (concurrency level), не даючи workqueue створювати занадто багато worker'ів, але й не змушуючи work'і без потреби чекати надто довго.

Ті, кому цікаво, можуть переглянути функцію worker'а в ядрі, називається вона worker_thread().

З усіма описаними функціями та структурами можна докладніше ознайомитись у файлах include/linux/workqueue.h, kernel/workqueue.cі kernel/workqueue_internal.h. Також по workqueue є документація у Documentation/workqueue.txt.

Ще варто відзначити, що механізм праці використовується в ядрі не тільки для відкладеної обробки переривань (хоча це досить частий сценарій).

Таким чином, ми розглянули механізми відкладеної обробки переривань в ядрі Linux - tasklet і workqueue, які є особливою формою багатозадачності. Про переривання, tasklet'и та workqueue можна почитати у книзі "Linux Device Drivers" авторів Jonathan Corbet, Greg Kroah-Hartman, Alessandro Rubini, щоправда, інформація там часом застаріла.

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

У сімействах ОС Windows - кожна програма запускає один процес виконання, в якому знаходиться щонайменше один потік (нитка). У процесі може бути безліч потоків, між якими ділиться процесорний час. Один процес не може безпосередньо звернутися до пам'яті іншого процесу, а потоки розділяють один адресний простір одного процесу. Тобто у Windows – процес це сукупність потоків.

У Linux трохи по-іншому. Сутність процесу така ж, як і в Windows - це програма, що виконується зі своїми даними. Але потік в Linux є окремим процесом (можна зустріти назву як «легковажний процес», LWP). Відмінність така ж - процес окрема програма зі своєю пам'яттю, не може безпосередньо звернутися до пам'яті іншого процесу, а ось потік, хоч і окремий процес, має доступ до пам'яті процесу-батька. LWP процеси створюються за допомогою системного виклику clone() із зазначенням певних прапорів.

Але також є така річ, яка називається POSIX Threads - бібліотечка стандарту POSIX, яка організує потоки (вони ж нитки) всередині процесу. Тобто тут уже розпаралелювання відбувається в рамках одного процесу.

І тут виникає питання відмінності термінів «потік», «процес», «нитка» тощо. Проблема в тому, що в англомовній літературі дані терміни визначаються однозначно, а в нас із нашим великим і могутнім є протиріччя, що може призвести до дикого дисонансу.

Але це все загалом, для більш точної інформації слід звернутися до відповідної літератури, або офіційної документації, можна почитати man’и. Наприкінці статті я наведу кілька корисних посиланьна ресурси, де детальніше розписано як усе працює, а поки займемося практикою.

Я розгляну два варіанти «розпаралелювання» програми — створення потоку/нитки за допомогою функцій з pthread.h (POSIX Threads) або створення окремого процесу за допомогою функції fork().

Сьогодні розглянемо потоки з бібліотеки pthread.

Шаблон коду для роботи з потоками виглядає так:

#include //потокова функція void* threadFunc(void* thread_data)( //завершуємо потік pthread_exit(0); ) int main()( //які дані для потоку (для прикладу) void* thread_data = NULL; //створюємо ідентифікатор потоку //створюємо потік по ідентифікатору thread і функції потоку threadFunc //і передаємо потоку покажчик на дані thread_data pthread_create(&thread, NULL, threadFunc, thread_data); // чекаємо завершення потоку pthread_nn;

#include

//Потокова функція

//завершуємо потік

pthread_exit(0);

int main () (

//які дані для потоку (для прикладу)

void * thread_data = NULL;

//створюємо ідентифікатор потоку

pthread_t thread;

//створюємо потік по ідентифікатору thread та функції потоку threadFunc

//і передаємо потоку покажчик на дані thread_data

pthread_create (& thread, NULL, threadFunc, thread_data);

//Чекаємо завершення потоку

pthread_join (thread, NULL);

return 0;

Як видно з коду, сутність потоку втілена у функції, даному випадку, threadFunc. Ім'я такої функції може бути довільним, а ось тип і тип вхідного аргументу, що повертається, повинні бути строго void*. Ця функціябуде виконуватися в окремому потоці виконання, тому необхідно з особливою обережністю підходити до реалізації цієї функції через доступ до однієї і тієї ж пам'яті батьківського процесу багатьма потоками. Завершення досягається кількома варіантами: потік досяг точки завершення (return, pthread_exit(0)), або потік було завершено ззовні.

Створення потоку відбувається за допомогою функції pthread_create(pthread_t *tid, const pthread_attr_t *attr, void*(*function)(void*), void* arg), де: tid - ідентифікатор потоку, attr - параметри потоку (NULL - стандартні атрибути , подробиці в man), function - покажчик на потокову функцію, в нашому випадку threadFunc і arg - покажчик на дані, що передаються в потік.

Функція pthread_join очікує завершення потоку thread. Другий параметр цієї функції – результат, що повертається потоком.

Спробуємо за цим шаблоном написати програму, яка виконує щось корисне. Наприклад, спробуємо скласти дві матриці та зберегти результат у третій результуючій матриці. Для вирішення цього завдання вже необхідно подумати про правильне розподілення даних між потоками. Я реалізував простий алгоритм – скільки рядків у матриці, стільки потоків. Кожен потік складає елементи рядка першої матриці з елементами рядка другої матриці та зберігає результат у рядок третьої матриці. Виходить, кожен потік працює рівно зі своїми даними і таким чином виключається доступ одного потоку до даних іншого потоку. Приклад програми з використанням потоків pthread виглядають так:

#include #include #include #include //розміри матриць #define N 5 #define M 5 //спеціальна структура для даних потоку typedef struct( int rowN; //номер рядка, що обробляється int rowSize; //розмір рядка //вказівники на матриці int** array1; int** array2;int** resArr;) pthrData; void* threadFunc(void* thread_data)( //отримуємо структуру з даними pthrData *data = (pthrData*) thread_data; //складаємо елементи рядків матриць і зберігаємо результат for(int i = 0; i< data->rowSize; i++) data->resArr[i] = data->array1[i] + data->array2[i]; return NULL; ) int main()( //виділяємо пам'ять під двовимірні масиви int** matrix1 = (int**) malloc(N * sizeof(int*)); int** matrix2 = (int**) malloc(N * sizeof(int*)); int** resultMatrix = (int**) malloc(N * sizeof(int*)); //Виділяємо пам'ять під елементи матриць for(int i = 0; i< M; i++){ matrix1[i] = (int*) malloc(M * sizeof(int)); matrix2[i] = (int*) malloc(M * sizeof(int)); resultMatrix[i] = (int*) malloc(M * sizeof(int)); } //инициализируем начальными значениями for(int i = 0; i < N; i++){ for(int j = 0; j < M; j++){ matrix1[i][j] = i; matrix2[i][j] = j; resultMatrix[i][j] = 0; } } //выделяем память под массив идентификаторов потоков pthread_t* threads = (pthread_t*) malloc(N * sizeof(pthread_t)); //сколько потоков - столько и структур с потоковых данных pthrData* threadData = (pthrData*) malloc(N * sizeof(pthrData)); //инициализируем структуры потоков for(int i = 0; i < N; i++){ threadData[i].rowN = i; threadData[i].rowSize = M; threadData[i].array1 = matrix1; threadData[i].array2 = matrix2; threadData[i].resArr = resultMatrix; //запускаем поток pthread_create(&(threads[i]), NULL, threadFunc, &threadData[i]); } //ожидаем выполнение всех потоков for(int i = 0; i < N; i++) pthread_join(threads[i], NULL); //освобождаем память free(threads); free(threadData); for(int i = 0; i < N; i++){ free(matrix1[i]); free(matrix2[i]); free(resultMatrix[i]); } free(matrix1); free(matrix2); free(resultMatrix); return 0; }

#include

#include

#include

#include

//Розміри матриць

#define N 5

#define M 5

//спеціальна структура даних потоку

typedef struct (

int rowN; //Номер оброблюваного рядка

int rowSize; //Розмір рядка

//Покажчики на матриці

int * * array1;

int * * array2;

int * * resArr;

) pthrData;

void * threadFunc (void * thread_data ) (

//отримуємо структуру з даними

pthrData * data = (pthrData *) thread_data;

//складаємо елементи рядків матриць та зберігаємо результат

for (int i = 0; i< data ->rowSize; i ++ )

data -> resArr [data -> rowN] [i] = data -> array1 [data -> rowN] [i] + data -> array2 [data -> rowN] [i];

return NULL;

int main () (

//Виділяємо пам'ять під двомірні масиви

int * * matrix1 = (int * * ) malloc (N * sizeof (int * ));

int * * matrix2 = (int * * ) malloc (N * sizeof (int * ));

int * * resultMatrix = (int * * ) malloc (N * sizeof (int * ) );

//Виділяємо пам'ять під елементи матриць

for (int i = 0; i< M ; i ++ ) {

matrix1 [i] = (int *) malloc (M * sizeof (int));

matrix2 [i] = (int *) malloc (M * sizeof (int));

resultMatrix [i] = (int *) malloc (M * sizeof (int));

//ініціалізуємо початковими значеннями

for (int i = 0; i< N ; i ++ ) {

for (int j = 0; j< M ; j ++ ) {

matrix1 [i] [j] = i;

matrix2 [i] [j] = j;

resultMatrix [i] [j] = 0;

//Виділяємо пам'ять під масив ідентифікаторів потоків

pthread_t * threads = (pthread_t *) malloc (N * sizeof (pthread_t));

//скільки потоків - стільки і структур з потокових даних

pthrData * threadData = (pthrData *) malloc (N * sizeof (pthrData));

//ініціалізуємо структури потоків

for (int i = 0; i< N ; i ++ ) {

threadData[i]. rowN = i;