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.
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 |
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);
}
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.
Obrazek 8:
Sdileni otevrenych souboru mezi rodicem a potomkem po fork
Normalne po fork nastavaji dva pripady prace s deskriptory:
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.
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.
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.
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:
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 (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); }
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.
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);
}
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):
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 |
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 |
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:
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);
}
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 */ }
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 (session) je balik jedne nebo vice skupin procesu. Priklad viz obr. 10.
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:
Funkce vrati chybu, jestlize je proces jiz vedoucim skupiny.
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.
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).
Ladislav Dobias