Sesta lezione

Input/Output

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.

File descriptors e streams

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.

Funzioni di I/O

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.

FunzioneFunzioni 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.

Il filesystem in UNIX

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:


L'accesso a sistema e periferiche attraverso il filesystem semplifica molto la scrittura di codice che interagisce strettamente col sistema operativo (programmazione di sistema). Inutile specificare quale sia il linguaggio di elezione per questo tipo di programmazione, della quale vedremo più avanti qualche esempio.

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.