Sei sulla pagina 1di 42

27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Sistemi Operativi
Linee guida formattazione 3

Introduzione ai Sistemi Operativi 4


Introduzione ai Sistemi Operativi 4
Definizione di Sistema Operativo 4
Tipi di sistemi operativi 4
Prima evoluzione 4
Seconda evoluzione 5
A strati 5
UNIX 5
Microkernel 5
Kernel modulare 6
Funzioni del sistema operativo 6
Funzioni principali di un sistema operativo 6
Interfacce per la comunicazione 6
Esecuzione dei programmi 6
Gestione dei dispositivi I/O 6
Gestione del File System 6
Comunicazione tra processi 6
Controllo degli errori 7
Allocazione delle risorse 7
Protezione e Sicurezza 7
Protezione 7
Preemption 7
Modalità privilegiata 8
Registri speciali 9
System Call 9
Funzionamento delle system call 9
Passaggi di parametri alle system call 9
Tipologie di system call 10

Processi 11
Gestione dei processi 11
Composizione di un processo 12
Stati di un processo 12
Process Control Block PCB 12
Creazione di un processo 12
Terminazione di un processo 13
Alcune delle system call più importanti 13
Context switch 14

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 1/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Tipi di scheduler 14
Politiche di scheduling 14
Punti di scheduling 14
Cambio di contesto 15
Context switch overhead 15
Kernel preemption 15
Codice rientrante 15
Kernel rientrante e preemptive 16
Threads vs Processi 16
Perchè si utilizzano i thread 16
Programmazione multi-thread 17
Terminazione di un thread 17
Up-Calls 17
Thread in Windows, Linux e Java 17
Comunicazione tra processi 18
Comunicazione tra processi 18
Paradigma produttore-consumatore 18
Comunicazione tramite message passing 19
Architettura client-server 19

Sincronizzazione 20
La sezione critica 20
Soluzioni al problema della sezione critica 20
Soluzione di Peterson 20
Sincronizzazione Hardware 20
Semafori 21
Deadlock e Starvation 21
Algoritmo del banchiere 22
Problemi di sincronizzazione e deadlock 22
Buffer limitato 22
Problema dei lettori-scrittori 22
La cena dei 5 filosofi 23
Barbiere che dorme 23
Strumenti di sincronizzazione dei SO 24

Scheduling 25
Concetti fondamentali 25
Criteri di scheduling 25
Politiche di scheduling 26
FCFS (First Come First Serve) 26
SJF (Shortest Job First) 26
Scheduler prioritario 27
Round Robin (RR) 27

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 2/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Code a multiplo livello 27


Scheduling in architetture multi-processore 28
Bilanciamento del carico 29

Scheduling in Linux 30
Linux scheduler 2.4.x 30
Coda dei processi 30
Stato dei processi 30
Principali caratteristiche dello scheduler 30
Quanto di tempo 30
Priorità dei processi 31
Process Descriptor 31
Funzione schedule() 31
Linux Scheduler 2.6.x 32
Scheduler SMP 32
CFS (Completely Fair Scheduler) 33
Implementazione 33

Memoria principale 34
Organizzazione della memoria principale 34
Binding 34
MMU (Memory Management Unit) 35
Dynamic Loading e Dynamic Linking 35
Allocazione contigua e frammentata 35
Paginazione 36
Implementazione della tabella di paging 36
Pagine condivise 36
Struttura della tabella di paging 37
Struttura della tabella delle pagine 38
Tabella delle pagine hash 38
Tabella delle pagine invertita 38
Segmentazione 38
Architettura di segmentazione 38
Differenze tra pagine e segmenti 39
Segmentazione Intel Pentium 39
Segmentazione AMD64 39

Memoria virtuale 40
Paginazione su richiesta 40
Sostituzione delle pagine 40
Allocazione dei frame 40
File mappati in memoria 40

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 3/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Linee guida formattazione


● Usare il grassetto solo per termini chiave dell’argomento
● Usare il corsivo per termini che non sono corretti tecnicamente ma che esemplificano
bene il concetto
● Usare le intestazione per dividere sezioni, moduli ecc

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 4/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Introduzione ai Sistemi Operativi

Introduzione ai Sistemi Operativi

Definizione di Sistema Operativo


Un sistema operativo è un programma che si interpone tra utente e hardware e che si
prefigge l'obiettivo di rendere facile ed efficiente per l’utente la risoluzioni di problemi tramite
il computer.
L’Ecosistema di un computer può essere diviso in 4 parti:
1. Hardware (CPU, RAM, I/O…)
2. Sistema Operativo
3. Applicazioni utilizzate da un utente
4. Utente
il compito del sistema operativo è quello di allocare e utilizzare risorse hardware in modo
corretto e sicuro, fornendo alle applicazioni (che poi verranno utilizzate dall’utente) una base
su cui appoggiarsi per funzionare.
Si dice kernel l’unico programma del sistema operativo che inizia quando il computer viene
acceso e finisce quando viene spento.

Tipi di sistemi operativi

Prima evoluzione
Il sistema operativo è in grado di eseguire una sola applicazione alla volta.
L’utilizzo di CPU è massimo ma appena l’elaborazione necessita di un evento di I/O la
CPU entra in fase di stallo che viene terminata solo quando l’evento viene generato.
Tuttavia quando un programma viene eseguito diventa padrone totale della macchina e
potrebbe portare a corruzione della memoria dello stesso SO o a un ciclo infinito che
sarebbe risolvibile soltanto con lo spegnimento della macchina.
Per risolvere tali problemi è necessario che il sistema operativo fornisca
● preemption: la possibilità di fermare l’esecuzione di un processo quando non sta
attivamente utilizzando la CPU
● protezione della memoria: la possibilità di assegnare porzioni di memoria al
sistema che non possono essere accedute dalle applicazioni

Un esempio di SO di prima evoluzione è MS-DOS che viene creato da Microsoft con l’intento
di raggruppare funzioni comuni nello sviluppo di applicazioni nel più piccolo spazio possibile.
Nonostante MS-DOS presenti una struttura e non sia totalmente a cazzo la separazione di
funzionalità non è netta e porta a confusione e difficoltà nello stabilire il ruolo di ogni
funzione: un’applicazione utente accede ai dispositivi hardware tramite funzione del sistema
operativo, ma è anche in grado di farlo direttamente bypassando il sistema operativo.

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 5/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Seconda evoluzione
Nasce dal tentativo di sfruttare a pieno le potenzialità della CPU implementando la
multiutenza.
La CPU viene utilizzate da più utenti diversi che sono in grado di eseguire programmi
diversi, in questo modo la CPU viene sfruttata maggiormente poiché nei momenti di stallo
dell’applicazione dell utente A viene eseguita l’applicazione dell’utente B.
Tuttavia si possono ancora presentare momenti in cui un processo potrebbe monopolizzare
la CPU o corrompere l’area di memoria utilizzata dalle applicazioni degli altri utenti.
Per risolvere tali problemi il SO deve fornire:
● protezione dagli utenti che eseguono ad esempio un ciclo infinito
● protezioni della memoria di un’applicazione dalle altre applicazioni

A strati
Il SO è diviso a strati in cui lo strato più interno rappresente l’hardware e quello più esterno
l’interfaccia per l’utente.
Ogni strato può accedere alle funzioni degli strati sottostanti.
Così facendo si garantisce modularità e separazione dei compiti all’interno del SO.

UNIX
Viene detto approccio monolitico ed è diviso in due parti principali:
● kernel
● applicazioni di sistema
Il kernel offre un’interfaccia tramite quale le applicazioni di sistema sono in grado di
comunicare con l’hardware e si occupa di comunicare con l’hardware, gestire la memoria,
effettuare lo scheduling della CPU, gestire i driver, gestire il filesystem ecc.
Esso risulta essere una componente piuttosto piena del SO, ma tale implementazione è
dovuta alle risorse hardware limitate su cui i primi sistemi UNIX vennero installati.

Microkernel
Viene SO a microkernel un SO il cui kernel svolge soltanto le funzionalità essenziali per far
sì che la macchina sia utilizzabile.
Ogni funzionalità aggiuntiva che non è strettamente necessaria (filesystem, driver per l’I/O
ecc.) viene implementata come moduli che comunicano con il kernel. Le applicazione
dell’utente comunicheranno tramite messaggi con i moduli che a loro volta comunicheranno
con il kernel.
Tale struttura è più sicura, affidabile, facile da estendere e da adattare a strutture
hardware diverse poiché le parti fondamentali che consentono l’utilizzo della macchina
risiedono in una piccola parte del SO rispetto ad un approccio monolitico.
Tuttavia tale approccio potrebbe risultare in un overhead per via dei moduli e del kernel che
si interpongono tra un’applicazione dell’utente e l’hardware.

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 6/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Kernel modulare
Utilizzato nei SO più moderni (Solaris) utilizza i concetti della programmazione ad oggetti:
ogni funzionalità fondamentale è separata dalle altre e rappresenta un modulo che
comunica tramite un’interfaccia con gli altri moduli.
Tramite kernel modulare è possibile caricare moduli solo quando è necessario.

Funzioni del sistema operativo

Funzioni principali di un sistema operativo

Interfacce per la comunicazione


Il SO deve rendere l’utilizzo del sistema semplice e piacevole, e per raggiungere tale scopo
utilizza le interfacce (UI User Interface) che si dividono in:
- CLI Command Line Interface;
- GUI Graphics User Interface.

Esecuzione dei programmi


Il SO si deve occupare di caricare un programma nella memoria principale ed eseguirlo,
segnalando eventuali errori e proteggendo l’esecuzione degli altri programmi che sono
eventualmente eseguiti nello stesso momento.

Gestione dei dispositivi I/O


Un programma in esecuzione potrebbe richiedere dispositivi di I/O, per cui il SO deve gestire
l’allocazione di tali risorse (ex. file, stampante, …) in modo trasparente rispetto all’utente.

Gestione del File System


Il SO deve gestire tutto ciò che riguarda i file, i loro permessi, la comunicazione tra loro, ecc.
ecc. …

Comunicazione tra processi


Due processi potrebbero, durante la loro esecuzione, aver bisogno di scambiarsi
informazioni. Questo scambio di informazioni viene gestito dal SO che lo effettua in due
diverse modalità:
- shared memory → area di memoria franca, ossia condivisa, dove entrambi i
processi possono leggere e scrivere informazioni;
- scambio di pacchetti → message passing ossia i due processi comunicano
direttamente tra di loro esponendo un interfaccia per ricevere messaggi e
implementandone una per inviarli.

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 7/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Controllo degli errori


Il SO deve prevenire il maggior numero di errori possibili così da essere il più sicuro ed
efficiente possibile. Tali errori possono scaturirsi nella CPU, nella memoria fisica, nei
dispositivi I/O o in un qualsiasi programma che viene eseguito dall’utente. Per ciascun tipo di
errore, il SO dovrà adottare un’opportuna azione per correggerlo e prevenirlo in futuro.

Allocazione delle risorse


