dalsi predchozi obsah
Dalsi: Signaly Predchozi: Prostredi procesu

Rizeni procesu





Uvod

Nyni se jiz muzeme venovat sprave a rizeni procesu. V teto kapitole budeme rozebirat vytvareni novych procesu, spousteni programu a ukoncovani programu. Probereme take identifikatory procesu -- realne, efektivni i uschovane.



Identifikatory procesu

Kazdy proces ma unikatni nezaporne cislo -- identifikator. Protoze je tento identifikator unikatni, casto se vyuziva pro zaruceni unikatnosti napr. docasnych souboru.

Krome beznych procesu existuji take procesy specialni. Proces 0 je napr. planovac, znamy tez jako swapper. S timto procesem nekoresponduje zadny program na disku. Proces 1 je init -- tento proces je inicializovan pri startovaci (bootstrap) procedure. Tomuto procesu odpovidal v drivejsich verzich unixu soubor /etc/init, v novejsich verzich /sbin/init. Procesy, ktere se spousteji pri inicializaci unixu, maji odpovidajici soubory v /etc/rc* a jsou rozdeleny podle jednotlivych stavu systemu. Proces init nikdy neumira. Je to normalni proces (neni uvnitr jadra jako napr. swapper). Na nekterych implementacich unixu je pritomen jeste pagedaemon, ktery zajistuje spravu virtualni pameti. Tento proces cislo 2 je opet v jadre, jako swapper.

Pro identifikator procesu (PID) existuje pocetna skupina funkci:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid (void);
Vraci: ID volajiciho procesu
pid_t getppid (void);
Vraci: ID rodice volajiciho procesu
uid_t getuid (void);
Vraci: ID realneho uzivatele volajiciho procesu
uid_t geteuid (void);
Vraci: ID efektivniho uzivatele volajiciho procesu
gid_t getgid (void);
Vraci: ID realne skupiny volajiciho procesu
gid_t getegid (void);
Vraci: ID efektivni skupiny volajiciho procesu



Funkce fork

Jedinou cestou, jak v unixu vytvorit novy proces, je pouziti funkce fork.

#include <sys/types.h>
#include <unistd.h>
pid_t fork (void);
Vraci: v potomkovi 0, v rodici PID potomka, -1 pri chybe

Novy proces vyprodukovany fork se nazyva potomek (child). Funkce se vola jednou, ale vraci dve hodnoty. Pomoci navratove hodnoty program urci svou totoznost. Vrati-li fork -1, doslo k chybe -- pravdepodobne doslo k prekroceni poctu soubezne bezicich procesu. Vrati-li 0, je zrejme, ze kod, testujici tuto hodnotu, je potomkem. Rodicovskemu (parent) procesu vrati fork identifikacni cislo potomka. Rodic musi disponovat PID potomku pro synchronizaci cinnosti.

Rodic i potomek provadeji stejny kod po volani funkce fork. Je ciste veci programu, jak rozdvojeni procesu osetri.

Priklad:

#include <sys/types.h>

int     glob = 6;     /* external variable in initialized data */
char    buf[] = "a write to stdout\n";

