Сигнали як засіб взаємодії процесів 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Сигнали як засіб взаємодії процесів



Лабораторна робота 4

Процеси та сигнали ОС Linux

 

Мета: навчитись використовувати комунікацію між процесами за допомогою сигналів та семафорів

 

Теоретичні відомості

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

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

Виконання процесу

Виконання процесу здійснюється ядром. Підсистема керування процесами відповідає за синхронізацію процесів, їх взаємодію, розпо­діл пам’яті та планування виконання процесів.

З практичної точки зору процес у системі Linux є об’єктом, створюваним у результаті виконання системного виклику fork. Кожен процес, за винятком нульового, породжується в результаті запуску ін­шим процесом операції fork. Процес, що запустив операцію fork, нази­вається батьківським, а створений процес – породженим. Кожен про­цес має одного батька, але може породити багато процесів. Ядро сис­теми ідентифікує кожен процес за його номером, що називається іден­тифікатором процесу (PID). Нульовий процес є особливим процесом, що створюється “вручну” у результаті завантаження системи. Процес 1, відомий під ім’ям init, є предком будь-якого іншого процесу в системі та пов’язаний з кожним процесом.

Ядро завантажує файл в пам’ять при виконанні сис­темної опе­рації exec, при цьому завантажений процес складається щонайменше із трьох частин, так званих областей: коду, даних і стека.

Процес у системі Linux може виконуватися у двох режимах – ядра або задачі. У режимі задачі процес виконує інструкції прикладної програми, системні структури даних йому недоступні.

Коли процес виконує спеціальну інструкцію (системний вик­лик), він перемикається в режим ядра. Кожні системній операції відпо­відає точка входу в бібліотеці системних операцій. Відповідно й образ процесу складається із двох частин: даних режиму ядра і режиму задачі.

Коли процес запускає системну операцію exec, ядро системи ви­діляє області під її код, дані та стек. Якщо процес запускає операцію fork, ядро робить фізичне копіювання адресного простору процесу-батька, не дозволяючи процесам спільно використати ці області. Якщо процес за­пускає операцію exit, ядро звільняє області, які використа­ли­ся процесом.

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

Процеси висувають різні вимоги до системи з погляду їхнього планування і загальної продуктивності. Можна виділити три основних класи додатків:

– інтерактивні додатки (командні інтерпретатори, редактори та ін.). Більшу частину часу вони проводять, чекаючи введенні даних від ко­ристувача, але для них критичний час відгуку (реакції системи на вве­дення даних);

– фонові додатки (не потребують втручання користувача). Ос­новний показник ефективності для них – мінімальний сумарний час виконання в системі;

– додатки реального часу. Вони звичайно прив’язані до тайме­ра і вимагають гарантованого часу здійснення тієї або іншої операції та часу відгуку.

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

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

Виконання процесу може бути перервано:

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

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

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

Контекст процесу

Під контекстом процесу розуміється вся інформація для опису процесу. Контекст процесу складається з декількох частин:

– адресний простір процесу в режимі задачі (код, дані і стек процесу, а також поділювана пам’ять і дані динамічних бібліотек);

– керуюча інформація (структури proc та user – запис таблиці процесів і додаткова інформація відповідно);

– оточення процесу (системні змінні, наприклад, домашній каталог, ім’я користувача та ін.);

– апаратний контекст (значення використовуваних машинних регістрів).

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

Аналогічним чином, при переході з режиму задачі в режим яд­ра, ядро системи зберігає інформацію, достатню для того, щоб пізніше по­вернутися в режим задачі та продовжити виконання з перерваного місця. Однак, перехід з режиму задачі в режим ядра є зміною режиму, але не пе­ремиканням контексту. Ядро міняє режим виконання з режиму задачі на режим ядра і навпаки, залишаючись у контексті одного процесу.

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

 

Стани процесу

Час життя процесу можна розділити на кілька станів, кожний з яких має певні характеристики, що описують процес. Перелічимо ос­новні зі станів:

1) процес виконується;

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

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

Оскільки процесор у кожен момент часу виконує тільки один про­цес, у стані “процес виконується” може перебувати тільки один процес.

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

 