Il SO deve gestire l‘allocazione delle risorse disponibili della macchina in modo sicuro ed
efficiente. Potrebbe infatti capitare che più utenti richiedano nello stesso momento la stessa
risorsa, quindi il SO dovrà gestire l’allocazione di tale risorsa per ogni utente in modo sicuro.

Protezione e Sicurezza
Il SO deve garantire:
- protezione → controllo degli accessi al sistema;
- sicurezza → controllo dell’integrità dei dati, non permettendone la violazione.

Protezione
Il SO deve proteggere se stesso, i suoi dati, i suoi programmi, i suoi utenti e le sue risorse da
quelli che vengono definiti bad users and programs.
Per garantire la protezione di tali risorse, vengono utilizzate tre diverse tecnologie:
- preemption → consente di arrestare un processo contro la sua volontà;
- modalità privilegiata → individua uno stato in cui un insieme di istruzioni, che
possono essere svolte solo quando la CPU si trova in un particolare stato, hanno il
controllo completo della macchina;
- registri speciali → utilizzati per memorizzare informazioni riguardo alla memoria dei
processi.

Preemption
Tale tecnica protegge la CPU tramite l’utilizzo dell’interrupt. L’utilizzo dell’interrupt permette
al SO di negare l’utilizzo della CPU ad un processo contro la sua volontà, ed assegnarla ad
un altro processo. Senza la preemption, un programma maligno potrebbe monopolizzare
l’utilizzo della CPU.
Per l‘implementazione della preemption viene adottata la tecnica del time-sharing, ovvero
viene individuata una piccola fetta di tempo (ex. 25/40 ms) in cui ogni processo può far uso
della CPU. Al termine di questo slice di tempo, viene generato un interrupt che segnala al
SO che è arrivato il momento di negare la CPU al processo corrente e allocarla per un altro
processo.

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 8/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Quando viene invocato l’interrupt, vengono eseguite due operazioni:


- context switch → cambio di contesto, operazione in cui viene salvato lo stato del
processo corrente così da riprendere esattamente da dove si era lasciato nel
momento in cui tale processo riotterrà la CPU;
- scheduling → tecnica che indica quale processo è il più adatto per utilizzare la CPU
nel prossimo slice di tempo.

Modalità privilegiata
Consente di proteggere l’I/O e la RAM.
Esistono due modalità in cui la CPU può eseguire istruzioni:
- Modalità PRIVILEGIATA → consente di eseguire tutte le istruzioni dell’instruction set
- Modalità UTENTE.
Vi sono diverse situazioni in cui il SO operativo interviene per eseguire in modalità
PRIVILEGIATA, tra queste:
- L’accesso a routine che devono essere eseguite in caso di errore: tali routine sono
definite in un’area di memoria protetta (EVT - Exception Vector Table) che può
essere acceduta solo in modalità PRIVILEGIATA
- L’accesso a dispositivi di I/O: gli accessi ai dispositivi di I/O in modalità UTENTE
vengono effettuati tramite l’utilizzo di system call.
Una system call fa entrare in modalità privilegiata la CPU e viene invocata tramite
apposite istruzioni di sistema (che generano una trappola) alle quali si dovranno
passare opportuni parametri tramite l’uso di registri particolari.
L’utilizzo di system call permette al SO di standardizzare e controllare l’accesso ai
dispositivi di I/O da parte delle applicazioni.

Se si è in modalità UTENTE e si prova ad eseguire una determinata azione, potenzialmente


dannosa e quindi eseguibile solo in modalità PRIVILEGIATA, viene attivata una trappola, un
interrupt software (chiamato anche eccezione) cambierà la cella a cui il program counter
sta puntando in modo da bloccare l’istruzione e eseguire un routine speciale definita dal SO.
Tale routine potrebbe essere ad esempio controllare che l’utente abbia i permessi di
eseguire l’istruzione incriminata.

La CPU entra in modalità PRIVILEGIATA modificando un particolare bit interno (che funge
da flag), tale bit viene modificato soltanto dal SO e ogni tentativo di modifica da parte di un
programma esterno viene bloccata dallo stesso SO.

All’accensione, il boot loader (ossia la routine che inizializza la macchina e avvia il sistema
operativo) viene eseguito in modalità privilegiata, e quindi il SO è molto vulnerabile in quanto
nel caso in cui venisse preso il controllo del pc in questo momento, l’attacker potrebbe
eseguire qualsiasi istruzione, accedere ad ogni cella di memoria e utilizzare ogni periferiche.

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 9/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Registri speciali
L’utilizzo di registri speciali è una tecnica che contribuisce alla protezione del sistema
operativo.
In particolare protegge lo stesso SO (il codice di esecuzione, EVT, ecc.) e la memoria
utilizzata dai diversi processi in esecuzione nella macchina.
Vengono introdotti i concetti di base e limite, che identificano una precisa area all’interno
della memoria (base da dove parte, limite fin dove arriva).
Il contenuto di tale area è modificabile solo dal SO ossia ogni processo dovrà richiedere al
SO di modificare aree di memoria di interesse.
Ciò consente integrità dei dati per ogni processo: la memoria utilizzata da un processo non
può essere sporcata da un altro processo poiché il SO si occupa di bloccare e prevenire
accessi non consentiti.

System Call
Una system call è un’interfaccia di programmazione ai servizi offerti dal sistema operativo.
Sono funzioni tipicamente scritte in un linguaggio ad alto livello (C o C++) e sono utilizzate
dai programmi tramite le API (Application Program Interface) che le rendono però non
portatili cioè ogni SO implementa le proprie system call (Windows32 API, POSIX API, Java
API).

Funzionamento delle system call


Tipicamente ogni system call è associata ad un numero e in memoria è presente una tabella
gestita dall’interfaccia delle system-call in cui sono memorizzate tutte le corrispondenze
(numero - routine da eseguire).
Quando un processo invoca una system call, l’interfaccia delle system call richiama la
precisa routine memorizzata nel kernel, la manda in esecuzione, e ne aspetta la
terminazione per segnalare eventuali errori. Il processo invocante non è a conoscenza del
come il SO ha implementato la system call.

Passaggi di parametri alle system call


In alcuni casi per eseguire una system call potrebbe essere necessario passare dei
parametri alla system call. Il passaggio di tali parametri può avvenire tramite:
- Registri → il processo invocante scrive in dei particolari registri i propri dati, e il SO
andrà a leggere tali registri estrapolando i dati necessari da passare alla system call;
- Tabella → prima della trappola utilizzata per invocare la system call, viene messo sul
primo registro l’indirizzo base di memoria della tabella dove sono memorizzati i
parametri da passare, e nel secondo registro l’ampiezza della tabella, così il SO
riuscirà ad individuare la tabella nella memoria, tale approccio (utilizzato da Linux e
Solaris) permette il passaggio di una struttura complessa ad una system call.

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 10/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

- Stack/pila → il SO mette a disposizione un’area di memoria dove il processo scrive i


parametri da passare alla system call inserendoli in uno stack (PUSH), e il SO poi
legge ed utilizza i dati dalla pila creata dal processo (POP). Questa tecnica ha un
vantaggio importante, quando la system call infatti ha dei valori di ritorno, si invertono
i ruoli, con il SO che fa il PUSH dei valori di ritorno e il processo che fa il POP di tali
valori → simmetria.

Tipologie di system call


- Gestione dei processi
- Gestione dei file
- Gestione dei device
- Gestione delle informazioni
- Comunicazione

10

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 11/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Processi

Gestione dei processi


L’utilizzo di processi deriva dal modo di pensare umano. Infatti, noi umani quando ci
troviamo ad affrontare un problema di grandi dimensioni, lo dividiamo in più sottoproblemi di
dimensioni minori, detti task, ciascuno dei quali indipendente dagli altri task.
Il SO divide i processi(=task) in due categorie:
- CPU intensive → quei processi che utilizzano la CPU per un tempo molto maggiore
della media di utilizzo della CPU;
- I/O intensive → quei processi che utilizzano poco la CPU ma utilizzano per un
tempo maggiore della media i dispositivi di I/O.

E’ impossibile però per il SO catalogare a priori un processo prima della sua esecuzione. Il
SO infatti etichetta il processo solamente quando esso è in esecuzione, infatti un processo
potrebbe potenzialmente essere CPU intensive per un periodo di tempo e per il restante
tempo di esecuzione potrebbe comportarsi come I/O intensive.

L'obiettivo principale che viene perseguito con l’utilizzo di processi è quello di sfruttare al
meglio la CPU: eseguendo ad esempio un processo I/O intensive potrebbe esserci un lasso
di tempo piuttosto lungo in cui la CPU non esegue nessuna operazione (in quanto attendo la
risposta dell’I/O). In tal caso è lecito pensare di occupare la CPU con un altro processo
mentre si attende l’evento dell’I/O.
Tale obiettivo viene centrato tramite l’uso della multiprogrammazione disponendo il
computer in modo che sia in grado di eseguire più processi contemporaneamente.
Esistono due tipi principali di multiprogrammazione
- Virtuale: l’esecuzione di processi viene sovrapposta (un processo utilizza CPU
mentre un altro attende I/O).
- Reale: sono disponibili più di 1 CPU per cui diversi processi vengono effettivamente
eseguiti contemporaneamente.

Nel caso della multiprogrammazione virtuale viene introdotto il concetto di time burst ossia
lo slice di tempo che viene garantito a un processo per eseguire codice sulla CPU.
Tale burst viene inizialmente definito dal SO e può essere modificato dinamicamente a
seconda della frequenza di utilizzo della CPU (la maggior parte dei processi usano la cpu
per Xms poi attendono per I/O).

È importante specificare che il concetto di processo è del tutto distinto da quello di


programma: un programma è infatti un’entità che risiede nel filesystem e che viene caricato
in memoria per essere eseguito, un processo è una parte di programma caricata in memoria.

11

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 12/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Composizione di un processo
Un processo in esecuzione è rappresentato in memoria da:
- sezione CODE/TEXT: contiene le istruzioni macchina che vengono eseguite sulla
CPU. Dimensione fissa.
- sezione DATA: contiene le variabili globali del programma. Dimensione fissa.
- sezione STACK: contiene le variabili utilizzate per gestire le chiamate a funzioni del
processo consentendo l’utilizzo di funzioni ricorsive. Dimensione variabile.
- sezione HEAP: contiene le strutture dati (viene utilizzata la funzione MALLOC per
allocare memoria). Dimensione variabile.

Stati di un processo
- NEW: processo appena creato e allocato in memoria.
- RUNNING: il processo è in esecuzione nella CPU (ciclo fetch-decode-execute).
- WAITING: in attesa di I/O dopo una richiesta o di un’informazione da un altro
processo.
- READY: pronto per essere eseguito, in attesa della CPU.
- TERMINATED: processo terminato → terminazione deferita, cioè il SO non termina
bruscamente il processo, ma avviene prima un controllo (circa 50ms).
- ZOMBIE: particolare stato presente in Linux, che indica un processo il cui padre è
terminato senza attendere la terminazione del figlio.

Process Control Block PCB


