Putket

Prosessit voivat käyttää signaaleja alkeelliseen kommunikaatioon. Signaalien avulla ei kuitenkaan ole mahdollista välittää prosessien välillä dataa, joten muitakin menetelmiä prosessien kommunikointiin tarvitaan. Ehkä eniten käytetety prosessien kommunikointimuoto Unix-tyyppisissä käyttöjärjestelmissä on putki (engl. pipe).

Annettaessa esim. komento ls -l | less komentotulkki käynnistää kaksi prosessia. Toinen prosesseista alkaa suorittaa ls:n koodia ja toinen komennon less koodia. Komentotulkki yhdistää prosessit putkella siten, että ls:n tulostus ei menekään ruudulle vaan ohjataan putkeen joka yhdistetään less-komennon syötteeksi.

Putki on ikään kuin väliaikainen aputiedosto, jonka kautta tieto kulkee ohjelmalta toiselle. Käyttöjärjestelmä toteuttaa putken keskusmuistissa olevana puskurialueena. Ohjelmallisesti putken käsittely tapahtuu lähes samalla tavalla kun tiedostojen käsittely.

Putken käyttö ohjelmallisesti

Putki luodaan pipe-systeemikutsulla:
       #include < unistd.h>

       int pipe(int filedes[2]);

DESCRIPTION
       pipe  creates a pair of file descriptors, pointing to a pipe, and
       places them in the array pointed to by filedes. filedes[0] is for
       reading, filedes[1] is for writing.

RETURN VALUE
       On success, zero is returned. On error, -1 is returned.
Eli komennon parametrina on kaksipaikkainen int-taulukko, joihin asetetaan putken päiden tiedostokuvaajat. Kun putki on avattu, putkeen kirjoittaminen ja putkesta lukeminen tapahtuvat write- ja read-kutsujen avulla täsmälleen samalla tavalla kun tiedostojakin käsitellään (ks. viikko 2). Ohjelma 8-1.c luo ensin putken. Tämän jälkeen forkataan lapsiprosessi. Koska putki on luotu ennen fork:ia, on putki forkin jälkeen auki sekä lapsella että vanhemmalla. Näin prosessien on mahdollista kommunikoida.

Ohjelma alkaa putken avaamisella:

int main(int argc, char *argv[]){
  int lapsi;
  int putki[2];       // taulukko putken tiedostokuvaajia varten
  char *mj1 = "koemerkkijono";

  // luodaan putki, parametrina 2 paikkainen int-taulukko
  pipe(putki);

  // putki[0] on tiedostokuvaaja, jonka avulla luetaan putkesta
  // putki[1] on tiedostokuvaaja, jonka avulla kirjoitetaan putkeen
Tämän jälkeen tapahtuu fork. Lapsi tekee readin putken lukupäälle. Vanhempi odottaa hiukan ja kirjoittaa putkeen dataa. Lapsessa putkelle tehty read-operaatio blokkaa lapsen siksi aikaa kunnes putkessa on dataa. Lopuksi sekä lapsi että vanhempi sulkevat putken molemmat päät.
  lapsi = fork();

  // sekä lapsi että vanhempi näkevät putken molemmat päät
  if ( lapsi==0 ){
    char mj2[MAX];
    // luetaan merkkijono putkesta
    int n = read(putki[0],mj2,MAX-1); 

    // laitetaan merkkijonoon kaiken varalta lopetusmerkki
    mj2[n] = '\0';
     
    printf("lapsi luki putkesta %d tavua, eli merkkijonon %s \n",n, mj2);

    // suljetaan putken molemmat päät lapsesta
    close(putki[0]);
    close(putki[1]);
    exit(0);
  }

  // aikuinen viivyttelee
  sleep(2);

  // kirjoitetaan merkkijono putkeen
  write(putki[1],mj1,strlen(mj1));

  wait(NULL);

  // suljetaan putken molemmat päät vanhemmasta
  close(putki[0]);
  close(putki[1]);

  return 0;
}
Huom: kun putkesta luetaan merkkijono, kannattaa merkkipuskuriin varalta laittaa loppumerkki, lähettäjä ei välttämättä ole lähettänyt merkkijonosta muuta kun normaalit tulostuvat merkit.

Koska ohjelmassa lapsi ainoastaan lukee putkesta, voitaisiin heti lapsen alussa sulkea putken kirjoituspää:

 if ( lapsi==0 ){
    char mj2[MAX];

    // suljetaan putken kirjoituspää
    close(putki[1]); 

    // luetaan merkkijono putkesta
    int n = read(putki[0],mj2,MAX-1);
    mj2[n] = '\0';

    printf("lapsi luki putkesta %d tavua, eli merkkijonon %s \n",n, mj2);

    // suljetaan putken lukupää
    close(putki[0]);

    exit(0);
  }
