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.
#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 putkeenTä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.
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.
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.
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.
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 toinenapu2.c lukee oletussyötteestä merkkijonoja ja tulostaa ne väärin päin:
[luuma@telinux1]$ apu2 abcdefg gfedcba 123456 654321Riveistä 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:
#includeEnsimmä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.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 }
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.
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 joukkoonOperaatioiden 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.