Компіляція програм, що містять дві і більш функцій



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

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

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

Припустимо, що в системі Linux встановлений компілятор GNU C - gcc і file1.c, file2.c - два файли, що містять функції С. Тоді наступний командний рядок скомпілює обидва файли і створить виконуваний файл, ім'я за умовчанням якого а.out.

 

$gcc file1.c file2.c

Крім того, при цьому створюються два об'єктні файли, названі file1.o file2.o. Якщо згодом змінити файл file1.c, але залишити file2.c незмінним, можна скомпілювати перший файл і об'єднати його з об'єктною версією другого файлу, скориставшись командою:

$gcc file1.c file2.o

Використання файлів заголовків

Якщо помістити функцію main(), а визначення функцій - в другий, перший файл все ж таки потребуватиме прототипів функцій. Замість того, щоб вводити їх при кожному використанні файлу функції, прототипи функцій можна зберігати у файлі заголовка. Файли заголовків містять визначення функцій - число передаваних аргументів, типи аргументів і повертаного значення. Саме це і робить стандартна бібліотека C, наприклад, поміщаючи прототипи функцій введення/виведення у файл stdio.h. Більшість системних файлів заголовків розташована в каталогах /usr/include або /usr/include/sys.

Автоматизація процесу створення програми
 за допомогою GNU-утиліти make

Утиліта make дозволяє автоматично перекомпілювати програму. Основна ідея утиліти make проста. Їй указуються цільові модулі, що беруть участь в процесі побудови виконуваного файлу, і правила, по яких протікає цей процес. Також задаються залежності, що визначають, коли конкретний цільовий модуль повинен бути перебудований. Крім очевидних цільових модулів повинен також існувати модуль clean. Він призначений для видалення всіх об'єктних файлів, що згенерували, і програм, щоб можна було почати все з початку. Правило для даного модуля включає команду rm, що видаляє перераховані файли:

 

clean:

rm –f  *.o <имя исполнимого файла>

Щоб передати всю інформацію про процес створення програми утиліті make, необхідно створити файл Makefile. У ньому цільові модулі перераховуються зліва. За ім'ям модуля слідує двокрапка і існуючі залежності. У наступному рядку указується правило, по якому створюється модуль. Рядок правила повинен починатися з символу табуляції, інакше утиліта make проінтерпретує її неправильно.

 

Приклад:

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

Листинг файлу main.c

#include<stdio.h>

#include "reciprocal.h"

int main(int argc,char **argv)

{

 int i;

 i=atoi(argv[1]);

 printf("i=%d",i);

 printf("The reciprocal of %d is %lf",i,reciprocal(i));

 return 0;

 }

 

Листинг файлу reciprocal.c

#include<stdio.h>

#include<stdlib.h>

#include "reciprocal.h"

double reciprocal(int i)

{

 if(i==0)

 {

printf(" Devided by zero \n");

exit(1);

}

return 1.0/i;}

Листинг файлу reciprocal.h

double reciprocal(int i);

 

Makefile

reciprocal: main.o reciprocal.o

gcc -o reciprocal main.o reciprocal.o

main.o: main.c reciprocal.h

gcc -c main.c

reciprocal.o: reciprocal.c reciprocal.h

gcc -c reciprocal.c

clean:

rm -f *.o reciprocal              

 

 

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

1. Що є багатофайлова програма на мові С?

2. Як створюються багатофайлові програми?

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

4. Для чого призначена GNU-утиліта make?

5. Що є Makefile і які вимоги по його складанню?

Лабораторна робота № 6

Взаємодія процесів. Розподілювана пам’ять. Семафори

Проведення заняття розраховано на 4 години

 

Мета роботи:отримати практичні навички навички по організації міжпроцесовоої взаємодії на основі використання розподілюваної пам’яті і семафорів.

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

Для синхронізації процесів, а точніше, для синхронізації доступу декількох процесів до ресурсів, що розділяються, використовуються семафори. Будучи однією з форм IPC, семафори не призначені для обміну великими об'ємами даних, як у разі FIFO або черг повідомлень. Натомість, вони виконують функцію, повністю відповідну своїй назві – дозволяти або забороняти процесу використання того або іншого ресурсу, що розділяється.