Блокувати

 

Запустити

 

Поновити

 

Очікує

 

Виконується

 

Блокований

 

Завершити

 

Зняти

 

Створити

 


Рис. 4.1 – Стани процесу та переходи між ними

Таблиця 4.1 – Назва і призначення сигналів в ОС Linux

Назва Призначення
#define NSIG Відсутній сигнал
#define SIGHUP Розрив зв’язку
#define SIGINT Переривання
#define SIGQUIT Аварійний вихід
#define SIGILL Неправильна машинна інструкція
#define SIGTRAP Переривання-пастка
#define SIGIOT Переривання вводу-виводу
#define SIGEMT Програмне переривання EMT
#define SIGFPE Помилка при виконанні операції з плаваю­чою точкою
#define SIGKILL Знищення процесу
#define SIGBUS Помилка шини
#define SIGSEGV Порушення сегментації
#define SIGSYS Помилка виконання системного виклику
#define SIGPIPE Запис у канал є, читання немає
#define SIGALRM Переривання від таймера
#define SIGTERM Програмний сигнал завершення від kill
#define SIGUSR1 Визначається користувачем
#define SIGUSR2 Визначається користувачем
#define SIGCLD Процес-нащадок завершився
#define SIGPWR Аварія живлення

 

Причини виникнення сигналів для різних версій можуть відріз­нятися; спочатку вони були обумовлені архітектурними особливостя­ми ЕОМ PDP-11.

Причини виникнення сигналів. Виникнення сигналів можна класифікувати в такий спосіб:

– введення користувачем керуючого символу з термінала всім про­цесам, асоційованим з цим терміналом (SIGINT, SIGQUIT, SIGHUP);

– виникнення аварійної ситуації при функціонуванні процесу користувача (SIGILL, SIGTRAP, SIGFPE, SIGBUS, SIGSEGV, SIGSYS, SIGPIPE);

– виникнення непередбаченої або такої, що не піддається іден-тифікації події (SIGTERM, SIGCLD, SIGPWR);

– виникнення деякої заздалегідь описаної події (SIGALRM).

Посилка сигналів виробляється процесами – один одному, за допомогою функції kill або ядром. Для кожного процесу визначений бінарний вектор, довжина якого дорівнює кількості сигналів у системі. При одержанні процесом сигналу I відповідний i -й розряд цього векто­ра стає рівним 1. Кожному сигналу відповідає адреса функції, що буде викликана для обробки даного сигналу.

 

Обробка сигналів. Ядро обробляє сигнали в контексті того проце­су, що одержує їх, тому щоб обробити сигнали, потрібно запустити процес

Існує три способи обробки сигналів:

– реакція за замовчуванням;

– ігнорування сигналу;

– виконання функції користувача після її отримання.

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

Обробляючи сигнал, ядро визначає тип сигналу і очищає (га­сить) розряд у записі таблиці процесів, що відповідає цьому типу сиг­налу. Таким чином, коли процес одержує будь-який не ігнорований ним сигнал (за винятком SIGILL та SIGTRAP), ОС LINUX автоматич­но відновлює реакцію “за замовчуванням” на всяке наступне одержан­ня цього сигналу.

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

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

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

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

 

Групи процесів

Незважаючи на те, що в системі Linux процеси ідентифікую­ться унікальним кодом (PID), системі іноді доводиться використо­ву­вати для ідентифікації процесів номер “групи”, у яку вони входять. На­приклад, процеси, що мають загального предка – інтерпретатора shell, взаємозалежні, і тому коли користувач натискає клавіші “delete” або “break”, або коли термінальна лінія “зависає”, всі ці процеси одер­жують відповідні сигнали. Ядро використовує код групи процесів для ідентифікації групи взаємозалежних процесів, які при настанні певних подій повинні одержувати загальний сигнал. Код групи запам’ято­вує­ться в таблиці процесів. При виконанні функції fork процес-нащадок успадковує код групи свого батька.

Для того, щоб утворити нову групу процесів, варто скориста­тися системною функцією setpgrp:

grp = setpgrp();

де grp – новий код групи процесів, рівний коду ідентифікації процесу, що здійснив виклик setpgrp().

