Sei sulla pagina 1di 36

Definizione e Gestione

dei Processi
Programma: Descrizione di un algoritmo utilizzando un linguaggio di
programmazione. Un insieme di byte che contiene le istruzioni che
dovranno essere eseguite (Entità Passiva)

Processo: Programma in esecuzione all’interno del calcolatore (Istanza del


programma). Ogni attività all’interno di un sistema operativo è
rappresentata da un processo (Entità Attiva)
Gestione dei processi

- Sistema monoprogrammato: un solo processo può essere in esecuzione


nel sistema operativo (multitasking realizzato virtualmente)

- Sistema multiprogrammato: più processi possono essere


simultaneamente presenti nel sistema
Compiti del Sistema Operativo

- creazione e terminazione dei processi


- sospensione e ripristino dei processi
- sincronizzazione e comunicazione tra processi
- gestione del blocco critico dei processi
Processi modello UNIX (Processi Pesanti)

- Ogni processo ha un proprio spazio di indirizzamento completamente


locale e non condiviso.
- I dati non sono perciò condivisi
- Può comunicare con altri processi pesanti utilizzando appositi canali
messi a disposizione dal sistema operativo (Modello a scambio di
messaggi)
- pipe
- socket
Il codice fra processo padre e processo figlio può essere condiviso
Diagramma degli stati di un processo pesante (Modello Unix)

zombie
init ready running

sleeping
swapped terminated
Stati di un processo pesante (Caso Generale)
- init: caricamento in memoria del processo e inizializzazione delle strutture
dati del Sistema Operativo
- ready: processo pronto ad utilizzare la CPU
- running: processo in esecuzione (utilizza la CPU)
- sleeping: processo sospeso in attesa di un evento
- terminated: deallocazione del processo dalla memoria

Stati di un processo pesante (UNIX)


- zombie: il processo è terminato, ma in attesa che il padre ne rilevi lo stato
di terminazione
- swapped: il processo (o parte di esso) è temporaneamente trasferito in
memoria secondaria
Rappresentazione dei processi in UNIX

Process control block: il descrittore del processo in UNIX è rappresentato


da 2 strutture dati:

• Process structure: mantiene le informazioni necessarie per la gestione


del processo, anche se questo è in stato swapped (residente in memoria
secondaria)

• User structure: il suo contenuto è necessario solo in caso di


esecuzione del processo (stato running); se il processo è soggetto a
swapping, anche la user structure può essere trasferita in memoria
secondaria
Thread (Processo Leggero)

Un thread è un singolo flusso sequenziale di controllo all’interno di un


task

- è un’unità di esecuzione che condivide codice e dati con altri thread ad


esso associati
- tutti i thread appartenenti allo stesso processo condividono lo stesso
spazio di indirizzamento (codice e dati)

Un Processo Pesante è l’equivalente di un task con un unico thread


Processo Single-threaded VS Processo Multit-hreaded

Codice Dati Files Codice Dati Files


Registri Registri
Registri e Stack
Stack Stack

thread
Vantaggi dei thread

• Condivisione di memoria: a differenza dei processi (pesanti), un thread può


condividere variabili con altri thread (appartenenti allo stesso task)

• Minor costo di cambio di contesto: il cambio di contesto fra thread dello


stesso task ha un costo notevolmente inferiore al caso dei processi “pesanti”

Svantaggi dei thread:

• Minor protezione: thread appartenenti allo stesso task possono modificare


dati gestiti da altri thread
Soluzioni adottate nei vari sistemi operativi

• MS-DOS: un solo processo utente ed un solo thread (processi pesanti)

• UNIX: più processi utente ciascuno con un solo thread (processi pesanti)

• Supporto della Java VM: un solo processo, più thread (green thread) ma
può utilizzare le API del sistema operativo ospitante (native thread)

• Linux, Windows, Mac OSx, Solaris: più processi utente ciascuno con più
thread
Realizzazione dei thread a livello kernel del sistema operativo (native thread)

