Tutorial 2 - Procesy, sygnały i deskryptory #
Ten tutorial zawiera wyjaśnienia działania funkcji wymaganych na laboratoriach oraz ich parametrów. Jest to jednak wciąż zbiór jedynie poglądowy najważniejszych informacji – należy koniecznie przeczytać wskazane strony manuala, aby dobrze poznać i zrozumieć wszystkie szczegóły.
Zarządzanie procesami #
Tworzenie procesów #
Stworzenie procesu potomnego wykonawane jest za pomocą polecenia fork. Przyjrzymy się definicji tej funkcji:
pid_t fork(void)
Jak widać, zwraca ona obiekt typu pid_t, jest to typ całkowitoliczbowy ze znakiem. W procesie rodzica funkcja zwraca identyfikator nowo utworzonego procesu, natomiast w utworzonym procesie zwracane jest 0, pozwala to łatwo rozdzielić logikę na tę wykonywaną przez dzieci oraz na tę wykonywaną prez rodzica.
Oczywiście stworzenie nowego procesu może się nie powieść (np. gdy systemowi zabraknie potrzebnych zasobów). W takim wypadku funkcja fork zwraca wartość -1 i ustawia odpowiednią wartość zmiennej errno.
Procesy utworzone przez dany proces nazywamy jego dziećmi, natomiast z perspektywy procesu potomnego, proces który go stworzył nazywamy rodzicem.
Więcej informacji można znaleźć w manualu:
man 3p fork
Identyfikacja procesów #
Każdy proces posiada unikatowy identyfikator typu pid_t.
Aby uzyskać informację o identyfikatorze procesu używamy funkcji getpid(), natomiast aby dowiedzieć się jaki jest idenyfikator procesu rodzica funkcji getppid().
Ich definicje są następujące:
pid_t getpid(void)
pid_t getppid(void)
Jak widać nie przyjmują one żadnych argumentów i zwracają obiekt typu pid_t.:
Zgodnie ze standardem POSIX obie funkcję zawszę kończą się sukcesem (man 3p getpid).
Zadanie #
Napisz program tworzący n procesów potomnych (n jest parametrem na pozycji 1), każdy z tych procesów czeka przez losowy czas [5-10] sekund po czym wypisuje na ekran swój PID i się kończy. Proces rodzica co 3s ma wyświetlać na stdout ile jeszcze posiada pod-procesów. Narazie nie przejmuj się czekaniem na zakończenie procesów potomnych.
Nowe strony z manuala:
- man 3p fork
- man 3p getpid
- man 3p sleep
- Job Control
Rozwiązanie zadania #
rozwiązanie 1 etap prog13a.c:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define ERR(source) \
(fprintf(stderr, "%s:%d\n", __FILE__, __LINE__), perror(source), kill(0, SIGKILL), exit(EXIT_FAILURE))
void child_work(int i)
{
srand(time(NULL) * getpid());
int t = 5 + rand() % (10 - 5 + 1);
sleep(t);
printf("PROCESS with pid %d terminates\n", getpid());
}
void create_children(int n)
{
pid_t s;
for (int i = 0; i < n; ++i)
{
if ((s = fork()) < 0)
ERR("Fork:");
if (s == 0)
{
child_work(n);
exit(EXIT_SUCCESS);
}
}
}
void usage(char *name)
{
fprintf(stderr, "USAGE: %s 0<n\n", name);
exit(EXIT_FAILURE);
}
int main(int argc, char **argv)
{
if (argc < 2)
usage(argv[0]);
int child_count = atoi(argv[1]);
if (child_count <= 0)
usage(argv[0]);
create_children(child_count);
return EXIT_SUCCESS;
}
Uwagi i pytania #
Upewnij się, że wiesz jak powstaje grupa procesów tzn. kiedy powłoka tworzy nową grupę i jakie procesy do niej należą.
Zwróć uwagę, że w makrze
ERRdodanokill(0, SIGKILL). Chodzi o to aby w razie błędu zatrzymać cały program (wszystkie procesy danej grupy).0jako argumentkilljest bardzo użyteczne, odnosi się do wszystkich procesów danej grupy, nie musimy przechowywać listy PID’ów.Zwróć uwagę, że nie analizujemy błędów funkcji
killw makrzeERR, to dla tego, że w przypadku sytuacji krytycznej podejmujemy jak najmniej działań, zresztą co mielibyśmy zrobić wywołać rekurencyjnieERR?Pytanie czemu po uruchomieniu wraca linia poleceń?
Odpowiedź
Proces rodzic nie czeka na zakończenie procesów potomnych, brak wywołania `wait` lub `waitpid`, naprawimy to w 2 etapieJak sprawdzić kto jest rodzicem procesów prog13a? Dlaczego właśnie ten proces?
Odpowiedź
Zaraz po uruchomieniu wykonać np.: `$ps -f`, rodzicem procesów będzie proces o PID 1 czyli init/systemd. Dzieje się tak dlatego, że proces rodzic kończy się bez czekania na procesy potomne a te nie mogą "wisieć" poza drzewem procesów. Nie przechodzą pod powłokę ale właśnie pod proces po PID=1Zwróć uwagę na miejsce i sposób ustalenia ziarna liczb losowych. Czy jeśli przesuniemy srand z procesu dziecka do rodzica to zmieni to działanie programu? Jak?
Odpowiedź
Procesy potomne będą losowały te same liczby ponieważ zaczynają z tym samym ziarnem, tak być nie może.Czy można jako argument
srandpodaćtime()zamiastgetpid()?Odpowiedź
Nie można, wywołania time() odbędą się w tej samej sekundzie wiec ziarna będą takie same, efekt taki sam jak poprzednio.Zwróć uwagę na losowanie liczby z przedziału
[A,B]. Niby oczywiste, ale warto opracować sobie prosty wzór.Jak zachowa się program bez
exitwywołanym zaraz pochild_workw procesie potomnym?Odpowiedź
Procesy potomne po zakończeniu wykonania kodu funkcji child_work przechodzą dalej w kodzie czyli wracają do pętli forkującej i tworzą własne procesy potomne, które to też mogą utworzyć swoje potomne itd. Niezły bałagan, do tego procesy dzieci nie czekają na swoje dzieci.Ile procesów (w sumie) zostanie utworzonych jeśli jako parametr uruchomienia podamy 3 przy założeniu, że nie ma
exitwspomnianego w poprzenim pytaniu?Odpowiedź
1 rodzic 3 potomne 1 poziomu, 3 drugiego i 1 trzeciego poziomu, w sumie 8 procesów, narysuj sobie drzewko, opisz gałęzie wartością n w momencie wykonania forka.Co zwraca
sleep? Czy powinniśmy jakoś na to reagować?Odpowiedź
zwraca czas "niedospany" w przypadku przerwania funkcją obsługi sygnału w tym programie proces dziecka nie dostaje sygnałów i ich nie obsługuje więc nie musimy się tym martwić. W innych programach często będziemy restartować sleep z nowym czasem spania tak aby w sumie przespać tyle ile było założone.W kolejnym etapie dodamy czekanie i zliczanie procesów potomnych. Pytanie skąd mamy wiedzieć ile procesów potomnych w danej chwili istnieje?
Odpowiedź
Można próbować zliczać sygnały SIGCHLD ale to zawodny sposób bo mogą się "sklejać" czyli mniej ich dostaniemy niż potomków się na prawdę zakończyło. Jedyną pewną metodą jest zliczanie udanych wywołań wait i waitpid.
Czekanie na procesy potomne #
Po zakończeniu wykonywania wszystkich swoich instrukcji proces potomny przechodzi w stan zombie (jego identyfikator dalej widnieje w tablicy procesów) i pozostaje w nim do momentu, aż proces rodzic odbierze informację o jego stanie (Status Information z man 3p wait). Dopiero wtedy zasoby procesu potomnego są całkowicie zwalniane z systemu.
Rodzic może odebrać informację o stanie wiszącego pod procesu korzystając z funkcji wait czekającą na dowolny proces potomny lub funkcji waitpid, która umożliwia określenie na które procesy potomne czekamy.
Przyjrzyjmy się ich definicjom:
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);
Jak można zauważyć, obie funkcje zwracają obiekt typu pid_t będący identyfikatorem procesu, o którego stanie dostaniemy informację.
Obie funkcje przyjmują argument stat_loc typu int*, który wskazuje miejsce w pamięci, do którego ma zostać zapisana informacja o odczytanym stanie procesu (jeżeli informacja ta nie będzie nam potrzebna możemy wstawić w to miejsce NULL).
Funkcja waitpid posiada dwa dodatkowe argumenty, kolejno pid typu pid_t oraz options typu int.
Argument pid określa, na które procesy chcemy czekać. Dla różnych wartości funkcja zachowuje się następująco:
pid == -1- czekamy na dowolny proces potomny.pid > 0- czekamy na proces o identyfikatorze równympid.pid == 0- czekamy na dowolny proces należący do grupy procesu wywołującego.pid < -1czekamy na dowolny proces którego identyfikator grupy jest równy wartości bezwzględnejpid.
argument options określa modyfikacje sposobu działania funkcji, i jest kombinacją następujących opcji:
WCONTINUED- funkcja powinna zwrócić również informacje o procesach, które zostały wznowione po zatrzymaniu.WNOHANG- funkcjawaitpidnie powinna zatrzymywać wywołania biężącego procesu jeżeli żaden z procesów na które czekamy nie może natychmiast powiadomić o swoim statusie. W takiej sytuacji funkcja zwaraca wartość0.WUNTRACED- funkcja powinna zwrócić również informacje o procesach, które zostały zatrzymane.
W ramach laboratorium wystarczy znajomość opcji WNOHANG.
Więcej informacji można znaleźć w manualu:
man 3p wait
Podsumowując możemy traktować funkcję waitpid jako bardziej rozbudowaną wersję funkcji wait - wywołanie funkcji wait(stat_loc) jest równażne wywołaniu waitpid(-1, stat_loc, 0).
Oczywiście obie te funkcję mogą się nie powieść, zwracają one wtedy -1 i ustawiają odpowiednią wartość zmiennej errno.
Uwaga: Jeżeli wywołamy funkcję wait lub waitpid i pula procesów potomnych na, które możemy czekać będzie pusta funkcja zwróci -1 i ustawi wartość zmiennej errno na ECHILD. Warto z tego skorzystać by mieć pewność że nie osierocimy żadnych procesów przed zakończeniem procesu.
Zadanie #
Rozbuduj program z poprzedniego zadania o poprawne czekanie na procesy potomne.
Nowe strony z manuala:
man 3p waitman 3p waitpid
Rozwiązanie zadania #
rozwiązanie prog13b.c:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define ERR(source) \
(fprintf(stderr, "%s:%d\n", __FILE__, __LINE__), perror(source), kill(0, SIGKILL), exit(EXIT_FAILURE))
void child_work(int i)
{
srand(time(NULL) * getpid());
int t = 5 + rand() % (10 - 5 + 1);
sleep(t);
printf("PROCESS with pid %d terminates\n", getpid());
}
void create_children(int n)
{
pid_t s;
for (int i = 0; i < n; ++i)
{
if ((s = fork()) < 0)
ERR("Fork:");
if (s == 0)
{
child_work(n);
exit(EXIT_SUCCESS);
}
}
}
void usage(char *name)
{
fprintf(stderr, "USAGE: %s 0<n\n", name);
exit(EXIT_FAILURE);
}
int main(int argc, char **argv)
{
if (argc < 2)
usage(argv[0]);
int n = atoi(argv[1]);
if (n <= 0)
usage(argv[0]);
create_children(n);
while (n > 0)
{
sleep(3);
pid_t pid;
while (1)
{
pid = waitpid(0, NULL, WNOHANG);
if (pid > 0)
n--;
if (0 == pid)
break;
if (0 >= pid)
{
if (ECHILD == errno)
break;
ERR("waitpid:");
}
}
printf("PARENT: %d processes remain\n", n);
}
return EXIT_SUCCESS;
}
Uwagi i pytania #
Koniecznie trzeba rozróżnić kiedy waitpid informuje nas o chwilowym braku zakończonych potomków (wartość zwracana zero) od permanentnego braku potomków (błąd
ECHILD). Ten ostatni przypadek nie jest w zasadzie błędem, tę sytuację trzeba normalnie obsłużyć.Czemu wołamy
waitpidw pętli?Odpowiedź
Nie wiemy z góry ile procesów zombie czeka, może być 0 , 1 , 2 aż do nCzemu wołamy waitpid z flagą
WNOHANG?Odpowiedź
Nie chcemy czekać nie wiadomo ile na jeszcze żyjące procesy, działamy w pętli, która co 3 sekundy ma wypisać daneCzemu wołamy
waitpidz pidem zero?Odpowiedź
Czekamy na dowolny proces potomny, nie musimy znać jego PID, zero oznacza dowolny z potomków.Czy w tym programie występują sygnały?
Odpowiedź
`SIGCHILD`, nie ma f. obsługi ale to nie szkodzi, w pewnym sensie jest obsługiwany przez waitpidCzy tym razem nie powinniśmy sprawdzać co zwraca sleep skoro są sygnały?
Odpowiedź
Nie bo nie ma funkcji obsługi sygnału.
Sygnały #
Sygnały (ang. signals) są asynchronicznym mechanizmem obsługi zdarzeń w systemach operacyjnych z rodziny Unix. Umożliwiając powiadamianie procesów o wystąpieniu określonych zdarzeń systemowych, wyjątków lub żądań sterowania wykonaniem.
Wysyłanie sygnałów #
Do wysyłania sygnałów służy funkcja kill.
#include <signal.h>
int kill(pid_t pid, int sig);
Argument pid określa, do którego procesu lub grupy procesów kierowany jest sygnał:
pid > 0- sygnał jest wysyłany do procesu o PID równympidpid = 0- sygnał wysyłany jest do procesów należących do grupy procesów nadawcy (nadawca również otrzymuje sygnał)pid = -1- sygnał wysyłany do wszystkich procesów, do których nadawca ma uprawnienia (w tym do samego siebie)pid < -1- sygnał wysyłany jest do procesów o identyfikatorze grupy równym co do modułupid
Argument sig specyfikuje jaki sygnał powinien być wysłany. Może on przyjmować następujące wartości:
- jedno z makr zdefiniowanych w pliku nagłówkowym
<signal.h>jak np.SIGTERM,SIGKILLczySIGUSR1. Pełną liste można znaleźć w manualu
man 7 signal
- wartość zero - w takim przypadku żaden sygnał nie zostanie wysłany, a jedynie zajdzie sprawdzenie potencjalnych błędów wykonania funkcji.
Funkcja zwraca 0 w przypadku poprawnego wykonania. W przeciwnym wypadu zwracana jest wartość -1 i ustawiana jest odpowiednia wartość zmiennej errno.
Więcej informacji można znaleźć w manualu
man 3p kill
Obsługa sygnałów #
Każdy sygnał posiada swój domyślny sposób obsługi przez proces. Liste sygnałów i ich domyślny sposób obsługi możesz sprawdzić w manualu
man 7 signal
Sposób obsługi danego sygnału jesteśmy w stanie sprawdzić lub zmienić za pomocą funkcji
#include <signal.h>
int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);
Argumenty:
sigspecyfikuje jaki o jaki sygnał nam chodzi i przyjmuje wartości makr z nagłówka<signal.h>.actmoże ustawić nowy sposób obsługi sygnału jeśli wskazuje na strukture typusigaction. Jeśli wartość jest równaNULL, to obsługa się nie zmienia.oact- jeśli przy wywołaniu jest ustawiony naNULL, to argument ten jest ignorowany. W przeciwnym wypadku struktura na którą wskazuje ten wskaźnik jest ustawiana na stary sposób obsługi sygnału (gdyactnie jestNULLem) lub aktualny (gdyactjestNULLem)
Zgodnie z POSIXem struktura sigaction musi posiadać co najmniej następujące pola:
void(*) (int) sa_handler- wskaźnik na funkcje obsługującą sygnał lub jedna z wartościSIG_IGNlubSIG_DFL. Funkcja obsługująca musi przyjmowaćint(kod obsługiwanego sygnału) i nic nie zwracać. MakroSIG_IGNoznacza, że sygnał będzie ignorowany, aSIG_DFLdomyślną obsługę sygnału.sigset_t sa_mask- zbiór sygnałów, które będą blokowane na czas wywołania funkcji obsługującej sygnałint sa_flags- specjalne flagi modyfikujące zachowanie sygnałuvoid(*) (int, siginfo_t *, void *) sa_sigaction- wskaźnik na funkcje obsługującą sygnał. Różni się odsa_handleprzyjmowanymi argumentami. Domyślnie wywoływana jest funkcjasa_handle, aby to zmienić należy dosa_flagsdodać flagąSA_SIGINFO.
Funkcja zwraca 0 w przypadku poprawnego wykonania. W przeciwnym wypadu zwracana jest wartość -1 i ustawiana jest odpowiednia wartość zmiennej errno.
Należy zaznaczyć, że procesy dzieci stworzone za pomocą funkcji fork dziedziczą sposób obsługi sygnału.
Więcej informacji można znaleźć w manualu
man 3p sigaction
Zadanie #
Program przyjmuje 4 parametry pozycyjne (n,k,p i r). Tworzy n procesów potomnych. Proces rodzic wysyła
naprzemiennie sygnały SIGUSR1 i SIGUSR2 do wszystkich procesów potomnych w pętli po odpowiednio k i p sekundach. Kończy
się gdy kończą się wszystkie procesy potomne. Każdy proces potomny losuje czas swojego spania z przedziału 5-10 sekund a
następnie w pętli śpi i wypisuje na ekran SUKCES jeśli ostatnim otrzymanym przez niego sygnałem był SIGUSR1 lub FAILURE
jeśli SIGUSER2. Taka pętla powtarza się r razy.
Co student musi wiedzieć:
man 7 signalman 3p sigactionman 3p nanosleepman 3p alarmman 3p memsetman 3p kill
Rozwiązanie zadania #
plik prog14.c:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define ERR(source) \
(fprintf(stderr, "%s:%d\n", __FILE__, __LINE__), perror(source), kill(0, SIGKILL), exit(EXIT_FAILURE))
volatile sig_atomic_t last_signal = 0;
void sethandler(void (*f)(int), int sigNo)
{
struct sigaction act;
memset(&act, 0, sizeof(struct sigaction));
act.sa_handler = f;
if (-1 == sigaction(sigNo, &act, NULL))
ERR("sigaction");
}
void sig_handler(int sig)
{
printf("[%d] received signal %d\n", getpid(), sig);
last_signal = sig;
}
void sigchld_handler(int sig)
{
pid_t pid;
while (1)
{
pid = waitpid(0, NULL, WNOHANG);
if (pid == 0)
return;
if (pid <= 0)
{
if (errno == ECHILD)
return;
ERR("waitpid");
}
}
}
void child_work(int l)
{
int t, tt;
srand(getpid());
t = rand() % 6 + 5;
for (int i = 0; i < l; i++)
{
for (tt = t; tt > 0; tt = sleep(tt))
{
}
if (last_signal == SIGUSR1)
printf("Success [%d]\n", getpid());
else
printf("Failed [%d]\n", getpid());
}
printf("[%d] Terminates \n", getpid());
}
void parent_work(int k, int p, int l)
{
struct timespec tk = {k, 0};
struct timespec tp = {p, 0};
sethandler(sig_handler, SIGALRM);
alarm(l * 10);
while (last_signal != SIGALRM)
{
nanosleep(&tk, NULL);
if (kill(0, SIGUSR1) < 0)
ERR("kill");
nanosleep(&tp, NULL);
if (kill(0, SIGUSR2) < 0)
ERR("kill");
}
printf("[PARENT] Terminates \n");
}
void create_children(int n, int l)
{
for (int i = 0; i < n; i++)
{
switch (fork())
{
case 0:
sethandler(sig_handler, SIGUSR1);
sethandler(sig_handler, SIGUSR2);
child_work(l);
exit(EXIT_SUCCESS);
case -1:
perror("Fork:");
exit(EXIT_FAILURE);
}
}
}
void usage(void)
{
fprintf(stderr, "USAGE: signals n k p l\n");
fprintf(stderr, "n - number of children\n");
fprintf(stderr, "k - Interval before SIGUSR1\n");
fprintf(stderr, "p - Interval before SIGUSR2\n");
fprintf(stderr, "l - lifetime of child in cycles\n");
exit(EXIT_FAILURE);
}
int main(int argc, char **argv)
{
int n, k, p, l;
if (argc != 5)
usage();
n = atoi(argv[1]);
k = atoi(argv[2]);
p = atoi(argv[3]);
l = atoi(argv[4]);
if (n <= 0 || k <= 0 || p <= 0 || l <= 0)
usage();
sethandler(sigchld_handler, SIGCHLD);
sethandler(SIG_IGN, SIGUSR1);
sethandler(SIG_IGN, SIGUSR2);
create_children(n, l);
parent_work(k, p, l);
while (wait(NULL) > 0)
{
}
return EXIT_SUCCESS;
}
Do komunikacji pomiędzy funkcją obsługi sygnału a resztą programu musimy użyć zmiennych globalnych, proszę pamiętać, że jest to sytuacja wyjątkowa, zmienne globalne są jako takie niepożądane oraz, co powinno być oczywiste, ale czasem się studentom myli - nie są współdzielone pomiędzy procesami pokrewnymi.
Typ zmiennej globalnej last_signal nie jest przypadkowy, co więcej jest to jedyny BEZPIECZNY i POPRAWNY typ. Wynika to z
asynchronicznej natury wywołania funkcji obsługi sygnału a dokładniej:
volatileoznacza wyłączenie optymizacji kompilatora, ważne żeby kompilator nie uznał wartości zmiennej za stałą bo jej zmiany nie wynikają z kodu i tak mogłoby się okazać, że czytelna dla nas pętlawhile(work)gdzieworkjest zmienną globalną zmienia się nawhile(1)po optymizacji.sig_atomic_toznacza największy typ numeryczny, który jest przetwarzany w pojedynczej instrukcji CPU. Jeśli weźmiemy większy typ numeryczny przerwanie obsługą sygnału może zakłócić wartość wynikową nawet dla przykładowego prostego porównaniaa==0o ile przerwanie wypadnie w trakcie porównania i zmieni już porównane bajty.
Z powyższego wynika, że nie przekazujemy pomiędzy funkcją obsługi a głównym kodem nic poza prostymi liczbami, stanami. Do tego dochodzi dobra praktyka nie przerywania programu na zbyt długo co pozostawia nam bardzo mało poprawnych, przenośnych i bezpiecznych rozwiązań w kwestii jak dzielić logikę programu pomiędzy główny kod a funkcję obsługi sygnału. Najprostsza zasada aby funkcje obsługi były ekstremalnie krótkie (przypisanie, inkrementacja zmiennej itp) a cała logika pozostała w głównym kodzie jest najlepsza.
Funkcja memset bywa konieczna a zazwyczaj jest użyteczna przy inicjowaniu nie w pełni znanych nam struktur. W tym przypadku POSIX wyraźnie mówi, że struktura sigaction może zawierać więcej pól niż jest to wymagane przez standard. W takim przypadku te dodatkowe pola, których wartości nie ustawilibyśmy (tutaj ze zerujemy za pomocą memset) mogą skutkować różnym działaniem na różnych systemach, a nawet różnym zachowaniem po między wywołaniami programu.
Czy podczas obsługi sygnału SIGCHLD można się spodziewać więcej niż jednego zakończonego procesu dziecka?Odpowiedź
Czy podczas obsługi sygnału SIGCHLD można się spodziewać braku zakończonego procesu dziecka? Zerknij na zakończenie main.Odpowiedź
Pamiętaj o możliwym KONFLIKCIE sleep i alarm - wg. POSIX sleep może używać w implementacji SIGALRM a nie ma jak
zagnieżdżać sygnałów, nigdy zatem w kodzie oczekującym na alarm nie używamy sleep, można za to użyć nanosleep tak jak w kodzie powyżej.
W wysyłaniu sygnałów (kill) pojawia się jako PID zero, dzięki temu nie musimy znać pidów procesów potomnych ale też wysyłamy sygnał sami do siebie!
Miejsca ustawienia obsługi sygnałów i ich blokowania w tym programie są bardzo ważne, zwróć uwagę jak to działa i odpowiedz na pytania poniżej. Pamiętaj aby zawsze dobrze przemyśleć kolejność tych ustawień w swoim programie, z tym jest związanych sporo błędów w pracach studentów!
Zwróć uwagę na sleep, czemu jest w pętli? Czy czas spania jest/mógłby być dokładny?Odpowiedź
Jaka jest domyślna dyspozycja znakomitej większości sygnałów (w tym SIGUSR1 i SIGUSR2)?Odpowiedź
Jaka jest konsekwencja wysyłana przez proces rodzic sygnałów SIGUSR1/2 do całej grupy procesów?Odpowiedź
Co by się stało, gdyby nie było włączone ignorowanie SIGUSR1 i SIGUSR2 w procesie rodzicu?Odpowiedź
Czy można przesunąć ustawienie ignorowania sygnałów za funkcję create_children? Procesy potomne przecież nie potrzebują tego ignorowania, mają od razu po starcie ustawianą funkcję obsługi?Odpowiedź
Czy można jakoś zmienić ten program tak aby wykluczyć ignorowanie sygnałów SIGUSR1 i SIGUSR2?Odpowiedź
A co się stanie jeśli za fork przeniesiemy obsługę SIGCHLD?Odpowiedź
Pytanie, czy wait na końcu main jest potrzebny? Przecież i tak funkcja parent_work() powinna działać co najmniej tyle czasu co najdłuższy z podprocesów?Odpowiedź
Czekanie na sygnał #
Często podczas pisania programów natkniemy sie na sytuację, w której proces, zanim wykona prace musi zostać poinformowany o tym, że inny proces zakończył swoje zadanie. Jak się pewnie domyślasz, ten problem można łatwo rozwiązać z wykorzystaniem sygnałów. Inspirując się poprzednim zadaniem, moglibyśmy napisać logikę, która opiera się na tym, że nasz proces śpi w pętli i sprawdza, czy ostatni otrzymany przez niego sygnał to ten, na który czeka. Niestety nie dość, że to rozwiązanie jest nieeleganckie, to jeszcze jest niepoprawne - mogło by dojść do sytuacji że sygnał na który czekamy zostanie “sklejony” z innym sygnałem i tak naprawdę nigdy nie dowiedzielibyśmy się, że nasz proces może rozpocząć pracę. Na szczęście system operacyjny dostarcza nam narzędzia, które pozwalają rozwiązać ten problem.
Aby zablokować program do momentu, gdy otrzyma on sygnał będziemy używać funkcji sigsuspend. Przyjrzyjmy się jej definicji:
int sigsuspend(const sigset_t *sigmask);
Jak możemy zauważyć funkcja ta zwaraca wartość typu int służącą do powiadomienia o potencjalnym błędzie, zwracane wtedy jest -1. Przyjmuje argument sigmask typu const sigset_t * jest to wskaźnik na zbiór sygnałów, na które funkcja będzie czekać.
Działanie tej funkcji jest następujące: ustawia ona maskę sygnałów na tę podaną w argumencie, czeka na przechwycenie jednego z tych sygnałów, po czym przywraca poprzednią maskę sygnałów i wznawia wykonywanie procesu.
Więcej informacji można znaleźć w manualu:
man 3p sigsuspend
Zarządzanie maską sygnałów #
Zbiór sygnałów będziemy nazywać maską sygnałów. Maskę sygnałów będziemy przechowywać jako obiekt o typie sigset_t. Standard nie określa w jaki sposób ma być zaimplementowany ten typ, może być to zarówno int, jak i struktura.
W celu modyfikacji maski sygnałów będziemy używac funcji sigsemptyset, inicjalizującej maskę jako zbiór pusty, oraz sigaddset ,dodającej sygnał do maski.
Przyjrzyjmy się ich definicją:
int sigemptyset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
Jak możemy zauważyć, obie funkcję przyjmują jako pierwszy argument set typu sigset_t *, jest to wskaźnik na maskę, którą chcemy edytować.
funkcja sigaddset dodatkowo przyjmuje argument signo będący kodem sygnału, który chcemy dodać do maski.
Obie funkcje zwracają wartość typu int, służącą do sygnalizacji przebiegu operacji: w razie sukcesu zwracają 0, a w razie błędu -1, ustawiając odpowiednią wartość zmiennej errno.
Więcej informacji znajdziesz w manualu:
man 3p sigaddset
man 3p sigemptyset
Zmiana maski sygnałów #
Skoro już zdefiniowaliśmy nową maskę sygnałów, chcielibśmy sprawić, by wpłynęła ona na działanie naszego procesu.
W tym celu będziemy używać funkcji sigprocmask, która określa, w jaki sposób zdefiniowana przez nas maska sygnałów ma wpłynąć na aktulaną maskę sygnałów procesu.
Przyjrzyjmy się jej definicji:
int sigprocmask(int how, const sigset_t *restrict set,
sigset_t *restrict oset);
Jak możemy zauważyć funkcja ta przyjmuje kolejno argumenty:
howtypuintokreśla w jaki sposób nowa maska ma wpłynąć na aktualną maskę. Dostępne opcje:SIG_BLOCK- wynikowa maska sygnałów jest sumą zbiorów maski wskazanej przezseti aktualnej maski sygnałów (określamy jakie sygnały checmy dodać do maski).SIG_SETMASK- wynikowa maska sygnałów jest maską sygnałów wskazywaną przezset.SIG_UNBLOCK- wynikowa maska sygnałów jest przecięciem aktualnej maski i dopełnieiem zbioru maski wskazanej przezset(Określamy jakie sygnały chcemy usunąć z maski).
settypuconst sigset_tto wskaźnik na maskę, na podstawie której chcemy modyfikować poprzednią maskę.osettypusigset_t *jest wskaźnikiem na obiekt do którego chcemy zapisać maskę sygnałów sprzed edycji.
Więcej informacji znajdziesz w manualu:
man 3p pthread_sigmask
Zadanie #
Napisz program, który tworzy jeden pod-proces, który co określony parametrami czas (w mikrosekundach) wysyła do procesu rodzica
SIGUSR1, co n-ty raz (określony parametrami) wysyła SIGUSR2. Proces rodzic czeka na otrzymanie sygnału SIGUSR2. Oba
procesy zliczają i wypisują na bieżąco ilości wysłanych/odebranych SIGUSR2. Używamy część funkcji z poprzedniego
zadania.
Nowe strony z manuala
- man 3p sigsuspend
- Opis metod czekania na sygnał w glibc tutaj
- man 3p pthread_sigmask (tylko opis sigprocmask)
- man 3p sigaddset
- man 3p sigemptyset
Rozwiązanie zadania #
rozwiązanie, plik prog15.c:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define ERR(source) \
(fprintf(stderr, "%s:%d\n", __FILE__, __LINE__), perror(source), kill(0, SIGKILL), exit(EXIT_FAILURE))
volatile sig_atomic_t last_signal = 0;
void sethandler(void (*f)(int), int sigNo)
{
struct sigaction act;
memset(&act, 0, sizeof(struct sigaction));
act.sa_handler = f;
if (-1 == sigaction(sigNo, &act, NULL))
ERR("sigaction");
}
void sig_handler(int sig) { last_signal = sig; }
void sigchld_handler(int sig)
{
pid_t pid;
while (1)
{
pid = waitpid(0, NULL, WNOHANG);
if (pid == 0)
return;
if (pid <= 0)
{
if (errno == ECHILD)
return;
ERR("waitpid");
}
}
}
void child_work(int m, int p)
{
int count = 0;
struct timespec t = {0, m * 10000};
while (1)
{
for (int i = 0; i < p; i++)
{
nanosleep(&t, NULL);
if (kill(getppid(), SIGUSR1))
ERR("kill");
}
nanosleep(&t, NULL);
if (kill(getppid(), SIGUSR2))
ERR("kill");
count++;
printf("[%d] sent %d SIGUSR2\n", getpid(), count);
}
}
void parent_work(sigset_t oldmask)
{
int count = 0;
while (1)
{
last_signal = 0;
while (last_signal != SIGUSR2)
sigsuspend(&oldmask);
count++;
printf("[PARENT] received %d SIGUSR2\n", count);
}
}
void usage(char *name)
{
fprintf(stderr, "USAGE: %s m p\n", name);
fprintf(stderr,
"m - number of 1/1000 milliseconds between signals [1,999], "
"i.e. one milisecond maximum\n");
fprintf(stderr, "p - after p SIGUSR1 send one SIGUSER2 [1,999]\n");
exit(EXIT_FAILURE);
}
int main(int argc, char **argv)
{
if (argc != 3)
usage(argv[0]);
int m = atoi(argv[1]);
int p = atoi(argv[2]);
if (m <= 0 || m > 999 || p <= 0 || p > 999)
usage(argv[0]);
sethandler(sigchld_handler, SIGCHLD);
sethandler(sig_handler, SIGUSR1);
sethandler(sig_handler, SIGUSR2);
sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGUSR1);
sigaddset(&mask, SIGUSR2);
sigprocmask(SIG_BLOCK, &mask, &oldmask);
pid_t pid;
if ((pid = fork()) < 0)
ERR("fork");
if (pid == 0)
child_work(m, p);
else
{
parent_work(oldmask);
while (wait(NULL) > 0)
;
}
sigprocmask(SIG_UNBLOCK, &mask, NULL);
return EXIT_SUCCESS;
}
Uwagi i pytania #
Program działa aż do
SIGINT(C-c)Uruchom z różnymi parametrami, z małą przerwą w mikrosekundach i częstymi
SIGUSR2różnica w licznikach powinna wzrastać szybciej niż gdy te parametry są większe. Za chwilę wyjaśni się skąd te błędy. Jeśli nie obserwujesz błędów spróbuj dać programowi trochę więcej czasu na działanie, około minuty powinno dać już różne wyniki.Program jest celowo napisany tak, aby występowały pewne problemy, można go lepiej napisać i je wyeliminować, proszę miej to na względzie kopiując rozwiązania z tego kodu!
Zwróć uwagę na
getppid, niby oczywiste ale widziałem prace gdzie studenci przekazywali pid rodzica do procesu potomnego.Oczekiwanie na sygnał poprzez sigsuspend jest bardzo ważną technika, którą musisz rozumieć, dobrze omawia to dokumentacja GNU podlinkowana powyżej. Pamiętaj, że zasada jest taka, blokujemy oczekiwane sygnały w całej pętli ( większej części programu) i przywracamy ich dochodzenie tylko wtedy gdy możemy czekać czyli właśnie w wywołaniu sigsuspend. Mamy zatem w programie dobrze określone momenty w których obsługujemy sygnały, to spore ułatwienie.
Jeśli funkcje obsługujące sygnały mogą być wykonywane tylko w czasie nieaktywności programu głównego (czyli jak w tym przykładzie) można nieco rozluźnić zasady tego co wolno przekazywać przez zmienne globalne, oraz te dotyczące długości i czasu ich działania.
Który licznik jest niepoprawny, ten w procesie dziecka czy rodzica?
Odpowiedź
To musi być ten który jest mniejszy, nie mamy w tym kodzie możliwości zliczania nieistniejących sygnałów, możemy tylko gubić te wysłane. Gubić może oczywiście tylko odbiorca. Problem jest w procesie rodzica.Pytanie czemu liczniki się różnią ?
Odpowiedź
1. sklejanie sygnałów ale to ma mały wpływ, 2. fakt, że sigsuspend NIE GWARANTUJE WYKONANIA MAKSYMALNIE JEDNEJ OBSŁUGI SYGNAŁU! To częsty błąd w rozumowaniu! Zaraz po wywołaniu obsługi SIGUSR2 jeszcze w obrębie jednego wykonania sigsuspend następuje obsługa SIGUSR1, zmienna globalna jest nadpisywana i proces rodzic nie ma szansy zliczyć SIGUSR2!!!Jak uruchomić ten program aby zminimalizować szansę na sklejanie się
SIGUSER2i jednocześnie obserwować różnice w licznikach?Odpowiedź
Uruchomić dla małych czasów i dużych ilości SIGUSR1 między SIGUSR2, teraz jest prawie zerowa szansa na sklejenie SIGUSR2, za to duża na wykonanie wiele razy funkcji obsługi sygnału w obrębie jednego sigsuspendPopraw powyższy program tak aby wyeliminować problem wielu wywołań obsługi sygnału w obrębie jednego
sigsuspendOdpowiedź
Można to zrobić np. dodając drugą zmienną globalną tylko do obsługi SIGUSR2, zwiększanie zmiennej count też można przenieść do funkcji obsługi sygnału w ten sposób uniknie się potencjalnego problemu z obsługą dwóch SIGUSR2 w obrębie jednego sigsuspend. Trzeba jeszcze przebudować kod związany z wypisywaniem zmienionego licznika count w rodzicu i gotowe.
Operacje niskopoziomowe na plikach, a sygnały #
W tej części tutoriala na samym początku pokażemy z jakimi problemami można się spotkać przy okazji operacji na plikach przy jednoczesnej obsłudze sygnałów, a następnie pokażemy jak możemy sobie z nimi radzić.
Zadanie #
Zmodyfikować poprzedni program tak, aby proces rodzic odbierał sygnały SIGUSR1 wysyłane co zadany czas (parametr 1) i zliczał je. Dodatkowo proces główny tworzy plik o nazwie podanej jako parametr 4 o zadanej ilości bloków o zadanym rozmiarze (parametry 2 i 3). Zawartość pliku ma pochodzić z /dev/urandom. Każdy blok kopiujemy osobno, kontrolując rozmiary. Po skopiowaniu bloku należy podać na stderr realną ilość przepisanych bloków oraz stan liczników sygnałów.
Co student musi wiedzieć:
man 4 urandom
Niepoprawne rozwiązanie #
plik prog16a.c:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define ERR(source) \
(fprintf(stderr, "%s:%d\n", __FILE__, __LINE__), perror(source), kill(0, SIGKILL), exit(EXIT_FAILURE))
volatile sig_atomic_t sig_count = 0;
void sethandler(void (*f)(int), int sigNo)
{
struct sigaction act;
memset(&act, 0, sizeof(struct sigaction));
act.sa_handler = f;
if (-1 == sigaction(sigNo, &act, NULL))
ERR("sigaction");
}
void sig_handler(int sig) { sig_count++; }
void child_work(int m)
{
struct timespec t = {0, m * 10000};
sethandler(SIG_DFL, SIGUSR1);
while (1)
{
nanosleep(&t, NULL);
if (kill(getppid(), SIGUSR1))
ERR("kill");
}
}
void parent_work(int b, int s, char *name)
{
int i, in, out;
ssize_t count;
char *buf = malloc(s);
if (!buf)
ERR("malloc");
if ((out = open(name, O_WRONLY | O_CREAT | O_TRUNC | O_APPEND, 0777)) < 0)
ERR("open");
if ((in = open("/dev/urandom", O_RDONLY)) < 0)
ERR("open");
for (i = 0; i < b; i++)
{
if ((count = read(in, buf, s)) < 0)
ERR("read");
if ((count = write(out, buf, count)) < 0)
ERR("read");
if (fprintf(stderr, "Block of %ld bytes transferred. Signals RX:%d\n", count, sig_count) < 0)
ERR("fprintf");
}
if (close(in))
ERR("close");
if (close(out))
ERR("close");
free(buf);
if (kill(0, SIGUSR1))
ERR("kill");
}
void usage(char *name)
{
fprintf(stderr, "USAGE: %s m b s \n", name);
fprintf(stderr,
"m - number of 1/1000 milliseconds between signals [1,999], "
"i.e. one milisecond maximum\n");
fprintf(stderr, "b - number of blocks [1,999]\n");
fprintf(stderr, "s - size of of blocks [1,999] in MB\n");
fprintf(stderr, "name of the output file\n");
exit(EXIT_FAILURE);
}
int main(int argc, char **argv)
{
int m, b, s;
char *name;
if (argc != 5)
usage(argv[0]);
m = atoi(argv[1]);
b = atoi(argv[2]);
s = atoi(argv[3]);
name = argv[4];
if (m <= 0 || m > 999 || b <= 0 || b > 999 || s <= 0 || s > 999)
usage(argv[0]);
sethandler(sig_handler, SIGUSR1);
pid_t pid;
if ((pid = fork()) < 0)
ERR("fork");
if (0 == pid)
child_work(m);
else
{
parent_work(b, s * 1024 * 1024, name);
while (wait(NULL) > 0)
{
}
}
return EXIT_SUCCESS;
}
Pamiętaj, z pliku/dev/randommożesz pobrać na prawdę losowe bajty ale w małych ilościach, z/dev/urandomodwrotnie, pseudo losowe liczby za to w dowolnych ilościach.
Zawsze gdy w poprawnym programie pojawia się alokacja pamięci musi być też jej zwalnianie!
Uprawnienia podawane w funkcjiopenmogą być także podane przy użyciu stałych (man 3p mknod), wyjątkowo ze względu na bardzo silne zakorzenienie notacji oktalnej u programistów i administratorów oraz na fakt, że łatwo takie liczby w kodzie wyszukać nie uznajemy tego za błąd stylu tzw. “magic numbers”.
Problemy #
Po wywołaniu programu z parametrami 1 20 40 out.txt powinieneś obserwować następujące problemy:
- Kopiowanie krótszych bloków niż zadano, na moim laptopie nigdy nie przekraczam 33554431 a powinno być 40MB, ale pojawiają się też i krótsze, powód to przerwanie odczytu (W TRAKCIE) obsługą sygnału
- Możliwe wystąpienie błędu
fprintf: Interrupted system call- przerwanie funkcją obsługi sygnału funkcjifprintfZANIM ta coś wyświetli - Analogiczne komunikaty dla
openiclose- może to być trudno zaobserwować w tym programie ale jest to możliwe wg. POSIX - Widać, że zliczamy w rodzicu mniej sygnałów niż wysyła potomek, ponieważ sumowanie odbywa się bezpośrednio w nieblokowanej obsłudze sygnału to łatwo się domyślić, że w grę wchodzi sklejanie się sygnałów, pytanie czemu w tym programie to sklejanie jest aż tak silne?
Odpowiedź
w tej architekturze (GNU/Linux) planista procesora blokuje uruchomienie obsługi sygnału podczas większych operacji IO, w tym czasie sygnały się sklejają.
Uwagi i pytania #
W jakim celu proces rodzic na zakończenie wysyła do całej grupy SIGUSR1?Odpowiedź
Jak proces potomny może się zakończyć po nadejściu SIGUSR1 skoro dziedziczy obsługę tego sygnału?Odpowiedź
Czemu proces rodzic nie zabija się sam tym sygnałem?Odpowiedź
Czy taka strategia może się nie powieść?Odpowiedź
Czy można to jakoś poprawić? Tzn. proces rodzic zawsze zabije potomka ale jednocześnie sam nie narazi się na przedwczesną śmierć?Odpowiedź
Czy taka strategia zakończenia potomka zawsze jest poprawna i łatwa do przeprowadzenia?Odpowiedź
Czemu po wywołaniu alokacji pamięci sprawdzamy przydzielony wskaźnik czy aby nie jest NULL?Odpowiedź
Czy nie dałoby się tego bufora uczynić zmienną automatyczną i uniknąć kodu związanego z alokacją i zwalnianiem?Odpowiedź
Czemu uprawnienia do nowego pliku są pełne (0777)?Odpowiedź
Rozwiązanie problemów #
W przypadu operacji I/O funkcje mogą być przerwane podczas swojego działania przez funkcję obługi sygnału. W takim wypadku funkcje zwracają wartość -1, która sygnalizuje błąd i ustwiają errno na EINTR. Standard POSIX mówi, że w takim przypadku wykananie funkcji zostaje przerwana zanim ta funkcja coś zrobi. Z tego powodu jak najbardziej poprawną i zalecaną reakcją na ten błąd jest restart funkcji z tymi samymi parametrami, jakie były podane przy pierwszym wywołaniu.
Ręczna obsługa tego błędu może być z czasem niewygodna (szczególnie, gdy wykonujemy dużo operacji I/O). Z tego powodu w tym celu wykorzystamy makro TEMP_FAILURE_RETRY, które jest rozszerzeniem biblioteki C projektu GNU. Tutaj przeczytasz więcej o tym makrze. Aby skorzystać makra musimy wcześniej zdefiniować makro _GNU_SOURCE, które daje nam dostęp do tego typu niestandardowych rozszerzeń.
rozwiązanie drugi etap, plik prog16b.c:
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define ERR(source) \
(fprintf(stderr, "%s:%d\n", __FILE__, __LINE__), perror(source), kill(0, SIGKILL), exit(EXIT_FAILURE))
volatile sig_atomic_t sig_count = 0;
void sethandler(void (*f)(int), int sigNo)
{
struct sigaction act;
memset(&act, 0, sizeof(struct sigaction));
act.sa_handler = f;
if (-1 == sigaction(sigNo, &act, NULL))
ERR("sigaction");
}
void sig_handler(int sig) { sig_count++; }
void child_work(int m)
{
struct timespec t = {0, m * 10000};
sethandler(SIG_DFL, SIGUSR1);
while (1)
{
nanosleep(&t, NULL);
if (kill(getppid(), SIGUSR1))
ERR("kill");
}
}
ssize_t bulk_read(int fd, char *buf, size_t count)
{
ssize_t c;
ssize_t len = 0;
do
{
c = TEMP_FAILURE_RETRY(read(fd, buf, count));
if (c < 0)
return c;
if (c == 0)
return len; // EOF
buf += c;
len += c;
count -= c;
} while (count > 0);
return len;
}
ssize_t bulk_write(int fd, char *buf, size_t count)
{
ssize_t c;
ssize_t len = 0;
do
{
c = TEMP_FAILURE_RETRY(write(fd, buf, count));
if (c < 0)
return c;
buf += c;
len += c;
count -= c;
} while (count > 0);
return len;
}
void parent_work(int b, int s, char *name)
{
int i, in, out;
ssize_t count;
char *buf = malloc(s);
if (!buf)
ERR("malloc");
if ((out = TEMP_FAILURE_RETRY(open(name, O_WRONLY | O_CREAT | O_TRUNC | O_APPEND, 0777))) < 0)
ERR("open");
if ((in = TEMP_FAILURE_RETRY(open("/dev/urandom", O_RDONLY))) < 0)
ERR("open");
for (i = 0; i < b; i++)
{
if ((count = bulk_read(in, buf, s)) < 0)
ERR("read");
if ((count = bulk_write(out, buf, count)) < 0)
ERR("read");
if (TEMP_FAILURE_RETRY(fprintf(stderr, "Block of %ld bytes transferred. Signals RX:%d\n", count, sig_count)) <
0)
ERR("fprintf");
}
if (TEMP_FAILURE_RETRY(close(in)))
ERR("close");
if (TEMP_FAILURE_RETRY(close(out)))
ERR("close");
free(buf);
if (kill(0, SIGUSR1))
ERR("kill");
}
void usage(char *name)
{
fprintf(stderr, "USAGE: %s m b s \n", name);
fprintf(stderr,
"m - number of 1/1000 milliseconds between signals [1,999], "
"i.e. one milisecond maximum\n");
fprintf(stderr, "b - number of blocks [1,999]\n");
fprintf(stderr, "s - size of of blocks [1,999] in MB\n");
fprintf(stderr, "name of the output file\n");
exit(EXIT_FAILURE);
}
int main(int argc, char **argv)
{
int m, b, s;
char *name;
if (argc != 5)
usage(argv[0]);
m = atoi(argv[1]);
b = atoi(argv[2]);
s = atoi(argv[3]);
name = argv[4];
if (m <= 0 || m > 999 || b <= 0 || b > 999 || s <= 0 || s > 999)
usage(argv[0]);
sethandler(sig_handler, SIGUSR1);
pid_t pid;
if ((pid = fork()) < 0)
ERR("fork");
if (0 == pid)
child_work(m);
else
{
parent_work(b, s * 1024 * 1024, name);
while (wait(NULL) > 0)
{
}
}
return EXIT_SUCCESS;
}
Uruchamiamy jak poprzednio - błędy znikają.
Uwagi i pytania #
Jakie inne przerwania w programie może spowodować funkcja obsługi sygnału?Odpowiedź
Skąd wiemy, które funkcje mogą być przerwane zanim coś osiągną (EINTR)?Odpowiedź
Jako ważne ćwiczenie przeanalizuj jak działa bulk_read i bulk_write. Musisz rozumieć czemu uwzględniają tak dużo przypadków, jakie to przypadki, kiedy operacja IO może być przerwana, jak rozpoznać EOF.
W przeciwieństwie do laboratorium L1, na L2 i kolejnych trzeba używać tych funkcji (lub analogicznych) gdy używasz read lub write (ponieważ w programie mamy już sygnały).
I brak będzie powodował odejmowanie punktów.
Obie funkcje bulk_ mogą być pomocne nie tylko gdy chodzi o ochronę przed sygnałami lub sklejanie dużych transferów I/O,
ale także tam gdzie dane nie są dostępne w sposób ciągły - pipe/fifo/gniazda które poznamy nieco później.
Podobnie jak read/write zachowują się wszystkie funkcje pokrewne takie jak fread/fwrite czy send/recv
Warto sobie uświadomić czemu użycie flagi SA_RESTART w sa_flags podczas ustawiania funkcji obsługi sygnału nie rozwiązuje nam problemu z EINTR:
Z góry musimy wiedzieć jakie sygnały będą obsługiwane w naszym programie i wszystkie one muszą być włączone z tą flagą, wystarczy jeden bez tej niej i problem
EINTRpowraca. Łatwo o taki błąd jeśli powrócimy do starszego kodu, łatwo zapomnieć o tym wymogu.Jeśli chcemy napisać sobie funkcję biblioteczną (np. bulk_read) to nie możemy nic zakładać o obsłudze sygnałów w programie używającym naszej biblioteki.
Nie możemy łatwo przenieść takiego kodu, w programie docelowym musiałaby być dokładnie taka sama obsługa sygnałów.
Czasem zależy nam na tym, aby właśnie być informowanym o przerwaniu, jaskrawym przykładem jest funkcja sigsuspend, która z tą flagą traci sens!
Po wywołaniu fprintf nie sprawdzamy błędów innych niż EINTR, czemu?Odpowiedź
Zwróć uwagę, że naprawdę duże (f)printf’y mogą być przerwane także w trakcie wypisywania! Trudno będzie coś mądrego z tym zrobić, zwłaszcza jeśli do tego wypisywania używamy skomplikowanych formatów. Co prawda funkcja zwróci ile znaków wypisała ale jak to potem zrestartować? Zazwyczaj nie łatwo jest nam policzyć ile znaków powinno być wypisane, tym bardziej nie mamy jak wyznaczyć od którego momentu trzeba wznowić wypisywanie. Dlatego funkcji tej rodziny unikamy tam gdzie restartowanie będzie krytyczne i musimy wypisać dużo danych. Tam gdzie błąd braku restartu nie jest krytyczny ( wypisywanie na ekran) lub gdy ilość danych jest mała tam nie ma tego problemu.
Przykładowe zadania #
Wykonaj przykładowe zadania. Podczas laboratorium będziesz miał więcej czasu oraz dostępny startowy kod, jeśli jednak wykonasz poniższe zadania w przewidzianym czasie, to znaczy, że jesteś dobrze przygotowany do zajęć.