Finora ci siamo concentrati sulle funzionalità del linguaggio C, come se il calcolatore che ospita i nostri programmi fosse poco più di una calcolatrice tascabile. Sappiamo che in realtà le cose non stanno così, e che al calcolatore sono collegate una quantità di periferiche utili.
La buona notizia è che il linguaggio C permette di comunicare con queste
periferiche in modo molto agevole, sia a livello molto basso, vicino
alle funzioni del sistema operativo (che non per niente è scritto
anch'esso in C) che a un livello più alto e più "addomesticato".
Queste le funzionalità di Input/Output fondamentali:
Ad ognuna di queste funzionalità corrisponde una funzione della libreria standard C, come vedremo fra breve. Soffermiamoci però un istante sul concetto di canale di comunicazione.
Il primo concetto da precisare è che, per la libreria standard C, nei canali di comunicazione fluisce sempre una sequenza di byte. Sono dunque assenti i criteri di "formattazione" dell'accesso (ad esempio i record di lunghezza fissata) presenti nel Fortran.
La gestione dei canali di comunicazione ha poi un rapporto molto stretto con il sistema operativo, che porta a distinguere due livelli di astrazione.
Nella libreria standard C per qualsiasi sistema sono presenti due modelli fondamentali di accesso alle periferiche ed alle eventuali strutture di filesystem da esse ospitate.
int), detto file descriptor.
Il grosso problema dell'accesso alle funzioni di questo livello è la possibile
dipendenza dal sistema operativo.
FILE *).
La tabella che segue offre una visione panoramica delle due diverse versioni delle varie funzioni di I/O: quelle di basso livello, specifiche per UNIX, che gestiscono la comunicazione diretta con le periferiche, e quelle standard C a livello di stream.
| Funzione | Funzioni di basso livello per UNIX | Funzioni stream standard |
| Apertura canale di comunicazione | int open(const char *path, int flags);
Apre una canale di comunicazione con il file o il dispositivo identificato da path. In flags possono essere
specificati, con un OR binario, vari parametri di accesso al file. Ad
esempio O_RDONLY (accesso in sola lettura),
O_WRONLY (accesso in sola scrittura),
O_RDWR (accesso in lettura e scrittura),
O_TRUNC (il file viene troncato al punto in cui viene chiuso),
O_CREAT (il file viene creato se non esiste). |
FILE *fopen( const char *path, const char *mode);
Apre una canale di comunicazione con il file o il dispositivo identificato da path. Le modalità di accesso vengono
specificate nella stringa mode: "r" specifica
l'accesso in sola lettura, "w" in sola scrittura,
"w+" in lettura e scrittura, "a"
e si posiziona al termine di un file per prolungarlo. |
| Posizionamento | off_t lseek(int fildes, off_t offset, int whence);
Causa il posizionamento alla posizione offset
nel file o dispositivo indicato da
fildes. whence indica da dove viene calcolata
la posizione. SEEK_SET calcola la posizione dall'inizio del
file, SEEK_CUR calcola la posizione dalla posizione
currente e SEEK_END la calcola a partire dal termine del file.
offset può dunque essere positivo o negativo. |
int fseek( FILE *stream, long offset, int whence);
Causa il posizionamento alla posizione offset
nel file o dispositivo indicato da
stream. Il significato di whence e
offset è lo stesso che per lseek. |
| Lettura dati | ssize_t read(int fd, void *buf, size_t count);
Causa la lettura immediata di count byte dal file
o dispositivo indicato da fd. I dati vengono trasferiti
nell'area di memoria a cui punta buf. La funzione restituisce
il numero di byte effettivamente letti, oppure -1 in caso
di errore. |
char *fgets(char *s, int size, FILE *stream);
Legge una stringa da stream, trasferendola
nell'area a cui punta s. Il trasferimento si ferma quando
vengono letti size - 1 caratteri, o quando viene letto
un carattere di new-line (\n); Viene sempre
aggiunto il carattere "zero" al termine di s, che quindi è
sempre una stringa valida. In caso di errore o di termine dei dati la
funzione restituisce NULL.
size_t fread( void *ptr, size_t size, size_t nmemb, FILE *stream);
Legge da stream un numero nmemb di elementi,
ciascuno di dimensione size byte, e trasferisce il tutto
(dunque in totale size * nmemb byte) nell'area di memoria
a cui punta ptr. La funzione restituisce il numero di
elementi effettivamente letti.
|
| Scrittura dati | ssize_t write(int fd, const void *buf, size_t count);
Causa la scrittura immediata di count byte verso il
file o dispositivo indicato da fd. I dati vengono
prelevati dall'area di memoria a cui punta buf. La funzione
restituisce il numero di byte effettivamente scritti, oppure -1
in caso di errore. |
int fprintf( FILE *stream, const char *format, ...);
Questa funzione è del tutto simile a printf, che abbiamo
già visto, ma invia a stream i dati risultanti dalla
formattazione.
int fputs(const char *s, FILE *stream);
Questo è un sistema più rapido ed efficiente di inviare a stream una stringa che non necessiti di formattazione
(s).
size_t fwrite( void *ptr, size_t size, size_t nmemb, FILE *stream);
Invia a stream un numero nmemb di elementi,
ciascuno di dimensione size byte, prelevati a partire dalla
locazione di memoria a cui punta ptr. La funzione
restituisce il numero di elementi effettivamente scritti |
| Funzioni di controllo. | int ioctl(int fd, int request, ...);
Questa è la funzione di controllo generica per le periferiche sotto UNIX. Le modalità di uso variano da periferica a periferica, ed anche fra i vari diversi sapori di UNIX. |
A questo livello non sono disponibili chiamate di controllo standard. E'
possibile usare ioctl utilizzando il file descriptor
che si ottiene con int fileno(FILE *stream); |
| Chiusura canale di comunicazione | int close(int fd);
Termina la comunicazione con il file o il dispositivo indicato da fd. |
int fclose( FILE *stream);
Termina la comunicazione con stream |
Vediamo ora le chiamate standard in azione in
(esempio di scrittura) e
(esempio di lettura), dove si evidenzia
la facilità di gestione offerta dai tipi strutturati. Entrambi i programmi si
riferiscono al file include comune
ex1.h.
Abbiamo visto come, nell'utilizzare i servizi di I/O offerti dal calcolatore, dobbiamo necessariamente avvicinarci ad un livello di dettaglio maggiore, più vicino al funzionamento specifico del sistema operativo utilizzato.
Nella precedente descrizione delle funzioni di I/O, si dice spesso
che possono servire
ad accedere ad un file (= sequenza di byte) oppure ad un dispositivo
o periferica. UNIX è il sistema operativo nel quale l'identificazione
fra file e device viene radicalizzata. Nell'architettura
del sistema operativo, infatti, il filesystem funge da interfaccia
comune non solo per l'accesso alla memoria di massa (dischi fissi
e rimuovibili, nastri) ma anche per l'accesso alla memoria,
a tutte le periferiche del sistema, come pure al sistema operativo stesso.
Tale struttura è rappresentata in questo diagramma funzionale:
Esercizio: Scrivere o immaginare una funzione che raccolga i dati di un certo numero di cerchi o altri oggetti grafici e sia in grado di salvarli su disco e recuperarli.