další předchozí obsah
Další: Signály Předchozí: Prostředí procesů

Řízení procesů





Úvod

Nyní se již můžeme věnovat správě a řízení procesů. V této kapitole budeme rozebírat vytváření nových procesů, spouštění programů a ukončování programů. Probereme také identifikátory procesů -- reálné, efektivní i uschované.



Identifikátory procesů

Každý proces má unikátní nezáporné číslo -- identifikátor. Protože je tento identifikátor unikátní, často se využívá pro zaručení unikátnosti např. dočasných souborů.

Kromě běžných procesů existují také procesy speciální. Proces 0 je např. plánovač, známý též jako swapper. S tímto procesem nekoresponduje žádný program na disku. Proces 1 je init -- tento proces je inicializován při startovací (bootstrap) proceduře. Tomuto procesu odpovídal v dřívějších verzích unixu soubor /etc/init, v novějších verzích /sbin/init. Procesy, které se spouštějí při inicializaci unixu, mají odpovídající soubory v /etc/rc* a jsou rozděleny podle jednotlivých stavů systému. Proces init nikdy neumírá. Je to normální proces (není uvnitř jádra jako např. swapper). Na některých implementacích unixu je přítomen ještě pagedaemon, který zajišťuje správu virtuální paměti. Tento proces číslo 2 je opět v jádře, jako swapper.

Pro identifikátor procesu (PID) existuje početná skupina funkcí:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid (void);
Vrací: ID volajícího procesu
pid_t getppid (void);
Vrací: ID rodiče volajícího procesu
uid_t getuid (void);
Vrací: ID reálného uživatele volajícího procesu
uid_t geteuid (void);
Vrací: ID efektivního uživatele volajícího procesu
gid_t getgid (void);
Vrací: ID reálné skupiny volajícího procesu
gid_t getegid (void);
Vrací: ID efektivní skupiny volajícího procesu



Funkce fork

Jedinou cestou, jak v unixu vytvořit nový proces, je použití funkce fork.

#include <sys/types.h>
#include <unistd.h>
pid_t fork (void);
Vrací: v potomkovi 0, v rodiči PID potomka, -1 při chybě

Nový proces vyprodukovaný fork se nazývá potomek (child). Funkce se volá jednou, ale vrací dvě hodnoty. Pomocí návratové hodnoty program určí svou totožnost. Vrátí-li fork -1, došlo k chybě -- pravděpodobně došlo k překročení počtu souběžně běžících procesů. Vrátí-li 0, je zřejmé, že kód, testující tuto hodnotu, je potomkem. Rodičovskému (parent) procesu vrátí fork identifikační číslo potomka. Rodič musí disponovat PID potomků pro synchronizaci činnosti.

Rodič i potomek provádějí stejný kód po volání funkce fork. Je čistě věcí programu, jak rozdvojení procesů ošetří.

Příklad:

#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);
}


Sdílení souborů

Dalším zajímavým rysem programu je sdílení souborů. Při přesměrování rodiče je totiž automaticky přesměrován i potomek. Říkáme, že deskriptory jsou zduplikovány, protože schéma vypadá jako po akci dup. Rodič i potomek sdílejí stejné tabulky souborů. Z toho plyne zajímavý úkaz -- oba procesy sdílejí i current file offset. Viz obr. 8.

Zde má být moc hezký obrázek 'poforku', škoda, že ho nevidíte

Obrázek 8: Sdílení otevřených souborů mezi rodičem a potomkem po fork

Normálně po fork nastávají dva případy práce s deskriptory:

  1. Rodič čeká na doběhnutí potomka. V tomto případě rodič nemusí s deskriptory nic dělat.
  2. Rodič i potomek jsou svou vlastní cestou. Rodič většinou po volání fork deskriptory uzavře a potomek má tím volné pole působnosti.

Po vyvolání funkce fork mají rodič i potomek spoustu věcí společnou a to zejména:

