0 Linux – zarządzanie procesami
Tomasz Zaworski edited this page 2020-11-15 02:28:24 +01:00
  1. Obsługa procesów
  2. Funkcje systemowe i ich argumenty
  3. fork
  4. getpid, getppid
  5. exit
  6. wait, waitpid
  7. exec*
  8. Obsługa błędów
  9. Zadania
  10. Zadania do wykonania na zajęciach
  11. Zadania domowe
  12. Przygotowanie do kolejnych zajęć

Obsługa procesów

Proces (zwany też zadaniem) jest jednostką aktywną, kontrolowaną przez system operacyjny i związaną z wykonywanym programem. Proces ma przydzielone zasoby typu pamięć (segment kodu, segment danych, segment stosu, segment danych systemowych), procesor, urządzenia zewnętrzne itp. Część przydzielonych zasobów jest do wyłącznej dyspozycji procesu (np. segment danych, segment stosu), część jest współdzielona z innymi procesami (np. procesor, segment kodu w przypadku współbieżnego wykonywania tego samego programu w ramach kilku procesów).

W zależności od aktualnie posiadanych zasobów wyróżnia się stany procesu (np. wykonywany, uśpiony, gotowy), które zmieniają się cyklicznie w związku z wykonywanym programem lub ze zdarzeniami zachodzącymi w systemie. Szczegółowy stan procesu, umożliwiający kontynuację jego wykonywania po przerwaniu nazywany jest kontekstem procesu.

W zakresie obsługi procesów system UNIX udostępnia mechanizm tworzenia nowych procesów, usuwania procesów oraz uruchamiania programów. Każdy proces, z wyjątkiem procesu systemowego o identyfikatorze 1, tworzony jest przez inny proces, który staje się jego przodkiem zwanym też procesem rodzicielskim lub krótko rodzicem. Nowo utworzony proces nazywany jest potomkiem lub procesem potomnym. Procesy w systemie UNIX tworzą zatem drzewiastą strukturę hierarchiczną, podobnie jak katalogi.

Potomek tworzony jest w wyniku wywołania przez przodka funkcji systemowej fork. Po utworzeniu potomek kontynuuje wykonywanie programu swojego przodka od miejsca wywołania funkcji fork. Proces może się zakończyć dwojako: w sposób naturalny przez wywołanie funkcji systemowej exit lub w wyniku reakcji na sygnał. Funkcja systemowa exit wywoływana jest niejawnie na końcu wykonywania programu przez proces lub może być wywołana jawnie w każdym innym miejscu programu. Zakończenie procesu w wyniku otrzymania sygnału nazywane jest zabiciem. Proces może otrzymać sygnał wysłany przez jakiś inny proces (również przez samego siebie) za pomocą funkcji systemowej kill lub wysłany przez jądro systemu operacyjnego. (Z funkcją fork wiąże się również ciekawy rodzaj ataku zwany fork-bombą.)

Proces macierzysty może się dowiedzieć o sposobie zakończenia bezpośredniego potomka przez wywołanie funkcji systemowej wait. Jeśli wywołanie funkcji wait nastąpi przed zakończeniem potomka, przodek zostaje zawieszony w oczekiwaniu na to zakończenie.

Jeżeli proces macierzysty zakończy działanie przed procesem potomnym, to proces potomny nazywany jest sierotą (ang. orphant) i jest „adoptowany" przez proces systemowy init, który staję się w ten sposób jego przodkiem. Jeżeli proces potomny zakończył działanie przed wywołaniem funkcji wait w procesie macierzystym, potomek pozostanie w stanie zombi (proces taki nazywany jest zombi, upiorem, duchem lub mumią). Zombi jest procesem, który zwalnia wszystkie zasoby (nie zajmuje pamięci, nie jest mu przydzielany procesor), zajmuje jedynie miejsce w tablicy procesów w jądrze systemu operacyjnego i zwalnia je dopiero w momencie wywołania funkcji wait przez proces macierzysty, lub w momencie zakończenia procesu macierzystego.

