Tiedostojärjestelmä

Unix-tyyppisten käyttöjärjestelmien tiedostojärjestelmissä on jokaista tiedostoa (hakemisto on myös eräänlainen tiedosto) kohti inode, eli tietue, joka sisältää tiedoston attribuutit (koko, oikeudet, luontiaika, ym.) sekä tiedon missä levylohkoissa itse tiedoston data on talletettuna.

Myös inodet säilytetään levyllä, eli jos halutaan lukea tietyn tiedoston sisältö, on tiedostojärjestelmän ensin etsittävä levyltä tiedostoa vastaava inode ja katsottava inoden sisältä missä päin levyä tiedoston data on talletettuna.

Tiedostojen nimi ei ole talletettu inodeen. Tiedoston nimi löytyy ainoastaan hakemistosta. Hakemistot ovat tiedostoja, jotka sisältävät tiedostojen nimiä ja viitteitä nimiä vastaaviin inodeihin.

Seuraava kuva havainnollistaa hakemiston ja inoden suhdetta.

Hakemisto siis sisältää sen sisältämien tiedostojen (ja hakemistojen) nimet sekä linkin niitä vastaaviin inodeihin. Tiedostojen attribuutit löytyvät inodesta, samoin kun linkit niihin levylohkoihin jotka tallettavat tiedoston datan.

Tiedoston attribuutit

Tiedostoa vastaavan inoden sisältöä on mahdollista lukea stat ja fstat komennoilla. man-sivulta:
SYNOPSIS
       #include < sys/types.h>
       #include < sys/stat.h>
       #include < unistd.h>

       int stat(const char *file_name, struct stat *buf);
       int fstat(int filedes, struct stat *buf);

DESCRIPTION
       These  functions  return  information about the specified file.  You do
       not need any access rights to the file to get this information but  you
       need  search rights to all directories named in the path leading to the
       file.

       stat stats the file pointed to by file_name and fills in buf.

       fstat  is  identical  to stat, only the open file pointed to by filedes
       (as returned by open(2)) is stat-ed in place of file_name.

       They all return a stat structure, which contains the following fields:

              struct stat {
                  dev_t         st_dev;      /* device */
                  ino_t         st_ino;      /* inode */
                  mode_t        st_mode;     /* protection */
                  nlink_t       st_nlink;    /* number of hard links */
                  uid_t         st_uid;      /* user ID of owner */
                  gid_t         st_gid;      /* group ID of owner */
                  dev_t         st_rdev;     /* device type (if inode device) */
                  off_t         st_size;     /* total size, in bytes */
                  blksize_t     st_blksize;  /* blocksize for filesystem I/O */
                  blkcnt_t      st_blocks;   /* number of blocks allocated */
                  time_t        st_atime;    /* time of last access */
                  time_t        st_mtime;    /* time of last modification */
                  time_t        st_ctime;    /* time of last change */
              };
stat ja fstat -operaatioiden avulla siis saadaan struct stat -tyyppinen tietue, jonka kenttinä löytyy tietoa tiedoston ominaisuuksista. Komennot eroavat toisistaan sen suhteen, että statin ensimmäisenä parametrina on tiedoston nimi ja fstatissa avoimen (openilla avatun) tiedoston tiedostokuvaaja.

Tiedoston nimi Unixeissa on joko absoluuttinen tai suhteellinen. Absoluuttinen polkunimi alkaa /-merkillä. Esim. tämän html-tiedoston absoluuttinen polkunimi on /home/luuma/public_html/kj/vko10/index.html. Suhteellinen polkunimi taas kertoo tiedoston nimen suhteessa työhakemistoon. Jos työhakemistona olisi /home/luuma/public_html/kj, viitataan tähän html-tiedostoon suhteellisella nimellä vko10/index.html.

Eli jos käytetään tiedstoista suhteellisia nimiä, on työhakemiston oltava oikea. Kun ohjelma käynnistetään komentotulkista, tulee ohjelman työhakemistoksi se hakemisto missä komentotulkki oli ohjelman käynnistyshetkellä. Ohjelma voi vaihtaa työhakemistoa komennolla chdir, man-sivulta:

       #include < unistd.h>

       int chdir(const char *path);

DESCRIPTION
       chdir changes the current directory to that specified in path.


RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and  errno  is
       set appropriately.
Työhakemiston saa selville komennolla getcwd, man-sivulta:
       #include < unistd.h>

       char *getcwd(char *buf, size_t size);