FORK

Створення нового процесу:

int fork(void);

pid = fork();

В ході виконання функції ядро виконують наступну послідов­ність дій:

– відводить місце в таблиці процесів під новий процес;

– присвоює породжуваному процесу унікальний код ідентифі­кації;

– робить копію контексту батьківського процесу. Оскільки ті або інші складові процесу, такі як область команд, можуть розділятися іншими процесами, ядро може іноді замість копіювання області в нову фізичну ділянку пам’яті просто збільшити значення лічильника по­силань на область;

– збільшує значення лічильника числа файлів, пов’язаних із процесом, як у таблиці файлів, так і у таблиці індексів;

– повертає батьківському процесу код ідентифікації породже­ного процесу, а породженому процесу – 0.

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

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

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

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

EXIT

Завершення виконання процесу:

void exit(int status);

де status – значення, що повертається функцією батьківському процесу. Процеси можуть викликати функцію exit як у явному, так й у неявному виді (по закінченні виконання програми функція exit ви­кликається автоматично з параметром 0). Також ядро може викликати функцію exit зі своєї ініціативи, якщо процес не прийняв надісланий йому сигнал. У цьому випадку значення параметра status дорівнює но­меру сигналу. Виконання виклику exit призводить до “припинення іс­нування” процесу, звільнення ресурсів і ліквідації контексту.

WAIT

Очікування завершення виконання процесу-нащадка:

int wait(int *stat);

pid = wait(stat_addr);

де pid – значення коду ідентифікації (PID) нащадка, що завер­шився, stat_addr – адреса змінної цілого типу, у яку буде поміщене зна­чення, що повертається функцією exit.

За допомогою цієї функції процес синхронізує продовження свого виконання з моментом завершення нащадка. Ядро веде пошук нащадків процесу, що припинили існування, і у випадку їхньої від­сутності повертає помилку.

Якщо виявлений нащадок, що припинив існування, ядро пере­дає його код ідентифікації й значення, що повертається через параметр функції exit, процесу, що викликав функцію wait. Таким чином, через параметр функції exit (status) процес, що завершується, може пере­давати різні значення у закодованому вигляді отримувачу інформації про причину завершення процесу, однак на практиці цей параметр використається за призначенням досить рідко.

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

SLEEP

Призупинка роботи процесу на певний час:

void sleep(unsigned seconds);

де seconds – кількість секунд, на яке потрібно призупинити ро­боту процесу.

Спочатку ядро підвищує пріоритет роботи процесу так, щоб заблокувати всі переривання, які могли б (шляхом створення кон­куренції) перешкодити роботі із чергами припинених процесів, і запа­м’ятовує старий пріоритет, щоб відновити його, коли виконання про­цесу буде відновлено. Процес одержує позначку “припиненого”, адре­са призупинки та пріоритет запам’ятовуються в таблиці процесів, а процес поміщається у чергу призупинених процесів. У найпростішому випадку (коли призупинка не допускає переривань) процес виконує пе­ремикання контексту і благополучно “засинає”. Коли припинений про­цес “пробуджується”, ядро починає планувати його запуск. Таким чи­ном, не можна гарантувати, що після закінчення заданого часу припи­нений процес відразу відновить свою роботу: він може бути виванта­жений на час призупинки і тоді потрібно його підкачування в пам’ять; у цей час на виконанні може перебувати процес із більш високим пріо­ритетом або процес, що не допускає переривань (наприклад, що пе­ребуває в критичному інтервалі) і т.д.

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

 

EXEC

Запуск програми.

Системний виклик exec здійснює кілька бібліотечних функцій – execl, execv, execle та ін. наведемо формат однієї з них:

int execv(char *path, char *argv[ ]);

res = execv(path, argv);

де path – ім’я виконуваного файла, argv – покажчик на масив параметрів, які передаються програмі. Цей масив аналогічний пара­метру argv командного рядка функції main. Список argv повинен міс­тити мінімум два параметри: перший – ім’я програми, що підлягає ви­конанню (відображається в argv[0] функції main нової програми), дру­гий – NULL (завершальний список аргументів).

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

