Přímý přístup na disk

O přímém přístupu k souborům jsem psal v první kapitole věnované práci se soubory. Nejdůležitější, co byste si měli pamatovat, je, že tento způsob práce se soubory není v normě ANSI C (ale je součástí normy POSIX), není tedy zaručena přenositelnost. Výhodou oproti datovým proudům je rychlost?. Funkce pracující s přímým přístupem na disk se chovají velice podobně jako funkce pracující s datovými proudy. Díky tomu může být výklad velice stručný. Funkce jsou deklarovány v knihovnách <sys/types.h>, <sys/stat.h>, <fcntl.h> a <unistd.h>.

Deskriptor souboru

Deskriptor souboru je kladné celé číslo, které slouží k identifikaci souboru v rámci programu. Někdy se mu také říká manipulační číslo. Toto číslo využívají všechny funkce pracující s přímým přístupem na disk.

Pokud již máte otevřený datový proud a chcete získat jeho deskriptor, můžete k tomu použít funkci fileno() (ne že by to bylo nějak zvlášť potřeba). Tato funkce je deklarována v souboru <stdio.h>.

int fileno(FILE *stream);

Otevření a uzavření souboru

Chcete-li pracovat se souborem pomocí přímého přístupu na disk, musíte jej nejdříve „otevřít“ funkcí open(), obdobně jako u datových proudů jste museli otevřít soubor pomocí fopen(). Návratovou hodnotou funkce open() je potřebný deskriptor souboru. Existují dvě možné verze deklarace funkce open().

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

Prvním argumentem funkce open() je pathname. To je překvapivě jméno otvíraného souboru. Druhým argumentem je flags, neboli příznak otevření souboru.

Příznaky flags pro open()
Příznak Otevření pro
O_RDONLY čtení
O_WRONLY zápis
O_RDWR čtení i zápis

Příznak se kombinuje pomocí bitového součtu | s některými příznaky z následující tabulky. Kromě těchto vypsaných existují další (implementačně závislé), takže si prostudujte nápovědu :-).

Další příznaky flags pro funkci open()
Příznak Význam
O_CREAT Pokud soubor neexistuje, vytvoří se.
O_EXCL Používá se spolu s volbou O_CREAT. V případě, že soubor existuje, otevírání selže a funkce open() vrátí chybu.
O_TRUNC Pokud soubor existuje a je nastaven pro zápis (O_WRONLY nebo O_RDWR) je zkrácen na nulovou délku
O_APPEND Zápis bude prováděn na konec souboru.

V systému MS-DOS jsou v souboru <fcntl.h> definovány ještě dva důležité příznaky, které určují zda chcete pracovat v textovém či binárním režimu.

#define O_TEXT      0x4000  /* CR-LF translation */
#define O_BINARY    0x8000  /* no translation    */

V Linuxu není rozdílu mezi binárními a textovými soubory, proto tyto příznaky v souboru <fcntl.h> nenajdete. Chcete-li, aby vaše programy byli co možná nejvíce přenositelné, musíte na to brát ohled (viz. příklad).

Nyní se podívejte na druhou deklarací funkce open(). Ta má navíc parametr mode, který určuje přístupová práva k nově vytvářenému souboru (jinak nemá smysl). Přístupová práva v Unixech jsou pak nastavena na hodnotu mode & ~umask. Tedy obdobně jako při vytváření adresářů.
Jaká přístupová práva lze použít je závislé na OS, respektive souborovém systému. V DOSu můžete nastavit S_IWRITE a S_IREAD, v Unixu jsou jiná přístupová práva, např. S_IRUSR a S_IWURS (je jich mnohem víc). Více najdete v nápovědě. Přístupová práva se opět mohou kombinovat pomocí bitového součtu |.

Návratovou hodnotou funkce open() je deskriptor souboru, nebo -1 v případě chyby.

K otevření souboru s přímým přístupem na disk slouží jednak funkce open() popsaná v předchozím odstavci, nebo funkce creat(). Funkce creat() dělá to samé jako funkce open() s nastaveným argumentem flags na O_CREAT|O_WRONLY|O_TRUNC.

int creat(const char *pathname, mode_t mode);

K uzavření souboru se používá funkce close(), jejímž jediným argumentem je deskriptor souboru, který uzavíráte. Děláte to ze stejných důvodů, jako při uzavírání datového proudu pomocí fclose().

int close(int fd);

