Il processo è l'astrazione di un programma in esecuzione e tutto il resto del OS si basa su questa astrazione.

Questa astrazione è una delle più antiche e importanti, siccome consente a gli utenti di percepire l’esistenza di operazioni concorrenti, anche se è disponibile una sola CPU.

Sappiamo che i computer spesso fanno molte cose nello stesso momento (es. le pagine inviate da un web server). Infatti per gestire queste attività è necessario un sistema di multiprogrammazione, dove la CPU passa da un processo all’altro rapidamente. In questo modo si da l’illusione che la CPU stia eseguendo più processi contemporaneamente (pseudoparallelismo in contrasto con il vero parallelismo hardware nei sistemi multiprocessore).

Modello di processo

^010163 Un processo è un’istanza di un programma in esecuzione, inclusi i valori attuali:

  • del Program Counter
  • dei registri
  • dello Stack (varia)

Ogni processo sembra di avere una sua CPU virtuale, ma in realtà è la CPU che passa il controllo tra processi rapidamente (multiprogrammazione).

Quando si passa da un processo all’altro si salva il pc del primo processo e si ripristina il pc del secondo.

Nella seguente grafico notiamo che nel corso del tempo un solo processo è attivo alla volta, ma tutti progrediscono :

Creazione del processo :luc_cross:

^32d69d I 4 eventi che creano un processo sono :

  1. inizializzazione del sistema (init)
  2. esecuzione di una system call per la creazione di un processo figlio da parte di un processo padre (es. fork())
  3. richiesta dell’utente di creare un nuovo processo (es. tramite bash)
  4. avvio di un batch job (es. cronjob)

I meccanismi di creazione sono:

  • UNIX: unica system call è fork(), che crea un clone esatto (figlio) del processo chiamante (padre), comprese tutte le variabili e i file descriptor aperti
    • di solito subito dopo, il processo creato chiama execve() per sostituire la sua immagine in memoria con il programma desiderato
  • Windows: funzione CreateProcess(), che gestisce sia la creazione del nuovo processo sia il caricamento del programma corretto.

review In tutti e due i meccanismi il processo padre e figlio hanno spazi di indirizzi distinti, quindi una modifica di una variabile dell’uno non interferisce con l’altro, mentre i riferimenti alle stesse risorse sono condivise (es. file).

Terminazione di un processo :luc_skull:

Un processo termina solo in una delle seguenti condizioni:

  • uscita normale (volontaria): ha completato il suo compito e invoca una syscall di terminazione (exit() in UNIX e ExitProcess() in Windows)
  • errore (volontaria): scopre un errore fatale (es. file non esistente)
  • errore fatale (involontario): errore in run-time
  • uccisione da un altro processo (involontaria): processo è stato terminato da un altro processo (kill() in UNIX e TerminalProcess() in Windows)

In Linux per terminare un processo utilizziamo il comando kill <livello_di_forzatura> <pid> dove il livello di forzatura -9 è brutale! Oppure con pkill <nome processo>.

Gerarchie di processi :luc_git_merge:

Generalmente quando un processo (P) crea un altro processo (F), il figlio F continua ad essere associato al genitore P.

In UNIX, un processo e tutti i suoi discendenti formano una gerarchia di processi (albero di processi).

Per esempio quando il computer è avviato, il processo init fa il fork di un processo per terminale (login), aspetta che si fa il login, e poi parte una shell che accetta comandi :

a loro volta questi comandi possono creare altri processi e cosi via.

review In Windows, non esiste il concetto di gerarchia, infatti tutti i processi sono considerati uguali. L’unica associazione figlio-padre, è che al padre viene fornito un token speciale chiamato handle per controllare il figlio.

Stati di un processo :luc_rotate_ccw:

^dd320b Un processo (non terminato) può trovarsi in 3 stati :

  • running: processo sta utilizzando la CPU
  • ready: processo è pronto per essere eseguito, ma attende che lo scheduler gli assegni la CPU
  • blocked: processo non può proseguire, perché in attesa di un evento (es. input da terminale o disponibilità di una risorsa)

Le possibili transazioni tra stati :

  • running blocked: quando il processo non può continuare perché deve aspettare qualcosa
  • running ready: lo scheduler fa eseguire un altro processo (es. finita quantum)
  • blocked ready: evento esterno atteso si verifica
  • ready running : lo scheduler decide di metterlo in esecuzione

Realizzazione dei processi :luc_table:

Per implementare il modello di processo, il OS mantiene una tabella dei processi che contiene una riga chiamata Process Control Block (PCB) per ogni processo.

Il PCB mantiene le informazioni associate ad un processo :

  • PID, UID (user id) e GID (group id)
  • spazio degli indirizzi di memoria
  • registri hardware (program counter e stack pointer)
  • stato dei file aperti
  • informazioni di scheduling
  • segnali (signal)
  • interrupt

Questi dati sono importanti per essere salvati quando il processo andrà nello stato blocked e ripresi quando il processo sarà ready, come se il processo non si fosse mai fermato.

Signal vs Interrupt

I signal e gli interrupt sono meccanismi utilizzati dai OS e nelle applicazioni per gestire eventi asincroni.

Interrupt :

Signal :

  • origine : eventi software (es. da processo o OS)
  • gestione : signal handler personalizzati o predefiniti
  • uso : gestione condizioni eccezionali nelle applicazioni
  • asincronia :
    • inviati asincronamente
    • possono essere gestiti in modo sincrono