Vastaavasti vanhempi voisi sulkea putken lukupään tarpeettomana. Vaikka lapsi sulkee putken kirjoituspään, säilyy putki kuitenkin avattuna vanhemmalle kirjoittamista varten. Käyttöjärjestelmä tuhoaa putken vasta siinä vaiheessa kun putki ei ole enää kenelläkään avattuna kirjoitusta tai lukua varten.

Putki ilman lukijaa tai kirjoittajaa

Putkesta luettaessa read palauttaa putkesta luettujen tavujen määrän jos lukuoperaatio onnistuu. Jos putkessa ei ole dataa, read odottaa niin kauan kun dataa on saatavilla. Jos putki ei ole kenellekään auki kirjoittamista varten ja putkesta yritetään lukea, palauttaa read arvon 0. Virhetilanteessa read palauttaa arvon -1. Virhetilanteessa kannattaa virheen syy tulostaa komennolla perror(), parametrina komennossa vapaavalintainen merkkijono.

Ohjelmassa 8-2.c kaksi prosessia, jotka vaihtavat dataa putken avulla. Vanhempi kirjoittaa putkeen kaksi merkkijonoa ja tämän jälkeen sulkee putken kirjoituspään. Vanhemman koodi:

    close(putki[0]);    // vanhempi sulkee putken lukupään
    sleep(2);
    // kirjoitetaan putkeen merkkijono
    write(putki[1],mj1,strlen(mj1));
    sleep(1);
    write(putki[1],mj1,strlen(mj1)+1);
    sleep(2);
    printf("vanhempi sulkee putken kirjoituspään\n");
    close(putki[1]);
Lapsi lukee putkesta dataa niin kauan kunnes read-operaatio palauttaa arvon 0 ja lapsi lopettaa:
    if ( lapsi==0 ){
        close(putki[1]);        // lapsi sulkee putken kirjoituspään
        int n;
	char mj2[MAX];

        while ( 1 ){
          n = read(putki[0],mj2,MAX-1);
          // jos putkella ei kirjoittajaa, palauttaa read arvon 0
	  // muussa virhetilanteessa read palauttaa -1
          if ( n==0 || n==-1 ) break;

	  mj2[n] = '\0';
          printf("lapsi luki putkesta: %s \n",mj2);
        }

        // virhetilanteessa tulostetaan virheen syy
        if ( n ==-1 ) perror("read epäonnistui");

        printf("putki suljettu, lapsi lopettaa\n");

        exit(0);
    } 
Huom: on oleellista, että lapsi sulkee aluksi putken kirjoituspään. Jos näin ei tehdä, ei lapsi tule koskaan loopista ulos sillä vaikka vanhempi sulkee putken kirjoituspään, jää putken kirjoituspää edelleen auki lapseen.

Jos yritetään kirjoittaa putkeen, joka ei ole kenelläkään auki lukemista varten, lähettää käyttöjärjestelmä kirjoituksen tehneelle prosessille signaalin SIGPIPE. Jos ohjelman on varauduttava tilanteeseen, on kirjoitettava käsittelijä signaalille SIGPIPE.

Muun datan kuin tekstin lähettäminen putkea pitkin

Putkea pitkin voidaan lähettää mitä tahansa dataa. Tällöin read:ia ja write:ä on käytettävä samaan tapaan kuin labratehtävissä 5 ja 6.

Ohjelmassa 8-3.c prosessi lähettää lapselleen taulukollisen kokonaislukuja. Ensin vanhempi kirjoittaa putkeen taulukon koon. Tämän jälkeen putkeen kirjoitetaan koko taulukon sisältö:

 
  // vanhemman koodi
  int t[5] = {2,3,5,1,4};
  int koko = 5;

  // lähetetään ensin putkeen taulukon koko int-lukuna
  write(putki[1],&koko, sizeof(int));
  // tämän jälkeen lähetetään putkeen koko taulukko
  write(putki[1],t, sizeof(int)*5);

  wait(NULL);
  close(putki[1]);
