Laboratoare:Operatii IO simple

= Fișiere. Sisteme de fișiere =

Fișierul este una din abstracțiile fundamentale în lumea sistemelor de operare; cealaltă abstracție 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, calea

/usr/local/app/file.txt

înseamnă că directorul rădăcină / are un subdirector usr urmat de subdirectorul local. Acesta are la rândul său are un subdirector app care 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. Caracterele interzise în nume sunt / în Unix și ?, ", /</tt>, \</tt>, &lt;</tt>, &gt;</tt> , *</tt>, |</tt>, :</tt> în Windows. De altfel, este recomandat ca denumirile de fișiere să nu se termine cu punct sau spațiu în Windows.

O diferență majoră între Linux/Unix și Windows este tratarea literelor mari sau mici. Linux/Unix sunt case-sensitive (Data</tt> este diferit de data</tt>), iar Windows nu.

Ierarhia sistemului de fișiere Unix are un singur director cunoscut sub numele de root</tt> și notat /</tt>, prin care se localizează orice fișier. Notația Unix pentru căile fișierelor este un șir de nume de directoare despărțite prin /</tt>, urmat de numele fișierului. Există și căi relative la directorul curent .</tt> sau la directorul părinte ..</tt>.

Î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 din 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 \</tt>, și pentru căile absolute trebuie specificat numele ierarhiei în forma C:\</tt>, <tt>E:\</tt> sau <tt>\\FILESERVER\myFile</tt> (pentru rețea). Ca și Unix, Windows folosește <tt>.</tt> și <tt>..</tt>.

= Operații pe fișiere =

Un descriptor de fișier în Unix este un întreg care indexează o tabelă cu pointeri spre structuri care descriu fișierele deschise de un proces. Un program care rulează într-un shell Unix își deschide 3 fișiere standard cu file descriptori cu valori speciale:


 * standard input (cu file descriptorul 0) (implicit, citit de la tastatură - de la terminal)
 * standard output (cu file descriptorul 1) (implicit, afișat pe ecran - la terminal)
 * standard error (cu file descriptorul 2) (implicit, afișat pe ecran - la terminal)

Pentru a asocia alți descriptori cu obiecte deschise, se folosește apelul de sistem <tt>open</tt>.

Pe 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 program, și orice fișier suplimentar se deschide cu <tt>OpenFile</tt> sau <tt>CreateFile</tt>.

În continuare, pentru descrierea comportării 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 mai îndepărtat 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 tipice de executat pe un fișier sunt prezentate în continuare. Exceptând deschiderea unui fișier, toate operațiile primesc ca argument descriptorul sau handle-ul acelui fișier. Pentru deschiderea fișierului se folosește ca argument numele acestuia.


 * deschiderea unui fișier: înseamnă asocierea unui descriptor de fișier sau un handle cu un fișier; acest descriptor sau handle este folosit în cadrul celorlalte operații; fișierul este identificat prin nume; apeluri pentru deschiderea unui fișier sunt <tt>fopen</tt> (ISO C), <tt>open</tt> (POSIX), <tt>CreateFile</tt> (Win32 API); aceste apeluri pot fi folosite și pentru crearea unui fișier; alternativ există apelul <tt>creat</tt> (POSIX).
 * închiderea unui fișier: înseamnă eliberarea structurilor de fișier asociate procesului și a descriptorului (handle-ului) acelui fișier; apelurile sunt <tt>fclose</tt> (ISO C), <tt>close</tt> (POSIX), <tt>CloseFile</tt> (Win32 API)
 * citirea dintr-un fișier: înseamnă copierea unui bloc de date într-un buffer; după ce se realizează citirea se actualizează cursorul de fișier; apelurile sunt <tt>fread</tt> (ISO C), <tt>read</tt> (POSIX), <tt>ReadFile</tt> (Win32 API)
 * scrierea într-un fișier: înseamnă copierea unui bloc de date dintr-un buffer în fișier; efectuarea scrierii înseamnă și actualizarea cursorului de fișier; apelurile sunt <tt>fwrite</tt> (ISO C), <tt>write</tt> (POSIX), <tt>WriteFile</tt> (Win32 API)
 * poziționarea într-un fișier: înseamnă schimbarea valorii cursorului de fișier; citiri sau scrieri ulterioare vor porni din locul indicat de acest cursor de fișier; apelurile sunt <tt>fseek</tt> (ISO C), <tt>lseek</tt> (POSIX), <tt>SetFilePointer</tt> (Win32 API)
 * schimbarea atributelor unui fișier: înseamnă stabilirea unor parametri pentru fișier; apelurile sunt <tt>fcntl</tt> (POSIX), <tt>SetFileAttributes</tt> (Win32 API)

