Tiedostojen käsittely Linuxissa

Tiedostojen käsittelyyn Linuxissa on kaksi rajapintaa, kaiken alla oleva tiedostokuvaajiin perustuva I/O-rajapinta sekä tämän päälle rakentuva stdio.h-kirjastossa määritelty streameihin perustuva standardi I/O -kirjasto.

Tiedostokuvaajiin perustuva I/O

Jokaiseen ohjelman avaamaan tiedostoon liittyy tiedostokuvaaja, joka on int-muuttuja, jonka avulla ohjelma pääsee käsiksi kuvaajaa vastaavaan tiedostoon. Tiedosto avataan open-funktiolla, joka palauttaa avattua tiedostoa vastaavan tiedostokuvaajan:
  int fd;
  fd = open( "tiedosto.txt", O_RDONLY );
Ensimmäinen parametri on avattava tiedosto, toisena operaation tyyppi O_RDONLY, O_WRONLY tai O_RDWR. Kun tiedosto avataan kirjoittamista varten, näyttää operaatio seuraavalta:
  fd = open( "tiedosto.txt", O_WRONLY | O_CREAT, 0600 );
Toiseen parametriin on lisätty |:llä määre O_CREAT, joka tarkoittaa, että tiedosto luodaan jos se ei jo ole olemassa. Kolmas parametri on tiedoston oikeudet, tässä tapauksessa 0600 eli omistajalle read- ja write-oikeudet ja muille ei mitään oikeuksia.

Katso tarkemmin man 2 open.

Virhetilanteen käsittely

Tehtäessä systeemikutsu tai kutsuttaessa kirjastofunktiota on syytä tarkastaa onnistuiko operaatio. Useimmiten operaatioiden paluuarvo kertoo onnistumisen. Esim. open palauttaa epäonnistuessaan arvon -1.

Seuraavassa testataan onnistuminen ja epäonnistuessa kutsutaan perror-funktiota, joka tulostaa konsolille virheen syyn sekä käyttäjän määräämän tekstin.

  fd = open( "tiedosto.txt", O_WRONLY | O_CREAT, 0600 );

  if ( fd == -1 ){
    perror("virhe openissa");
    return 0;
  }
Virhetilanteessa asettaa järjestelmä automaattisesti virheen tyypin ilmoittavan arvon globaaliin muuttujaan errno. Esim. open:in yhteydessä virhetilanteen sattuessa errno voi saada seuraavia arvoja (openin man-sivulta):
       EEXIST pathname already exists and O_CREAT and O_EXCL were used.

       EISDIR pathname refers to a directory and the access requested involved
              writing (that is, O_WRONLY or O_RDWR is set).

       EACCES The requested access to the file is not allowed, or one  of  the
              directories  in  pathname did not allow search (execute) permis-
              sion, or the file did not exist yet and write access to the par-
              ent directory is not allowed.

       ENAMETOOLONG
              pathname was too long.

       ENOENT O_CREAT  is  not  set  and the named file does not exist.  Or, a
              directory component in pathname does not exist or is a  dangling
              symbolic link.       

       EMFILE The process already has the maximum number of files open.

       ENFILE The  limit  on  the total number of files open on the system has
              been reached.

       ...
errno-muuttujan arvot ovat oikeastaan kokonaislukuja mutta niihin viitataan aina C-standardikirjastoissa määriteltyjen symboolisten vakioiden kautta. Komentojen käyttämät errno-arvon vakiot on määritelty kunkin komennon man-sivulla, samaan tyyliin kun yllä oleva listaus openin man-sivulta. errno-muuttuja saadaan käyttöön liittämällä ohjelmaan includella errno.h-otsikkotiedosto.

Virheen tyypin voi siis tarkastaa errno:n avulla tai vaihtoehtoisesti voidaan käyttää funktiota perror tulostamaan virheestä kertova ilmoitus ruudulle.

Kun tiedostoa ei enää tarvita, suljetaan tiedosto:

  close( fd );