Il Process Control Block (PCB) è una struttura dati abbastanza consistente (circa 3-4 Kbyte)
associata ad ogni processo. E’ formata dai seguenti campi:
- stato del processo
- PC (program counter, indica quale istruzione sta eseguendo la CPU)
- registri della CPU
- informazioni di scheduling della CPU
- informazioni sulla gestione della memoria
- statistiche del processo
- informazioni dello stato di I/O

Creazione di un processo
Ogni processo è creato da un altro processo. Il processo creatore viene chiamato processo
padre (init su linux), e rispettivamente il processo creato si chiamerà processo figlio, che a
sua volta creerà altri figli, dando vita ad una struttura ad albero.
Il padre ha la responsabilità di tutto ciò che che esegue il figlio (cercare di evitare la
creazione di processi zombie), e dovrà aspettare la terminazione di ogni processo figlio.
Padre e figlio possono condividere o meno risorse: nel caso in cui non le condividano si
verifica il paradigma concorrente.
Appena creato, il processo figlio è una copia del processo padre (due istanze dello stesso
programma), il figlio poi potrà decidere se caricare o meno nel suo spazio di memoria un

12

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 13/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

nuovo programma. Dopo la creazione del processo figlio, il processo padre continuerà la sua
esecuzione, aspettando la terminazione di tutti i suoi processi figli.
La creazione di un figlio, su linux, avviene tramite la system call fork() il cui valore di ritorno
è il pid (process id) del nuovo processo creato. Inizialmente il pid di un processo figlio
appena creato è 0, poi in seguito è assegnato automaticamente dal SO. Il processo padre
può continuare la sua esecuzione parallelamente o aspettare la terminazione del figlio e poi
riprendere l’esecuzione.
Nel caso il processo padre termini prima del processo figlio, dando vita ad un processo
zombie, sarà il processo init (su linux) ad assumere il ruolo di padre e attendere la
terminazione del figlio raccogliendo il suo valore di ritorno.

Terminazione di un processo
La terminazione di un processo può avvenire:
1. dopo che segue la sua ultima istruzione
2. quando viene bloccato e terminato direttamente dal SO per utilizzo di troppe risorse,
o inutilità, ecc.ecc.
3. quando il padre ne richiede la terminazione
4. quando la RAM è satura e ci sono troppe risorse allocate e viene interrotto
direttamente dal SO
5. quando il processo padre termina, e di conseguenza vengono terminati tutti i
processi figli a cascata (solo in alcuni vecchi SO che non consentono altrimenti di
gestire processi zombie).

Alcune delle system call più importanti


- fork() → genere un processo figlio, ha come valore di ritorno il pid del figlio appena
creato.
- exec() → sostituisce la sezione code del processo corrente con quella di un
programma memorizzato nel file system (paradigma fork-exec presente solo su linux
→ padre chiama fork() e figlio chiama exec() per eseguire codice diverso dal padre).
- wait() → come parametro riceve il pid del processo figlio di cui aspettare la
terminazione, dopo di che il padre che ha invocato la wait() riprenderà la sua
esecuzione. Il valore di ritorno è il valore di ritorno del processo figlio.
- exit() → termina il processo. Tra parentesi viene passato un intero che potrà essere
utilizzato come informazione da parte del processo padre.

13

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 14/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Context switch
I processi sono organizzati in code i cui elementi sono i PCB di ogni processo o un puntatore
ad essi. Il processo attualmente in esecuzione viene indicato con current.
Esiste una coda per ogni ogni stato di processo (coda per i processi in stato ready, per quelli
in I/O waiting, ecc.ecc.).
Nella coda READY vengono messi tutti i processi pronti per essere eseguiti e che sono in
attesa della CPU, nella coda I/O waiting tutti i processi in attesa di una risorsa di I/O (esiste
una coda per ogni risorsa) e così via.
Ogni processo può migrare da una coda all’altra a seconda del suo stato.

Tipi di scheduler
Esistono due tipi di scheduler:
- long-term scheduler → modulo del SO che viene invocato ogni 10 secondi (in
alcuni SO anche ogni minuto) che controlla i processi e seleziona quelli che possono
essere trasferiti dalla memoria virtuale a quella principale, ovvero li trasferisce e li
distribuisce nelle varie code viste in precedenza. Controlla il grado della
multiprogrammazione.
- short-term scheduler → chiamato anche CPU-scheduler, è un modulo del SO che
decide a quale dei processi nella coda dei processi ready assegnare la CPU. La
decisione del processo deve avvenire in tempo brevissimo, circa 1ms.

Politiche di scheduling
1. Non dimenticare nessun processo.
2. Eseguire per primi i processi con priorità maggiore.
3. Assegnare la CPU entro le deadline, cioè entro tempi ragionevoli: alcuni processi
(ad esempio quelli realtime) potrebbero necessitare di prendere spesso il controllo
della CPU pena il malfunzionamento del programma (un player video deve poter
aggiornare l’immagine abbastanza spesso che l’utente non si accorga di niente).
4. Ottimizzare il più possibile il meccanismo di esecuzione dei processi (utilizzo della
CPU, tempo di risposta, ecc.ecc.)

Punti di scheduling
I punti di scheduling sono quei particolari punti durante l’esecuzione di un processo in cui è
viene effettuata un’operazione di scheduling da parte del SO. In questi particolari punti il SO
ottiene il controllo, scegliendo le azioni da eseguire.
Questi punti di scheduling sono:
- trappole → system calls, errori, page faults, ecc.ecc.
- interrupts → I/O interrupt, timer interrupt, ecc.ecc.
- particolari punti esplicitati del processo → sleep, yield, syscalls, ecc.ecc.

Nei punti di scheduling, il processo corrente può essere privato dell’utilizzo della CPU e
rimpiazzato da un altro processo dando vita ad un context switch, ovvero ad un cambio di
contesto.

14

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 15/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Cambio di contesto
Il processo corrente, ovvero quello che viene eseguito in questo momento dalla CPU, viene
rimpiazzato da un altro processo. Il SO salva lo stato del processo che verrà rimpiazzato
(save), e carica lo stato del nuovo processo da eseguire (load).
I dati che vengono salvati sono generalmente gli indirizzi dei registri di memoria, i dati,
ecc.ecc. ma questi dipendono dal tipo di architettura. I dati vengono salvati con singole
istruzioni (CISC e RISC) riducendo al minimo l’impatto sulle prestazione, oppure vengono
salvati tramite apposite tecniche software che salvano un singolo registro alla volta
(puramente RISC).

Context switch overhead


Il Context switch overhead indica la perdita di tempo ad ogni cambio di contesto. Questo
contributo alla perdita di tempo è diviso in due categorie:
- contributo diretto → salvataggio e ripristino del processo da eseguire.
- contributo indiretto → operazioni di cache miss, in quanto il nuovo processo troverà
in cache i dati del programma rimpiazzato, quindi dovrò utilizzare del tempo per
popolare la cache con i dati del nuovo processo.

L’utilizzo della cache durante l’esecuzione di un processo è molto importante, in quanto ne


velocizza l’esecuzione poiché avendo il TLB popolato (Translation Lookaside Buffer) non
avrà bisogno della traduzione degli indirizzi.

L’overhead totale in un punto di scheduling comprenderà quindi:


- tempo utilizzato per il cambio di contesto
- tempo utilizzato per la decisione dello scheduler su quale processo eseguire
- tempo utilizzato per la gestione delle system call e degli interrupt di sistema.

Kernel preemption
Preemption: interruzione forzata dell’esecuzione di un processo.
Esistono due tipi di preemption:
- livello user → viene sospeso un processo eseguito in modalità user
- livello kernel → viene sospeso un processo eseguito in modalità kernel

Un SO che gestisce la preemption a livello kernel si dice che un preemptive kernel, ed ogni
processo kernel è gestito tramite un kernel control path (KCP), ognuno dei quali è gestito
come un normale processo, con il proprio spazio di memoria, ecc.ecc.

Codice rientrante
Una funzione si dice rientrante se mentre è in esecuzione può essere invocata da un’altra
funzione mantenendo l’integrità dei dati, cioè se può essere interrotta durante la sua
esecuzione (da un segnale o da un interrupt per esempio) e successivamente ripresa
nuovamente prima che l’esecuzione interrotta finisca. Un codice rientrante deve evitare

15

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 16/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

variabili globali e non deve cambiare il suo codice durante l’esecuzione, oltre a dover evitare
di chiamare funzioni non rientranti.

Kernel rientrante e preemptive


Un SO moderno ha un kernel preemptive e rientrante. La caratteristica del codice rientrante
infatti permette la gestione di più KCP contemporaneamente, ovvero permette la gestione di
più processi di livello kernel contemporaneamente.
Questo tipo di kernel sono più efficienti in quanto consentono una gestione dei processi
ottimizzata, a costo però di una implementazione più difficile delle metodologie di
salvataggio e restore dei vari processi KCP bloccati/terminati in precedenza.
Un’ulteriore difficoltà si avrà dal punto di vista della sincronizzazione dei processi, infatti i
kernel rientranti dovranno prevedere la sincronizzazione tra i processi, in quanto questi
possono essere bloccati e poi ripresi e potranno lavorare contemporaneamente sugli stessi
dati del SO.

Threads vs Processi

I thread sono anche chiamati processi leggeri, poiché condividono la parte di data, file e
codice. Ogni singolo thread invece è composto da un program counter, dai suoi registri, dal
suo stack e heap. Anche i thread, così come i processi, hanno degli stati predefiniti (ready,
running, blocked, terminated). Esistono due tipi di thread, gli user thread e i kernel thread.

Perchè si utilizzano i thread


Vantaggi:
- creazione più veloce
- nel cambio di contesto tra un thread e l’altro si perde meno tempo (minor overhead)
poiché i thread condividono la parte di memoria dove sono contenuti i dati su cui
lavorano)
- occupano meno memoria
- sfruttano al massimo l’architettura multi-processore

16

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 17/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Svantaggi:
- la concorrenza deve essere controllata dall’utente programmatore, mentre i processi
erano controllati dal SO.

Tutti i moderni sistemi operativi consentono l’utilizzo di threads tramite apposite librerie
(POSIX Pthreads, Win32 threads, Java threads).

Programmazione multi-thread
Quando si utilizza la programmazione multi-thread, è importante capire come il SO interpreta
i thread. Esistono 3 politiche adottate dal SO:
- MANY TO ONE → i thread creati dall’utente vengono trattati dal SO come un unico
kernel thread (ex. se ho 4 thread, il SO li vede come un unico thread. Nel caso in cui
il SO decida di dare la CPU al kernel thread che contiene i 4 user thread, interverrà lo
scheduler della libreria thread che deciderà quale dei 4 thread eseguire).
- ONE TO ONE → ogni user thread viene tradotto in un kernel thread, con il SO che
decide quale thread mandare in esecuzione. E’ una tecnica considerata pericolosa
poichè se l’utente crea 200 thread potrebbe bloccare il SO (deny of service).
- MANY TO MANY → consente al SO di gestire un numero m di user thread in un
numero n di kernel thread, con n ≤ m. Questa tecnica è del tutto dinamica ed è
gestita dal SO tramite le up-call, utilizzate per la mappatura dinamica dei thread.

