Prosessi

Kun ohjelma otetaan suoritukseen, muodostaa käyttöjärjestelmä suoritusta varten prosessin. Prosessi muodostuu seuraavista elementeistä:
  • Ohjelman osoiteavaruus
  • Ohjelmakoodi
  • Ohjelman muuttujat
  • Pino
  • Prosessorin tila, eli CPU:n rekisterien (yleiskäyttöiset rekisterit, PC, SP, PSW, ym.) arvot
  • Tiedot ohjelman avaamista tiedostoista ja muista ohjelmalle varatuista resursseista (putket, signaalikäsittelijät, jaetut muistialueet ym.)
  • Moniajokäyttöjärjestelmä toimii karkeasti ottaen siten, että jokaista ajettavaa ohjelmaa varten on oma prosessi. Käyttöjärjestelmä vuorottelee prosesseja suoritusvuorossa prosessorilla ja näin syntyy illuusio siitä, että koneessa tapahtuu monia asioita yhtä aikaa vaikka prosessoreja olisi vain yksi. Käyttöjärjestelmä järjestää asiat siten, että jokainen prosessi luulee olevansa ainoa koneen käyttäjä. Prosessien (ja sovellusohjelmoijien) ei tarvitse olla huolissaan siitä, että välillä CPU:lla onkin suoritusvuorossa jokin toinen prosessi.

    Komento ps

    Suorituksessa olevia prosesseja voi tutkia komentotulkin komennolla ps.

    ps ilman mitään optioita näyttää ainoastaan kyseisestä komentotulkista käynnistetyt, suorituksessa olevat prosessit

    [luuma@telinux1]$ ps
      PID TTY          TIME CMD
    23716 pts/105  00:00:00 bash
    26607 pts/105  00:00:00 ps
    [luuma@telinux1]$
    
    Edellä näytettiin prosessi, joka suorittaa komentotulkkia eli bash:ia sekä prosessi, joka suoritti ps-komennon. Jokaisella prosessilla on sen yksikäsitteisesti erittelevä numero, prosessi id, eli PID. Edellä bashia suorittavan prosessin PID oli 23716 ja ps-komentoa suorittavan prosessin PID 26607.

    Komentotulkki on siis myös prosessi. Aina kun komentotulkista käynnistetään joku ohjelma, esim. [luuma@telinux1]$ ps, luo komentotulkki uuden prosessin, joka alkaa suorittamaan käynnistettävää ohjelmaa.

    ps-komennolla on todella suuri määrä optioita. Esim. seuravat muodot ovat hyödyllisiä:

    ps ax		näytä järjestelmän kaikki prosessit
    ps axu		kaikki prosessit, "u"-muodossa eli mm. muistin ja cpu:n käyttö
    ps axl		kaikki prosessit, "l"-muodossa eli mm. prioriteetit
    ps u Uluuma	näytä käyttäjän luuma kaikki prosessit "u"-muodossa
    ps l Uluuma	näytä käyttäjän luuma kaikki prosessit "l"-muodossa
    
    Huom: kaksi viimeistä eivät välttämättä toimi opiskelijoiden käyttäjätunnuksella (ainakaan cs.stadia.fi:ssa). Omat prosessit saa näytettyä myös listaamalla kaikki prosessit ja rajoittamalla näytettävät grep-komennolla.
    ps axu | grep luuma
    

    Komento pstree näyttää prosessilistan siten, että prosessien väliset suhteet tulevat näkyviin:

    [luuma@telinux1]$ pstree luuma
    bash---man---less
    
    bash---pstree
    
    bash---emacs
    
    Käyttäjä luuma on pstreen perusteella kirjautunut koneeseen kolmelle eri terminaalille. Jokainen terminaali suorittaa komentotulkkia eli bashia. Yhdessä terminaalissa on avattuna emacs, yhdessä suoritetaan pstree-komentoa ja yhdessä man:nia joka taas käyttää less-ohjelmaa tulostuksen muotoiluun. Komento pstree ilman argumenttia kertoo kaikista järjestelmän prosesseista, ote seuraavassa:
    init-+-amavisd---2*[amavisd]
         |-3*[bash]
         |-klogd
         |-kscand
         |-ksoftirqd/0
         |-ksoftirqd/1
         |-kswapd
         |-sshd-+-sshd---bash---ssh
         |      |-12*[sshd---bash---screen]
         |      |-7*[sshd---bash]
         |      |-3*[sshd---bash---emacs]
         |      |-sshd---bash---screen---screen---irssi
         |      |-sshd---bash---pstree
         |      `-sshd---bash---nano
    
    Puun juuressa on kaikkien prosessien äiti init, jonka lapsina on erinäinen määrä prosesseja, mm. sshd, joka huolehtii kaikista ssh:lla sisään kirjautumisista. Systeemiin on kirjautunt 12 käyttäjää, jotka suorittavat komentotulkissa screeniä, kolme emacsin käyttäjää jne.

    Prosessien luominen ohjelmakoodista

    Kun Linux käynnistyy, ladataan muistiin prosessi init, jonka PID on 1. Kaikki muut järjestelmään tulevat prosessit on luotu ohjelmakoodista käsin käyttämällä komentoa fork, ote man-sivulta:
    NAME
           fork - create a child process
    
    SYNOPSIS
           #include < sys/types.h>
           #include < unistd.h>
    
           pid_t fork(void);
    
    DESCRIPTION
           fork  creates a child process that differs from the parent process only
           in its PID and PPID. 
    
    RETURN VALUE
           On success, the PID of the child process is returned  in  the  parent's
           thread  of execution, and a 0 is returned in the child's thread of exe-
           cution.  On failure, a -1 will be returned in the parent's context,  no
           child process will be created, and errno will be set appropriately.
    
    Seuraavassa ote ohjelmasta 5-1.c:
      pid_t id;
    
      id = fork();          // luodaan uusi prosessi
    
      if ( id==0 ) {        // lapsen koodi
        printf("olen lapsi, pid=%d, ppid=%d\n", getpid(), getppid());
        sleep(2);           // viivytellään 2 sekunnin ajan
        printf("lapsi lopettaa nyt\n");
        exit(0);
      }
    
      // vanhempi jatkaa täältä
      printf("olen vanhempi, pid=%d, lapsen pid=%d\n", getpid(), id);
    
      wait( NULL );
      printf("lapsi lopetti\n");
    
      return 0;
    }
    
    Samalla kun tehdään fork, syntyy myös lapsiprosessi, joka on täydellinen klooni vanhemmasta. Lapsi siis rupeaa suorittamaan samaa koodia kun vanhempi ja suorituskohta on heti forkin jälkeinen komento. fork-komento palauttaa vanhemmalle syntyneen lapsiprosessin PID:in. Syntyneessä lapsessa sensijaan forkin palauttama arvo on nolla.

    forkin jälkeen siis molemmat prosessit, vanhempi ja lapsi suorittavat samaa koodia, mutta vanhemmalla lapsen PID, esimerkissä muuttuja id saa arvokseen syntyneen lapsen PID:in mutta lapsella muuttujan arvo on 0. Esimerkissä forkin jälkeen seuraavaksi suoritettava komento on if ( id==0 ). Lapsella tämä ehto on tosi, ja lapsi rupeaa suorittamaan if:in sisällä olevaa koodia ja vanhempi hyppää if:in yli. Näin vanhempi ja lapsi menevät molemmat suorittamaan omaa, toisistaan erillistä koodia.

    Lapsi printtaa ruudulle oman ja vanhempansa PID:it käyttäen komentoja getpid ja getppid, joista jälkimmäinen siis palauttaa vanhemman PID:in. Tämän jälkeen lapsi odottaa 2 sekuntia käyttäen sleep-komentoa ja lopettaa sitten.

    Vanhempi tulostaa oman PID:in sekä lapsen PID:in joka siis on forkin palauttamana tallessa muuttujassa id. Tämän jälkeen vanhempi rupeaa odottamaan lapsen lopettamista komennolla wait. Parametrina on NULL koska vanhempi ei ole kiinnostunut lapsen exitin yhteydessä palauttamasta arvosta.

    Komento wait odottaa seuraavana kuolevaa lasta. Komennolle voidaan antaa parametri, jollon saadaan selville lapsen lopetusstatus:

      int status;
      wait(&status);
    
    wait-komento blokkaa suorittavan prosessin niin kauaksi aikaa kunnes joku sen lapsista kuolee. Jos vanhemmalla ei ole yhtään lasta, palaa wait heti.

    Seuraavassa ote ohjelmasta 5-2.c, joka toimii muuten samoin paitsi vanhempi käyttää waitpid-komentoa lapsen odottamiseen. waitpid toimii muuten kuten wait, mutta waitpid:issä voidaan parametrin avulla kertoa mitä lasta odotetaan, lisäksi on mahdollista asettaa waitpid toimimaan aynkroonisesti eli siten, että se ainoastaan tarkastaa onko lapsi jo kuollut ja palaa välittömästi.

    Seuraavassa vanhemman koodi:

      int status;
      waitpid ( id, &status, 0 );
      printf("lapsi lopetti ");
    
      if ( WIFEXITED(status) ){
        prinf(" ja palautti arvon %d\n",  WEXITSTATUS(status));
      }
    
    Nyt waitpidissa kerrotaan että odotetaan lasta jonka PID on muuttujassa id. Jos ensimmäisen parametrin arvo on -1, odotetaan mitä tahansa lasta. Toinen parametri on osoitin muuttujaan, johon luetaan lapsen lopetusstatus. Kolmannen parametrin arvona 0, eli waitpid odottaa niin kauan kunnes lapsi kuolee. Vaihtoehtona olisi WNOHANG, jolloin ainoastaan tarkastetaan onko lapsi kuollut, ks. man-sivu.

    waitpidin jälkeen tarkastetaan onko lapsi todellakin kuollut: if ( WIFEXITED(status). waitpid (samoin kuin wait) nimittäin palaa myös niissä tapauksissa, joissa lapsiprosessi menee taustasuoritustilaan eli ns. suspended-tilaan. Normaalisti tälläistä ei tapahdu. Suspend-tilaan voi joutua esim. jos prosessia suorittavassa konsolissa painetaan ctrl+z. Jos lapsi on lopettanut, tulostetaan lopetusstatus. Tämä tapahtuu valmiiksi määritellyn WEXITSTATUS(status) makron avulla joka erottaa lapsen exitillä palauttaman lopetusstatuksen waitpidillä saadun parametrin arvon sisältä.

    Lapsen ja vanhemman muuttujat

    Lapsi on siis täydellinen kopio vanhemmasta. Molemmilla on täsmälleen sama koodi ja samannimiset muuttujat. Lapsen ja vanhemman käyttämät muistialueet ovat kuitenkin erilliset, eli vaikka molemmissa näkyvät samannimiset muuttujat, on kyse eri muuttujista eli jos esim. lapsi muuttaa jonkun molemmille näkyvän muuttujan arvoa, ei muutoksella ole vaikutusta vanhemman samannimiseen muuttujaan. Syntyhetkellä vanhemman muuttjien arvot kopioituvat lapsen muuttujien arvoksi eli myös muuttujien arvon suhteen lapsi on täydellinen kopio vanhemmasta.

    Jos vanhemmalla on lapsen syntyhetkellä avoinna tiedostoja, ovat tiedostot avoinna myös lapsella. Näin vanhemmalta lapselle "periytynyt" avoin tiedosto sulkeutuu vasta kun sekä lapsi että vanhempi ovat sen sulkeneet. Lapsen ja vanhemman näkemä tiedosto on siis sama.

    Lopettanut prosessi

    Kun prosessi lopettaa, käyttäjärjestelmä säilyttää prosessin jättämää lopetusstatusta niin kauan kunnes prosessin vanhempi suorittaa wait:in tai waitpid:in.

    Kuollutta prosessia, jolle ei ole tehty wait:ia sanotaan zombieksi. Kun vanhempi sitten tekee wait:in tai waitpid:in, zombie häviää.

    On myös mahdollista, että vanhempi kuolee, ennen lasta. Tällöin prosessista tulee orpo ja init-prosessi perii lapsen, eli lapsen vanhemmaksi tulee init jonka PID on 1. init suorittaa aika-ajoin wait-komentoa, jotta lopetaneet orvot eivät jää zombieiksi.

    Uuden koodin lataaminen prosessille

    Edellisissä esimerkeissä lapsiprosessin suorittama koodi sisältyy vanhemman koodiin. Näin ei tietenkään aina ole.

    Esim. komentotulkki (joka siis itsekin on normaali prosessi) toimii siten, että kun käyttäjä käynnistää uuden ohjelman, luodaan ensin prosessi komennon suorittamista varten. Uusi prosessi sitten lataa suoritettavan komennon koodin ja alkaa suorittamaan uutta koodia.

    Uuden koodin lataaminen prosessille tapahtuu jollakin exec-komentoperheen komennoista:

    NAME
           execl, execlp, execle, execv, execvp - execute a file
    
    SYNOPSIS
           #include < unistd.h>
    
           extern char **environ;
    
           int execl(const char *path, const char *arg, ...);
           int execlp(const char *file, const char *arg, ...);
           int  execle(const  char  *path,  const  char  *arg  , ..., char * const
           envp[]);
           int execv(const char *path, char *const argv[]);
           int execvp(const char *file, char *const argv[]);
    
    DESCRIPTION
           The exec family of functions replaces the current process image with  a
           new  process  image.   The  functions described in this manual page are
           front-ends for the function execve(2).  (See the manual page for execve
           for detailed information about the replacement of the current process.)
    
           ...
    
    Vaihtoehtoja exec:eissä on monia.

    Seuraavassa ote ohjelmasta 5-3.c, joka käyttää execl-komentoa.

      id = fork();          // luodaan uusi prosessi
    
      if ( id==0 ) {        // lapsen koodi
        // suoritetaan ls-komento optiolla -l
        execl("/bin/ls", "ls", "-l", NULL);
      }
    
      // vanhempi jatkaa täältä
      wait( NULL );
      printf("lapsi lopetti\n");
    
      return 0;
    }
    
    execl-tomii siten, että ensimmäisenä parametrina on suoritettava ohjelmakoodi. Seuraavaksi tulevat ohjelman käynnistävät komentoriviparametrit alkaen ohjelman nimestä. Viimeiseksi tulee NULL kertomaan että muita parmetrejä ei ole.

    Komento execl("/bin/ls", "ls", "-l", NULL); toimii samoin kun komentoriviltä annettu komento [luuma@telinux1]$ ls -l.

    Jos halutaan antaa enemmän käynnistusoptioita, esim. ls -l -a -u onnistuu se helposti: execl("/bin/ls", "ls", "-l", "-a", "-u", NULL);

    Jos execl onnistuu, ei komennosta palata ikinä, sillä prosessin suorittama koodi korvautuu täydellisesti ladattavalla koodilla. Jos execl epäonnistuu, palauttaa se arvon -1. Tämäkin tilanne on useimmiten syytä tarkastaa:

        int s = execl("/bin/xyx", "xyz", "-l", NULL);
        if ( s==-1 ){
          perror("execv epäonnistui");
          exit(0);
        }
    

    execlp toimii täysin samoin, mutta ensimmäisenä parametrina ei tarvitse antaa kokonaista polkunimeä suoritettavaan ohjelmaan jos suoritettava ohjelma löytyy PATH-muuttujassa määritellyllä suorituspolulla. Koska ls-ohjelma on suorituspolulla, olisi edellä voitu käyttää execlp:tä seuraavasti: execlp("ls", "ls", "-l", NULL); Tässä komennon nimi siis toistuu kahteen kertaan, ensin polulta etsittävänä ohjelmakoodin nimenä ja vielä uudelleen komentoriviparametrina.

    execv toiminta on esitelty seuraavassa (ks. 5-4.c):

      if ( id==0 ) {        // lapsen koodi
        char *arg[3];
        arg[0] = "ls";
        arg[1] = "-l";
        arg[2] = NULL;
        // suoritetaan ls-komento optiolla -l
        execv("/bin/ls",arg);
      }
    
    Parametrina on nyt suoritettava kooditiedosto ja tämän jälkeen ohjelman komentoriviparametrit merkkijono-osoitintaulukkona, eli muodossa char *arg[], missä arg[0], arg[1], ... muodostavat ohjelman käynnistävän komentorivin siten, että viimeisenä on NULL-pointteri.

    execv:stä on olemassa myös versio execvp, joka etsii käynnistettävää ohjelmaa PATH-muuttujassa määritellyltä suorituspolulta.

    execv ja execvp ovat käyttökelpoisia erityisesti silloin kun käynnistettävän ohjelman komentoriviparametrien määrä ei ole ennalta tiedossa vaan joudutaan rakentamaan ajon aikana.