další předchozí obsah
Další: Soubory a adresáře Předchozí: Programátorské základy

  Souborový vstup a výstup





Úvod

Nejdříve popíšeme funkce pro souborový vstup a výstup. Většina vstupně/výstupních operací může být provedena pomocí pouze pěti funkcí: open, read, write, lseek a close. Tyto se také nazývají nebufferované I/O funkce (na rozdíl např. od tzv. standardních vstupně/výstupních funkcí, které jsou bufferované).



  Funkce open

Soubor je otevřen jádrem po volání funkce open.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open (const char *pathname, int oflag, ... /* mode_t mode */ );
Vrací: deskriptor nebo -1 při chybě

Někdy se používá i třetí argument. pathname je jméno souboru, který chceme vytvořit nebo otevřít. oflag je způsob otevření souboru; tento argument můžeme spojovat pomocí bitového nebo (|).

O_RDONLY Otevření pouze pro čtení.
O_WRONLY Otevření pouze pro zápis.
O_RDWR Otevření pro čtení i zápis.
O_APPEND Připojení na konec souboru.
O_CREAT Vytvoření neexistujícího souboru.
Vyžaduje třetí argument pro určení práv.
O_EXCL Vygeneruje chybu, když je použito zároveň s O_CREAT.
Funkce jen otestuje existenci souboru.
O_SYNC Každý write čeká na fyzické ukončení I/O operace.

Tento výčet není úplný, je jen orientační.

Deskriptor, který funkce vrátí je nejmenší volný deskriptor systému. Tato vlastnost je důležitá pro další práci se soubory.



Ořezávání délky jmen souborů

V systému je definována proměnná udávající maximální délku jména souboru NAME_MAX. Jestliže překročíte délku, System V jméno tiše zkrátí na odpovídající počet znaků. Naproti tomu BSD systémy vrátí chybu ENAMETOOLONG. Tento problém není jen u vytváření nových souborů.



Funkce creat

Nový soubor můžeme vytvořit také voláním funkce create.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int creat (const char *pathname, mode_t mode);
Vrací: deskriptor otevřený jen pro zápis nebo -1 při chybě

Tato funkce je ekvivalentní volání

  open (pathname, O_WRONLY | O_CREAT | O_TRUNC | mode);

pathname je jméno souboru, který chceme vytvořit nebo otevřít. Parametr mode se týká práv -- vysvětlíme později (v "Soubory a adresáře"). Funkce create byla používána dříve, kdy funkce open neuměla to, co umí dnes.



  Funkce close

Tato funkce zavře otevřený soubor.

#include <unistd.h>
int close (int filedes);
Vrací: 0 když OK, -1 při chybě

Uzavření souboru způsobí také odemčení všech zámků na soubor.

(Se zámky pracují funkce fcntl, lockf, flock. Zde platí: různý systém -- různý způsob zamykání.).



Funkce lseek

Každý otevřený soubor má svůj current file offset. Toto nezáporné číslo ukazuje počet bajtů od počátku souboru. Normální I/O operace začínají od začátku, tj. offset 0 (když není specifikována podmínka O_APPEND). Pozice ukazovátka v souboru může být explicitně nastavena pomocí lseek.

#include <sys/types.h>
#include <unistd.h>
off_t lseek (int filedes, off_t offset, int whence);
Vrací: nový offset když je vše OK, -1 při chybě

Interpretace offset závisí na parametru whence, který může být:

Můžeme tedy snadno převinout soubor na začátek. Tato technika se používá pro určení, zda dané zařízení umožňuje seek.

#include <sys/types.h>

int
main(void)
{
        if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
                printf("cannot seek\n");
        else
                printf("seek OK\n");
        exit(0);
}



  Funkce read

Data ze souboru obdržíme voláním funkce read.

#include <unistd.h>
ssize_t read (int filedes, void *buff, size_t nbytes);
Vrací: počet načtených bajtů, 0 když konec, -1 při chybě

Existuje několik případů, ve kterých je počet načtených bajtů menší, než kolik požadujeme:



  Funkce write

Data zapíšeme do souboru pomocí funkce write.

#include <unistd.h>
ssize_t write (int filedes, void *buff, size_t nbytes);
Vrací: počet zapsaných bajtů, -1 při chybě

Data se zapisují na aktuální pozici (offset).



Účinnost I/O

Jako ilustraci důležitosti nastavení správné velikosti bufferu pro I/O operace si ukážeme program a tabulku výsledků s tímto programem dosažených.

Program kopíruje soubor za použití funkcí read a write a následujících předpokladů.

#define    BUFFSIZE    8192

