Tutorial 2 - Procesy, sygnały i deskryptory #
Uwagi wstępne:
- Szybkie przejrzenie tutoriala prawdopodobnie nic nie pomoże, należy samodzielnie uruchomić programy, sprawdzić jak działają, poczytać materiały dodatkowe takie jak strony man. W trakcie czytania sugeruję wykonywać ćwiczenia a na koniec przykładowe zadanie.
- Na żółtych polach podaję dodatkowe informacje, niebieskie zawierają pytania i ćwiczenia. Pod pytaniami znajdują się odpowiedzi, które staną się widoczne dopiero po kliknięciu. Proszę najpierw spróbować sobie odpowiedzieć na pytanie samemu a dopiero potem sprawdzać odpowiedź.
- Pełne kody do zajęć znajdują się w załącznikach na dole strony. W tekście są tylko te linie kodu, które są konieczne do zrozumienia problemu.
- Materiały i ćwiczenia są ułożone w pewną logiczną całość, czasem do wykonania ćwiczenia konieczny jest stan osiągnięty poprzednim ćwiczeniem dlatego zalecam wykonywanie ćwiczeń w miarę przyswajania materiału.
- Większość ćwiczeń wymaga użycia konsoli poleceń, zazwyczaj zakładam, ze pracujemy w jednym i tym samym katalogu roboczym więc wszystkie potrzebne pliki są “pod ręką” tzn. nie ma potrzeby podawania ścieżek dostępu.
- Czasem podaję znak $ aby podkreślić, że chodzi o polecenie konsolowe, nie piszemy go jednak w konsoli np.: piszę “$make” w konsoli wpisujemy samo “make”.
- To co ćwiczymy wróci podczas kolejnych zajęć. Jeśli po zajęciach i teście coś nadal pozostaje niejasne proszę to poćwiczyć a jeśli trzeba dopytać się u prowadzących.
- Tym razem część rozwiązań jest podzielone na 2 możliwe do uruchomienia etapy
Zadanie 1 - procesy potomne #
Cel: Program tworzy 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. Co student musi wiedzieć:
- man 3p fork
- man 3p getpid
- man 3p wait
- man 3p waitpid
- man 3p sleep
- Job Control
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 (n--; n >= 0; n--)
{
if ((s = fork()) < 0)
ERR("Fork:");
if (!s)
{
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)
{
int n;
if (argc < 2)
usage(argv[0]);
n = atoi(argv[1]);
if (n <= 0)
usage(argv[0]);
create_children(n);
return EXIT_SUCCESS;
}
Do kompilacji używamy “make prog13a” ostatni plik Makefile z tutoriala nr 1
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 ERR dodano kill(0, SIGKILL). Chodzi o to aby w razie błędu zatrzymać cały program (wszystkie procesy danej grupy).
Zero jako argument kill jest 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 kill w makrze ERR, to dla tego, że w przypadku sytuacji krytycznej podejmujemy jak najmniej działań, zresztą co mielibyśmy zrobić wywołać rekurencyjnie ERR?
Pytanie czemu po uruchomieniu wraca linia poleceń?Odpowiedź
Jak sprawdzić kto jest rodzicem procesów prog13a? Dlaczego właśnie ten proces?Odpowiedź
Zwróć 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ź
Czy można jako argument srand podać time() zamiast getpid()?Odpowiedź
Zwróć uwagę na losowanie liczby z przedziału [A,B]. Niby oczywiste, ale warto opracować sobie prosty wzór.
Jak zachowa się program bez exit wywołanym zaraz po child_work w procesie potomnym?Odpowiedź
Ile procesów (w sumie ) zostanie utworzonych jeśli jako parametr uruchomienia podamy 3 przy założeniu, że nie ma exit wspomnianego w poprzenim pytaniu?Odpowiedź
Co zwraca sleep? Czy powinniśmy jakoś na to reagować?Odpowiedź
W kolejnym etapie dodamy czekanie i zliczanie procesów potomnych. Pytanie skąd mamy wiedzieć ile procesów potomnych w danej chwili istnieje?Odpowiedź
rozwiązanie 2 etap 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 (n--; n >= 0; n--)
{
if ((s = fork()) < 0)
ERR("Fork:");
if (!s)
{
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)
{
int n;
if (argc < 2)
usage(argv[0]);
n = atoi(argv[1]);
if (n <= 0)
usage(argv[0]);
create_children(n);
while (n > 0)
{
sleep(3);
pid_t pid;
for (;;)
{
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;
}
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 waitpid w pętli?Odpowiedź
Czemu wołamy waitpid z flagą WNOHANG?Odpowiedź
Czemu wołamy waitpid z pidem zero?Odpowiedź
Czy w tym programie występują sygnały?Odpowiedź
Czy tym razem nie powinniśmy sprawdzać co zwraca sleep skoro są sygnały?Odpowiedź
Zadanie 2 - sygnały #
Cel: 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 signal
- man 3p sigaction
- man 3p nanosleep
- man 3p alarm
- man 3p memset
- man 3p kill
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;
for (;;)
{
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;
while (l-- > 0)
{
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)
{
while (n-- > 0)
{
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 nie jest przypadkowy, co więcej jest to jedyny BEZPIECZNY i POPRAWNY typ. Wynika to z asynchronicznej natury wywołania f. obsługi sygnału a dokładniej: Primo “volatile” oznacza 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ętla while(work) gdzie work jest zmienną globalną zmienia się na while(1) po optymizacji. Secundo sig_atomic_t oznacza 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 prostego porównania a==0 o 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 (nie wiemy jakie tam są jeszcze pola w danej implementacji).
Zwróć uwagę, że obsługa sigchild w pętli jest prawie identyczna jak poprzednio w pętli.
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 mainOdpowiedź
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;
for (;;)
{
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;
while (l-- > 0)
{
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)
{
while (n-- > 0)
{
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;
}
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 2)?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 SIGUSR1i2 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 SIGUSR1i2?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ź
Zadanie 3 - czekanie na sygnał #
Cel: Program 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.
Co student musi wiedzieć:
- man 3p sigsuspend
- Opis metod czekania na sygnał w glibc tutaj
- man 3p getppid
- man 3p pthread_sigmask (tylko opis sigprocmask)
- man 3p sigaddset
- man 3p sigemptyset
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;
for (;;)
{
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)
{
int m, p;
if (argc != 3)
usage(argv[0]);
m = atoi(argv[1]);
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 (0 == pid)
child_work(m, p);
else
{
parent_work(oldmask);
while (wait(NULL) > 0)
;
}
sigprocmask(SIG_UNBLOCK, &mask, NULL);
return EXIT_SUCCESS;
}
Program działa aż do SIGINT (C-c)
Uruchom z różnymi parametrami, z małą przerwą w mikrosekundach i częstymi SIGUSR2 róż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ź
Pytanie czemu liczniki się różnią ?Odpowiedź
Jak uruchomić ten program aby zminimalizować szansę na sklejanie się SIGUSER2 i jednocześnie obserwować różnice w licznikach?Odpowiedź
Popraw powyższy program tak aby wyeliminować problem wielu wywołań obsługi sygnału w obrębie jednego sigsuspendOdpowiedź
Zadanie 4 - operacje niskopoziomowe na plikach a sygnały #
Cel: Zmodyfikować program z zadania 3 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
Tym razem rozwiązanie jest podzielone na 2 możliwe do uruchomienia etapy.
rozwiązanie 1 etap, 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 transfered. 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/random możesz pobrać na prawdę losowe bajty ale w małych ilościach, z /dev/urandom odwrotnie, pseudo losowe liczby za to w dowolnych ilościach.
Powinieneś obserwować następujące problemy podczas uruchamiania z parametrami 1 20 40 out.txt :
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
fprintf: Interrupted system call - przerwanie funkcją obsługi sygnału funkcji fprintf ZANIM ta coś wyświetli
Analogiczne komunikaty dla open i close - może to być trudno zaobserwować w tym programie ale jest to możliwe wg. POSIX
Jak sobie z tymi efektami radzić pokazujemy w następnym etapie.
Zawsze gdy w poprawnym programie pojawia się alokacja pamięci musi być też jej zwalnianie!
Uprawnienia podawane w funkcji open mogą 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”.
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 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 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 transfered. 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ą.
Co to jest błąd EINTR?Odpowiedź
Jaka jest poprawna reakcja na ten błąd?Odpowiedź
Kiedy dokładnie te funkcje są przerywane?Odpowiedź
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 podczas instalowania 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 EINTR powraca. Ł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? Jeśli nie możemy pisać na stderr (zapewne ekran) to i tak nie zaraportujemy błędu.
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ęć.