• Molti Sistemi Operativi offrono l’implementazione del concetto di thread:


- Il Sistema Operativo gestisce direttamente i cambi di contesto
tra thread dello stesso task (trasferimento di registri) e tra task.

Realizzazione dei thread a livello utente (es. Green thread)

• il passaggio da un thread al successivo (nello stesso task) non richiede


interruzioni al Sistema Operativo (maggior rapidità)
• Il Sistema Operativo vede processi pesanti: la sospensione di un thread
causa la sospensione di tutti i thread del task
Interazione tra processi: I processi, pesanti o leggeri, possono interagire

Classificazione in base all’interazione

• processi indipendenti: due processi P1 e P2 sono indipendenti se


l’esecuzione di P1 non è influenzata da P2, e viceversa

• processi interagenti: P1 e P2 sono interagenti se l’esecuzione di P1 è


influenzata dall’esecuzione di P2, e/o viceversa
Processi interagenti

Tipi di interazione:

• Cooperazione: interazione prevedibile e desiderata, insita nella logica del


programma concorrente. I processi cooperanti collaborano per il
raggiungimento di un fine comune

• Competizione: interazione prevedibile ma "non desiderata" tra processi che


interagiscono per sincronizzarsi nell’accesso a risorse comuni

• Interferenza: interazione non prevista e non desiderata, potenzialmente


deleteria tra processi
Processi interagenti

L’interazione può avvenire mediante


• memoria condivisa (modello ad ambiente globale): il Sistema operativo
consente ai processi (thread) di condividere variabili; l’interazione avviene
tramite l’accesso a variabili condivise

• scambio di messaggi (modello ad ambiente locale): i processi (pesanti) non


condividono variabili e interagiscono mediante meccanismi di
trasmissione/ricezione di messaggi; il Sistema operativo prevede dei
meccanismi a supporto dello scambio di messaggi (tramite ad esempio pipe
o socket)
Processi cooperanti (ESEMPIO Produttore e Consumatore)

• Due processi accedono a un buffer condiviso di dimensione limitata:


un processo svolge il ruolo di produttore di informazioni che verranno prelevate
dall’altro processo (consumatore)

• Il buffer rappresenta un deposito di informazioni condiviso

Necessità di sincronizzare i processi:

•quando il buffer è vuoto (il consumatore NON può prelevare messaggi)


•quando il buffer è pieno (il produttore NON può depositare messaggi)
Processi in Competizione: mutua esclusione (ESEMPIO)

Due thread P1 e P2 hanno accesso ad una struttura condivisa organizzata a


pila rispettivamente per inserire e prelevare dati.

• La struttura dati è rappresentata da un vettore stack i cui elementi


costituiscono i singoli dati e da una variabile top che indica la posizione
dell’ultimo elemento contenuto nella pila.

• I thread utilizzano le operazioni Inserimento e Prelievo per depositare e


prelevare i dati dalla pila

• P1 e P2 eseguono concorrentemente operazioni (inserimento e prelievo


rispettivamente) sullo stack condiviso
P1
Stack P2
Inserimento
Prelievo
L’esecuzione contemporanea di queste operazioni da parte dei processi può portare ad
un uso scorretto della risorsa. Consideriamo questa sequenza di esecuzione:

T0: top++; (P1)


T1: x=stack[top]; (P2)
T2: top--; (P2)
T3: stack[top]=y; (P1)

• Viene assegnato a x un valore non definito e l’ultimo valore valido contenuto nella
pila viene cancellato dal nuovo valore di y.

• Potremmo individuare situazioni analoghe, nel caso di esecuzione contemporanea di


una qualunque delle due operazioni da parte dei due processi.
• Il problema sarebbe risolto se le due operazioni di Inserimento e Prelievo fossero
eseguite sempre in mutua esclusione (istruzioni indivisibili): finché non è terminata
l’operazione corrente, nessun’altra operazione può essere eseguita sullo stack

Istruzioni Indivisibili

