Introduzione all'Input e Output in Linux
- Linux tratta ogni risorsa (file, dispositivo, pipe, socket) come un file unificato.
- Le operazioni di I/O si basano su chiamate di sistema universali:
open()
,read()
,write()
eclose()
. - I file descriptor sono numeri interi che identificano file aperti e permettono di gestire operazioni di input e output.
- Esistono file descriptor standard predefiniti:
STDIN_FILENO
(0) per input,STDOUT_FILENO
(1) per output eSTDERR_FILENO
(2) per errori. - La shell setta e reindirizza questi file descriptor prima dell'esecuzione dei comandi, facilitando la gestione degli I/O.
- L'approccio tutto è un file consente ai programmi di operare sugli stessi tipi di dati indipendentemente dalla loro natura fisica o logica.
Descrittori di File in Linux
Tutte le chiamate di sistema che effettuano operazioni di input/output (I/O) in Linux sono implementate attraverso l'uso di file.
Spesso si usa lo slogan che in Linux (e Unix in generale) "tutto è un file". Questo significa che anche dispositivi hardware come tastiere, mouse, stampanti e dischi sono rappresentati come file nel sistema operativo.
Per accedere ad un file bisogna fare riferimento ad esso attraverso un file descriptor (un descrittore di file). Si tratta di un numero intero non negativo che viene adoperato dal sistema operativo per etichettare un file aperto. I file descriptor sono utilizzati per identificare i file aperti e per eseguire operazioni su di essi, come leggere, scrivere o chiudere il file.
Un file descriptor viene adoperato per far riferimento a tutti i tipi di file tra cui:
- File regolari: file di testo, immagini, video, ecc.
- Pipe: canali di comunicazione tra processi.
- Socket: canali di comunicazione tra processi su macchine diverse.
- Dispositivi: come tastiere, mouse, stampanti, dischi, ecc.
Inoltre, ogni processo ha il proprio insieme di file descriptor. Quindi può accadere che uno stesso file abbia file descriptor diversi in processi diversi.
File Descriptor
Un file descriptor è un numero intero non negativo che rappresenta un file su cui un processo può eseguire operazioni di input/output.
Ogni processo ha il proprio insieme di file descriptor. Due processi possono accedere allo stesso file, ma avranno file descriptor diversi per quel file.
File Descriptor Standard
Per convenzione, qualsiasi programma Linux (e Unix) si aspetta di poter adoperare tre file descriptor predefiniti:
File Descriptor | Scopo | Nome POSIX | Stream associato |
---|---|---|---|
0 | Input | STDIN_FILENO |
stdin |
1 | Output | STDOUT_FILENO |
stdout |
2 | Errori | STDERR_FILENO |
stderr |
Questi file descriptor vengono aperti per conto del programma dalla shell (la riga di comando) prima ancora che il programma venga eseguito. Per essere più precisi, in realtà il programma eredita delle copie di questi file descriptor dalla shell che lo ha avviato.
La shell lavora tipicamente sempre con questi tre file descriptor e, quando si tratta di una shell interattiva, essi sono associati al terminale da cui l'utente sta interagendo.
Quando da riga di comando specifichiamo una redirezione (ad esempio >
, <
, >>
), la shell modifica i file descriptor standard per reindirizzare l'input o l'output verso un file specifico prima di eseguire il programma.
Ad esempio, quando eseguiamo un comando come:
ls > file.txt
la shell reindirizza l'output standard (stdout
, file descriptor 1) verso il file file.txt
. In questo modo, invece di stampare l'output sul terminale, il comando ls
scrive direttamente nel file specificato.
Per riferirci a questi file descriptor standard, possiamo adoperare i numeri 0
, 1
e 2
ma questo è sconsigliato. Meglio adoperare le costanti POSIX STDIN_FILENO
, STDOUT_FILENO
e STDERR_FILENO
che sono definite in <unistd.h>
.
La motivazione sta nel fatto che sebbene i file descriptor standard siano sempre associati a stdin
, stdout
e stderr
, in alcuni casi potrebbero essere reindirizzati a file o dispositivi diversi. Utilizzando le costanti POSIX, il codice diventa più leggibile e portabile.
Chiamate di Sistema per I/O
In Linux, le operazioni di input/output (I/O) su file sono gestite attraverso un insieme di chiamate di sistema che permettono ai processi di interagire con i file. Le principali chiamate di sistema per I/O includono:
-
open()
: apre un file e restituisce un file descriptor.Questa chiamata di sistema ha la forma:
fd = open(pathname, flags, mode);
Questa system call apre il file identificato da
pathname
e restituisce un file descriptorfd
che si può adoperare nelle operazioni successive.Nel caso in cui il file non esista,
open
potrebbe crearne uno nuovo a seconda di come sono impostati iflags
.flags
è una maschera di bit che specifica le modalità di apertura del file, come ad esempio se aprirlo in lettura, scrittura o entrambe.Il parametro
mode
è usato solo se il file viene creato e specifica i permessi del file (ad esempio, chi può leggere, scrivere o eseguire il file). Nel caso in cui il file non venga creato, questo parametro viene ignorato. -
read()
: legge dati da un file.La chiamata di sistema
read
ha la forma:numero = read(fd, buffer, count);
Questa system call legge fino a
count
byte dal file identificato dafd
e li memorizza nel bufferbuf
. Restituisce il numero di byte effettivamente letti o -1 in caso di errore.Se non ci sono più dati da leggere,
read
restituisce 0 per indicare la fine del file (EOF
: End Of File). -
write()
: scrive dati su un file.La chiamata di sistema
write
ha la forma:numero = write(fd, buffer, count);
Questa system call scrive fino a
count
byte dal bufferbuf
nel file identificato dafd
. Restituisce il numero di byte effettivamente scritti o -1 in caso di errore.Inoltre, e questo è importante,
write
può restituire un numero di byte inferiore acount
se non c'è abbastanza spazio disponibile nel file o se il file è stato chiuso da un altro processo. -
close()
: chiude un file.La chiamata di sistema
close
ha la forma:status = close(fd);
Questa system call chiude il file identificato da
fd
. Dopo la chiusura, il file descriptor non è più valido e non può essere utilizzato per ulteriori operazioni di I/O.Se la chiusura ha successo,
close
restituisce 0, altrimenti restituisce -1 in caso di errore.
Universalità dell'I/O in Linux
Una delle caratteristiche distintive del modello di I/O in Linux (e Unix in generale) è la sua universalità. In Linux, tutto è un file, il che significa che le stesse chiamate di sistema, come open()
, read()
, write()
e close()
, possiamo operare su qualsiasi tipo di file, che si tratti di un file regolare, di un dispositivo, di una pipe o di un socket.
La conseguenza è che, se scriviamo un programma che adopera soltanto queste chiamate di sistema, esso sarà in grado di leggere e scrivere su qualsiasi tipo di file senza dover conoscere in anticipo il tipo specifico del file.
L'universalità dell'I/O in Linux e Unix è ottenuta assicurandosi che ciascun filesystem o driver di dispositivo implementi le stesse interfacce di I/O. Poiché i dettagli di implementazione sono nascosti dietro le chiamate di sistema e sono gestiti dal kernel, il programma non ha bisogno di preoccuparsi di come i dati sono effettivamente memorizzati o trasmessi.
Soltanto quando non possiamo prescindere da specifiche funzionalità di un dispositivo o di un file, dobbiamo adoperare la chiamata di sistema ioctl()
(input/output control) per inviare comandi specifici al dispositivo o al file. Vedremo ioctl()
nelle prossime lezioni.