DESCRIPTION
       The  getcwd() function copies an absolute pathname of the current work-
       ing directory to the array pointed to by buf, which is of length  size.

       If  the  current  absolute path name would require a buffer longer than
       size elements, NULL is returned, and errno is set to ERANGE; an  appli-
       cation  should  check  for  this error, and allocate a larger buffer if
       necessary.

       If buf is NULL, the behaviour of getcwd() is undefined.

RETURN VALUE
       NULL on failure with errno set accordingly, and  buf  on  success.  The
       contents of the array pointed to by buf is undefined on error.
Esimerkkinä ohjelma 10-1.c, joka ensin tulostaa ohjelmatiedostonsa koon tavuina. Tämän jälkeen ohjelma tulostaa työhakemistonsa, siirtyy hakemistotasolla yhden tason alaspäin (eli sama kuin cd ..) ja tulostaa jälleen oman työhakemistonsa. Koodi seuraavassa:
#define SIZE 1024

int main(int argc, char *argv[]){
  struct stat attrib;
  char buf[SIZE];

  // pyydetään ohjelmatiedoston attribuutit
  stat(argv[0], &attrib);

  // tulostetaan koko  
  printf("tiedoston %s koko %d tavua\n", argv[0], attrib.st_size);

  // tulostetaan työhakemisto
  getcwd(buf, SIZE);
  printf("työhakemisto: %s\n", buf);

  // mennään yksi taso alaspäin
  chdir("..");

  getcwd(buf, SIZE);
  printf("ja nyt:       %s\n", buf);

  return 0;
}

Hakemistojen käsittely

Unixeissa siis myös hakemistot ovat tiedostoja. Jos hakemiston sisältöä haluaa lukea, on hakemisto avattava komennolla opendir. man-sivulta:
       #include < sys/types.h>
       #include < dirent.h>

       DIR *opendir(const char *name);

DESCRIPTION
       The  opendir()  function  opens a directory stream corresponding to the
       directory name, and returns a pointer to  the  directory  stream.   The
       stream is positioned at the first entry in the directory.

RETURN VALUE
       The  opendir()  function  returns  a pointer to the directory stream or
       NULL if an error occurred.
opendir siis palauttaa osoittimen DIR-tyyppiseen muuttujaan.

Avoinna olevan hakemiston sisältö luetaan komennolla readdir. readdir palauttaa hakemistosta yhden hakemistomerkinnän (directory entry) kerrallaan.

readdirin man-sivulla (huom: sektiossa 3) sanotaan seuraavasti:

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

       struct dirent *readdir(DIR *dir);

DESCRIPTION
       The  readdir()  function returns a pointer to a dirent structure repre-
       senting the next directory entry in the directory stream pointed to  by
       dir.   It  returns  NULL  on  reaching  the  end-of-file or if an error
       occurred.

       According to POSIX, the dirent structure contains a field char d_name[]
       of  unspecified  size,  with  at most NAME_MAX characters preceding the
       terminating null character.  Use of other fields will harm  the  porta-
       bility  of  your  programs.  POSIX 1003.1-2001 also documents the field
       ino_t d_ino as an XSI extension. 

       The data returned by readdir() may be overwritten by  subsequent  calls
       to readdir() for the same directory stream.

RETURN VALUE
       The readdir() function returns a pointer to a dirent structure, or NULL
       if an error occurs or end-of-file is reached.

readdir siis palauttaa osoittimen struct dirent -tyyppiseen tietueeseen. Tietueella on kenttinä tiedoston nimi d_name, ja tiedoston inoden numero d_ino.

readdirillä luetaan hakemiston sisältöä yksi merkintä kerrallaan. Hakemiston koko sisältö saadaan luetuksi kutsumalla readdiriä toistuvasti kunnes koko sisältö on luettu ja readdir palauttaa NULL:in.

Avoinna olevan hakemiston sulkeminen tapahtuu komennolla closedir, joka saa parametrikeen opendirin palauttaman DIR-osoittimen. Ohjelma 10-2.c toteuttaa suunnilleen saman kun komento ls ilman komentoriviparametreja.