Návratovou hodnotou je 0, nebo, v případě neúspěchu, -1. Pokud chcete mít jistotu, že po zavření souboru jsou data na disku, musíte použít funkci sync(). Prostudujte si nápovědu …

Čtení, zápis a posun

Ze souboru se čte pomocí funkce read(), do souboru se zapisuje pomocí funkce write(). (Možná si říkáte, jaká je nuda tohle číst … co potom to psát!).

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

Argument fd je deskriptor otevřeného souboru, argument buf je ukazatel do paměti, kde jsou zapisovaná data, nebo kam načtená data ukládáte (argument je třeba přetypovat). Argument count je počet načítaných (zapisovaných) bajtů. Návratovou hodnotou je počet skutečně načtených (zapsaných) bajtů, nebo, v případě neúspěchu, -1.

Opět upozorňuji, že pokus načtení (zápisu) více bajtů, než je velikost paměti buf skončí tragédií a že jistotu vyprázdnění vyrovnávací paměti budete mít až po použití funkce fsync().

Podobně, jako se můžete pohybovat v datovém proud pomocí fseek(), můžete se zde pohybovat v souboru pomocí lseek().

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

Funkce lseek posune ukazatel deskriptoru souboru fildes o vzdálenost offset bajtů od místa určeným argumentem whence.
Argument whence může být SEEK_SET (posun od začátku), SEEK_CUR (posun od aktuální pozice) nebo SEEK_END (od konce souboru). Pokud posunete ukazatel za konec souboru a pak něco zapíšete, vzniklá mezera (mezi původním koncem souboru a nově zapsanými daty) bude vyplněna nulami.

Návratovou hodnotou je v případě úspěchu pozice na kterou se deskriptor přesunul, jinak -1.

Informace o typech ssize_t, off_t a dalších se dočtete třeba v manuálové stránce k types.h. Zjednodušeně řečeno jsou to typy u nichž je zaručeno, že jsou tak velké, aby se do nich vešlo, co se vejít má :-)

Příklad

Konečně něco zábavnějšího. Následující dva programy budou dělat to samé. Jeden bude využívat datové proudy, druhý přímý přístup na disk. Můžete tak porovnat jejich rychlost (v Linuxu např. pomocí programu time()). Nejdříve se podívejte na soubor s přímým přístupem na disk (zapis1.exe).

  1. /*------------------------------------------------*/
  2. /* c35/zapis1.c                                   */
  3. #define _CRT_NONSTDC_NO_DEPRECATE
  4. #define _CRT_SECURE_NO_WARNINGS
  5. #include <stdio.h>
  6. #include <stdlib.h>
  7. #include <string.h>
  8. #include <sys/types.h>
  9. #include <sys/stat.h>
  10. #include <fcntl.h>
  11. #ifdef unix
  12. #include <unistd.h>
  13. #else
  14. #include <io.h>
  15. #endif
  16.  
  17. #define POCET_CYKLU 1000000u
  18.  
  19. #ifdef __unix__
  20. #define OPEN open("jmeno.txt",O_RDWR|O_CREAT|O_TRUNC,\
  21.                 S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)
  22. #else
  23. #define OPEN open("jmeno.txt",O_RDWR|O_CREAT|O_TRUNC|O_TEXT)
  24. #endif
  25.  
  26.  
  27. int main(void)
  28. {
  29.     int id;
  30.     size_t i;
  31.     char SNO[] = "Soubor nelze otevrit (uz existuje?)\n";
  32.     char TEXT1[] = "Tenhle text budeme zapisovat do souboru\n";
  33.     char TEXT2[] = "-------------- prepsano ---------------\n";
  34.  
  35.     if ((id = OPEN) <= 0) {
  36.         /* fprintf by bylo vhodnejsi, ale nemohl jsem si to odpustit :-} */
  37.         write(fileno(stderr), SNO, strlen(SNO));
  38.         perror(NULL);
  39.         exit(1);
  40.     }
  41.  
  42.     for (i = 0; i < POCET_CYKLU * 10u; i++) {
  43.         if (write(id, TEXT1, strlen(TEXT1)) == -1) {
  44.             fprintf(stderr, "Data nelze zapisovat");
  45.             perror(NULL);
  46.             exit(1);
  47.         }
  48.     }
  49.  
  50.  
  51.     lseek(id, 0, SEEK_SET);
  52.     for (i = 0; i < POCET_CYKLU; i++) {
  53.         if (write(id, TEXT2, strlen(TEXT2)) == -1) {
  54.             fprintf(stderr, "Data nelze zapisovat");
  55.             perror(NULL);
  56.             exit(1);
  57.         }
  58. #ifdef __unix__
  59. #define clrf 0
  60. #else
  61. #define clrf 1
  62. #endif
  63.         lseek(id, (strlen(TEXT1) + clrf) * 9, SEEK_CUR);
  64. #undef clrf
  65.     }
  66.  
  67.     close(id);  /* spravne by bylo kontrolovat navratovou hodnotu */
  68.     return 0;
  69. }
  70.  
  71. /*------------------------------------------------*/

