File I/O: The Universal I/O model
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 punctul 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:
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 lucrul cu fișiere:
Pentru deschiderea/crearea unui fișier se folosește funcția open.
int open(const char *pathname, int flag); /* deschidere */ int open(const char *pathname, int flags, mode_t mode); /* creare */
Valori posibile pentru flags sunt:
Valorile flags sunt reprezentate prin biti, astefl ca pot fi combinate prin operatorul | (sau pe biti).
// open a file in write only and delete all its contents (truncate to 0) open(pathname, O_WRONLY | O_TRUNC, mode);
Valoarea lui mode este reprezentata de drepturile noului fisier creat (pe biti). In general, se foloseste un numar in baza 8. Acesta are trei cifre, fiecare cu trei biti.
r w x | r - x | r - - |
---|---|---|
1 1 1 | 1 0 1 | 1 0 0 |
7 | 5 | 4 |
Fiecare cifra se refera la:
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 rc; 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ă aibe 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.
Î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
pentru informații despre apelurile de sistem
Intrați în directorul 1-redirect
și urmăriți conținutul fișierului redirect.c
.
Compilați fișierul (folosiți make
). Rulați programul obținut folosind comanda ./redirect
.
Deschideți alt terminal și rulați comanda:
watch -d lsof -p $(pidof redirect)
lsof este un utilitar care afișează informații despre fișierele deschise (ce fișiere sunt deschise în sistem, ce fișiere a deschis un anumit user etc). Căutați în manual (man 8 lsof
) pentru a identifica semnificația coloanei FD și a coloanei TYPE.
Folosiți comanda ENTER pentru a continua programul. În paralel urmăriți cum se modifică tabela de file-descriptori.
În cod, observați parametrii cu care s-a realizat redirectarea cu ajutorul funcțieidup2 (dup2(fd2, STDERR_FILENO)). Observați ce se întamplă dacă parametrii sunt în ordine inversă.
mergeti in directorul 2-read-write
si deschideti fisierul read-write.c
.
Puneti numele vostru in variablila name. Afisati variabila pe ecran folosind doar functia write. Urmariti liniile cu TODO 1.
Cititi de la tastatura variabila name folosind doar functia read.
Functia read citeste buffer-e binare, astfel incat nu adauga caracterul \0 in capat. Adaugati caracterul \0.
Afisati noul nume pe ecran folosind functia write.
Urmariti liniile cu TODO 2.
Scrieti-va numele intr-un fisier numit output.txt folosind functia printf printf.
La crearea unui fisier nou, trebuie sa ii setati modul (al treilea parametru al functiei open). Modul cel mai des folosit este 0644.
Urmariti linii cu TODO 3.
Intrați în directorul 3-lseek
și urmăriți codul sursă din lseek.c
.
Ce valoare va întoarce al doilea apel al funcției lseek
? Decomentați linia de afișare, compilați și rulați pentru verificare.
Sursa închide doar file descriptorul fd1
. Este nevoie să se închidă și file descriptorul fd2
? De ce?
Intrați în directorul 4-mcat
.
Completați fișierul astfel încât programul rezultat mcat
să aibă funcționalitate similară cu a utilitarului cat
(urmăriți comentariile cu TODO 1
)
Programul 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.
Nu aveți voie să citiți tot fișierul în memorie. Puteți citi doar bucăți de dimensiune maximum BUFSIZE.
Verificați codul de eroare întors de apelurile de sistem. Puteți folosi macro-ul DIE. Revedeți secțiunile Crearea, deschiderea și închiderea fișierelor și Scrierea și citirea fișierelor.
Testați cu o comandă de genul:
./mcat Makefile
Extindeți funcționalitatea astfel încât output-ul să fie redirectat într-un fișier primit ca al doilea argument - funcționalitate similară cu a utilitarului cp
. (urmăriți comentariile cu TODO 2
)
Revedeți secțiunea de redirectări.
Testați funcționalitatea:
./mcat Makefile out ; ./mcat out
Inițializați fișierul /dev/nasty
:
./set_nasty.sh
Încercați funcționalitatea de copiere pe fișierul /dev/nasty
:
./mcat /dev/nasty ./mcat /dev/nasty out ; ./mcat out
Dacă apar diferențe, fiți atenți la ce întorc funcțiile read și write (eventual afișați aceste valori) și reparați problema.
Testați scrierea cu:
./mcat Makefile /dev/nasty ; cat /dev/nasty
read
/write
întorc o valoare mai mică decât al treilea parametru.
Intrați în directorul 5-trouble
. Compilați și rulați programul trouble
.
Programul ar trebui să afișeze în fișierul tmp1.txt
mesajul din msg
. Afișați fișierul tmp1.txt
.
Ce observați? Identificați și remediați problema. Revedeți secțiunea: Crearea, deschiderea și închiderea fișierelor.
Vrem să ne asigurăm că doar o instanță a unui program rulează la un moment dat. Pentru asta se creează un fișier temporar pe care se încearcă obținerea unui lock folosind apelul flock.
Intrați în directorul 6-singular
și completați sursa singular.c
(urmăriți comentariile cu TODO ).
man 2 flock
, nonblocking
Testați rulând executabilul din două terminale diferite, sau cu comanda:
./singular & sleep 3 ; ./singular
Găsiți o metodă prin care ne putem asigura că programul nostru are doar o singură instanță, folosind mai puține apeluri de sistem.
fcntl
(POSIX), SetFileAttributes (Win32 API)