1. Indice

2. Filesystem UNIX

Tutti i dischi e le relative partizioni vengono rese accessibili mediante un unico filesystem virtuale attraverso le operazioni di mounting (montaggio).

All’interno del virtual filesystem UNIX vi sono alcune cartelle principali sempre presenti:

Il comando per rendere accessibile un filesystem in una determinata posizione del virtual filesystem è mount:

sudo mount /dev/sdb1 /mnt/usb			# Rende accessibile il contenuto della partizione /dev/sdb1 nella cartella /mnt/usb
sudo umount /mnt/usb					# Scollega il dispositivo

mount -t vfat /dev/sdb1 /mnt

Il file di configurazione con le informazioni relative al montaggio di filesystem all’avvio è /etc/fstab.

2.1. Gestione dei File

I sistemi UNIX sono caratterizzati dall’omogeneità della rappresentazione delle risorse, tutte trattate come file.

I file sono di tre tipi:

In particolare i directory file sono costituiti da una serie di record che ne descrivono il contenuto:

<nome_file1, i-number1>
<nome_file2, i-number2>
<nome_file3, i-number3>
...

Una parte del disco è dedicata alla i-List, ovvero la lista di tutti i descrittori di file, detti i-node, riferiti e identificati da un i-number.

L’i-node descrive le caratteristiche del file:

Quando viene creato un file di nome fileName viene creato un nuovo i-node e viene aggiunto alla i-List all’indice IN, che rappresenta l’i-number. Al contenuto della directory viene quindi aggiunto un nuovo record <fileName, IN>.

Dopo la creazione, il file ha un solo nome, che combacia con l’hard link al descrittore del file.

Il filesystem permette di definire più hard link associato ad un i-node, generando a tutti gli effetti nomi alternativi per lo stesso file.

È possibile inoltre definire anche collegamenti simbolici (soft-link), per avere alias di riferimento all’hard link.

Hard Link Soft Link
Eliminazione/spostamento
dell'hard link
Non ha alcun effetto sugli altri hard link Il soft link non è più in grado di accedere al file
Effetti L'i-node viene eliminato solo se il numero di hard link è 0 Non ha alcun effetto sull'hard link

Per creare un link si utilizza il comando ln:

# Creazione di un hard link per un file già esistente
ln target link_name

# Creazione di un soft link
ln -s target link_name

2.2. Accesso ai File

Il meccanismo di accesso ai file è di tipo sequenziale. Ad ogni file aperto è associato un I/O pointer riferimento per la lettura/scrittura sequenziale sul file.

Le operazioni di lettura/scrittura provocano l’avanzamento del riferimento.

All’interno del descrittore di processo (user structure) si trova una tabella dei file aperti di processo. Gli elementi di questa tabella si chiamano file descriptor, è non sono altro che riferimenti agli elementi corrispondenti nella Tabella di File Aperti di Sistema.

All’interno della Tabella dei File Aperti di Sistema si trovano tanti elementi quanti sono i file aperti dal sistema. In particolare, se due processi diversi aprono lo stesso file avremo due entry separate.

All’interno dei record di questa tabella troviamo un I/O pointer al file e un riferimento all’i-node del file.

Gli i-node sono salvati nella memoria principale nella Tabella dei file Attivi.

Attraverso I/O pointer e i-node possiamo trovare l’indirizzo fisico in cui effettuare le prossima lettura/scrittura sequenziale.

STDIN, STDOUT e STDERR sono descrittori di default, generati automaticamente al momento dell’esecuzione del programma.

Quando invece viene generato un processo figlio, questo erediterà una copia di tutte le strutture dati del padre, in particolare anche la User Structure e i relativi file descriptor.

Ciò implica che un processo padre e i suoi processi figli descrittori che puntano allo stesso elemento della Tabella di File di Sistema e quindi condividono l’I/O pointer nell’accesso sequenziale al file.

Per l’accesso ai file sono presenti diverse primitive:

/**
* @param path è il path del file da "aprire"
* @param flags determina le modalità di accesso
* @param prot (opt) parametro opzionale nel caso in cui l'apertura provochi la creazione di un nuovo file, contiene i bit di protezione associati al file.
* @return il file descriptor
*/
int open(const char* path, int flags, int prot);