Terminazione di un thread
La cancellazione dei thread può avvenire in due modi:
- terminazione asincrona → non appena il thread ha finito le istruzioni da eseguire,
elimino il thread. Non viene utilizzata.
- terminazione deferita → appena finite le operazioni da eseguire, il thread si mette in
condizione di terminazione (sarà quindi in grado di ricevere eventuali valori di ritorno,
ecc.ecc.) e verrà periodicamente controllato dal thread principale che deciderà se
dovrà essere cancellato o meno.

Up-Calls
Le up-calls sono delle funzioni di un sistema operativo utilizzate nel modello
MANY-TO-MANY della programmazione multi-thread. Sono delle chiamate che il SO effettua
per comunicare con la libreria dei thread per notificare il cambio di mappatura dei thread.

Thread in Windows, Linux e Java


Windows
In windows, la programmazione multi-thread è gestita tramite la politica ONE-TO-ONE, con
ogni singolo kernel thread che avrà un id, un set di registri proprietari e un’area di memoria
privata.

17

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 18/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Linux
In Linux, i thread vengono chiamati tasks, e ogni nuovo task viene creato tramite la funzione
clone(). Se alla funzione non passo nessun parametro, il funzionamento sarà uguale a quello
di fork() (crea due processi che operano in maniera del tutto separata), tramite invece
l’apposito passaggio di parametri potrò copiare e creare un nuovo thread condividendo con il
vecchio thread le aree di memoria, dei particolari registri, ecc.ecc.

Java
I thread in Java sono gestiti dalla Java Virtual Machine (JVM). I thread possono essere creati
tramite due metodi:
- estendendo la classe Thread
- implementando l’interfaccia Runnable.

Comunicazione tra processi


I processi si dividono in:
- processi indipendenti → hanno un’esecuzione privata e il loro funzionamento non
può essere influenzato da altri processi
- processi cooperanti → la loro esecuzione può essere influenzata da altri processi.
Si utilizzano maggiormente i processi cooperanti, per aumentare la velocità computazionale
e per la maggior efficienza della condivisione delle informazioni.

Comunicazione tra processi


I processi cooperanti possono comunicare tra loro in due modi:
- shared memory → o memoria condivisa, viene scelta un’area di memoria in cui tutti i
processi andranno a scrivere e leggere dati. E’ la soluzione più veloce e conveniente,
ma è anche pericolosa, perchè la gestione della concorrenza è lasciata al
programmatore e risulta essere a volte di difficile implementazione
- message passing → i due processi si scambiano messaggi chiamati mail box, il cui
invio/ricezione una system call. E’ una tecnica più sicura, in quanto è totalmente
gestita dal SO, ma è allo stesso tempo più lenta perchè deve essere instaurata ogni
volta una comunicazione (link tramite precise porte di comunicazione).

Paradigma produttore-consumatore
Quando si parla di processi cooperanti, viene introdotto il paradigma del
produttore-consumatore: ci sono due thread, uno che produce informazioni chiamato
produttore, e uno che legge ed elabora le informazioni, chiamato consumatore.
Questo tipo di funzionamento utilizza un buffer, ovvero una zona di memoria condivisa alla
quale possono accedere sia il produttore che il consumatore dove andranno a scrivere e
leggere le informazioni. Il buffer può essere:
- limitato → una volta esaurito lo spazio, il produttore si ferma
- illimitato → una volta esaurito lo spazio, il produttore ricomincia a scrivere dal primo
indirizzo di memoria a scrivere sovrascrivendo le informazioni.

18

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 19/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Comunicazione tramite message passing


Nella comunicazione tramite message passing, due processi comunicano tramite l’uso di
messaggi inviati tramite le funzioni send() e receive(). I due processi dovranno instaurare
una connessione, e poi potranno procedere con la comunicazione. Lo scambio di messaggi
può avvenire in modo:
- diretto → viene utilizzato quando si conoscono gli id dei due processi. Il link viene
instaurato automaticamente, e solitamente il link permette una comunicazione
bidirezionale
- indiretto → vengono utilizzate le mailbox, ovvero delle caselle postali, dove verranno
indirizzati i messaggi da parte dei mittenti che non conoscono comunque l’id del
processo destinatario. Le mailbox avranno quindi il compito di smistare i messaggi a
più processi con determinate caratteristiche, utilizzando un broadcast (che è una
mailbox predefinita) o un multicast (che è una mailbox che può essere creata).

Una comunicazione tramite message passing si divide poi in:


- bloccante o sincrona → il mittente si blocca finchè il destinatario non ha ricevuto il
messaggio, mentre il destinatario si blocca appena instaurata la connessione e
finchè non è in grado di decifrare il messaggio
- non bloccante o asincrona → il mittente e il destinatario non si bloccano, cioè una
volta inviato il messaggio (mittente), o una volta stabilita la connessione
(destinatario), continuano ad eseguire le loro operazioni.

Architettura client-server
E’ un tipo di architettura utilizzata quando si intende effettuare una comunicazione tra due
processi che risiedono però in due macchine diverse. Si utilizzano i socket, le RPC (Remote
Procedure Calls) e le RMI (Remote Method Invocation) utilizzate da Java.

1. Socket
Utilizzano la concatenazione di indirizzo ip e porta della macchina in cui è attivo il processo

2. RPC

3. RMI

19

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 20/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Sincronizzazione

La sezione critica
La sezione critica è la parte di codice che inizia nel momento in cui faccio il primo accesso
(in lettura o scrittura) ad una variabile condivisa, e finisce quando viene effettuato l’ultimo
accesso a tale variabile condivisa. E’ chiamata sezione critica poichè tale variabile è
condivisa tra più processi, e di conseguenza potrebbe essere modificata da un processo
mentre un altro processo ne sta effettuando la lettura, portando quindi ad una potenziale
violazione dell’integrità del dato contenuto nella variabile.

Soluzioni al problema della sezione critica


1. Mutua esclusione → un solo processo alla volta può eseguire il codice contenuto
nella sua sezione critica
2. Progresso → se nessun processo sta operando nella sua sezione critica ed esistono
processi in attesa di poter operare nella loro sezione critica, uno di questi processi in
attesa deve iniziare ad eseguire il codice contenuto nella sua sezione critica, senza
postporre il problema all’infinito
3. Attesa limitata → il tempo di attesa di ogni processo tra la richiesta di eseguire la
sua sezione critica e l’inizio vero e proprio dell’esecuzione deve essere stabilito
democraticamente, così da evitare che sia sempre lo stesso processo ad eseguire
nella sua sezione critica

Soluzione di Peterson
La soluzione di Peterson è un algoritmo che garantisce la mutua esclusione nell’esecuzione
della sezione critica di due processi. I due processi condividono due variabili, una che indica
quale sarà dei due il processo che entrerà nella sezione critica, mentre l’altro indica lo stato
di un processo, ovvero se è pronto o meno ad entrare nella sezione critica.

Sincronizzazione Hardware
Questo tipo di sincronizzazione viene introdotto per risolvere le falle della soluzione
puramente software presentata da Peterson.
Vengono introdotte le istruzioni atomiche, ovvero quelle operazioni che non possono
essere divise, ma una volta che iniziano ad essere eseguite dalla CPU, la loro esecuzione
viene finita dalla CPU senza che essa venga utilizzata per altri processi.
Esistono due tipi di istruzioni atomiche:
- TEST and SET → legge il valore di un indirizzo di memoria e lo setta a “true”
- SWAP → scambia il contenuto di due celle di memoria individuate del loro indirizzo

20

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 21/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Semafori
Un semaforo è una variabile o una struttura dati astratta che contiene al suo interno
un’istanza di una variabile intera. La variabile interna al semaforo può essere modificata
tramite due operazioni:
- wait()
- signal()

Esistono due tipi di semafori:


- semafori contatori → non garantiscono in maniera robusta la mutua esclusione, ma
limitano gli accessi ad un determinato dominio (ex. possono stare seduti al tavolo un
numero massimo di 5 filosofi)
- semafori binari → garantiscono in maniera robusta la mutua esclusione in quanto la
variabile intera contenuta all’interno del semaforo (che chiameremo s) può assumere
solo due valori, 0 o 1, cioè s=0 e s=1. Vengono chiamati mutex in C e lock in Java.

Deadlock e Starvation
Deadlock: situazione che si verifica quando un insieme di processi che cooperano in un task
sono in attesa di un evento che può essere generato solo da loro stessi (i processi sono in
attesa, quindi non eseguono nulla in attesa di un evento che può essere generato solo da
loro stessi). Una situazione di deadlock può essere generata da:
- mutua esclusione: una risorsa può essere utilizzata solo da un processo alla volta
- hold and wait: un processo può “bloccare” una risorsa allocata mentre è in attesa di
un assegnamento di altre risorse
- no preemption: nessuna risorsa può essere rimossa forzatamente da un processo
che la sta utilizzando
- attesa circolare: ogni processo utilizza una risorsa che è richiesta anche dal processo
successivo nella catena.

Tali condizioni possono essere eliminate mettendo in atto determinate politiche:


- mutua esclusione: non eliminabile
- hold and wait: un processo richiede tutte le risorse di cui necessita appena inizia la
sua esecuzione
- no preemption: viene risolta dalle evoluzioni dei sistemi operativi moderni (ogni SO
moderno prevede la preemption)
- attesa circolare: le risorse vengono definite e organizzate in una lista circolare di dati
di tipo risorsa.

Starvation: la starvation è una condizione che si verifica quando alcuni processi non
entrano mai nella loro sezione critica. La soluzione a tale condizione è l’inserimento della
politica di aging, ovvero di ogni processo viene calcolato il tempo in cui tale processo non
esegue nessuna operazione perchè è in attesa di un evento o di una risorsa.

21

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 22/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Algoritmo del banchiere


E’ un algoritmo che i SO utilizzano per ottimizzare l’allocazione delle risorse. Il SO assegna
dei pesi ad ogni risorsa, e ad ogni richiesta da parte di un processo di una risorsa, il sistema
operativo analizza il processo richiedente e calcola la possibilità che l’allocazione di tale
risorsa porti ad uno stato di deadlock; nel caso in cui la probabilità che tale processo porti ad
una condizione di deadlock sia alta. il SO non allocherà la risorsa. Viene chiamato algoritmo
del banchiere perchè il ragionamento effettuato dall’algoritmo è molto simile a quello di una
banca, che deve valutare a quale cliente può concedere o meno un prestito, preoccupandosi
allo stesso tempo di non rimanere a secco di liquidità.

Problemi di sincronizzazione e deadlock

Buffer limitato
Si prende in considerazione un buffer contenente N elementi
Soluzione
- semaforo binario inizializzato a 1
- semaforo contatore full inizializzato a 1
- semaforo contatore empty inizializzato ad N

Si analizza un’architettura del tipi produttore-consumatore. Il produttore prima produce un


