Najciekawsza dla mnie funkcja biblioteki glibc, fork()
Co robi? Ano prostą rzecz, kopię procesu, który jej użył.
Powstają dwa w zasadzie identyczne procesy, mają identyczne kopie danych, identyczny kod, dostęp do wszystkich otwartych uprzednio plików... I tu właśnie piękno i swoista magia funkcji fork().
To trochę jak w książkach Science Fiction, mamy dwa identyczne klony astronauty po nieudanej teleportacji. Astronauta pojawił sie na planecie w odległej galaktyce, ale wskutek awarii sprzętu nie zniknął na Ziemi.
Obaj mają tę samą wiedzę, doswiadczenie, imię, nazwisko, wspólną przeszłość aż do momentu teleportacji.
Obaj zrobią to, co po teleportacji zaplanowali zrobić. Obaj uważają się za ten jeden, jedyny, oryginalny egzemplarz, a nie klon oryginału.
Jak to działa? Napiszmy prościutki programik:
#include <stdio.h> #include <stdlib.h> #include <stddef.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> main (argc, argv) { printf("START!\n"); fork(); printf("Jestem procesem nadrzednym, moj PID to %i\n", getpid()); }
Co zobaczymy na ekranie po jego skompilowaniu i uruchomieniu? Ano coś takiego:
root@mobile3:/home/rcp# ./test_fork START! Jestem procesem nadrzednym, moj PID to 9364 Jestem procesem nadrzednym, moj PID to 9365
Napis "START" tylko jeden, ale dwa napisy "Jestem procesem nadrzędnym"!
"START" nasz proces wyświetlił przed wykonaniem fork.
Komunikat "Jestem procesem..." został wyświetlony po wykonaniu fork(). Działały juz dwa procesy, oba go wyświetliły!
Pochwaliły się nawet swoimi PID, 9364 i 9365.
Pożytek z takiego klonowania jednak niewielki, nawet w książkach Science Fiction. Proces po fork() musi wiedzieć, czy jest procesem nadrzędnym, czy potomnym.
Wtedy proces nadrzędny będzie mógł kontynuować swoje główne zadanie, na przykład sprawdzać stan czujników temperatury w reaktorach, a proces podrzędny zająć się czasochłonnym dodawaniem kolejnych składników do reaktora który osiągnął właściwą temperaturę.
Na szczęście łatwo to zrobić.
fork() zwraca w procesie potomnym wartość 0, w procesie nadrzędnym PID procesu potomnego (dla porządku, może też zwrócić -1, jeśli zakonczył się błędem).
Zmodyfikujmy trochę nasz testowy programik:
main (argc, argv) { int m; if (! (m = fork()) ) { printf("Jestem procesem potomnym, moj PID to %i\n", getpid()); } else { printf("Jestem procesem nadrzednym, uruchomilem proces potomny o PID %i\n", m); } }
Po skompilowaniu i uruchomieniu już widzimy, że procesy połapały się kto jest kim:
root@mobile3:/home/rcp# ./test_fork Jestem procesem nadrzednym, uruchomilem proces potomny o PID 10212 Jestem procesem potomnym, moj PID to 10212
W procesie potomnym fork() zwrócił 0, w nadrzędnym 1022 (zachowaliśmy tę wartość w zmiennej 'm'). Tak właśnie fork() został użyty do robienia zdjęć w przykładowym serwerze kamer.
Jeśli ktoś nie wierzy, że procesy są dwa, znów troszeczkę zmodyfikujemy nasz testowy programik:
main (argc, argv) { printf("Jestem pierwotnym procesem, zrobię sleep 10 sekund\n"); sleep(10); int m; if (! (m = fork()) ) { printf("Jestem procesem potomnym, moj PID to %i\n", getpid()); printf("Jestem procesem potomnym, zrobię sleep na 10 sekund\n"); fflush(0); sleep(10); } else { printf("Jestem procesem nadrzednym, uruchomilem proces potomny o PID %i\n", m); printf("Jestem procesem nadrzednym, zrobię sleep na 10 sekund\n"); fflush(0); sleep(10); } }
Dodaliśmy 10 sekund sleep przed fork() i po nim, da nam to czas na zatrzymanie procesu (procesów) i sprawdzenie, czy są (CTRL Z z klawiatury i ^Z na kopii ekranu).
root@mobile3:/home/rcp# ./test_fork Jestem pierwotnym procesem, zrobię sleep 10 sekund ^Z [1]+ Stopped ./test_fork root@mobile3:/home/rcp# ps ax|grep test_fork 10952 pts/0 T 0:00 ./test_fork 10958 pts/0 S+ 0:00 grep test_fork root@mobile3:/home/rcp# fg ./test_fork Jestem procesem nadrzednym, uruchomilem proces potomny o PID 10960 Jestem procesem nadrzednym, zrobię sleep na 10 sekund Jestem procesem potomnym, moj PID to 10960 Jestem procesem potomnym, zrobię sleep na 10 sekund ^Z [1]+ Stopped ./test_fork root@mobile3:/home/rcp# ps ax|grep test_fork 10952 pts/0 T 0:00 ./test_fork 10960 pts/0 T 0:00 ./test_fork 10963 pts/0 S+ 0:00 grep test_fork
Widzimy wyraźnie jeden proces test_fork podczas pierwszego sleep i dwa podczas drugiego.
Kolejny eksperymencik. Tym razem proces potomny zakończy działanie natychmiast po wyświetleniu komunikatu:
main (argc, argv) { int m; if (! (m = fork()) ) { printf("Jestem procesem potomnym, koncze prace\n"); } else { printf("Jestem procesem nadrzednym, zrobię sleep na 10 sekund\n"); fflush(0); sleep(10); } }
I jakie procesy teraz zobaczymy jak proces nadrzędny robi sleep?
root@mobile3:/home/rcp# ./test_fork Jestem procesem nadrzednym, zrobię sleep na 10 sekund Jestem procesem potomnym, koncze prace ^Z [1]+ Stopped ./test_fork root@mobile3:/home/rcp# ps ax|grep fork 11534 pts/0 T 0:00 ./test_fork 11535 pts/0 Z 0:00 [test_fork] <defunct> 11538 pts/0 S+ 0:00 grep fork
Nadal dwa! A przecież proces potomny dawno skończył swoje działanie!
Zwróćcie uwagę na literkę w 3 kolumnie tego co komenda ps ax wyświetliła.
To stan procesu, T oznacza że proces jest zatrzymany.
To prawda, zatrzymaliśmy go naciskając CTRL Z.
Ale przy drugim procesie, potomnym, którego miało już nie być, mamy literke Z.
To proces "zombie". Zakończył się, ale nadal w systemie istnieje, bo proces nadrzędny może chcieć się dowiedzieć z jakim rezultatem zakończył pracę.
Po zakończeniu pracy procesu nadrzędnego zniknie, jednak nie zawsze możemy sobie na takie oczekiwanie pozwolić.
Proces nadrzędny może działać nawet przez wiele lat, uruchamiając kilkaset procesów potomnych w sekundzie...
Jak sobie z tymi zombie poradzic? To proste, zainteresować się swoim potomkiem.
Po skończeniu pracy procesu potomnego system wysyła do procesu nadrzędnego sygnał SIGCHLD. Wystarczy go obsłużyć.
Kolejna drobna rozbudowa naszego programu:
catch_CHLD(int signal_num) { printf("Obsługuję sygnał %i\n", signal_num); fflush(0); } main (argc, argv) { signal(SIGCHLD, catch_CHLD); int m; if (! (m = fork()) ) { printf("Jestem procesem potomnym, kończę pracę\n"); } else { printf("Jestem procesem nadrzednym, zrobię sleep na 10 sekund\n"); fflush(0); sleep(10); } }
Jeśli nadejdzie sygnał SIGCHLD proces przerwie działanie i wykona kod funkcji catch_CHLD.
Może ona oczywiście zrobić coś pożyteczniejszego niż proste wyświetlenie komunikatu, że dotarł sygnał, jednak już sam fakt, że proces nadrzędny obsłużył SIGCHLD wystarczy, żeby proces potomny zniknął z tablicy procesów systemu, nie będzie już zombie.
Na ekranie zobaczymy:
root@mobile3:/home/rcp# ./test_fork Jestem procesem nadrzednym, zrobię sleep na 10 sekund Jestem procesem potomnym, kończę pracę Obsługuję sygnał 17
Jeśli skomplilujecie i uruchomicie poprzedni program, zauważycie pewnie coś dziwnego.
Zniknął gdzieś ten czas 10 sekund przez jaki proces nadrzędny miał być uśpiony. Kończy on swoje działanie natychmiast.
No cóż, tak właśnie działa sleep(). Proces pozostaje uśpiony przez zadany czas, ale jeśli nadejdzie sygnał, przerywa swoje działanie. Jest to wyraźnie opisane w dokumentacji biblioteki GNU C.Zwraca jednak ilość czasu, który pozostał do końca uśpienia. Jeśli nie zależy nam na bardzo precyzyjnym odmierzeniu czasu uśpienia, a przerwania nie przychodzą bardzo często, możemy z tego faktu skorzystać:
main (argc, argv) { signal(SIGCHLD, catch_CHLD); int m; int sleep_time; if (! (m = fork()) ) { printf("Jestem procesem potomnym, koncze prace\n"); } else { printf("Jestem procesem nadrzednym, zrobię sleep na 10 sekund\n"); fflush(0); sleep_time = 10; while (sleep_time > 0) sleep_time = sleep (sleep_time); } }
Jeśli sleep() zwróci wartość większą od zera, zostanie wywołany ponownie, na czas który pozostał do końca pierwotnie wskazanego czasu. Niestety wartość pozostałego czasu zwracana przez sleep() wyrażona jest w sekundach, 7 sekund może znaczyć, że pozostało 6,65 sekundy albo 7,28. Przy większej liczbie przerwań błędy te mogą się kumulować, dlatego lepiej w takim przypadku skorzystac z pomiaru czasu korzystając z zegara sytemu, jak w przykładzie z programu obsługi serwera kamer lub skorzystać ze znacznie dokładniejszego nanosleep()
Proces potomny dostaje do dyspozycji własną kopię obszaru danych i kodu procesu nadrzędnego, a także kopie deskryptorów otwartych plików i połączeń sieciowych.
Nie jest w stanie zniszczyć danych programu nadrzędnego, bo działa na własnej kopii.
To, że proces potomny może zmienić zawartość pliku otwartego przez proces nadrzędny jest oczywiste, programista powinien wziąć to pod uwagę.
W przypadku otwartych plików i połączeń sieciowych sytuacja nie jest już tak jasna. Dla przykładu:
mysql_init (&mysql); mysql_real_connect (&mysql, "localhost", "rcp", "Akuku", "rcp", 0, NULL, 0) if (! (m = fork()) ) { // proces potomny, coś robi, po czym wysyła zapytanie do bazy danych sprintf(buf, "Select * from wydzialy where idw > 100); mysql_query (&mysql, &buf[0]); } else { // proces nadrzedny, coś robi, po czym też wysyła zapytanie do bazy danych sprintf(buf, "Select id_drzwi from drzwi where nr_drzwi=23"); mysql_query (&mysql, &buf[0]); }
Jeśli zapytanie do bazy danych z jednego procesu zostanie zadane w trakcie wykonywania zapytania przez drugi proces, nic dobrego z tego nie wyniknie.
Oba procesy korzystają z tej samej struktury MYSQL, nasz program 'wybuchnie', motor bazy danych nie spodziewa się dwu zapytań na tym samym połączeniu w tym samym czasie.
Jeśli zapytania nie będą sie zdarzały w tym samym czasie, wszystko będzie OK.
Aby uniknąć trudnej technicznie zabawy w synchronizację zapytań z kilku procesów, najlepiej w procesie potomnym otworzyć nowe połączenie z bazą:
mysql_init (&mysql);
mysql_real_connect (&mysql, "localhost", "rcp", "Akuku", "rcp", 0, NULL, 0)
if (! (m = fork()) )
{
// proces potomny, coś robi, po czym wysyła zapytanie do bazy danych,
// otwiera do niej nowe połączenie:
mysql_init (&mysql_1);
mysql_real_connect (&mysql_1, "localhost", "rcp", "Akuku", "rcp", 0, NULL, 0)
sprintf(buf, "Select * from wydzialy where idw > 100);
mysql_query (&mysql_1, &buf[0]);
}
else
{
// proces nadrzedny, coś robi, po czym też wysyła zapytanie do bazy danych
sprintf(buf, "Select id_drzwi from drzwi where nr_drzwi=23");
mysql_query (&mysql, &buf[0]);
}