int main(void){
    DIR *hakemisto;          // hakemistokahva
    struct dirent *entry;    // osoitin hakemistomerkintään

    // avataan nykyhakemisto eli ".", operaatio palauttaa hakemistokahvan  
    hakemisto = opendir(".");


    while( 1 ){
       entry = readdir(hakemisto);
       if ( entry == NULL ) break;    // ollaan lopussa 

       // tulostetaan hakemistomerkinnän nimi ruudulle
       printf("%s\n",entry->d_name);
    }   

    // suljetaan hakemisto
    closedir(hakemisto);

    return 0;
}
Ojelma 10-3.c on hieman kehittyneempi versio edellisestä. Ohjelma tulostaa joko parametrina annetun hakemiston sisällön tai jos parametria ei ole, tulostetaan sen hakemiston sisältö missä ohjelma käynnistetään.
int main(int argc, char *argv[]){
  DIR *hakemisto;          	// hakemistokahva
  struct dirent *entry;		// osoitin hakemistomerkintään
  struct stat attrib;		// muuttuja tiedoston/hakemiston attribuuteille
  char *hnimi = ".";		// avattavan hakemiston nimi
 
  // jos komentoriviparametri, sijoitetaan tämä luettavan hakemiston nimeksi
  if ( argc>1 ) hnimi = argv[1];   

  // avataan hakemisto, operaatio palauttaa hakemistokahvan  
  hakemisto = opendir(hnimi);

  // testataan epäonnistuiko operaatio
  if ( hakemisto == NULL ) {
    printf("hakemistoa %s ei olemassa\n",hnimi);
    return 0;
  }

   // laitetaan ohjelman hakemistoksi tutkittava hakemisto
  chdir(hnimi);
Nyt hakemisto on avattuna ja muuttujassa hakemisto on DIR-osoitin, jonka kautta hakemiston sisältöä luetaan. Viimeisenä komentona edellä oli chdir, jolla siirrytään siihen hakemistoon, minkä sisältö tulostetaan. Tämä ei ole hakemiston selaamisen kannalta oleellista, mutta koska jatkossa käytetään stat-komentoa ja suhteellisia polkunimiä, niin ohjelman työhakemiston on oltava sama kun selattava hakemisto.

Perusratkaisuna jälleen while-silmukka, jossa hakemiston sisältö luetaan entry kerrallaan. Tällä kertaa toimitaan siten, että jos entry on hakemisto, tulostetaan hakemiston nimen perään /-merkki. Jos entry on normaali tiedosto, tulostetaan tiedoston koko tavuina.

Tiedoston tyyppi selviää stat-funktion avulla. Funktion palauttaman structin st_mode-kenttä sisältää pakattuna paljon tietoa, mm. tiedon siitä minkä tyyppinen tiedosto on, eli onko kyseessä hakemisto vai normaali tiedosto. stat-komennon man-sivulta selviää, että kentän arvon tutkimiseen on olemassa valmiita makroja, esim. S_ISDIR(attrib.st_mode) on tosi (eli lausekkeella arvo 1) jos kyseessä on hakemisto ja S_ISREG(attrib.st_mode) on tosi jos kyseessä normaali tiedosto.

Ohjelma tutkii tiedoston attribuuttien st_mode-kenttää ja muotoilee tämän perusteella tulostuksen oikeanlaiseksi.


  while( 1 ){
    entry = readdir(hakemisto);

    if ( entry == NULL ) break;

    // luetaan hakemistomerkinnän attribuutut muuttujaan attrib
    stat(entry->d_name, &attrib); 
         
    // testataan, onko kyseessä hakemisto
    if ( S_ISDIR(attrib.st_mode) ) {
      // tulostetaan hakemistonimen perään kenoviiva eli /-merkki
      printf("%s/\n",entry->d_name);
    }
    else if ( S_ISREG(attrib.st_mode) ) { 
      // tulostetaan tiedoston nimen lisäksi koko tavuina 
      printf("%-30s %10d B\n",entry->d_name, attrib.st_size);
    }
  }   

  // suljetaan hakemisto
  closedir(hakemisto);

  return 0;
}

Linkitys ja tiedoston poistaminen

Unixeissa tiedosto voidaan laittaa näkymään useammassa hakemistossa kahta eri tekniikkaa käyttäen. Jos tiedostoon luodaan kova linkki (hard link), laitetaan tiedosto näkymään uuteen hakemistoon siten, että hakemistosta tulee viittaus tiedoston inodeen. Tiedoston nimi uudessa hakemistossa on täysin riippumaton tiedoston alkuperäisestä nimestä. Seuraavassa tilanne, missä tiedostoon myfile.c on luotu kova linkki ja linkitetyssä hakemistossa tiedoston nimi on koodi.c.

inodessa on kenttä, joka kertoo kuinka monessa hakemistossa tiedosto on linkitettynä. Kenttän arvo on stat-komennon palauttaman tietueen kentässä st_nlink. Tiedosto ja inode eivät tiedä mistä hakemistoista tiedostoon viitataan.

Kova linkki luodaan komennolla link, man-sivulta:

       #include < unistd.h>

       int link(const char *oldpath, const char *newpath);