elemento, poi, dopo aver bloccato il semaforo binario e il semaforo empty, aggiunge
l’elemento al buffer, e poi rilascia i diritti del semaforo binario e segnala al semaforo full che
ora nel buffer sono presenti degli elementi. Il consumatore allo stesso modo, acquisirà i diritti
del semaforo full e del semaforo binario, rimuoverà l’elemento dal buffer e rilascerà i diritti
del semaforo binario, segnalando all’altro semaforo empty che il numero di elementi è
cambiato.

Problema dei lettori-scrittori


Il problema descrive un insieme di thread che vogliono leggere (lettori) e scrivere (scrittori)
su una base di dati.
1. Nessun processo può accedere e leggere o scrivere su una risorsa condivisa se in
quel momento è in esecuzione uno scrittore sulla risorsa.
2. Due o più lettori possono accedere e leggere dalla risorsa condivisa.

Quindi il problema consiste nel dover consentire a più lettori di poter leggere la risorsa
condivisa contemporaneamente, ed invece consentire ad un solo scrittore per volta di poter
scrivere sulla risorsa condivisa. All’interno della base di dati, potranno quindi esserci o più
scrittori o solamente uno scrittore.

Soluzione
- semaforo binario mutex inizializzato a 1
- semaforo binario wrt inizializzato a 1

22

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 23/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

- variabile intera readcount inizializzata a 0 che indica il n° di lettori che stanno


contemporaneamente accedendo alla risorsa condivisa.

Il codice dello scrittore sarà molto semplice, esso infatti acquisirà i permessi sul semaforo
binario wrt e scriverà all’interno della base di dati. Il codice del lettore sarà invece
leggermente più complicato: ogni volta che un lettore vuole entrare nella base dati, aumenta
il numero di lettori (cioè la variabile intera readcount). Il primo lettore che entra nella base di
dati acquista la mutua esclusione del semaforo wrt, così tutti gli altri lettori si accoderanno
sull’altro semaforo mutex ed entreranno direttamente nella loro sezione critica.

La cena dei 5 filosofi


Il problema consiste in una tavolo dove sono disposti 5 piatti di riso, ciascuno con vicino una
bacchetta. Ciascun filosofo si siederà al tavolo, e per mangiare utilizzerà due bacchette. Un
filosofo può fare due cose: mangiare o pensare. Quando il filosofo non mangia, pensa.
Il problema consiste nell’evitare la situazione di deadlock generata quando nessun filosofo
riesce a mangiare poichè non riesce ad acquisire la seconda bacchetta che è già bloccata
da un altro filosofo creando un circolo vizioso dal quale non si esce.

Soluzione
- array di 5 semafori binari inizializzati ad 1

Questa soluzione tutta via può portare ad una condizione di deadlock quando tutti i filosofi
riescono ad ottenere la loro bacchetta sinistra e non riescono ad ottenere la bacchetta destra
necessaria per mangiare. Si sono quindi individuate ulteriori soluzioni.

Ulteriori soluzioni
- Limitare a 4 il numero di filosofi che arrivano al tavolo allo stesso tempo
- Numerare le bacchette e stabilire una convenzione in base a cui ogni filosofo chiede
la bacchetta con il numero più bassa
- Implementazione tramite Monitor JAVA.

Un monitor JAVA è un oggetto ad alto livello di astrazione che permette di implementare un


meccanismo conveniente e efficiente per la sincronizzazione dei processi. Questo
particolare tipo di oggetto permette un solo flusso di esecuzione attivo alla volta, in modo
tale che i processi si alterneranno e non andranno a corrompere i valori delle variabili.

Barbiere che dorme


In un negozio c’è solo un barbiere, una sedia da barbiere ed N sedie per i clienti in attesa.
Quando non ci sono clienti, il barbiere dorme nella sua sedia, ma quando arriva un nuovo
cliente sveglia il barbiere che inizia a servire il cliente. Se un nuovo cliente arriva e ci sono
delle sedie di attesa libere, si mette a sedere, altrimenti se ne va.

23

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 24/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Soluzione
Si implementano 3 semafori :
- un semaforo contatore costumers inizializzato a 0 che blocca il barbiere che è in
attesa dei clienti
- un semaforo contatore barbers inizializzato a 0 che blocca i clienti in attesa che si
liberi il barbiere
- un semaforo binario mutex per la mutua esclusione
- una variabile intera inizializzata ad N.

Il barbiere resterà in attesa di un segnale da parte di un cliente, poi utilizzerà il semaforo


mutex per avere la mutua esclusione sulla variabile che indica i posti liberi di attesa,
aumentandolo di uno in quanto un cliente ora è nella sua sedia che sta ricevendo il taglio, e
poi effettua il taglio di capelli. I clienti invece, appena verranno eseguiti, controlleranno il
numero di posti liberi nelle sedie, e nel caso in cui ci sia posto, diminuiscono il valore di uno.
Una volta accertata la possibilità di entrare nel negozio, segnalano al barbiere l’arrivo di un
cliente, e aspettano la risposta della disponibilità da parte del barbiere, ricevendo poi così il
taglio.

Strumenti di sincronizzazione dei SO


- Solaris
1. Varietà di lock
2. Mutex adattivi per rendere più efficiente la mutua esclusione su variabili
interessate da parti di codice molto corte
3. Variabili condizionate e lock scrittore-lettore

- Windows
1. Interrupt per sistemi a singolo processore
2. Spin lock per sistemi multiprocessore
3. Oggetti del dispatcher che si comportano come mutex e semafori
4. Eventi generati dal dispatcher

- UNIX
1. Mutex
2. Variabili condizionate
3. Lock lettore-scrittore (estensione non portabile)
4. Spin lock (estensione non portabile)

24

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 25/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Scheduling

Concetti fondamentali
L’obiettivo della multiprogrammazione è quello di ottimizzare al massimo l’utilizzo della CPU,
evitando che essa resti in attesa di processi senza eseguire alcuna operazione.
Ogni processo, durante la sua esecuzione, alterna fasi di CPU Burst e I/O Burst, cioè due
fasi in cui il processo, rispettivamente, utilizza la CPU e resta in attesa di risorse di I/O.
Durante la fase di I/O Burst, la CPU non viene utilizzata dal processo, e quindi la CPU passa
da un processo ad un altro.
Il nuovo processo che viene eseguito dalla CPU viene scelto dallo scheduler. Lo scheduler
è un modulo del SO che sceglie tra i processi pronti per essere eseguiti, quale sia il migliore
da mandare in esecuzione, e una volta scelto, gli alloca la CPU. L’intervento dello scheduler,
e quindi il cambio di processo nella CPU, è richiesto quando:
- il processo in CPU passa dallo stato di running a waiting
- il processo in CPU passa dallo stato di running a ready
- il processo in CPU passa dallo stato di waiting a ready
- il processo in CPU termina.
Le politiche di scheduling si dividono in:
- non preemptive → la CPU esegue lo stesso processo finchè esso è in stato di
running e non si sospende autonomamente (termina, fa una richiesta di I/O, …)
- preemptive → la CPU può essere assegnata ad un nuovo processo anche se il
processo è ancora in stato di running. Di questa operazione si occupa il SO, e questa
tecnica è nota come time-sharing, ovvero si condivide la CPU, che viene assegnata
ad un processo diverso in base ad un quanto di tempo stabilito.
Dopo aver deciso il processo migliore dalla coda dei processi pronti per essere eseguiti in
base ad una precisa politica di scheduling, il modulo del SO che dà il controllo effettivo della
CPU al nuovo processo scelto è chiamato dispatcher. Questa operazione comprende la
fase di cambio di contesto. Ogni dispatcher ha la sua latenza, ovvero il tempo che esso
impiega per stoppare un processo in esecuzione e allocare la CPU ad un altro processo.

Criteri di scheduling
- Massimizzare l’utilizzazione della CPU → tenere la CPU in esecuzione il più
possibile, evitando periodi di tempo in cui essa non esegue nessuna operazione.
- Massimizzare il throughput → cercare di completare il maggior numero possibile di
processi nell’unità di tempo.
- Minimizzare il turnaround time → minimizzare il tempo di esecuzione totale di un
particolare processo.
- Minimizzare il tempo di attesa → minimizzare il tempo di attesa di un processo nella
coda dei processi in stato ready.
- Minimizzare il tempo di risposta → minimizzare il tempo tra una richiesta e la prima
risposta prodotta.

25

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 26/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Politiche di scheduling

FCFS (First Come First Serve)


E’ una politica di scheduling molto veloce e facile da implementare, secondo la quale il primo
processo che arriva nella coda dei processi in stato ready viene eseguito. Così facendo si
potrebbe però andare in contro ad un waiting time medio piuttosto alto.
Infatti, se nella coda arrivano 3 processi P1=24, P2=3, P3=3, il waiting time medio sarebbe di
17 ([0+24+27]/3). Se si ipotizza infatti un ipotetico ordine di arrivo P2 P3 P1 il waiting time
medio si abbasserebbe drasticamente, passando a 3 ([6+0+3]/3), rendendo così questa
politica di scheduling più efficiente.
E’ un algoritmo instabile, in quanto dipende dall’ordine di arrivo dei processi nella coda dei
processi in stato ready, e si potrebbe inoltre generare il processo convoglio, secondo il
quale se nella coda si trovano dei processi I/O bound che seguono dei processi CPU bound
si avrebbe uno scarso utilizzo della CPU.

SJF (Shortest Job First)


E’ una politica di scheduling secondo la quale il prossimo processo da eseguire sarà quello
con CPU burst time minore. E’ la politica che più minimizza il waiting time medio dei
processi. Questa politica è implementata in due modi:
- non preemptive → Una volta che la CPU è stata allocata ad un processo dal
dispatcher, essa concluderà l’esecuzione di tale processo.
- preemptive → Se mentre la CPU sta eseguendo un processo P1, arriva un nuovo
processo P2 che ha un tempo di CPU burst minore del remaining time di P1, la CPU
viene sottratta a P1 ed inizia ad eseguire P2. Questa particolare politica viene
chiamata SRTF (Shortest Remaining Time First).

I principali problemi di questa politica di scheduling sono:


1. Complessità di gestione
2. Il CPU burst time del processo può essere solo stimato, non si conosce
precisamente (si utilizza una stima basata sulla lunghezza dei precedenti tempi di
CPU burst, utilizzando una media esponenziale)
3. Starvation: i processi con CPU burst time molto lungo, rischiano di non prendere mai
la CPU. Per risolvere questo problema si utilizza la politica di aging, ovvero si calcola
il tempo trascorso dall’ultima volta che il processo è stato eseguito dalla CPU (o da
quanto è in attesa per essere eseguito la prima volta dalla CPU), e nel caso in cui
questo tempo sia molto alto, si “bara” andando a modificare solo apparentemente il
CPU burst time del processo.

26

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 27/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Scheduler prioritario
E’ una politica secondo la quale il processo da eseguire è quello con priorità più alta. Tale
priorità viene assegnata ad un processo tramite un numero intero (la priorità più alta è
indicata con il numero più basso ex.1,2,3…). Tale priorità può essere fissa o variare
dinamicamente. Anche in questo caso, l’implementazione di questa politica può avvenire in
maniera preemptive e non preemptive. Il problema di questa politica è sempre la starvation,
infatti un processo con priorità bassa potrebbe non riuscire mai ad ottenere l’allocazione
della CPU. Anche in questo caso si utilizza la politica di aging, secondo la quale in questo
caso, la priorità di un processo aumenta dinamicamente col passare del tempo di attesa di
un processo.

