File I/O: The Universal I/O model
Using the Windows File System
Fișierul este una dintre abstractizările fundamentale în domeniul sistemelor de operare; cealaltă abstractizare este procesul. Dacă procesul abstractizează execuția unei anumite sarcini pe procesor, fișierul abstractizează informația persistentă a unui sistem de operare. Un fișier este folosit pentru a stoca informațiile necesare funcționării sistemului de operare și interacțiunii cu utilizatorul.
Un sistem de fișiere este un mod de organizare a fișierelor și prezentare a acestora utilizatorului. Din punct de vedere al utilizatorului, un sistem de fișiere are o structură ierarhică de fișiere și directoare, începând cu un director rădăcină. Localizarea unei intrări (fișier sau director) se realizează cu ajutorul unei căi în care sunt prezentate toate intrările de până atunci. Astfel, pentru calea /usr/local/file.txt
directorul rădăcină '/
' are un subdirector usr
care include subdirectorul local
ce conține un fișier file.txt
.
Fiecare fișier are asociat, așadar, un nume cu ajutorul căruia se face identificarea, un set de drepturi de acces și zone conținând informația utilă.
Sistemele de fișiere suportate de sistemele de operare de tip Unix și Windows sunt ierarhice. Sistemele Linux/Unix sunt case-sensitive (Data
este diferit de data
), iar sistemele Windows sunt case-insensitive..
Ierarhia sistemului de fișiere Unix are un singur director cunoscut sub numele de root
și notat '/
', prin care se localizează orice fișier (a nu se confunda cu directorul /root
, care este home-ul utilizatorului privilegiat, root). Notația Unix pentru căile fișierelor este un șir de nume de directoare despărțite prin '/
', urmat de numele fișierului. Există și căi relative la directorul curent '.
' sau la directorul părinte '..
'.
În Unix nu se face nicio deosebire între fișierele aflate pe partițiile discului local, pe CD sau pe o mașină din rețea. Toate aceste fișiere vor face parte din ierarhia unică a directorului root
. Acest lucru se realizează prin montare
: sistemele de fișiere vor fi montate într-unul dintre directoarele sistemului de fișiere rădăcină.
În Windows există mai multe ierarhii, câte una pentru fiecare partiție și pentru fiecare loc din rețea. Spre deosebire de Unix, delimitatorul între numele directoarelor dintr-o cale este '\
', și pentru căile absolute trebuie specificat numele ierarhiei în forma C:\
, E:\
sau \\FILESERVER\myFile
(pentru rețea). Ca și Unix, Windows folosește '.
' pentru directorul curent și '..
' pentru directorul părinte.
În Unix, un descriptor de fișier este un întreg care indexează o tabelă cu pointeri spre structuri care descriu fișierele deschise de un proces. În cazul în care un program rulează într-un shell Unix, procesul părinte (shell-ul) deschide pentru procesul copil (programul respectiv) 3 fișiere standard având descriptori de fișiere cu valori speciale:
În Windows, noțiunea de bază pentru managementul fișierelor este handle-ul, o valoare din care se obține un pointer spre o structură descriptivă a fișierului. Aceleași 3 fișiere standard sunt deschise de fiecare proces.
În continuare, pentru descrierea comportamentului operațiilor de intrare-ieșire pe Windows, s-a ales ca toate apelurile să facă parte din API-ul Win32, care este cel mai aproape de kernelul Windows. Sistemul oferă ca alternativă apeluri standard (POSIX, de exemplu, compatibile între Windows și Linux), dar acestea se implementează în Windows prin apelurile Win32 și formează un nivel de abstractizare aflat mai departe de kernel.
Un fișier are asociat cursorul de fișier (file pointer) care indică poziția curentă în cadrul fișierului. Cursorul de fișier este un întreg care reprezintă deplasamentul (offset-ul) față de începutul fișierului.
Operațiile specifice pentru lucru cu fișiere:
Pentru deschiderea/crearea unui fișier se folosește funcția open.
int open(const char *pathname, int flags); /* deschidere */ int open(const char *pathname, int flags, mode_t mode); /* creare */
Pentru crearea de fișiere se poate utiliza și creat:
int creat(const char *pathname, mode_t mode);
Funcția este echivalentă cu apelul open
unde flag-ul O_CREAT e setat și fișierul nu există deja:
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
Închiderea de fișiere se realizează cu close:
int close(int fd)
O greșeală frecventă de programare este neverificarea codului de eroare întors la close, pentru că se poate întâmpla ca o eroare la scriere (EIO
) să fie întoarsă utilizatorului abia la close
.
Ștergerea efectivă a unui fișier de pe disk se realizează cu funcția unlink:
int unlink(const char *pathname);
Dacă, spre exemplu, dorim să deschidem fișierul in.txt
pentru citire și scriere, cu eventuala creare a acestuia, iar fișierul out.txt
pentru scriere, cu trunchiere putem folosi următoarea secvență de cod:
#include <sys/types.h> /* open */ #include <sys/stat.h> /* open */ #include <fcntl.h> /* O_RDWR, O_CREAT, O_TRUNC, O_WRONLY */ #include <unistd.h> /* close */ #include "utils.h" int main(void) { int fd1, fd2; fd1 = open ("in.txt", O_RDWR | O_CREAT, 0644); DIE(fd1 < 0, "open in.txt"); /* will fail if out.txt does not exist */ fd2 = open ("out.txt", O_WRONLY | O_TRUNC); DIE(fd2 < 0, "open out.txt"); rc = close(fd1); DIE(rc < 0, "close fd1"); rc = close(fd2); DIE(rc < 0, "close fd2"); return 0; }
Atenție! O greșeală frecventă este omiterea drepturilor de creare a fișierului (0644 în exemplul de mai sus) când se apelează open cu flag-ul O_CREAT setat.
Funcția read e folosită pentru citirea din fișier a maxim count
octeți:
ssize_t read(int fd, void *buf, size_t count);
Funcția read întoarce numărul de octeți efectiv citiți, cel mult count
. Valoarea minimă este de 1
octet, iar când se ajunge la sfârșitul de fișier se va întoarce 0
.
Funcția write e folosită pentru scrierea în fișier a maxim count
octeți:
ssize_t write(int fd, const void *buf, size_t count);
Valoarea întoarsă este numărul de octeți ce au fost efectiv scriși, cel mult count
. În mod implicit nu se garantează că la revenirea din write scrierea în fișier s-a terminat. Pentru a forța actualizarea se poate folosi fsync sau fișierul se poate deschide folosind flagul O_FSYNC
, caz în care se garantează că după fiecare write fișierul a fost actualizat.
Observație:
Pentru read/write există versiunile pread/pwrite, care permit specificarea unui offset în fișier de la care să se efectueaze operația de citire/scriere. (De asemenea, există și versiunile pread64
/pwrite64
care folosesc offset-uri de 64 de biți - pentru a putea specifica offset-uri mai mari decât 4GB).
Funcția lseek permite mutarea cursorului unui fișier la o poziție absolută sau relativă.
off_t lseek(int fd, off_t offset, int whence)
Parametrul whence
reprezintă poziția relativă de la care se face deplasarea:
SEEK_SET
- față de poziția de începutSEEK_CUR
- față de poziția curentăSEEK_END
- față de poziția de sfârșitObservație lseek permite și poziționări după sfârșitul fișierului. Scrierile care se fac în astfel de zone nu se pierd, ceea ce se obține fiind un fișier cu goluri, o zonă care este sărită - nu este alocată pe disc.
Pentru această funcție există și o versiune lseek64 la care offset-ul este pe 64 de biți.
Pe lângă trunchierea la 0 care se poate face prin apelul open
cu flag-ul O_TRUNC
, se poate specifica trunchierea unui fișier la o dimensiune specificată, prin apelurile de sistem ftruncate și truncate:
int ftruncate(int fd, off_t length); int truncate(const char *path, off_t length);
În cazul ftruncate, parametrul fd
este file descriptorul obținut cu un apel open care a asigurat drept de scriere. În cazul truncate, fișierul reprezentat prin path
trebuie să aibă drept de scriere.
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> /* open */ #include <sys/stat.h> /* open */ #include <fcntl.h> /* O_CREAT, O_RDONLY */ #include <unistd.h> /* close, lseek, read, write */ #include "utils.h" /* Print the last 100 bytes from a file */ int main (void) { int fd, rc; char *buf; ssize_t bytes_read; /* alocate space for the read buffer */ buf = malloc(101); DIE(buf == NULL, "malloc"); /* open file */ fd = open("file.txt", O_RDONLY); DIE(fd < 0, "open"); /* set file pointer at 100 characters _before_ the end of the file */ rc = lseek(fd, -100, SEEK_END); DIE(rc < 0, "lseek"); /* read the last 100 characthers */ bytes_read = read(fd, buf, 100); DIE(bytes_read < 0, "read"); /* set '\0' at end of buffer for printing purposes*/ buf[bytes_read] = '\0'; printf("the last %ld bytes: \n%s\n", bytes_read, buf); /* close file */ rc = close(fd); DIE(rc < 0, "close"); /* cleanup */ free(buf); return 0; }
În Linux redirectările se realizează cu ajutorul funcțiilor de duplicare a descriptorilor de fișiere dup și dup2 (observați diferența dintre cele 2 în link-urile anterioare):
int dup(int oldfd); int dup2(int oldfd, int newfd);
De exemplu, pentru redirectarea ieșirii în fișierul output.txt
, sunt necesare două linii de cod:
fd = open("output.txt", O_RDWR|O_CREAT|O_TRUNC, 0600); dup2 (fd, STDOUT_FILENO);
Funcția fcntl permite efectuarea unor operații speciale asupra descriptorilor de fișier.
Pentru a crea un handle asociat cu un fișier, director sau altă resursă abstractizată sub forma unui fișier (port COM, pipe, modem etc.) se folosește funcția CreateFile. Funcția se ocupă atât de crearea, cât și de deschiderea unui fișier (și întoarce în ambele cazuri un handle asociat cu fișierul):
HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile ); |
handle1 = CreateFile( "out.txt", GENERIC_READ, /* access mode */ FILE_SHARE_READ, /* sharing option */ NULL, /* security attributes */ OPEN_EXISTING, /* open only if it exists */ FILE_ATTRIBUTE_NORMAL,/* file attributes */ NULL ); |
Atenție! Explicațiile complete se găsesc pe pagina de manual pentru CreateFile. În continuare vom prezenta cele mai importante proprietăți.
Drepturile de acces cerute la deschiderea fișierului sunt specificate în dwDesiredAccess
:
GENERIC_WRITE
GENERIC_READ
Lista completă aici
Parametrul dwCreationDisposition
precizează modul în care apelul acționează în cazul în care fișierul există sau nu; poate avea valori de forma:
CREATE_ALWAYS
- creează un fișier nou; dacă fișierul există, apelul îl suprascrie, ștergând atributele existente;CREATE_NEW
- creează un fișier nou; apelul eșuează dacă fișierul există deja;OPEN_ALWAYS
- deschide fișierul, dacă acesta există; altfel, se comportă ca și CREATE_NEW;OPEN_EXISTING
- deschide fișierul; dacă nu există, apelul eșuează;TRUNCATE_EXISTING
- deschide fișierul (cu drept de acces GENERIC_WRITE
) și îl trunchiază la dimensiunea zero; dacă fișierul nu există, apelul eșuează.
Dacă fișierul există deja și dwCreationDisposition
este CREATE_ALWAYS
sau OPEN_ALWAYS
, apelul NU eșuează, dar GetLastError
returnează ERROR_ALREADY_EXISTS
.
Pentru copierea și mutarea fișierelor există apelurile CopyFile, MoveFile și ReplaceFile.
Un exemplu de schimbare a atributelor găsiți aici.
Când fișierul nu mai este folosit, fișierul este închis cu apelul generic pentru orice tip de handle-uri CloseHandle
BOOL CloseHandle(HANDLE hObject);
Ștergerea se face prin închiderea fișierului și folosirea apelului de sistem DeleteFile
CloseHandle(hFile); DeleteFile("myfile.txt");
unde DeleteFile are signatura
BOOL DeleteFile(LPCTSTR lpFileName);
ReadFile operează asupra unui fișier care are drepturi de acces cel puțin pentru citire, copiind un număr de octeți (începând cu poziția curentă a cursorului de fișier) într-un buffer și întoarce într-o variabilă numărul de octeți citiți.
BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped ); |
bRet = ReadFile( hFile, /* open file handle */ lpBuffer, /* where to put data */ dwBytesToRead,/* number of bytes to read */ &dwBytesRead, /* number of bytes that were read */ NULL /* no overlapped structure */ ); |
ReadFile primește un handle de fișier hFile
, creat anterior cu drepturi cel puțin de citire. Rezultatul citirii este copiat în lpBuffer
, iar numărul de octeți efectiv citiți este întors în variabila pointată de lpNumberOfBytesRead
. Numărul de octeți efectiv citiți poate fi mai mic decât numărul de octeți care se doresc a fi citiți - nNumberOfBytesToRead
.
În mod normal, după acest apel, cursorul de fișier este actualizat cu numărul de octeți citiți. Singura excepție este cazul în care fișierul este deschis pentru operații de I/O de tip OVERLAPPED
- asincrone, caz în care conceptul de cursor de fișier nu mai este folositor (și deci nu mai este actualizat). Mai multe detalii despre operațiile asincrone în Laborator 10 - Operatii IO avansate - Windows.
ReadFile returnează o valoare diferită de zero în caz de succes, și zero altfel. Dacă se returnează o valoare diferită de zero, dar numărul de octeți citiți este zero, atunci s-a ajuns la sfârșitul de fișier.
Apelul WriteFile copiază în mod sincron sau asincron un număr specificat de octeți dintr-un buffer în conținutul unui fișier și returnează într-o variabilă numărul efectiv de octeți copiați. Scrierea în fișier se face în general începând din poziția curentă a cursorului și după terminarea operației, poziția cursorului fișierului este actualizată (rămân valabile observațiile anterioare despre operații OVERLAPPED
).
BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped ); |
bRet = WriteFile( hFile, /* open file handle */ lpBuffer, /* start of data to write */ dwBytesToWrite, /* number of bytes to write */ &dwBytesWritten,/* number of bytes that were written */ NULL /* no overlapped structure */ ); |
Handle-ul de fișier în care se scrie hFile
[in] trebuie să fi fost creat cu drepturi de acces GENERIC_WRITE
. Parametrii WriteFile au aceleași semnificații cu parametrii ReadFile, adaptate pentru operații de scriere.
Fiecare fișier deschis are asociat un cursor (memorat pe 64 de biți) care reprezintă poziția curentă de citire/scriere. Un proces poziționează cursorul la un offset specificat cu SetFilePointer:
DWORD SetFilePointer( HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod ); |
/* Example: How to get current position */ currentPos = SetFilePointer( myFileHandle, 0, /* offset 0 */ NULL, /* no 64bytes offset */ FILE_CURRENT ); |
Deplasarea se face asupra unui fișier reprezentat prin handle-ul hFile
deschis în prealabil, creat cu unul din drepturile de acces GENERIC_READ
sau GENERIC_WRITE
. O valoare pozitivă înseamnă o deplasare înainte, iar una negativă, înapoi.
Numărul de octeți cu care se mută cursorul este specificat de lDistanceToMove
[in] și lpDistanceToMoveHigh
; cele două câmpuri de 32 de biți formează o valoare de 64 de biți. Uzual cel de-al doilea câmp este NULL.
Parametrul dwMoveMethod
specifică punctul de start pentru mutarea cursorului, și poate avea una din valorile:
FILE_BEGIN
- punctul de start este începutul fișierului; lDistanceToMove
este considerat unsignedFILE_CURRENT
- punctul de start este valoarea curentă a cursoruluiFILE_END
- punctul de start este valoarea curentă a sfârșitului de fișier
Apelul returnează noua valoare a cursorului, dacă lpDistanceToMoveHigh
este NULL; altfel, se returnează jumătatea low a valorii, jumătatea high luând locul lpDistanceToMoveHigh.
Varianta extinsă SetFilePointerEx a apelului SetFilePointer memorează valoarea cursorului într-un singur câmp, în loc de două câmpuri separate, apelul extins făcând lucrul cu valorile cursorului mai ușor.
Un fișier poate fi trunchiat sau extins folosind apelul SetEndOfFile, care face poziția sfârșitului de fișier EOF egală cu poziția curentă a cursorului fișierului. În cazul extinderii fișierului peste limita sa, conținutul adăugat este nedefinit.
BOOL SetEndOfFile(HANDLE hFile);
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <windows.h> #include "utils.h" #define BUF_SIZE 100 int main (void) { HANDLE hFile; DWORD dwBytesRead, dwPos, dwBytesToRead = BUF_SIZE, dwRet; BOOL bRet; CHAR outBuffer[BUF_SIZE+1]; /* deschidem fisierul */ hFile = CreateFile( "file.txt", GENERIC_READ, FILE_SHARE_READ, NULL, /* no security attributes */ OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL /* no pattern */ ); DIE(hFile == INVALID_HANDLE_VALUE, "CreateFile"); /* set file pointer at 100 bytes _before_ the end of file */ dwPos = SetFilePointer( hFile, -100, NULL, /* used only for offsets on 64bytes */ FILE_END ); DIE(dwPos == INVALID_SET_FILE_POINTER, "SetFilePointer"); /* read last 100 bytes into buffer */ dwRet = ReadFile( hFile, outBuffer, dwBytesToRead, &dwBytesRead, NULL); /* do nothing asynchronous */ DIE(dwRet == FALSE, "ReadFile"); /* print buffer */ outBuffer[dwBytesRead] = '\0'; printf("last %ld bytes: \n%s\n", dwBytesRead, outBuffer); fflush(stdout); /* close file */ bRet = CloseHandle (hFile); DIE(bRet == FALSE, "CloseHandle"); return 0; }
În rezolvarea laboratorului folosiți arhiva de sarcini lab02-tasks.zip
Observații: Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul utils
din arhivă există un fișier utils.h
cu funcții utile.
man
/MSDN
pentru informații despre apelurile de sistem
1-redirect
redirect.c
. make
)../redirect
.watch -d lsof -p $(pidof redirect)
2-lseek
lseek.c
.lseek
? De ce?fd1
. Este nevoie să se închidă și file descriptorul fd2
? De ce? 3-mcat
.mcat
să aibă funcționalitate similară cu a utilitarului cat
(urmăriți comentariile cu TODO 1)mcat
va primi ca argument în linia de comandă numele unui fișier al cărui conținut îl va afișa la ieșirea standard../mcat Makefile
cp
. (urmăriți comentariile cu TODO 2)./mcat Makefile out ; ./mcat out
/dev/nasty
:./set_nasty.sh
/dev/nasty
: ./mcat /dev/nasty ./mcat /dev/nasty out ; ./mcat out
./mcat Makefile /dev/nasty ; cat /dev/nasty
win/Debug
(în directorul Debug
al soluției, nu al fiecărui proiect în parte).
win
1-cat
și urmăriți sursa cat.c
cat.exe
crc.c
din proiectul 2-crc
și completați funcția GenerateCrc
TODO 1
CompareFiles
.GetSize
pentru calcuarea dimensiunii unui fișierSetFilePointer
TODO - 2
TODO - 3
4-trouble
trouble
tmp1.txt
mesajul din msg
tmp1.txt
5-singular
și completați sursa singular.c
( urmăriți comentariile cu TODO )./singular & sleep 3 ; ./singular
ls -a -R
ls
3-ls
ls.c
pentru ca programul 3-ls.exe
să se comporte ca utilitarul ls
.TODO 1
ls.exe ..
-a
TODO 2
-R
ListFile
TODO 3
4-trouble
trouble
fcntl
(POSIX), SetFileAttributes (Win32 API)