= Operații pe fișiere în Linux =

În continuare sunt descrise operațiile de fișiere într-un sistem Linux. Apelurile descrise mai jos sunt valabile în orice sistem compatibil POSIX.

open
Funcția open este folosită pentru deschiderea unui fișier; funcția este declarată în <tt>fcntl.h</tt> iar sintaxa apelului este una din următoareele:

Dacă, spre exemplu, dorim să deschidem fișierul <tt>alina.txt</tt> pentru scriere, cu trunchiere, și fișierul <tt>dan.txt</tt> pentru citire și scriere, cu eventuala creare a acestuia, vom folosi următoarea secvență de cod:

'''Exemplu 1. io-01.c'''

ATENȚIE! O greșeală frecventă este omiterea drepturilor de creare a fișierului (<tt>0644</tt> în exemplul de mai sus) când se apelează <tt>open</tt> cu flag-ul <tt>O_CREAT</tt> activat.

creat
Declarată tot în <tt>fcntl.h</tt> este și funcția <tt>creat</tt>, care creează un fișier și are următoarea sintaxă:

Funcția este echivalentă cu:

close
Funcția este declarată în <tt>unistd.h</tt> iar sintaxa apelului este următoarea:

unde <tt>FILEDES</tt> este filedescriptorul care se dorește închis.

Dacă dealocarea are loc fără probleme atunci valoarea întoarsă este <tt>0</tt>, altfel <tt>-1</tt>. Ca și la <tt>open</tt>, mai multe informații despre cum s-a terminat operația se pot obține inspectând variabila <tt>errno</tt>.

O greșeală frecventă de programare este neverificarea codului de eroare întors la <tt>close</tt>, pentru că se poate întâmpla ca o eroare la scriere (<tt>EIO</tt>) să fie întoarsă utilizatorului abia la <tt>close</tt>.

Scrierea și citirea
Scrierea într-un fișier și citirea dintr-un fișier se realizează cu ajutorul apelurilor <tt>read</tt> și <tt>write</tt>. Aceste apeluri primesc trei argumente:


 * 1) descriptorul fișierului folosit
 * 2) buffer-ul ce conține datele pentru scriere sau unde se stochează datele citite din fișier
 * 3) numărul de octeți care vor fi citiți/scriși

read
Funcția read este declarată în <tt>unistd.h</tt> și are sintaxa de apel:

Funcția întoarce numărul de octeți citiți. Valoarea minimă este de un octet, iar când se ajunge la sfârșit se va întoarce <tt>0</tt>. Dacă se face read după ce s-a ajuns la sfârșit se va întoarce în continuare <tt>0</tt>. Dacă apare o eroare se întoarce <tt>-1</tt> și variabila <tt>errno</tt> este setată corespunzător cauzei care a determinat eroarea.

write
Funcția write este declarată, de asemenea, în <tt>unistd.h</tt> iar sintaxa este următoarea:

unde parametrii au semnificații similare cu cei ai apelului <tt>read</tt>. Valoarea întoarsă este numărul de octeti ce au fost efectiv scriși. În mod implicit nu se garanteaza că la revenirea din write scrierea în fișier s-a terminat. Pentru a forța actualizarea se poate folosi <tt>fsync</tt> sau fișierul se poate deschide folosind flagul <tt>O_FSYNC</tt>, caz în care se garanteaza că după fiecare <tt>write</tt> fișierul a fost actualizat.