int
main(void)
{
    int      var;        /* automatic variable on the stack */
    pid_t    pid;

    var = 88;
    if(write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
        err_sys("write error");
    printf("before fork\n");    /* we don't flush stdout */

    if ( (pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0) {        /* child */
                glob++;                  /* modify variables */
        var++;
    } else
                sleep(2);                /* parent */

    printf("pid = %d, glob = %d, var = %d\n",getpid(), glob, var);
    exit(0);
}


Sdileni souboru

Dalsim zajimavym rysem programu je sdileni souboru. Pri presmerovani rodice je totiz automaticky presmerovan i potomek. Rikame, ze deskriptory jsou zduplikovany, protoze schema vypada jako po akci dup. Rodic i potomek sdileji stejne tabulky souboru. Z toho plyne zajimavy ukaz -- oba procesy sdileji i current file offset. Viz obr. 8.

Zde ma byt moc hezky obrazek 'poforku', skoda, ze ho nevidite

Obrazek 8: Sdileni otevrenych souboru mezi rodicem a potomkem po fork

Normalne po fork nastavaji dva pripady prace s deskriptory:

  1. Rodic ceka na dobehnuti potomka. V tomto pripade rodic nemusi s deskriptory nic delat.
  2. Rodic i potomek jsou svou vlastni cestou. Rodic vetsinou po volani fork deskriptory uzavre a potomek ma tim volne pole pusobnosti.

Po vyvolani funkce fork maji rodic i potomek spoustu veci spolecnou a to zejmena:

Oba procesy se budou lisit hlavne v techto bodech:

Pro fork existuji dve zakladni vyuziti.

Nektere operacni systemy podporuji slouceny fork a exec -- spawn.



Funkce vfork

Tato funkce neni v kazdem unixu.

Ma stejnou volaci sekvenci i stejne navratove podminky jako fork, ale semantika techto funkci se lisi.

Ukolem vfork je vytvorit novy proces urceny k exec programu. vfork tedy po vytvoreni noveho procesu nezkopiruje jeho adresni prostor -- potomek misto toho bezi v adresnim prostoru rodice.

Nejlepe je rozdil videt na programu:

Priklad:

#include <sys/types.h>

int     glob = 6;  /* external variable in initialized data */

int
main(void)
{
    int      var;     /* automatic variable on the stack */
    pid_t    pid;

    var = 88;
    printf("before vfork\n");    /* we don't flush stdio */

    if ( (pid = vfork()) < 0)
        err_sys("vfork error");
    else if (pid == 0) {        /* child */
                glob++;         /* modify parent's variables */
        var++;
               _exit(0);        /* child terminates */
    }

    /* parent */
    printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
    exit(0);
}
Vzhledem k tomu, ze rodic i potomek bezi ve stejnem adresni prostoru, je potomek schopen zmenit obsahy promennych rodice.

Znovu o ukoncovani procesu

Existuji tri zpusoby, jak korektne ukoncit proces a dve jak ho ukoncit predcasne. Viz kapitolu Ukonceni procesu.

Bez ohledu na to, jak proces skoncil, je spusten vzdy ten samy kod v jadre, ktery uzavre vsechny deskriptory, uvolni pamet po procesu atd. Problem nastava pri synchronizaci techto ukoncovacich procesu.

Vetsinou byva bezne, ze rodic ceka na dokonceni potomka (pomoci funkce wait nebo waitpid). V pripade, ze potomek skonci a rodic se jeste nedostal do stavu, kdy na neho pasivne ceka, stava se z potomka zombie. To znamena, ze potomek uz prakticky neexistuje (ma uz uzavrene deskriptory a odalokovanou pamet), ale je stale evidovan systemem.

Skonci-li rodicovsky proces drive nez potomek, pak potomek prejde na proces init. Rikame, ze init zdedil proces. V tomto stavu se jiz nemuze z potomka stat zombie, protoze kdyz tento potomek skonci, init s touto moznosti pocita a zavola nejakou cekaci funkci, ktera zjisti status ukonceni.



Funkce wait a waitpid

V pripade, ze proces skonci normalne, zasila jadro rodici procesu signal SIGCHLD. Protoze je zanik potomka asynchronni udalosti, je i zaslani signalu asynchronni. Implicitni reakci je ignorovani tohoto signalu. Proces ale muze na signal cekat pomoci funkce wait. Zpracovani programu po volani wait muze mit tri stavy:

Volani wait ma nasledujici syntaxi:

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int *statloc);
pid_t waitpid (pid_t pid, int *statloc, int options);
Obe vraci: PID kdyz OK, 0, -1 pri chybe

Hlavni rozdily mezi funkcemi:

Argument statloc urcuje status ukonceni potomka. Pokud nas tento nezajima, lze nastavit na NULL.

pid u funkce waitpid muze nabyvat techto hodnot:

Pro rizeni cekani jsou definovana makra v souboru <sys/wait.h>. Jsou to makra:

WIFEXITED(status)
je pravda pri normalnim ukonceni programu. Pro zjisteni nizsich 8 bitu navratoveho kodu pouzijeme makro WEXITSTATUS(status).

WIFSIGNALED(status)
je pravda pri abnormalnim ukonceni programu. Pro zjisteni cisla signalu, ktery ukoncil provadeni potomka volame makro WTERMSIG(status).

WIFSTOPPED(status)
je pravda, kdyz potomek byl zastaven. Pro zjisteni cisla signalu, ktery zastavil potomka volame makro WSTOPSIG(status).

Priklad:

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

void
pr_exit(int status)
{
  if (WIFEXITED(status))
      printf("normal termination, exit status = %d\n",
          WEXITSTATUS(status));
  else if (WIFSIGNALED(status))
      printf("abnormal termination, signal number = %d%s\n",
          WTERMSIG(status),
#ifdef    WCOREDUMP
          WCOREDUMP(status) ? " (core file generated)" : "");
#else
          "");
#endif
  else if (WIFSTOPPED(status))
       printf("child stopped, signal number = %d\n",
           WSTOPSIG(status));
}


  Podminky zavodu