Lapsi lukee ensin putkesta yhden int-arvon, joka kertoo minkä kokoinen taulukko putkesta on tulossa. Luku luetaan putkesta suoraan int-muuttujaan. Lapsi varaa taulukolle riittävän tilan ja lukee taulukon datan putkesta. Koko taulukko luetaan putkesta suoraan taulukolle varattuun muistitilaan jota int-osoitin t osoittaa. Lapsi testaa tuliko dataa varmasti oikea määrä, jos tuli, taulukko tulostetaan:
  if ( lapsi==0 ){
    close(putki[1]);
    int n, koko;
    int *t;          // osoitin luvuille varattavaan muistialueeseen

    // luetaan putkesta taulukon koko
    read(putki[0],&koko,sizeof(int));
    // varataan sopiva muistialue
    t = malloc(koko*sizeof(int));
    // luetaan taulukko
    n = read(putki[0],t,koko*sizeof(int));
    if( n!=koko*sizeof(int) ){
      printf("luettiin putkesta väärä määrä tavuja\n");
    }
    else{
      for( n=0; n < koko; n++ )
        printf(" t[%d]=%d \n", n, t[n]);
    }
    close(putki[0]);
  }
Samalla tekniikalla putkea pitkin voidaan välittää mitä tahansa dataa. Ainoa edellytys on se, että putkea lukevan on tiedettävä minkä tyyppistä dataa on tulossa, jotta data osataan vastaanottaa oikean kokoisina paloina ja sijoitaa oikean tyyppisiin muuttujiin.

Syötevirtojen uudelleenohjaus

Tiedostokuvaaja 0 viittaa prosessin oletussyötteeseen (standard input) eli näppäimistöön. Käyttäen tiedostokuvaajaa 0, näppäimistöltä voidaankin lukea aivan kuten tiedostosta (tai putkesta), systeemikutsulla read:
 
  char buf[80];

  read(0,buf, 80);
  printf("luettiin merkkijono %s", buf);
Tiedostokuvaaja 1 viittaa prosessin oletustulostukseen (standard output) eli näyttöön. Käyttäen tiedostokuvaajaa 1, näytölle kirjoittaminen tapahtuu kuten tiedostoon kirjoittaminen systeemikutsulla write:
 
  char *p = "merkkijono";

  write(1,p, strlen(p));
Standardikirjaston printf- ja scanf-funktioiden toteutus käyttääkin systeemikutsuja write ja read tulostukseen ja syötteen lukemiseen.

Aivan kuten mikä tahansa tiedosto, voidaan myös oletussyöte ja tuloste sulkea komennolla close. Miksi näin haluttaisiin tehdä? Syy esim. oletustulosuksen sulkemiselle voisi olla se, että halutaankin että tulostus ohjataan näytön sijasta putkeen.

Annettaessa esim. komento ls -l | less komentotulkki luo putken ja uudelleenohjaa komentoa ls suorittavan oletustulostuksen putkeen sekä uudelleenojaa putken lukupään komennon less oletussyötteen paikalle.