Interrupts

  • origine : dispositivi I/O (es. tastiera o disco)
  • gestione : ISR - Interrupt Service Routine
  • uso :
    • comunicazione tra hardware e software
    • risposta pronta per gli eventi hardware
  • asincroni e gestiti subito

L’idea alla base parte dalla necessità di prelevare la CPU da un processo per gestire un evento I/O. Ogni volta che arriva un segnale di interrupt, la CPU smette di fare quello che stava facendo e passa il controllo al OS.

Quando arriva un interrupt la CPU deve sapere esattamente quale codice eseguire per gestire l’evento :

  • l’interrupt vector : “puntatore” che associa un numero di interrupt alla funzione specifica che deve gestirlo (interrupt handler).
  • tabella che contiene tutti questi puntatori si chiama Interrupt Descriptor Table (IDT).

I tipi di interrupt possono essere :

  • software : per syscall
  • dispositivo hardware : asincroni perché non possiamo prevederli
  • eccezioni (es ) : generate dalla CPU stessa

Lo scheduler è un componente fondamentale che fa da mediatore ogni volta che si verifica un interrupt. Un processo infatti non può cedere la CPU ad un altro processo (context switch) senza passare per lo scheduler.

PS : vedremo meglio in gestione interrupt nel OS

Signal

^37bc8e Possiamo vedere i segnali come degli “interrupt software” inviate ad un processo mentre sta girando.

I 2 tipi di segnali sono :

  • hardware-induced (es. SIGKILL) : eccezione hardware kernel processo
  • software-induced (es. SIGQUIT o SIGPIPE) : creati da kernel/processo processo

Le azioni possibili di un processo quando riceve un segnale sono :

  • term : processo terminato immediatamente
  • ign : segnale ignorato
  • core : termina ma prima salva un file (“core dump”) che è una fotografia della sua memoria in quel momento
  • stop : processo in pausa
  • cont : processo riprende dalla pausa

Esiste un'azione di default per ogni segnale, ma possiamo sovrascriverla. Inoltre i segnali possono essere bloccati temporaneamente.

Gestione dei segnali

La gestione (catching) dei segnali avviene nel seguente modo :

  1. registrazione : processo dice al OS che se gli arriva il segnale x, deve eseguire l’azione y (handler)
  2. invio : quando il OS invia il segnale, interrompe il flusso normale del processo ed esegue l’handler
  3. salvataggio contesto : contesto di esecuzione corrente deve essere salvato/ripristinato

Per esempio possiamo “sovrascrivere” cosa farebbe CTRL+C :

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
 
// Definizione dell'Handler (azione al segnale)
void handler(int signum) {
    printf("\nHo catturato il segnale! Numero segnale: %d\n", signum);
    printf("Sto uscendo in modo pulito...\n");
    
    // Terminazione del programma
    exit(signum);
}
 
int main() {
    // REGISTRAZIONE: Collego il segnale SIGINT (Ctrl+C) alla mia funzione
    signal(SIGINT, handler);
 
    // Ciclo infinito per tenere il programma vivo
    while(1) {
        printf("Sto dormendo... (Premi Ctrl+C per interrompermi)\n");
        sleep(1);
    }
 
    return 0;
}
Permessi e consegna dei segnali

Un processo può inviare segnali solo ai processi dello stesso utente (eccezione per root).

Il comando kill è una system call che dice al kernel mandare un segnale ad un processo (signal software-included). Il segnale non arriva subito al processo, ma viene accodato nel kernel.

Ogni processo (o thread) ha associato una struttura che mantiene :

  • segnali pendenti : in attesa di consegna
  • maschera dei segnali bloccati : segnali temporaneamente sospesi (arrivano vengono bloccati temporaneamente, finché non si toglie il blocco)
  • tabella delle disposizioni : per ogni segnale c’è scritto cosa fare :
    • default : azione standard
    • ignorato
    • handler : specifica funzione scritta

Nel caso in cui sia specificato un handler utente :

  1. il kernel salva il contesto (registri, PC, SP, flag)
  2. prepara lo stack del segnale e imposta l’indirizzo dell’handler
  3. handler eseguito in user space del processo
  4. al termine, un trampolino invoca la system call rt_sigreturn
  5. kernel ripristina il contesto salvato
  6. kernel riprende il PC originale, continuando dal punto interrotto

ARCHITETTURA

Modello della multiprogrammazione :luc_gauge:

La multiprogrammazione è utilizzata per ottimizzare l’utilizzo della CPU.

Consideriamo infatti la seguente considerazione probabilistica: se in media un processo spede una frazione di tempo in attesa di I/O, allora la probabilità che processi presenti in memoria stiano aspettando contemporaneamente l’I/O è . Quindi l’utilizzo della CPU è calcolato coma la probabilità complementare, ovvero .

Ad esempio:

  • se un processo è in attesa di I/O per l’80% del tempo (), per portare l’utilizzo della CPU al 90%, sono necessari almeno processi in memoria (grado di multiprogrammazione )
  • un computer ha 512 MB di memoria, se il OS occupa 128 MB e un processo 127 MB, può contenere in memoria fino a 3 processi
  • l’utilizzo della CPU quindi è pari a
  • se aggiungiamo 512 MB, possiamo avere altri 4 processi e quindi avremo che l’utilizzo della CPU sarà

Questo modello semplificato, permette di effettuare previsioni sull’utilizzo della CPU.


Threads