DESCRIPTION
       link  creates  a  new  link  (also known as a hard link) to an existing
       file.

       If newpath exists it will not be overwritten.

       This new name may be used exactly as the old  one  for  any  operation;
       both names refer to the same file (and so have the same permissions and
       ownership) and it is impossible to tell which name was the  `original'.

RETURN VALUE
       On  success,  zero is returned.  On error, -1 is returned, and errno is
       set appropriately.
Kuten man-sivu kuvaa, linkityksen jälkeen ei enää mistään erota mikä oli tiedoston aluperäinen nimi.

Kovan linkin (kuten myös ns. symbolisen linkin josta kohta enemmän) saa poistettua komennolla unlink, man-sivulta:

       #include < unistd.h>

       int unlink(const char *pathname);

DESCRIPTION
       unlink  deletes  a  name from the filesystem. If that name was the last
       link to a file and no processes have the file open the file is  deleted
       and the space it was using is made available for reuse.

       If  the  name  was the last link to a file but any processes still have
       the file open the file will remain in existence  until  the  last  file
       descriptor referring to it is closed.

       If the name referred to a symbolic link the link is removed.
Eli komento poistaa tiedoston jos se suoritetaan tiedostolle johon on enää yksi linkki jäljellä. Unixeissa ei ole mitään erillistä tiedoston poistamiseen tarkoitettua käskyä, poistaminen hoidetaan aina unlink-komennolla ja KJ pitää huolen siitä, että jos tiedosto ei ole enää missään hakemistossa, se poistetaan.

Toinen linkkityyppi on symbolinen linkki (symbolic link tai soft link), joka luodaan komennolla symlink. Erona kovaan linkkiin on se, että symbolinen linkki ei osoita suoraan inodeen vaan sisältää jonkun polkunimen. Jos on luotu symbolinen linkki, missä nimi x.c on linkitetty tiedostoon /home/luuma/koe.c, niin tiedoston x.c avaaminen saakin aikaan sen, että avataan tiedosto /home/luuma/koe.c. Symbolinen linkki on siis vain viite johonkin toiseen kohtaan hakemistohierarkiassa. Symbolisen linkin luominen ei kasvata inoden linkkikenttää.

Muistiinkuvatut tiedostot (advanced topic)

Katsomme vielä yhden tiedostoihin liittyvän asian. Edellisllä tunnilla käytettiin komentoa mmap liittämään jaettuja muistialueita prosessien muisiavaruuteen, ks. esim. 9-1.c.

Jaettu muistialue luotiin shm_open-komennolla, joka palautti tiedostokuvaajan luotuun muistialueeseen. Komennolle mmap annettiin sitten muistialuetta edustanut tiedostokuvaaja, ja mmap palautti osoittimen jaetulle muistialueelle.

mmap-komentoa voidaan käyttää myös tiedoston sisällön mappaamiseksi keskusmuistiin. Tämä seikka selviää myös mmap-komennon man-sivulta:

       #include < sys/mman.h>

       void  *mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);


DESCRIPTION
       The mmap function asks to map length bytes starting  at  offset  offset
       from  the  file  (or  other object) specified by the file descriptor fd
       into memory, preferably at address start.  This  latter  address  is  a
       hint  only,  and is usually specified as 0.  The actual place where the
       object is mapped is returned by mmap, and is never 0.
Tarkastellaan ohjelmaa 10-4.c, joka mappaa tiedoston muistiinsa ja tulostaa muistiin mapatun tiedoston sisällön lukien tiedostoa osoittimen avulla aivan kuin kyseessä olisi keskusmuistin sisällön lukeminen.
int main(int argc, char *argv[]){
  int fd;

  if ( argc<2 ) return 0;

  // avataan komentoriviparametrina annettu tiedosto
  fd = open( argv[1], O_RDONLY );

  if ( fd == -1 ){
    perror("ongelma openissa");
    return -1;
  }

  // selvitetään tiedoston koko fstat-funktiolla
  struct stat st;
  fstat( fd, &st );
  int koko = st.st_size;

  // mapataan tiedosto muistiin, osoitteeksi p
  char *p;
  p = (char *) mmap( 0, koko, PROT_READ, MAP_SHARED, fd, 0 );
  if ( p == MAP_FAILED ){
    perror("ongelma jaetun muistialueen luomisessa");
    return -1;
  }

  // tulostetaan mapatun muistialueen sisältö
  int i;
  for ( i=0; i < koko; i++ ){
    printf("%c", p[i] );
  }

  // poistetaan mappays ja suljetaan tiedosto
  munmap( p, koko );
  close( fd );

  return 0;
}
Jos tiedosto olisi avattu ja mapattu luku- ja kirjoitusoikeuksilla, menisivät kaikki mapatulle muistialueelle tehdyt muutokset tiedostoon.