Observație: pentru <tt>read</tt> și <tt>write</tt> există o versiune <tt>pread</tt>, respectiv <tt>pwrite</tt>, care permite specificarea unui offset în fișier de la care să se efectueaze operația de citire / scriere. Mai există și o versiune <tt>pread64</tt> și <tt>pwrite64</tt> la care offset-urile sunt pe 64 de biți.

Poziționarea în fișier (lseek)
Funcția lseek permite mutarea cursorului unui fișier la o poziție absolută sau relativă. Similar cu <tt>read</tt> și <tt>write</tt>, este declarată în <tt>unistd.h</tt>. Sintaxa este următoarea:

Valoarea întoarsă reprezintă offset-ul la care s-a ajuns; un <tt>lseek</tt> pe <tt>SEEK_CUR</tt> cu <tt>OFFSET </tt> zero întoarce poziția curentă fără a o modifica.

Pentru această funcție există și o versiune <tt>lseek64</tt> la care <tt>OFFSET</tt>-ul este pe 64 de biți. Tipul <tt>off_t</tt> este un tip aritmetic folosit pentru a reprezenta dimensiuni de fisiere. În sistemele GNU el este un <tt>long int</tt>.

<tt>lseek</tt> 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 fisier cu... goluri, o zonă care este "sărită" nu este alocată pe disc.

Exemplu utilizare operații I/O
'''Exemplu 2. io-02.c'''

Operații speciale (fcntl)
Funcția fcntl permite efectuarea unor operații speciale asupra descriptorilor de fișier. Sintaxa este următoarea:

<tt>COMMAND</tt> este tipul operației; o parte din valorile posibile și semnificațiile acestora sunt prezentate în urmatorul tabel:

= Operații pe fișiere în Windows =

NTFS
Sistemele de fișiere native folosite pe Windows sunt FAT32 și NTFS.

NTFS consideră un fișier ca fiind o colecție de atribute și valori pentru acestea. Exemple de atribute: meta-date despre fișier (nume, lungime, timestamp), conținutul fișierului, informații de securitate. Fișierele sunt stocate folosind clustere, care sunt grupări de sectoare fizice de pe disc; cluster-ele sunt folosite pentru o mai bună performanță. Numărul de sectoare care formează un cluster depinde de dimensiunea partiției, dar poate fi adaptat și la alte valori (cu cât partiția e mai mare, cu atât un cluster conține mai multe sectoare).

Mai multe detalii despre NTFS puteți găsi aici.

Fișierele și directoarele sunt și în Windows obiecte securizate; cererea de drepturi asupra unui fișier se face prin intermediul a două flag-uri, date ca parametri către funcțiile de lucru cu fișiere:
 * modul de acces specifică setul de operații pe care un proces îl poate face pe un obiect.
 * modul de partajare specifică felul în care mai multe procese pot partaja un același obiect

Restricții asupra numelor de fișiere
Windows nu permite folosirea de nume <tt>rezervate</tt> pentru a denumi fișiere. Numele rezervate sunt ale unor device-uri speciale de sistem (cum ar fi LPT2, COM1). Numele unui fișier este case-insensitive și lungimea maximă a unui nume este de <tt>MAXPATH</tt> (260) de caractere (incluzand prefix-ul de drive si caracterul <tt>'\0'</tt> de sfârșit de string).

Handle-uri către fișiere
Un <tt>handle</tt> este un identificator pentru un obiect gestionat de kernel-ul Windows. În particular, pentru a ne referi la un fișier, vom folosi un <tt>handle</tt> către acesta, care va avea un rol asemanător cu descriptorul de fișier din Unix. Vom discuta mai multe detalii despre <tt>handle</tt>-uri la laboratorul de procese.

Crearea / deschiderea
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):

Drepturile de acces cerute la deschiderea fișierului sunt specificate in dwDesiredAccess [in]. Există variabile generice care încapsulează drepturile de acces, cum ar fi <tt>GENERIC_EXECUTE</tt>, <tt>GENERIC_READ</tt> și <tt>GENERIC_WRITE</tt>.