W ramach istniejącego procesu może nastąpić uruchomienie innego programu w wyniku wywołania jednej z funkcji systemowych execl, execlp, execle, execv, execvp, execve. Funkcje te będą określane ogólną nazwą exec. Uruchomienie nowego programu oznacza w rzeczywistości zmianę programu wykonywanego dotychczas przez proces, czyli zastąpienie wykonywanego programu innym programem, wskazanym odpowiednio w parametrach aktualnych funkcji exec. Bezbłędne wykonanie funkcji exec oznacza zatem bezpowrotne zaprzestanie wykonywania bieżącego programu i rozpoczęcie wykonywania programu, którego nazwa jest przekazana jako argument. W konsekwencji, z funkcji systemowej exec nie ma powrotu do programu, gdzie nastąpiło jej wywołanie, o ile wykonanie tej funkcji nie zakończy się błędem.

Wyjście z funkcji exec można więc traktować jako jej błąd bez sprawdzania zwróconej wartości.

Funkcje służące do obsługi procesów opisane są w 2 i 3 części pomocy systemowej i w większości zdefiniowane w plikach sys/types.h oraz unistd.h.

Funkcje systemowe i ich argumenty

fork

#include <unistd.h>

pid_t fork( void );

Wartości zwracane:

  • poprawne wykonanie funkcji: utworzenie procesu potomnego; w procesie macierzystym funkcja zwraca identyfikator (pid) procesu potomnego (wartość większą od 1), a w procesie potomnym wartość 0,
  • zakończenie błędne: -1.

Możliwe kody błędów (errno) w przypadku błędnego zakończenie funkcji:

  • EAGAIN - błąd alokacji wystarczającej ilości pamięci na skopiowanie stron rodzica i zaalokowanie struktury zadań,
  • ENOMEM - nie można zaalokować niezbędnych struktur jądra z powodu braku pamięci.

W momencie wywołania funkcji (przez proces który właśnie staje się przodkiem) tworzony jest proces potomny, który wykonuje współbieżnie ze swoim przodkiem ten sam program. Potomek rozpoczyna wykonywanie programu od wyjścia z funkcji fork i kontynuuje wykonując kolejną instrukcję, znajdującą się w programie po wywołaniu funkcji fork. Do funkcji fork wchodzi zatem tylko proces macierzysty, a wychodzą z niej dwa procesy: macierzysty i potomny, przy czym każdy z nich otrzymuję inną wartość zwrotną funkcji fork. Wartością zwrotną funkcji fork w procesie macierzystym jest identyfikator (PID) potomka, a w procesie potomnym wartość 0. W przypadku błędnego wykonania funkcji fork potomek nie zostanie utworzony, a proces wywołujący otrzyma wartość -1.

Typ danych pid_t jest liczbą całkowitą ze znakiem (int), która reprezentuje ID procesu.

Przykład
#include <stdio.h>

main()
{
    printf("Poczatek\n");
    fork();
    printf("Koniec\n");
}

Wynik:

Poczatek
Koniec
Koniec

getpid, getppid

#include <sys/types.h>
#include <unistd.h>

pid_t getpid( void );
pid_t getppid( void );

Wartości zwracane:

  • poprawne wykonanie funkcji: zwrócenie własnego identyfikatora (w przypadku funkcji getpid) lub identyfikator procesu macierzystego (dla funkcji getppid),
  • zakończenie błędne: -1.
Przykład
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    printf("PID: %d\nParent PID: %d\n",
           getpid(), getppid());
}

Przykładowy wynik:

PID: 20047
Parent PID: 20023

exit

#include <stdlib.h>

void exit( int status );

Wartości zwracane:

  • poprawne wykonanie funkcji: przekazanie w odpowiednie miejsce tablicy procesów wartości status, która może zostać odebrana i zinterpretowana przez proces macierzysty,
  • zakończenie błędne: -1.