Per quanto riguarda flags ci sono diverse macro definite in <fcntl.h> per descrivere le possibili modalità. Se compatibili possono essere messe in OR. Alcuni esempi sono O_RDONLY, O_WRONLY, ORDWR, O_APPEND, O_CREAT. Ad esempio, la composizione O_WRONLY | O_CREAT specifica che il file da scrivere, se non esiste, va creato.

Per la lista completa è possibile consultare il manuale:

man 2 open

Dopo l’apertura, l’I/O pointer viene posizionato all’inizio del file, tranne se si utilizza la modalità O_APPEND, dove il puntatore parte dalla fine del file.

Altre funzioni per l’utilizzo dei file:

/**
* Chiude un file precedentemente aperto
*
* @param fd file descriptor che si vuole chiudere. È genrato da funzioni come open(), pipe(), ...
*
* @returns `0` se la chiusura è riuscita, `-1` se ci sono stati errori
*/
int close(int fd)

/**
* Legge da un file
*
* @param fd descrittore del file da cui leggere
* @param buf puntatore al buffer in cui scrivere i dati letti
* @param count numero massimo di byte da leggere (intero positivo)
*
* @returns il numero di byte letti. Un valore negativo in caso di errore
*/
ssize_t read(int fd, void* buf, size_t count)

/**
* Scrive su un file
*
* @param fd descrittore del file su cui scrivere
* @param buf puntatore al buffer da cui leggere i dati da scrivere
* @param count numero massimo di byte da scrivere (intero positivo)
*
* @returns il numero di byte scritti. Un valore negativo in caso di errore
*/
ssize_t write(int fd, const void* buf, size_t count)

All’interno di read e write il valore di ritorno può essere diverso dal numero di byte richiesti. Per la read può avvenire per diversi motivi, vedremo più avanti che l’uso tipico infatti è di mettrela all’interno di un while.

Per quanto riguarda la write invece questa differenza tra i valori può essere dovuto alla terminazione dello spazio disponibile.

Vediamo un sempio di lettura di lettura testo da file e stampa a video con buffer di dimensioni fisse:

#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>

#define BUF_SIZE 64

int main(int argc, char** argv) {
	if (argc < 2) {
		printf("Usage: %s FILENAME\n", argv[0]);
		exit(-1);
	}

	int fd = open(argv[1], O_RDONLY);

	if (fd < 0) {
		perror("Errore nella open");
		exit(-1);
	}

	char buffer[BUF_SIZE];

	ssize_t nread;

	while ((nread = read(fd, buffer, BUF_SIZE-1)) > 0) {
		buffer[nread] = '\0';
		printf("%s", buffer);
	}

	close(fd);

	if (nread < 0) {
		perror("Errore nella read");
		exit(-1);
	}

	exit(0);
}

Queste funzioni di sistema non sono bufferizzate.

La libreria standard del C mette invece a disposizione funzioni bufferizzate (fopen, fclose, fwrite, …) e altre funzioni che permettono la lettura/scrittura formattata (fprintf, fscanf, …)

Poiché la fread utilizza un buffer interno, una quantità maggiore di dati rispetto a quella chiesta potrebbe essere letta e memorizzata, anche se poi viene restituita solo la quantità richiesta. Con la read invece viene letta al più la quantità di dati chiesta.

2.3. Comunicazione tra processi - pipe

I processi possono comunicare sfruttando il meccanismo delle pipe.

È un tipo di comunicazione indiretta senza naming esplicito, che realizza il concetto di mailbox. Attraverso le pipe si possono accordare messaggi in buffer di dimensioni predefinite, che saranno estratti con politica FIFO.

La pipe è quindi un canale monodirezionale a due estremi associati a due file descriptor diversi (uno per la lettura e uno per la scrittura).

/**
* Funzione che apre i file descriptor per la comunicazione
*
* @param fd vettore di due interi. Conterrà i descrittori della pipe:
*			- fd[0] è il descrittore per la lettura
*			- fd[1] è il descrittore per la scrittura
*
* @returns `0` se ha successo. `-1` altrimenti
*/
int pipe(int* fd)

È possibile per i processi figli ereditare una copia degi file descriptor, rendendoli in grado di comunicare sia con il padre che con gli altri figli.

Nel caso di comunicazioni di processi in gerarchie diverse si utilizzano altri strumenti, come i socket.

