Appunti di Sistemi Operativi (OS)

Sistemi operativi
Sistemi operativi

Un piccolo breviario con la spiegazione succinta dei principali concetti riguardante i sistemi operativi (operating systems, OS).

Sistemi Operativi

Sono i software fondamentali per poter utilizzare di una macchina di tipo Von Neumann (quella col processore e la memoria per dati/programmi). Consentono di usare il processore, la memoria, il network, l’I/O, di gestire l’allocazione in memoria dei programmi e dei processi e di regolare il loro accesso alle risorse e le interazioni tra di loro.

Come diceva la mia prima insegnante di programmazione, un computer senza sistema operativo è solo ferraglia (hardware, in inglese).

Kernel

Il kernel è il sottoinsieme di programmi del sistema operativo che sovraintende all’accesso delle risorse hardware (processore, RAM, memoria di massa, rete e I/O). I programmi “utente” quelli che servono ad eseguire le azioni della vita quotidiana – far girare un sito web, gestiore un foglio di calcolo, montare un video, mantenere un database – accedono alle risorse hardware solamente per mezzo del sistema operativo (e se lo saltano la circostanza è eccezionale e si fa solo in casi estremi).

Quali sono i principali compiti svolti dal kernel dei sistemi operativi?

Sintetizzando al massimo si possono individuare nel kernel le seguenti 2 attività:

  1. Allocazione delle risorse (memoria e I/O)
  2. Pianificazione e gestione dei processi (scheduling, ciclo di vita, segnalazioni tra processi, arbitraggio)

Allocazione delle risorse (RAM e I/O)

Spazio kernel e spazio utente

I computer moderni dividono la memoria in spazio del kernel e spazio utente. Lo spazio utente è il luogo in cui viene eseguito il software applicativo, mentre lo spazio del kernel è dedicato al lavoro dietro le quinte necessario per far funzionare un computer, come l’allocazione della memoria e la gestione dei processi. A causa di questa separazione tra spazio kernel e spazio utente, il lavoro svolto dal kernel non è in genere visibile all’utente.

Il sistema operativo è responsabile dell’occupazione/liberazione dello spazio di memoria per i processi. Mantiene le mappature dalla memoria virtuale a quella fisica (che sono archiviate nelle tabelle delle pagine). Decide anche quanta memoria allocare a ciascun processo e quando un processo deve essere rimosso dalla memoria.

L’accesso alla memoria RAM è un processo molto dispendioso anche in termini di tempo. Per eseguire operazioni di I/O in memoria infatti il microprocessore si avvale di un dispositivo hardware di controllo dedicato detto Memory Controller. Il MC gestisce l’individuazione delle locazioni di memoria attraverso l’indirizzamento e l’I/O dei dati da e verso il processore attraverso l’infrastruttura del bus dati (data bus). Nei processori di ultima generazione il memory controller è stato integrato nei microprocessori (infatti si chiama IMC – Integrated Memory Controller) ma l’infrastruttura fisica del bus (con i suoi clock) e la RAM rimangono pur sempre periferici e quindi introducono latenze.

Se un programma deve inizalizzare delle variabili non va a prendersi la RAM direttamente, ma se la fa allocare dal kernel. Ogni processo ha a disposizione una memoria virtuale visibile come heap (“mucchio”, area di memoria utilizzata per variabili globali) o come stack (“pila”, area di memoria utilizzata per chiamate a funzione, ricorsione e variabili locali): per ogni processo il sistema operativo gli presenta quello che a lui sembra essere l’intervallo di memoria completamente indirizzabile. Quindi su una macchina a 32 bit, ogni processo “pensa” di avere a sua disposizione 4 GB di memoria contigua.

In realtà, il sistema operativo, dietro le quinte, è impegnato a mappare le allocazioni di memoria virtuale su blocchi reali di memoria fisica. Quindi, ad esempio, un’allocazione di memoria virtuale di 400 byte viene mappata su 100 blocchi fisici da 4 byte. Quei blocchi fisici non devono essere contigui (e quasi mai lo sono – nulla impedisce che accada, ma su una macchina che esegue qualsiasi tipo di lavoro, è altamente improbabile) ma l’allocazione della memoria virtuale deve essere contigua.

Memoria Virtuale