Виклик exec повертає 0 при успішному завершенні та –1 при аварійному. В останньому випадку керування повертається у про-граму, що викликає.

Як приклад використання цього виклику можна розглядати ро­боту командного інтерпретатора shell: при виконанні команди він спо­чатку породжує свою копію за допомогою виклику fork, а потім запус­кає відповідно зазначеній команді програму системним викликом exec.

KILL

Посилка всім або деяким процесам будь-якого сигналу:

int kill(pid, sig);

де sig – номер сигналу, pid – ідентифікатор процесу, що виз­начає групу родинних процесів, яким буде посланий сигнал:

– якщо pid – позитивне ціле число, ядро посилає сигнал про­цесу з ідентифікатором pid.

– якщо значення pid дорівнює 0, сигнал посилається всім про­цесам, що входять в одну групу із процесом, що викликав функцію kill.

– якщо значення pid дорівнює –1, сигнал посилається всім про­цесам, у яких реальний код ідентифікації користувача збігається з тим, під яким виконується процес, що викликав функцію kill. Якщо процес, що послав сигнал, виконується під кодом ідентифікації суперкористу­вача, сигнал розсилається всім процесам, крім процесів з ідентифіка­торами 0 та 1.

– якщо pid – від’ємне ціле число, але не –1, сигнал посилається всім процесам, що входять у групу з номером, рівним абсолютному значенню pid.

Виклик kill повертає 0 при успішному завершенні та –1 при аварійному (наприклад, специфікація неіснуючого в ОС Linux сигналу або неіснуючого процесу).

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

Посилка сигналу може супроводжувати виникнення будь-якої події. Сигнали SIGUSR1, SIGUSR2 та SIGKILL можуть бути послані тільки за допомогою системного виклику kill.

 

SIGNAL

Дозволяє процесу самому визначити свою реакцію на одер­жання того або іншого сигналу:

#include <signal.h>

int signum;

void handler(int);

void (*signal(signum, void (*handler)(int)))(int)

Після визначення реакції на сигнал signal при одержанні про­цесом цього сигналу буде автоматично викликатися функція handler(), яка повинна бути описана або оголошена перш, ніж буде здійснений системний виклик signal.

При багаторазовій обробці того самого сигналу, процес пови­нен щораз здійснювати системний виклик signal для встановлення необхідної реакції на даний сигнал. Використання констант SIG_DFL і SIG_IGN доз­воляє спростити реалізацію двох найчастіших реакцій процесу на сигнал:

– signal(SIGINT,SIG_IGN) – ігнорування сигналу;

– signal(SIGINT,SIG_DFL) – відновлення стандартної реакції на сигнал.

Аргументом функції-оброблювача є ціле число – номер оброб­люваного сигналу. Значення його встановлюється ядром.

 

PAUSE

Припиняє функціонування процесу до одержання ним деякого сигналу:

void pause();

Цей системний виклик не має параметрів. Робота процесу від­новлюється після одержання ним будь-якого сигналу, крім тих, які іг­норуються цим процесом.

 

ALARM

Посилка процесу сигналу побудки SIGALARM:

unsigned alarm(unsigned secs);

Цим системним викликом процес інформує ядро ОС про те, що ядро повинне послати цьому процесу сигнал побудки через secs се­кунд. Виклик alarm повертає число секунд, задане при попередньому здійсненні цього системного виклику.

Якщо secs дорівнює 0, то специфіковане раніше посилка про­цесу сигналу SIGALARM буде скасована.

Наведена нижче програма в результаті виконання породить три процеси (процес-предок 1 і процеси-нащадки 2 та 3).

#include <sys/types.h>

#include <fcntl.h>

#include <stdio.h

void main(void)

{

int pid2, pid3, st; /* process 1 */

printf(“Process 1, pid = %d:\n”, getpid());

pid2 = fork();

if (pid2 == 0) /* process 2 */

{

printf(“Process 2, pid = %d:\n”, getpid());

pid3 = fork();

if (pid3 == 0) /* process 3 */

{

printf(“Process 3, pid = %d:\n”, getpid());

sleep(2);

printf(“Process 3: end\n”);

}

/* process 2 */

if (pid3 < 0) printf(“Cann’t create process 3: error %d\n”, pid3);

wait(&st);

printf(“Process 2: end\n”);

}

else /* process 1 */

{

if (pid2 < 0) printf(“Cann’t create process 2: error %d\n”, pid2);

wait(&st);

printf(“Process 1: end\n”);

}

exit(0);

}

