Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции_Р2.doc
Скачиваний:
4
Добавлен:
01.04.2025
Размер:
2 Мб
Скачать
☆
      1. Керування потоками в Linux

Базова підтримка багатопотоковості

Донедавна єдиним засобом підтримки багатопотоковості в ядрі Linux був системний виклик сі one(), який дає можливість створити новий процес на базі наявного. Цей виклик багато в чому схожий на виклик fork О, але має кілька особливостей.

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

Для нового процесу потрібно задати новий стек (оскільки внаслідок виклику сіone() два процеси можуть спільно використовувати загальну пам'ять, для них неприпустиме використання загального стека).

Підтримка сlone() означає реалізацію багатопотоковості за схемою 1:1. При цьому первинними в системі є процеси, а не потоки (у Linux послідовності інструкцій процесора, з якими працює ядро, називають процесами, а не потоками ядра).

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

Бібліотека підтримки потоків LinuxThreads

Безпосередньо використовувати системний виклик сіone() для створення багатопотокових застосувань складно. Крім того, таке використання веде до втрати здатності дo перенесення застосувань (виклик сіone() реалізований тільки в Linux і не є частиною POSIX). Для того щоб прикладний програміст міг використати у своїх програмах потоки POSIX, необхідна реалізація цього стандарту в бібліотеці підтримки потоків.

Основною бібліотекою підтримки потоків у Linux довгий час була бібліотека LinuxThreads. У ній підтримуються ті можливості стандарту POSIX. lb, які реалізуються на основі виклику сіопеО. На жаль, через обмеженість цього виклику бібліотеці властиві об'єктивні недоліки, які ми розглянемо докладно.

Недоліки традиційної підтримки багатопотоковості в Linux

Основи підтримки багатопотоковості в ядрі Linux не зазнали принципових змін від появи системного виклику с1опе(). За цей час Linux із експериментальної системи перетворився на систему промислового рівня, в якій виконують корпоративні застосування в цілодобовому режимі з великим навантаженням.

Таке використання системи висунуло до реалізації багатопотоковості вимоги зисокої надійності та масштабованості. Стало очевидно, що реалізація, заснована на системному виклику сі one (), цим вимогам не відповідає. Назвемо деякі причини цієї невідповідності.

Оскільки потоки, створені за допомогою clone(), фактично були процесами, що спільно використовують ресурси, створення кожного потоку збільшувало кількість процесів у системі.

Кожний потік у системі, будучи за своєю суттю процесом, мав власний ідентифікатор процесу (pid), що не відповідало стандарту POSIX. Крім того, якщо потік створював інший потік, між ними виникав зв'язок «предок-нащадок», тоді як всі потоки мають бути рівноправними.

Кожне багатопотокове застосування користувача обов'язково створювало додатковий потік (потік-менеджер), який відповідав за створення та знищення потоків і перенаправляв їм сигнали. Наявність такого потоку знижувала надійність і масштабованість системи (наприклад, у разі аварійного завершення потоку-менеджера застосування залишалося у невизначеному стані). Усі ці фактори робили багатопотоковість у Linux швидше цікавим полем для експериментів, аніж реальним засобом підвищення продуктивності й масштабо-ваності застосувань. Розв'язати всі проблеми шляхом усунення недоліків сlone() і LinuxThreads виявилося неможливим, потрібне було повне перероблення засобів реалізації багатопотоковості в ядрі та створення нової бібліотеки підтримки потоків.

Особливості нової реалізації багатопотоковості в ядрі Linux

Підвищилася продуктивність операції створення і завершення потоків. Знято обмеження на загальну кількість потоків у системі. Система залишається стабільною за умов одночасного створення і завершення сотень тисяч потоків (за наявності достатнього обсягу оперативної пам'яті) [67].

Усі потоки процесу тепер повертають один і той самий ідентифікатор (pid), крім того, зв'язок «предок-нащадок» між ними не підтримується (у створеного потоку зберігається той самий предок, що й у потоку-творця). Як процес у системі реєструють тільки початковий потік застосування.

Реалізацію виклику clone() розширено таким чином, щоб зробити непотрібним потік-менеджер.

Сьогодні нові засоби інтегровані у тестову версію ядра. Доступна також оновлена реалізація бібліотеки потоків, яка дістала назву NPTL (Native POSIX Threads Library).

Бібліотека підтримки потоків NPTL

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

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

Потоки ядра Linux

Крім процесів і потоків користувача, Linux також підтримує спеціальний вид планованих об'єктів, які мають уже знайому назву — потоки ядра. Такі потоки планують як звичайні процеси і потоки, кожен з яких має свій ідентифікатор (pid). Відмінності потоків ядра від процесів і потоків користувача полягають у тому, що:

функції потоку для них визначають у коді ядра;

вони виконуються тільки в режимі ядра;

для них недоступні ділянки пам'яті, виділені в режимі користувача.

Програмний інтерфейс керування потоками POSIX

Керування потоками є частиною стандарту POSIX з 1996 року, відповідний програмний інтерфейс дістав назву потоки POSIX (POSIX threads). Цей стандарт реалізовано у більшості сучасних UNIX-сумісних систем, у Linux його підтримка була частково реалізована в бібліотеці LinuxThreads і значно повніше - у бібліотеці NPTL.

Усі типи даних і функції, визначені стандартом, мають префікс pthread_, для їхнього використання необхідно підключати заголовний файл pthread.h.

Створення потоків POSIX

Щоб додати новий потік у поточний процес, у POSIX використовують функцію :thread_create() із таким синтаксисом:

#іnсlude<pthread.h>

int pthread_create(pthread_t *th. pthread_attr_t *attr.

void *(*thread_fun)(void *). void *arg);

Розглянемо параметри цієї функції:

th — покажчик на заздалегідь визначену структуру типу pthread_t, яка далі буде передана в інші функції роботи з потоками; далі називатимемо її дескриптором потоку (thread handle);

attr — покажчик на структуру з атрибутами потоку (для використання атрибутів за замовчуванням потрібно передавати нульовий покажчик);

thread_fun — покажчик на функцію потоку, що має описуватися як

void *mythread_fun(void *value) {

// виконання коду потоку }

arg — дані, що передаються у функцію потоку (туди вони потраплять як параметр value).

Ось приклад створення потоку POSIX:

#inc!ude<pthread.h>

// функція потоку

void *thread_fun(void *num) {

printf ("потік номер %й\п". (int)num);

}

// ...

pthread_t th;

// створюємо другий потік

pthread_create (&th. NULL. thread_fun. (void *)++thread_num);

// тут паралельно виконуються два потоки

Для потоків, створених у такий спосіб, розмір стека задають за замовчуванням. Крім того, після завершення їх обов'язково треба приєднати.

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

Завершення потоків POSIX

Для завершення потоку достатньо дійти до кінця його функції потоку thread_fun() або викликати з неї pthread_exit():

#include<pthread.h>

void pthread_exit(void *retval);

Параметр retval задає код повернення. Наведемо приклад завершення потоку:

void *thread_fun(void *num) {

printf ("потік номер Xd\n", (int)num);

pthreadexit (0): }

Для коду початкового потоку програми є дві різні ситуації:

виконання оператора return або виконання всіх операторів до кінця та і п() завершує весь процес, при цьому всі потоки теж завершують своє виконання;

виконання функції pthread_exit() усередині функції main() завершує тільки початковий потік, ця дія не впливає на інші потоки у програмі — вони продовжуватимуть виконання, поки самі не завершаться.

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

Приєднання потоків POSIX

Для очікування завершення виконання потоків використовують функцію pthread _join(). Вона має такий синтаксис:

int pthread_join(pthread_t th, void **status);

Ця функція блокує потік, де вона була викликана, доти, поки потік, заданий дескриптором th, не завершить своє виконання. Потік, на який очікують, має існувати в тому самому процесі й не бути від'єднаним. Якщо status не є нульовим покажчиком, після виклику він вказуватиме на дані, повернені потоком (аргумент функції pthread_exit()). Якщо потік уже завершився до моменту виклику функції pthread_join(), його інформація має бути зчитана негайно.

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

Розглянемо приклад приєднання потоку. Припустимо, що треба асинхронно запустити доволі тривалий запит до бази даних, виконати паралельно з ним деяку роботу (наприклад, відобразити індикатор виконання), після чого дочекатися результатів запиту. Пропоноване рішення зводиться до створення окремого потоку для виконання запиту (потоку-помічника, helper thread) і до очікування його за-зершення у базовому потоці. Ось приблизний програмний код:

struct result_data {

// інформація з бази даних

}:

void *query_fun(void *retval) {

struct result_data db_result;

// одержати дані з бази (search_db() тут не задаємо)

dbjresuit e search_db(...);

// визначаємо результат

*(struct result_data *)retval = db_result;

pthread_exit(O);

}

int main 0 {

pthread_t query_th;

void *status;

struct result_data query_res;

// почати виконувати запит паралельно з основним кодом mainO

pthread_create(&query_th. NULL. query_fun. &query_res);

// вчинити деякі дії паралельно з виконанням запиту

// тут два потоки виконуються паралельно

pthread_join(query_th, Sstatus); // дочекатися результату запиту

// тепер можна безпечно використати результат запиту query_res

return 0; }

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

Від'єднані потоки. Задання атрибутів потоків POSIX

Для того, щоб отримати від'єднаний потік, треба задати відповідні атрибути потоку, виконавши такі дії.

Описати змінну для їхнього зберігання (атрибути потоку відображає структура даних pthread_attr_t):

pthread_attr_t attr;

Ініціалізувати цю змінну так: pthread_attr_init(&attr);

Задати атрибут за допомогою виклику спеціальної функції (різним атрибутам відповідають різні функції).

Для задання атрибута, який визначає, чи буде цей потік від'єднаним чи ні, використовують таку функцію:

pthread_attr_setdetachstate(pthread_attr_t attr, int detachstate);

Другим параметром може бути одне зі значень: PTHREAD_CREATE_JOINABLE (приєднуваний потік); PTHREAD_CREATE_DETACHED (від'єднаний потік).

Ось приклад задання атрибута і його використання під час створення від'єднаного потоку:

pthread_attr_init(&attr);

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_create(&th. &attr. ...);

Завершуються такі потоки виходом із функції потоку.