La memoria virtuale è una tecnica che dà a un programma applicativo l’impressione di avere una memoria di lavoro RAM non frammentata, mentre in realtà può essere fisicamente frammentata e si può persino estendere nello spazio di archiviazione su disco. I sistemi che utilizzano questa tecnica semplificano la programmazione di applicazioni di grandi dimensioni e utilizzano la memoria fisica reale (ad es. RAM) in modo più efficiente rispetto a quelli senza memoria virtuale.

Attenzione che la “memoria virtuale” non è solo “l’utilizzo dello spazio su disco per estendere le dimensioni della memoria fisica”. L’estensione della memoria è una normale conseguenza dell’utilizzo di tecniche di memoria virtuale, che può essere gestita con le sovrapposizioni (overlay) o lo scambio completo di programmi e relativi dati (swap) su disco mentre questi sono inattivi.

Per “memoria virtuale” si intende più precisamente l’inganno che viene fatto ai programmi facendo credere loro di utilizzare grandi blocchi di indirizzi contigui.

malloc

Facciamo un esempio. Un programma C alloca dinamicamente una locazione di memoria. Cioè lo fa a runtime, non viene riservata una memoria iniziale dal compilatore (ad esempio come quando dichiaro una variabile int) ma lo fa in corsa per esempio per dichiarare un array di dimensione arbitraria che lo user si inventa al momento, mentre il programma sta già girando.

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
 
    // Questo puntatore contiene l'indirizzo base del blocco creato
    int* ptr;
    int n, i;
 
    printf("Scrivi il numero di elementi dell'array:");
    scanf("%d",&n);
 
    // Alloca dinamicamente la memoria usando malloc()
    ptr = (int*)malloc(n * sizeof(int));
...

In questo esempio al momento della compilazione non sappiamo quanta memoria servirà al programma perché è l’utente che lo decide dopo averlo avviato.

L’istruzione malloc() riserverà al processo, che rappresenta la realizzazione fisica del programma C, un’area di memoria (virtuale) n volte la dimensione di una variabile di tipo intero. Per esempio se la variabile intera è rappresentata in 32 bit (4 byte) e n=100, il processo dovrà allocare 400 byte.

Dove sono realmente questi 400 byte? Il programma non lo sa (il programmatore ancora meno).

Il kernel gli assegnerà 400 byte che lui – il programma – vedrà come contigui, ma dietro le quinte il kernel sa esattamente nella RAM fisica dove si trovano questi 400 byte che possono anche essere sparpagliati. Per mantenere questa associazione il kernel usa una tecnica chiamata paginazione (paging) che ha il compito non soltanto di fornire una mappatura tra indirizzi virtuali e fisici ma anche di segregare i processi in aree di memoria stagne e di fornire i puntamenti ai blocchi di RAM non contigui in modo tale che la memoria virtuale risulti non frammentata.

Gestione dei processi

Un programma si può definire come una sequenza (“passiva”) di istruzioni scritte in un determinato linguaggio che ha lo scopo di risolvere un problema o, più in generale, di effettuare un’attività.

Processo

È l’effettiva esecuzione delle istruzioni che definiscono il programma. Un programma può essere suddiviso in più processi; per esempio se un programma apre più finestre, ogni finestra è un processo separato. I processi sono definiti e gestiti dal sistema operativo.

Un singolo processore del computer esegue una o più istruzioni alla volta (per ciclo di clock), una dopo l’altra. Per consentire agli utenti di eseguire più programmi contemporaneamente (ad esempio, in modo che il tempo del processore non venga sprecato in attesa di input da una risorsa), i sistemi informatici a processore singolo possono eseguire la divisione del tempo o time sharing.
La condivisione del tempo consente ai processi di passare da uno stato di esecuzione ad uno stato di attesa per continuare ad essere eseguiti.
Nella maggior parte dei casi ciò avviene molto rapidamente, fornendo l’illusione che diversi processi vengano eseguiti “contemporaneamente”. Questo è noto come concorrenza o multiprogrammazione.

Pianificazione e gestione dei processi.