Відповідно до програми спочатку буде створений процес 1 (як нащадок інтерпретатора shell), він повідомить про початок своєї ро­боти і породить процес 2. Після цього робота процесу 1 призупиниться і почне виконуватися процес 2, як більш пріоритетний. Він також по­відомить про початок своєї роботи й породить процес 3. Далі почне ви­конуватися процес 3, він повідомить про початок роботи і “засне”. Після цього відновить своє виконання або процес 1, або процес 2 за­лежно від величин пріоритетів і від того, наскільки процесор заванта­жений іншими процесами. Оскільки жоден із процесів не виконує ніякої ро­боти, вони, найімовірніше, встигнуть завершиться до понов­лення про­цесу 3, який у цьому випадку завершиться останнім.

Приклад синхронізації роботи процесів. Програма в результаті виконання породить три процеси (процес-предок 0 і процеси-нащадки 1 та 2). Процеси 1 та 2 будуть обмінюватися сигналами і видавати від­повідні повідомлення на екран, а процес 0 через певну кількість секунд відправить процесам 1 та 2 сигнал завершення і сам припинить своє функціонування.

#include <sys/types.h>

#include <fcntl.h>

#include <stdio.h>

#include <signal.h>

#include <unistd.h>

#define TIMEOUT 10

extern int f1(int), f2(int), f3(int);

int pid0, pid1, pid2;

void main(void)

{

setpgrp();

pid0 = getpid();

pid1 = fork();

if (pid1 == 0) /* process 1 */

{ signal(SIGUSR1, f1);

pid1 = getpid();

pid2 = fork();

if (pid2 < 0) puts(«Fork error»);

if (pid2 > 0) for(;;);

else /* process 2 */

{ signal(SIGUSR2, f2);

pid2 = getpid();

kill(pid1,SIGUSR1);

for (;;);

}

}

else /* process 0 */

{ signal(SIGALRM, f3);

alarm(TIMEOUT);

pause();

}

exit(0);

}

int f1(int signum)

{ signal(SIGUSR1, f1);

printf(«Process 1 (%d) has got a signal from process 2 (%d)\n»,pid1,pid2);

sleep(1);

kill(pid2, SIGUSR2);

return 0;

}

int f2(int signum)

{ signal(SIGUSR2, f2);

printf(«Process 2 (%d) has got a signal from process 1 (%d)\n»,pid2,pid1);

sleep(1);

kill(pid1, SIGUSR1);

return 0;

}

int f3(int signum)

{ printf(«End of job - %d\n», pid0);

kill(0, SIGKILL);

return 0;

}

Взаємодія процесів у LINUX

Пакет IPC (Interprocess communication) містить у собі три ме­ханізми. Механізм повідомлень дає процесам можливість посилати ін­шим процесам потоки форматованих даних, механізм поділу пам'яті дозволяє процесам спільно використати окремі частини віртуального адресного простору, а семафори – синхронізувати своє виконання з ви­конанням паралельних процесів. Незважаючи на те, що вони реалізую­ться у вигляді окремих блоків, вони мають загальні властивості.

Для використання механізмів IPC необхідно підключити до програми файл <sys/ipc.h>

 

Семафори

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

На сьогоднішній день запропонована велика кількість різних систем синхронізації процесів. До них відносяться:

– блокування пам’яті;

– семафори;

– критичні області;

– умовні критичні області;

– монітори;

– виключаючі області тощо.

Один зі способів синхронізації паралельних процесів – сема­фори Дейкстри, реалізовані в ОС LINUX.

Синхронізація процесів

Визначимо поняття процесу як завдання, виконуваного в ос. Оскільки завдання в багатовикористовуваній системі повинні викону­ватися незалежно і можуть виконуватися паралельно, то і процеси, що їх представляють, повинні бути незалежними і паралельно викону-ваними.

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