• Data un'istruzione I(d), che opera su un dato d, essa è indivisibile (o atomica) , se,
durante la sua esecuzione da parte di un processo P, il dato d non è accessibile ad
altri processi.

• I(d), a partire da un valore iniziale d0 , può operare delle trasformazioni sullo stato
di d, fino a giungere allo stato finale dF; poiché I(d) è indivisibile, gli stati
intermedi non possono essere rilevati da altri processi concorrenti.
Sezione Critica

• La sequenza di istruzioni con le quali un processo accede e modifica un insieme di


variabili comuni prende il nome di sezione critica.

• Quando un processo si trova all’esterno di una sezione critica non può rendere
impossibile l’accesso alla stessa sezione ad altri processi.
Il problema del deadlock

Quando due o più processi competono per l’uso di risorse comuni, è possibile
incorrere in situazioni di stallo (deadlock, o blocco critico)

Definizione: un insieme di processi è in deadlock se ogni processo dell’insieme è in


attesa di un evento che può essere causato solo da un altro processo dell’insieme.

Esempio di Deadlock
Si considerino due processi P1 e P2 che condividono le stesse risorse R1 e R2, da
accedere in mutua esclusione; se contemporaneamente:
• P1 sta usando R1 e richiede R2
• P2 sta usando R2 e richiede R1
P1 e P2 sono bloccati indefinitamente: deadlock!
Domande Proposte:

1) Descrivere le differenze fra Green Thread e Native Thread


2) Indicare quali sono le differenze fra Programma e Processo
3) Disegnare e descrivere il diagramma degli stati di un processo UNIX
4) Disegnare e descrivere il diagramma degli stati di un thread Java
5) Descrivere entrambe le implementazioni possibili di un Thread Java
indicando in particolare le differenze fra ereditarietà nella classi e nelle
interfacce
6) Descrivere il significato di sezione critica. Indicare come viene
implementata una sezione critica in Java
7) Descrivere i metodi studiati a lezione della classe Thread di Java e i metodi
wait(), notify() e notifyAll() della classe Object
8) Descrivere il significato di cooperazione, concorrenza, sezione critica,
deadlock e starvation
Implementazione dei Thread in Java

Due diverse modalità per implementare i thread in Java:

• estendendo la classe Thread


- Soluzione poco flessibile: NON permette di utilizzare l’ereditarietà su altre
classi in quanto Java supporta solo l’ereditarietà singola delle classi

• implementando l’interfaccia Runnable


- Soluzione più flessibile della precedente: in Java è permessa
l’ereditarietà multipla delle interfacce
Implementazione come estensione della classe Thread

• La sottoclasse deve ridefinire il metodo run() [overriding]