Round Robin (RR)


In questa particolare politica, la coda dei processi in stato ready è una coda circolare. Viene
stabilito un intervallo di tempo detto quantum (solitamente tra i 10 e i 100ms), e ad ogni
processo viene allocata la CPU proprio per questo quando di tempo. Una volta esaurito il
quanto di tempo, se il processo non ha ancora finito di eseguire tutte le sue operazioni, viene
messo in fondo alla coda, ed attenderà nuovamente il suo turno, mentre la CPU eseguirà un
altro processo. Il tempo di attesa di un processo è noto quindi a priori: dati infatti n processi,
il tempo di attesa sarà di (n-1)*q. La durata del quanto di tempo deve essere bilanciata,
ovvero non deve essere ne’ troppo piccola (ci sarebbero troppi cambi di contesto), ne’ troppo
grande (diventerebbe FCFS). E’ considerata una buona politica di scheduling, ma non viene
mai utilizzata come unica politica utilizzata, ma è sempre affiancata da altre politiche nelle
code a multiplo livello.

Code a multiplo livello


Un’ulteriore possibilità di organizzazione della coda dei processi in stato ready, è quella di
dividere tale coda in più code separate. Le code in cui esse sono separate si dividono in:
- foreground
- background
Ciascuna dei due tipi di code implementa una politica di scheduling (RR per le foreground e
FCFS per le background), e avrà inoltre a disposizione una certa quantità di CPU con cui
eseguire la propria politica di scheduling (ex. 80% per il RR e il 20% per FCFS).

27

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 28/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Scheduling in architetture multi-processore


Quando si tratta di schedulare dei processi in architetture a più processori, ci sono 3 possibili
configurazioni:
1. shared memory
2. NUMA
3. Cluster di Computer

1. Shared Memory
Tutte le CPU elaborano informazioni ottenute da una memoria RAM condivisa tra tutte le
CPU. Ci sono due possibili implementazioni:
- Asimmetrica (ASMP) → le CPU non sono trattate tutte nello stesso modo, ma
esistono master e slave, con i primi che eseguono solamente il SO e le funzioni di
scheduling, mentre gli slave eseguono i processi secondari di minor importanza che
gli son stati assegnati dal master. Il problema di questo tipo di architettura è che un
eventuale guasto del master, porterebbe ad una condizione di deadlock.
- Simmetrica (SMP) → tutte le CPU sono trattate allo stesso modo, per cui ogni CPU
eseguirà lo scheduling dei propri processi in maniera autonoma. Il SO viene eseguito
da tutte le CPU, quindi sarà organizzato in due possibili implementazioni:
1. Global Ready Queue → le CPU ottengono i processi da eseguire da una
coda globale condivisa. Il carico per ogni CPU sarà bilanciato, a patto che la
coda di processi sia sincronizzata poichè deve gestire gli accessi da più CPU
diverse.
2. Private Ready Queue → ogni CPU ha una propria coda di processi. Queste
code non dovranno essere sincronizzate, poichè sono proprietarie di ogni
CPU, ma viene introdotto il problema del bilanciamento del carico, in
quanto una CPU avrebbe potuto eseguire molti processi a discapito di CPU
che ne avrebbero eseguiti pochissimi.

28

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 29/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Bilanciamento del carico


Nel caso in cui in un’architettura multi-processore venga adottata la tecnica SMP
(SimmetricMultiProcessing) con le code di processi proprietarie per ogni CPU, è importante
gestire il bilanciamento del carico per ogni CPU. Il bilanciamento può avvenire mediante
l’uso di due tecniche distinte:
- push migration: una CPU in sovraccarico (cioè con molti processi che sono in
attesa nella sua coda), spinge alcuni dei suoi processi nelle code di attesa di CPU
con meno carico di lavoro o in idle (stato di una CPU quando non esegue nessun
processo).
- pull migration: una CPU, prima di andare in stato di idle, effettua una richiesta alle
altre CPU, nel quale comunica la possibilità di prendersi carico di processi
appartenenti alla coda delle altre CPU, diminuendo così il carico di lavoro di una
precisa CPU e bilanciandolo.

Non sempre per un processo è però conveniente cambiare CPU dalla quale viene eseguito.
Infatti, un processo dovrebbe ripopolare di nuovo la cache in un’altra CPU, richiedendo così
maggior tempo di esecuzione. Pertanto, i sistemi operativi SMP cercano di evitare il più
possibile la migrazione di un processo da una CPU ad un’altra.
Un processo, in questo caso, al momento della creazione da parte del SO, può essere
catalogato come un processo che può essere eseguito su CPU diverse (soft affinity), o
come un processo che dovrà essere eseguito sempre dallo stesso processore (hard
affinity).

29

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 30/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Scheduling in Linux

Linux scheduler 2.4.x

Coda dei processi


In questa versione dello scheduler implementato su una piattaforma Linux, un’importante
caratteristica è la struttura della coda dei processi. In questa versione infatti la coda dei
processi consiste in una lista doppiamente linkata, con un nodo che indica un processo e
contiene il puntatore al nodo successivo e anche a quello precedente (l’ultimo nodo avrà
come successivo il primo nodo, e il primo nodo avrà come precedente l’ultimo nodo).

Stato dei processi


- TASK_RUNNING → il processo è in esecuzione nella CPU o è in attesa di essere
eseguito
- TASK_INTERRUPTIBLE → il processo è in stato di attesa (waiting) e rimarrà tale
finchè non si verificano determinate condizioni → attesa che può essere interrotta
- TASK_UNINTERRUPTIBLE → anche in questo caso il processo è in stato di attesa,
ma questa volta la sua attesa non è interrompibile tramite un segnale o tramite
determinate condizioni che diventano vere. Questo stato è solitamente utilizzato
quando un processo esegue un device driver; il driver non può essere interrotto
altrimenti il device hardware potrebbe riscontrare problemi nel suo funzionamento,
oltre a rimanere in uno stato non prevedibile
- TASK_STOPPED → il processo è stato stoppato dal SO tramite un apposito segnale
- TASK_ZOMBIE → il processo ha terminato la sua esecuzione ma il padre non ha
invocato la wait() per la sua terminazione.

Principali caratteristiche dello scheduler


1. Time-sharing → la CPU è divisa in spicchi da assegnare ad ogni processo
2. Priorità dinamiche → implementazione della politica di aging
3. Distinzione tra processi real-time e non
4. I processi I/O intensive sono più veloci rispetto agli CPU intensive
5. Le system call sono disponibili a gestire i parametri di scheduling

Quanto di tempo
Il quanto di tempo è un intervallo di tempo assegnato ad ogni processo in cui tale processo
viene eseguito dalla CPU. Tale intervallo di tempo è sempre un multiplo del timer interrupt
hardware di sistema. Man mano che un processo esegue sulla CPU, il suo quanto di tempo
viene consumato fino a quando non finisce. Il quanto di tempo viene riassegnato alla fine di
un’epoca, ovvero il momento in cui tutti i processi finiscono il loro quanto di tempo. Quando
un processo finisce il suo quanto di tempo, può verificarsi però che il processo stesso non
abbia finito la sua esecuzione, quindi necessita di essere eseguito ulteriormente dalla CPU.

30

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 31/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Nel momento in cui finisce un’epoca e viene riassegnato il quanto di tempo, ai nuovi
processi viene assegnato il quanto di tempo base (ex. 210 ms), mentre a questi processi che
non hanno ancora finito la loro esecuzione, viene assegnato il quanto di tempo base più la
metà del suo tempo residuo che gli era rimasto alla fine della scorsa epoca. Questa
assegnazione di un quanto di tempo variabile consente l’implementazione di una priorità
dinamica (i processi con più tempo residuo saranno quelli con priorità maggiore).
Le due system call per cambiare il quanto di tempo base (solitamente di 210 ms) sono nice()
e set_priority(), che rispettivamente diminuiscono e aumentano il quanto di tempo.
I processi I/O intensive sono favoriti rispetto a quelli CPU intensive: infatti i primi, alla
scadenza di un epoca, avranno sempre un tempo residuo molto alto e di conseguenza
otterranno una priorità maggiore rispetto ai CPU intensive che finiscono subito il loro quanto
di tempo e devono aspettare la fine di un’epoca per ottenere un nuovo assegnamento.

Priorità dei processi


- Statica → è assegnata ai processi real-time, con numeri che vanno da 1 a 99
- Dinamica → è assegnata ai processi convenzionali e dipende dal residuo del quanto
di tempo dell’epoca corrente.
La priorità dei processi real time è sempre maggiore di quella dei processi convenzionali. Il
SO alloca la CPU a dei processi convenzionali se non ci sono processi real time da
eseguire.

Process Descriptor
Il process descriptor è una struttura dati che all’interno contiene le informazioni di un
processo di cui il SO deve essere a conoscenza per rendere la gestione dei processi più
efficiente.
Al suo interno contiene diversi campi:
- politica di scheduling → che può essere FIFO (solo per processi real time), RR (solo
per processi real time), OTHER che indica un processo convenzionale, e YIELD che
è un flag che indica se il processo ha rilasciato la CPU
- priorità → utilizzerà rt_priority per i processi real-time e priority per i processi
convenzionali
- counter → è il contatore del quanto di tempo rimasto, che diminuisce man mano che
il processo esegue in CPU

Funzione schedule()
E’ una funzione che confronta tra loro i vari candidati pronti ad essere eseguiti dalla CPU
con l’ultimo processo che è stato eseguito proprio dalla CPU. Nel caso in cui l’ultimo
processo eseguito dalla CPU abbia esaurito il suo quanto di tempo, la funzione schedule()
gli assegna un nuovo quanto e lo mette in fondo alla coda dei processi pronti per essere
eseguiti. Nel caso in cui il processo eseguito dalla CPU non abbia finito ancora la sua
esecuzione, gli viene data la possibilità tramite la funzione schedule() di essere nella lista dei
possibili candidati tra i processi che potranno essere scelti per essere eseguiti. Nel caso in

31

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 32/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

cui il processo eseguito dalla CPU non sia in stato STAK-RUNNING, la funzione rimuove il
processo dalla coda dei processi pronti per essere eseguiti. Una volta analizzato e
collocato l’ultimo processo eseguito dalla CPU, la funzione schedule() invoca la funzione
goodness() che esamina tutti i possibili processi che possono essere eseguiti dalla CPU, e
ne determina il migliore da mandare in esecuzione. La funzione schedule() può essere
invocata in due diverse modalità:
- invocazione diretta → tramite system call, semafori o richiesta di I/O
- invocazione “pigra” → eseguita da un processo che si trova nella coda dei processi
con need_resched = 1.

Linux Scheduler 2.6.x