Tiedostosta lukeminen tapahtu read-käskyllä. Hieman lyhennelty ote man-sivulta (huom. kyseessä sektion 2 man sivu):
       #include < unistd.h>

       ssize_t read(int fd, void *buf, size_t count);

DESCRIPTION
       read()  attempts to read up to count bytes from file descriptor fd into
       the buffer starting at buf.


RETURN VALUE
       On success, the number of bytes read is returned (zero indicates end of
       file), and the file position is advanced by this number.  It is not  an
       error  if  this  number  is smaller than the number of bytes requested;
       this may happen for example because fewer bytes are actually  available
       right  now  (maybe  because we were close to end-of-file, or because we
       are reading from a pipe, or from a terminal),  or  because  read()  was
       interrupted  by  a  signal.  On error, -1 is returned, and errno is set
       appropriately. In this case it is left  unspecified  whether  the  file
       position (if any) changes.
Eli readille annetaan parametrina tiedostokuvaaja, muistiosoite, johon tieto tulee ja luettavien tavujen määrä. Jos tiedosto on lopussa, palauttaa operaatio arvonaan nollan. Paluuarvon tyyppi on ssize_t, joka on usein sama kuin int.

Ohjelma 2-1.c, joka lukee tiedoston sisällön.

Tiedostoon kirjoittaminen tapahtuu write-kutsulla. Ote man-sivulta:

       #include < unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);

DESCRIPTION
       write()  writes  up  to  count bytes to the file referenced by the file
       descriptor fd from the buffer starting at buf.

RETURN VALUE
       On  success,  the  number of bytes written are returned (zero indicates
       nothing was written).  On error, -1  is  returned,  and  errno  is  set
       appropriately.
Ohjelma 2-2.c, joka kirjoittaa käyttäjän syöttämän datan tiedostoon. Ohjelmoija on ollut laiska, eikä viitsi tarkastaa writen onnistumista. Tarkistus on kuitenkin syytä tehdä tuotantokäyttöön tarkoitetussa koodissa.

Oletussyöte ja oletustuloste

Jokaisella ohjelmalla on oletusarvoisesti avattuna kolme tiedostoukuvaajaa, 0, 1 ja 2. Kuvaaja 0 on standard input, oletussyöte eli näppäimistö. Kuvaaja 1 on standard output, oletustulostus eli näyttö ja kuvaaja 2 on standard error, virheilmoitusten eli tulostuspaikka.

Komennolla write(1, "teksti", 6 ) siis voidaan kirjoittaa näytölle ja komennolla read(0, buf, sizeof(buf)) voidaan lukea näppäimistöltä syötettyä dataa. Yleensä käyttäjän kanssa kommunikointi kannattaa hoitaa toisin, esim. printf- ja scanf-funktioita käyttämällä. Myöhemmin kuitenkin tulemme huomaamaan, että on erittäin hyödyllistä päästä käsiksi standard inputiin ja outputiin tiedostokuvaajien tasolla.

Muun kuin merkkimuotoisen datan tallentaminen ja lukeminen

Tiedostoon kirjoitettavan datan ei tietenkään tarvitse koostua pelkistä chareista. Ohjelma 2-3.c tallentaa tiedostoon structin, joka voidaan lukea ohjelmalla 2-4.c

Liikkuminen tiedoston sisällä

Kun tiedosto avataan, tiedostolle tehtävät operaatiot kohdistuvat tiedoston alkuun. Jos esim. luetaan readilla 10 tavua, siirrytään tiedostossa tavun numero 11 kohdalle ja seuraava read tulee kohdistumaan tiedoston kohtaan tavusta 11 alkaen. lseek-komennolla on mahdollisuus muuttaa kohtaa, mihin operaatiot tiedostossa kohdistuvat. Kohtaa, johon operaatiot kohdistuvat sanotaan joskus tiedosto-osoittimeksi tai englanniksi offsetiksi.

