Lucrarea 6

Apeluri sistem de baza pentru lucrul cu fisiere

1. Scopul lucrării

Lucrarea de fata prezintă apelurile sistem uzuale folosite in operatiile de intrare-iesire cu fisiere. Operatiile de I/E pe fisiere pot fi realizate folosind cinci functii: open, read, write, lseek si close. Aceste functii sunt referite ca I/E fără tampon, deoarece ele determina un apel sistem in nucleu (kernel).

Partajarea resurselor intre procese tine cont de conceptul de operatei atomica. Se evidentiază acest concept prin folosirea adecvata a argumentelor apelului sistem open.

2. Consideratii teoretice

2.1. Descriptori de fisier

Pentru kernel toate fisierele deschise sunt referite de un descriptor de fisier = un întreg pozitiv. La deschiderea unui fisier sau la crearea unui fisier nou, kernel-ul Returnează un descriptor de fisier procesului care a executat apelul.

Fiecare proces Unix are la dispozitie 20 (in versiuni mai noi numărul a fost extins la 64) de descriptori fisier. Prin conventie, primii trei sunt deschisi automat la începutul unui proces. Descriptorul de fisier 0 identifica intrarea standard, 1 identifica iesirea standard, iar 2 iesirea standard de erori. Cei 17 descriptori rămasi pot fi folositi de proces pentru deschiderea de fisiere ordinare, pipe, speciale sau directoare.

Exista cinci apeluri sistem care generează descriptori de fisiere: creat, open, fcntl, dup si pipe. In lucrare vor fi descrise primele doua apeluri sistem. Cele ramase in lucrările următoare.

2.2. Apeluri sistem

Fiecare apel sistem este prezentat folosind definirea sau prototipul functiei. Aceasta abordare permite precizarea semnificatiei si tipului fiecărui argument si tipul valorii returnate.

2.2.1. Apelul sistem OPEN

Deschiderea sau crearea unui fisier se poate face prin apelul sistem open. Sintaxa acestui apel este:

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int open( const char *path, int flags,... /* mode_t mod */);

Returnează descriptorul de fisier sau -1 in caz de eroare.

Numărul de argumente al acestei functii poate fi doi sau trei. Argumentul al treilea este folosit doar la crearea de fisiere noi. Apelul cu doua argumente este folosit pentru citirea si scrierea fisierelor existente. Functia Returnează cel mai mic descriptor de fisier disponibil. Acesta poate fi utilizat in apelurile sistem: read, write, lseek si close. IDU efectiv sau GIDU efectiv al procesului care executa apelul trebuie sa aibă drepturi de citire/scriere, functie de valoarea argumentului flags. Pointerul din fisier este pozitionat pe primul octet din fisier.

Argumentul flags se formează printr-un SAU pe biti intre constantele:

O_RDONLY Fisierul este deschis in citire.

O_WRONLY Fisierul este deschis in scriere.

O_RDWR Fisierul este deschis in adăugare (citire+scriere).

Acestea sunt definite in fisierul antet fcntl.h.

Următoarele constante simbolice sunt optionale:

O_APPEND Scrierile succesive se produc la sfârsitul fisierului.

O_CREAT Daca fisierul nu exista el este creat.

O_EXCL Daca fisierul exista si O_CREAT este pozitionat, apelul open esuează.

O_NDELAY La fisiere pipe si cele speciale pe bloc sau caracter cauzează trecerea in modul fără blocare atât pentru apelul open cat si pentru operatiile viitoare de I/E. In versiunile noi System V înlocuit cu O_NONBLOCK.

O_TRUNC Daca fisierul exista ii sterge continutul.

O_SYNC Fortează scrierea efectiva pe disc prin write. Întârzie

mult întregul sistem, dar e eficace in cazuri critice.

Argumentul al treilea, mod, poate fi o combinatie de SAU pe biti intre constantele simbolice:

S_IRUSR, S_IWUSR, S_IXUSR User: read, write, execute

S_IRGRP, S_IWGRP, S_IXGRP Group: read, write, execute