int
main(void)
{
    int   n;
    char  buf[BUFFSIZE];

    while ( (n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
        if (write(STDOUT_FILENO, buf, n) != n)
            err_sys("write error");

    if (n < 0)
        err_sys("read error");

    exit(0);
}

Tímto programem autoři knihy [7] zkoušeli kopírovat soubor o velikosti 1,468.802 bajtů, při použití několika různých velikostí bufferu. Poznamenejme, že standardní výstup byl přesměrován do /dev/null (do tzv. černé díry). Soubor byl uložen na Berkeley filesystému s velikostí bloku 8192 bajtů. Proto zvětšování bufferu nad tuto hodnotu nemělo již význam pro zrychlení. Výsledky shrnuje tabulka 7.

 

BUFFSIZE User CPU System CPU
(sekundy)
Clock time
(sekundy)
počet cyklů
(sekundy)
1 23.8 397.9 423.4 1468802
2 12.3 202.0 215.2 734401
4 6.1 100.6 107.2 367201
8 3.0 50.7 54.0 183601
16 1.5 25.3 27.0 91801
32 0.7 12.8 13.7 45901
64 0.3 6.6 7.0 22950
128 0.2 3.3 3.6 11475
256 0.1 1.8 1.9 5738
512 0.0 1.0 1.1 2869
1024 0.0 0.6 0.6 1435
2048 0.0 0.4 0.4 718
4096 0.0 0.4 0.4 359
8192 0.0 0.3 0.3 180
16384 0.0 0.3 0.3 90
32768 0.0 0.3 0.3 45
65536 0.0 0.3 0.3 23
131072 0.0 0.3 0.3 12
Tabulka 7: Časy kopírování při různé velikosti bufferu


Sdílení souborů

Unix podporuje sdílení souborů mezi různými procesy. Před popisem funkce dup musíme toto sdílení blíže popsat. Jádro používá celkem tři datové struktury:

  1. Každý proces má položku v tabulce procesů, která mj. obsahuje tabulku otevřených souborových deskriptorů. Ta obsahuje:

    1. příznaky deskriptoru souboru
    2. ukazatel na tabulku souborů

  2. Tabulku souborů jádro udržuje pro všechny otevřené soubory. Každý záznam obsahuje:

    1. status souboru
    2. ukazatel v souboru (offset)
    3. ukazatel do tabulky v-uzlů

  3. Každý otevřený soubor je v tabulce v-uzlů. V-uzel obsahuje informace o typu souboru a ukazatele na funkce, které s ním pracují.

Obrázek 1 ukazuje uspořádání těchto tří tabulek pro jeden proces, který má otevřené dva různé soubory -- jeden soubor je otevřený na standardním vstupu (deskriptor číslo 0) a druhý je otevřený na standardním výstupu (deskriptor 1).

Zde má být moc hezký obrázek 'filestru', škoda, že ho nevidíte

Obrázek 1: Struktury v jádře pro otevřené soubory

Pokud si dva nezávislé procesy otevřou ten samý soubor, nastane situace, která je zobrazena na obr. 2.

Zde má být moc hezký obrázek '2proc1fi', škoda, že ho nevidíte

Obrázek 2: Dva procesy mají otevřen stejný soubor



Atomické operace



Připojení na konec souboru

V případě, že unix nepodporuje příznak O_APPEND, lze ho nahradit:

         if (lseek(fd, 0L, 2) < 0 )
           err_sys("lseek error");
         if (write(fd, buff, 100) != 100)
           err_sys("write error");

Tento scénář pracuje dobře pro jeden proces, ale ve víceúlohovém prostředí může být zdrojem potíží. Potíže vznikají právě proto, že takováto operace není atomická, tj. není dále nedělitelná. Proto je nutné zde použít atomickou operaci open s příznakem O_APPEND.



Vytváření souboru

Obdobné potíže mohou vzniknout při vytváření souboru.

 
      if ( (fd = open(pathname, O_WRONLY)) < 0)
      if (errno == ENOENT) {
         if (fd = creat(pathname, mode)) < 0)
            err_sys("creat error");
      }
      else  err_sys("open error");

Problém může nastat mezi funkcemi open a create.



Funkce dup a dup2

Existující deskriptor lze duplikovat pomocí následujících funkcí:

#include <unistd.h>
int dup (int *filedes);
int dup2 (int *filedes, int filedes2);
Vrací: nový deskriptor nebo -1 při chybě

Nový deskriptor je nejnižší volný deskriptor (ve stejné tabulce souborů jako starý). S dup2 specifikujeme nový deskriptor hodnotou filedes2. Jestliže je deskriptor filedes2 otevřen, pak se nejdřív uzavře.

Na obrázku 3 je ilustrována situace po provedení:

    newfd = dup(1);

Zde má být moc hezký obrázek 'dup', škoda, že ho nevidíte

Obrázek 3: Souborové struktury v jádře po dup(1)



Funkce fcntl

Tato funkce může změnit charakter otevřeného souboru.

#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
int fcntl (int *filedes, int cmd, ... /* int arg */ );
Vrací: když OK, záleží na cmd nebo -1 při chybě

Funkce fcntl se dá použít pro tyto účely:



Funkce ioctl

Funkce ioctl je určená k provádění řídících operací nad I/O zařízením.

#include <unistd.h> /* SVR4 */
#include <sys/ioctl.h> /* BSD 4.3+ */
int ioctl (int *filedes, int request, ...);
Vrací: -1 při chybě, něco jiného když je OK

Funkce se používá ke speciálním operacím s disky, páskou, terminálem atd.



Cvičení

  1. Napište vlastní implementaci funkce dup2 bez použití standardní funkce fcntl. Ošetřete možné chybové stavy.
  2. Pokud otevřete soubor pro čtení i psaní s flagem přidání (O_APPEND), můžete stále číst funkcí read odkudkoli ze souboru pomocí lseek? Můžete použít lseek k přemazání existujících dat v souboru? Napište program na ověření.


další předchozí obsah
Další: Soubory a adresáře Předchozí: Programátorské základy

Ladislav Dobias
Sat Nov 1 15:38:32 MET 1997