Sistemi Multiprogrammati

Un sistema multiprogrammato è un sistema che permette l'esecuzione simultanea di più programmi, che a loro volta possono essere composti da vari processi o thread. Questo può permettere a più utenti di accedere al sistema contemporaneamente, o ad uno stesso utente di eseguire più programmi simultaneamente (aumentando l'utilizzabilità del sistema), o ad un singolo programma di scomporre la propria attività in un insieme di attività concorrenti (semplificando la struttura logica del programma).

Si passa così dal concetto di programma sequenziale al concetto di programma parallelo, utilizzando un paradigma di programmazione concorrente. Questo, se da un lato permette di strutturare meglio i programmi, dall'altro richiede una particolare attenzione a problemi di sincronizzazione fra le varie attività (spesso l'esecuzione di un programma concorrente risulta non deterministica) e una stretta interazione con il sistema operativo.

Prima di passare a vedere quali sono le funzionalità offerte dal sistema operativo a supporto della programmazione concorrente e come utilizzarle, è bene andare a definire in maniera più formale alcuni concetti di base.

 

Puntatori

Come accennato, un sistema concorrente permette l'esecuzione contemporanea di varie attività tramite multiprocessing o multithreading: andiamo allora a capire cosa si intende con questi concetti.

Si definisce algoritmo il procedimento logico seguito per risolvere un determinato problema. Tale algoritmo può essere descritto tramite un opportuno formalismo (chiamato linguaggio di programmazione), in modo da poter essere eseguito su un elaboratore. Tale codifica di un algoritmo, espressa tramite un linguaggio di programmazione è detta programma.

L'esecuzione di un programma può essere di tipo sequenziale, oppure può essere composta da più attività che eseguono in parallelo. In questo secondo caso (programma concorrente), le varie attività che eseguono in parallelo devono essere opportunamente sincronizzate fra loro e necessitano di comunicare e cooperare al fine di portare a termine il proprio scopo. Il modo in cui tali attività concorrenti cooperano e comunicano le distingue fra processi e thread.

I processi sono caratterizzati da un insieme di risorse private, fra cui gli spazi di memoria, per cui un processo non può accedere allo spazio di memoria di un altro per comunicare con esso. Per questo motivo, i processi si sincronizzano e comunicano fra loro tramite scambio di messaggi. Viceversa i thread condividono varie risorse, fra cui lo spazio di memoria, per cui possono comunicare tramite memoria comune, e sincronizzarsi tramite semafori.

 

Supporto di Sistema

Come noto, il sistema operativo è il programma che si occupa di gestire le risorse hardware della macchina, ivi comprese anche la CPU e la memoria (fisica e virtuale). Quindi, il sistema operativo deve fornire anche un supporto (più o meno esteso) alla concorrenza: per poter creare processi o thread, per sincronizzarli e per farli comunicare, il programma deve richiedere tali funzioni al sistema operativo, tramite specifiche chiamate di sistema, o system call (brevemente, syscall).

Spesso, l'insieme delle syscall esportate dal sistema operativo, detto anche interfaccia, o più propriamente Application Programming Interface (API), risulta essere troppo di basso livello per essere utilizzabile in maniera semplice dall'utente. Per questo motivo, vengono talvolta fornite delle librerie che implementano funzionalità di più alto livello basandosi sulle chiamate di sistema. Tipicamente, una libreria mette a disposizione delle chiamate di libreria (library call, o più brevemente libcall) che esportano un'interfaccia più potente e semplice da usare rispetto a quella fornita dalle chiamate di sistema. Bisogna però tenere presente che una chiamata di libreria eseguirà del codice di libreria in modo utente, e poi eventualmente chiamerà una o più syscall che eseguiranno in modo kernel.

Un tipico esempio per chiarire quanto appena detto è costituito dalla libreria standard del linguaggio C (libc): fra le altre cose, essa implementa gli stream di I/O, fornendo un modo semplice e potente per accedere ai file. Un file può essere aperto tramite la libcall fopen() e può essere acceduto in scrittura tramite la libcall fprintf(), che permette di scrivere numeri e stringhe effettuando automaticamente la conversione di formato. Tali funzioni si basano su alcune chiamate di sistema, che in sistemi Unix sono open() e write(), le quali permettono un accesso al file senza bufferizzazione dello stream e senza conversione di formato. Un utente può indifferentemente scegliere di accedere ad un file tramite le chiamate di sistema o le chiamate della libc, ma deve sempre sapere cosa sta facendo (e cosa la propria scelta implica), in quanto i due tipi di accesso non possono essere arbitrariamente mescolati (per esempio, non si può - chiaramente - effettuare una fprintf() su un file aperto con open()).

 

Note generali su Unix

Nel seguito analizzeremo le syscall disponibili a supporto della multiprogrammazione, ed in particolare vedremo come creare differenti flussi di esecuzione all'interno di un programma, come sincronizzarli e come realizzare la comunicazione fra thread e processi. Tali syscall dipendono dal sistema operativo, ed in questa sede faremo riferimento ai sistemi operativi Unix-like (Linux in particolare), secondo lo standard POSIX.
Sebbene il supporto alla multiprogrammazione fornito dal sistema sia sfruttabile utilizzando diversi linguaggi di programmazione, in questa sede utilizzeremo il linguaggio C. Si assume che il lettore sia familiare con i costrutti di base di tale linguaggio e con le librerie standard da esso fornite. In ogni caso, nel capitolo 2 verrà fatta una brevissima introduzione sulla struttura di un programma C, mentre nel capitolo 3 verrà presentata brevemente la I/O Standard Library.

Ogni system call ritorna un intero, che in caso di valore negativo segnala una condizione di errore: è allora buona norma verificare che tale valore sia maggiore o uguale a 0 ogni volta che si invoca una syscall, utilizzando per esempio un pezzo di codice simile al seguente.

int res;

res = syscall(...);
if (res < 0) {
	perror("Error calling syscall");
	exit(-1);
}

/* Program continues here... */

La funzione perror() invia sullo standard error (tipicamente, il video) un messaggio di errore composto dalla stringa passata come parametro seguita da una descrizione del motivo per cui l'ultima syscall chiamata è fallita.

I prototipi delle system call e le definizioni di costanti e strutture dati ad esse necessarie sono contenuti in alcuni header file di sistema (generalmente nella directory /usr/include/sys); per poter utilizzare una determinata syscall bisogna quindi includere gli adeguati header. I sistemi Unix mettono a disposizione il comando man, che fornisce una breve descrizione della semantica di una syscall specificata, assieme a specificarne la sintassi ed a elencare gli header da includere per utilizzare tale syscall. Riferirsi sempre alle manpage per includere i file corretti ed utilizzare una syscall con la giusta sintassi.