S_IROTH, S_IWOTH, S_IXOTH Other: read, write, execute

Acestea definesc drepturile de acces asupra unui fisier si sunt definite in fisierul antet sys/stat.h.

2.2.2. Apelul sistem CREAT

Un fisier nou este creat prin:

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int creat( const char *path, mode_t mod);

Returnează descriptorul de fisier sau -1 in caz de eroare.

Apelul este echivalent cu:

open( path, O_WRONLY | O_CREAT | O_TRUNC, mod);

Argumentul path specifica numele fisierului, iar mod drepturile de acces.

Daca fisierul creat nu exista, este alocat un nou i-node si o legătura spre el este plasata in directorul unde acesta a fost creat. Proprietarul (IDU efectiv si GIDU efectiv) procesului care executa acest apel trebuie sa aibă permisiunea de scriere in director. Fisierul deschis va avea drepturile de acces specificate de argumentul al doilea din apel (vezi umask). Apelul întoarce cel mai mic descriptor de fisier disponibil (vezi pipe). Fisierul este deschis in scriere iar dimensiunea sa initială este 0. Timpii de acces si modificare din i-node sunt actualizati.

Daca fisierul exista (este nevoie de permisiunea de căutare in director) continutul lui este pierdut si el este deschis in scriere. Nu se modifica proprietarul sau drepturile de acces ale fisierului. Argumentul al doilea este ignorat.

2.2.3. Apelul sistem READ

Pentru a citi un număr de octeti dintr-un fisier, de la pozitia curenta, se foloseste apelul read. Sintaxa este:

#include <unistd.h>

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

Returnează numărul de octeti cititi efectiv, 0 la EOF, -1 in caz de eroare.

Citeste noct octeti din fisierul deschis referit de fd si ii depune in tamponul referit de buf. Pointerul in fisier este incrementat automat după o operatie de citire cu numărul de octeti cititi. Procesul care executa o operatie de citire asteaptă până când kernel-ul depune datele de pe disc in bufferele cache. In mod uzual, kernel-ul încearcă sa rapidizeze operatiile de citire citind in avans blocuri de disc consecutive pentru a anticipa eventualele cereri viitoare.

2.2.4. Apelul sistem WRITE

Pentru a scrie un număr de octeti intr-un fisier, de la pozitia curenta, se foloseste apelul write. Sintaxa este:

#include <unistd.h>

ssize_t write( int fd, const void *buf, size_t noct);

Returnează numărul de octeti scrisi si -1 in caz de eroare.

Apelul scrie noct octeti din tamponul buf in fisierul a cărui descriptor este fd.

Interesant de semnalat la acest apel este faptul ca scrierea fizica pe disc este întârziata. Ea se efectuează la initiativa nucleului fără ca utilizatorul sa fie informat. Daca procesul care a efectuat apelul, sau un alt proces, citeste datele care încă nu au fost scrise pe disc, kernel-ul le citeste înapoi din bufferele cache. Scrierea întârziata este mai rapida, dar are trei dezavantaje: a) o eroare disc sau căderea kernelului produce pierderea datelor; b) un proces care a initiat o operatie de scriere nu poate informat in cazul aparitiei unei erori de scriere; c) ordinea scrierilor fizice nu poate fi controlata.

Pentru a elimina aceste dezavantaje, in anumite cazuri, se foloseste fanionul O_SYNC. Dar cum aceasta scade viteza sistemului si având in vedere fiabilitatea sistemelor UNIX de astăzi se prefera mecanismul de lucru cu tampoane cache.

2.2.5. Apelul sistem CLOSE

Pentru a disponibiliza descriptorul atasat unui fisier in vederea unei noi utilizări se foloseste apelul close.

#include <unistd.h>

int close( int fd);

Returnează 0 in caz de succes si -1 in caz de eroare.

Apelul nu goleste bufferele kernel-ului si deoarece fisierul este oricum închis la terminarea procesului apelul se considera a fi redundant.

2.2.6. Apelul sistem LSEEK