• si crea un’istanza del thread tramite new() [si invoca il costruttore della
classe creata]
• si esegue un thread invocando il metodo start() che a sua volta invoca il
metodo run()
- Il metodo start() analizzerà il sistema operativo che ospita la JVM e, se è
presente il supporto nativo ai thread, il metodo start() creerà un thread nativo
che sarà gestito direttamente dal sistema operativo. In caso contrario, i thread
generati saranno gestiti dalla JVM
ESEMPIO di creazione di un Thread Java estendendo la classe Thread
public class SimpleThread extends Thread{ // Estenzione della classe Thread

public SimpleThread(String str){


super(str);
}

public static void main(String[] args){ // Main


SimpleThread st1=new SimpleThread("Thread 1"); // Creazione dell’instanza del Thread
st1.start(); //Generazione e avvio del nuovo Thread
}

public void run(){ // Definizione delle azioni del Thread nel metodo run()
…………………………………………………………………
}
ESEMPIO di creazione di un Thread Java implementando l’interfaccia Runnable
public class EsempioRunnable extends MiaClasse implements Runnable { // implementazione dell’interfaccia

public void run() { //Implementazione del metodo run() che conterrà le azioni svolte dal Thread
………………………………………………………………………………
}
}

public class Esempio {

public static void main(String args[]){ // Main


EsempioRunnable e = new EsempioRunnable(); // Creazione di una nuova istanza dell’oggetto
Thread t = new Thread (e); // Creazione di una nuova istanza dell’oggetto Thread (wrapper)
t.start(); //Generazione e avvio del nuovo Thread
}
}
Implementazione come implementazione della classe Runnable

• la classe deve ridefinire il metodo run()[clausola implements]


• si crea un’istanza di tale classe tramite new()[si invoca il costruttore della classe]
• si crea un’istanza della classe Thread con new(), passandole come parametro
l’oggetto che implementa Runnable [wrapper]
• si esegue il thread invocando il metodo start() sull’oggetto con classe Thread
creato
Altri metodi di interesse per della classe Java Thread

• sleep(long ms) sospende thread per il numero di ms specificato

• join() attende la terminazione del thread specificato

• yield() costringe il thread a cedere il controllo della CPU

• stop() forza la terminazione di un thread

- Se il thread interrotto stava compiendo un insieme di operazioni da eseguirsi in


maniera atomica, l’interruzione può condurre ad uno stato inconsistente del
sistema (DEPRECATO)
• suspend() blocca l’esecuzione di un thread, in attesa di una successiva invocazione di
resume() e non libera le risorse impegnate dal thread

- Se il thread sospeso aveva acquisito una risorsa in maniera esclusiva (ad es., interrotto
durante l’esecuzione di un metodo synchronized), tale risorsa rimane bloccata
(DEPRECATO)

- ATTENZIONE: Alcuni dei metodi proposti possono generare delle eccezioni ed è quindi
necessario inserirli all’interno di una sezione try, catch e finally.
Ciclo di vita di un Thread Java yield()

New start() Not


Runnable
Thread Runnable

Termine del metodo run()

Dead
• New Thread:
- subito dopo l’istruzione new()
- il costruttore alloca e inizializza le variabili di istanza

• Runnable: - il thread è eseguibile ma potrebbe non essere in esecuzione

• Not Runnable: - Il thread non può essere messo in esecuzione. Entra in questo stato quando è
in attesa della terminazione di un’operazione di I/O, cerca di accedere ad un metodo
“synchronized” di un oggetto bloccato, o dopo aver invocato uno dei seguenti metodi: sleep(),
wait(), suspend(). Esce da questo stato quando si verifica la condizione complementare

• Dead: - Il thread giunge a questo stato per “morte naturale” (termine metodo run() ) o perché
un altro thread ha invocato il suo metodo stop() [deprecato]
Per evitare che thread diversi interferiscano durante l’accesso ad oggetti condivisi si
possono imporre accessi esclusivi:

• La JVM supporta la definizione di lock sui singoli oggetti tramite la keyword synchronized

• synchronized può essere definita:


- su metodo
- su singolo blocco di codice

• Un thread che non riesce ad entrare in una sezione synchronized resta sospeso sulla
richiesta della risorsa fino a che la risorsa non diventa disponibile
Metodo wait() della classe Object
• il thread che la invoca si blocca in attesa che un altro thread invochi notify() o notifyAll()
per quell’oggetto.

Metodo notify() della classe Object


• il thread che la invoca risveglia uno dei thread in attesa, scelto arbitrariamente

Metodo notifyAll() della classe Object


• il thread che la invoca risveglia tutti i thread in attesa: essi competeranno per l’accesso
all’oggetto. È preferibile (e può essere necessaria) se più thread sono in attesa
Regole empiriche

• Se due o più thread possono modificare lo stato di un oggetto, è necessario dichiarare


synchronized i metodi di accesso

• Se deve attendere la variazione dello stato di un oggetto, un thread deve invocare il


metodo wait()

• Ogni volta che un metodo attua una variazione dello stato di un oggetto, esso deve
invocare notifyAll()

• È necessario verificare che ad ogni chiamata a wait() corrisponda una chiamata a


notifyAll()

Potrebbero piacerti anche