Якщо за умовами роботи потрібно, щоб поділювані ресурси одномоментно були доступні тільки одному процесу, то такі ресурси називаються критичними.

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

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

Такі примітиви за назвою P та V -операції були запропоновані Дейкстрою в 1968 р. Ці операції можуть виконуватися тільки над спе­ціальними змінними, називаними семафорами або семафорними змін­ними. Семафори є цілими величинами та спочатку були визначені як приймаючі тільки ненегативні значення. Крім того, якщо їх викорис­тати для рішення завдань взаємного виключення, то область їх значень може бути обмежена лише 0 або 1. Однак надалі була показана важли­ва область застосування семафорів, що приймають будь-які цілі пози­тивні значення. Такі семафори одержали назву “загальних семафорів” на відміну від “двійкових семафорів”, використовуваних у завданнях взаєм­ного виключення. P та V операції є єдиними операціями, виконуваними над семафорами. Іноді вони називаються семафорними операціями.

Дамо визначення P та V- операцій у тому вигляді, у якому вони були запропоновані Дейкстрою.

V -операція (V (S)) – операція з одним аргументом, що повинен бути семафором.

Ця операція збільшує значення аргументу на 1.

P- операція (P (S)) – операція з одним аргументом, що повинен бути семафором. Її призначення – зменшити величину аргументу на 1, якщо тільки ре­зультуюче значення не стає негативним.

Завершення P -операції, тобто рішення про те, що дійсний мо­мент є підходящим для виконання зменшення і наступне зменшення значення аргументу, повинне розглядатися як неподільна операція.

Ці визначення справедливі як для загальних, так для двійкових семафорів.

Реалізація семафорів

Системні виклики для роботи із семафорами містяться в пакеті IPC (підключається файл описів – <sys/ipc.h>). Ці виклики забезпе­чують синхронізацію виконання паралельних процесів, виконуючи на­бір дій тільки над групою семафорів (засобами низького рівня).

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

Кожен семафор у системі Linux являє собою набір значень (вектор семафорів). Пов’язані із семафорами системні функції є уза­гальненням операцій P та V- семафорів Дейкстри, у них допускається одночасне виконання декількох операцій (над семафорами, що на­лежать одному вектору, так звані векторні операції). Ядро виконує операції комплексно; жоден зі сторонніх процесів не зможе переуста­новлювати значення семафорів, поки всі операції не будуть виконані. Якщо ядро з якихось причин не може виконати всі операції, воно не виконує ні однієї; процес припиняє свою роботу доти, поки ця можли­вість не буде надана.

Семафор в LINUX складається з наступних елементів:

– значення семафора;

– ідентифікатор останнього із процесів, що працювали із семафором;

– кількість процесів, що очікують збільшення значення семафора;

– кількість процесів, що очікують моменту, коли значення се­мафора стане рівним 0.

Для створення набору семафорів та одержання доступу до них використається системна функція semget, для виконання різних керую­чих операцій над набором – функція semctl, для роботи зі значеннями семафорів – функція semop.

 

SHMGET

Створення області поділюваної пам’яті або одержання номера дескриптора існуючої області:

int shmget(key_t key, int size, int flag);

id = shmget(key, size, flag);

де id – ідентифікатор області поділюваної пам’яті, key – номер області, size – обсяг області в байтах, flag – параметри створення і права доступу.

Ядро використає key для ведення пошуку в таблиці поділю­ваної пам’яті: якщо підходящий запис виявлений і є дозвіл на доступ, ядро повертає викликаючому процесу зазначений у записі дескриптор. Якщо запис не знайдений і користувач встановив прапор IPC_CREAT, що вказує на необхідність створення нової області, ядро перевіряє знаходження розміру області у встановлених системою межах і ви­діляє область.

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

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

SHMAT

Приєднує область поділюваної пам’яті до віртуального адрес­ного простору процесу:

void *shmat(int id, void *addr, int flag);

virtaddr = shmat(id, addr, flag);



Поделиться:


Последнее изменение этой страницы: 2016-04-26; просмотров: 468; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.144.172.115 (0.195 с.)