Czasami mamy potrzebę wykonanie paru operacji jednocześnie lub chcemy, aby pewne rzeczy działy się w osobnym procesie lub wątku. W erze komputerów wieloprocesorowych oraz procesorów wielordzeniowych można pokusić się o wykorzystanie dodatkowej mocy, jaką dają dodatkowe procesory lub rdzenie. W pierwszej części opiszę jak w prosty sposób stworzyć w C prosty program powołujący do życia kolejne procesy.
Seria “Współbieżność w C”:
- Część 1 – fork()
- Część 2 – wait() i waitpid()
- Część 3 – semafory
- Część 4 – pamięć współdzielona (w przygotowaniu)
Jak stworzyć proces potomny?
Powoływanie do życia dodatkowych procesów (zwanych procesami potomnymi lub potomkami, procesy powołujące nazywamy macierzystymi lub rodzicami) w C jest dość proste. Podstawowym poleceniem używanym do tego celu jest fork(). Zasada działania tego polecenia jest prosta: w momencie jego wywołania tworzony jest nowy, bliźniaczy proces, dzielący z procesem macierzystym pamięć (w tym stan rejestrów i flag procesora, wszystko, według teorii, aż do pierwszej modyfikacji, po której proces otrzymuje własny obszar pamięci). Nowy proces potomny wykonywany jest od miejsca w kodzie, gdzie nastąpiło wywołanie funkcji fork().
Wszelkie przykłady w tym artykule wykonane zostały w C i skompilowane przy pomocy kompilatora gcc z GNU Compiler Collection.
Przykład 1. fork() – działanie programu z zależności od miejsca wywołania
1 2 3 4 5 6 7 8 | int main() { printf("Wyświetlę się raz.\n"); fork(); //od tej chwili żyją już 2 procesy printf("Wyświetlę się dwa razy.\n"); fork(); //od tej chwili żyją już 4 procesy printf("Wyświetlę się cztery razy.\n"); return 0; } |
Rezultat wykonania danego kodu będzie następujący (w nawiasach numer procesu wyświetlającego):
1 2 3 4 5 6 7 | (P1) Wyświetlę się raz. (P1) Wyświetlę się dwa razy. (P1) Wyświetlę się cztery razy. (P2) Wyświetlę się dwa razy. (P2) Wyświetlę się cztery razy. (P3) Wyświetlę się cztery razy. (P4) Wyświetlę się cztery razy. |
Takie zachowanie, jak wspomniałem wyżej, spowodowane jest tym, że kod wykonywany jest od miejsca wywołania funkcji fork(), co oznacza, że od pierwszego wywołania dalszą część kodu będą wykonywać już 2 procesy (stąd 2 razy wyświetlony tekst “Wyświetlę się dwa razy.” pomimo pojedynczej instrukcji w kodzie), a po następnym wywołaniu już 4 (dlatego tekst “Wyświetlę się cztery razy.” faktycznie wyświetla się 4 razy).
Kolejność wyświetlania komunikatów nie jest przypadkowa. Procesy potomne są opóźnione w czasie względem swoich procesów macierzystych. Związane jest to z faktem, iż na każde utworzenie procesu potrzebne są dodatkowe cykle procesora oraz konieczność przydzielenia zasobów systemowych (np. stworzenie nowego PCB – bloku kontrolnego procesu, ang. Process Control Block), co oczywiście zajmuje czas, więc zanim proces potomny zostanie uruchomiony do procesu macierzystego przekazywany jest jego PID (identyfikator procesu, ang. process identificator) pozwalający mu kontynuować działanie, a następnie procesowi potomnemu tworzony jest nowy PCB, a następnie przydzielany czas procesora.
Powyższy program działa w przybliżeniu następująco (zakładam, że proces pierwszy P1 jest procesem macierzystym):
- P1 wyświetla komunikat #1.
- P1 wywołuje fork() i powołuje do życia P2.
- P1 wyświetla komunikat #2
- P1 wywołuje fork() i powołuje do życia P3.
- P1 wyświetla komunikat #3.
- P2 wyświetla komunikat #2.
- P2 wywołuje fork() i powołuje do życia P4.
- P2 wyświetla komunikat #3.
- P3 wyświetla komunikat #3.
- P4 wyświetla komunikat #3.
Zależności między procesami wyglądają następująco:
- P1 jest procesem macierzystym P2 i P3 i nie bezpośrednio P4.
- P2 jest procesem macierzystym P4.
- P4 jest procesem potomnym P2 i nie bezpośrednio P1.
- P3 jest procesem potomnym P1.
To, że P1 jest nie bezpośrednim procesem macierzystym wiąże się z tym, że proces P4 wie tylko o swoim bezpośrednim ojcu P2. Dopiero P2 ma wiedzę o swoim ojcu P1. W taki sposób powstaje systemowe drzewo procesów (które na systemach typu Linux można wyświetlić poleceniem pstree). Nasuwają się jednak pytania: “Jak rozpoznać czy jesteśmy w procesie potomnym, czy może w macierzystym?” oraz “Jak można sterować tworzeniem procesów i zabijaniem procesów?”
Gdzie jesteśmy, czyli rozpoznawanie rodziców i potomków.
Rozpoznawanie w jakiego typu procesie jesteśmy w danej chwili jest bardzo łatwe. Z pomocą przychodzi nam oczywiście fork(), a raczej rezultat wykonania tej funkcji. Fork() zwraca liczbę typu int (typ liczbowy, liczby całkowite) z zakresu wartości od 0 w górę. Cóż to za liczba? Jest to:
- w przypadku, gdy jesteśmy w procesie macierzystym, jest to numer PID procesu potomnego;
- w przypadku, gdy jesteśmy w procesie potomnym, jest to wartość zero;
Należy także pamiętać, że fork() wykona się podwójnie: raz powołując do życia nowy proces potomy (w procesie głównym, zwracając identyfikator powołanego procesu) i drugi raz w procesie potomnym (zwracając zero). Załóżmy, że mamy taki fragment kodu:
Przykład 2. fork() – rozpoznawanie procesu potomnego i macierzystego
1 2 3 4 5 6 | int idPotomnego = fork(); if (idPotomnego == 0) { printf("Jesteś w procesie potomnym, jego ID = %d\n", getppid()); } else { printf("Jesteś w procesie macierzystym, jego ID = %d\n", getppid()); } |
Pierwsza linijka powyższego kodu zostanie wykonana 2 razy, zwracając odpowiednie wartości (0 dla procesu potomnego oraz inną wartość dla procesu macierzystego). Dzięki temu możliwe jest kontrolowanie procesu potomnego oraz komunikacja między tymi procesami (w tym też zagadnienie synchronizacji), ale o tym w następnych częściach artykułu. W części II opiszę zagadnienie synchronizacji procesów za pomocą semaforów.
Przykładowy program znajdziecie tutaj.


Jak tam prace nad artykułem o semaforach?
W toku. Od jakiegoś czasu nie miałem chwili, żeby go dokończyć, ale postaram się skończyć go do końca nadchodzącego tygodnia. W końcu ile można go pisać?