Anche questa evoluzione dello scheduler si basa su una coda di processi pronti per essere
eseguiti. In architetture multi CPU, ogni CPU ha la sua coda di processi. Esistono poi due tipi
di array:
- l’array per i processi attivi
- l’array per i processi che hanno eseguito il loro quanto di tempo.
Lo scheduler muove in continuazione i processi tra un array e l’altro.
Viene introdotta la mappatura BITMAP, ovvero una matrice di bit che tiene conto di tutti i
processi che possono essere eseguiti. Ciascun riquadro di tale matrice rappresenta un
processo, e il processo più in alto a sinistra indica il processo con priorità più alta, ovvero
quello che dovrà essere eseguito per primo (nel primo quadrante il processo avrà priorità 0,
nel secondo 1, e così via…). Rispetto allo scheduler 2.4.x, i kernel thread hanno un’area di
memoria dedicata e non hanno bisogno di sovrascrivere i precedenti kernel thread
memorizzati, cosa che avveniva invece nella passata edizione dello scheduler linux. Inoltre,
non c’è una visita ciclica all’interno della coda dei processi per determinare il processo con
priorità più alta, in quanto i processi sono già ordinati in base a tale caratteristica grazie alla
mappatura bitmap.

Scheduler SMP
Acronimo di Symmetric Multi Processor, è un’architettura nella quale ogni processore è
uguale agli altri, ciascuno dei quali ha una sua cache proprietaria e condividono memoria.
Ogni processore invoca la funzione schedule(), e in questa fase i diversi processori si
scambiano informazioni riguardo al proprio stato (se stanno eseguendo processi, o se sono
in stato di busy, …). Nel caso in cui ci sia un processo con alta priorità nella coda dei
processi del processore 1, ma il processore 1 sia impiegato nell’esecuzione di un altro
processo, tramite questo scambio di informazioni il processo con questa alta priorità
potrebbe venir eseguito da un altro processore. E’ buona norma comunque eseguire un
processo nello stesso processore in cui esso si trova in coda, per ridurre i problemi riguardo
la ripopolazione della cache: più è grande la cache, più tale tecnica è conveniente, in quanto
ripopolare la cache richiederebbe più tempo.

32

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 33/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Questo tipo di architettura necessita comunque di alcune tecniche di bilanciamento per


evitare che ci siano processori con code piene di processi e processori che non stanno
eseguendo nessuna operazione e che hanno allo stesso tempo la propria runqueue vuota.

CFS (Completely Fair Scheduler)


Il Completely Fair Scheduler viene introdotto con la release 2.6.23 del kernel del sistema
Linux. E’ una versione che rivoluziona completamente i concetti cardine sui quali si basava
lo scheduling dei processi nelle precedenti edizioni del kernel Linux. Infatti, il CFS infatti
punta a creare una CPU ideale, che sia in grado di occuparsi di più processi
contemporaneamente senza dare priorità ad un processo rispetto ad un altro. Il problema di
questa implementazione è il numero di cambio di contesto.

Implementazione
Viene introdotta nel PCB (Process Control Block) un nuovo campo vruntime che indica (in
nanosecondi) il tempo che tale processo ha passato in esecuzione sulla CPU. Un processo
con vruntime molto basso indica che ha più bisogno di essere eseguito dalla CPU. Per
raccogliere i processi che sono pronti ad essere utilizzati, non vengono utilizzate più code,
ma alberi rosso-neri, che hanno complessità di ricerca, inserimento e cancellazione pari a
O(logn), piuttosto che complessità lineare come nelle precedenti versioni. La chiave su cui si
basa l’ordinamento in questi alberi è il vruntime, e quindi il prossimo task da eseguire sarà
quello più a sinistra e più in basso dell’albero rosso-nero, cioè quello con vruntime minore.
Un processo con priorità più alta, accumulerà vruntime in maniera più lenta, mentre un
processo con priorità più bassa accumulerà vruntime in maniera più veloce. Ogni task viene
interrotto dal SO (preemption) non ogni quanto di tempo stabilito, ma ad intervalli variabili:
infatti, il processo che si trova più in basso e più a sinistra dell’albero rosso-nero viene
eseguito finchè un altro processo prende il suo posto nell’albero, e andrà quindi a
rimpiazzarlo sulla CPU.

Il problema principale di questo tipo di scheduler è il grande numero di cambio di contesto.


Appena un processo che è in fase di esecuzione raggiunge vruntime più alto di uno che è
presente nella coda dei processi, il processo corrente viene sostituito con quello con
vruntime minore. Per risolvere questo problema viene introdotta una soglia di isteresi,
ovvero un piccolo quanto di tempo bonus da assegnare ad un processo. Per esempio, se un
processo esegue per 2ns in CPU, gli viene assegnata questa isteresi di 3ns ad esempio,
così che il processo eseguirà per altri 3ns, eseguendo ulteriori operazioni così da finire la
propria esecuzione o eventualmente ridurre il numero di volte che verrà eseguito
nuovamente per completare le proprie operazioni.
Con questo tipo di scheduler viene creato un SO più tunabile, ovvero modificabile, in quanto
i vari parametri di isteresi, o di incremento del vruntime, sono modificabili dall’utente tramite
opportuni comandi.

33

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 34/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Memoria principale

Organizzazione della memoria principale


I processi sono memorizzati nella memoria principale (memoria RAM) in aree di memoria
contigue determinate da un registro base e un registro limite, che indicano rispettivamente
l’inizio e la fine dei registri che contengono le informazioni di un determinato processo. Per
memorizzare i dati nei registri viene effettuata un’operazione chiamata binding, ovvero una
conversione delle variabili utilizzate in un programma in effettivi indirizzi fisici relativi nella
memoria RAM.

Binding
L’operazione di binding può avvenire in 3 diversi momenti:
- a tempo di compilazione → viene eseguita a tempo di compilazione dal compilatore.
E’ una tecnica utilizzata quando il compilatore conosce l’architettura hardware e
associa ogni variabile ad un preciso registro nella RAM. Il codice viene chiamato
assoluto, in quanto può essere eseguito solo nelle celle di memoria selezionate dal
compilatore e il SO non può spostare il processo da queste celle. Questa tecnica
preclude la multi-programmazione.

- a tempo di caricamento → il binding viene eseguito quando il processo viene


spostato dall’hard disk alla RAM. Viene utilizzata questa tecnica quando il
compilatore non conosce l’architettura della RAM e quindi non sa quali indirizzi sono
liberi e quali occupati. Il compilatore quindi inizia a memorizzare il processo da un
registro chiamato simbolicamente base, e continuerà a memorizzare il processo in
celle contigue a quella del registro base. Il registro base verrà poi scelto dal SO a
tempo di caricamento del programma. Il programma potrà essere quindi memorizzato
in qualsiasi posizione della RAM, ma una volta scelta tale posizione, non potrà
essere spostato durante l’esecuzione.

- a tempo di esecuzione → è la tecnica di binding utilizzata al giorno d’oggi, con il


binding che viene eseguito mentre il programma viene eseguito: il processo può
essere quindi memorizzato anche in celle di memoria non contigue e il processo può
essere spostato in altre celle durante l’esecuzione. Questa tecnica massimizza
l’utilizzo della memoria.

Gli indirizzi vengono divisi in due categorie:


- logici → sono gli indirizzi di memoria generati dalla CPU
- fisici → sono gli indirizzi di memoria visti dalla RAM.
Gli indirizzi logici e fisici sono gli stessi quando si effettua il binding a tempo di compilazione
e a tempo di caricamento, mentre possono differire (molto spesso differiscono) quando si
effettua il binding a tempo di esecuzione.

34

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 35/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

MMU (Memory Management Unit)


E’ un dispositivo hardware che effettua la mappatura tra indirizzi logici e fisici quando si
effettua il binding a tempo di esecuzione. In uno schema di una MMU molto semplice, una
variabile è memorizzata in una cella con indirizzo 346 (indirizzo logico), e la MMU va a
sommare questo valore al valore di un indirizzo fisico vero e proprio presente in ram (ex.
14000), con la variabile che verrà quindi memorizzata nella cella di memoria RAM con
indirizzo 14346. Il programma utilizzerà solamente gli indirizzi logici, senza sapere realmente
quale sia il reale indirizzo fisico della RAM.

Dynamic Loading e Dynamic Linking


Il dynamic loading e il dynamic linking sono due diverse metodologie utilizzate per il
caricamento di una libreria quando si utilizza il binding a livello di esecuzione.
- dynamic loading → carico in RAM la libreria solamente nel momento in cui il
processo richiede una funzione di tale libreria. Questo tipo di implementazione
permette un importante risparmio di memoria, in quanto le librerie che non vengono
utilizzate non sono caricate.
- dynamic linking → non viene caricata la libreria vera e propria, ma vengono utilizzati
degli stub, all’interno dei quali c’è il link dell’area di memoria nella quale è
memorizzata effettivamente la libreria. Quando viene richiesta una libreria, viene
caricata, e se tale libreria dovesse essere richiesta in seguito, non viene ricaricata,
ma si utilizza lo stub, all’interno del quale è memorizzato il link della zona di memoria
RAM in cui è stata caricata la libreria. E’ una tecnica molto utilizzata quando le
librerie sono condivise da molti processi diversi.

Allocazione contigua e frammentata


Allocazione continua: la memoria principale è divisa in due parti: una parte riservata
dedicata ai processi del sistema operativo (nella parte bassa della RAM), e una riservata ai
processi degli utenti (nella parte alta della RAM). Ogni processo è memorizzato in aree di
memoria comprese tra un registro base, che contiene il più piccolo indirizzo fisico, e un
registro base+limite, con il limite che indica il range di indirizzi logici, con la MMU che
mappa dinamicamente i registri logici e quelli fisici. I nuovi processi sono memorizzati in dei
buchi presenti nella RAM, con un buco che indica un insieme di blocchi disponibili in cui
memorizzare un processo. Tutti i SO tengono traccia della parte di memoria allocata e di
quella libera da allocare. Esistono varie metodologie con cui viene scelto il buco in cui
memorizzare:
- first-fit → alloco il processo nel primo buco abbastanza grande da contenere il
processo
- best-fit → alloco il processo nel buco più piccolo in grado di contenerlo
- next-fit → alloco il processo nel primo buco abbastanza grande libero dopo l’ultimo
processo allocato

35

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 36/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

- worst-fit → alloco il processo nel buco più grande . E’ la tecnica peggiore perchè si
ha uno grande spreco di memoria.

Allocazione frammentata: la memoria non utilizzata è abbastanza per allocare il processo,


ma non è contigua. Questa tecnica permette di massimizzare l’uso della memoria, ma
richiede più tempo in quanto prima di allocare il processo vengono compattate tutte le aree
vuote in un’unica grande area allocabile. Il tutto è possibile solo se la riallocazione è
dinamica e avviene a tempo di esecuzione.