Застосування семафорів пояснимо на простому прикладі. Припустимо, є ресурс, що розділяється (наприклад, файл). Необхідно блокувати доступ до ресурсу для інших процесів, коли якийсь процес проводить операцію над ресурсом (наприклад, записує у файл). Для цього пов'яжемо з даним ресурсом якусь цілочисельну величину – лічильник, доступний для всіх процесів. Приймемо, що значення 1 лічильника означає доступність ресурсу, 0 ‑ його недоступність. Тоді перед початком роботи з  ресурсом процес повинен перевірити значення лічильника. Якщо воно дорівнює нулю – ресурс зайнятий і операція недопустима – процесу залишається чекати. Якщо значення лічильника дорівнює 1 - можна працювати з  ресурсом. Для цього, перш за все, необхідно заблокувати ресурс, тобто змінити значення лічильника на 0. Після виконання операції для звільнення ресурсу значення лічильника необхідно змінити на 1. У приведеному прикладі лічильник грає роль семафора.

Для нормальної роботи необхідно забезпечити виконання наступних умов:

1. Значення семафора повинне бути доступно різним процесам. Тому семафор знаходиться не в адресному просторі процесу, а в адресному просторі ядра.

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

Таким чином, семафори є системним ресурсом, дії над яким проводяться через інтерфейс системних викликів. Семафори в System V володіють наступними характеристиками:

· Семафор є не одним лічильником, а групою, що складається з декількох лічильників, об'єднаних ознаками (наприклад, дескриптором об'єкту, правами доступу і т. д.)

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

· Для кожної групи семафорів (надалі ми називатимемо групу просто семафором) ядро підтримує структуру даних semid_ds, що включає наступні поля:

struct                 ipc_perm sem_perm Опис прав доступу
struct                 sem *sem_base        Покажчик на перший елемент масиву

                                                                       семафорів
ushort               sem_nsems                  Число семафорів в групі

time_t               sem_otime                   Час останньої операції

Значення конкретного семафора з набору зберігається у внутрішній структурі sem:

ushort               semval                          Значення семафора
pid_t                 semid                            Ідентифікатор процесу, що

виконав останню

операцію над семафором.

ushort               semncnt                        Число процесів, чекаючих

збільшення значення семафора.

ushort               semzcnt                        Число процесів, чекаючих

обнулення семафора.

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

Для отримання доступу до семафора (і для його створення, якщо він не існує) використовується системний виклик semget():

#include<sys/types.h>

#include<sys/ipc.h>

#include<sys/sem.h>

int semget(key_t key,int nsems,int semflag);

У разі успішного завершення операції функція повертає дескриптор об'єкту, у разі невдачі ‑ -1.

Аргумент nsems задає число семафорів в групі. У разі, коли ми не створюємо, а лише дістаємо доступ до існуючого семафора, цей аргумент ігнорується. Аргумент semflag визначає права доступу до семафора і прапорці для його створення (IPC_CREAT, IPC_EXCL).

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

#include<sys/types.h>

#include<sys/ipc.h>

#include<sys/sem.h>

int semop(int semid,struct sembuf *semop,size_t nops);

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

Кожен елемент набору операцій semop має вигляд вид:

struct sembuf {

                                 short  sem_num;       /* номер семафора в групі */

                                 short  sem_op;           /* операція */

                                 short  sem_flg;           /* прапори операції */

                                 };

UNIX допускає три можливі операції над семафором, визначувані полем semop:

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

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

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

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

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

Таким чином, при роботі з семафорами процеси використовують різні комбінаціїз трьох операцій, визначених системою, по-своєму трактуючи значення семафорів.

