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é.
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 |
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);
}
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.
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:
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.
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.
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í.
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:
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 (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); }
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.
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);
}
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é):
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ě |
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ě |
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:
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);
}
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 */ }
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 (session) je balík jedné nebo více skupin procesů. Příklad viz obr. 10.
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:
Funkce vrátí chybu, jestliže je proces již vedoucím skupiny.
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.
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).
Ladislav Dobias