Un fișier poate fi deschis de mai multe ori (de procese diferite, sau de același proces). În acest caz, la prima deschidere, parametrul <tt>dwShareMode [in]</tt> va avea una din valorile:
 * <tt>FILE_SHARE_DELETE</tt> permite unor operații de deschidere ulterioare să capete acces de tip delete.
 * <tt>FILE_SHARE_READ</tt> permite unor operații de deschidere ulterioare să capete acces de tip read.
 * <tt>FILE_SHARE_WRITE</tt> permite unor operații de deschidere ulterioare să capete acces de tip write.

La deschiderea unui fișier se poate preciza prin parametrul <tt>lpSecurityAttributes [in]</tt> modul în care handle-ul returnat de apel poate fi moștenit de procesele fii ale procesului apelant. Daca <tt>lpSecurityAttributes</tt> este <tt>NULL</tt>, handle-ul nu poate fi moștenit.

Parametrul <tt>dwCreationDisposition [in]</tt> precizează modul în care apelul acționează în cazul în care fișierul există sau nu; poate avea valori de forma:
 * <tt>CREATE_ALWAYS</tt> creează un fișier nou; dacă fișierul există, apelul îl suprascrie, ștergând atributele existente;
 * <tt>CREATE_NEW</tt> creează un fișier nou; apelul eșuează dacă fișierul există deja;
 * <tt>OPEN_ALWAYS</tt> deschide fișierul, daca acesta există; altfel, se comportă ca și <tt>CREATE_NEW</tt>;
 * <tt>OPEN_EXISTING</tt> deschide fișierul; dacă nu există, apelul eșuează;
 * <tt>TRUNCATE_EXISTING</tt> deschide fișierul (cu drept de acces <tt>GENERIC_WRITE</tt>) și îl trunchiază la dimensiunea zero; dacă fișierul nu există, apelul eșuează.

Un set de flaguri și atribute suplimentare (valabile numai în cazul fișierelor) pot fi precizate în <tt>dwFlagsAndAttributes [in]</tt>. Valori uzuale sunt:


 * <tt>FILE_ATTRIBUTE_NORMAL</tt> fișierul nu are alte atribute setate (folosit numai singur)
 * <tt>FILE_ATTRIBUTE_READONLY</tt> fișierul va fi read only pentru toate procesele

<tt>hTemplateFile [in]</tt> este un handle la un fișier template cu drepturi de acces pentru citire, template ale cărui atribute și flaguri vor fi folosite și pentru fișierul nou creat.

Apelul returnează în caz de succes un handle al fișierului. Dacă <tt>dwCreationDisposition</tt> este <tt>CREATE_ALWAYS</tt> sau <tt>OPEN_ALWAYS</tt>, apelul nu eșuează, dar <tt>GetLastError</tt> returnează <tt>ERROR_ALREADY_EXISTS</tt>. În caz de eroare, apelul returnează <tt>INVALID_HANDLE_VALUE</tt>, iar informații suplimentare despre eroare pot fi returnate de <tt>GetLastError</tt>.

Următoarea secvență de cod deschide fișierul <tt>alina.txt</tt> pentru scriere și îl trunchiază. Fișierul <tt>dan.txt</tt> este deschis pentru citire și scriere și este creat dacă nu există:

'''Exemplu 3. io-03.c'''

Un apel <tt>FormatMessage</tt> cu <tt>GetLastError</tt> printează The system cannot find the file specified dacă fișierul nu există.

Pentru copierea și mutarea fișierelor există apelurile <tt>CopyFile</tt>, <tt>MoveFile</tt> și <tt>ReplaceFile</tt>.

Închiderea
Când fișierul nu mai este folosit, fișierul este închis cu apelul generic pentru orice tip de handle-uri CloseHandle

Ștergerea
Ștergerea se face prin închiderea fișierului și folosirea apelului de sistem DeleteFile

unde <tt>DeleteFile</tt> are signatura

Citirea și scrierea unui fișier
Un proces poate face operații de citire și de scriere asupra unui fișier prin apelurile de sistem <tt>ReadFile</tt>, <tt>WriteFile</tt> și cele extinse <tt>ReadFileEx</tt>, <tt>WriteFileEx</tt>. Apelurile primesc un handle de fișier creat cu drepturi corespunzătoare și scriu sau citesc un număr specificat de octeți începând cu poziția curentă a cursorului (sau dintr-o altă poziție specificată).