Як приклад, розглянемо два випадки використання бінарного семафора (тобто значення якого можуть приймати тільки 0 або 1). У першому прикладі значення 0 є таким, що відпирає, а 1 замикає деякий ресурс (файл, пам'ять, що розділяється, і т. п.), асоціюється з семафором. Визначимо операції, що замикають ресурс і що звільняють його:

static struct sembuf sop_lock[2]={

        0,0,0                        /* чекати обнулення семафора */

        0,1,0                        /* потім збільшити значення семафора на 1*/

};

static struct sembuf sop_unlock[1]={

        0-1,0                        /*обнулить значення семафора*/

};

Отже, для замикання ресурсу процес проводить виклик:

semop(semid,&sop_lock[0],2);

що забезпечує атомарне виконання двох операцій:

1. Очікування доступності ресурсу. У випадку, якщо ресурс вже зайнятий (значення семафора рівне 1), виконання процесу буде припинено до звільнення ресурсу (значення семафора рівне 0).

2. Замикання ресурсу. Значення семафора встановлюється рівним 1.

Для звільнення ресурсу процес повинен провести виклик:

semop(semid &sop_unlock[0],1);

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

У другому прикладі змінимо трактування значень семафора: значенню 1 семафора відповідає доступність деякого асоційованого з семафором ресурсу, а нульовому значенню – його недоступність. В цьому випадку зміст операцій дещо зміниться.

static struct sembuf sop_lock[2]={

        0-1,0,       /* очікувати вирішуючого сигналу(1), потім обнулити семафор*/

};

static struct sembuf sop_unlock[1]={

        0,1,0        /* з більшити значення семафора на 1*/

};

Процес замикає ресурс викликом:

semop(semid,&sop_lock[0],1);
а звільняє|:

semop(semid,&sop_unlock[0],1);

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

Можна запропонувати наступне рішення даної проблеми:

/*Створюємо семафор, якщо він вже існує semget() повертає помилку, оскільки вказаний прапор IPC_EXCL*/

if((semid=senget(key,nsems,perms|IPC_CREAT|IPC_EXCL))<0)

{

if(errno==EEXIST)

  {

    /*Дійсно, помилка викликана існуванням об’єкта */

        if((semid=senget(key,nsems,perms))<0)

             return (-1); /* Можливо не вистачає системних ресурсів */

    }

else return (-1); /* Можливо, не вистачає системних ресурсів */

}

/*Якщо семафор створений нами, проініціалізуємо його*/

else semop(semid,&sop_unlock[0],1);

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

1. Як відомо, в UNIX процеси виконуються у власному адресному просторі і, по суті, ізольовані один від одного. Проте від ізольованого процесу мало користі. Сама концепція UNIX полягає в модульності, тобто заснована на взаємодії окремих процесів. Поясніть, що потрібно забезпечити для реалізації взаємодії процесів. Для вирішення яких завдань потрібна міжпроцесова взаємодія?

2. Перерахуйте основні засоби міжпроцесової взаємодії.

3. Процес виділяє сегмент пам'яті за допомогою функції shmget(). Поясніть призначення аргументів функції.

4. Яке призначення і синтаксис функцій shmat() і shmctl()?

5. Яку роль в міжпроцесовій взаємодії грають семафори?

6. Які операції допустимі над семафорами? Поясніть.

7.Які системні виклики використовуються при виконанні операцій над семафорами?

Завдання:Привести приклад програми, що ілюструє методику сумісного використання пам'яті двома процесами. Семафори використовувати як засоби синхронізації.

 

Лабораторна робота № 7

Взаємодія процесів. Програмні канали

Проведення заняття розраховано на 2 години

 

Мета роботи: отримати практичні навички навички по організації міжпроцесової взаємодії на основі використання каналів.

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

Канал – це програмна реалізація комунікаційного пристрою, що допускає однонаправну взаємодію. Дані, записувані на “вхідному” кінці каналу, читаються на “вихідному” кінці. Канали є являються послідовними пристроями: дані читаються в тому порядку, в якому були записані. Канал звичайно використовується як засіб зв'язку між двома потоками одного процесу або між батьківським і дочірним процесами. Інформаційна місткість ємкість каналу обмежена. Якщо процес, що пише, поміщає дані в канал швидше, ніж той,що читає, процес їх витягує, і буфер каналу переповнюється, то процес, що пише, блокується до тих пір, поки буфер не звільниться. І навпаки: якщо процес, що читає, звертається до каналу, в який ще не встигли поступити дані, він блокується в очікуванні даних. Таким чином, канал автоматично синхронізує обидва процеси.

Канал створюється за допомогою функції pipe(), яка повертає 0 у разі успіху і –1 - інакше.

Їй необхідно передати масив з двох цілих чисел:

· у елементі з індексом 0 функція зберігає дескриптор файлу, відповідного вихідному кінцю каналу;

· у елементіз індексом 1 зберігається дескриптор файлу, відповідного вхідному кінцю каналу.

Часто буває необхідно перенаправити стандартні потоки введення, виведення виведення і помилок, з'єднавши їх з одним з прикінців каналу. В цьому випадку використовується функція dup2(), яка робить один файловий дескриптор рівним іншому. От як, наприклад, можна пов'язати стандартний вхідний потік з файлом fd:

dup2(fd STDIN_FILENO);

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

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

Лабораторна робота № 8


Дата добавления: 2018-04-04; просмотров: 427; Мы поможем в написании вашей работы!

Поделиться с друзьями:






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