4. Prosessinhallinta
Sisältö:
4 Prosessinhallinta
Prosessinhallinnan systeemikutsut:
- uuden prosessin luonti fork()
- prosessin koodinvaihto exec()
- prosessin lopettaminen exit()
- prosessin päättymisen odotus wait(), waitpid()
4.1 Prosession luonti
Prosessi luodaan funktiolla pid_t *fork(void);, joka palauttaa kutsuvalle prosessille lapsiprosessin numeron ja lapsiprosessille 0. fork()-kutsun tuloksena syntyy uusi prosessi, joka on 'klooni' äitiprosessista.
- yhteinen koodi
- samat muuttujien arvot
- samat prosessinkuvaajan perustiedot
- yhteiset avoimet tiedostot
Kummallakin on kuitenkin:
- oma data-alue
- oma prosessinkuvaaja
fork()-kutsun jälkeen ei voi olla varma siitä kumpi prosessi (äiti vai lapsi) jatkaa aiemmin suoritustaan. Jos järjestys tärkeää, on ohjelmoitava itse synkronointi ( Esim 1.
#include < sys/types.h > #include < unistd.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) perror("write error"); printf("before fork\n"); /* we don't flush stdout */ if ( (pid = fork()) < 0) perror("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); }
Lapsiprosessi luodaan, kun:
- halutaan suorittaa äiti- ja lapsiprosessissa erillinen osa samassa tiedostossa olevasta koodista. Esim. verkkosovelluksissa on tavallista, että palvelija luo lapsiprosessin antamaan palvelua ja jää itse odottamaan uusia palvelupyyntöjä.
- halutaan suorittaa kokonaan toinen ohjelma. Tällöin fork()-kutsun jälkeen on lapsiprosessissa myös exec()-kutsu, eli se vaihtaa suoritettavaa koodia. Esim. komentotulkit käyttävät tätä menetelmää.
Lapsi perii äidiltä kaikki avoimet tiedostokuvaajat. Sekä äiti että lapsi käyttävät yhteistä avoimet tiedostot taulun alkiota niillä on yhteinen luku / kirjoituspositio.
4.2 Prosession päättyminen
Prosessin suoritus päättyy normaalisti, kun suoritetaan:
- funktiossa main() funktio return(status)
- funktio exit(status) tai
- funktio _exit(status)
exit() kutsuu funktiolla atexit() rekisteröityjä funktioita ja purkaa stdio:n puskurit (vrt. funktio flush()). _exit() hoitelee Unix-spesifiset lopputoimet Prosessin suoritus voi päättyä myös 'epänormaalisti', kun kutsutaan funktiota abort() tai prosessi saa signaalin, johon se ei varautunut tai ei voi varautua. Epänormaaleissa päättymisissä generoi ydin paluuarvon.
Unix-spesifisiin lopputoimiin kuuluu tiedostojen sulkeminen, muistitilan vapauttaminen sekä äitiprosessin signalointi, mutta prosessinkuvaaja jää vielä olemaan ("zombie"). Koska paluuarvon välittäminen äitiprosessille ja laskutustietojen kokoaminen on vielä kesken.
Jos äitiprosessi on päättynyt ennen lapsiprosessia, merkitsee ydin zombien äidiksi prosessin 1 (init). Se kokoaa laskutustiedot ja vapauttaa prosessinkuvaajan.
Kun prosessi päättyy, saa äiti aina signaalin SIGCHLD. Oletusarvo on, että äiti ei välitä tästä signaalista. Äiti voi pysähtyä odottamaan lapsen päättymistä funktioon wait() tai waitpid():
#include < sys/types.h > #include < sys/wait.h > pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); Palauttaa: päättyneen prosessin pid, parametrissa statloc on päättyneen lapsen statusJos lapsiprosessi on jo päättynyt, pääsee äitiprosessi heti jatkamaan. Funktiolla waitpid() voi määrätä odotettavaksi jonkin tietyn prosessin päättymistä, kun taas funktiolla wait() odotetaan minkä tahansa lapsen päättymistä.
- pid == -1 odota mitä tahansa lapsiprosessia
- pid > 0 odota lasta, jolla ko. pid
- pid == 0 odota mitä tahansa äidin kanssa samaan prosessiryhmän kuuluvaa lasta
- pid < -1 odota prosessiryhmään |pid| kuuluvaa prosessia
4.3 Prosessin koodin vaihto
Prosessi vaihtaa suoritettavaa koodia funktiolla exec(). Siitä on kuusi erilaista muotoa, jotka eroavat komentoriviargumenttien ja ympäristömuuttujien välityksessä. Koodia etsitään annetun polkunimen perusteella funktioissa execl(), execv(), execle(), execve() tai tiedostonimen perusteella ympäristömuuttujassa PATH luetelluista hakemistoista fuktioissa execlp() tai execvp();.Koodille voi välittää komentoriviargumentteja joko listana (execl()) tai vektorina (execv()). Koodille voi välittää edellisten lisäksi myös haluamansa ympäristömuuttujat aina vektorina execle() tai execve() tai äidin ympäristömuuttujat periytyvät lapselle sellaisenaan environ-muuttujasta.
#include < unistd.h > int execl(const char *pathname, const char *arg0, ... /* NULL */); int execv(const char *pathname, char *const argv[]); int execle(const char *pathname, const char *arg0, ... /* NULL, char *const envp[] */); int execve(const char *pathname, char *const argv[], char *const envp[]); int execlp(const char *filename, const char *arg0, ... /* NULL */) int execvp(const char *filename, char *const argv[]);
#include < sys/types.h > #include < sys/wait.h > #include < unistd.h > char *env_init[] = { "USER=unknown", "PATH=/tmp", NULL }; int main(void) { pid_t pid; if ( (pid = fork()) < 0) perror("fork error"); else if (pid == 0) { /* specify pathname, specify environment */ if (execle("/bin", "echo", "myarg1", "MY ARG2", (char *) 0, env_init) < 0) perror("execle error"); } if (waitpid(pid, NULL, 0) < 0) perror("wait error"); if ( (pid = fork()) < 0) perror("fork error"); else if (pid == 0) { /* specify filename, inherit environment */ if (execlp("echo", "echo", "only 1 arg", (char *) 0) < 0) perror("execlp error"); } exit(0); }
4.4 Prosessin ominaisuudet
4.5 Prosessin tunnistus
Prosessin oikeudet määräävät mitä tiedostoja prosessi saa käyttää ja mihin muihin prosesseihin prosessi voi vaikuttaa. Oikeudet tarkistetaan prosessin euid ja egid tietojen perusteella (e = effective). Prosessi voi toimia sen käynnistäjän oikeuksin, jolloin euid = uid, käynnistäjän ryhmän oikeuksin, jolloin egid = gid (jokin ryhmä johon käyttäjä kuuluu) tai exec()-kutsussa saaduin oikeuksin (setuid, setgid), jolloin euid = kooditiedoston uid ja egid = kooditiedoston gid.Todellinen (real) käyttäjä uid ja todelliset ryhmät gid ja groups saadaan istuntoa käynnistettäessä salasanatiedostosta. Vain root voi muuttaa näitä. Omistaja- ja ryhmänumeroita voi muuttaa tietyin edellytyksin funktioilla:
#include#include int setuid(uid_t uid); int setgid(gid_t gid); int setreuid(uid_t ruid, uid_t euid); int setregid(gid_t rgid, gid_t egid); int seteuid(uid_t uid); int setegid(gid_t gid);
root saa tietenkin käyttää kaikkia uid ja gid arvoja. Prosessin todellinen omistaja voi aina asettaa funktioilla setuid() ja setgid(). Funktiot seteuid() ja setegid() toimivat tavallisilla käyttäjillä kuten setuid() ja setgid(). rootin käytössä setuid() asettaa aina:
- uid = annettu uid
- euid = annettu uid
- saved-uid = annettu uid
Kun taas seteuid() asettaa vain euid = annettu uid. Ryhmäjutut vastaavasti.
4.6 Prosessin kuluttama aika
Prosessi voi kysellä kuluttamaansa reaaliaikaa ja CPU- aikaa funktiolla times():
#include < sys/times.h > struct tms { clock_t tms_utime; käyttäjätilassa clock_t tms_stime; etuoikeutetussa tilassa clock_t tms_cutime; summa lasten ajoista clock_t tms_cutime; summa lasten ajoista }; clock_t times(struct tms *buf); Palauttaa: reaaliaika tiksauksina
Esimerkki (Esim 2 )
#include < stdio.h > #include < sys/times.h > #include < unistd.h > static void pr_times(clock_t, struct tms *, struct tms *); static void do_cmd(char *); int main(int argc, char *argv[]) { int i; for (i = 1; i < argc; i++) do_cmd(argv[i]); /* once for each command-line arg */ exit(0); } static void do_cmd(char *cmd) /* execute and time the "cmd" */ { struct tms tmsstart, tmsend; clock_t start, end; int status; fprintf(stderr, "\ncommand: %s\n", cmd); if ( (start = times(&tmsstart)) == -1) /* starting values */ perror("times error"); if ( (status = system(cmd)) < 0) /* execute command */ perror("system() error"); if ( (end = times(&tmsend)) == -1) /* ending values */ perror("times error"); pr_times(end-start, &tmsstart, &tmsend); exit(status); } static void pr_times(clock_t real, struct tms *tmsstart, struct tms *tmsend) { static long clktck = 0; if (clktck == 0) /* fetch clock ticks per second first time */ if ( (clktck = sysconf(_SC_CLK_TCK)) < 0) perror("sysconf error"); fprintf(stderr, " real: %7.2f\n", real / (double) clktck); fprintf(stderr, " user: %7.2f\n", (tmsend->tms_utime - tmsstart->tms_utime) / (double) clktck); fprintf(stderr, " sys: %7.2f\n", (tmsend->tms_stime - tmsstart->tms_stime) / (double) clktck); fprintf(stderr, " child user: %7.2f\n", (tmsend->tms_cutime - tmsstart->tms_cutime) / (double) clktck); fprintf(stderr, " child sys: %7.2f\n", (tmsend->tms_cstime - tmsstart->tms_cstime) / (double) clktck); }
4.7 Prosessien sukulais- ja omistussuhteet
Init-prosessi on kaikkien prosessin alku ja sen pid == 1. 'Adoptoi' lapsipro- sessin, jos sen oma äiti on kuollut.
Jokainen prosessi kuuluu johonkin prosessiryhmään. Rymän tunnisteena on ryhmän johtoprosessin pid. Vaikka johtoprosessi päättyisikin, jää prosessiryhmä voimaan. Prosessiryhmän numeroa voi kysyä funktiolla pid_t getpgrp(void); . Prosessi voi asettaa funktiolla pid_t setpgid(pid_t pid, pid_t pgid); itsensä tai lapsiprosessinsa prosessiryhmän tai voi ryhtyä itse uuden prosessiryhmän johtoprosessiksi. Jos parametrit ovat samoja, syntyy uusi prosessiryhmä. Jos pid = 0 tai pgid = 0, käytetään prosessin omaa numeroa. Komentotulkin tasolla voi tappaa koko ryhmän tappamalla työn johtoprosessin, esim:
$ kill %1Istunto tarkoittaa joukkoa käynnistettyjä prosesseja. Siihen liittyy läheisesti töiden hallinta (jobs(1), fg(1), bg(1), kill(1)). Istunto koostuu yhdestä tai useammasta prosessiryhmästä.
Prosessi voi julistautua uuden istunnon johtajaksi funktiolla pid_t setsid(void); setsid() perustaa uuden prosessiryhmän, ja kutsunut prosessi on sen johtoprosessi. Samalla menetetään mahdollinen kontrollipääte. Tavallinen käyttötapa: äiti luo lapsiprosessin, kuolee itse pois ja lapsiprosessi julistautuu istunnon johtoprosessiksi.
Istuntoon voi liittyä kontrollipääte, josta luetaan syötteitä ja jonne voidaan tulostaa. Vain yksi prosessiryhmä voi olla kerrallaan edustalla, ts. voi ottaa vastaan syötteitä näppäimistöltä ja tulostaa näytölle. Muut prosessiryhmät ovat taustalla. Kaikki istunnon prosessiryhmät voivat kuitenkin käyttää istuntoon liittyvää kontrollipäätettä suoraan sen tiedotonimen /dev/tty kautta. Edustaprosessi voi asettaa / selvittää prosessiryhmän funktioilla tcgetpgrp() ja tcsetpgrp():
#include#include pid_t tcgetpgrp(int filedes); Palauttaa: edustaprosessin prosessiryhmä int tcsetpgrp(int filedes, pid_t pgrpid);
Ydin tarvitsee näitä mm. selvittääkseen mille prosessille syötteet annetaan ja mille prosessille lähetetään mahdolliset signaalit. filedes osoittaa kontrollipäätteeseen.
Kontrollipäätemäärittelyjä ei tarvitse yleensä murehtia, sillä ne määrittyy automaattisesti, kun järjestelmään logataan sisään ja kun prosesseja käynnistetään komentotulkista (töidenhallinta).
Jos taustalla oleva työ haluaa tulostaa näytölle tai lukea näppäimistöltä, se jää odottamaan edustalle pääsyä.
Jan Lindström (Jan.Lindstrom@cs.Helsinki.FI)