Citirea
<tt>ReadFile</tt> 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. În mod normal, după o astfel de operație, 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, caz în care conceptul de cursor de fișier nu mai este folositor (și deci nu mai este actualizat).

Apelul <tt>ReadFile</tt> poate opera atât sincron, cât și asincron; <tt>ReadFileEx</tt> este exclusiv asincronă. În cadrul acestui laborator vom insista pe folosirea sincronă, urmând ca cea asincronă (mai complicată, dar care oferă performanțe mai bune în cazul accesului concurent) să fie prezentată într-un laborator ulterior:

<tt>ReadFile</tt> primește un handle de fișier <tt>hFile [in]</tt>, care trebuie să fi fost creat cu un drept de acces cel puțin egal cu <tt>GENERIC_READ</tt>. Pentru ca operația să fie asincronă, hFile trebuie să fi fost deschis cu FILE_FLAG_OVERLAPPED precizat în <tt>CreateFile</tt>. Bufferul în care se copiază rezultatul citirii este <tt>lpBuffer [out]</tt>.

Numărul de octeți care se dorește a fi citit se precizează în <tt>nNumberOfBytesToRead [in]</tt>, iar numărul efectiv citit este returnat în variabila pointată de <tt>lpNumberOfBytesRead [out]</tt>.

<tt>ReadFile</tt> 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.

O operație de citire simplă dintr-un fișier poate arăta astfel:

Scrierea
Apelul <tt>WriteFile</tt> 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 );

Handle-ul de fișier în care se scrie <tt>hFile [in]</tt> trebuie să fi fost creat cu drepturi de acces <tt>GENERIC_WRITE</tt>. Parametrii <tt>WriteFile</tt> au aceleași semnificații cu parametrii <tt>ReadFile</tt>, adaptate pentru operații de scriere.

Poziționarea in fișier
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 <tt>SetFilePointer</tt> (pentru apelarea căruia trebuie inclus <tt>windows.h</tt>):

Fișierul destinație al operației are handle-ul <tt>hFile [in]</tt> și trebuie să fi fost în prealabil creat cu unul din drepturile de acces <tt>GENERIC_READ</tt> sau <tt>GENERIC_WRITE</tt>.

Numărul de octeți cu care se mută cursorul este specificat de <tt>lDistanceToMove [in]</tt> și <tt>lpDistanceToMoveHigh [in, out]</tt>; 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. O valoare pozitivă înseamnă o deplasare înainte, iar una negativă, înapoi.

Parametrul <tt>dwMoveMethod</tt> specifică punctul de start pentru mutarea cursorului, și poate avea una din valorile:


 * <tt>FILE_BEGIN</tt> punctul de start este începutul fișierului; <tt>liDistanceToMove</tt> este considerat unsigned
 * <tt>FILE_CURRENT</tt> punctul de start este valoarea curentă a cursorului
 * <tt>FILE_END</tt> punctul de start este valoarea curentă a sfârșitului de fișier

Apelul returnează noua valoare a cursorului, dacă <tt>lpDistanceToMoveHigh</tt> este NULL; altfel, se returnează jumătatea low a valorii, jumătatea high luând locul <tt>lpDistanceToMoveHigh</tt>. În caz de eroare, valoarea returnată este egală cu <tt>INVALID_SET_FILE_POINTER</tt>. <tt>SetFilePointer</tt> poate fi folosit și pentru a interoga poziția curentă a cursorului, precizând punctul de start <tt>FILE_CURRENT</tt> și offsetul zero:

Varianta extinsă <tt>SetFilePointerEx</tt> a apelului <tt>SetFilePointer</tt> 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 <tt>SetEndOfFile</tt>, care face poziția sfârșitului de fișier <tt>EOF</tt> egală cu poziția curentă a cursorului fișierului. În cazul extinderii fișierului peste limita sa, conținutul adăugat este nedefinit.