Il sistema operativo mantiene separati i suoi processi e alloca le risorse di cui hanno bisogno in modo che abbiano meno probabilità di interferire tra loro e causare errori di sistema (ad esempio deadlock – la situazione in cui due processi aspettano a vicenda che l’altro acceda ad una locazione di memoria – o thrashing – situazione in cui il processo spende più tempo a paginare la memoria che ad eseguire le istruzioni). Il kernel può anche fornire meccanismi per la comunicazione tra processi per consentire ai processi di interagire in modi sicuri e prevedibili.

Arbitraggio dei processi

Quando il kernel decide che bisogna parcheggiare un processo (P1) e servirne un altro (P2) in base a un qualche criterio, avviene il salvataggio in una particolare area della RAM del set di registri e flag del processore, del program counter (il registro che contiene l’indirizzo della prossima istruzione del programma da caricare nel processore) e dello stack (che rappresenta la nidificazione delle chiamate a funzione) relativi al processo P1 e preleva dalla stessa area, in una locazione diversa destinata al processo P2, i diversi valori per lo stesso set di registri e dello stack per il processo P2 e li carica nel processore per poi avviarne l’esecuzione. Questa sequenza scrittura – lettura si chiama commutazione del contesto (context switching).

Sincronizzazione dei processi concorrenti.

Se ci sono processi che concorrono a scrivere in un stessa locazione di memoria (ad esempio due utenti che condividono un conto bancario ed eseguono operazioni allo stesso momento), il sistema operativo deve orchestrare le operazioni su questa locazione di memoria perché questa rappresenti in ogni istante il risultato che tutti si attendono.

Esempio

Alice e Bob aprono un conto bancario condiviso. Il loro saldo iniziale è di 0 €. Ognuno di loro deposita 100 €. Ci aspettiamo che il saldo finale debba essere di 200 €.

Ma consideriamo la seguente sequenza di operazioni:

  • 1) Alice legge Saldo (legge 0 €),
  • 2) Alice incrementa Saldo ma non lo salva ancora: Saldo + 100 €: a questo proposito si consideri che l’istruzione seguente
    s = s + 100
    viene eseguita in più passaggi dal processore (eax, eay sono registri interni del processore – il processore esegue tutte le operazioni aritmetiche internamente perché così opera in modo enormemente più veloce – e s la locazione di memoria virtuale che contiene il Saldo)
    mov     eay, 0
    mov     eax, 100
    add     eax, eay
    push    s
  • 3) Bob legge il Saldo (legge anche lui 0 €, poiché Alice non ha ancora scritto il nuovo Saldo, non è ancora arrivata a push s),
  • 4) Bob incrementa il Saldo e neanche lui lo sovrascrive: Saldo + 100 €,
  • 5) Alice scrive il Saldo (scrive 100 €),
  • 6) Bob scrive Saldo (scrive anche lui 100 €).

Questa sequenza porta al saldo finale di 100 € (che non è corretto). Questo esempio illustra chiaramente l’importanza della sincronizzazione tra i processi di Alice e Bob. In particolare, la seguente sequenza (se applicata) produrrebbe risultati corretti:

  1. Alice legge,
  2. Alice incrementa,
  3. Alice scrive,
  4. Bob legge,
  5. Bob incrementa,
  6. Bob scrive .

Potrebbero esserci altre sequenze di letture e scritture che producono ugualmente risultati corretti, però l’importante è vedere come il sistema operativo esegua l’arbitraggio dei due processi affinché venga eseguito prima completamente il processo di Alice – che è partito prima – e poi quello di Bob.

Commutazione del contesto

Un altro dei compiti del kernel è fare in modo che ogni processo abbia accesso alla CPU e alle sue risorse per un certo periodo limitato di tempo. Questo è dovuto al principio che un processore serve un processo alla volta. Ultimamente – negli ultimi 20 anni – con l’avvento dei processori multicore abbiamo effettivamente la possibilità di eseguire più processi in contemporanea moltiplicando il numero di CPU. Attualmente per i processori commerciali siamo a 8 core per cui si possono eseguire 8 processi contemporaneamente. Ma il principio è sempre quello: una CPU (un core) esegue un solo processo alla volta. La tecnica del multitasking prevede un uso a divisione di tempo della singola CPU (time sharing).

Risorse web

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito utilizza Akismet per ridurre lo spam. Scopri come vengono elaborati i dati derivati dai commenti.