Funkcja kończy działanie procesu, który ją wykonał i powoduje przekazanie w odpowiednie miejsce tablicy procesów wartości status, która może zostać odebrana i zinterpretowana przez proces macierzysty.

Jeśli proces macierzysty został zakończony, a istnieją procesy potomne - to wykonanie ich nie jest zakłócone, ale ich identyfikator procesu macierzystego wszystkich procesów potomnych otrzyma wartość 1 będącą identyfikatorem procesu init (proces potomny staje się sierotą (ang. orphant) i jest „adoptowany" przez proces systemowy init).

  • exit(0) - oznacza poprawne zakończenie wykonanie procesu.
  • exit(dowolna wartość różna od 0) - oznacza wystąpienie błędu.

Jako kody wyjścia czasem stosuje się makra EXIT_SUCCESS i EXIT_FAILURE.

Przykład

Plik test-exit.c:

#include <stdlib.h>

int main()
{
    exit(124);
}

Wynik:

$ gcc test-exit.c -o test-exit.bin
$ ./test-exit.bin
$ echo $?          # zmienna $? przechowuje kod powrotu ostatnio wykonywanego polecenia 
124

wait, waitpid

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait( int *status );
pid_t waitpid( pid_t pid, int *status, int options );

Wartości zwracane:

  • poprawne wykonanie funkcji: identyfikator procesu potomnego, który się zakończył,
  • zakończenie błędne: -1 lub 0 jeśli użyto opcji WNOHANG, a nie było dostępnego żadnego potomka.

Możliwe kody błędów (errno) w przypadku błędnego zakończenie funkcji:

  • ECHILD - jeśli proces o zadanym identyfikatorze pid nie istnieje lub nie jest potomkiem procesu wywołującego. (Może się to zdarzyć również w przypadku potomka, który ustawił akcję obsługi sygnału SIGCHLD na SIG_IGN)
  • EINVAL - jeśli argument options jest niepoprawny.
  • EINTR - jeśli opcja WNOHANG nie była ustawiona, a został przechwycony niezablokowany sygnał lub SIGCHLD.

Argumenty funkcji:

  • status - status zakończenia procesu (w przypadku zakończenia w sposób normalny) lub numer sygnału w przypadku zabicia potomka lub wartość NULL, w przypadku gdy informacja o stanie zakończenia procesu nie jest istotna,
  • pid - identyfikator potomka, na którego zakończenie czeka proces macierzysty:
  • pid < -1 oznacza oczekiwanie na dowolny proces potomny, którego identyfikator grupy procesów jest równy modułowi wartości pid,
  • pid = -1 oznacza oczekiwanie na dowolny proces potomny; jest to takie samo zachowanie, jakie stosuje funkcja wait,
  • pid = 0 oznacza oczekiwanie na każdy proces potomny, którego identyfikator grupy procesu jest równe identyfikatorowi wołającego procesu,
  • pid > 0 oznacza oczekiwanie na potomka, którego identyfikator procesu jest równy wartości pid.
  • options - jest sumą OR zera lub więcej następujących stałych:
  • WNOHANG oznacza natychmiastowe zakończenie jeśli potomek się nie zakończył,
  • WUNTRACED oznacza zakończenie także dla procesów potomnych, które się zatrzymały, a których status jeszcze nie został zgłoszony.

Oczekiwanie na zakończenie procesu potomnego. Funkcja zwraca identyfikator (pid) procesu, który się zakończył. Pod adresem wskazywanym przez zmienną status umieszczany jest status zakończenia, który zawiera albo numer sygnału (najmniej znaczące 7 bitów), albo właściwy status zakończenia (bardziej znaczący bajt).

Gdy działa parę procesów potomnych zakończenie jednego z nich powoduje powrót z funkcji wait.

Jeżeli funkcja wait zostanie wywołana w procesie macierzystym przed zakończeniem procesu potomnego, wykonywanie procesu macierzystego zostanie zawieszone do momentu zakończenia potomka. Jeżeli proces potomny zakończył działanie przed wywołaniem funkcji wait, powrót z funkcji wait nastąpi natychmiast, a w czasie pomiędzy zakończeniem potomka, a wywołaniem funkcji wait przez jego przodka potomek pozostanie w stanie zombi. Zombi nie jest tworzony, gdy proces macierzysty ignoruje sygnał SIGCLD.

Przykład
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
 
int main()
{
    if (fork() == 0)
        printf("hello from child\n");
    else
    {
        wait(NULL);
        printf("hello from parent\nchild has terminated\n");
    }
 
    printf("Bye\n");
    return 0;
}

Wynik:

hello from child
Bye
hello from parent
child has terminated
Bye

exec*

Rodzina funkcji exec:

#include <unistd.h>

int execl ( char *path, char *arg0, ..., char *argn, (char *) NULL );
int execlp( char *file, char *arg0, ..., char *argn, (char *) NULL );
int execv ( char *path, char * argv[] );
int execvp( char *file, char * argv[] );
int execle( char * path, char *arg0, ..., char *argn, (char *) NULL , char *envp[] );
int execve( char * path, char *argv[], char *envp[] );

Wartości zwracane:

  • poprawne wykonanie funkcji: wywołanie programu podanego jako parametr,
  • zakończenie błędne: -1.

Możliwe kody błędów (errno) w przypadku błędnego zakończenie funkcji: Każda z tych funkcji może zakończyć się niepowodzeniem i ustawić jako wartość errno dowolny błąd określony dla funkcji bibliotecznej execve(2).

Argumenty funkcji:

  • path, file - pełna nazwa ścieżkowa lub nazwa pliku z programem,
  • arg0 ... argn - nazwa i argumenty programu który ma być wywołany.

W ramach istniejącego procesu może nastąpić uruchomienie innego programu w wyniku wywołania jednej z funkcji systemowych execl, execlp, execle, execv, execvp, execve. Funkcje te określane są ogólną nazwą exec. Bezbłędne wykonanie funkcji exec oznacza bezpowrotne zaprzestanie wykonywania bieżącego programu i rozpoczęcie wykonywania programu, którego nazwa jest przekazana przez argument.

W wyniku wywołania funkcji typu exec następuje reinicjalizacja segmentów kodu, danych i stosu. Nie zmieniają się atrybuty procesu takie jak pid, ppid, tablica otwartych plików i kilka innych atrybutów z segmentu danych systemowych

Różnice pomiędzy wywołaniami funkcji exec wynikają głównie z różnego sposobu budowy ich listy argumentów: w przypadku funkcji execl i execlp są one podane w postaci listy, a w przypadku funkcji execv i execvp jako tablica. Zarówno lista argumentów, jak i tablica wskaźników musi być zakończona wartością NULL. Funkcja execle dodatkowo ustala środowisko wykonywanego procesu.

Funkcje execlp oraz execvp szukają pliku wykonywalnego na podstawie ścieżki przeszukiwania podanej w zmiennej środowiskowej PATH. Jeśli zmienna ta nie istnieje, przyjmowana jest domyślna ścieżka :/bin:/usr/bin.

Wartością zwrotną funkcji typu exec jest status, przy czym jest ona zwracana tylko wtedy, gdy funkcja zakończy się niepoprawnie, będzie to zatem wartość -1.

Funkcje exec nie tworzą nowego procesu, tak jak w przypadku funkcji fork.

Przykłady
execl("/bin/ls", "ls", "-l", NULL);
execlp("ls", "ls", "-l", NULL);
char* const av[] = {"ls", "-l", NULL};
execv("/bin/ls", av);
char* const av[] = {"ls", "-l", NULL};
execvp("ls", av);

#include <stdio.h>

main()
{
    printf("Poczatek\n");
    execlp("ls", "ls", "-a", NULL);
    printf("Koniec\n");
}

W wyniku wywołania funkcji systemowej execlp następuje zmiana wykonywanego programu, zanim sterowanie dojdzie do instrukcji wyprowadzenia napisu Koniec na standardowe wyjście. Zmiana wykonywanego programu powoduje, że sterowanie nie wraca już do poprzedniego programu i napis Koniec nie pojawia się na standardowym wyjściu w ogóle.

Obsługa błędów

#include <stdio.h>

void perror( const char *s );
#include <errno.h>

int errno;

Funkcja perror drukuje komunikat na standardowym wyjściu diagnostycznym, opisujący ostatni błąd, który pojawił się podczas wywołania systemowego lub funkcji bibliotecznej.

Numer błędu jest zapisywany w zmiennej zewnętrznej errno.

Kody błędów.

Przykład
#include <stdio.h>
#include <errno.h>

int main ()
{
  FILE * pFile;
  pFile = fopen ("unexist.ent","rb");
  if (pFile == NULL)
  {
    perror ("The following error occurred");
    printf( "Value of errno: %d\n", errno );
  }
  else
    fclose (pFile);
  return 0;
}

Przykładowy wynik:

The following error occurred: No such file or directory
Value of errno: 2

Zadania

Zadania do wykonania na zajęciach

Zainstaluj program htop. Uruchom go w osobnym terminalu, tak aby wyświetlać tylko procesy użytkownika sop (htop -u sop):

  1. włącz opcję wyświetlania drzewa procesów (F5 - Tree),
  2. dodaj kolumnę PPID (parent PID) do wyświetlanych (F2 - Setup, Columns, działa tabulacja).

Podczas wykonywania zadań będzie to pomoc przy monitorowaniu działających programów.

Zadanie 1

Napisz program lab_fork.c, który tworzy proces potomny:

  1. proces-rodzic i proces-potomek muszą wyświetlić informacje o sobie, tj. "Jestem rodzicem/potomkiem [pid zwrócony z fork()] [pid procesu]",
  2. proces-rodzic czeka aż proces-potomek zakończy pracę, a następnie wyświetla komunikat "Zakonczono proces [pid zwrócony z wait()]".

W przypadku błędu funkcji fork, wyświetl stosowny komunikat o błędzie przy pomocy funkcji perror oraz zakończ działanie programu kodem wyjścia zapisanym w zmiennej errno.

Zadanie 2

Napisz program lab_orphan.c tworzący proces-sierotę. Możesz wykorzystać funkcję sleep.

Sprawdź jak zmienia się PPID w trakcie wykonywania programu.

Zadanie 3

Napisz program lab_zombie.c tworzący proces-zombie. Możesz wykorzystać funkcje sleep i wait.

Sprawdź jak zmienia się STATE (S) w trakcie wykonywania programu.

Zadanie 4

Napisz program lab_exec.c, który wywoła w procesie potomnym program podany jako parametr wykonania i traktujący kolejne parametry jako parametry tego nowego procesu.

Jeżeli wywołanie procesu zakończy się niepowodzeniem, wyświetl stosowny komunikat przy pomocy funkcji perror oraz zakończ działanie programu z kodem wyjścia EXIT_FAILURE.

W bashu kod wyjścia programu można wyświetlić np.:

$ ./lab_exec ls
$ echo $?

Zadania domowe

Zadanie domowe 1

Napisz program lab_tree_fork.c tworzący następujące drzewo procesów potomnych ([1] to proces-rodzic):

[1]
 |---[2]  (sleep 20)
 |---[3]
      |---[4]  (sleep 15)
      |---[5]  (sleep 10)
  • Procesy [1] i [3] maja poczekać na zakończenie ich procesów-potomków.
  • Proces [2] ma wstrzymać swoje działanie na 20 sek., [4] na 15 sek., a [5] na 10 sek.

Przygotowanie do kolejnych zajęć

W ramach przygotowania do kolejnych zajęć proszę przerobić ze strony Linux Journey materiały z sekcji Permissions oraz The Filesystem.


Źródło materiałów