Лабораторна робота №3 Тема: «Процеси і потоки»
Процеси і потоки
На найвищому рівні абстракції система складається з безлічі процесів. Кожен процес відповідальний за забезпечення службових функцій певного характеру|вдачі|, незалежно від того, чи є|з'являється| він елементом файлової системи, драйвером дисплея, модулем збору|збирання| даних, модулем управління або чим-небудь ще.
В межах кожного процесу може бути безліч потоків. Число потоків варіюється. Один розробник ПО, використовуючи тільки|лише| єдиний потік, може реалізувати ті ж самі функціональні можливості|спроможності|, що і інший, що використовує п'ять потоків. Деякі завдання|задачі| самі по собі приводять|призводять| до багатопоточності і дають відносно прості рішення|розв'язання|, інші через свою природу, є|з'являються| однопоточними, і звести їх до багатопотокової реалізації достатньо|досить| важко.
Чому процеси?
Чому ж не узяти просто один процес з|із| множиною|безліччю| потоків? Тоді як деякі операційні системи вимушують|змушують| вас програмувати тільки|лише| в такому варіанті, виникає ряд|лава| переваг при розділенні|поділі| об'єктів на безліч процесів:
можливість|спроможність| декомпозиції завдання|задачі| і модульної організації рішення|розв'язання|;
зручність супроводу;
надійність.
Концепція розділення|поділу| завдання|задачі| на частини|на шматки|, тобто, декілька незалежних завдань|задачі|, є|з'являється| дуже могутньою. І саме така концепція лежить в основі QNX|. Операційна система QNX| складається з безлічі незалежних модулів, кожен з яких наділений деякою зоною відповідальності. Ці модулі незалежні і реалізовані в окремих процесах. Єдина можлива встановити залежність цих модулів один від одного — налагодити між ними інформаційний зв'язок за допомогою невеликої кількості строго|суворий| певних інтерфейсів.
Це природно веде до спрощення супроводу програмних продуктів, завдяки незначному числу взаємозв'язків. Оскільки кожен модуль чітко визначений, і усувати несправності в одному такому модулі буде набагато простіше - тим паче, що він не пов'язаний з іншими.
Запуск процесу
Тепер звернемо увагу на функції, призначені для роботи з|із| потоками і процесами. Будь-який потік може здійснити запуск процесу; єдині обмеження, що накладаються|накладають| тут, витікають з|із| основних принципів захисту (правила доступу до файлу, обмеження на привілеї|привілегії| і т. д.).
Запуск процесу з|із| командного рядка
Наприклад, при запуску процесу з|із| командного інтерпретатора ви можете ввести|запроваджувати| командний рядок:
$ program1|
Ця вказівка наказує|пропонує| командному інтерпретатору запустити програму program1| і чекати завершення її роботи. Або, ви могли набрати:
$ program2| &
Ця вказівка наказує|пропонує| командному інтерпретатору запустити програму program2| без очікування|чекання| її завершення. У такому разі|в такому разі| говорять, що програма program2| працює у фоновому режимі.
Якщо ви побажаєте|забажаєте| скоректувати пріоритет програми до її запуску, ви можете застосувати команду nice| — точно так, як і в Unix|:
$ nice| program3|
Запуск процесу з|із| програми
Нас зазвичай|звично| не турбує той факт, що командний інтерпретатор створює процеси — це просто мається на увазі. У деяких прикладних завданнях|задачах| можна покластися на сценарії командного інтерпретатора (пакети команд, записані у файл), які зроблять цю роботу за вас, але|та| у ряді|в ряді| інших випадків ви побажаєте|забажаєте| створювати процеси самостійно.
Наприклад, у великій мультипроцесорній|мультипроцесор| системі ви можете побажати|забажати|, щоб|аби| одна головна програма виконала запуск всіх інших процесів вашого застосування на підставі деякого конфігураційного файлу. Іншим прикладом|зразком| може служити необхідність запуску процесів по деякій події.
Розглянемо|розглядуватимемо| деякі з функцій, які забезпечує для запуску інших процесів (або підміни одного процесу іншим):
system|();
сімейство функцій ехес|();
сімейство функцій spawn|();
fork|();
vfork|().
Яку з|із| цих функцій застосовувати, залежить від двох вимог: переносимості|переносимий| і функціональності. Як завжди, між цими двома вимогами можливий компроміс.
Зазвичай|звично| при всіх запитах на створення|створіння| нового процесу відбувається|походить| наступне|слідуюче|. Потік в первинному|початковому| процесі викликає|спричиняє| одну з наведених вище функцій. Зрештою|врешті решт| функція змусить|примусить| адміністратор процесів створити адресний простір|простір-час| для нового процесу. Потім ядро виконає запуск потоку в новому процесі. Цей потік виконає декілька інструкцій і викличе|спричинятиме| функцію main|().
У розглянутому прикладі після породження процесу – нащадка, батьківський процес видає виводить на термінал ідентифікатор породженого процесу, затримується на 5 секунд і викликає функцію для опиту стану процесу – нащадка. Породжений процес виводить повідомлення, що містить значення змінної x. Слід звернути увагу на те, що значення цієї змінною збігаються і у батька, і у нащадка.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int x, pid;
x=2;
printf("Single process, x=%d\n",x);
pid=fork();
if(pid == 0)
printf("New, x=%d\n",x); // Нащадок
else if(pid > 0){ // Батько
printf("Old, pid=%d, x=%d\n",pid,x);
sleep(5);
wait(pid);
}
else {
perror("Fork error ");
return -1;
}
return 0;
}
З практичної точки зору в більшості випадків в рамках породженого процесу завантажується для виконання програма, визначена одним з системних викликів execl(), execv(),... Кожен з цих системних викликів здійснює заміну програми, що визначає функціонування даного процесу:
execl(name,arg0,arg1...,argn,0)
char *name, *arg0, *arg1,...,*argn;
execv(name,argv)
char *name, *argv[];
execle(name,arg0,arg1...,argn,0,envp)
char *name, *arg0, *arg1,...,*argn,*envp[];
execve(name,argv,envp)
char *name, *arg[],*envp[];
Запуск потоку
Тепер, коли ми знаємо, як запустити інший процес, давайте розглянемо|розглядуватимемо|, як здійснити запуск іншого потоку.
Будь-який потік може створити інший потік в тому ж самому процесі; на це не накладаються|накладають| ніяких|жодних| обмежень (за винятком об'єму|обсягу| пам'яті, звичайно!) Найбільш загальний|спільний| шлях|колія| реалізації цього — використання виклику функцій pthread_create|():
#include| <pthread|.h>
int| int|
pthread_create| (pthread_t| *thread, const| pthread_attr_t| *attr, void| *(*start_routine|) (void| *), void| *arg);
Функція pthread_create|() має чотири аргументи :
thread| - покажчик на pthread_t|, де зберігається ідентифікатор потоку;
attr| - атрибутний запис;
start_routine| - підпрограма, з|із| якою починається|розпочинає| потік;
arg| - параметр, який передається підпрограмі start_routine|.
Відзначимо, що покажчик thread| і атрибутний запис (attr|) — необов'язкові елементи, ви можете передавати замість них NULL|.
Параметр thread| може використовуватися для зберігання ідентифікатора новостворюваного потоку. Звернете увагу, що в прикладах|зразках|, приведених нижче, ми передамо|передаватимемо| NULL|, позначивши цим, що ми не піклуємося про те, який ідентифікатор матиме новостворюваний потік.
Якби|аби| нам була до цього справа|річ|, ми б зробили так:
pthread_t| tid|;
pthread_create| (&tid, ...
printf| («Новий потік має ідентифікатор %d\n», tid|);
Таке застосування|вживання| досконале|довершене| типово, тому що|бо| вам часто може потрібно знати, який потік виконує яка ділянка коди.
Невеликий тонкий момент. Новий потік може почати|розпочинати| працювати ще до привласнення|присвоєння| значення параметру tid|. Це означає, що ви повинні уважно відноситися до використання tid| як глобальна змінна. У прикладі|зразку|, приведеному вище, все буде коректно, тому що|бо| виклик pthread_create|() відпрацював|відробляв| до використання tid|, що означає, що на момент використання tid| мав коректне значення.
Новий потік починає|розпочинає| виконання з функції start_routine| (), з|із| параметром arg|.
Атрибутний запис потоку
Коли ви здійснюєте запуск нового потоку, він може слідувати|прямувати| ряду|лаві| чітко певних установок за умовчанням, або ж ви можете явно задати його характеристики.
Перш, ніж ми перейдемо до обговорення завдання|задавання| атрибутів потоку, розглянемо|розглядуватимемо| тип|типа| даних
Синхронізація
Найпростіший метод синхронізації — це «приєднання» (joining|) потоків. Реальна ця дія означає очікування|чекання| завершення.
Приєднання виконується одним потоком, що чекає завершення іншого потоку. Потік, що чекає, викликає|спричиняє| pthreadjoin|():
#include| <pthread|.h>
int|
pthread_join| (pthread_t| thread|, void| **value_ptr);
Функції pthreadjoin|() передається ідентифікатор потоку, до якого ви бажаєте приєднатися, а також необов'язковий аргумент value_ptr|, який може бути використаний для збереження|зберігання| повертаного приєднуваним потоком значення. (Ви можете передати|передавати| замість цього параметра NULL|).
Де нам брати ідентифікатор потоку?
У функції pthread_create|() як перший аргумент покажчик на pthread_t|. Там і буде збережений ідентифікатор знов|знову| створеного потоку.