Oba procesy se budou lišit hlavně v těchto bodech:

Pro fork existují dvě základní využití.

Některé operační systémy podporují sloučený fork a exec -- spawn.



Funkce vfork

Tato funkce není v každém unixu.

Má stejnou volací sekvenci i stejné návratové podmínky jako fork, ale sémantika těchto funkcí se liší.

Úkolem vfork je vytvořit nový proces určený k exec programu. vfork tedy po vytvoření nového procesu nezkopíruje jeho adresní prostor -- potomek místo toho běží v adresním prostoru rodiče.

Nejlépe je rozdíl vidět na programu:

Příklad:

#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, že rodič i potomek běží ve stejném adresní prostoru, je potomek schopen změnit obsahy proměnných rodiče.

Znovu o ukončování procesů

Existují tři způsoby, jak korektně ukončit proces a dvě jak ho ukončit předčasně. Viz kapitolu Ukončení procesu.

Bez ohledu na to, jak proces skončil, je spuštěn vždy ten samý kód v jádře, který uzavře všechny deskriptory, uvolní paměť po procesu atd. Problém nastává při synchronizaci těchto ukončovacích procesů.

Většinou bývá běžné, že rodič čeká na dokončení potomka (pomocí funkce wait nebo waitpid). V případě, že potomek skončí a rodič se ještě nedostal do stavu, kdy na něho pasivně čeká, stává se z potomka zombie. To znamená, že potomek už prakticky neexistuje (má už uzavřené deskriptory a odalokovanou paměť), ale je stále evidován systémem.

Skončí-li rodičovský proces dříve než potomek, pak potomek přejde na proces init. Říkáme, že init zdědil proces. V tomto stavu se již nemůže z potomka stát zombie, protože když tento potomek skončí, init s touto možností počítá a zavolá nějakou čekací funkci, která zjistí status ukončení.



Funkce wait a waitpid

V případě, že proces skončí normálně, zasílá jádro rodiči procesu signál SIGCHLD. Protože je zánik potomka asynchronní událostí, je i zaslání signálu asynchronní. Implicitní reakcí je ignorování tohoto signálu. Proces ale může na signál čekat pomocí funkce wait. Zpracování programu po volání wait může mít tři stavy:

Volání wait má následující syntaxi:

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int *statloc);
pid_t waitpid (pid_t pid, int *statloc, int options);
Obě vrací: PID když OK, 0, -1 při chybě

Hlavní rozdíly mezi funkcemi:

Argument statloc určuje status ukončení potomka. Pokud nás tento nezajímá, lze nastavit na NULL.

pid u funkce waitpid může nabývat těchto hodnot:

Pro řízení čekání jsou definována makra v souboru <sys/wait.h>. Jsou to makra:

WIFEXITED(status)
je pravda při normálním ukončení programu. Pro zjištění nižších 8 bitů návratového kódu použijeme makro WEXITSTATUS(status).

WIFSIGNALED(status)
je pravda při abnormálním ukončení programu. Pro zjištění čísla signálu, který ukončil provádění potomka voláme makro WTERMSIG(status).

WIFSTOPPED(status)
je pravda, když potomek byl zastaven. Pro zjištění čísla signálu, který zastavil potomka voláme makro WSTOPSIG(status).

Příklad:

#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));
}


  Podmínky závodu

Podmínky závodu (anglicky Race conditions)-- to je termín z knihy [7]. Jedná se vlastně o situaci, kdy se více procesů pokouší v jednom okamžiku dostat k systémovým zdrojům.

Typický program s podmínkami závodu je uveden na příkladu:

Příklad:

#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 dochází ke kolizi na výstupu a je třeba zavést další synchronizační činnosti.

Základní prostředek pro synchronizaci přinášejí makra TELL_WAIT, TELL_PARENT, TELL_CHILD, WAIT_PARENT a WAIT_CHILD. Později si ukážeme implementaci těchto maker (viz kapitolu Funkce pro příklad a Roury).

