Domanda: Spiega il funzionamento del comando find in UNIX, descrivendo la sintassi generale, la differenza tra test e azioni, e fornisci esempi pratici di utilizzo con opzioni come -name, -type, -size e -exec. Confronta inoltre find con locate, evidenziando vantaggi e svantaggi di entrambi gli approcci.
Risposta:
Il comando find è uno strumento fondamentale per la ricerca di file e cartelle all’interno del filesystem UNIX. A differenza di altre utility di ricerca, find offre una sintassi complessa ma potente che permette di effettuare ricerche basate su molteplici proprietà dei file.
“Il comando
findpermette di trovare file e cartelle all’interno del sistema. Questo comando permette di effettuare la ricerca combinando dei test sulle proprietà dei file, che siano filename, file type, owner, permessi, timestamp,…”Fonte: Introduzione ai comandi UNIX
Sintassi Generale
La sintassi del comando è la seguente:
find [path1...] [espressione]
Il parametro path specifica i percorsi in cui effettuare la ricerca. È importante sottolineare che la ricerca avviene soltanto nei percorsi specificati. L’espressione descrive i criteri di ricerca e le eventuali azioni da eseguire sui file trovati.
Struttura delle Espressioni
Le espressioni di find sono composte da quattro tipi di elementi:
true o falsetrue se hanno successotruetrue“Gli elementi di una espressioni sono collegati da operatori, ad esempio
-oindicaORe-aindicaAND. In caso non siano specificati operatori, l’utilizzo dell’operatoreANDè implicito per collegare due espressioni.”Fonte: Introduzione ai comandi UNIX
Test Principali
Dal file Introduzione ai comandi UNIX:
-name pattern: ricerca basata sul nome del file. Il pattern può includere metacaratteri e deve essere scritto tra apici per evitarne l’espansione-type [dfl]: ricerca basata sul tipo di file (d per directory, f per file regolari, l per symbolic link)-size [+-]n[ckMG]: ricerca basata sulla dimensione. Il prefisso [+-] indica se cercare file maggiori o minori della dimensione specificata, mentre [ckMG] rappresenta l’unità di misura (byte, kilobyte, megabyte, gigabyte)-user utente: cerca file appartenenti a un utente specifico (per UID o username)-group gruppo: cerca file appartenenti a un gruppo specifico (per GID o groupname)-perm [-/]mode: ricerca basata sui permessi del fileAzioni sui File Trovati
Le azioni più comuni sono:
-delete: elimina i file trovati, ritorna true in caso di successo-exec command \;: esegue un comando sui file trovati. Gli argomenti dopo command sono considerati parte del comando fino al carattere \;. La stringa {} rappresenta il nome del file corrente“È importante sottolineare che questi comandi vanno inseriti dopo i test, altrimenti avranno effetto su tutti i file”
Fonte: Introduzione ai comandi UNIX
Esempi Pratici
# Cerca tutti i file .txt nella directory corrente
find . -name "*.txt"
# Cerca tutte le directory nella home dell'utente
find ~ -type d
# Cerca file più grandi di 10MB con permessi di scrittura per il proprietario
find . -size +10M -perm -u=w
# Cerca file dell'utente pippo e crea una lista in list.txt
find . -size +10M -perm -u=w -user pippo -exec echo {} >> list.txt \;
Confronto: find vs locate
Il comando locate offre un approccio alternativo alla ricerca di file:
locate [options] file1 file2 ....
“
locatepermette di ricercare un file specificato sfruttando un database aggiornato periodicamente dal sistema. È possibile forzare l’aggiornamento del database tramite comandosudo updatedb.”Fonte: Introduzione ai comandi UNIX
Vantaggi di locate:
find poiché interroga un database pre-costruitoSvantaggi di locate:
findfind, locate non è sempre pre-installato sui sistemi UNIXVantaggi di find:
Svantaggi di find:
Approfondimenti:
La scelta tra find e locate dipende dalle esigenze specifiche: locate è ideale per ricerche rapide di file per nome, mentre find è la scelta migliore quando servono criteri di ricerca complessi, quando è necessaria la certezza di risultati aggiornati, o quando si devono eseguire azioni sui file trovati. L’importante ricordare che find è più appropriato per script automatizzati e operazioni critiche dove l’accuratezza è fondamentale.
Domanda: Spiega il sistema di permessi di accesso al filesystem in UNIX, descrivendo le tre classi di utenti (Owner, Group Owner, Others) e i relativi permessi (r, w, x) per file e directory. Illustra inoltre il funzionamento dei permessi speciali SUID e SGID, spiegando come vengono rappresentati sia in formato simbolico che ottale, e come influenzano i privilegi di un processo attraverso gli identificatori EUID e EGID.
Risposta:
Il sistema di permessi di accesso al filesystem UNIX rappresenta uno dei meccanismi fondamentali per la protezione e la sicurezza dei dati. Permette di controllare in modo granulare chi può accedere a quali risorse e con quali modalità.
Struttura del Sistema dei Permessi
“I file all’interno del filesystem sono presenti numerosi file sensibili, protetti dall’accesso degli utenti casuali. Il meccanismo dei permessi ne gestisce l’accesso.”
Fonte: Utenti e Gruppi
Per ogni file o directory nel filesystem vengono definiti due attributi fondamentali:
Questo porta alla definizione di tre classi di utenti per ciascun file:
Permessi per i File
Per i file regolari, a ciascuna classe vengono applicati tre tipi di permessi:
r (Read): permette di leggere il contenuto del filew (Write): permette di modificare il contenuto del file. È importante notare che non permette di cancellare il file (questa operazione dipende dai permessi sulla directory che lo contiene)x (eXecute): permette di eseguire il file come programma“Quando un utente prova ad utilizzare un file, vengono applicati i permessi specifici della categoria alla quale egli appartiene.”
Fonte: Utenti e Gruppi
Permessi per le Directory
Per le directory, gli stessi permessi assumono significati diversi:
r (Read): permette di leggere il contenuto della cartella (elenco dei file). Se negato, non è possibile utilizzare il comando ls sulla directoryw (Write): permette di modificare il contenuto della cartella (aggiunta, rimozione e rinomina di file)x (eXecute): permette di attraversare la cartella. Se negato, non è possibile utilizzare cd per entrare nella directoryVisualizzazione e Rappresentazione dei Permessi
Utilizzando il comando ls -l, i permessi vengono visualizzati nel seguente formato:
┌─── Tipo (d=directory, -=file, l=link)
│
│ ┌────────── Permessi Owner
│ │ ┌─────── Permessi Group Owner
│ │ │ ┌──── Permessi Others
│ │ │ │
│┌┴┐┌┴┐┌┴┐
drwxr-xr-x 2 owner_name group_owner_name 512 2008-11-04 16:58 nome
Rappresentazione Ottale:
“Le triple dei permessi sono codificate in cifre in base 8, ottenute sommando: 1 se è permessa l’esecuzione, 2 se è permessa la scrittura, 4 se è permessa la lettura”
Fonte: Utenti e Gruppi
Esempi:
777: tutti i permessi a tutti (rwxrwxrwx)750: tutti i diritti all’owner, lettura ed esecuzione al group owner, niente agli others (rwxr-x—)644: lettura e scrittura all’owner, solo lettura a group owner e others (rw-r–r–)Modifica dei Permessi
Il comando chmod permette di modificare i permessi, ed è utilizzabile dal root o dall’owner del file:
chmod +x file.txt # aggiunge permessi di esecuzione a TUTTI
chmod u-x file.txt # rimuove permessi di esecuzione all'OWNER
chmod g-x file.txt # rimuove permessi di esecuzione al GROUP OWNER
chmod o=x file.txt # assegna SOLO esecuzione agli OTHERS
chmod 755 file.txt # imposta i permessi in formato ottale
chmod -R 755 directory/ # applica ricorsivamente
Permessi Speciali: SUID e SGID
Oltre ai permessi standard, esistono due permessi speciali particolarmente importanti:
“Oltre a questi permessi, ne esistono altri due ‘speciali’ che vengono acquisiti durante l’esecuzione:
SUID: il processo acquisisce i privilegi dell’owner invece di quelli di chi lo esegue.SGID: il processo acquisisce i privilegi del group owner invece di quelli del gruppo di chi lo esegue.”Fonte: Utenti e Gruppi
Rappresentazione Simbolica:
x, si utilizza s (o S se il permesso di esecuzione non è presente)
-rwsr-xr-x (comando /usr/bin/passwd)-rwxr-sr-xRappresentazione Ottale:
Per i permessi speciali si aggiunge una cifra antecedente alle tre cifre standard:
4 se è attivo SUID2 se è attivo SGIDEsempi:
4755: SUID attivo, rwsr-xr-x2755: SGID attivo, rwxr-sr-x6754: SUID e SGID attivi, rwsr-sr–EUID e EGID: Privilegi di un Processo
I permessi speciali sono strettamente collegati al concetto di identificatori effettivi di un processo. Dal file Processi, sappiamo che un processo ha diversi identificatori:
“I privilegi di un processo dipendono da due parametri:
- Effective User ID
EUID- Effective Group ID
EGID”Fonte: Utenti e Gruppi
Funzionamento Normale:
“Quando un processo viene eseguito, normalmente
<EUID, EGID>corrispondono rispettivamente all’UIDdell’utente che ha eseguito il processo e alGIDdel gruppo principale di tale utente.”Fonte: Utenti e Gruppi
Con SUID/SGID:
“Per permettergli di eseguire con privilegi diversi, è possibile impostare i permessi
SUIDeSGID.”Fonte: Utenti e Gruppi
Quando un file eseguibile ha il bit SUID attivo:
EUID del processo diventa l’UID dell’owner del file, invece dell’UID dell’utente che lo esegueAnalogamente, con il SGID attivo:
EGID diventa il GID del group owner del fileEsempio Pratico: il comando passwd
Il comando passwd è un esempio classico di utilizzo di SUID:
ls -l /usr/bin/passwd
# Output: -rwsr-xr-x 1 root root 68208 ... /usr/bin/passwd
“Il comando
passwdpermette di cambiare file di sistema pur non avendo i privilegi disu.”Fonte: Utenti e Gruppi
Grazie al bit SUID, quando un utente normale esegue passwd, il processo acquisisce temporaneamente i privilegi di root (owner del file), permettendogli di modificare il file /etc/shadow che normalmente non sarebbe accessibile.
Approfondimenti:
Il sistema di permessi UNIX rappresenta un equilibrio tra sicurezza e funzionalità. I permessi SUID/SGID sono potenti ma devono essere usati con cautela, poiché rappresentano potenziali vettori di attacco se applicati a programmi non sicuri. Per questo motivo, solo root può modificare l’owner di un file tramite il comando chown, mentre chgrp può essere usato dal proprietario solo se appartiene al gruppo di destinazione.
Domanda: Spiega il meccanismo della system call fork() in UNIX, descrivendo come vengono creati i processi figli, cosa ereditano dal padre (codice, dati, file descriptors), e perché la funzione restituisce valori diversi al padre e al figlio. Illustra inoltre come il padre può attendere la terminazione dei figli mediante wait(), spiegando il concetto di stato zombie e come viene gestito il parametro status per determinare se un processo è terminato volontariamente o involontariamente.
Risposta:
La system call fork() rappresenta il meccanismo fondamentale per la creazione di nuovi processi nei sistemi UNIX. È uno degli elementi distintivi della programmazione di sistema UNIX e permette la realizzazione di applicazioni multiprogrammate.
Creazione di Processi con fork()
“Ogni processo è in grado di creare dinamicamente processi tramite la chiamata di sistema
fork. Il processo creato, detto figlio, ha uno spazio di dati separato, ma condivide con il processo che lo ha creato, detto padre, il codice.”Fonte: Processi
La funzione è definita come segue:
/**
* La funzione non richiede parametri
* Restituisce un risultato intero DIVERSO a padre e figlio
*/
pid_t fork(void)
Cosa Eredita il Figlio
Al momento della creazione, il processo figlio riceve dal padre:
“Poiché il processo figlio condivide il codice con il padre, ne eredita una copia delle aree dati globali (stack, heap, User Structure, …). Ciò significa che alla creazione il figlio ha il proprio
%RIPche punta alla all’istruzione successiva allaCALL fork”Fonte: Processi
Per quanto riguarda i file, dal file Filesystem:
“Quando viene generato un processo figlio, questo erediterà una copia di tutte le strutture dati del padre, in particolare anche la
User Structuree 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.”
Questo significa che padre e figlio possono utilizzare i file già aperti dal padre, condividendo la posizione di lettura/scrittura.
Valori di Ritorno Diversi
Una caratteristica peculiare di fork() è che restituisce valori diversi a padre e figlio:
“La funzione è però progettata affinché restituisca un risultato intero diverso a padre e figlio:
- Padre: restituisce il
PIDdel figlio, o un valore negativo in caso di fallimento- Figlio: restituisce
0”Fonte: Processi
Questo meccanismo permette di discriminare il comportamento di padre e figlio attraverso una semplice struttura condizionale:
pid_t pid;
pid = fork();
if(pid < 0){
// Errore nella fork
perror("Errore fork");
exit(-1);
}
else if(pid == 0){
// Codice del FIGLIO
printf("Sono il figlio, PID: %d\n", getpid());
}
else{
// Codice del PADRE
printf("Sono il padre, mio figlio ha PID: %d\n", pid);
}
Attesa della Terminazione: wait()
Il processo padre può sincronizzarsi con i figli attendendo la loro terminazione mediante la system call wait():
/**
* Il padre si mette in attesa della terminazione del figlio
* @param status puntatore a intero che contiene lo stato di terminazione del figlio
* @return il PID del figlio che è terminato, valore negativo se non ha processi figli
*/
pid_t wait(int* status)
“La sospensione del padre accade solo se tutti i figli sono ancora in esecuzione. Nel caso in cui almeno un figlio è terminato, la funzione ritorna immediatamente le informazioni di terminazione.”
Fonte: Processi
Stato Zombie
Il concetto di stato zombie è cruciale per comprendere il funzionamento di wait():
“Questo è possibile grazie all’esistenza dello stato
zombie. I processi figli che terminano infatti entrano nello stato dizombie, proprio per permettere al padre di ottenere le informazioni sulla terminazione di quest’ultimo.”Fonte: Processi
Un processo zombie è un processo che ha completato l’esecuzione ma il cui descrittore (PCB) rimane nella tabella dei processi per permettere al padre di recuperare le informazioni sulla sua terminazione. Solo dopo che il padre ha chiamato wait(), il sistema può liberare completamente le risorse del processo figlio.
Gestione del Parametro status
Il parametro status passato a wait() contiene informazioni dettagliate sulla terminazione del figlio:
“La variabile
statuscontiene diverse informazioni su come il figlio è terminato, oltre allo stato di terminazione eventualmente fornito dal figlio stesso. Se il byte meno significativo di*statusfosse0, allora la terminazione è stata volontaria e il byte più significativo contiene lo stato di terminazione.”Fonte: Processi
Macro per analizzare status:
Per gestire status in modo portabile, la libreria <sys/wait.h> fornisce delle macro:
WIFEXITED(status): restituisce true se il processo è terminato volontariamente (tramite exit() o ritorno da main)WEXITSTATUS(status): restituisce lo stato di terminazione passato dal figlio tramite exit()Esempio pratico:
int status;
pid_t pid = fork();
if(pid == 0){
// Figlio
printf("Figlio in esecuzione\n");
exit(42); // Terminazione volontaria con codice 42
}
else{
// Padre
wait(&status);
if(WIFEXITED(status)){
printf("Figlio terminato volontariamente\n");
printf("Codice di uscita: %d\n", WEXITSTATUS(status));
}
else{
printf("Figlio terminato involontariamente\n");
}
}
Terminazione Volontaria vs Involontaria
Un processo può terminare in due modi:
“Un processo può terminare in due modi:
- Involontariamente: accade in caso di azioni illegali (ad esempio segmentation fault o divisioni per zero) oppure in caso di interruzioni causate dalla ricezione di segnali
- Volontariamente: quando si esegue l’ultima istruzione o viene chiamata la system call
exit()”Fonte: Processi
La system call exit() è definita così:
/**
* È una chiamata senza ritorno che permette di terminare volontariamente un processo.
* @param status permette di comunicare al padre lo stato di terminazione
*/
void exit(int status)
Approfondimenti:
Il meccanismo fork()-wait() è alla base della programmazione di sistema UNIX. È importante notare che i processi zombie non consumano risorse significative (solo l’entry nella tabella dei processi), ma un accumulo eccessivo può esaurire la tabella dei processi. Un padre che non chiama wait() sui propri figli crea “orfani zombie”. Se il padre termina prima di chiamare wait(), i figli zombie vengono adottati dal processo init (PID=1) che si occupa di ripulirli. Questo meccanismo garantisce che nessun processo zombie rimanga indefinitamente nel sistema.
Domanda: Spiega il concetto di multiprogrammazione e time-sharing nei sistemi operativi moderni. Definisci cosa sono i CPU-burst e gli IO-burst, illustrando con un esempio come la multiprogrammazione migliora l’utilizzo della CPU rispetto all’esecuzione sequenziale. Confronta infine il paradigma time-sharing con la multiprogrammazione classica, spiegando il ruolo del quanto di tempo e del cambio di contesto, e descrivi quando e perché questi meccanismi comportano un overhead nel sistema.
Risposta:
La multiprogrammazione e il time-sharing rappresentano due paradigmi fondamentali che hanno rivoluzionato l’utilizzo dei sistemi di elaborazione, permettendo di sfruttare al massimo le risorse hardware e di fornire un’esperienza interattiva agli utenti.
Nascita della Multiprogrammazione
L’evoluzione verso i sistemi multiprogrammati nasce da un’osservazione critica sull’efficienza dei mainframe:
“Venne osservato che l’efficienza dei mainframe, che avevano costi nell’ordine dei milioni di euro, era tendenzialmente bassa. Questo accadeva perché le risorse, a causa di come venivano gestiti i programmi, venivano utilizzate in media meno della metà del loro potenziale.”
Fonte: Concetti Introduttivi
Per risolvere questo problema venne introdotta la multiprogrammazione:
“Per riuscire a ottenere un drastico miglioramento nell’efficienza di uso delle risorse della macchina fu realizzata la tecnica della multiprogrammazione. Questa tecnica permetteva a più programmi di venire caricati in memoria in parallelo, gestendoli in modo concorrente.”
Fonte: Concetti Introduttivi
CPU-Burst e IO-Burst
Per comprendere come funziona la multiprogrammazione, è necessario introdurre due concetti fondamentali:
“Nella multiprogrammazione andiamo quindi ad identificare due momenti durante l’esecuzione di un processo:
- CPU-burst: intervalli di tempo nel quale un processo deve eseguire istruzioni e necessita di occupare la CPU
- IO-burst: intervalli di tempo nel quale un processo deve eseguire un’operazione di IO e non deve utilizzare la CPU”
Fonte: Concetti Introduttivi
Questi due concetti sono cruciali perché permettono di capire che durante un IO-burst, la CPU rimarrebbe inutilizzata se non ci fosse un meccanismo per assegnarla ad un altro processo.
Confronto: Esecuzione Sequenziale vs Multiprogrammazione
Dal file Concetti Introduttivi possiamo vedere un esempio concreto:
“Nell’esecuzione sequenziale i processi vengono eseguiti in ordine di arrivo, e vengono eseguiti dall’inizio alla fine. In questo modo notiamo che sono presenti diverse unità di tempo dove la CPU è in attesa di qualcosa. Possiamo calcolare l’efficienza di questo esempio, che è di $\frac{10}{27} \approx 37\%$”
In un sistema sequenziale, quando un processo entra in attesa per un’operazione di I/O, la CPU rimane inattiva sprecando cicli preziosi.
“Nell’esecuzione multi-tasking invece, quando il primo processo va in IO-burst, e si mette in attesa, la CPU inizia a lavorare prima sul secondo processo, e poi sul terzo quando anche il secondo va in attesa. Durante i momenti nei quali i vari programmi sono sospesi, questi vengono poi recuperati nell’ordine in cui ricevono i dati che attendono, e verranno messi in esecuzione quando colui che occupa l’esecuzione terminerà e/o andrà nuovamente in attesa. Possiamo quindi calcolare anche in questo caso l’efficienza, che stavolta è di $\frac{10}{12} \approx 83\%$”
Fonte: Concetti Introduttivi
Questo esempio dimostra come la multiprogrammazione permetta di più che raddoppiare l’efficienza del sistema, passando dal 37% all’83%.
Meccanismi della Multiprogrammazione
La nuova gestione richiede l’introduzione di algoritmi di scheduling:
“Tuttavia, questa nuova gestione dei programmi creò la necessità dell’implementazione di algoritmi di scheduling, che permettevano alla CPU di eseguirli uno alla volta e di sostituirli quando venivano messi in attesa, così da ridurre il più possibile i “tempi morti”.”
Fonte: Concetti Introduttivi
Un elemento chiave è l’introduzione della preemption:
“I primi algoritmi di scheduling deviarono dal principio di first-come-first-serve, e introdussero il concetto di interruzione. Non si lasciava più l’accesso alla CPU ad un programma per tutto il suo time-to-live, ma si riservava la possibilità sostituirlo anche durante la sua eseguzione qual’ora questo fosse andato in attesa, così da permettere ad un altro programma di sfruttare quei cicli che sarebbero altrimenti stati sprecati.”
Fonte: Concetti Introduttivi
Time-Sharing: Un Passo Ulteriore
Il paradigma time-sharing rappresenta un’evoluzione della multiprogrammazione:
“Sono sistemi che hanno come primo obiettivo quello di dividere il tempo d’uso della CPU tra i vari processi.”
Fonte: Concetti Introduttivi
La differenza fondamentale è espressa chiaramente:
“Mentre nei sistemi multiprogrammati quando la CPU viene assegnata ad un processo, gli altri non la possono utilizzare finché questo non termina la sua cpu-burst, nel paradigma time-sharing la CPU è assegnata ad ogni processo per un quanto di tempo uguale e predeterminato per tutti.”
Fonte: Concetti Introduttivi
Il Quanto di Tempo e la Preemption
Nel time-sharing, il quanto di tempo determina il comportamento del sistema:
“La politica quindi diventa la seguente:
- Se durante il quanto di tempo hai terminato la cpu-burst vai in attesa fino al prossimo cpu-burst
- Se durante il quanto di tempo non hai terminato, il tuo stato intermedio viene salvato e verrà ripristinato quando tornerà il tuo turno. Questa revoca viene chiamata preemption.”
Fonte: Concetti Introduttivi
Il Cambio di Contesto
Il salvataggio e ripristino dello stato è un’operazione critica:
“Il salvataggio e ripristino dello stato intermedio corrispondono a tutti gli effetti al cambio di contesto che avevamo visto nel corso di [Calcolatori Elettronici].”
Fonte: Concetti Introduttivi
L’Overhead del Sistema
Un aspetto cruciale da considerare è il costo della multiprogrammazione:
“La multiprogrammazione non è però gratuita. Il costo di migliorare i tempi si chiama overhead, e consiste nel tempo aggiuntivo usato dall’OS per eseguire il codice aggiuntivo introdotto dalle operazioni intermedie, come l’algoritmo di schedulazione, l’algoritmo di cambio di contesto, …”
Fonte: Concetti Introduttivi
L’overhead rappresenta tempo sottratto all’esecuzione dei programmi:
“Questo tempo è a tutti gli effetti sottratto dall’esecuzione dei programmi applicativi. Per poter vedere un guadagno nei tempi di esecuzione è quindi necessario che l’overhead sia contenuto. indicativamente attorno all’ $1\%/2\%$.”
Fonte: Concetti Introduttivi
Le conseguenze di un overhead eccessivo possono essere gravi:
“Se avessimo infatti overhead del $70\%$ del tempo totale, potrebbe non essere conveniente avere un sistema multiprogrammato. Se fosse ancora più alto il sistema potrebbe persino andare in crash, in quanto impiegherebbe tutto il tempo a eseguire le istruzioni di overhead e non avrebbe più tempo per eseguire i programmi applicativi.”
Fonte: Concetti Introduttivi
Approfondimenti:
La scelta del quanto di tempo nel time-sharing è un compromesso delicato. Un quanto troppo piccolo aumenta la frequenza dei cambi di contesto, incrementando l’overhead. Un quanto troppo grande riduce la reattività del sistema e lo fa avvicinare al comportamento di un sistema multiprogrammato classico.
Un esempio notevole di time-sharing è l’algoritmo Round-Robin (RR), menzionato nel file Concetti Introduttivi, che rappresenta uno degli algoritmi di scheduling più utilizzati per i sistemi interattivi.
La memoria divenne rapidamente il bottleneck di questi sistemi, portando all’introduzione della gestione dinamica della memoria attraverso meccanismi di swap e alla virtualizzazione della memoria, argomenti che approfondiremo nelle domande successive sulla gestione della memoria.
Domanda: Spiega la Tassonomia di Flynn per la classificazione delle architetture dei sistemi di elaborazione. Descrivi in dettaglio le quattro categorie principali (SISD, SIMD, MISD, MIMD), illustrando per ciascuna le caratteristiche distintive, esempi di applicazione e vantaggi/svantaggi. Confronta inoltre le architetture SIMD e MIMD in termini di costo hardware, memoria richiesta, flessibilità e modelli computazionali supportati, spiegando infine la differenza tra macchine MIMD a memoria distribuita (DM-MIMD) e a memoria condivisa (SM-MIMD).
Risposta:
La Tassonomia di Flynn rappresenta uno degli schemi di classificazione più utilizzati e riconosciuti per categorizzare le architetture dei sistemi di elaborazione, basandosi sulla capacità del sistema di gestire flussi multipli di istruzioni e dati.
La Tassonomia di Flynn
“È un metodo di classificazione dei sistemi di elaborazione, che utilizza due punti di vista:
- La capacità del sistema di avere più flussi di istruzioni
- La capacità del sistema di avere più flussi di dati”
Questa classificazione genera quattro categorie principali, rappresentabili in una matrice 2×2:
| Single Instruction (SI) | Multiple Instruction (MI) | |
|---|---|---|
| Single Data (SD) | SISD | MISD |
| Multiple Data (MD) | SIMD | MIMD |
1. Macchine SISD (Single Instruction Single Data)
“Sono macchine a singolo stream, che rappresentano le tradizionali macchine sequenziali basate sul modello di Von Neumann usata da tutti i calcolatori convenzionali.”
Le macchine SISD rappresentano l’architettura più semplice: un singolo processore esegue una singola istruzione alla volta su un singolo dato. Sono caratterizzate da:
Esempi: I computer personali tradizionali con un singolo core rappresentano macchine SISD.
2. Macchine SIMD (Single Instruction Multiple Data)
“Si differenzia dalle macchine SISD per il numero di Data Processor, ciascuno dei quali possiede una propria Data Memory. Questo permette a più unità di elaborazione di eseguire contemporaneamente la stessa istruzione, lavorando su flussi di dati differenti.”
Caratteristiche distintive delle SIMD:
“La topologia di interconnessione tra i vari processori può essere sia regolare che creata ad hoc. Questa architettura permette comunicazioni regolari efficienti e poco costose, che non creano conflitti.”
Il modello computazionale è importante:
“Il modello di computazione di queste macchine è Sincrono, ovvero gestito da un unica unità di controllo.”
Questo permette due tipi di parallelismi:
Esempi di architetture SIMD:
Un aspetto interessante è la portabilità:
“I programmi che beneficiano dell’architettura
SIMD, ad esempio per lavorare su grandi vettori, possono essere eseguiti, con opportune ma comunque piccole modifiche, da processoriSISD. Infatti avendo un operazione tra vettoric = a + b, il compilatore può tradurla infor (...) c[i] = a[i] + b[i].”
3. Macchine MISD (Multiple Instruction Single Data)
“Queste macchine hanno più flussi di istruzioni che lavorano contemporaneamente su un unico flusso di dati.”
La categoria MISD è particolarmente interessante:
“Molti considerano questa categoria ancora ‘vuota’, ovvero senza esempi reali. Altri invece categorizzano i processori basati su pipeline proprio come macchine
MISD.”
Le pipeline dei processori moderni potrebbero essere considerate MISD, dove diverse unità funzionali (fetch, decode, execute) operano contemporaneamente su stadi diversi dello stesso stream di dati.
4. Macchine MIMD (Multiple Instruction Multiple Data)
“In queste macchine abbiamo tante unità di elaborazione connesse a tante unità di dati. Abbiamo infatti più flussi di istruzioni in parallelo che elaborano insiemi di dati che possono essere distinti, privati o condivisi.”
Le macchine MIMD si dividono in due sottocategorie fondamentali:
A. Macchine a Memoria Distribuita (DM-MIMD):
“Ogni coppia
IP-DP(con le relative memoria) costituisce in pratica una macchinaSISD.”
Caratteristiche principali:
“Una qualsiasi rete di calcolatori rappresenta una macchina
DM-MIMD. Infatti queste reti di interconnessione regolari permettono ai nodi di scambiare informazioni secondo il paradigma message passing. Queste reti permettono algoritmi ad elevata località e un elevata scalabilità.”
Due sottotipi importanti:
B. Macchine a Memoria Condivisa (SM-MIMD):
“Sono macchine multiprocessore che permettono la condivisione della memoria tra processori attraverso delle aree. Affinché questa architettura funzioni lo switch NxN deve essere molto efficiente.”
Una limitazione importante:
“A differenza delle
MD-MIMDquesta architettura ha una scalabilità limitata.”
Questo vale quando il numero di processori N è “piccolo” (N < 100), ma l’accoppiamento fra i nodi può essere molto stretto con comunicazioni veloci.
Confronto SIMD vs MIMD
Dal file Classificazione delle Architetture, una tabella comparativa evidenzia le differenze:
| Aspetto | SIMD | MIMD |
|---|---|---|
| Hardware | Poco - Unica Unità di Controllo | Molto - Tante Unità di Controllo |
| Costo | Costoso - Necessita processori specifici | Poco costoso - Processori general-purpose |
| Memoria | Poca - Una sola copia del programma | Tanta - Ogni unità ha una copia del programma |
| Flessibilità | Poca | Alta in termini di modelli computazionali supportati |
Analisi dettagliata:
Hardware: Le SIMD richiedono meno hardware complessivamente perché condividono un’unica unità di controllo, mentre le MIMD necessitano di tante unità di controllo quanti sono i processori.
Costo: Paradossalmente, le SIMD sono più costose perché richiedono processori specializzati per l’elaborazione vettoriale, mentre le MIMD possono utilizzare processori commerciali standard.
Memoria: Nelle SIMD il codice è condiviso (un’unica copia), mentre nelle MIMD ogni processore deve avere la propria copia del codice da eseguire.
Flessibilità: Le MIMD sono molto più flessibili perché ogni processore può eseguire programmi diversi, mentre le SIMD sono limitate ad applicazioni che possono essere espresse come operazioni vettoriali sincrone.
Approfondimenti:
La scelta tra SIMD e MIMD dipende fortemente dall’applicazione. Le SIMD eccellono in:
Le MIMD sono preferibili per:
La maggior parte dei sistemi moderni è ibrida: i processori SISD tradizionali includono estensioni SIMD (SSE, AVX in Intel, NEON in ARM) per operazioni vettoriali, mentre i data center utilizzano architetture MIMD per la scalabilità. Le GPU moderne sono essenzialmente architetture SIMD massive, con migliaia di core che eseguono la stessa istruzione su dati diversi.
Un concetto importante è che le architetture MIMD permettono vera asincronia, mentre le SIMD richiedono sincronizzazione, rendendole più semplici da programmare a livello hardware ma potenzialmente meno efficienti quando i dati hanno comportamenti irregolari o imprevedibili.
Domanda: Confronta gli algoritmi di scheduling FCFS, SJF, SRTF e RR analizzandone il funzionamento, i vantaggi e gli svantaggi. Spiega in particolare la differenza tra algoritmi preemptive e non-preemptive, e come il quanto di tempo influenza le prestazioni del Round-Robin. Illustra inoltre il concetto di starvation e come può essere risolto nei sistemi Multi-Level Feedback Queue.
Risposta:
Lo scheduling della CPU rappresenta uno degli aspetti fondamentali nella gestione dei processi in un sistema operativo multiprogrammato. Gli algoritmi di scheduling determinano quale processo pronto debba essere assegnato alla CPU e per quanto tempo, influenzando direttamente le prestazioni del sistema.
Scheduling e Classificazione degli Algoritmi
Lo scheduling è definito come:
“L’attività mediante la quale il sistema operativo effettua delle scelte tra i processi, riguardo al caricamento in RAM e all’assegnazione della CPU.”
Fonte: Gestione Processi
Esistono tre livelli di scheduling, ma quello che ci interessa è lo scheduling a breve termine:
“A breve termine: è lo scheduling propriamente detto. È la politica con la quale il sistema operativo assegna la CPU ai processi pronti. Interviene quando il processo in esecuzione perde il controllo della CPU”
Fonte: Gestione Processi
Una distinzione fondamentale è tra algoritmi preemptive e non-preemptive:
“Può essere di due tipi:
- Non preemptive scheduling (senza diritto di revoca): il sistema operativo non può revocare la CPU, ma deve essere lui a rilasciarla
- Preemptive scheduling (con diritto di revoca): il sistema operativo può forzare la revoca della CPU ad un processo in base a determiate variabili. (quanti di tempo, priorità, …)”
Fonte: Gestione Processi
Questa distinzione è cruciale: negli algoritmi non-preemptive, un processo mantiene la CPU fino a quando non termina o si sospende volontariamente. Negli algoritmi preemptive, il sistema operativo può forzare il processo a rilasciare la CPU anche se non ha completato la sua esecuzione.
Algoritmo FCFS (First-Come-First-Served)
L’algoritmo FCFS è il più semplice tra gli algoritmi di scheduling:
“Assegna la CPU al processo pronti in attesa da più tempo”
Fonte: Gestione Processi
Funzionamento:
“Quando un processo entra nella coda dei processi
prontiil suo descrittore viene collegato all’ultimo elemento della coda. Quando la CPU è libera viene assegnata al processo il cui descrittore si trova nella testa della coda. È a tutti gli effetti equivalente alla politicaFIFO.”Fonte: Gestione Processi
Vantaggi:
Svantaggi:
“Questo algoritmo è altamente dipendente dall’ordine nel quali i processi arrivano per quanto riguarda i tempi medi di attesa e di overhead.”
Fonte: Gestione Processi
Inoltre:
“Possiamo anche dedurre che questo algoritmo viene pesantemente deabilitato da processi con CPU-Burst grandi che arrivano prima di altri con CPU-Burst breve.”
Fonte: Gestione Processi
Questo fenomeno è noto come convoy effect: processi brevi devono attendere il completamento di processi lunghi, aumentando drammaticamente i tempi medi di attesa.
Algoritmo SJF (Shortest-Job-First)
L’algoritmo SJF rappresenta un miglioramento rispetto a FCFS:
“È un algoritmo a priorità statica non preemptive. La priorità viene assegnata ad ogni processo su base inversa rispetto al suo CPU-Burst”
Fonte: Gestione Processi
Funzionamento: I processi vengono ordinati nella coda dei pronti in base alla durata prevista del loro CPU-Burst, con i processi più brevi che hanno priorità maggiore.
Vantaggi:
Svantaggi:
“In questo algoritmo abbiamo più overhead. Infatti l’inserimento in coda pronti è adesso un inserimento ordinato, perciò ha una complessità $O(n)$.”
Fonte: Gestione Processi
Inoltre:
“Essendo non preemptive, ha la limitazione che nel caso di arrivo di processi con elevati CPU-Burst prima di altri con tempi più bassi, ritorniamo nel problema di
FCFS”Fonte: Gestione Processi
Un altro problema fondamentale è la necessità di conoscere a priori il CPU-Burst, cosa non sempre possibile.
Algoritmo SRTF (Shortest-Remaining-Time-First)
SRTF è l’evoluzione preemptive di SJF:
“È un miglioramento di
SJF, che introduce la possibilità di essere preemptive. Inoltre, proprio per via della preemption, non si guarderà più la CPU-Burst iniziale del processo, ma quella che gli rimane da eseguire.”Fonte: Gestione Processi
Funzionamento: Quando arriva un nuovo processo, il sistema confronta il tempo rimanente del processo in esecuzione con il CPU-Burst del nuovo processo. Se il nuovo processo ha un tempo di esecuzione inferiore, viene effettuato un cambio di contesto.
Dal file Gestione Processi:
“Nel caso un processo con CPU-Burst elevata (100) che però sta per finire (rimanente 2), non ha senso sostituirlo con un altro appena arrivato che magari ha CPU-Burst (50), che è sì più breve di quella iniziale ma molto più alta di quella rimanente.”
Vantaggi:
Svantaggi:
“È un sistema che funziona bene per sistemi statici, dove il numero di processi non varia e i loro CPU-Burst è noto”
Fonte: Gestione Processi
Il problema più grave è la starvation:
“Inoltre questo algoritmo, in caso di sistemi aperti con processi variabili, soffre di starvation. Infatti se arrivano continuamente processi con priorità più alta del primo, questo starà in attesa per un tempo che può diventare lunghissimo.”
Fonte: Gestione Processi
Soluzione alla starvation in SJF/SRTF:
“Per risolvere si utilizzano tecniche di aging, che monitorano i tempi di attesa e modificano opportunamente le priorità per rimediare alla starvation.”
Fonte: Gestione Processi
Stima del CPU-Burst
Un problema comune a SJF e SRTF è stimare il CPU-Burst quando non è noto a priori:
“Non sempre sappiamo a priori il CPU-Burst di un processo. Si utilizza quindi la media esponenziale per stimarlo, tenendo conto della storia dei valori misurati nei precedenti intervalli di esecuzione”
Fonte: Gestione Processi
La formula utilizzata è: $s_{n+1} = a \cdot t_n + (1-a) \cdot s_n$ dove $t_n$ è la durata effettiva e $s_n$ la stima, con fattore $a$ tipicamente pari a $\frac{1}{2}$.
Algoritmo Round-Robin (RR)
Il Round-Robin è progettato specificamente per sistemi a partizione di tempo:
“È un algoritmo progettato appositamente per i sistemi a partizione di tempo, rientrando negli algoritmi preemptive.”
Fonte: Gestione Processi
Funzionamento:
“La coda dei proessi pronti è realizzata come una coda circolare, nella quale ogni processo ottiene la CPU per un quanto di tempo (tipicamente $10ms\sim100 ms$) al termine del quale perde il controllo della CPU e il suo descrittore viene inserito nella coda.”
Fonte: Gestione Processi
La coda viene gestita con modalità FIFO, garantendo equità tra i processi.
Influenza del Quanto di Tempo:
“È un algoritmo particolarmente adatto per i sistemi interattivi, in quanto è in grado di assicurare tempi di risposta abbastaza brevi, determinati esclusivamente da due fattori:
- Il quanto di tempo: il tempo di risposta è teoricamente migliore per piccoli valori del quanto di tempo. Tuttavia in questo caso non possiamo più ignorare il cambio di contesto tra processi. Infatti, cambi troppo frequenti comporterebbero un overhead che potrebbe addirittura diventare più grande del quanto stesso.”
Fonte: Gestione Processi
Questo è un punto cruciale:
Il secondo fattore è:
”- Il numero medio di processi pronti: se fossero presenti tanti processi pronti, potremmo andare incontro a tempi di turnaround molto elevati, anche per processi con CPU-Burst molto brevi, che dovrebbero comunque attendere diversi cicli prima di poter terminare”
Fonte: Gestione Processi
Tempo di attesa massimo in RR:
Conoscendo il numero medio di processi in coda $n_m$, possiamo calcolare: $A_m \le n_m \cdot Q$ dove $Q$ è il quanto di tempo.
Vantaggi:
Svantaggi:
Sistemi Multi-Level Feedback Queue
Per superare i limiti dei singoli algoritmi, i sistemi moderni utilizzano code multiple con feedback:
“All’interno del nostro sistema non utilizzeremo solamente un algoritmo di scheduling, ma implementeremo un sistema che possiede più code, ognuna ordinata con un algoritmo diverso.”
Fonte: Gestione Processi
Esempio di configurazione:
RR(10) - priorità massimaRR(50) - priorità mediaFCFS - priorità minimaGestione della starvation:
“Come sempre, introducendo il concetto di priorità, introduciamo anche la generazione di problemi di starvation nelle code con priorità più bassa, tuttavia abbiamo anche visto come è possibile implementare processi di aging che permettono di aumentare la priorità di un processo, in questo caso cambandone eventualmente la coda.”
Fonte: Gestione Processi
Funzionamento dinamico:
Il sistema operativo inserisce i nuovi processi nella coda di livello più alto. Se un processo non termina entro il suo quanto di tempo:
“Se il CPU-Burst del processo fosse maggiore, la preemption lo sostituisce e lo inserisce nella coda di livello 1.”
Fonte: Gestione Processi
Questo meccanismo permette di:
Preemption per code FCFS:
“Per ovviare a questo problema, rendiamo la coda
FCFSpreemptive per l’inserimento dei nuovi processi. Anche in questo caso il processo rimarrà nella coda di livello 2, ma faremo attenzione ad inserirlo in testa.”Fonte: Gestione Processi
Questo garantisce che anche processi nelle code a bassa priorità possano progredire.
Confronto Complessivo
Dal file Gestione Processi, possiamo classificare gli algoritmi:
| Algoritmo | Tipo | Complessità | Ottimalità | Starvation | Uso pratico |
|---|---|---|---|---|---|
| FCFS | Non-preemptive | $O(1)$ | No | No | Sistemi batch semplici |
| SJF | Non-preemptive | $O(n)$ | Sì (tempo attesa) | Sì | Teorico, batch con CPU-burst noti |
| SRTF | Preemptive | $O(n)$ | Sì (tempo attesa) | Sì | Sistemi embedded statici |
| RR | Preemptive | $O(1)$ | No | No | Sistemi time-sharing, interattivi |
| Multi-Level | Preemptive | Variabile | Bilanciato | Risolvibile | Sistemi operativi moderni |
Approfondimenti:
La scelta dell’algoritmo di scheduling dipende fortemente dal tipo di sistema:
I sistemi operativi moderni come Linux e Windows utilizzano varianti sofisticate di Multi-Level Feedback Queue, combinando priorità dinamiche, aging, e considerazioni sulla natura del processo (I/O-bound vs CPU-bound). Queste implementazioni bilanciano:
Il concetto di starvation è particolarmente importante: un processo che attende indefinitamente rappresenta non solo un problema di performance, ma anche di correttezza del sistema. Le tecniche di aging incrementano gradualmente la priorità dei processi in attesa, garantendo che prima o poi qualsiasi processo ottenga la CPU, indipendentemente dal suo CPU-burst o dalla sua priorità iniziale.
Domanda: Spiega la schedulazione dei sistemi in tempo reale, illustrando le caratteristiche dei processi periodici e la definizione di sistema periodico. Analizza gli algoritmi Rate Monotonic (RM) e Earliest Deadline First (EDF), descrivendo come funzionano, le loro differenze (priorità statica vs dinamica), e il calcolo del fattore di utilizzo della CPU per determinare se un sistema è schedulabile. Fornisci esempi pratici di come vengono assegnate le priorità e gestite le deadline.
Risposta:
La schedulazione dei sistemi in tempo reale rappresenta un ambito specializzato della gestione dei processi, dove oltre all’efficienza nell’uso della CPU è necessario garantire il rispetto di vincoli temporali stringenti. Questi sistemi sono tipici di applicazioni embedded critiche dove il mancato rispetto di una deadline può avere conseguenze catastrofiche.
Caratteristiche dei Sistemi in Tempo Reale
Gli algoritmi di scheduling visti precedentemente funzionano per sistemi generici, ma non si applicano bene a sistemi embedded:
“Gli algoritmi che abbiamo visto fin’ora, per quanto comunque funzionali, non si applicano bene a sistemi embedded dove dobbiamo soddisfare anche altri requisiti. I sistemi embedded infatti sono caratterizzati da un sistema operativo multiprogrammato che elabora parametri in tempo reale.”
Fonte: Gestione Processi
L’architettura di questi sistemi è peculiare:
“I sistemi in tempo reale possono essere rappresentati come sistemi con CPU, RAM, memoria flash e, soprattutto, due classi principali di periferiche: attuatori (output) e sensori (input).”
Fonte: Gestione Processi
Processi Periodici
La caratteristica distintiva di questi sistemi è la periodicità:
“In questi sistemi i sensori inviano periodicamente al sistema operativo misurazioni di dati che è necessario elaborare per poter produrre output agli attuatori.”
Fonte: Gestione Processi
La periodicità dipende dall’applicazione:
“Il periodo con il quale campioniamo i dati dipende dall’oggetto che stiamo misurando. I periodi di campionamento possono variare dall’ordine dei microsecondi (sistemi di bilanciamento, braccia robotiche, …) all’ordine dei secondi (misure di temperatura, pressione, …).”
Fonte: Gestione Processi
Questa periodicità permette di modellare matematicamente il sistema:
“Possiamo quindi sviluppare il nostro sistema tenendo conto del fatto che gli input, e i relativi processi di elaborazione dell’input, sono periodici.”
Fonte: Gestione Processi
Per un processo $i$ con periodo $t_i$:
\[r_{j+1} = r_j + t_i = r_0 + (j+1)\cdot t_i\]dove $r_j$ indica il $j$-esimo inserimento in coda pronti.
Definizione di Sistema Periodico
Un sistema periodico è caratterizzato da:
“Quello che abbiamo descritto è quindi un sistema composto da $N$ processi periodici, ovvero di un sistema periodico.”
Fonte: Gestione Processi
Elementi fondamentali di un processo periodico $i$:
Vincolo temporale critico:
“È tuttavia necessario che il turnaround del processo sia minore di $t_i$. In particolare vogliamo che termini entro una deadline $d_i < t_{i+1}$. Questo avviene perché i risultati che il processo produrrà devono essere trasmessi agli attuatori che dovranno quindi produrre cambiamenti opportunamente.”
Fonte: Gestione Processi
Periodo del sistema complessivo:
Il sistema ha un periodo complessivo definito come:
\[T = \text{mcm}(t_i), \quad \forall i \in N\]Sistemi Hard Real-Time vs Soft Real-Time
La classificazione dipende dalla criticità delle deadline:
“In un sistema hard real time lo scheduler deve garantire che tutte le deadline vengano rispettate, altrimenti si genera un errore fatale. In sistemi soft real time lo scheduler può permettersi che qualche deadline possa non essere rispettata senza la generazioni di errori fatali, ma andando incontro a brevi errori temporanei.”
Fonte: Gestione Processi
Algoritmo Rate Monotonic (RM)
Il Rate Monotonic è un algoritmo a priorità statica:
“È un algoritmo a priorità statica $\quad p(i) \propto \frac{1}{t_i}$”
Fonte: Gestione Processi
Principio di funzionamento:
La priorità è assegnata in modo inversamente proporzionale al periodo: processi con periodo più breve hanno priorità più alta. Questa scelta è intuitiva: un processo che deve essere eseguito più frequentemente deve avere precedenza per non perdere la propria deadline.
Esempio pratico:
Dal file Gestione Processi:
| Periodo $t_i$ | CPU-Burst $C_i$ | Priorità | |
|---|---|---|---|
Pa |
2 | 1 | Alta (periodo=2) |
Pb |
5 | 1 | Bassa (periodo=5) |
Il periodo del sistema è $T = \text{mcm}(2, 5) = 10$.
“Possiamo subito calcolare che la priorità di
Pasarà maggiore di quella diPb.”Fonte: Gestione Processi
Nello schema di esecuzione, Pa viene sempre schedulato prima di Pb quando entrambi sono pronti. L’efficienza del sistema è del 70%, lasciando il 30% per gestione del sistema o altre routine.
Varianti:
“Esiste di due tipi, sia non preemptive che preemptive.”
Fonte: Gestione Processi
Nella versione preemptive, l’arrivo di un processo ad alta priorità può interrompere l’esecuzione di uno a bassa priorità.
Ottimalità di RM:
“Questo algoritmo è ottimo nella classe degli algoritmi a priorità statica. In particolare, esiste la proprietà che: È possibile schedulare degli eventi a priorità statica se e solo se possiamo farlo tramite
RM.”Fonte: Gestione Processi
Fattore di Utilizzo della CPU
Per determinare se un sistema è schedulabile, si calcola il fattore di utilizzo:
“La formula è abbastanza semplice, infatti: \(\sum_{i=0}^N{n_i \cdot C_i} \le T \quad \Rightarrow \quad \sum_{i=0}^N{\frac{T}{t_i} \cdot C_i} \le T \quad \Rightarrow \quad \boxed{\sum_{i=0}^N{C_i \over t_i} \le 1}\)”
Fonte: Gestione Processi
Definizione formale:
\[U := \sum_{i=0}^N{C_i \over t_i}\]Interpretazione:
Esempio di calcolo:
Per l’esempio precedente con Pa e Pb:
Il sistema è schedulabile con un margine del 30%.
Considerazioni pratiche:
“Operativamente nella realtà la formula è leggermente diversa, dato che non possiamo far coincidere la deadline con il periodo, rendendo la formula reale qualcosa del genere: \(U \le 1 - \alpha\)”
Fonte: Gestione Processi
dove $\alpha$ rappresenta il margine di sicurezza per gestire overhead e ritardi.
Algoritmo Earliest Deadline First (EDF)
L’EDF rappresenta un approccio radicalmente diverso basato su priorità dinamica:
“È un algoritmo preemptive a priorità dinamica. Questa viene calcolata per ogni processo in base alla vicinanza alla sua deadline.”
Fonte: Gestione Processi
Principio di funzionamento:
Ad ogni istante, lo scheduler seleziona il processo la cui deadline è più vicina. Le priorità cambiano dinamicamente durante l’esecuzione in base alle deadline rimanenti.
Esempio pratico:
Dal file Gestione Processi:
| Periodo $t_i$ | CPU-Burst $C_i$ | |
|---|---|---|
Pa |
4 | 2 |
Pb |
10 | 5 |
Analisi temporale:
All’istante t=0:
Pa ha deadline in t=4Pb ha deadline in t=10p(Pa) > p(Pb) → esegue PaAll’istante t=4:
Pa (nuova istanza) ha deadline in t=8Pb (in esecuzione) ha deadline in t=10 ma ha già eseguito per 2 unità, quindi deadline rimanente = 6p(Pa) > p(Pb) → preemption, esegue PaAll’istante t=8:
Pb ha deadline in t=10 (rimanente = 2)Pa ha deadline in t=12 (rimanente = 4)p(Pb) > p(Pa) → esegue PbAll’istante t=16:
Pa ha deadline in t=20 (rimanente = 4)Pb ha deadline in t=20 (rimanente = 4)“All’istante
16abbiamo chePa = 4 == Pb = 4. La scelta che compiamo è quella di mantenere il processo in esecuzione.”Fonte: Gestione Processi
Confronto Rate Monotonic vs Earliest Deadline First
| Caratteristica | Rate Monotonic (RM) | Earliest Deadline First (EDF) |
|---|---|---|
| Tipo di priorità | Statica (basata sul periodo) | Dinamica (basata sulla deadline) |
| Assegnazione priorità | $p(i) \propto \frac{1}{t_i}$ | Processo con deadline più vicina |
| Preemption | Opzionale (esistono versioni non-preemptive) | Sempre preemptive |
| Complessità | Bassa (priorità calcolate una sola volta) | Media (ricalcolo priorità ad ogni evento) |
| Overhead | Minimo | Maggiore (gestione priorità dinamiche) |
| Ottimalità | Ottimo tra algoritmi a priorità statica | Ottimo tra tutti gli algoritmi |
| Utilizzo CPU | Garantisce schedulabilità se $U \le 1$ (teorico) | Garantisce schedulabilità se $U \le 1$ |
| Predicibilità | Alta (comportamento deterministico) | Media (dipende dal carico dinamico) |
| Implementazione | Semplice | Più complessa |
| Applicazioni tipiche | Sistemi embedded con carico prevedibile | Sistemi con requisiti temporali variabili |
| Gestione overload | Processi a bassa priorità possono soffrire starvation | Distribuzione più equa del carico |
| Teorema | Schedulabile se e solo se schedulabile con RM | Se schedulabile, allora schedulabile con qualsiasi altro |
Considerazioni aggiuntive:
Approfondimenti:
Un aspetto fondamentale è che entrambi gli algoritmi richiedono la conoscenza a priori dei parametri dei processi (periodo, CPU-burst, deadline). Questo li rende adatti principalmente per sistemi embedded e applicazioni real-time dove il comportamento è deterministico.
La scelta tra RM e EDF in sistemi reali dipende anche da fattori pratici:
La condizione $U \le 1$ è necessaria ma non sempre sufficiente per RM. Esistono bound teorici più stringenti (come il bound di Liu e Layland: $U \le n(2^{1/n} - 1)$ per $n$ processi), ma nella pratica $U \le 1$ è un’ottima approssimazione, specialmente con $n$ grande.
Per EDF, la condizione $U \le 1$ è sia necessaria che sufficiente, rendendolo teoricamente superiore. Tuttavia, in caso di overload ($U > 1$), EDF può comportarsi peggio di RM, perché nessun processo è garantito di completare, mentre con RM almeno i processi ad alta priorità mantengono le loro deadline.
Un’applicazione pratica comune è nei sistemi di controllo industriale, dove sensori di temperatura, pressione, velocità devono essere letti a frequenze diverse ma tutte critiche. L’algoritmo RM permette di assegnare naturalmente priorità più alta ai sensori che richiedono campionamento più frequente, garantendo la stabilità del sistema di controllo.
Domanda: Spiega il problema dei Dining Philosophers, illustrando come una soluzione basata su semafori può portare a deadlock. Descrivi poi il concetto di Monitor come astrazione di alto livello per la sincronizzazione, spiegando la differenza tra “signal and wait” e “signal and continue”, e mostra come implementare una soluzione al problema dei filosofi usando i Monitor che eviti il deadlock. Infine, spiega come implementare un Monitor utilizzando i semafori.
Risposta:
Il problema dei Dining Philosophers è uno dei classici problemi di sincronizzazione che illustra magistralmente le sfide della coordinazione tra processi concorrenti e i rischi di deadlock in sistemi con risorse condivise.
Il Problema dei Dining Philosophers
“È un problema che considera 5 filosofi
p_iche passano tutta la loro vita a fare due cose: Thinking e Eating”
Descrizione del problema:
“Questi filosofi si trovano attorno ad una tavola dove al centro si trova una ciotola di riso infinita. I filosofi stanno prevalentemente nello stato thinking, ma ogni tanto vogliono passare nello stato eating. Al tavolo però si trovano solamente 5 bacchette (chopsticks)
c[i]distribuite in modo da essercene una tra ogni coppia di filosofi.”
Regole per mangiare:
Quando un filosofo vuole mangiare deve seguire questa sequenza:
Soluzione con Semafori e il Deadlock
Una prima soluzione intuitiva utilizza i semafori per proteggere l’accesso alle bacchette:
sem chopstick[5] = 1;
proc philosopher{
int i;
do{
// hungry
wait(chopstick[i]);
wait(chopstick[(i+1) % 5]);
// eating
signal(chopstick[(i+1) % 5]);
signal(chopstick[i]);
// thinking
} while (true);
}
Il problema del deadlock:
“Questo algoritmo possiede un principale punto critico […] genera deadlock.”
Come si verifica il deadlock:
“Immaginiamo che tutti i processi riescano a prendere possesso del primo
chopstick[i]ma vengano interrotti ‘in cascata’ (0$\to \dots \to$5) prima di poter prendere il secondo. Quando i processi avranno recuperato la prima bacchetta e proveranno a recuperare la seconda, entreranno tutti nello statobloccatoin cascata, generando un deadlock (blocco).”
Visualizzazione grafica del deadlock:
“Per visualizzare graficamente l’accesso alle risorse possiamo utilizzare dei grafi”
Nel grafo di allocazione risorse, il deadlock si manifesta come un ciclo: ogni filosofo possiede una bacchetta e attende quella del vicino, creando una catena circolare di attese.
“Tutti deadlock sono rappresentati da cicli. Non tutti i cicli rappresentano deadlock: solo quando le risorse coinvolte sono presenti in singola istanza”
Introduzione ai Monitor
Per risolvere elegantemente questo problema, vengono introdotti i Monitor:
“È un astrazione di alto livello che fornisce un meccanismo semplice e efficace per la sincronizzazione dei processi.”
Struttura di un Monitor:
“Possiamo rappresentarlo come un
abstract data type, con variabili interne accessibili solo all’interno delle varie procedure.”
monitor monitorName{
// shared variable declarations
procedure P1(/*...*/) {
// ...
}
// ...
procedure Pn(/*...*/) {
// ...
}
initialization_code(/*...*/) {
// ...
}
}
Proprietà fondamentale dei Monitor:
“Per definizione un solo processo alla volta può essere attivo dentro al monitor, ciò rende le procedure mutualmente esclusive.”
Questo significa che se un processo sta eseguendo una procedura del monitor, gli altri processi che vogliono accedervi devono attendere in una coda di ingresso (entryQueue).
Variabili di Condizione (Condition Variables)
La mutua esclusione da sola non è sufficiente per schemi di sincronizzazione complessi:
“Questo meccanismo non è però ancora sufficientemente potente da poter risolvere alcuni schemi di sincronizzazione. Per migliorarne l’efficacia si definiscono quindi all’interno del
monitordelle variabili di condizionamento.”
Operazioni sulle condition variables:
“Ad ogni condition variable (
condition x;) all’interno delmonitoril sistema assegna due operazioni:
x.wait(): sospende l’operazione in attesa di unax.signal()x.signal(): riprende l’esecuzione di uno dei processi che ha chiamato lax.wait(). Se nessun processo aveva chiamato lax.wait()non ha alcun effetto.”
Signal and Wait vs Signal and Continue
L’introduzione delle condition variables comporta una scelta progettuale importante:
“L’introduzione di queste condition variable comporta alcune scelte progettuali da fare. Ad esempio, se
Pinvoca lax.signal(), mentre c’eraQinx.wait(), quale dei due processi dovrà riprendere prima?”
Due approcci possibili:
“Abbiamo due opzioni possibili:
- Signal and wait: facciamo riprendere
Qdandogli la precedenza suPanche se questo era in esecuzione.- Signal and continue: facciamo proseguire
P, mettendo in attesa della mutua esclusioneQ”
Considerazioni:
“Ambedue le opzioni hanno dei pro e dei contro, e sta all’implementatore scegliere quale delle due opzioni selezionare.”
Soluzione al Dining Philosophers con Monitor
Ecco l’implementazione che evita il deadlock:
monitor DiningPhilosophers{
enum {THINKING, HUNGRY, EATING} state[5];
condition self[5];
void pickup(int i) {
state[i] = HUNGRY;
test(i);
if (state[i] != EATING)
self[i].wait();
}
void putdown(int i) {
state[i] = THINKING;
test((i-1) % 5);
test((i+1) % 5);
}
void test(int i) {
if ( (state[(i-1) % 5] != EATING) &&
(state[i] == HUNGRY) &&
(state[(i+1) % 5] != EATING)) {
state[i] = EATING;
self[i].signal();
}
}
initialization_code() {
for (int i = 0; i < 5; ++i) {
state[i] = THINKING;
}
}
}
Come ogni processo utilizza il monitor:
DiningPhilosophers.pickup(i);
// EAT
DiningPhilosophers.putdown(i);
Come questa soluzione evita il deadlock:
“Questo metodo, che non si basa direttamente sul possesso delle bacchette, ma piuttosto sul fatto che:
- Se i miei vicini sono
THINKINGio posso mangiare- Se almeno uno dei miei vicini è
EATINGpasso allo statoHUNGRYe mi metto in attesa, in quanto prima o poi restituirà la bacchetta che mi serve- Se entrambi i miei vicini sono
HUNGRYsignifica che io possono mangiare”
L’intuizione chiave:
“L’ultimo di questi casi avviene proprio perché quando qualcuno è
HUNGRYnon prende nessuna bacchetta finché non le può prendere entrambe.”
Questo elimina la possibilità di deadlock perché non si forma mai una catena circolare di attese. Un filosofo passa allo stato EATING solo quando entrambe le bacchette sono disponibili, evitando di trattenere risorse mentre ne attende altre.
Nota sulla starvation:
Questa soluzione potrebbe comunque provocare starvation se i due vicini di un filosofo mangiano continuamente, impedendogli di acquisire le bacchette. Tuttavia, è un problema diverso dal deadlock e può essere risolto con politiche di fairness.
Implementazione dei Monitor con Semafori
In linguaggi come C++, che non forniscono nativamente i monitor, possiamo implementarli usando i semafori:
“In
C++non sono forniti nativamente i monitor, però possiamo implementarli tramite l’utilizzo dei semafori.”
Semafori necessari per l’implementazione “signal and wait”:
/**
* Semaforo di mutua esclusione sul monitor
*/
sem mutex = sem_ini(1);
/**
* Semaforo sul quale si mettono in attesa i processi
* che si sono sospesi all'interno del monitor
*/
sem next = sem_ini(0);
/**
* Indica il numero di processi in attesa sul semaforo `next`
*/
int next_count = 0;
Struttura delle procedure del monitor:
“Ogni procedura sarà quindi rimpiazzata con il seguente segmento di codice:”
wait(mutex);
// corpo della procedura
if (next_count > 0)
signal(next);
else
signal(mutex);
Implementazione delle condition variables (signal&wait):
class condition{
sem x_sem = sem_ini(0);
int x_count = 0;
public void wait() {
x_count++;
if (next_count > 0)
signal(next);
else
signal(mutex);
wait(x_sem);
x_count--;
}
public void signal() {
if (x_count > 0) {
next_count++;
signal(x_sem);
wait(next);
next_count--;
}
}
}
Spiegazione del meccanismo:
mutex: protegge l’accesso al monitor, garantendo la mutua esclusionenext: contiene i processi che hanno fatto signal() e stanno cedendo il controllo (signal and wait)next_count: traccia quanti processi sono in attesa su nextx_sem: per ogni condition variable, il semaforo su cui attendono i processi che hanno chiamato x.wait()x_count: numero di processi in attesa sulla condition variableIl flusso di wait():
next se ci sono processi, altrimenti su mutex)Il flusso di signal():
next_count (si prepara ad attendere)next (cedendo il monitor al processo risvegliato)next_countGestione delle Priorità nelle Condition Variables
Un aspetto importante è la gestione dell’ordine di risveglio:
“Se più processi fossero in coda sulla stessa condizione
x, dobbiamo decidere quale risvegliare alla chiamatax.signal(). Un algoritmoFCFSnon è adeguato, in quanto può portare i processi a starvation.”
Soluzione con conditional-wait:
“Quello che quindi possiamo fare è implementare un costrutto di conditional-wait nella forma
x.wait(c)dovecindica la priorità (dove0è la priorità massima, come se fosse il tempo). A questo punto gestiamo la coda secondo un ordine di priorità decrescente.”
Monitor per una Singola Risorsa
Un esempio pratico di monitor è l’allocatore di risorse:
monitor ResourceAllocator{
bool busy;
condition x;
void acquire(int time) {
while (busy) {
x.wait(time);
}
busy = true;
}
void release() {
busy = false;
x.signal();
}
intialization_code() {
busy = false;
}
}
Questo semplice monitor gestisce l’allocazione di una singola risorsa, garantendo mutua esclusione e permettendo l’uso di priorità temporali.
Approfondimenti:
I monitor rappresentano uno dei contributi più significativi alla programmazione concorrente, introdotti da Per Brinch Hansen e C.A.R. Hoare negli anni ‘70. La loro eleganza sta nel nascondere la complessità della sincronizzazione dietro un’interfaccia ad alto livello, riducendo drasticamente la probabilità di errori rispetto alla gestione manuale dei semafori.
Vantaggi dei Monitor rispetto ai Semafori:
signal() o fare una wait() su un semaforo sbagliatoSvantaggi:
I monitor sono nativamente implementati in linguaggi come Java (attraverso synchronized e wait()/notify()) e C# (con lock e Monitor.Wait()/Monitor.Pulse()), dimostrando la loro rilevanza pratica nei sistemi moderni.
Il problema dei Dining Philosophers, oltre ad essere un classico esempio didattico, rappresenta situazioni reali come la gestione di risorse condivise in database distribuiti, dove transazioni multiple competono per lock su record diversi, e la corretta gestione è cruciale per evitare deadlock che bloccherebbe l’intero sistema.
Domanda: Spiega la differenza tra rilocazione statica e rilocazione dinamica nella gestione della memoria, descrivendo il ruolo della MMU (Memory Management Unit) nella rilocazione dinamica. Illustra come avviene la traduzione degli indirizzi virtuali in indirizzi fisici, quali controlli di protezione vengono effettuati, e perché la rilocazione dinamica è preferibile nei sistemi che supportano meccanismi di swapping.
Risposta:
La gestione della memoria richiede un meccanismo di traduzione tra gli indirizzi virtuali utilizzati dai processi e gli indirizzi fisici effettivi della memoria. Questa traduzione può avvenire in due modalità fondamentalmente diverse: rilocazione statica e rilocazione dinamica.
Rilocazione Statica
Nella rilocazione statica, la traduzione degli indirizzi virtuali in indirizzi fisici avviene in fase di caricamento del processo in memoria. Quando il linker genera il modulo di caricamento, utilizza indirizzi virtuali. Prima che il processo inizi l’esecuzione, il caricatore si occupa di tradurre questi indirizzi sommando il contenuto del program counter:
“Quando compiliamo un programma, il linker si occuperà quindi di tradurre gli indirizzi simbolici, inseriti dal programmatore, in indirizzi virtuali. […] Nel caso di rilocazioni statiche, il caricatore genererà l’immagine del processo traducendo gli indirizzi virtuali sommandogli il contenuto del program counter”
Fonte: Gestione della memoria
Una volta completata questa fase, l’immagine del processo contiene direttamente indirizzi fisici. Questo approccio presenta un limite critico per i meccanismi di swapping:
“La rilocazione statica funziona in sistemi embedded ma non supporta meccanismi di swapping. Infatti, queste porzioni contengono al loro interno indirizzi fisici, richiedendo quindi di essere reinserite nelle medesime porzione di memoria nelle quali si trovavano prima dello swap, cosa tendenzialemnte impossibile.”
Fonte: Gestione della memoria
Rilocazione Dinamica
Per superare i limiti della rilocazione statica, si adotta la rilocazione dinamica, dove la traduzione degli indirizzi viene ritardata fino alla fase di esecuzione:
“Per evitare gli inconvenienti legati alla rilocazione statica è necessario ritardarla, attuandola solamente in fase di esecuzione. Questa nuova rilocazione si chiama rilocazione dinamica.”
Fonte: Gestione della memoria
In questo approccio, quando un processo viene allocato in memoria, il caricatore trasferisce nelle locazioni della partizione scelta direttamente i contenuti del modulo di caricamento, mantenendo quindi gli indirizzi virtuali. Poiché questi indirizzi sono indipendenti dalle particolari locazioni fisiche, il processo può essere successivamente spostato in altre aree di memoria senza problemi.
Il Ruolo della MMU (Memory Management Unit)
La traduzione degli indirizzi da virtuali a fisici in fase di esecuzione è delegata a un componente hardware chiamato MMU (Memory Management Unit), che si interpone tra la CPU e la memoria:
“Si delega quindi alla fase di esecuzione l’azione di traduzione degli indirizzi da virtuali a fisici, attraverso l’
MMUche interfaccia la CPU alla memoria. In questa nuova architettura, la CPU utilizza solo ed esclusivamente indirizzi virtuali.”Fonte: Gestione della memoria
La funzione di rilocazione, nel caso più semplice di spazio virtuale unico, è una funzione di somma che lega ogni indirizzo virtuale $x$ a un indirizzo fisico $y$:
\[y = f(x) \approx I + x\]dove $I$ rappresenta l’indirizzo iniziale della partizione di memoria nella quale il processo è caricato.
Traduzione degli Indirizzi e Controlli di Protezione
La MMU non si limita a tradurre gli indirizzi, ma effettua anche controlli di protezione fondamentali. Prima di tradurre un indirizzo virtuale $x$, verifica che questo rientri nei limiti dello spazio virtuale del processo:
“La
MMU, oltre a tradurre gli indirizzi da virtuali a fisici, si occupa anche di effettuare dei controlli di protezione. Prima di tradurre un indirizzo virtuale, si effettua infatti in controllo che $x \in [0, 6140)$. Se questo non fosse vero allora laMMUgenera un eccezione di protezione, tipicamente il segmentation fault, che il sistema operativo dovrà gestire opportunamente.”Fonte: Gestione della memoria
Questo meccanismo di protezione impedisce a un processo di accedere ad aree di memoria al di fuori del proprio spazio virtuale, garantendo l’isolamento tra processi.
Vantaggi della Rilocazione Dinamica per lo Swapping
La rilocazione dinamica è preferibile nei sistemi che supportano meccanismi di swapping per diverse ragioni fondamentali:
Flessibilità di rilocazione: Poiché l’immagine del processo mantiene indirizzi virtuali, può essere scaricata sulla swap-area e successivamente ricaricata in qualsiasi area libera della memoria fisica, non necessariamente nella stessa posizione precedente.
Supporto alla multiprogrammazione: Permette di gestire efficacemente più processi in memoria, potendo spostare dinamicamente i processi per ottimizzare l’utilizzo dello spazio disponibile.
Gestione trasparente: Il processo non è consapevole della sua posizione fisica in memoria né degli eventuali spostamenti, rendendo lo swapping completamente trasparente all’applicazione.
Efficienza della memoria: Facilita tecniche come il compattamento della memoria per eliminare la frammentazione esterna, poiché i processi possono essere facilmente rilocati.
Approfondimenti:
La rilocazione dinamica costituisce la base per tecniche di gestione della memoria più avanzate come la segmentazione e la paginazione. Nel caso di spazio virtuale segmentato, la MMU deve gestire traduzioni più complesse mantenendo informazioni per ciascun segmento. Con la paginazione, la funzione di rilocazione diventa una vera e propria tabella di corrispondenza tra pagine virtuali e frame fisici, eliminando completamente il problema della frammentazione esterna pur introducendo un overhead nella gestione delle tabelle delle pagine.
Domanda: Spiega la tecnica della paginazione a domanda, descrivendo come funziona il meccanismo di page fault e la gestione della swap-area. Illustra nel dettaglio gli algoritmi di rimpiazzamento delle pagine FIFO, LRU e Second-Chance (Clock Algorithm), confrontandone vantaggi, svantaggi e complessità implementativa. Spiega inoltre il fenomeno del thrashing e il ruolo dei bit di controllo U (uso) e M (modifica) negli algoritmi di rimpiazzamento.
Risposta:
La paginazione a domanda rappresenta una delle tecniche più importanti e utilizzate nella gestione della memoria virtuale, permettendo di eseguire processi la cui memoria virtuale eccede la capacità della memoria fisica disponibile.
Tecnica della Paginazione a Domanda
La paginazione a domanda si basa sul concetto fondamentale che un processo non necessita sempre di tutte le sue pagine contemporaneamente in memoria:
“È una tecnica di rilocazione dinamica, con allocazione non contigua in uno spazio virtuale unico a caricamento a domanda. Basandosi sui concetti della paginazione, sfrutta il fatto che un processo non necessita sempre di tutte le sue pagine.”
Fonte: Gestione della memoria
La tecnica è analoga alla segmentazione a domanda e rappresenta la soluzione più utilizzata anche nei sistemi paginati. Durante l’esecuzione di un processo è possibile mantenere in memoria soltanto una parte del suo spazio virtuale, ovvero solo quella necessaria al punto corrente dell’esecuzione.
Meccanismo di Page Fault
Il meccanismo hardware che supporta la paginazione a domanda introduce nuovi bit di controllo nel descrittore di ogni pagina:
“Il meccanismo hardware è molto simile a quello della segmentazione a domanda, e aggiunge ai campi di lettura
Re scritturaW:
P: bit di presenza, caratterizza la presenza della pagina in memoriaU: bit di uso, utilizzato dagli algoritmi di rimpiazzamentoW: bit di scrittura, utilizzato dagli algoritmi di rimpiazzamento”Fonte: Gestione della memoria
Quando un processo viene creato, tutti i descrittori delle sue pagine hanno P == 0. In questo stato, il campo che normalmente contiene l’indice del frame fisico contiene invece l’indirizzo della pagina sul disco nella swap-area.
Il processo di gestione del page fault avviene nel seguente modo:
“Quando il processo farà l’accesso ad un indirizzo
x, che appartiene alla paginapg, si verificherà innanzitutto chepgsia tra le pagine del processo, e successivamente, il valore diPdel descrittore associato. SeP == 0si generapage fault, fermando l’esecuzione del processo.”Fonte: Gestione della memoria
La routine di gestione del page fault esegue i seguenti passi:
pfbloccatoP = 1 e inserendo l’indirizzo del framepronto poi in esecuzione, con il PC decrementato di uno per rieseguire l’istruzione che ha causato il page faultQuesto meccanismo è completamente trasparente al processo.
Gestione della Swap-Area
La swap-area è una porzione dedicata della memoria secondaria (disco) utilizzata per contenere le pagine non presentemente in RAM:
“Nel caso di sistemi che utilizzano la paginazione a domanda anche la swap-area è divisa in settori fisici di dimensioni uguali a quelle delle pagine.”
Fonte: Gestione della memoria
Questa organizzazione permette una corrispondenza diretta tra pagine virtuali e settori della swap-area, semplificando notevolmente la gestione rispetto alle tecniche di segmentazione.
Algoritmi di Rimpiazzamento delle Pagine
Quando non ci sono frame liberi disponibili, è necessario selezionare una pagina da rimuovere dalla memoria. Esistono diversi algoritmi per questa scelta:
Algoritmo Ottimo (Teorico)
L’algoritmo ottimo rimpiazza le pagine che non verranno più riferite o che verranno utilizzate più lontano nel futuro:
“Ottimo: rimpiazza le pagine che non verranno più riferite o che verranno utilizzate più nel futuro rispetto alle altre. (Teorico e irrealizzabile)”
Fonte: Gestione della memoria
Questo algoritmo è irrealizzabile in pratica poiché richiederebbe la conoscenza anticipata del futuro comportamento del processo, ma serve come riferimento teorico per valutare l’efficienza degli altri algoritmi.
Algoritmo FIFO (First In First Out)
L’algoritmo FIFO implementa una politica semplice:
“FIFO: rimpiazza la pagina locale (aka del processo in esecuzione) che è da più tempo in memoria. Il rimpiazzamento è locale per evitare di propagare in futuro l’errore anche ad altri processi.”
Fonte: Gestione della memoria
Vantaggi:
Svantaggi:
Algoritmo LRU (Least Recently Used)
L’algoritmo LRU rappresenta un miglioramento significativo rispetto al FIFO:
“LRU: rimpiazza la pagina non utilizzata da più tempo. È più efficiente rispetto al FIFO, ma anche più difficile da realizzare. Infatti è necessario modificare opportunamente la tabella delle pagine per introdurre un campo dove salvare un timestamp, ed è necessario consultarli tutti per recuperare il più vecchio e aggiornare i nuovi. Tra gli algoritmi implementabili è quello che riduce al minimo il trashing, ovvero diminuisce al minimo il
page-fault.”Fonte: Gestione della memoria
Vantaggi:
Svantaggi:
Algoritmo Second-Chance (Clock Algorithm)
Per superare le limitazioni implementative dell’LRU, UNIX utilizza un’approssimazione chiamata Second-Chance o Clock Algorithm:
“L’algoritmo utilizzato da
UNIXè un’approssimazoine dell’algoritmoLRU, noto come second-chance o clock algorithm.”Fonte: Gestione della memoria
Il funzionamento dell’algoritmo è il seguente:
“La tabella delle pagine fisiche viene gestita come un array circolare. Viene mantenuta una variabile
vittima. che consiste in un puntatore contenente l’indice della pagina fisica successiva a quella che è stata rimpiazzata per ultima. Al prossimopage-faultsi inizia a verificare la pagina il cui indice è contenuto nella variabilevittima.”Fonte: Gestione della memoria
La logica di selezione esamina il bit U:
“In particolare si esamina il bit
Udella pagina associata:
U == 0: la pagina viene scelta per il rimpiazzamentoU == 1: si azzera il bit e si incrementa la variabilevittima, andando ad esaminare la pagina successiva, riverificando il bitU.”Fonte: Gestione della memoria
L’incremento è circolare: vittima = (vittima + 1) % M, garantendo che si troverà certamente una pagina con U == 0. Nel caso peggiore, si selezionerà la pagina iniziale dopo aver azzerato tutti i bit U durante la scansione completa.
Vantaggi:
U per pagina invece di timestampSvantaggi:
Ottimizzazione con il Bit di Modifica (M)
Per ridurre il numero di trasferimenti tra memoria e disco, l’algoritmo può considerare anche il bit M:
“Per ridurre il numero di trasferimenti tra memoria e disco, viene spesso preso in considerazione anche il valore di
M. Le pagine sono quindi classificate prima rispetto al valore diU, e successivamente si privilegiano le pagine conM == 0”Fonte: Gestione della memoria
Questo perché una pagina con M == 0 (non modificata) non richiede di essere riscritta sulla swap-area, risparmiando un’operazione di I/O costosa.
Il Ruolo dei Bit di Controllo U e M
I bit di controllo sono fondamentali per l’efficienza degli algoritmi di rimpiazzamento:
“Al fine di facilitare l’implementazione di algoritmi di rimpiazzamento efficienti, sono spesso disponibili ulteriori bit di controllo nel descrittore di un segmento. Ad esempio abbiamo i bit
U(uso) eM(modifica), che possono essere letti e azzerati via software, e che vengono settati automaticamente via hardware ogni volta che viene fatto un riferimento/modifica al segmento.”Fonte: Gestione della memoria
Bit U (Uso):
Bit M (Modifica/Dirty Bit):
M == 0 la pagina su disco è già aggiornata)Il Fenomeno del Thrashing
Il thrashing è un fenomeno critico che si verifica quando il sistema passa più tempo a gestire page fault che ad eseguire realmente i processi:
“Tra gli algoritmi implementabili è quello che riduce al minimo il trashing, ovvero diminuisce al minimo il
page-fault.”Fonte: Gestione della memoria
Il thrashing si manifesta quando:
Per mitigare il thrashing, i sistemi operativi implementano strategie come:
Approfondimenti:
La paginazione a domanda rappresenta il fondamento della memoria virtuale moderna. UNIX implementa meccanismi sofisticati per gestire la memoria, come il pagedaemon (pid=2) che monitora periodicamente il numero di frame liberi e interviene quando scende sotto la soglia lotsfree. Se la situazione peggiora ulteriormente e il numero di frame liberi scende sotto minfree, interviene lo swapper per trasferire interi processi dalla memoria centrale alla swap-area. Questa gestione a più livelli (lotsfree > desfree > minfree) permette di mantenere il sistema reattivo anche sotto carico elevato, evitando situazioni di thrashing critico.
Domanda: Spiega il concetto di “processo esterno” nella gestione delle periferiche, descrivendo come avviene la sincronizzazione tra processi applicativi e dispositivi di I/O. Illustra il ruolo dei semafori nella gestione delle interruzioni, analizza il problema dell’inefficienza del trasferimento byte per byte confrontandolo con soluzioni più efficienti, e descrivi la struttura del descrittore di un dispositivo e le sue funzioni principali.
Risposta:
La gestione delle periferiche di I/O rappresenta uno degli aspetti più critici di un sistema operativo, richiedendo meccanismi sofisticati di sincronizzazione tra l’attività della CPU e quella dei dispositivi esterni. Il concetto di “processo esterno” è fondamentale per comprendere questa interazione.
Il Concetto di Processo Esterno
Un dispositivo di I/O, insieme al suo controllore, esegue permanentemente una sequenza di azioni che può essere equiparata all’esecuzione di un processo. Tuttavia, a differenza dei processi tradizionali, questa sequenza non corrisponde all’esecuzione di un programma software, ma piuttosto a una sequenza di azioni cablate eseguite in parallelo al flusso della CPU:
“Ogni dispositivo esegue permanentemente una sequenza di azioni equiparabile con l’esecuzione di un processo. La differenza però è che il processo non corrisponde all’eseuzione di un programma, ma piuttosto ad una sequenza di azioni cablate eseguite in parallelo al flusso della CPU. Per distiguenre questo tipo di ‘processi’ li identificheremo con il termine di processi esterni.”
Il comportamento tipico di un processo esterno può essere schematizzato come segue:
while (true) {
// Attendo l'invio di un comando
while (Start == 0);
// Eseguo il comando
executeComand();
// Registro l'evento
setComandOutputAndFlag();
}
Normalmente un dispositivo si trova in stand-by, aspettando che il bit di start del registro di controllo venga settato. Una volta attivato, esegue il comando e alla fine registra l’evento ponendo a uno il relativo bit di flag.
Sincronizzazione tramite Semafori
La gestione dei dispositivi di I/O diventa quindi un problema di sincronizzazione tra processi applicativi e processi esterni. Per evitare l’attesa attiva (busy waiting), che sarebbe estremamente inefficiente, si utilizza un meccanismo basato su semafori:
“Per forzare la sospensione del processo in attesa su un flag è possibile associare al dispositivo un semaforo inizializzato a zero e sostituire il ciclo di attesa attiva una
waitsu tale semaforo.”
Il comportamento del processo applicativo diventa:
semaforo dato_disponibile = 0;
for (int i = 0; i < n; ++i) {
// preparo il comando
executeComand();
wait(dato_disponibile);
// verifico l'esito
}
Poiché il processo si sospende sulla wait, non può più controllare direttamente la disponibilità del dato. Sarà quindi il controllore del dispositivo a segnalare l’evento risvegliando il processo applicativo attraverso un’interruzione e la relativa funzione di gestione (inth) che eseguirà una signal sul semaforo.
Inefficienza del Trasferimento Byte per Byte
Il primo approccio alla gestione delle interruzioni presenta un problema di efficienza significativo. Nel trasferimento byte per byte, ogni singolo byte trasferito richiede:
Come illustrato dal diagramma temporale:
“L’esempio appena fatto questo è altamente inefficiente, per trasferire $n$ byte abbiamo infatti un processo che per $n$ volte viene schedulato solo per poi tornare in attesa.”
Questo comporta un numero elevatissimo di context switch, con un overhead inaccettabile soprattutto per trasferimenti di grandi quantità di dati.
Soluzione Efficiente: Delega alla Routine di Interruzione
Una soluzione molto più efficiente consiste nel delegare alla routine di interruzione (inth) l’operazione di gestire i singoli byte:
“Come suggerisce l’immagine […] possiamo delegare ad
inthl’operazione di caricare il singolo byte affinchéPElo possa leggere. In questo casoPInon carica il singolo dato, ma piuttosto la locazione di memoria dove si trovano tutti i dati.inthsi occuperà di accedervi e fornire i singoli byte aPE. Questo ci permette di almeno dimezzare il numero di cambi di contesto nello stesso arco temporale.”
In questo approccio, il processo applicativo fornisce al dispositivo:
La routine inth gestisce autonomamente i trasferimenti intermedi, risvegliando il processo applicativo solo al completamento dell’intera operazione o in caso di errore.
Struttura del Descrittore di un Dispositivo
Per implementare questa comunicazione efficiente, è necessaria una struttura dati condivisa tra il processo applicativo e la routine di interruzione: il descrittore del dispositivo. Insieme alle funzioni di accesso, costituisce il device driver:
“L’unione tra il descrittore del dispositivo e le funzioni di accesso costituisce il device driver.”
Il descrittore del dispositivo ha due scopi principali:
I campi tipicamente presenti in un descrittore sono:
“I descrittori possono avere parametri diversi, ma in generale i seguenti dati sono sempre presenti:
- Indirizzo registro di controllo
- Indirizzo registro di stato
- Indirizzo registro dati
- Stato di sincronizzazione:
dato_disponibile- Contatore dati da trasferire:
contatore- Indirizzo del buffer:
puntatore- Risultato del trasferimento:
esito”
Le funzioni del descrittore includono:
Memorizzazione indirizzi hardware: mantiene gli indirizzi dei registri del controllore (controllo, stato, dati)
Sincronizzazione: il semaforo dato_disponibile permette al processo di sospendersi e alla routine di interruzione di risvegliarlo
Gestione del trasferimento: i campi contatore e puntatore permettono alla routine inth di gestire autonomamente i trasferimenti multipli
Comunicazione dell’esito: il campo esito permette di comunicare al processo applicativo il risultato dell’operazione (successo o codice d’errore)
Implementazione della Funzione Read
Un esempio di funzione del driver che utilizza il descrittore:
int read(int disp, char* pbuf, int cont) {
descrittore[disp].contatore = cont;
descrittore[disp].puntatore = pbuf;
<attivo il dispositivo settando `bitStart = 1`>
descrittore[disp].dato_disponibile.wait();
if (descrittore[disp].esito == ERROR_CODE) {
return -1;
}
else {
return (cont - descrittore[disp].contatore);
}
}
La routine di interruzione gestisce i trasferimenti intermedi decrementando contatore e incrementando puntatore, riattivando il dispositivo per il byte successivo finché contatore != 0. Solo alla fine risveglia il processo applicativo.
Approfondimenti:
Questo meccanismo di sincronizzazione tra processi applicativi e processi esterni rappresenta un esempio fondamentale di comunicazione tra componenti hardware e software. La struttura a livelli del sottosistema I/O, con l’interfaccia device-independent che nasconde i dettagli specifici dei dispositivi, permette di scrivere applicazioni portabili. In UNIX, questa astrazione è realizzata trattando i dispositivi come file, rendendo le operazioni di I/O uniformi indipendentemente dal tipo di periferica utilizzata.
Domanda: Confronta i metodi di allocazione fisica dei file su disco: allocazione contigua, allocazione a lista concatenata e allocazione a indice. Per ciascun metodo, descrivi il funzionamento, i vantaggi e gli svantaggi in termini di frammentazione, costo di ricerca dei blocchi e supporto all’accesso diretto. Spiega poi in dettaglio il metodo di allocazione utilizzato in UNIX, illustrando la struttura degli i-node, l’organizzazione del disco in regioni (BootBlock, SuperBlock, i-List, DataBlocks), e il meccanismo di indirizzamento multiplo che permette di gestire file di dimensioni variabili.
Risposta:
L’organizzazione fisica dei file su disco è uno degli aspetti più critici nella progettazione di un file system. I diversi metodi di allocazione rappresentano differenti compromessi tra efficienza, semplicità e funzionalità. L’analisi di queste tecniche ci permette di comprendere le scelte implementative dei sistemi moderni come UNIX.
Contesto: Il Livello di Organizzazione Fisica
Prima di analizzare i metodi specifici, è importante comprendere il ruolo di questo livello:
“Il terzo strato è il livello di Organizzazione Fisica, che ha come compito primario quello di allocare i record logici di ogni file nell’unità di memorizzazione secondaria.”
Fonte: Il file system
In questo livello, lo spazio disponibile viene visto come:
“In questo livello lo spazio disponibile per l’allocazione sul disco viene visto come un insieme di blocchi fisici.”
Fonte: Il file system
Il blocco fisico rappresenta l’unità fondamentale:
“Il blocco fisico è l’unità di allocazione e di trasferimento delle informazioni sul dispositivo, e gli è associata una posizione particolare sulla superficie del disco e una dimensione costante $D_b$, generalmente molto maggiore della dimensione di un record logico $D_r$.”
Fonte: Il file system
In un singolo blocco possiamo contenere $N_b = \frac{D_b}{D_r}$ record logici.
1. Allocazione Contigua
L’allocazione contigua rappresenta il metodo più semplice concettualmente:
“Questa tecnica mappa ogni file su un insieme di blocchi fisicamente contigui.”
Fonte: Il file system
Funzionamento:
“Per fare ciò è innanzitutto necessario calcolare il numero di blocchi necessari per salvare un file, e successivamente si procede a cercare una partizione di blocchi liberi abbastanza grande da contenere il file.”
Fonte: Il file system
Vantaggi:
L’allocazione contigua offre vantaggi significativi in termini di prestazioni:
“Questo tipo di allocazione ha diversi vantaggi, tra i quali:
- Semplice ricerca di un blocco: noto l’indirizzo del primo blocco del file $(B)$ e l’
IO pointerdi un dato record $(i)$, un dato blocco si trova in posizione: $B + \Big\lfloor\frac{i}{N_b}\Big\rfloor$- Possibilità di accesso sequenziale e diretto allo stesso costo”
Fonte: Il file system
La formula $B + \lfloor\frac{i}{N_b}\rfloor$ permette di calcolare direttamente la posizione di qualsiasi blocco, rendendo l’accesso diretto molto efficiente. Inoltre, l’accesso sequenziale beneficia della contiguità dei blocchi, riducendo i movimenti della testina del disco.
Svantaggi:
Tuttavia, gli svantaggi sono significativi e limitanti:
“Tuttavia ha anche molti svantaggi:
- Frammentazione Esterna: man mano che si riempie il disco si rimangono zone contigue sempre più piccole, rendendo necessario il compattamento
- Costo della ricerca dello spazio libero: è necessario analizzare tutto il disco finché non si trova uno spazio sufficientemente grande per conservare il file
- Crescita delle dimensioni del file: qualora il file aumentasse di dimensione potrebbe essere necessario rilocare l’intero file se non fossero disponibili ulteriori blocchi contigui liberi.”
Fonte: Il file system
La frammentazione esterna è particolarmente problematica: dopo ripetute operazioni di creazione ed eliminazione di file, il disco si frammenta in piccole zone contigue, rendendo difficile allocare file di grandi dimensioni anche se lo spazio totale libero sarebbe sufficiente. Il compattamento richiede di spostare tutti i file per raccogliere lo spazio libero, operazione estremamente costosa.
2. Allocazione a Lista Concatenata
Per superare i limiti dell’allocazione contigua, si adotta l’allocazione a lista concatenata:
“Questa tecnica memorizza ogni file in un insieme di blocchi non contigui organizzati in una lista concatenata.”
Fonte: Il file system
Funzionamento:
“L’organizzazione è ancora di tipo sequenziale, ma stavolta blocchi successivi non devono necessariamente essere vicini.”
Fonte: Il file system
Ogni blocco contiene, oltre ai dati, un puntatore al blocco successivo nella catena. Il descrittore del file mantiene solo il puntatore al primo blocco.
Vantaggi:
“Cio ci fornisce diversi vantaggi:
- Non soffre di frammentazione esterna
- Minor costo di allocazione: non è più necessario scorrere l’intero disco per allocare un file
- Accesso sequenziale a basso costo: dato il primo blocco abbiamo il puntatore al successivo”
Fonte: Il file system
L’eliminazione della frammentazione esterna è un vantaggio cruciale: qualsiasi blocco libero può essere utilizzato, indipendentemente dalla sua posizione.
Svantaggi:
“Vi sono ancora diversi svantaggi:
- Broken Links: se un blocco viene danneggiato, non solo perdiamo le informazioni che contiene ma anche le informazioni sul successivo, rendendo irraggiungibile il resto del file
- Overhead spaziale: dover salvare i puntatori diminuisce lo spazio effettivo utilizzabile
- Accesso diretto oneroso: in questo caso, per accedere all’$i$-esimo blocco sono necessari $\Big\lfloor\frac{i}{N_b}\Big\rfloor$ accessi al disco
- Costo della ricerca di un blocco”
Fonte: Il file system
Il problema dei broken links è particolarmente grave. Una soluzione parziale consiste nell’utilizzare liste doppiamente concatenate:
“Per arginare il problema dei broken links è possibile realizzare una double-linked-list, andando a sacrificare un altra porzione del blocco per salvare un secondo puntatore che punta al blocco precedente.”
Fonte: Il file system
Questo aumenta la tolleranza ad errore da zero a uno, ma raddoppia l’overhead spaziale per i puntatori.
Ottimizzazione: File Allocation Table (FAT):
Alcuni sistemi operativi, come Windows, introducono una struttura dati aggiuntiva:
“Alcuni sistemi operativi, ad esempio
Windows, affronato il problema affiancando ad una allocazione basata su una linked-list una struttura dati nella quale viene descritta la mappa di allocazione di tutti i blocchi, detta File Allocation TableFAT.”Fonte: Il file system
La FAT risolve elegantemente diversi problemi:
“Essa contiene tanti elementi quanti sono i blocchi del dispositivo, ed in ogniuno è presente un valore che indica:
- Se il blocco è libero
- L’indice dell’elemento della tabella che rappresenta il blocco successivo nella lista”
Fonte: Il file system
Vantaggi della FAT:
“In questo modo, anche in perdita di concatenamento è possibile effettuare il recupero del puntatore perso accedendo alla
FAT. LaFATpuò anche essere copiata in memoria centrale o in una cache, così da velocizzare notevolmente l’accesso diretto.”Fonte: Il file system
3. Allocazione a Indice
L’allocazione a indice rappresenta un approccio radicalmente diverso:
“Questo tipo di allocazione si basa ancora sull’utilizzo di blocchi non contigui per l’allocazione di un file, ma in questo caso ad ogni file è associato un blocco indice in cui sono contenuti tutti gli indirizzi dei blocchi su cui è allocato il file.”
Fonte: Il file system
Funzionamento:
Il descrittore del file punta a un blocco indice che contiene un array di puntatori, ciascuno dei quali punta a un blocco dati del file. Questo permette di accedere direttamente a qualsiasi blocco conoscendo il suo indice.
Vantaggi:
“Questo tipo di allocazione ha gli stessi vantaggi dell’allocazione tramite linked-list introducendo però anche:
- Possibilità di accesso diretto
- Maggiore velocità di accesso”
Fonte: Il file system
L’accesso diretto è efficiente perché non richiede di attraversare una lista: è sufficiente leggere il blocco indice e poi accedere direttamente al blocco desiderato.
Svantaggi - Il Problema della Scalabilità:
“Il grande svantaggio di questa tecnica è la non scalabilità, infatti se il file crescesse tanto da necessitare più blocchi di quelli indicizzabili da un unico blocco avremmo un problema.”
Fonte: Il file system
Il calcolo della dimensione massima è matematicamente determinabile:
“In particolare dato un disco di capacità $C$, otteniamo quanti bit sono necessari per indicizzare tutti i blocchi di dimensione $D_b$: \(\quad I = \log{(\frac{C}{D_b})} \text{ indici}\)”
Fonte: Il file system
Conseguentemente:
“Ciò significa che in un singolo blocco è possibile salvare al massimo $\frac{D_b}{I}$ indici. Per ottenere la dimensione massima di un file è quindi sufficiente moltiplicare per ogni indice salvato la dimensione di un blocco: \(\quad D_M = \frac{D_b^2}{I} = \frac{D_b^2}{\log{(\frac{C}{D_b})}}\)”
Fonte: Il file system
Questa limitazione rende il metodo inadatto per file di grandi dimensioni senza ulteriori estensioni.
4. Allocazione in UNIX: Indicizzazione Multilivello
UNIX adotta una soluzione elegante che combina i vantaggi dell’allocazione a indice superandone i limiti:
“Il metodo di allocazione utilizzato in
UNIXè a indicizzazione su più livelli di indirizzamento basata su un grafo aciclico diretto.”Fonte: Il file system
Principio Fondamentale di UNIX:
“La regola sulla quale ci dobbiamo basare è che:
In
UNIXtutto è un file.”Fonte: Il file system
I file si dividono in tre categorie:
“I file si dividono in tre categorie:
- Ordinari: sono i veri e propri file
- Directory: sono dei file che rappresentano delle raccolte di altri file
- Speciali: rappresentano i dispositivi (ad esempio quelle in
/dev/)”Fonte: Il file system
Struttura con i-node:
“Ad ogni file possono essere associati uno o più nomi simbolici detti link/alias. Ogni link si riferirà però ad uno ed un solo descrittore, detto
i-node. I singolii-nodesono identificati univocamente daglii-numbere sono contenuti nellai-list.”Fonte: Il file system
Questo permette la condivisione di file senza duplicazione: più nomi possono puntare allo stesso i-node.
Organizzazione del Disco in UNIX
Il disco in UNIX è diviso in quattro regioni distinte:
“Questa tecnica formatta il disco in blocchi fisici di dimensione costante e prefissata, dividendo la superficie del disco in quattro regioni.”
Fonte: Il file system
1. BootBlock:
“La prima regione, detta
BootBlock, occupa un blocco fisico, allocato ad un indirizzo prefissato, e contiene il programma di inizializzazione del sistema da eseguire nella fase di bootstrap.”Fonte: Il file system
2. SuperBlock:
“La seconda regione è il
SuperBlock. Occupa anch’essa la dimensione di un blocco e descrive l’allocazione del file system. In particolare qui sono contenuti:
- I limiti delle quattro regioni
- Il puntatore alla lista dei blocchi liberi
- Il puntatore alla lista degli
i-nodeliberi.”Fonte: Il file system
Il SuperBlock è fondamentale per la gestione del file system: contiene tutte le metainformazioni necessarie per allocare nuovi file e i-node.
3. i-List:
“La terza regione è la
i-Listche contiene il vettore di tutti glii-node.”Fonte: Il file system
4. DataBlocks:
“L’ultima area è il
DataBlocks. Questa area è tipicamente molto più grande delle altre, poiché rappresenta la zona effettivamente diposnibile per la memorizzazione dei file. I blocchi liberi di questa zono sono organizzati in una lista collegata il cui primo indirizzo è contenuto nelSuperBlock.”Fonte: Il file system
Struttura degli i-node
Gli i-node contengono tutte le informazioni necessarie per gestire un file:
“Gli
i-nodesono quindi formati da:
- Tipo: indica se il file è ordinario, directory o speciale (dispositivi)
- Proprietario e Gruppo: indicano chi è l’utente proprietario del file e qual è il gruppo di appartenenza del proprietario
- Dimensione: indica in numero di blocchi occupati dal file in memoria di massa
- Data: indica la data dell’ultima modifica effettuata sul file
- Link: il numero di nomi che riferiscono il file (hard-link). Se si ha un solo nome, questo valore è
1- Bit di Protezione: è l’insieme dei
12bitche esprime la politica di protezione da applicare sul file. contiene i 9 bit dei premessi,SUID,GUIDeSTIcky bit.- Vettore di Indirizzamento: è costituito da un insieme di indirizzi che consente l’indirizzamento dei blocchi di dati sui quali è allocato il file (tipicamente sono
13 - 15).”Fonte: Il file system
Meccanismo di Indirizzamento Multiplo
Il vettore di indirizzamento è il cuore del sistema di allocazione UNIX. Analizziamo il funzionamento con un esempio concreto:
“Per capire meglio come vengono gestiti gli indirizzi nel vettore di indirizzamento supponiamo di avere:
- 13 indirizzi nel vettore (così come nelle prime versioni del sistema operativo)
- Blocchi di dimensione $D_b = 512 B$ con indirizzi su
32bit. Ciò significa che un blocco contiene $128$ indirizzi.”Fonte: Il file system
Il Problema dell’Indirizzamento Diretto:
“Se tutti gli indirizzi indirizzassero direttamente ai blocchi di dati avremmo che un file può essere al massimo $13 \cdot 512B = 6,5KB$, poco per gli standard moderni.”
Fonte: Il file system
La Soluzione: Indirizzamento a Più Livelli:
“Quello che si fa quindi è sfruttare l’indirizzamento multiplo:
- I primi
10indirizzi nel vettore accedono direttamente ai blocchi di dati, per una dimensione totale di $10 \cdot 512B = 5KB$- L’
11-esimo indirizzo nel vettore punta a un blocco indice sfruttando l’indirizzamento singolo. Ciò significa che solo questo permette l’accesso a $128 \cdot 512B = 64KB$ di memoria- Il
12-esimo indirizzo nel vettore punta invece a un blocco indice che punta a blocchi indici sfruttando l’indirizzamento doppio. In questo caso questo indirizzo permette l’accesso a $128 \cdot 128 \cdot 512B = 8MB$ di memoria- Il
13-esimo indirizzo nel vettore utilizza l’indirizzamento triplo indicizzando $128^3 \cdot 512B = 1GB$ di memoria.”Fonte: Il file system
Efficienza del Meccanismo:
Questa struttura è estremamente efficiente perché:
Dimensione Massima:
“Avendo solamente 13 indirizzi nel vettore la dimensione massima di un file è nell’ordine del Giga Byte $(5KB + 64KB + 8MB + 1GB)$.”
Fonte: Il file system
Rappresentazione delle Directory
Le directory in UNIX sono implementate come file speciali:
“Le directory sono rappresentati da un file, in cui contenuto ne descrive la struttura logica. In particolare ogni record coniene la coppia di informazioni
<nome_relativo, i-number>associate al file”Fonte: Il file system
Questa semplicità permette di utilizzare gli stessi meccanismi di allocazione e accesso dei file ordinari anche per le directory.
Confronto Complessivo dei Metodi
| Contigua | Lista Concatenata | Indice Semplice | UNIX (Indice Multiplo) | |
|---|---|---|---|---|
| Frammentazione | Esterna | Nessuna | Nessuna | Nessuna |
| Accesso Diretto | Efficiente (O(1)) | Inefficiente (O(n)) | Efficiente (O(1)) | Efficiente (O(livelli)) |
| Accesso Sequenz. | Efficiente | Efficiente | Efficiente | Efficiente |
| Overhead Spazio | Nessuno | Puntatori in blocchi | Blocco indice | Blocchi indice multipli |
| Scalabilità | Limitata | Buona | Limitata | Eccellente |
| Crescita File | Difficile | Facile | Limitata | Facile |
| Affidabilità | Buona | Problematica | Buona | Molto buona |
| Complessità | Bassa | Media | Media | Alta |
Approfondimenti:
Il metodo di allocazione UNIX rappresenta un capolavoro di ingegneria del software, bilanciando efficienza per file piccoli (la maggioranza) con capacità di gestire file grandi. La maggior parte dei file nei sistemi UNIX è inferiore a 5KB, quindi beneficia dell’indirizzamento diretto senza overhead. Solo i file più grandi pagano il costo degli accessi indiretti multipli.
La separazione tra nome del file (nelle directory) e descrittore (i-node) permette elegantemente la realizzazione degli hard link: più nomi per lo stesso file condividono lo stesso i-node. Il campo “Link” nell’i-node conta quanti nomi puntano al file, e solo quando questo contatore raggiunge zero il file viene effettivamente eliminato.
Il meccanismo degli i-node è stato così efficace che è stato adottato, con variazioni, da molti file system moderni (ext2, ext3, ext4 in Linux). La struttura continua a evolversi: i file system moderni utilizzano vettori di indirizzamento più grandi (15 indirizzi invece di 13) e blocchi di dimensioni maggiori, permettendo di gestire file nell’ordine dei Terabyte mantenendo la stessa struttura concettuale.
Domanda: Spiega il concetto di matrice degli accessi come modello per rappresentare lo stato di protezione di un sistema. Descrivi le due modalità di realizzazione della matrice: Access Control List (ACL) e Capability List (CL), confrontandone vantaggi e svantaggi. Illustra come UNIX implementa il meccanismo di protezione combinando ACL statiche (bit di protezione negli i-node) con Capability List dinamiche (tabella dei file aperti di processo). Infine, spiega il modello di sicurezza multilivello Bell-La Padula, descrivendo le due proprietà fondamentali (semplice sicurezza e proprietà *) e come questo modello previene attacchi di tipo Trojan Horse attraverso l’esempio fornito con soggetti e oggetti a diversi livelli di classificazione.
Risposta:
La protezione e la sicurezza nei sistemi operativi richiedono modelli formali per rappresentare e gestire i diritti di accesso alle risorse. La matrice degli accessi costituisce il fondamento teorico su cui si basano i meccanismi di protezione moderni, mentre i modelli multilivello come Bell-La Padula affrontano scenari di sicurezza più complessi.
Il Concetto di Matrice degli Accessi
La matrice degli accessi rappresenta uno strumento fondamentale per modellare la protezione:
“Un sistema di protezione può essere rappresentato utilizzando il modello della matrice degli accessi.”
Fonte: Protezione e sicurezza
Struttura e Funzionamento:
“Questo modello mantiene le informazioni che specificano il tipo di accesso che i soggetti hanno per gli oggetti all’interno di una struttura tabellare. Ciò consente di rappresentare lo stato di protezione garantendo il rispetto dei vincoli.”
Fonte: Protezione e sicurezza
La matrice è una tabella bidimensionale dove:
Esempio di matrice:
Dal file Protezione e sicurezza:
| $O_1$ | $O_2$ | $O_3$ | $D_1$ | $D_2$ | $D_3$ | |
|---|---|---|---|---|---|---|
| $D_1$ | read* |
read |
execute |
terminate |
receive |
|
| $D_2$ | owner write |
control receive |
terminate |
|||
| $D_3$ | write execute |
read |
send |
send receive |
Dinamicità del Sistema:
“Il numero di oggetti e dei soggetti all’interno della matrice è dinamicamente modificabile, permettendo ai processi di cambiare dominio anche durante l’esecuzione.”
Fonte: Protezione e sicurezza
Questo permette di modificare lo stato di protezione in modo controllato attraverso transizioni di stato.
Meccanismo di Verifica:
“Il meccanismo associato a questa struttura ha il compito di verificare se le richieste di accesso di un processo che opera in un certo dominio sono consentite oppure no.”
Fonte: Protezione e sicurezza
Quando un processo nel dominio $D_i$ richiede di eseguire un’operazione $M$ su un oggetto $O_j$, il meccanismo controlla che $M$ sia presente nella cella $(i,j)$ della matrice.
Politica DAC e Copy Flag:
“La politica del Discretional Access Control (
DAC) permette agli utenti di decidere il contenuto degli elementi della matrice. In particolare, alla creazione di un nuovo oggetto $O$, questo sarà aggiunto alle colonne della tabella, e l’utenteownerstabilirà come settare le intersezioni con le righe della tabella.”Fonte: Protezione e sicurezza
La propagazione dei diritti è regolata dal copy flag:
“Un soggetto $S_i$ può propagare un diritto di accesso $\alpha$ per un oggetto $X$ ad un altro soggetto $S_j$ se e solo se $S_i$ ha accesso a $X$ e $\alpha$ ha il copy flag.”
Fonte: Protezione e sicurezza
Il copy flag (*) determina se un diritto può essere copiato da un dominio ad un altro.
Realizzazione della Matrice: ACL vs CL
La matrice può essere memorizzata in due modi fondamentali:
“Possiamo vedere la matrice per accessi come una serie di righe (Capability List) o di colonne (Access Control List).”
Fonte: Protezione e sicurezza
Access Control List (ACL) - Memorizzazione per Colonne:
“La memorizzazione per colonne prende il nome di Access Control List (
ACL), ed è quella utilizzata nei sistemiUNIX. In questo modo ad ogni oggetto è associata una lista che contiene tutti i soggetti che possono accedevi e i relativi diritti di accesso.”Fonte: Protezione e sicurezza
Vantaggi dell’ACL:
Svantaggi dell’ACL:
Capability List (CL) - Memorizzazione per Righe:
“La memorizzazione per righe si dice invece Capability List (
CL), e associa ad ogni soggetto una lista che contiene gli oggetti da lui accessibili i relativi diritti di accesso.”Fonte: Protezione e sicurezza
Vantaggi della CL:
“A differenza delle
ACL, questa tecnica permette di avere un unica struttura dati sulla quale effettuare le ricerche da parte di un processo. I meccanismi di protezione che si basano su questa struttura sono quindi molto più efficaci, in quanto non è più necessario scorrere l’ACLdopo averla trovata per effettuare un check sui diritti del processo.”Fonte: Protezione e sicurezza
Svantaggi della CL:
“Nasce però un altro problema, ovvero quello di dove salvare le
CL. I soggetti, a differenza degli oggetti, cambiano dinamicamente e frequentemente, rendendo più complesso il salvataggio e la gestione dellaCLall’interno del sistema, andando a compensare il guadagno che ottenevamo sulla verifica a runtime.”Fonte: Protezione e sicurezza
“Ipotizziamo infatti che volessimo cambiare i diritti di accesso ad un sottoinsieme dei soggetti per un dato oggetto $O$. Mentre nell’
ACLci basta accedere all’oggetto stesso e agire in maniera sequenziale, nel caso dellaCLsarà necessario aggiornare tutte le singole liste sul sistema, introducendo un enorme overhead.”Fonte: Protezione e sicurezza
Implementazione in UNIX: Approccio Ibrido
UNIX adotta una soluzione elegante che combina i vantaggi di entrambi gli approcci:
ACL Statiche tramite Bit di Protezione:
“In
UNIX, oltre ad avere i bit di protezione, si mette in atto un altro meccanismo.”Fonte: Protezione e sicurezza
Negli i-node, UNIX memorizza:
“Poiché in
UNIXgli oggetti sono file, le singoleACLsono conservate all’interno del descrittore del file, sottoforma dei 9 bit di protezione:
- I primi
3 bitrappresentano i diritti di un singolo soggetto, l’owner.- I secondi
3 bitrappresentano i diritti dei soggetti che sono nel gruppo del proprietario.- Gli ultimi
3 bitstabiliscono i diritti per tutti gli altri soggetti.”Fonte: Protezione e sicurezza
Un elemento della ACL assume la forma: UID, GID: <diritti>.
Il Processo di Verifica alla Open:
“Quando un processo
PIDapre un file tramite unaopen, specificando un azione, il sistema operativo verifica:
- Che il file esista
- Che i diritti combacino”
Fonte: Protezione e sicurezza
La verifica procede per step:
“La verifica di queste condizioni accade per step:
- La
opencontrolla seUIDdel processo corrisponde con quello dell’ownerdell’oggetto. Se il soggetto è owner controlla i diritti di owner per verificare se l’operazione è consentita o meno- Altrimenti, controlla se il
GIDcorrisponde con quello dell’groupdell’oggetto. Se corrispondono allora effettuerà il controllo di protezione sui diritti del group owner- Altrimenti effettua il controllo sui diritti degli others”
Fonte: Protezione e sicurezza
CL Dinamiche tramite File Descriptor:
“Solamente quando si ha esito positivo su questi controlli, verrà inserito un nuovo file descriptor
fdnella Tabella dei File Aperti di Processo, che punterà alla tabella dei file aperti di sistema. La tabella dei file aperti di processo agisce a tutti gli effetti come unaCLdinamica del processo.”Fonte: Protezione e sicurezza
Il Meglio di Entrambi i Mondi:
“A livello statico quindi
UNIXsalva la matrice perACL, ma quando un processo esegue si costruisce dinamicamente la propriaCL, per avere i vantaggi di entrambe le soluzioni.”Fonte: Protezione e sicurezza
Questo approccio ibrido combina:
Modello di Sicurezza Multilivello: Bell-La Padula
Per ambienti che richiedono sicurezza rigorosa, vengono introdotti modelli multilivello:
“La maggior parte dei sistemi operativi permette ai singoli utenti di determinare chi possa leggere e scrivere i loro file e i loro oggetti, secondo il Controllo Discrezionale degli Accessi, o
DAC. In alcuni ambienti è però richiesta una sicurezza più rigorosa e controllata (ospedali, aziende, ambienti militari, …). In questo caso sono stabilite delle regole su chi può vedere cosa, modificabili solo da chi possiede permessi speciali. Questa tecnica ha il nome di Controllo degli Accessi Obbligatorio, oMAC.”Fonte: Protezione e sicurezza
Il Modello Bell-La Padula:
“Un ulteriore criterio di sicurezza può essere quello di avere diversi livelli di protezione, ognuno con criteri di accesso diversi. Questo modello si chiama Modello Bell-La Padula, progettato per gestire la sicurezza in ambiente militare.”
Fonte: Protezione e sicurezza
I Quattro Livelli di Classificazione:
“Il Modello Bell-La Padula stabilisce 4 livelli di sicurezza:
- Non classificato (pubblico)
- Confidenziale
- Segreto
- Top-Secret (privato)”
Fonte: Protezione e sicurezza
Applicazione ai Soggetti e agli Oggetti:
“I livelli di sicurezza si applicano sia agli oggetti $O$ che ai soggetti $S$.”
Fonte: Protezione e sicurezza
Il modello definisce una funzione $SC(\cdot)$ che rappresenta il livello di sicurezza di un’entità (Security Classification).
Le Due Proprietà Fondamentali
1. Proprietà di Semplice Sicurezza (Simple Security Property):
“Le regole che sfruttano la funzione:
- Proprietà di semplice sicurezza: un soggetto in esecuzione a livello di sicurezza $k$, può leggere solo oggetti al suo livello o a livelli inferiori, secondo la regola che $SC(S) \ge SC(O)$.”
Fonte: Protezione e sicurezza
Questa regola previene che informazioni riservate vengano lette da soggetti non autorizzati: un soggetto può “guardare in basso” ma non “in alto” nella gerarchia.
2. Proprietà * (Star Property):
”- Proprietà *: un soggetto in esecuzione al livello di sicurezza $k$, può scrivere solo oggetti al suo livello o a livelli superiori, secondo la regola che $SC(S) \le SC(O)$.”
Fonte: Protezione e sicurezza
Questa regola cruciale previene che informazioni riservate vengano copiate in oggetti di livello inferiore: un soggetto può “scrivere in alto” ma non “in basso”.
Prevenzione di Attacchi Trojan Horse
Il modello Bell-La Padula è stato specificamente progettato per prevenire attacchi come i Trojan Horse. Analizziamo l’esempio fornito:
“Per chiarire come opera il modello prendiamo la seguente tabella:”
Fonte: Protezione e sicurezza
| $O_1$ (file riservato) | $O_2$ (trojan-horse) | $O_3$ (file di appoggio) | |
|---|---|---|---|
| $S_1$ | ownerreadwrite |
execute |
write |
| $S_2$ | ownerexecute |
ownerreadwrite |
Scenario dell’Attacco:
“Ipotizziamo che $O_2$ abbia accesso alle seguenti istruzioni:
- Aprire in lettura $O_1$
- Aprire in scrittura $O_3$”
Fonte: Protezione e sicurezza
Comportamento senza Bell-La Padula (solo DAC):
“Se $O_2$ venisse eseguito da $S_2$ queso non potrebbe accedere in nessun modo ad $O_1$, in quanto $S_2$ non possiede alcun diritto sull’oggetto.”
Fonte: Protezione e sicurezza
Ma:
“Se $O_2$ venisse invece eseguito da $S_2$, questo opererà con i diritti di $S_2$ potendo:
- Accedere in lettura a $O_1$
- Accedere in scrittura a $O_3$
Facendo così $S_2$ è in grado di leggere indirettamente il contenuto di $O_1$, attraverso la copia in $O_3$, effettuata da $O_2$ per conto di $S_1$.”
Fonte: Protezione e sicurezza
Questo è precisamente un attacco Trojan Horse: $S_2$ inganna $S_1$ a eseguire un programma ($O_2$) che copia informazioni riservate in un file accessibile a $S_2$.
Protezione con Bell-La Padula:
“Sfruttando invece la classificazione Bell-La Padula possiamo classificare come Pubblico $(P)$ e Privato $(R)$ i soggetti e gli oggetti:
- $SC(S_1) = SC(O_1) = R$
- $SC(S_2) = SC(O_2) = SC(O_3) = P$”
Fonte: Protezione e sicurezza
Ora, quando $O_2$ viene eseguito da $S_1$:
“Se adesso $O_2$ venisse eseguito da $S_1$:
- Potrebbe accede in lettura per la Proprietà di semplice sicurezza $SC(S_1) = R = SC(O_1)$
- Non potrebbe accedere in scrittura per la Proprietà * $SC(S_1) = R \ge SC(O_3) = P$”
Fonte: Protezione e sicurezza
L’Attacco è Bloccato:
La Proprietà * impedisce al processo (che opera con clearance $R$) di scrivere su $O_3$ (che ha classificazione $P$), poiché $SC(S_1) = R \not\le SC(O_3) = P$. Il Trojan Horse può leggere il file riservato, ma non può copiarne il contenuto in un file di livello inferiore!
Limiti e Considerazioni del Modello
Obiettivo del Modello:
“È importante sottolineare che il modello Bell-La Padula è stato concepito per __mantenere segreti i dati_ e non per garantirne l’integrità.”
Fonte: Protezione e sicurezza
Il Modello BiBa per l’Integrità:
“Esiste infatti un altro modello, detto BiBa, che si preoccupa di garantire l’integrità dei dati. Questo modello di fatto inverte le regole del modello Bell-La Padula:
- La lettura è permessa solo su file di livello equo o superiore $SC(S) \le SC(O)$.
- La scrittura solo su file di livello equo o inferiore $SC(S) \ge SC(O)$.”
Fonte: Protezione e sicurezza
Incompatibilità tra i Modelli:
“Ovviemente i due modelli, BiBa e Bell-La Padula, non sono compatibili e non possono essere realizzati contemporaneamente.”
Fonte: Protezione e sicurezza
Questa incompatibilità deriva dal fatto che le regole sono esattamente opposte. Un sistema deve scegliere se privilegiare la confidenzialità (Bell-La Padula) o l’integrità (BiBa).
Tabella Comparativa dei Metodi di Protezione
| Aspetto | ACL | CL | UNIX (Ibrido) |
|---|---|---|---|
| Memorizzazione | Per oggetto (colonne) | Per soggetto (righe) | ACL statiche + CL dinamiche |
| Efficienza accesso | Richiede ricerca | Accesso diretto | Accesso diretto (runtime) |
| Modifica permessi | Facile per singolo oggetto | Richiede aggiornare tutte le CL | Modifica centralizzata su i-node |
| Scalabilità | Numero di oggetti | Numero di soggetti | Ben bilanciata |
| Revoca | Immediata | Complessa | Effettiva alla prossima open |
| Gestione runtime | Inefficiente | Efficiente | Efficiente (file descriptor) |
| Persistenza | Naturale (nel descrittore) | Problematica | ACL persistenti, CL temporanee |
| Uso pratico | Windows (NTFS), componente UNIX | Raramente pura | Sistemi UNIX/Linux |
Approfondimenti:
La matrice degli accessi e il modello Bell-La Padula rappresentano contributi fondamentali alla teoria della sicurezza informatica, sviluppati negli anni ‘70 ma ancora rilevanti oggi. Il modello Bell-La Padula è stato utilizzato in sistemi militari classificati e ha influenzato lo sviluppo di SELinux (Security-Enhanced Linux), che implementa il Mandatory Access Control sopra il tradizionale DAC di UNIX.
Un aspetto interessante è che il concetto di “ruolo” nei moderni sistemi RBAC (Role-Based Access Control) estende ulteriormente questi modelli, permettendo una gestione più flessibile delle autorizzazioni separando i diritti dagli utenti specifici e assegnandoli invece a ruoli funzionali.
La scelta tra ACL e CL ha implicazioni pratiche significative: le ACL sono più adatte per sistemi dove i permessi cambiano raramente ma vengono verificati frequentemente, mentre le CL sono preferibili quando i soggetti cambiano dinamicamente ma i permessi rimangono stabili. L’approccio ibrido di UNIX rappresenta una soluzione pragmatica che ottimizza entrambi gli scenari: verifiche frequenti durante l’esecuzione (CL) e gestione centralizzata dei permessi (ACL).