Il linguaggio C è un linguaggio da sempre strettamente legato ai sistemi Unix. Da un lato perché il linguaggio C è nato su sistemi Unix. Dall’altro perché i sistemi Unix (ed I moderni sistemi operativi) sono scritti in larghissima parte in C.
Alcune caratteristiche:
A differenza dei linguaggi di scripting visto finora, il linguaggio C è un linguaggio compilato.
Storicamente il nome del compilatore C su sistemi Unix-like e cc (C Compiler).
Nei sistemi Linux il compilatore di default è il gcc (GNU C Compiler). Questo compilatore è oramai disponibile sulla quasi totalità dei sistemi Unix.
Come nel caso di script shell ed awk, i sorgenti devono essere creati utilizzando un editor di testo (vi/emacs…)
Non utilizzare word processor!
Questo corso NON si prefigge l’insegnamento del linguaggio C. Utilizzeremo questo linguaggio di programmazione per interagire con alcune componenti del sistema operativo.
Un codice sorgente C consiste, tipicamente, di:
Direttive al pre-processore. Una direttiva di questo tipo è identificata dal carattere “#” ad inizio riga.
Tra queste, fondamentali sono le direttive di include:
Una sequenza di funzioni. È sempre necessario definire la firma delle funzioni prima della loro invocazione.
Tra le funzioni, vi è una funzione denominata main da cui inizierà l’esecuzione. Le “firme” standard della funzione main sono:
In applicazioni complesse è opportuno distribuire le funzionalità in più file.
Semplifica lo sviluppo, il debugging ed il riuso del codice.
In ogni applicazione esiste sempre un’unica funzione main
La verifica della correttezza delle singole funzioni avviene durante la fase di compilazione.
La verifica della consistenza tra le diverse funzioni e l’accorpamento delle diverse funzionalità avviene durante la linking:
Per gcc, il suffisso del nome del file identifica la/le operazione/i che il compilatore esegue:
Per compilazione si intende, impropriamente, la traduzione di un codice sorgente in un eseguibile. In realtà la compilazione è solo una delle fasi di traduzione dal codice sorgente all’eseguibile. Per il linguaggio C, il processo di compilazione, si suddivide nelle seguenti fasi:
Per eseguire l’intero processo di compilazione del programma test1.c è sufficiente eseguire:
gcc test1.c
Il nome di default del file eseguibile generato da gcc è a.out
È possibile indicare un nome di file eseguibile diverso utilizzando l’opzione -o
Se l’applicazione è distribuita in più file sorgente, è sufficiente eseguire il seguente comando:
È possibile richiedere a gcc, con l’opzione -c, la sola fase di “compilazione” del codice, in cui il compilatore verifica la sola correttezza sintattica del sorgente, senza verificare la presenza di una implementazione di tutte le funzioni utilizzate e, successivamente, richiedere il linking dei diversi file object. È possibile quindi suddividere l’unica invocazione di gcc indicata sopra, come segue:
In questo modo, la modifica di “filei.c” richiede la ricompilazione del solo filei.c e l’esecuzione dell’operazione di linking.
Un debugger è una applicazione che consente l’esecuzione controllata del codic eseguibile.
L’applicazione gdb è il debugger di default su sistemi Linux.
Per poter utilizzare gdb in modo “user-friendly” è necessario:
Alcuni comandi base:
Gdb fornisce, inoltre, la possibilità di eseguire debugging di applicazioni multi-processo e multi-thread.
Alcuni esempi di definizione di variabili: Mostra codice
Commenti e definizione di costanti: Mostra codice
Tutte le variabili devono essere dichiarate prima del loro uso.
Ogni variabile è visibile solo all'interno del blocco in cui è dichiarata.
Il C fornisce i seguenti tipi di dato base: char (carattere), int, short, long (interi), float, double (numeri con virgola).
È possibile costruire tipi di dato complessi tramite il costrutto struct. Una struttura è un agglomerato di elementi I cui tipi possono essere tipi di dato base o strutture.
È possibile definire array di elementi sia di tipo base che array di strutture.
Attenzione: Il primo elemento dell'array in C è l'elemento zero. Quindi gli elementi di un array X di 10 elementi sono X[0],...,X[9].
A differenza di awk, tutti gli elementi dell'array sono sempre dello stesso tipo. Inoltre la dimensione dell'array deve essere nota all'atto della dichiarazione (allocazione statica), o comunque, prima del suo utilizzo (allocazione dinamica).
NON esiste in C il tipo "stringa". Una stringa è un array di caratteri terminato dal carattere nullo ('\0').
Una delle caratteristiche del C è la possibilità di dichiarare puntatori a variabili.
Un puntatore non è altro che una variabile che contiene l’indirizzo di memoria di a cui è memorizzata un’altra variabile.
La dichiarazione di un puntatore avviene utilizzando la sintassi: tipo *nome; dove il tipo può essere semplice o composto.
Per ottenere l’indirizzo di una variabile è possibile utilizzare la scrittura: &nomevar.
L’assegnazione *nomevar=val, assegna alla variabile puntata da nomevar il valore val.
Attenzione: la dichiarazione di un puntatore NON alloca lo spazio per contenere il valore della variabile. Un puntatore, quindi, deve essere utilizzato:
Per referenziare una variabile allocata staticamente
Per referenziare una variabile allocata dinamicamente, solo successivamente alla sua allocazione (attraverso le system call malloc e calloc).
Un esempio di utilizzo dei puntatori: Mostra codice.
Un secondo esempio di utilizzo dei puntatori su variabili dinamiche: Mostra codice.
Per definizione, il nome di un array, in C, corrisponde all’indirizzo della prima locazione di memoria utilizzata dall’array.
Il C definisce anche una aritmetica dei puntatori. Se, ad esempio, p è un puntatore ad una variabile di che occupa x byte, allora p++ corrisponde al valore di p incrementato di x.
L’aritmetica dei puntatori consente un accesso semplice anche agli elementi di un array. Così, se p è il nome di un array, p+k è il puntatore al k-mo elemento dell’array. Di nuovo, si noti che NON è necessario sapere quanto spazio occupa un singolo elemento dell’array per calcolare l’indirizzo del k-mo elemento dell’array.
Il codice: Mostra codice, riporta un esempio delle possibilità offerte dall'aritmetica dei puntatori
Il programma dichiara due array. Il primo è un array di strutture dichiarato staticamente. Il secondo è un array di interi, dichiarato dinamicamente.
È possibile osservare che indipendentemente dal tipo di dato o dal tipo di dichiarazione, è possibile accedere agli elementi dell'array sia utilizzando la tecnica "classica", indicando l'indice dell'elemento tra parentesi quadre, che utilizzando il nome dell'array come puntatore al primo elemento dell'array ed utilizzando l'aritmetica dei puntatore per spostarsi al suo interno.
Il terminatore o delimitatore di istruzioni è il “;”. È possibile utilizzare il terminatore “,” ma con un significato specifico.
A differenza di awk, il delimitatore in C è obbligatorio al termine di ogni istruzione.
Le parentesi { } delimitano un blocco di istruzioni. Non è necessario utilizzare il ; dopo la fine di un blocco, I.e., dopo una “}”.
È necessario delimitare i blocchi contenenti almeno due istruzioni
È opzionale delimitare i blocchi contenenti un’unica istruzione (es. “if”)
Nel seguente esempio: Mostra codice, la sintassi del costrutto if-then-else.
La condizione può essere una qualsiasi espressione. In C, come awk, un valore pari a zero o NULL viene interpretato come falso; un valore non-nullo viene interpretato come vero.
La lista-then viene eseguita se la condizione è vera. Altrimenti, sei blocco else è presente, viene eseguita la lista-else.
Il costrutto while consente di iterare l’esecuzione di una lista di istruzioni in funzione di una condizione. Le istruzioni vengono eseguite se e solo se la condizione di controllo è vera.
A differenza del costrutto while, il do-while verifica la condizione dopo aver eseguito il blocco di istruzioni. In questo caso, quindi, la lista di istruzioni viene eseguita sempre almeno una volta.
Attenzione: NON utilizzare il delimitatore “;” tra prima della “{“. In questo caso, il compilatore considererebbe il costrutto while terminato prima dell’inizio del blocco di istruzioni, generando, con alta probabilità, un ciclo infinito.
Il costrutto for è un secondo costrutto di iterazione, la cui sintassi è indicata nel seguente esempio: Mostra codice. Utilizza tre espressioni. La prima di inizializzazione, la seconda per il controllo dell'interazione e la terza per l'aggiornamento dell'espressione di controllo.
A differenza di awk, le espressioni expr1 ed expr3 possono essere arbitrarie ed utilizzare un numero arbitrario di variabili.
Un esempio di utilizzo di for e while: Mostra codice
Esempi di operatori e loro composizione: Mostra codice
Il linguaggio C fornisce i seguenti tipi di operatori:
Quando una sequenza di istruzioni deve essere utilizzata in più punti del codice è possibile raggrupparla in un blocco di codice e definire una funzione.
È opportuno dichiarare sempre le funzioni utilizzate all’interno di ogni file. Semplifica la correzione degli errori legati ai tipi. Possibilmente tramite l’header.
La firma di una funzione include:
Il passaggio dei parametri può avvenire:
Un esempio di definizione di funzione: Mostra codice
In C è possibile definire anche puntatori a funzioni, che possono essere assegnati, inseriti in array etc., proprio come se fossero puntatori a variabili. I puntatori a funzioni rendono possibile scrivere una modulo, indipendentemente dal comportamento che alcune sue componenti avranno o dal tipo di dato che processeranno. Un semplice esempio: Mostra codice; Mostra codice.
La funzione ps prende in input due puntatori a void e stampa il primo ed il secondo parametro dopo averli convertiti in interi.
La funzione sp prende in input due puntatori a void e stampa il secondo ed il primo parametro dopo averli convertiti il primo in intero ed I secondo in float.
La funzione stampa prende in input tre parametri.
Il primo, ordine, è un puntatore ad "una funzione di tipo void che riceve come parametri due puntatori di tipo void".
Gli altri due parametri, x ed y, sono due puntatori di tipo void.
All'interno di stampa, l'invocazione della funzione, avviene semplicemente, utilizzando il nome del parametro formale ordine.
All'interno del main, l'invocazione della funzione stampa avviene passando come primo parametro il nome della funzione da utilizzare.
Si noti che la firma delle funzioni "parametro" (ps e sp nell'esempio) devono coincidere con la firma del parametro (il primo parametro della funzione stampa). D'altro canto, è sempre possibile combinare l'uso di puntatori a variabili void, per soddisfare il matching dei tipi, ed il casting esplicito, per convertire I tipi di dato all'interno delle funzioni.
Finora abbiamo considerato le funzioni di input o di output come “equivalenti” tra loro. In realtà, dal punto di vista del sistema operativo, esistono classi diverse di funzioni di I/O.
Alcune funzioni di I/O standard utilizzano, per questioni di performance, il buffering dei dati, I.e., utilizzano un’area di memoria per memorizzare temporaneamente le informazioni prima di inviarle effettivamente al device. In questo modo si riduce il numero di system call “effettive”.
Esistono tre tipi di buffering:
Le funzioni di I/O tipo scanf, printf, fprintf sono funzioni di I/O, di norma, bufferizzate.
In questo corso focalizzeremo l’attenzione sull’I/O non bufferizzato.
La maggior parte delle operazioni sui file ordinari in ambiente UNIX possono essere eseguite utilizzando solo le cinque chiamate di sistema open, read, write, lseek, close.
Prima di eseguire una operazione su un file e’ necessario “aprire” il file. Ad ogni file aperto, il kernel associa un identificativo detto file descriptor.
La shell nei sistemi Unix assegna, per convenzione I seguenti file descriptor:
In luogo degli interi, e per garantire la portabilità del codice, è possibile utilizzare le costanti simboliche STDIN_FILENO (0), STDOU_FILENO (1) e STDERR_FILENO (2) definite nell’header di sistema unistd.h
#include
#include
#include
int open(const char *pathname, int oflag);
int open(const char *pathname, int oflag, mode_t mode);
La system call open consente sia di aprire un file già esistente che di creare un file nel caso in cui questo non esista;
Può essere invocata con due o tre parametri. Restituisce il file descriptor o -1 in caso di errore;
Il file descriptor ritornato è il minimo file descriptor non utilizzato dal processo.
Attenzione: il kernel riutilizza I file descriptor!
Il parametro pathname contiene il nome del file da aprire, identificato attraverso un pathname (assoluto o relativo);
Il parametro oflag permette di specificare le opzioni di apertura (che vedremo nella prossima slide), mediante costanti definite in , combinate attraverso il simbolo “|” (OR);
L’ultimo parametro, mode, è opzionale e viene utilizzato solo nel caso di creazione del file. Consente di definire, all’atto della creazione, la politica d’accesso al file.
Di seguito riportiamo possibili valori di oflag. Si tenga conto che su diversi sistemi Unix possono esistere flag di apertura specifici.
Il parametro oflag deve necessariamente contenere esattamente uno dei seguenti:
I successivi flag sono opzionali:
È possibile comporre diversi flag utilizzando l’operatore OR. Ad esempio: O_RDWR | O_APPEND | O_CREAT | O_TRUNC
Quando il file non esiste, e se viene specificato il parametro O_CREAT, il parametro mode consente di definire la politica di accesso (le protezioni) al file che si sta creando.
Come abbiamo visto nella lezione 1, le protezioni per il file sono definite con tre triple di bit r (read), r (write) , x (execute), rispettivamente per il proprietario, il gruppo e gli “altri”.
Il parametro mode consente di specificare ogni singolo bit della tripla attraverso l’OR delle seguenti costanti. Per comodità, sono presenti tre costanti, una per ogni tripla, che consente di abilitare, contemporaneamente, I tre bit della tripla.
Esempio: Per consentire l’accesso in lettura, scrittura ed esecuzione al proprietario ed al gruppo ed il solo permesso in lettura agli altri, è possibile utilizzare il seguente valore per il parametro mode:
S_IRWXU | S_IRWXG | S_IROTH
#include
#include
#include
int creat (const char *pathname, mode_t mode);
In taluni casi il programma deve creare nuovi file, I.e., aprire file che sicuramente non esistono. Esempio classico è l’apertura di un file temporaneo che, nella maggior parte dei casi, è proprio del processo e viene cancellato al termine di una operazione.
La system call creat (non “create”) crea un nuovo file aprendolo in sola scrittura.
È equivalente a: open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
Restituisce il file descriptor del file o -1 in caso di errore
#include
int close(int filedes);
Chiude il file identificato da filedes e precedentemente aperto con open o creat.
Ritorna 0 in caso di successo o -1 in caso di errore.
Ad ogni file aperto è associato un intero, detto offset, che rappresenta la posizione (espressa in numero di byte dall’inizio del file) in cui verrà effettuata la prossima operazione di I/O
L’offset è inizializzato, per default, a zero dalla open e dalla creat.
Anche l’opzione O_APPEND inizializza l’offset a zero per le sole operazioni di lettura. La prima scrittura, però, sposta l’offset a fine file.
Le operazioni di lettura e scrittura incrementano il valore dell’offset di un numero di byte pari al numero di byte letti o scritti. È possibile modificare l’offset del file utilizzando la seguente system call:
off_t lseek (int filedes, off_t offset, int whence);
Lseek ritorna il nuovo valore dell’offset o -1 in caso di errore:
Il parametro whence indica come utilizzare il parametro offset:
Ad esempio, per conoscere l’offset corrente, è sufficiente eseguire: lseek(filedes, (off_t)0, SEEK_CUR);
La lettura da file può essere effettuata utilizzando la system call read, definita come segue:
#include
ssize_t read (int filedes, void *buff, size_t nbytes);
I parametri della system call sono:
Si noti che il secondo parametro è un void *, I.e., è un puntatore “generico”. Questo consente di leggere attraverso la read qualsiasi tipo di dato. È possibile leggere, quindi, una qualsiasi sequenza di byte (contigui) nel file ed assegnarla ad un’area di memoria a partire dall’indirizzo indicato da buff.
Attenzione: possono verificarsi problemi di compatibilità tra i tipi su architetture non compatibili.
Ad esempio, si supponga che le architetture A e B rappresentino lo stesso tipo di dato utilizzando un diverso numero di byte od una diversa rappresentazione in memoria, e.g. little-endian e big-endian. In questo caso, un file scritto su una architettura A potrebbe essere interpretato erroneamente su una architettura B.
Non esistono problemi di compatibilità quando in un file vengono scritti solo variabili di tipo carattere (o array di caratteri).
L’operazione di lettura avviene sempre partendo dall’offset corrente del file. La system call read ritorna:
Il numero di byte effettivamente letti può essere inferiore al parametro nbytes quando:
Dopo l’operazione l’offset viene incrementato di nbytes.
Attenzione: Il linguaggio C NON esegue alcun controllo sulle dimensioni delle variabili. Ad esempio, se il buffer utilizzato per la read è un intero (4 byte su architettura Intel x86), viene richiesta la lettura di 200 byte e questi sono tutti disponibili (i.e., il valore di ritorno della read e’ 200), la system call, “semplicemente” utilizza i 4 byte della variabile intera e tenta di sovrascrivere i successivi 196. Se questi non sono allocati all’utente, allora il sistema operativo bloccherà il tentativo di accesso alla memoria, altrimenti, i dati (dell’utente) saranno modificati. Quest’ultimo caso porta a comportamenti anomali del programma, difficilmente identificabili.
La scrittura in un file può essere effettuata utilizzando la system call write, definita come segue:
#include
ssize_t write (int filedes, void *buff, size_t nbytes);
I parametri della system call sono:
Come nel caso della read, il parametro buff e’ ti tipo void *. È possibile scrivere quindi su file qualunque tipo di dato.
Valgono, per la write, le stesse considerazioni sulla compatibilità di file scritti su architettura non compatibili.
Anche la system call write può scrivere un numero di byte inferiore a nbytes. È sempre necessario verificare il valore di ritorno delle system call per risolvere eventuali problemi sorti durante la loro esecuzione.
I sistemi operativi consentono la condivisione tra più processi dei file aperti
Il kernel usa tre strutture dati indipendenti per gestire i file:
Se due processi indipendenti aprono lo stesso file, condividono solo il v-node/i-node, come mostrato in figura.
Le informazioni sullo “stato” sono contenute nella file table. Le sytem call read, write e lseek modificano, quindi, solo l’offset per il processo che le invoca.
Se il processo 1 apre il file CON O_APPEND: prima di ogni write, eseguita dal processo 1, il sistema operativo rilegge la dimensione del file dal v-node e scrive il suo valore nell’offset corrente; se anche un altro processo modifica la dimensione del file, l’utilizzo di questa opzione garantisce la scrittura delle informazioni alla fine “effettiva” del file.
Se il processo 1 apre il file SENZA O_APPEND: sembrerebbe possibile simulare il processo appena descritto eseguendo un posizionamento a fine file seguito “immediatamente” da una write: questa sequenza può NON essere equivalente ad una write eseguita con O_APPEND. Difatti, lo scheduler potrebbe interrompere il processo 1 subito dopo la seek ed eseguire il processo 2 che potrebbe modificare la dimensione del file. Quando lo scheduler riprende l’esecuzione del processo 1, viene eseguita la seek, utilizzando un valore dell’offset non consistente con lo l’effettivo stato del file.
Il programma di seguito modifica il file testfile: Mostra codice.
Il programma: apre il file in append e, all'interno di un ciclo, legge un carattere x, compone una stringa come x seguita da "aaaaaaaa\n" e scrive la stringa nel file. Visto che il file è aperto in append, se anche spostiamo l'offset "ad inizio file", la stringa viene sempre scritta a fine file.
Si noti che ogni write incrementa l'offset del file di 10 byte.
Il programma termina quando l'utente digita il carattere f.
L'esempio di seguito: Mostra codice, riporta l'output del programma quando l'input sono I carattere 1,2,3,4,5,f.
Una seconda variante dello stesso programma: Mostra codice
Il programma: apre il file NON utilizzando il flag O_APPEND. Posizione, quindi, l'offset a fine file e, all'interno di un ciclo, legge un carattere x, compone una stringa come x seguita da "bbbbbbbb" e scrive la stringa nel file. Visto che all'interno del ciclo l'offset del file non viene modificato da una seek, il programma, scrive la stringa sempre "a fine file".
Il programma termina quando l'utente digita il carattere f.
Di seguito l'output del programma quando l'input è costituito dai caratteri 1,2,3,4,5,f. : Mostra codice
I due programmi appena visti, eseguiti in isolamento sono corretti. Nel momento in cui vengono eseguiti in concorrenza e sullo stesso file, esibiscono un comportamento inatteso.
Mostra codice: il file testfile dopo la seguente sequenza di esecuzione riportata di seguito. Tra parentesi indichiamo l'intervallo dei caratteri in input:
Esegui A; Esegui B; A scrive 3 stringhe (1-3); B scrive 5 stringhe (a-e); A scrive 5 stringhe (4-8).
Quando A e B partono, ogni processo punta al proprio elemento nella file table, ma entrambi gli elementi puntano allo stesso v-node (stesso file). Assumendo il file inizialmente vuoto, entrambi I processi inizializzano il proprio offset al byte 0, i.e. Off_A=0, Off_B=0. Si noti che la presenza del getchar, forza i processi ad attendere l'input dall'utente.
Quindi A scrive le prime 3 righe del file, a seguito dell'input dei caratteri 1-3. La scrittura modifica l'offset del solo processo A, I.e., Off_A=30, Off_B=0.
Quando B scrive 5 righe a seguito dell'input dei caratteri a-e, il sistema operativo utilizza l'elemento della file table indicato da B, in cui l'offset è ancora 0. Le 5 righe scritte da B, quindi, sovrascrivono le righe scritte da A. A questo punto, Off_A=30, Off_B=50.
Ora A scrive altre 5 righe. Avendo, però, il processo A aperto il file con O_APPEND, prima di ogni write, il sistema operativo aggiorna l'offset di A ponendolo a "fine file". La prima scrittura, quindi, aggiorna il valore dell'offset Off_A=50 prima dell'esecuzione dell'operazione di I/O.
Vediamo ora il comportamento degli stessi programmi quando invertiamo l’ordine delle scritture. La nuova sequenza di esecuzione e’ la seguente:
Esegui A; Esegui B; B scrive 3 stringhe (a-c); A scrive 5 stringhe (1-5); B scrive 3 stringhe (a-c).
Come nel caso precedente, assumendo il file inizialmente vuoto, entrambi I processi inizializzano il proprio offset al byte 0, i.e. Off_A=0, Off_B=0.
Quindi B scrive le prime 3 righe del file, a seguito dell’input dei caratteri a-c. La scrittura modifica l’offset del solo processo B, I.e., Off_A=0, Off_B=30.
Quando A scrive 5 righe a seguito dell’input dei caratteri 1-5, avendo aperto il file con il parametro O_APPEND, il sistema operativo aggiorna il valore di Off_A a 30 prima di iniziare la scrittura. Al termine delle scritture, I valori degli offset sono Off_A=80, Off_B=30.
Infine, la scrittura delle tre righe da parte del processo B avviene a partire dal suo offset corrente, 30. Quindi l’effetto delle tre scritture di e’ la sovrascrittura delle prime tre dighe scritte da A.
Il file testfile dopo la sequenza di operazioni indicata: Mostra codice.
Un file descriptor può essere “duplicato” utilizzando una delle seguenti system call:
int dup (int filedes);
int dup2(int filedes, int filedes2);
La duplicazione consiste nel far puntare due file descriptor (dello stesso processo) allo stesso elemento della file table (lo stesso file).
La system call dup ritorna un file descriptor che punta allo stesso file indirizzato da filedes. Il valore ritornato da dup è il minimo file descriptor non utilizzato.
La system call dup2 prende in input anche un secondo parametro, filedes2 che contiene il file descriptor da usare nella duplicazione. Se filedes2 è aperto, dup2 chiude il file ad esso associato prima di duplicare il descrittore filedes.
Nell’esempio, il file puntato da filedes2 viene chiuso, dopodichè l’entry filedes2 della process table viene fatto puntare all’entry della file table a cui punta filedes. In alcuni casi dup2 potrebbe essere simulata attraverso una sequenza di open, close e dup. Si tenga conto, però, che a differenza delle simulazioni, la system call dup2 viene eseguita atomicamente. L’utilità di dup e dup2 consiste nel fatto che è possibile implementare semplicemente la redirezione all’interno di una applicazione.
Frammento di codice di una variante di uno dei programmi appena illustrati: Mostra codice.
A differenza della versione originale, questo programma invia su standard output un messaggio "Comando:" e scrive su testfile stringhe contenenti il primo carattere digitato dall'utente concatenato alla stringa "aaaaaaaaa\n".
La presenza, però, di dup2 implica:
L'effetto di dup2 sarà, quindi, che ogni operazione di I/O su standard output corrisponderà, in realtà, ad una scrittura su file "testfile".
Difatti, l'esecuzione del frammento di codice con input gli interi 1-5 ed il carattere f, provoca la scrittura nel file "testfile" delle righe riportate nel seguente esempio: Mostra codice.
1. Introduzione ai sistemi Unix
2. Principi di programmazione Shell
3. Esercitazioni su shell scripting - parte prima
5. Esercitazioni su shell scripting - parte seconda
6. Espressioni Regolari ed Introduzione ad AWK
7. Esercitazioni su espressioni regolari ed awk scripting
9. Esercitazioni su awk scripting - parte seconda
10. Programmazione in linguaggio C: Input/Output di basso livello
11. Esercitazioni su I/O di basso livello
12. Interazione con file di sistema e variabili d'ambiente
13. Esercitazioni sulla gestione dei file di sistema e le variabili...
14. System call per la gestione di file e directory
15. Esercitazioni su gestione file e directory
16. La programmazione multi-processo
17. Esercitazioni su programmazione multi-processo
18. I Segnali
20. I Socket
Brian W. Kernighan, Dennis KM. Ritchie, Linguaggio C
W.R. Stevens, S.A. Rago, Advanced Programming in the Unix Environment
Capitolo 3 (3.1/3.10)