Příklad:

#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 zmínili v předchozích kapitolách, fork se často využívá spolu s funkcí exec pro spouštění jiných programů. Samotné spuštění programu zajistí funkce exec. Tato funkce má několik variant, podle množství předávaných informací.

#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[]);
Všech šest vrací: -1 při chybě, nic při úspěchu

Prvním rozdílem mezi funkcemi je pathname a filename.

Dříve, než se používaly prototypy ANSI C, byl normální způsob, jak ukázat argumenty příkazové řádky pro tři funkce (execl, execle, execlp) takovýto:

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

Zde je jasně vidět, že sekvence musí být ukončena (char *) 0.

Další představu o funkcích exec dává obrázek 9.

Zde má být moc hezký obrázek 'execy', škoda, že ho nevidíte

Obrázek 9: Vztahy mezi šesti fukcemi exec

Příklad:

#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);
}


Změna UID a GID

Existují funkce, pomocí kterých můžeme změnit efektivní ID uživatele i skupiny. Jsou to:

#include <sys/types.h>
#include <unistd.h>
int setuid (uid_t uid);
int setgid (gid_t gid);
Obě vrací: 0 když OK, -1 při chybě

Pravidla, kdy může proces měnit identifikátory (pro GID je to podobné):

  1. Pokud má proces pravomoce superuživatele, funkce setuid nastaví reálné UID, efektivní UID a uschované set-UID na uid.
  2. Není-li superuživatel, pak musí mít reálné UID nebo uschované set-UID shodné s uid. setuid pak nastaví efektivní UID na uid. Reálné UID a uschované set-UID zůstávají nezměněny.
  3. Pokud žádná z těchto dvou podmínek není splněna, errno je nastavena na EPERM a funkce vrací chybu.


Funkce setreuid a setregid

Systémy BSD podporují prohození reálného UID a efektivního UID.

#include <sys/types.h>
#include <unistd.h>
int setreuid (uid_t ruid, uid_t euid);
int setregid (gid_t rgid, gid_t egid);
Obě vrací: 0 když OK, -1 při chybě



Funkce seteuid a setegid

Jsou i funkce, které mění jen efektivní ID. Tyto budou pravděpodobně přidány do POSIX.1.

#include <sys/types.h>
#include <unistd.h>
int seteuid (uid_t uid);
int setegid (gid_t gid);
Obě vrací: 0 když OK, -1 při chybě



Funkce system

Tato funkce je jistou obdobou funkcí řady exec. Hlavním rozdílem je však to, že funkce předává řízení systému s argumentem tvaru řetězce.

Např.: system ("date > file");

ANSI C sice tuto funkci definuje, ale pozor, je silně implementačně závislá.

#include <stdlib.h>
int system (const char *cmdstring);
Vrací: (viz níže)

Vzhledem k tomu, že se volání system realizuje voláním fork, exec a waitpid, jsou různé návratové hodnoty této funkce:

  1. Pokud zhavaruje fork nebo waitpid, system vrátí -1 a errno je nastaveno dle chyby.
  2. Jestliže zhavaruje až exec, pak system vrátí stejný kód, jako kdyby shell provedl exit (127).
  3. Jinak vrátí system až ukončovací status shellu, ve tvaru jako u waitpid.

Příklad:

#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);
}


Procesní čas

Každý proces má k dispozici speciální strukturu, ve které je uschováván čas CPU, který spotřeboval. Tuto strukturu proces obdrží voláním funkce times.

#include <sys/times.h>
clock_t times (struct tms *buf);
Vrací: čas na hodinách na zdi v ticích když OK, -1 při chybě

Funkce vyplní strukturu:

struct tms {
  clock_t tms_utime;  /* uživatelský čas CPU */
  clock_t tms_stime;  /* systémový čas CPU */
  clock_t tms_cutime; /* uživatelský čas CPU, ukončený potomek */
  clock_t tms_cstime; /* systémový čas CPU, ukončený potomek */
}



Skupiny procesů

Každý proces má nejen svůj identifikátor, ale také skupinu, do které je zařazen. Skupinu procesů tvoří jeden nebo více procesů. Každá skupina má unikátní identifikátor. Tento identifikátor je obdobný PID a může být uchováván v proměnné typu pid_t.

#include <sys/types.h>
#include <unistd.h>
pid_t getgrp(void);
Vrací: ID skupiny procesů volajícího procesu

Každá skupina má svůj vedoucí proces. Tento vedoucí se pozná podle toho, že jeho PID je shodné s ID skupiny. Je možný i stav, kdy vedoucí založí skupinu a sám zanikne -- skupina pak zaniká až po zániku všech ostatních procesů dané skupiny.

Proces se může připojit do skupiny procesů pomocí funkce:

#include <sys/types.h>
#include <unistd.h>
int setpgrp(pid_t pid, pid_t pgid);
Vrací: 0 když OK, -1 při chybě

Jestliže se oba argumenty shodují, proces se stává vedoucím skupiny.



Seance

Seance (session) je balík jedné nebo více skupin procesů. Příklad viz obr. 10.

Zde má být moc hezký obrázek 'prseance', škoda, že ho nevidíte

Obrázek 10: Příklad seance procesů

Tento stav se dosáhne např. tímto postupem:

    proc1 | proc2 &
    proc3 | proc4 | proc5

Procesy založí novou skupinu voláním:

#include <sys/types.h>
#include <unistd.h>
pid_t setpgrp(void);
Vrací: ID skupiny procesů když OK, -1 při chybě

Volající proces se nestává vedoucím skupiny, tato funkce vytvoří novou seanci. V praxi následuje toto:

  1. Proces se stává vedoucím seance.
  2. Proces se stává vedoucím skupiny procesů.
  3. Proces nemá přiřazen žádný řídící terminál. Jestliže měl proces přiřazen řídící terminál, je toto přiřazení zrušeno.

Funkce vrátí chybu, jestliže je proces již vedoucím skupiny.



Řídící terminál

Existují ještě další charakteristiky seancí a procesních skupin:

Jsou případy, kdy chce program poslat něco na řídící terminál bez ohledu na to, kam má přesměrovaný výstup. Jedinou cestou, jak to zaručit, je použití speciálního souboru /dev/tty, což je synonymum pro řídící terminál. Jestliže ale proces nemá přiřazen řídící terminál, pak open skončí s chybou.



Funkce tcgetgrp a tcsetgrp

Potřebujeme způsob, jak oznámit jádru, která procesní skupina pracuje na popředí, aby řadič terminálu věděl, kam posílat informace.

#include <sys/types.h>
#include <unistd.h>
pid_t tcgetgrp(int filedes);
Vrací: ID skupiny procesů na popředí, -1 při chybě
pid_t tcsetgrp(int filedes, pid_t pgrpid);
Vrací: 0 když OK, -1 při chybě

Funkce tcgetgrp vrátí identifikátor procesní skupiny, která běží na popředí a má terminál přiřazený na deskriptoru filedes. Funkce tcsetgrp přiřadí terminál procesní skupině pgrpid (jestliže má ovšem proces přiřazený řídící terminál).



Cvičení

  1. Napiště program, který vytvoří zombie, a pak použijte funkci system ke spuštění příkazu ps(1), kterým zkontrolujete, zda jste opravdu vytvořili zombie.
  2. Napište malý program, který zavolá fork a jeho potomek vytvoří novou seanci (session). Ověřte, že se potomek stane vedoucím skupiny procesů a že potomek již nemá přidělen řídící terminál.


další předchozí obsah
Další: Signály Předchozí: Prostředí procesů

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