Fișierul este precizat de parametrul <tt>hFile</tt>; acesta trebuie să fi fost în prealabil creat cu dreptul <tt>GENERIC_WRITE</tt>.

Apelul întoarce o valoare diferită de zero în caz de succes, și zero altfel.

Exemplu utilizare operații I/O
'''Exemplu 4. io-04.c'''

= Redirectări =

Redirectările sunt operațiile prin care ieșirea, intrarea sau erorile sunt redirectate în fișiere sau către intrarea altui proces. Câteva exemple de redirectări sunt prezentate mai jos:

Redirectarea ieșirii către un fișier se realizează cu ajutorul operatorului <tt>&gt;</tt>. Redirectarea intrării dintr-un fișier se realizează cu ajutorul operatorului <tt>&lt;</tt>. Redirectarea erorii într-un fișier se realizează cu ajutorul operatorului <tt>2&gt;</tt>. În fine, redirectarea ieșirii unei comenzi către o altă comandă se realizează cu ajutorul operatorului <tt>|</tt>.

Redirectări în Linux
În Linux redirectările se realizează simplu, cu ajutorul funcțiilor de duplicare a file descriptorilor. De exemplu, pentru redirectarea ieșirii în fișierul output.txt, sunt necesare două linii de cod:

Dacă se dorește să se redirecteze intrarea, ieșirea sau eroarea unui proces nou creat, aceași secvență trebuie efectuată în procesul copil, după <tt>fork</tt>, dar înainte de <tt>exec</tt>. Trebuie de asemenea să ne asigurăm că descriptorii de fișier nu au fost deschiși cu flagul <tt>CLOSE_ON_EXEC</tt>.

Redirectări în Windows
În Windows, redirectarea și crearea unui proces cu unul din handlerele standard redirectat se face după metode diferite. Astfel, pentru redirectarea unuia din handlerele standard se vor folosi funcțiile:

unde <tt>std_handle</tt> poate fi <tt>STD_INPUT_HANDLE</tt>, <tt>STD_OUTPUT_HANDLE</tt> sau <tt>STD_ERROR_HANDLE</tt>.

Pentru redirectări într-un proces nou creat, fișierul în/din care se redirectează trebuie creat astfel încât să fie moștenibil, pentru ca handle-ul să poată fi moștenit în procesele copii ale procesului curent.

Apoi, apelul <tt>CreateProcess</tt> de creare a procesului care va avea handle-ul redirectat trebuie să aibă parametrul <tt>bInheritHandles</tt> cu valoarea <tt>TRUE</tt> și câmpurile prezentate mai jos trebuie să aibă valorile corespunzătoare:

Câmpul cb trebuie să fie lungimea în octeți a structurii. Pentru a lua în considerare noile handlere, dwFlags trebuie setat la STARTF_USESTDHANDLES. Handlerele pentru redirectare se specifică în <tt>hStdInput</tt>, <tt>hStdOutput</tt> și <tt>hStdError</tt>.

Un exemplu de redirectare a ieșirii unui proces nou creat este prezentat mai jos:

'''Exemplu 3. io-05.c'''

= Wrapper-e = În domeniul sistemelor de operare, prin wrapper înțelegem un layer software subțire (a se citi: care nu aduce un overhead prea mare) peste sistemul de operare, cu scopul de a abstractiza serviciile oferite de acesta, adaptându-le la o interfață comună. Interfața comună este astfel definită incât să se potrivească cu mai multe sisteme de operare. Programele pe care le scriem ulterior nu vor folosi direct apelurile de sistem specifice fiecărui sistem de operare, ci interfața comună.

Un wrapper este folositor atunci când dorim să scriem software portabil pe mai multe platforme (spre exemplu, temele de la Sisteme de Operare) cu un "overhead" minim de portare și fară a plati un preț prea scump măsurat în performanță (după cum știți, există și alte soluții pentru această problemă, de exemplu, mașina virtuală Java - JVM).

Una din metodele posibile pentru realizarea unui wrapper este folosirea preprocesorului. Să presupunem că incercăm să abstractizăm conceptul de fișier și operațiile disponibile cu el. Vom exemplifica doar operațiile de read/write.