Ohjelmasta käsin uudelleenohjaus tapahtuu seuraavasti.

  • Suljetaan oletussyöte: close(0).
  • Kopioidaan tiedostokuvaajaksi numero 0 jokin muu tiedostokuvaaja, esim. putken lukupää käyttäen systeemikutsua dup2.
  • Esim. jos putki[0] on putken lukupää ja oletussyöte on suljettu, voidaan putkesta tuleva data ohjata oletussyötteen tilalle käskyllä dup2(putki[0],0). Nyt luettaessa dataa "näppäimistöltä", esim. komennoilla gets ja scanf, luetaankin data putkesta.

    dup2-komentoa kutsutaan seuraavasti:

     dup2(putki[0], 0);
    
    Komennon jälkeen oletussyötteen tiedostokuvaaja 0 rupeaakin tarkoittamaan samaa asiaa kuin putken lukupään tiedostokuvaaja putki[0] ja tämän takia siis kaikki normaalisti näppäimistöltä dataa lukevat operaatiot kohdistuvatkin putkeen.

    Vastaavasti voidaan sulkea prosessin oletustulostus tekemällä komento close(1) ja voidaan uudelleenohjata esim. jokin putki oletustulostuksen tiedostokuvaajaan komennolla dup2. Näin printf:llä tulostettu teksti meneekin putkeen.

    On myös mahdollista uudelleenohjata oletussyötteen tai tulostuksen paikalle jokin avoinna oleva tiedosto. Komentotulkki tekee näin esim. silloin kun suoritetaan komento ls > tulostus.txt. Komento saa aikaan sen, että komentotulkki avaa tiedoston tulostus.txt ja ohjaa ls:n oletustulostuksen tiedostoon.

    Katsotaan tarkemmin ohjelmaa 8-4.c, joka käyttää oletussyötteen ja tulostuksen uudelleenohjausta.

    Käytössä on kaksi yksinkertaista ohjelma. apu1.c tulostaa oletustulostukseen eli ruudulle kaksi merkkijonoa:

     
    [luuma@telinux1]$ apu1
    koemerkkijono
    toinen
    
    apu2.c lukee oletussyötteestä merkkijonoja ja tulostaa ne väärin päin:
    [luuma@telinux1]$ apu2
    abcdefg
    gfedcba
    123456
    654321
    
    Riveistä siis ensimmäinen ja kolmas ovat käyttäjän kirjoittamia.

    Jos ohjelmat putkitetaan, käy seuraavasti:

    [luuma@telinux1]$ apu1 | apu2
    onojikkremeok
    neniot
    [luuma@telinux1]$
    
    Eli apu1:n tulostus ohjautuu apu2:lle, joka tulostaa molemmat rivit käänteisessä järjestyksessä.

    Ohjelma 8-4.c, toimii siten, että se luo kaksi prosessia. Toinen prosesseista laitetaan suorittamaan apu1:n koodia ja toinen apu2:n koodia. Prosessit yhdistetään putkella ja apu1:n koodia suorittavan prosessin oletustulostus ohjataan putkeen, jonka toinen pää ohjataan apu2:n koodia suorittavan prosessin oletussyötteeseen.

    Aluksi luodaan putki ja ensimmäinen lapsi:

    #include
    int main(void){
        int putki[2];
        int lapsi1, lapsi2, x;
    
        pipe(putki);
    
        // luodaan kaksi lapsiprosessia jotka molemmat lataavat uuden
        // suoritettavan koodin, eli lapset alkavat suorittaa kahta eri ohjelmaa
        // ohjataan lapsen 1 suorittaman ohjelman tulostus eli standard output
        // lapsen 2 suorittaman ohjelman syötteeksi eli standard inputiksi
    
        lapsi1 = fork();
        if ( lapsi1==0 ){
            close(1);                       // suljetaan lapsen oletustulostus
    
            // ohjataan putken kirjoituspää oletustulostuken tilalle
            dup2(putki[1], 1);
            // nyt kaikki ohjelman tulostus meneekin näytön sijasta putkeen
    
            close(putki[0]);                // suljetaan putken lukupää
            execlp("apu1", "apu1", NULL);   // ladataan uusi koodi
        }
    
    Ensimmäinen lapsi siis ohjaa oman tulostuksensa putkeen ja lataa apu1:n koodin. Toinen lapsi ohjaa putken toisen pään oletussyötteekseen ja lataa apu2:n koodin.
        lapsi2 = fork();
        if ( lapsi2==0 ){
            close(0);                       // suljetaan lapsen oletussyöte
            // ohjataan putken lukupää olteussyötteen tilalle
            dup2(putki[0], 0);
            // nyt esim. fgetsillä, getsillä ja scanf:llä luetaankin putkesta
    
            close(putki[1]);                // suljetaan putken kirjoituspää
            execlp("apu2", "apu2", NULL);   // ladataan uusi koodi
        }
    
    Vanhempi sulkee putken molemmat päät ja alkaa odottamaan lasten valmistumista:
        close(putki[0]);
        close(putki[1]);
    
        wait(NULL);
        wait(NULL);
    
        return 0;
    
    Huom: on erittäin tärkeää, että kaikki ylimääräiset putkien päät suljetaan. Koska putket on luotu ennen lapsia, näkyvät kaikki putket molemmissa lapsissa ja vanhemmassa. Jos joku putken ylimääräisistä kirjoituspäistä (vanhemmassa tai lapsi 2:ssa) jää sulkematta, jumiutuu ohjelma, sillä apu2 lopettaa toimintansa siinä vaiheessa kun kaikki putkeen kirjoittajat ovat sulkeneet putkensa. apu1:stä suorittavan lapsi 1:n putket sulkeutuvat automaattisesti (KJ:n toimesta) siinä vaiheessa kun apu1:n koodi on suoritettu loppuun.

    Advanced topic: Monen syötteen yhtäaikainen odottaminen

    Joskus tulee tilanteita, jolloin prosessin on pystyttävä odottamaan yhtä aikaa kahdesta suunnasta (esim. putkesta ja näppäimistöltä, tai useammasta putkesta) tulevaa dataa.

    Näitä tilanteita varten on käytössä komento select. Tutustutaan selectin käyttöön ohjelman 8-5.c avulla. Ohjelmassa luodaan lapsiprosessi, joka lähettää viiden sekunnin kuluttua putkea pitkin merkkijonon vanhemmalle.

    Vanhempi ryhtyy odottamaan yhtä aikaa dataa sekä putkesta että näppäimistöltä selectin avulla. Ensin otetaan käyttöön fd_set-tyyppinen muuttuja, jonka avulla koodataan tieto siitä, mistä syötteistä ollaan kiinnostuneita.

      // muuttuja, joka koodaa tiedon siitä minkä tiedostokuvaajien datasta
      // ollaan kiinnostuneita
      fd_set fds;
    
      // nollataan muuttuja fds ja asetetaan halutut tiedostokuvaajat tarkkailuun
      FD_ZERO(&fds);                // nollataan ensin tarkkailtavien joukko
      FD_SET(0, &fds);              // lisätään joukkoon 0 eli oletussyöte syöte
      FD_SET(putki[0], &fds);       // lisätään putki joukkoon
    
    Operaatioiden jälkeen muuttuja fds siis edustaa tiedostokuvaajajoukkoa, johon kuuluvat tiedostokuvaajat 0 ja putki[0], eli oletussyöte ja putken lukupää.

    Sitten kutsutaan selectiä, ensimmäisenä parametrina tiedostokuvaajien kokonaismäärä joka on vakiossa FD_SETSIZE, toisena parametrina fds:n eli tarkkailtavien tiedostokuvaajien joukkon osoite. Muina parametreinä NULL sillä niille ei ole juuri nyt käyttöä.

      // jäädään odottamaan, että putkesta tai näppäimistöltä voidaan lukea dataa
      r = select( FD_SETSIZE, &fds, NULL, NULL, NULL);
      if ( r==-1 ){
        perror("ongelma selectissä");
        return -1;
      }
    
    select blokkaa kutsujan niin kauaksi aikaa kunnes jossain tarkkailtavassa tiedostokuvaajassa on dataa luettavaksi. Kun selectistä tullaan ulos (ja paluuarvo ei ollut -1), on muuttujaan fds asetettu tieto siitä tiedostokuvaajasta, jossa on dataa valmiina lukemista varten.

    Tarkastetaan onko data putkessa vai oletussyötteessä, luetaan data ja tulostetaan se ruudulle:

      // katsotaan kummassa tiedostokuvaajassa dataa on ja luetaan data
      if ( FD_ISSET(0, &fds) ){
        // tiedostokuvaaja 0 eli oletuussyöte sisältää dataa
        n = read(0, buf, 80);
        buf[n] = '\0';
        printf("näppäimistöltä:  %s", buf);
      }
      if ( FD_ISSET(putki[0], &fds) ){
        // putki sisältää dataa
        n = read(putki[0], buf, 80);
        buf[n] = '\0';
        printf("putkesta:  %s", buf);
     }
    
    Tarkemmin sanoen select odottaa niin kauan kunnes joku odotetuista tiedostokuvaajista menee sellaiseen tilaan, jossa tiedostokuvaajalle suoritettu read ei blokkaa. Tästä seuraa, että select palaa myös siinä tapauksessa, jos esim. joku odotettavista putkista sulkeutuu, sulkeutuneelle putkelle tehtävä read-käskyhän palauttaa välittömästi arvon 0.

    selectiin on mahdollisuus laittaa ajastus, eli aika mikä syötteitä maksimissaan odotetaan. Ajastukseen käytetään struct timeval -tyypistä muuttujaa. tietueella on kaksi kenttää, toiseen asetetaan ajastuksen sekuntimäärä ja toiseen millisekunnit. tietuemuuttuja annetaan selectille neljäntenä parametrina. Seuraavassa asetetaan selectille 2.5 sekunnin maksimiodotusaika: Jos ajastus tapahtuu, palauttaa select arvon 0.

      
      struct timeval ajastin;
      ajastin.tv_sec = 2;	      // täydet sekunnit eli 2
      ajastin.tv_usec = 500000;   // loput mikrosekunteina, 0.5s = 500000 mikrosek 
    
      r = select( FD_SETSIZE, &fds, NULL, NULL, &ajastin);
    
      if ( r==0 ){
        printf("selectistä tultiin ulos ajastuksen takia\n");
      }
    
    selectin toisen parametrin voidaan odottaa kirjoitukseen tarkoitettuja tiedostokuvaajia. Joskus on nimittäin mahdollista, että tiedostokuvaajaan ei voida kirjoittaa, esim. putkeen mahtuu ainoastaan rajallinen määrä dataa, eli jos putki on täynnä, blokkaa kirjoitusoperaatio siksi aikaa kunnes joku lukee putkesta dataa. selectin kolmannen parametrin avulla voidaan tarkkailla jos jossain tiedostokuvaajassa tapahtuu poikkeustilanne. Jos jotain parametria ei tarvita, laitetaan kutsuun parametriksi NULL.