Pentru pozitionarea absoluta sau relativa a pointerul de fisier pentru un apel read sau write se foloseste apelul lseek.

#include <sys/types.h>

#include <unistd.h>

off_t lseek( int fd, off_t offset, int interp);

Returnează un deplasament in fisier sau -1 in caz de eroare.

Nu se efectuează nici o operatie de I/O si nu se trimite nici o comanda controlerului de disc. Daca interp este SEEK_SET pozitionarea este fata de începutul fisierului (primul octet din fisier este la deplasament zero). Daca interp este SEEK_CUR pozitionarea este relativa la pozitia curenta. Daca interp este SEEK_END pozitionarea este fata de sfârsitul fisierului.

Apelurile open, creat, write si read executa implicit lseek. Daca un fisier este deschis folosind constanta simbolica O_APPEND se efectuează un apel lseek la sfârsitul fisierului înaintea unei operatii de scriere.

2.2.7. Apelul sistem LINK

Pentru a adăuga o noua legătura la un director se foloseste apelul:

#include <unistd.h>

int link(const char *oldpath, const char newpath);

Returnează 0 in caz de reusita si -1 in caz contrar.

Argumentul oldpath trebuie sa fie o legătura existenta, ea furnizează numărul i-node-ului.

Daca legaturile . si .. din fiecare director sunt ignorate structura sistemului de fisiere este arborescenta. Programe care parcurg structura ierarhica (de exemplu, comanda find) pot fi usor implementate fără probleme de traversare multipla sau bucla infinita. Pentru a respecta aceasta cerinta doar superuser-ul are dreptul sa stabilească o noua legătura la un director. Crearea celei de a doua legaturi la un director este utila pentru a-l putea muta in alta parte a arborelui sau pentru a-l putea redenumi.

2.2.8. Apelul sistem UNLINK

Pentru a sterge o legătura (cale) dintr-un director se foloseste apelul:

#include <unistd.h>

int unlink( const char *path);

Returnează 0 in caz de reusita si -1 in caz contrar.

Apelul decrementează contorul de legaturi din i-node si sterge intrarea director. Daca acesta devine 0 spatiul ocupat de fisierul in cauza devine disponibil pentru o alta utilizare, la fel si i-node-ul. Doar superuser-ul poate sa steargă un director.

2.3. Implementarea semafoarelor prin fisiere

Apelul sistem creat esuează daca fisierul exista si daca nu exista permisiunea de scriere. Acest lucru permite utilizarea unui fisier pe post de semafor. Se poate astfel crea un mecanism de acces la o resursa partajabila. Doua procese cu acces exclusiv la resursa vor executa următoarea secventa:

a) Înainte de a accesa resursa, procesele încearcă sa creeze un fisier (cu nume cunoscut) dar fără permisiunea de scriere.

b) Ambele procese vor executa creat, dar doar unul din ele va reusi. Procesul care nu reuseste rămâne în starea de asteptare.

c) Procesul care a reusit apelul creat executa codul propriu de lucru cu resursa după care eliberează resursa prin stergerea fisierului semafor, printr-un apel unlink.

in mod paradoxal, facilitatea aceasta poate fi exploatata numai de utilizatorii obisnuiti (fără drept de superuser), deoarece un apel creat executat de superuser trunchează fisierul indiferent de drepturile sale de acces.

Protocolul de semafor poate fi descris prin doua functii, lock si unlock, care respecta secventa:

if ( lock("semaf")) {

... unul singur ...
unlock("semaf");

} else

... n-a reusit lock ...

Numele semaf este arbitrar, dar este de dorit ca el sa fie unic. Functia lock selectează dintre procesele care executa concurent acesta secventa de cod, un singur proces care va executa secventa protejata unul singur.

Codul functiei lock si unlock:

#define LOCKDIR "/tmp/" 
#define MAXINCERC 3 
#define WAITTIME 5 
BOOLEAN lock( char *name) 
{ 
char *path, *lockpath(); 
int fd, incerc; 
extern int errno; 
path = lockpath( name); /* generează nume semafor */ 
incerc = 0; 
  while (( fd=creat( path, 0)) < 0 && errno == EACCES) { 
    if ( ++incerc>=MAXINCERC) 
      return FALSE; 
    sleep( WAITTIME); 
  } 
  if ( fd < 0 || close(fd) < 0) 
  err_sys("lock"); 
  return(TRUE); 
}
 
unlock( char *name) 
{ 
char *lockpath(); 
  if ( unlink( lockpath( name)) < 0) 
    err_sys("unlock"); 
} 
static char *lockpath( char *name) 
{ 
static char path[20]; 
char *strcat(); 
  strcpy( path, LOCKDIR); 
  return( strcat( path, name)); 
} 

Functia lockpath generează un nume de fisier care se utilizează ca semafor. Acest fisier este creat in directorul /tmp, deoarece acest director exista in orice sistem UNIX si oricine are permisiuni de scriere acolo.

Daca apelul creat nu reuseste se testează variabila errno, deoarece singurul caz acceptat este absenta dreptului de scriere ('permision denied'). Constanta simbolica EACCES este definita in fisierul errno.h.

Numărul de încercări de a crea fisierul semafor nu depăseste MAXINCERC si timpul scurs intre încercări succesive e WAITTIME.

Functia unlock trebuie doar sa steargă fisierul.

Constanta simbolica O_EXCL oferă o cale generala (atât pentru superuser) de folosire a unui fisier ca semafor. Ca atare, in functia lock in locul liniei:

