SOP Lab 6

Wstęp do programowania w C, procesy

Bash był fajniejszy


Zaczniemy od dość prostego przykładu – do skryptów basha możemy bez większego problemu przekazać jakieś dane w postaci argumentów. Do programu w C również. Zaczniemy od funkcji main(). Program w C zawsze się od takowej zaczyna, jest to bowiem funkcja, która jest wywoływana przy uruchomieniu programu. Możemy ją oczywiście wywołać pustą, ale wtedy nie przekażemy do niej żadnych argumentów. Aby cokolwiek do niej przekazać, użyjemy konstrukcji

int main(int argc, char *argv[]){}

argc jest intem, w którym znajdować się będzie liczba argumentów przekazanych do programu. Argv[] natomiast jest tablicą o rozmiarze takim jak argc, która zawierać będzie kolejno wszystkie przekazane do programu argumenty.

✍️ Szybkie zadanko

Napisz programik, który przyjmie ileś argumentów, a następnie je wszystkie wypisze na terminalu. Przypomnę przy okazji, że do wydrukowania tekstu użyjemy funkcji printf (należy zaincludować ).

int printf(stala_lancuchowa, argument1, argument2...)

stała łańcuchowa to w uproszczeniu tekst, który chcemy wypisać. Może zawierać zwykłe znaki, które są przepisywane na ekran oraz kody formatujące kolejne argumenty.

  • %c – pojedynczy znak
  • %s – string
  • %d – signed int
  • %f – float
  • %u – unsigned int

Czyli, aby wydrukować zawartość zmiennych int jajco i łańcucha PJATK wywołamy:

printf("jajco ma wartość %d, natomiast PJATK ma wartość %s", jajco, PJATK)

🚪 Main()

Wywołanie programu to tak naprawdę wywołanie funkcji main() tego programu. Jeśli int main() zwraca 0, to znaczy że program się wykonał prawidłowo i wszyscy jesteśmy szczęśliwi. Stąd też return 0 na końcu. Jakakolwiek inna wartość oznacza błąd. Możemy również użyć exit(0) dla prawidłowo wykonanych programów. Parametr basha $? zawiera wartość zwracaną przez ostatnio zakończony program.

👨‍👦 Pora na procesy

Linux zawiera szereg rozwiązań umożliwiających rozdzielania programu na wiele procesów, zarządzanie nimi i komunikację między nimi.

Wywołanie fork() jest używane do stworzenia nowego procesu, nazywanego child process, który będzie działał równolegle do procesu dokonującego wywołania, czyli parent process. Kiedy utworzony zostanie proces podrzędny, oba procesy wykonają już następną instrukcję. Dla przykładu:

#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]){

printf("%s", "zaraz sie podziele!\n");
fork();
printf("Hello World\n");

return 0;
}

Systemowa funkcja fork() nie przyjmuje żadnych argumentów, a zwraca wartość typu int o wartości zależnej od tego, w którym procesie ją sprawdzimy:

  • < 0 – błąd podczas tworzenia procesu
  • == 0 – jesteśmy w procesie dziecka
  • > 0 – jesteśmy w rodzicu, wartość to PID dziecka

Wywołanie wait() służy do poczekania na zmiany w procesie-dziecku wywołującego procesu. Taką zmianą może być na przykład zakończenie, zatrzymanie sygnałem, wznowienie. W wypadku terminacji, system uwolni zasoby zarezerwowane dla dziecka. Mamy również kilka funkcji, które pomogą nam odczytywać pid bieżącego i nadrzędnego procesu, są to odpowiednio

getpid();
getppid();

procesem wywołującym funkcję main() będzie terminal, z którego została wywołana.

🕓 Synchronizacja procesów – wait() i waitpid()

Po utworzeniu dziecka proces rodzic może:

  • zakończyć się od razu, co uczyni dziecko „sierotą” (adoptowaną przez init)
  • poczekać na dziecko dzięki funkcji wait() lub waitpid()

🛑 wait()

  • Czeka, aż jakikolwiek proces potomny zakończy się.
  • Zwraca PID zakończonego procesu.
  • Przykład:
#include <sys/wait.h>

int status;
pid_t pid = wait(&status);

🧠 waitpid()

  • Czeka na konkretny proces o danym PID.
  • Można użyć z dodatkowymi flagami (np. WNOHANG).
  • Przykład:
    pid_t pid = waitpid(child_pid, &status, 0);
    

📌 Odczyt kodu zakończenia:

if (WIFEXITED(status)) {
    int code = WEXITSTATUS(status);
    printf("Proces zakończył się kodem %d\n", code);
}

📌 Podsumowanie

Funkcja Opis
fork() Tworzy nowy proces
getpid() Zwraca PID bieżącego procesu
getppid() Zwraca PID procesu nadrzędnego
wait() Czeka na zakończenie jakiegokolwiek dziecka
waitpid() Czeka na konkretny PID procesu

Zadania do laboratorium

✅ Zadanie 1 – Trzy procesy potomne

Napisz program, który:

  • Tworzy trzy procesy potomne (fork()).
  • Każdy proces potomny powinien:
    • wypisać swój PID i PPID,
    • zakończyć działanie kodem exit(i) (gdzie i to numer procesu).
  • Proces rodzica powinien:
    • poczekać na każdego potomka osobno (waitpid()),
    • wypisać informacje o zakończeniu dzieci: PID + kod wyjścia (WEXITSTATUS()).

✅ Zadanie 2 – fork() i identyfikacja procesu

Napisz program, który:

  • Tworzy jeden proces potomny.
  • Dziecko:
    • wypisuje swój PID i PPID,
    • śpi 2 sekundy,
    • sprawdza ponownie PPID (czy zmienił się na 1),
    • kończy działanie.
  • Rodzic kończy się od razu, bez wait(). ❓Czy rodzic się zmienia? Jeśli tak, to kto jest nowym rodzicem?

✅ Zadanie 3 – Dwa procesy potomne z kolejnością

Napisz program, który:

  • Tworzy dwa procesy potomne.
  • Proces 1:
    • wypisuje “Jestem procesem 1”,
    • kończy się po 1 sekundzie.
  • Proces 2:
    • czeka 2 sekundy,
    • wypisuje “Jestem procesem 2”,
    • kończy się.

Rodzic powinien użyć waitpid() i wypisać, który proces zakończył się jako pierwszy i drugi.

🎯 Możesz dodać timestamp (time() lub gettimeofday()), by precyzyjnie zmierzyć czas zakończenia.


✅ Zadanie 4 – Dziecko czeka na inny proces

Napisz program, który:

  • Tworzy dziecko.
  • Dziecko tworzy swoje dziecko (czyli wnuka).
  • Proces wnuka:
    • wypisuje “Wnuk działa”, śpi 2 sekundy, kończy się exit(5).
  • Proces dziecko:
    • czeka na zakończenie wnuka (wait()),
    • wypisuje PID zakończonego procesu i WEXITSTATUS.
  • Proces rodzic:
    • nie czeka na nikogo i kończy się od razu.

✅ Zadanie 5– Drzewo procesów

Stwórz program, który buduje strukturę:

Rodzic
├── Dziecko 1
│   └── Wnuk 1
└── Dziecko 2
    └── Wnuk 2

Każdy proces powinien wypisać swoje:

  • PID, PPID, poziom w drzewie
  • nazwę roli (np. “Dziecko 1”)