Podminky zavodu (anglicky Race conditions)-- to je termin z knihy [7]. Jedna se vlastne o situaci, kdy se vice procesu pokousi v jednom okamziku dostat k systemovym zdrojum.

Typicky program s podminkami zavodu je uveden na prikladu:

Priklad:

#include <sys/types.h>

static void charatatime(char *);

int
main(void)
{
    pid_t   pid;

    if ( (pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0) {
        charatatime("output from child\n");
    } else {
        charatatime("output from parent\n");
    }
    exit(0);
}

static void
charatatime(char *str)
{
    char    *ptr;
    int      c;

    setbuf(stdout, NULL);            /* set unbuffered */
    for (ptr = str; c = *ptr++; )
        putc(c, stdout);
}

V tomto programu dochazi ke kolizi na vystupu a je treba zavest dalsi synchronizacni cinnosti.

Zakladni prostredek pro synchronizaci prinaseji makra TELL_WAIT, TELL_PARENT, TELL_CHILD, WAIT_PARENT a WAIT_CHILD. Pozdeji si ukazeme implementaci techto maker (viz kapitolu Funkce pro priklad a Roury).

Priklad:

#include <sys/types.h>

static void charatatime(char *);

int
main(void)
{
    pid_t    pid;

    TELL_WAIT();

    if ( (pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0) {
        WAIT_PARENT();        /* parent goes first */
        charatatime("output from child\n");
    } else {
        charatatime("output from parent\n");
        TELL_CHILD(pid);
    }
    exit(0);
}

static void
charatatime(char *str)
{
    char    *ptr;
    int        c;

        setbuf(stdout, NULL);         /* set unbuffered */
    for (ptr = str; c = *ptr++; )
        putc(c, stdout);
}



Funkce exec

Jak jsme se zminili v predchozich kapitolach, fork se casto vyuziva spolu s funkci exec pro spousteni jinych programu. Samotne spusteni programu zajisti funkce exec. Tato funkce ma nekolik variant, podle mnozstvi predavanych informaci.

#include <unistd.h>
int execl (const char *pathname, const char *arg0, ... /* (char *) 0 */ );
int execv (const char *pathname, char *const argv[]);
int execle (const char *pathname, const char *arg0, ... /* (char *) 0, char *const envp[] */ );
int execve (const char *pathname, char *const argv[], char *const envp[] );
int execlp (const char *filename, const char *arg0, ... /* (char *) 0 */ );
int execvp (const char *filename, char *const argv[]);
Vsech sest vraci: -1 pri chybe, nic pri uspechu

Prvnim rozdilem mezi funkcemi je pathname a filename.

Drive, nez se pouzivaly prototypy ANSI C, byl normalni zpusob, jak ukazat argumenty prikazove radky pro tri funkce (execl, execle, execlp) takovyto:

  char *arg0, char *arg1, ..., char *argn, (char *) 0

Zde je jasne videt, ze sekvence musi byt ukoncena (char *) 0.

Dalsi predstavu o funkcich exec dava obrazek 9.

Zde ma byt moc hezky obrazek 'execy', skoda, ze ho nevidite

Obrazek 9: Vztahy mezi sesti fukcemi exec

Priklad:

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

char    *env_init[] = { "USER=unknown", "PATH=/tmp", NULL };

int
main(void)
{
  pid_t   pid;

  if ( (pid = fork()) < 0)
      err_sys("fork error");
  else if (pid == 0) { /* specify pathname, specify environment */
          if (execle("/home/stevens/bin/echoall",
                     "echoall", "myarg1", "MY ARG2", (char *) 0,
                     env_init) < 0)
          err_sys("execle error");
  }
  if (waitpid(pid, NULL, 0) < 0)
          err_sys("wait error");

  if ( (pid = fork()) < 0)
          err_sys("fork error");
  else if (pid == 0) { /* specify filename, inherit environment */
          if (execlp("echoall",
                     "echoall", "only 1 arg", (char *) 0) < 0)
               err_sys("execlp error");
  }
  exit(0);
}


Zmena UID a GID

Existuji funkce, pomoci kterych muzeme zmenit efektivni ID uzivatele i skupiny. Jsou to:

#include <sys/types.h>
#include <unistd.h>
int setuid (uid_t uid);
int setgid (gid_t gid);
Obe vraci: 0 kdyz OK, -1 pri chybe

Pravidla, kdy muze proces menit identifikatory (pro GID je to podobne):

  1. Pokud ma proces pravomoce superuzivatele, funkce setuid nastavi realne UID, efektivni UID a uschovane set-UID na uid.
  2. Neni-li superuzivatel, pak musi mit realne UID nebo uschovane set-UID shodne s uid. setuid pak nastavi efektivni UID na uid. Realne UID a uschovane set-UID zustavaji nezmeneny.
  3. Pokud zadna z techto dvou podminek neni splnena, errno je nastavena na EPERM a funkce vraci chybu.


Funkce setreuid a setregid

Systemy BSD podporuji prohozeni realneho UID a efektivniho UID.

#include <sys/types.h>
#include <unistd.h>
int setreuid (uid_t ruid, uid_t euid);
int setregid (gid_t rgid, gid_t egid);
Obe vraci: 0 kdyz OK, -1 pri chybe



Funkce seteuid a setegid

Jsou i funkce, ktere meni jen efektivni ID. Tyto budou pravdepodobne pridany do POSIX.1.

#include <sys/types.h>
#include <unistd.h>
int seteuid (uid_t uid);
int setegid (gid_t gid);
Obe vraci: 0 kdyz OK, -1 pri chybe



Funkce system

Tato funkce je jistou obdobou funkci rady exec. Hlavnim rozdilem je vsak to, ze funkce predava rizeni systemu s argumentem tvaru retezce.

Napr.: system ("date > file");

ANSI C sice tuto funkci definuje, ale pozor, je silne implementacne zavisla.

#include <stdlib.h>
int system (const char *cmdstring);
Vraci: (viz nize)

Vzhledem k tomu, ze se volani system realizuje volanim fork, exec a waitpid, jsou ruzne navratove hodnoty teto funkce:

  1. Pokud zhavaruje fork nebo waitpid, system vrati -1 a errno je nastaveno dle chyby.
  2. Jestlize zhavaruje az exec, pak system vrati stejny kod, jako kdyby shell provedl exit (127).
  3. Jinak vrati system az ukoncovaci status shellu, ve tvaru jako u waitpid.

Priklad:

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

int
system(const char *cmdstring)  /* version without signal handling */
{
    pid_t    pid;
    int      status;

    if (cmdstring == NULL)
        return(1);        /* always a command processor with Unix */

    if ( (pid = fork()) < 0) {
        status = -1;    /* probably out of processes */

    } else if (pid == 0) {                /* child */
        execl("/bin/sh", "sh", "-c", cmdstring, (char *) 0);
        _exit(127);        /* execl error */

    } else {                            /* parent */
        while (waitpid(pid, &status, 0) < 0)
            if (errno != EINTR) {
                status = -1; /* error other than EINTR from waitpid() */
                break;
            }
    }

    return(status);
}


Procesni cas

Kazdy proces ma k dispozici specialni strukturu, ve ktere je uschovavan cas CPU, ktery spotreboval. Tuto strukturu proces obdrzi volanim funkce times.

#include <sys/times.h>
clock_t times (struct tms *buf);
Vraci: cas na hodinach na zdi v ticich kdyz OK, -1 pri chybe

Funkce vyplni strukturu:

struct tms {
  clock_t tms_utime;  /* uzivatelsky cas CPU */
  clock_t tms_stime;  /* systemovy cas CPU */
  clock_t tms_cutime; /* uzivatelsky cas CPU, ukonceny potomek */
  clock_t tms_cstime; /* systemovy cas CPU, ukonceny potomek */
}



Skupiny procesu

Kazdy proces ma nejen svuj identifikator, ale take skupinu, do ktere je zarazen. Skupinu procesu tvori jeden nebo vice procesu. Kazda skupina ma unikatni identifikator. Tento identifikator je obdobny PID a muze byt uchovavan v promenne typu pid_t.

#include <sys/types.h>
#include <unistd.h>
pid_t getgrp(void);
Vraci: ID skupiny procesu volajiciho procesu

Kazda skupina ma svuj vedouci proces. Tento vedouci se pozna podle toho, ze jeho PID je shodne s ID skupiny. Je mozny i stav, kdy vedouci zalozi skupinu a sam zanikne -- skupina pak zanika az po zaniku vsech ostatnich procesu dane skupiny.

Proces se muze pripojit do skupiny procesu pomoci funkce:

#include <sys/types.h>
#include <unistd.h>
int setpgrp(pid_t pid, pid_t pgid);
Vraci: 0 kdyz OK, -1 pri chybe

Jestlize se oba argumenty shoduji, proces se stava vedoucim skupiny.



Seance

Seance (session) je balik jedne nebo vice skupin procesu. Priklad viz obr. 10.

Zde ma byt moc hezky obrazek 'prseance', skoda, ze ho nevidite

Obrazek 10: Priklad seance procesu

Tento stav se dosahne napr. timto postupem:

    proc1 | proc2 &
    proc3 | proc4 | proc5

Procesy zalozi novou skupinu volanim:

#include <sys/types.h>
#include <unistd.h>
pid_t setpgrp(void);
Vraci: ID skupiny procesu kdyz OK, -1 pri chybe

Volajici proces se nestava vedoucim skupiny, tato funkce vytvori novou seanci. V praxi nasleduje toto:

  1. Proces se stava vedoucim seance.
  2. Proces se stava vedoucim skupiny procesu.
  3. Proces nema prirazen zadny ridici terminal. Jestlize mel proces prirazen ridici terminal, je toto prirazeni zruseno.

Funkce vrati chybu, jestlize je proces jiz vedoucim skupiny.



Ridici terminal

Existuji jeste dalsi charakteristiky seanci a procesnich skupin:

Jsou pripady, kdy chce program poslat neco na ridici terminal bez ohledu na to, kam ma presmerovany vystup. Jedinou cestou, jak to zarucit, je pouziti specialniho souboru /dev/tty, coz je synonymum pro ridici terminal. Jestlize ale proces nema prirazen ridici terminal, pak open skonci s chybou.



Funkce tcgetgrp a tcsetgrp

Potrebujeme zpusob, jak oznamit jadru, ktera procesni skupina pracuje na popredi, aby radic terminalu vedel, kam posilat informace.

#include <sys/types.h>
#include <unistd.h>
pid_t tcgetgrp(int filedes);
Vraci: ID skupiny procesu na popredi, -1 pri chybe
pid_t tcsetgrp(int filedes, pid_t pgrpid);
Vraci: 0 kdyz OK, -1 pri chybe

Funkce tcgetgrp vrati identifikator procesni skupiny, ktera bezi na popredi a ma terminal prirazeny na deskriptoru filedes. Funkce tcsetgrp priradi terminal procesni skupine pgrpid (jestlize ma ovsem proces prirazeny ridici terminal).



Cviceni

  1. Napiste program, ktery vytvori zombie, a pak pouzijte funkci system ke spusteni prikazu ps(1), kterym zkontrolujete, zda jste opravdu vytvorili zombie.
  2. Napiste maly program, ktery zavola fork a jeho potomek vytvori novou seanci (session). Overte, ze se potomek stane vedoucim skupiny procesu a ze potomek jiz nema pridelen ridici terminal.


dalsi predchozi obsah
Dalsi: Signaly Predchozi: Prostredi procesu

Ladislav Dobias
Sat Nov 1 15:38:32 MET 1997