Ote lseekin man-sivulta:

       #include < sys/types.h>
       #include < unistd.h>

       off_t lseek(int fildes, off_t offset, int whence);

       The lseek() function repositions the offset of the open file associated
       with the file descriptor fildes to the argument offset according to the
       directive whence as follows:

       SEEK_SET
              The offset is set to offset bytes.

       SEEK_CUR
              The offset is set to its current location plus offset bytes.

       SEEK_END
              The offset is set to the size of the file plus offset bytes.

RETURN VALUE
       Upon  successful completion, lseek() returns the resulting offset loca-
       tion as measured in bytes from the beginning of the file.  Otherwise, a
       value  of (off_t)-1 is returned and errno is set to indicate the error.
Kolmas parametri siis säätelee, miten operaatio toimii, eli siirretäänkö tiedosto-osoitinta suhteessa tiedoston alkuun, loppuun vai nykyiseen sijaintiin.

Ohjelma 2-5.c lukee tiedoston lopusta alkuun. Ensin selvitetään tiedoston koko siirtämällä tiedosto-osoitin tiedoston loppuun. Tämän jälkeen luetaan tavu kerrallaan alkaen tiedoston lopusta.

Streameihin perustuva I/O kirjasto

Streameiin perustuva I/O-kirjasto on määritelty stdio.h-kirjastossa. Tiedostokuvaajien sijasta tiedostoja edustavat osoittimet FILE-tyyppiseen muuttujaan.

Stream I/O-ssa käytetään funktioita fopen, fclose, fread, fwrite, fgets, fputs, fprintf ym.

Seuraavassa esimerkkiohjelmia c-kurssin materiaalista.

Ohjelma esim7-1.c lukee tekstitiedostoa fgets-komennolla.

Ohjelma esim7-2.c kirjoittaa tiedostoon käyttäen fputs-komentoa ja ohjelma esim7-3.c kirjoittaa tiedostoon käyttäen fprintf-komentoa.

Jos tiedostoon kirjoitetaan muuta kuin tekstiä, tarvitaan fwriteä, ks. esim7-4.c ja näin kirjoitetun tiedoson lukeminen tapahtuu freadilla, ls. esim7-5.c

Tiedostokuvaajat vs. stream I/O

Stream I/O kirjasto on hiukan tehokkaampi kuin tiedostokuvaajiin perustuva I/O sillä stdio.h-kirjasto toteuttaa sisäisesti tiedoston puskurointia. Puskurointi tarkoittaa tässä yhteydessä sitä, että jos tiedostosta luetaan esim. fread:illa dataa 10 tavua, lukee kirjastofunktion toteutus todellisuudessa huomattavasti suuremman määrän dataa levyltä. Jos freadia kutsutaan pian uudestaan, on data jo valmiina eikä käyttöjärjestelmätason koodin suorittamista (eli systeemikutsua) tarvita.

Suorituskykyä voi vertailla suorittamalla ohjelmat 2-6.c ja 2-7.c käyttäen Linuxin time-komentoa, joka mittaa suoritukseen kuluneen ajan. Ohjelmat lukevat ensimmäisenä komentoriviparametrina saamansa tiedoston sisällön käyttäen toisena parametrina annetun kokoisia paloja.

Joskus on tarvetta saada FILE-osoitinta vastaava tiedostokuvaaja. Tämä onnistuu komennolla fileno:

       #include < stdio.h>

       int fileno(FILE *stream);

DESCRIPTION

       The function fileno examines the argument stream and returns its  inte-
       ger descriptor.
Operaatio toisinpäin, eli tiedostokuvaajaa vastaavan FILE-osoittimen hankkiminen tapahtuu komennolla fdopen:
SYNOPSIS
       #include < stdio.h>

       FILE *fopen(const char *path, const char *mode);

       The fdopen function associates a stream with the existing file descrip-
       tor, fildes.  The mode of the stream (one of the values "r", "r+", "w",
       "w+", "a", "a+") must be compatible with the mode of the file  descrip-
       tor.