V programu se definuje makro clrf v závislosti na operačním systému a jeho hodnota se přičítá k délce textu. Je to proto, protože v DOSu (a Windows) je znak konec řádku '\n' do souboru zapisován pomocí dvou bajtů, kdežto v Unixu pomocí jednoho. Funkce strlen() znak '\n' počítá vždy jako jeden znak. Zbytek už je otázka malé násobilky.

Visual Studio

Makro _CRT_SECURE_NO_WARNINGS je tu kvůli funkci scanf(), viz scanf().

Makro _CRT_NONSTDC_NO_DEPRECATE povoluje funkce, které považuje VS za zastaralé, jako je například funkce mkdir(), nebo open().
Tyto funkce můžete ve VS studiu nahradit funkcemi s podtržítkem na začátku (např. _open()), což jsou novější ekvivalentní funkce ze standardu jazyka C++.

Druhý program využívá datové proudy (zapis2.exe).

  1. /*------------------------------------------------*/
  2. /* c35/zapis2.c                                   */
  3. #define _CRT_SECURE_NO_WARNINGS
  4. #include <stdio.h>
  5. #include <stdlib.h>
  6. #include <string.h>
  7.  
  8. #define POCET_CYKLU 1000000u
  9.  
  10. #define OPEN fopen("jmeno.txt","w+")
  11.  
  12. int main(void)
  13. {
  14.     FILE *id;
  15.     size_t i;
  16.     char SNO[] = "Soubor nelze otevrit\n";
  17.     char TEXT1[] = "Tenhle text budeme zapisovat do souboru\n";
  18.     char TEXT2[] = "-------------- prepsano ---------------\n";
  19.  
  20.     if ((id = OPEN) == NULL) {
  21.         fprintf(stderr, "%s", SNO);
  22.         perror(NULL);
  23.         exit(1);
  24.     }
  25.  
  26.     for (i = 0; i < POCET_CYKLU * 10u; i++) {
  27.         if (fwrite(TEXT1, strlen(TEXT1), 1, id) != 1) {
  28.             fprintf(stderr, "Data nelze zapisovat");
  29.             perror(NULL);
  30.             exit(1);
  31.         }
  32.     }
  33.  
  34.     fseek(id, 0, SEEK_SET);
  35.     for (i = 0; i < (POCET_CYKLU); i++) {
  36.         if (fwrite(TEXT2, strlen(TEXT2), 1, id) != 1) {
  37.             fprintf(stderr, "Data nelze zapisovat");
  38.             perror(NULL);
  39.             exit(1);
  40.         }
  41. #ifdef unix
  42. #define clrf 0
  43. #else
  44. #define clrf 1
  45. #endif
  46.         fseek(id, (strlen(TEXT1) + clrf) * 9, SEEK_CUR);
  47. #undef clrf
  48.     }
  49.  
  50.     fclose(id);
  51.     return 0;
  52. }
  53.  
  54. /*------------------------------------------------*/

Po spuštění obou programů vznikne na disku soubor jmeno.txt velikosti 382 MiB. Na mém stařičkém pentiu běžel program zapis1.exe 3 minuty a 14.1 vteřiny, zatímco program zapis2.exe 3 minuty a 47.1 vteřiny. To je zhruba o 17% pomaleji. Otázka je, zda zrychlení programu pomocí přímého přístupu na disk stojí za všechny ty komplikace (nižší komfort při programování a nižší přenositelnost kódu).

Navíc také platí, že „fopen()“ využívá vyrovnávací pamtěti, což může vést k tomu, že je nakonec rychlejší než „open()“. Mé doporučení zní: používejte fopen(), pokud nemáte nějaký fakt dobrý důvod použít open(). Někdy máte možnost přistupovat ke zdrojům jen skrze deskriptory souborů (například při práci se sítí). Přesně pro takovéto situace tu jsou funkce jako open().

Komentář Hlášení chyby
Vytvořeno: 29.8.2003
Naposledy upraveno: 26.6.2014
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..