Esercizio - Implementare il banchiere e calcolarne la complessità (eserciziario, esercizio 3 e laboratorio 6)
In questa pagina 4
Testo (eserciziario del corso di Sistemi Operativi, Ing. Informatica UniPD, esercizio 3; laboratorio 6, task 3 e 4). Implementare l'algoritmo del banchiere e determinarne la complessità computazionale. Nel laboratorio si parte dal programma banker.c (adattato da una fonte pubblica): con gli stati iniziali dati va portato in uno stato sicuro (task 3) e poi esteso ad analizzare due grafi di allocazione (task 4). Teoria: Deadlock - condizioni, prevenzione, evitamento e rilevamentoUn deadlock (stallo) è una situazione in cui un insieme di processi attende all'infinito risorse possedute da altri processi dello stesso insieme; può verificarsi solo se valgono insieme quattro condizioni (mutua esclusione, hold and wait, assenza di prelazione, attesa circolare); il grafo di allocazione delle risorse (archi di richiesta P->R e di assegnazione R->P) senza cicli esclude lo stallo, con un ciclo lo implica se ogni risorsa ha una sola istanza, e lo rende solo possibile se ha più istanze; si può prevenire (negare una delle quattro condizioni, per esempio ordinare le risorse per negare l'attesa circolare), evitare (stato sicuro: esiste una sequenza sicura; algoritmo del grafo con archi di rivendicazione per istanza singola, algoritmo del banchiere con Available, Max, Allocation, Need = Max - Allocation e complessità O(m n^2) per istanze multiple), rilevare e ripristinare (grafo wait-for per istanza singola, algoritmo di rilevamento per istanze multiple; terminazione dei processi o prelazione delle risorse con scelta della vittima e rollback), oppure ignorare.Deadlock - condizioni, prevenzione, evitamento e rilevamento →.
Il programma del laboratorio
banker.c ha tre processi e tre risorse:
| Allocation | Max | |
|---|---|---|
| 0 1 0 | 1 1 0 | |
| 1 1 0 | 1 1 1 | |
| 0 0 1 | 0 1 1 |
con avail = {0, 0, 0}. Calcola need = max - alloc e poi ripete un ciclo "scorri tutti i processi non finiti, se need[i] <= avail aggiungi alloc[i] ad avail e segna finish[i]" finché in un giro nessun processo finisce. Stampa SAFE sequence e la sequenza se tutti finiscono, altrimenti UNSAFE system.
Esito sul dato iniziale. : , , . Con avail nessuno ha (ognuno vuole almeno un'istanza): UNSAFE.
Task 3. Si modifica lo stato iniziale. La soluzione del corso (banker_new.c) cambia solo le prime due righe di alloc: e . Controllo: ora , , . Con avail : ha bisogno di nulla, finisce e restituisce ; ora vuole e manca; ma () finisce: avail ; infine (). Stato SAFE con la sequenza .
Task 4: due grafi (nello stesso formato, avail ricavato da quanto non è assegnato).
- Grafo A ( e con 2 istanze ciascuna; assegnata a e ; assegnata a e ; richieste e ). Allocation (colonne ): , , , ; Max Allocation richieste: , , , ;
avail. () finisce:avail; () finisce: ; (), poi . SAFE, sequenza . Il grafo ha un ciclo () ma con più istanze non è deadlock. - Grafo B (cinque risorse a istanza singola e cinque thread). Dal disegno: , , , , (assegnazioni); , , , , , (richieste). Tutte le risorse sono assegnate:
avail. Solo ha e finisce, liberando ; nessun altro può proseguire ( vuole anche e , vuole , vuole , vuole ). UNSAFE: c'è il ciclo , risorse a istanza singola, quindi è un deadlock vero tra (e , che attende di , è bloccato dietro di loro).
I due stati sono stati verificati con un programma Python che ripete lo stesso algoritmo.
Implementazione completa con le due funzioni
Il programma seguente generalizza banker.c: usa funzioni, accetta le matrici da costanti e include anche l'algoritmo di richiesta (request). È compilato con zig cc -Wall -Wextra -Wpedantic per Linux senza avvisi; non è stato eseguito qui (binari Linux su Windows), ma lo stesso algoritmo in Python dà i risultati attesi.
#include <stdio.h>
#include <string.h>
#define N 5 /* numero di processi */
#define M 3 /* numero di tipi di risorsa */
static int available[M] = {3, 3, 2};
static int max_[N][M] = {{7, 5, 3}, {3, 2, 2}, {9, 0, 2}, {2, 2, 2}, {4, 3, 3}};
static int alloc[N][M] = {{0, 1, 0}, {2, 0, 0}, {3, 0, 2}, {2, 1, 1}, {0, 0, 2}};
static void need_of(int need[N][M])
{
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
need[i][j] = max_[i][j] - alloc[i][j];
}
/* Algoritmo di sicurezza: ritorna 1 se lo stato e' sicuro e scrive la sequenza in seq[]. */
static int is_safe(const int avail[M], int seq[N])
{
int need[N][M], work[M], finish[N] = {0}, count = 0;
need_of(need);
memcpy(work, avail, sizeof work);
while (count < N) { /* al piu' N giri */
int found = 0;
for (int i = 0; i < N; i++) { /* N processi */
if (finish[i])
continue;
int ok = 1;
for (int j = 0; j < M; j++) /* M risorse */
if (need[i][j] > work[j]) {
ok = 0;
break;
}
if (ok) {
for (int j = 0; j < M; j++)
work[j] += alloc[i][j]; /* il processo termina e restituisce tutto */
finish[i] = 1;
seq[count++] = i;
found = 1;
}
}
if (!found)
return 0; /* nessuno puo' finire: stato non sicuro */
}
return 1;
}
/* Algoritmo di richiesta: 1 = concessa, 0 = il processo deve attendere, -1 = errore. */
static int request(int i, const int req[M])
{
int need[N][M], seq[N];
need_of(need);
for (int j = 0; j < M; j++)
if (req[j] > need[i][j])
return -1; /* supera il massimo dichiarato */
for (int j = 0; j < M; j++)
if (req[j] > available[j])
return 0; /* risorse non disponibili */
int new_avail[M]; /* simula l'assegnazione */
for (int j = 0; j < M; j++) {
new_avail[j] = available[j] - req[j];
alloc[i][j] += req[j];
}
int safe = is_safe(new_avail, seq);
if (safe) {
memcpy(available, new_avail, sizeof available); /* conferma */
return 1;
}
for (int j = 0; j < M; j++) /* annulla la simulazione */
alloc[i][j] -= req[j];
return 0;
}
int main(void)
{
int seq[N];
if (is_safe(available, seq)) {
printf("stato SICURO, sequenza:");
for (int i = 0; i < N; i++)
printf(" P%d", seq[i]);
printf("\n");
} else {
printf("stato NON sicuro\n");
}
int r1[M] = {1, 0, 2};
int r2[M] = {3, 3, 0};
int r3[M] = {0, 2, 0};
printf("P1 chiede (1,0,2): %d\n", request(1, r1)); /* atteso 1 */
printf("P4 chiede (3,3,0): %d\n", request(4, r2)); /* atteso 0: non disponibile */
printf("P0 chiede (0,2,0): %d\n", request(0, r3)); /* atteso 0: stato non sicuro */
return 0;
}Il main ripete l'esempio delle slide: totali , , con cinque thread. Risultati attesi (calcolati a mano e con Python): stato iniziale sicuro con sequenza (la scansione per indice ne trova una, diversa da quella delle slide , che è pure valida); la richiesta di è concessa; la richiesta di deve attendere perché dopo la prima available ; la richiesta di è rifiutata perché lo stato sarebbe non sicuro.
Complessità
Nell'algoritmo di sicurezza:
- ogni "giro" scorre i processi; per ciascuno non finito controlla fino a componenti: per giro;
- ogni giro ha successo (almeno un processo finisce) oppure l'algoritmo si ferma: ci sono al più giri;
- in totale .
L'algoritmo di richiesta aggiunge per i controlli e una chiamata alla sicurezza: il costo resta . È un costo che si paga a ogni richiesta, perciò in pratica pochi sistemi lo usano (anche perché richiede di conoscere in anticipo i massimi). Una versione che, dopo aver finito un processo, non riparte dall'inizio ma tiene una lista dei processi pronti può ridurre il termine a per giro, ma il caso peggiore resta quadratico in a meno di strutture dati più elaborate.
Domande d'esame
Qual è la complessità del controllo di sicurezza del banchiere con processi e risorse? Traccia. : al più giri, ognuno scorre processi e confronta componenti.
Versione ripasso
banker.cdel corso: alloc max ; , ; , ;avail-> UNSAFE (Need , , ). Teoria: Deadlock - condizioni, prevenzione, evitamento e rilevamentoUn deadlock (stallo) è una situazione in cui un insieme di processi attende all'infinito risorse possedute da altri processi dello stesso insieme; può verificarsi solo se valgono insieme quattro condizioni (mutua esclusione, hold and wait, assenza di prelazione, attesa circolare); il grafo di allocazione delle risorse (archi di richiesta P->R e di assegnazione R->P) senza cicli esclude lo stallo, con un ciclo lo implica se ogni risorsa ha una sola istanza, e lo rende solo possibile se ha più istanze; si può prevenire (negare una delle quattro condizioni, per esempio ordinare le risorse per negare l'attesa circolare), evitare (stato sicuro: esiste una sequenza sicura; algoritmo del grafo con archi di rivendicazione per istanza singola, algoritmo del banchiere con Available, Max, Allocation, Need = Max - Allocation e complessità O(m n^2) per istanze multiple), rilevare e ripristinare (grafo wait-for per istanza singola, algoritmo di rilevamento per istanze multiple; terminazione dei processi o prelazione delle risorse con scelta della vittima e rollback), oppure ignorare.Deadlock - condizioni, prevenzione, evitamento e rilevamento →.- Task 3:
banker_new.cmette alloc , : SAFE, sequenza . - Task 4: grafo A (multi-istanza): SAFE (ciclo ma non deadlock); grafo B (istanza singola, avail 0): solo finisce, UNSAFE, deadlock di .
- Codice:
is_safe(avail, seq)ripete: scorri i non finiti, seneed <= worksommaalloce segna finito; se un giro non trova nessuno -> non sicuro.request(i, req):req <= need(errore se no),req <= available(attende se no), simula, seis_safeconferma altrimenti annulla. - Complessità: al più giri processi componenti = .
- Strutture del programma:
static int available[M],max_[N][M],alloc[N][M];need_of()calcolamax_ - alloc;is_safe(avail, seq)copiaavailinwork, ripete i giri e ritorna 1 secount == N;request(i, req)ritorna (concessa), (attende), (errore). - Esempio delle slide (, Available ): sicuro; scansione per indice (valida come );
request(1,(1,0,2))1;request(4,(3,3,0))0 (non disponibile);request(0,(0,2,0))0 (unsafe). - Task 4, grafo A: a 2 istanze; hanno , hanno ; richieste , ;
avail=(0,0)SAFE . Grafo B: 5 risorse singole,avail=0; solo () finisce UNSAFE (ciclo ). - Costo: per richiesta; richiede i massimi in anticipo: per questo è poco usato nei sistemi reali.
- Errori tipici: non annullare la simulazione se non è sicura; confrontare con
availablee non conwork; dimenticare di aggiornareneed.