'''Exemplu 6. io-wrapper.h'''

Observăm că în funcție de sistemul de operare definit, diferă:
 * fișierele header incluse
 * definițiile tipurilor cu care lucrează wrapper-ul

De asemenea, observăm că semnăturile funcțiilor definite sunt identice pentru ambele sisteme de operare. Iată un exemplu de implementare a lor:

'''Exemplu 6. io-wrapper.c'''

Acum putem genera fișiere executabile compatibile cu o platformă Linux sau Windows, în funcție de un singur macro, definit automat de catre compilator.

Observăm că folosind această tehnică putem să convertim inclusiv între procedură și funcție (funcțiile de pe Windows primesc ca parametru transmis prin referință numărul de octeți citiți/scriși, iar cele de pe Linux îl întorc direct). Desigur, abordarea de mai sus este incompletă, pentru că ar fi trebuit convertite și codurile de eroare într-un format comun.

Odată scris acest wrapper, putem folosi în continuare funcțiile os_read și os_write pentru a citi / scrie din fișiere, fară a ne preocupa de sistemul de operare pe care rulează programul nostru. Acesta este insă un caz fericit, pentru că așa după cum veți observa la laboratorul de procese, nu toate serviciile oferite de sisteme de operare diferite se pot "unifica" atât de ușor (este vorba de fork + exec vs. CreateProcess).

= Exerciții =

Quiz
Pentru autoevaluare raspundeti la intrebarile din acest quiz.

Exercitii pre-laborator
<ol> <li>(Linux) Rulati strace pentru urmatoarele comezi, pentru a observa apelurile de sistem utilizate (identificati-le pe cele relevante). <ul> <li>touch test_file</li> <li> cp /etc/network/interfaces test_file</li> <li> ls -l test_file </li> <li> rm test_file </li> </ul> </li> <li> (Platform Independent) Realizați un program denumit mycat care simulează comanda cat de afișare a conținutului unui fișier. Comanda primește ca argument fișierul de afișat. Pentru testare puteti folosi chiar fisierul sursa C. </li> <li>(Platform Independent) Realizați un program denumit <tt>mysize</tt> care afișează dimensiunea fișierului primit ca argument prin citirea conținutului acestuia (folosind <tt>read</tt>, respectiv <tt>ReadFile</tt>). De ce nu se recomandă aflarea dimensiunii unui fișier în acest fel? Care funcții se recomandă să se folosească pe Linux, respectiv Windows?

</li> <li> (Platform independent) Sa se scrie un program care citeste si afiseaza un octet de la o pozitie aleatoare dintr-un fisier folosind o singura parcurgere a fisierului si fara sa citeasca tot fisierul in memorie. </li> </ol>

Exercitii de laborator
In rezolvarea exercitiilor puteti folosi arhiva de sarcini a laboratorului.

<ol>

<li>(Platform Independent) Intrați în directorul 01_xfile al arhivei. Studiați conținutul său și completați în fișierul xfile.c funcțiile xread și xwrite astfel încât programul <tt>xtest</tt>, obținut prin compilare, să producă mesajul "OK". Hints: <ul> <li>programul testează implementarea voastră pentru cele două funcții; o descriere a funcționalității lor găsiți în <tt>xfile.h</tt></li> <li>revedeți documentația pentru apelurile de sistem read și write (pentru Linux), respectiv ReadFile și WriteFile pentru Windows</li> <li>nu vă puteți baza pe faptul că read/write/ReadFile/WriteFile funcționează corect (fără eroare), sau că scriu / citesc câți octeți le-ați trimis, și nu mai puțini. Verificați rezultatul întors de ele!</li> </ul> (1 punct) </li>

<li>(Platform Independent) Intrați în directorul 02_fcomp al arhivei. Studiați conținutul său. Examinați fișierul xfile.h și observați că a mai apărut o funcție în el. Copiați fișierul xfile.c din directorul 01_xfile în acest director și implementați și funcția xlen. Completați apoi fișierul fcomp.c pentru a obține un program care compară două fișiere, afișând un mesaj care spune dacă ele sunt identice ca și conținut sau diferă. Hints: <ul> <li>revedeți funcțiile lseek/SetFilePointer, pentru poziționarea în fișier, respectiv open/close/CreateFile/CloseHandle, pentru închiderea și deschiderea fișierelor</li> <li>nu aveți voie să citiți fișierele în întregime în memorie. Puteți citi doar bucăți limitate din ele, folosind un buffer.</li> <li>puteți folosi funcțiile xread și xwrite din xfile.c</li> </ul> (1 punct) </li>