Paginazione
Molto spesso, un processo viene allocato in memoria in maniera non contigua. Si dividono
gli indirizzi logici in blocchi della stessa dimensione (potenze di 2), chiamati pagine
(frammentazione interna), mentre si dividono gli indirizzi fisici in blocchi della stessa
dimensione chiamati frames. Per allocare ed eseguire un processo di n pagine, occorrerà
trovare n frames liberi nella RAM. Occorrerà quindi costruire una tabella delle pagine per
tradurre da indirizzi logici ad indirizzi fisici. Gli indirizzi logici generati si dividono in page
number e page offset: i primi rappresentano l’indice di una pagina all’interno della tabella
delle pagine che contiene il registro base nella memoria fisica reale, mentre i secondi,
combinati con la base, rappresentano l’indirizzo limite fisico inviato alla memoria centrale per
l’elaborazione.

Implementazione della tabella di paging


La tabella di paging è memorizzata nella memoria principale (RAM) tramite il suo registro
base (PageTableBaseRegister) e il suo registro limite (PageTableLenghtRegister) che indica
la lunghezza della tabella. Ogni istruzione (associata ad un preciso indirizzo nella RAM)
richiede due accessi di memoria, uno per la tabella di paging e uno per l’istruzione vera e
propria. Questo doppio accesso viene gestito da una memoria cache associativa (o anche
Translation look-aside buffer TLBs). Questa memoria è un cache che visualizza il numero
della pagina e il relativo frame fisico in cui è memorizzato il dato, e consultare tale memoria
è molto più veloce rispetto al consultare l’intera tabella delle pagine.
Ogni entry nella tabella delle pagine ha un bit a 0 o ad 1 che indicano rispettivamente che la
pagina è una pagina non valida o valida, cioè che la pagina fa parte o meno dello spazio di
memoria logico del processo.

Pagine condivise
Utilizzando la paginazione si può condividere del codice. Si ottiene quindi:
- codice condiviso → una copia del codice (in sola lettura)viene condivisa tra i processi
(come per esempio per i text editor, i compilatori, le finestre di sistema). Per questa
tecnica però il codice condiviso deve essere memorizzato nella stessa posizione
dello spazio logico per tutti i diversi processi che lo utilizzano.

36

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 37/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

- codice privato e dati privati → ogni processo ha la propria copia del codice e dei dati.
In questo caso, le pagine contenenti il codice possono essere memorizzate ovunque
nello spazio di memoria logico dei processi.

Struttura della tabella di paging


La tabella delle pagine può essere generata eseguendo 3 diverse tipologie di paging:
- paging gerarchico
- tabella delle pagine hash
- tabella delle pagine invertita

Paging gerarchico: lo spazio degli indirizzi logici viene diviso in più tabelle. Una tecnica
utilizzata per il paging gerarchico è la tabella a due livelli. In questo tipo di implementazione,
ci sono due diversi tipi di tabelle delle pagine: una tabella esterna e una interna. Ciascuna
entry della tabella delle pagine esterna punterà ad una nuova tabella delle pagine interna. Il
vantaggio di questo tipo di implementazione è che solo la tabella delle pagine esterna
necessita di essere completamente allocata, mentre la tabella delle pagine interne può
essere allocata quando c’è un riferimento ad essa.

37

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 38/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Struttura della tabella delle pagine

Tabella delle pagine hash


Viene utilizzata quando lo spazio degli indirizzi logici è maggiore di 32 bit. In questa tecnica,
il numero virtuale della pagina viene trasformato tramite una funzione di hash e memorizzato
in una nuova tabella che conterrà gli indirizzi delle pagine tradotti. Il risultato di tale
trasformazione tramite la funzione hash è un indice che viene utilizzato per accedere alla
tabella. Solo gli indici delle pagine utilizzate sono allocati. Una funzione hash è una
particolare funzione che permette di mappare un dato di dimensioni variabili ad un tipo di
dato di una lunghezza fissa. Questo tipo di funzione può portare a delle collisioni, ovvero due
dati distinti vengono mappati allo stesso indice (funzione non iniettiva). Questo tipo di
collisione viene evitata creando delle liste linkate: se due elementi vengono mappati allo
stesso indice, sotto tale indice si crea una lista che conterrà tutti i dati tradotti tramite la
funzione hash. Per cercare un determinato dato occorrerà quindi ciclare all’interno di tale
lista. La tabella delle pagine è unica per tutto il sistema, quindi ad ogni pagina sarà associato
il PID di un preciso processo.

Tabella delle pagine invertita


In questo tipo di tabella, esiste una entry per ogni frame (indirizzo di memoria fisico). Ogni
entry contiene il PID del processo e l’indirizzo virtuale della pagina realmente memorizzata in
memoria. Con questa tecnica, occorre meno memoria per memorizzare ogni tabella delle
pagine, ma aumenta il tempo per ricercare una precisa tabella quando viene fatto un
riferimento ad una sua pagina. Inoltre, l’implementazione delle pagine condivise risulta
essere difficoltosa (occorre implementare una catena).

Segmentazione
La segmentazione è uno schema di gestione della memoria che corrisponde alla vista della
memoria che generalmente ha l’utente. Un programma è un insieme di segmenti, con un
segmento che può essere un metodo, una funzione, un oggetto, un array, uno stack, …

Architettura di segmentazione
Un indirizzo logico è composta da una coppia di valori definiti dal compilatore
<segment-number, offset>.
La tabella dei segmenti è una tabella composta da entry, ciascuna composta da una base,
che contiene l’indirizzo fisico in cui il segmento è memorizzato, e un limite che indica la
lunghezza del segmento. La tabella dei segmenti è indicata anch’essa dal registro base
(SegmentTableBaseRegister) STBR che indica la locazione in memoria effettiva della

38

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 39/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

tabella, e il registro di lunghezza (SegmentTableLengthRegister) STLR, che indica il numero


di segmenti che fanno parte del programma.

Differenze tra pagine e segmenti


1. Una pagina è sempre di lunghezza fissa, un segmento è di lunghezza variabile
2. La paginazione utilizza la tecnica della frammentazione interna, mentre la
segmentazione potrebbe portare ad una frammentazione esterna
3. Nella paginazione, il compilatore assegna un singolo intero come indirizzo, il quale
poi viene diviso tramite hardware in page-number e offset, mentre nella
segmentazione il compilatore specifica l’indirizzo nelle due quantità
<segment-number, offset>
4. Nel paging, la dimensione di una pagina è decisa dal S.O., mentre nella
segmentazione la dimensione viene decisa dal compilatore.

Ciascuna entry della tabella dei segmenti ha un bit di validazione e un campo che indica i
privilegi (lettura/scrittura/esecuzione).

Segmentazione Intel Pentium


Ogni segmento ha una dimensione massima di 4GB. Ogni processo ha un massimo di 16K
processi. I segmenti sono divisi in due tabelle: la Global Descriptor Table, che contiene i
segmenti condivisi tra più processi, e la Local Descriptor Table, che è una tabella che ogni
processo si crea per memorizzare nella memoria principale i propri segmenti. Nella memoria
esiste solo una GDT, ma esistono più LDT, una per ogni processo. La segmentazione
implementata dall’Intel Pentium supporta sia la segmentazione che la segmentazione
tramite paginazione. La CPU genera l’indirizzo logico, il quale poi viene elaborato dall’unità
di paginazione che lo trasforma in un indirizzo lineare, e il risultato viene poi elaborato
dall’unità di paginazione, che produce il vero e proprio indirizzo fisico relativo alla memoria
principale.

Segmentazione AMD64
Questo tipo di architettura definisce un indirizzo virtuale di 64bit, dei quali solo 48 però sono
utilizzati, in quanto i SO e le applicazioni non necessitano di tale quantità di memoria. I bit
non utilizzati (dal 48 al 67), replicano l’informazione del bit 47. Gli indirizzi che rispettano
questa forma vengono detti in forma canonica. Questa forma permetterebbe di avere uno
spazio di indirizzi logico di 256TB, che è comunque molto maggiore dei 4GB delle macchine
ad architettura a 32 bit. Questa implementazione è utile in quanto può essere facilmente
scalata ad architetture a 64bit effettivi (e non ai soli 48 utilizzati ora) e permette di dividere lo
spazio RAM in una parte alta in cui memorizzerà i processi relativi al kernel e una parte
bassa in cui memorizzerà i processi dell’utente.

39

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 40/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

Memoria virtuale
La memoria virtuale di un processo è la vista logica di come il processo è memorizzato in
memoria. Serve per ampliare la memoria principale (RAM) tramite l’utilizzo di altre memorie
(solitamente le unità disco), in quanto solamente una parte del processo deve essere
memorizzato nella memoria principale per permetterne l’esecuzione. Questa tecnica può
essere implementata tramite due architetture:
- paginazione su richiesta
- segmentazione su richiesta.

Paginazione su richiesta
Questa tecnica consiste nel caricare in memoria principale una pagina solamente quando
viene richiesto un riferimento a tale pagina. In questo caso si risparmia memoria e si ha una
esecuzione più efficiente. Se il riferimento non è valido, la pagina non viene caricata, se il
riferimento risulta essere invece valido e la pagina non è già caricata in memoria, la pagina
richiesta viene caricata. Si utilizza il lazy swapper, chiamato così poichè non carica tutte le
pagine del processo subito in memoria, ma ne carica una ad una quando una determinata
pagina viene richiesta. Ciascuna entry nella tabella ha anche un questo caso un bit di
validazione: i indica che la pagina non è in memoria, v indica invece che la pagina è in
memoria. Il primo riferimento ad una pagina crea una trappola al SO chiamata page fault,
che può indicare diverse situazioni come una pagina non caricata in memoria, o una pagina
vuota. Nel caso in cui la pagina richiesta non risieda in memoria, il sistema operativo cerca
la pagina richiesta nella memoria secondaria (ex. disco ottico), la carica in memoria, ed
effettua il reset della tabella delle pagine e fa ripartire l’istruzione che questa volta troverà
nella memoria virtuale la pagina corrispondente.

Page fault nel dettaglio


1 → Trappola al SO
2 → Salvataggio dei registri utente e dello stato del processo
3 → Determinare la natura dell’interrupt (in questo caso il SO capisce che è una trappola
determinata dal page fault)
4 → Controllo se il riferimento alla pagina è legale e determinazione della locazione della
pagina in memoria
5 → Caricare la pagina in memoria dove c’è un frame libero.

In tutto questo, mentre viene cercata la pagina in memoria, la CPU viene assegnata ad altri
processi. Nel momento in cui arriva un interrupt da parte di un dispositivo I/O (in questo caso
dal disco in cui la pagina era memorizzata), la CPU salva lo stato dell’altro processo che
stava eseguendo. Il SO quindi, una volta determinata la natura dell’interrupt, corregge la
tabella delle pagine in modo che il vecchio processo ora richiederà una pagina che è

40

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 41/42
27/5/2019 Rissunto Sistemi Operativi - Documenti Google

presente in memoria. A questo punto, la CPU potrà essere riallocata al vecchio processo
che ripartirà dalla vecchia istruzione che era stata interrotta per mancanza della pagina
richiesta in memoria.

Sostituzione delle pagine

Allocazione dei frame

File mappati in memoria

41

https://docs.google.com/document/d/1yG4rCedxEB99KMMpslmbZG03cJzNW8H4sWUtKpLRKp0/edit#heading=h.k2twfbn202ja 42/42

Potrebbero piacerti anche