Dobbiamo sottolineare che il comportamento di read sull’estremità della pipe è bloccante se il buffer è vuoto e c’è almento uno scrittore attivo. La funzione restituisce invece EOF (ovvero 0) se il buffer è vuoto e tutti gli scrittori hanno chiuso il file descriptor.

Analogamente, la write sull’estremità della pipe è bloccante se il buffer è pieno, ma restituisce errore se non ci sono lettori attivi.

Di conseguenza diventa importante chiudere gli estremi non utilizzati da un processo, ovvero pipefd[1] per i lettori e pipefd[0] per gli scrittori, così da evitare deadlock e comportamenti inattesi.

3. Pilotare Applicazioni

Abbiamo già visto e utilizzato STDIN, STDOUT e STDERR che sono descrittori i di defaults. Questi sono generati automaticamente al momento dell’esecuzione del programma, e la loro apertura e chiusura è gestita automaticamente dal sistema operativo.

È possibile accedervi attraverso le MACRO definite in <unistd.h>:

Per duplicare un file descriptor possiamo utilizzare la funzione dup2:

/**
* Permette di duplicare un file descriptor in un altro.
*
* @param target è il file descriptor da duplicare
* @param newfd è il file descriptor dove verrà messa la copia di target. Se era precedentemente aperto la funzione lo chiude prima di copiarci sopra target
*
* @return un valore negativo in caso di error
*/
int dup2(int target, int newfd)

È possibile combinare le funzioni pipe e dup2 per redirezionare il flusso dei dati dagli standard verso gli altri fd.

int main() {
	int pipe_fd[2];
	pid_t pid;

	pipe(pipe_fd);
	pid = fork();

	if (pid == 0) {
		// Figlio che deve scrivere
		close(pipe_fd[0]);

		// redirigo lo stdout verso la pipe
		dup2(pipe_fd[1], STDOUT_FILENO);

		// da adeso in poi ogni volta che si stamperà sullo stdout
		// questi verranno rediretti e spostati verso la pipe
		execl("bin/ls", "ls", "-l", NULL);
	}
	if (pid > 0) {
		// padre lettore
		char buffer[1024];
		int nread = -1;
		int index = 0;

		close(pipe_fd[1]);

		while (nread != 0) {
			nread = read(pipe_fd[0], &buffer[index], sizeof(buffer) - 1);
			buffer[index + nread] = '\0';
			index += nread;
		}

		printf("Il padre ha letto: %s\n", buffer);
	}

	return 0;
}

Attraverso la reindirizzamento possiamo utilizzare applicazioni che permettono l’input attraverso STDIN e output verso STDOUT e pilotarle tramite programi C a due processi a nostro piacimento, anche senza conoscere il codice stesso dell’applicazione.

Come è possibile vedere dall’esempio di seguito è infatti sufficiente:

int FtS[2];	// father 2 son
int StF[2];	// son 2 father
pid_t pid;

pipe(FtS);
pipe(StF);

pid = fork();

if (pid == 0) {
	// Rispetto a quello che mi da il padre sono in lettura
	close(FtS[1]);

	// Le letture devono essere pilotate verso l'applicazione
	dup2(FtS[0], STDIN_FILENO);

	// Avendolo copiato in STDIN_FILENO, posso chiuderlo
	close(FtS[0]);

	// Rispetto a quello che fornisco al padre sono in scrittura
	close(StF[0]);

	// Le mie scritture provengono dall'applicazione
	dup2(StF[1], STDOUT_FILENO);

	// Avendolo copiato in STDOUT_FILENO, posso chiuderlo
	close(StF[1]);

	execl("<path_applicazione>", "<nome_applicazione>", NULL);
}
else {
	// padre
	char sendBuf[BUFFER_SIZE];
	char recvBuf[BUFFER_SIZE];

	int nread;

	// chiudo le estremità non utilizzate
	close(FtS[0]);
	close(StF[1]);

	// Il '\n' è FONDAMENTALE
	sprintf(sendBuf, "<formato_stringa_applicazione>\n");
	write(FtS[1], sendBuf, strlen(sendBuf));

	// ricevo la risposta del figlio a.k.a. l'applicazione
	nread = read(StF[0], recvBuf, sizeof(recvBuf)-1);
	recvBuf[nread] = '\0';
	printf("%s\n", recvBuf);
}