<li>(Platform Independent) Intrați în directorul 03_fcp al arhivei. Studiați conținutul său. Copiați fișierul xfile.c obținut la exercițiul 2 (din directorul 02_fcomp) și completați fișierul fcp.c astfel încât să obțineți un program care copiază o porțiune dintr-un fișier de intrare, descrisă prin (offset, lungime) într-un fișier de ieșire. Ordinea parametrilor o puteți afla examinând fișierul fcp.c. Hints: <ul> <li>puteți folosi funcțiile definite în xfile.h</li> <li>nu aveți voie să citiți toată porțiunea din fișierul de intrare în memorie; folosiți un buffer</li> <li>atenție la drepturile cu care creați fișierul de ieșire, dacă acesta nu există; recomandăm 644; revedeți documentația funcției creat pentru mai multe detalii</li> </ul> (1 punct) </li>

<li>(Platform Independent) Intrați în directorul 04_fscatter al arhivei. Studiați conținutul său. Completați fișierul fscatter.c astfel încât să obțineți un program care "sparge în bucăți" de o dimensiune maximă dată un fișier de intrare. Dacă fișierul de intrare se numește file.txt, bucățile se vor numi file.txt1, file.txt2,.., file.txtN, unde N este numărul de bucăți. Testați apoi funcționarea corectă a programului vostru folosind operatorul din shell ">>" și utilitarul "diff". Hints: <ul> <li>puteți folosi funcția fcp, scrisă la exercițiul anterior (nu uitați să copiați xfile.h și xfile.c)</li> <li>avand in vedere ca itoa nu este independenta de platforma si golurile de memorie ar trebui evitate, puteti folosi pentru concatenarea de string-uri si int-uri functia sprintf : char buffer[BUF_SIZE]; sprintf(buffer, "%s%d", nume_fisier, id);</li> </ul> (2 puncte) </li>

<li>(Platform independent) Intrați în directorul 05_ffind al arhivei. Studiați conținutul său. Completați fișierul ffind.c astfel încât să obțineți un program care are un efect similar cu comanda "ls". Pentru fiecare fișier, afișați: numele, dimensiunea și dacă este director sau nu ("Y"/"N"). Nu afișați fișierele speciale "." și "..". Hints: <ul> <li>pe Linux puteți folosi funcțiile opendir, readdir și closedir pentru a enumera fișierele din director. Ele se regăsesc în fișierul header dirent.h</li> <li>pe Linux puteți folosi funcția stat pentru a obține informațiile despre fișiere</li> <li>pe Linux, puteți porni de la următorul exemplu <li>pe Windows puteți folosi funcțiile FindFirstFile și FindNextFile pentru a enumera fișierele dintr-un director</li> <li>pe Windows nu e nevoie să apelați o funcție suplimentară pentru a obține informațiile despre fișiere; ele se află în structura întoarsă de funcțiile de enumerare a fișierelor</li> <li>pe Windows, pattern-ul de căutare a fișierelor trebuie să conțină calea absolută până la directorul curent. Pentru a o obține, folosiți funcția GetCurrentDirectory</li> <li>pe Windows, puteți porni de la următorul exemplu </ul> (1 punct) </li>

'''Punctajul maxim se ia cu 10 puncte. Bonusul poate recupera lipsa de activitate de la alte laboratoare'''.

Pe Windows, puteți compila în linie de comandă cu:

cl /EHsc file.c[pp]

= Soluţii =

= Resurse utile =


 * Low-Level I/O (<tt>info libc "Low-Level I/O"</tt>)
 * Duplicating Descriptors (<tt>info libc "Duplicating Descriptors"</tt>)
 * File Management Functions