while (( fd=creat( path, 0)) < 0 && errno == EACCES) {

va apare linia:

while ((fd=open( path, O_WRONLY | O_CREAT | O_EXCL, 0666)) < 0 && errno == EEXIST) {

Se constata ca doua procese ce executa simultan apelul open pe acelasi fisier cu O_EXCL si O_CREAT nu reusesc amândouă. De regula, un fisier utilizat ca semafor nu contine date utilizator.

2.4. Citirea unui director

Un director poate fi citit de oricine are drept de citire asupra directorului. Scrierea unui director poate fi făcuta doar de către kernel.

Structura unui director depinde de versiunea de UNIX. Pentru Unix versiunea 7, un director este format din intrări de 16 octeti, din care 14 octeti pentru numele fisierului si 2 octeti pentru i-node. Ridicarea restrictiei de 14 caractere pentru numele fisierului, odată cu versiunea 4.2BSD fiecare intrare a devenit de lungime variabila, adică fiecare program ce va citi un director este dependent de sistem. Pentru a simplifica acest lucru, au fost introduse următoarele functii:

#include <sys/types.h>

#include <dirent.h>

DIR *opendir( const char *pathname);

Returnează pointer daca este OK, NULL in caz de eroare.

struct dirent *readdir( DIR *dp);

Returnează pointer daca este OK, NULL in caz de eroare.

rewinddir( DIR *dp);

int closedir( DIR *dp);

Returnează -1 in caz de eroare.

Structura dirent definita in fisierul antet dirent.h este dependentă de implementare. SVR4 si 4.3+BSD definesc structura astfel încât ea contine cel putin doi membri:

struct dirent {

ino_t d_ino;

char d_name[NAME_MAX +1];

}

Structura DIR este o structura interna folosita de aceste patru functii pentru a păstra informatii despre directorul citit. Primul apel readdir citeste prima intrare dintr-un director. Ordinea intrărilor dintr-un director este dependenta de implementare. De regula nu este alfabetica.

3. Aplicatii

3.1. Sa se scrie un program care creeaza un fisier cu o zona libera de 20 de octeti. Nu contează continutul fisierului.

#include <sys/types.h> 
#include <sys/stat.h> 
#include "fcntl.h" 
#include "hdr.h" 
char buf1[]="Laborator "; 
char buf2[]="SO Unix an IV+V"; 
int 
main( void) 
{ 
int fd; 
  if ( ( fd=creat("file.gol", FILE_MODE)) < 0) 
    err_sys("Eroare creat"); 
    err_sys("buf1: eroare write"); 
    err_sys("lseek error"); 
    if ( write( fd, buf2, sizeof(buf2)) != sizeof(buf2)) 
      err_sys("buf2: eroare write"); 
      exit(0); 
} 

Efectul executiei programului se poate vedea prin comanda:

$od c file.gol

Fisierul antet hdr.h, va fi inclus de multe din programele din lucrările următoare. Continutul sau este următorul:

#ifndef __hdr_h 
#define __hdr_h 
#include <sys/types.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h> 
#define MAXLINE 1024 
#define FILE_MODE ( S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) 
#define DIR_MODE ( FILE_MODE | S_IXUSR | S_IXGRP | S_IXOTH) 
#define ever (;;) 
typedef void Sigfunc( int); /* tipul rutinei de tratare */ 
void print_exit( int); /* lucrarea 10 */ 
void print_mask( const char*); /* lucrarea 10 */ 
Sigfunc *signal_intr( int, Sigfunc *); /* lucararea 10,11 */ 
void err_sys( const char *, ...); 
void err_quit( const char *, ...); 
void err_dump( const char *, ...); 
void err_ret( const char *, ...); 
void err_msg( const char *, ...); 
#endif /* __hdr_h */ 

3.2. Sa se scrie un program care copiază continutul unui fisier existent intr-un alt fisier. Numele celor doua fisiere se citesc ca argumente din linia de comanda. Se presupune ca oricare dintre apelurile sistem read sau write poate genera erori.

#define BUFSIZE 512 
void copy( depe, pe) 
char *depe, *pe; 
{ 
int depefd, pefd, nr, nw, n; 
char buf[BUFSIZE]; 
  if (( depefd=open( depe, O_RDONLY)) < 0) 
    err_sys( depe); 
  if (( pefd=creat( pe, 0666)) < 0) 
      err_sys( pe); 
  while(( nr=read( depefd, buf, sizeof( buf))) != 0) { 
    if ( nr < 0) err_sys("read"); 
    nw=0; 
    do { 
      if (( n=write( pefd, buf[nw], nrnw))==1) 
      err_sys("write"); 
      nw += n; 
    } while ( nw < nr); 
  } 
  close( depefd); close(pefd); 
} 

3.3. Apelurile sistem întorc de regula o valoare. In general, daca un apel sistem întoarce valoarea (-1), apelul respectiv nu a reusit. In acest caz variabila errno contine un cod de eroare. Aflarea erorii propriu-zise se poate face prin apelul functiei strerror cu argumentul valoarea variabilei errno. Netratarea erorilor in cazul operatiilor de I/E poate conduce la erori greu de depistat. Cunoscând aceste informatii sa se scrie o familie de functii utile in tratarea erorilor Acestea sa fie cuprinse in modulul err.c. Modulul poate fi compilat la err.o si inclus in linia de comanda la compilarea oricărui program ce foloseste aceste functii.

/* A se compila cu: gcc c err.o err.c */

#include <errno.h> 
#include <stdarg.h> 
#include "hdr.h" 
static void err_do( int, const char *, va_list); 
char *pname=NULL; 
/* Eroare fatala datorata unui apel    Eroare fatala independenta de apelul   
sistem. Afisează mesaj si termina      sistem. Afisează mesaj si termina      
procesul.                              procesul. */                           

void                                   void                                   
err_sys(const char *frmt,...)          err_quit(const char *frmt,...)         
{                                      {                                      
 va_list ap;                            va_list ap;                           
 va_start( ap, frmt);                   va_start( ap, frmt);                  
 err_do( 1, frmt, ap);                  err_do( 0, frmt, ap);                 
 va_end(ap);                            va_end(ap);                           
 exit(1);                               exit(1);                              
}                                      }                                      

/* Eroare nefatală datorata unui apel  Eroare nefatală independenta de un     
sistem. Afisează mesaj si revine.      apel sistem. Afisează mesaj si         
                                       revine. */                             

void                                   void                                   
err_ret(const char *frmt, ...)         err_msg(const char *frmt, ...)         
{                                      {                                      
 va_list ap;                            va_list ap;                           
 va_start( ap, frmt);                   va_start( ap, frmt);                  
 err_do( 1, frmt, ap);                  err_do( 0, frmt, ap);                 
 va_end(ap);                            va_end(ap);                           
 return;                                return;                               
}                                      }                                      


/* 
Eroare fatala relativa la un apel sistem. 
Afisează mesaj, dump core si termina procesul. 
*/ 
void 
err_dump( const char *frmt, ...) 
{ 
 va_list ap;
 va_start( ap, frmt);
 err_do( 1, frmt, ap); 
 va_end(ap); 
 abort(); /* dump core */ 
 exit(1); 
} 
static void 
err_do( int errfan, const char *frmt, va_list ap) 
{ 
 int errno_save; 
 char buf[MAXLINE]; 
 errno_save=errno; 
 vsprintf( buf, frmt, ap); 
 if ( errfan) 
  sprintf( buf+strlen( buf), ": %s", strerror( errno_save)); 
 strcat( buf, "\n"); 
 fflush( stdout); 
 fputs( buf, stderr); 
 fflush( NULL); 
 return; 
} 

4. Probleme propuse spre rezolvare

4.1. Ce se poate spune despre deplasamentul in cazul apelului lseek ? Cum se poate afla dimensiunea unui fisier ?

4.2. Folosind apelurile de sistem prezentate scrieti un program C care sa afiseze in ordine inversa liniile unui fisier.

4.3. Sa se scrie un program care citeste si tipăreste caracterele 0, 20, 40 ,... dintr-un fisier creat anterior.

4.4. Un fisier deschis in citire si scriere cu O_APPEND, poate fi citit si scris de oriunde folosind lseek ? Sa se scrie un program care lansat in background de N ori scrie intr-un fisier ID procesului curent. Nici unul dintre programe nu poate sa-si continue executia pana ce toate procesele nu si-au scris ID propriu in fisier. In final fiecare proces afisează ID procesului următor.

4.5. Sa se scrie un program care sa permită scrierea unor siruri de caractere din intrarea standard intr-un fisier, începând cu o anumita pozitie. Apelul programului se face sub forma: rw poz fis

4.6. Se considera programul:

#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include "hdr.h" 
void main( void) 
{ 
if ( open("temp", O_RDWR) < 0) 
err_sys("open"); 
if ( unlink("temp") < 0) 
err_sys("unlink"); 
printf("Unlink realizat !\n"); 
sleep(15); /* 15 secunde întârziere */ 
printf("GATA.\n"); 
exit(0); 
} 

Considerând ca fisierul temp exista si programul este lansat in background sa se explice rezultatul comenzilor:

$ ls -l temp; df /home; a.out &

$ ls -l temp; df /home; # se asteaptă 10s

$ df /home

Ce concluzie se poate trage legat de apelul sistem unlink ?

4.7. Sa se rescrie functia lock astfel încât ea sa expandeze numele fisierului semafor cu numele de login (se va folosi functia getlogin). Sa se adauge un argument functiei, care in caz ca functia Returnează FALSE, sa permită afisarea numelui utilizatorului care încearcă lock.

4.8. Cunoscând structura unui director sa se scrie un program care sa afiseze continutul tuturor intrărilor director (numele fisierului si i-node-ul asociat) folosind apelul sistem open. Calea completa spre director se specifica in linia de comanda.

4.9. Sa se rescrie programul anterior folosind functii ce lucrează cu directoare.

4.10. Sa se scrie un program care elimina tot al cincilea octet dintr-un fisier. Observatie: nu se va folosi un fisier temporar.

4.11. Sa se scrie un pachet B-tree. Sa se implementeze functii pentru crearea fisierelor B-tree, pentru deschiderea si închiderea unui fisier; functii pentru memorarea, selectarea si stergerea articolelor. Avantajul major al acestui pachet este faptul ca articolele sunt in ordine. Sa se scrie si o functie de parcurgere.