24 Sep 2024, 7:42 AM
24 Sep 2024, 7:42 AM

4. Gestione dei processi e threads

Un processo è definito come un'istanza di programma in esecuzione. Un processo viene eseguito in modo sequenziale (nel senso una esecuzione alla volta, ma non per forza in ordine), ma in un sistema multiprogrammato i processi evolvono in modo concorrente.

Da cosa è formato un processo?

Un processo consiste di:

Memoria.png|700

Attributi (Process Control Block)

Vediamo ora in maggiore dettaglio la parte degli attributi.
All’interno del S.O. ogni processo è rappresentato dal Process Control Block (PCB), che rappresenta appunto la sezione "Attributi" nell'immagine sopra. Il PCB è una struttura dati con una tabella in memoria principale che conserva le seguenti informazioni:

PCB.png|500

Stati di un processo

Durante la sua esecuzione, un processo evolve attraverso diversi stati:

StatiProcesso.png|700

Scheduling

Lo scheduler si occupa di controllare e selezionare il processo da eseguire nella CPU. Ce ne sono di 2 tipi:

I processi vengono inseriti in delle code:

I processi possono essere descritti come

Come fa un processo a sapere a quale tipo appartiene? Bella domanda.

L'operazione più critica è quella fatta dal dispatcher, ovvero assegna la CPU ad un certo processo e fa passare il processo da ready a running. Come avviene nel dettaglio?

  1. Cambio di contesto: viene salvato il PCB del processo che esce e viene caricato il PCB (precedentemente registrato) del processo che entra
  2. Passaggio alla modalità utente (all’inizio della fase di dispatch il sistema si trova in modalità kernel)
  3. Salto all'istruzione da eseguire del processo appena arrivato nella CPU

Il tempo necessario al cambio di contesto è puro sovraccarico

Creazione di un processo

Per quanto riguarda la creazione dei processi, la maggior parte dei SO permette a processi di crearne di nuovi tramite apposite chiamate di sistema. Si parla in questo caso di processi padre e processi figlio; il figlio può ottenere le risorse dal padre o direttamente dall'SO.

A seconda delle politiche del SO si hanno 2 tipi di esecuzione:

Come si crea un processo in UNIX?

Come si termina un processo?

Un processo può terminare in diversi modi:

Threads

Qual è la differenza tra processo e thread?
Un thread è definito come l'unità minima di utilizzo della CPU, un processo come l'unità minima di possesso delle risorse. All'interno di un processo, i thread condividono codice, dati e risorse, ma ognuno può avere stati e registri diversi. La suddivisione di un processo in più thread permette che parte di essi siano in esecuzione mentre altri sono bloccati per l'attesa di un evento di I/O (questo è solo uno dei vantaggi). Dato che per definizione condividono codice e dati, la comunicazione e la condivisione di risorse tra thread è più semplice rispetto a quella tra processi; anche la creazione di thread è più veloce della creazione di processi.

Per riassumere, ad un processo sono associati:

Invece, ad una singola thread sono associati:

Inoltre le thread condividono:

In un S.O. classico abbiamo che 1 processo = 1 thread , ma grazie al concetto di multithreading abbiamo la possibilità di supportare più thread per un singolo processo. Di conseguenza, abbiamo una separazione tra “flusso” di esecuzione (thread) e spazio di indirizzamento

Vantaggi dei threads

Come accennato in precedenza, l'uso di threads presenta vari vantaggi:

Stati di un thread

Gli stati di un thread sono gli stessi di un normale processo (pronto, in esecuzione, in attesa), ma la loro correlazione dipende dall'implementazione. Ad esempio un thread in wait potrebbe bloccare o meno altri thread dello stesso processo o anche l'intero processo.

Implementazione dei threads

Esistono essenzialmente due modi per implementare i thread in un SO, poi un terzo che è la combinazione dei primi due, di seguito riportati.

User-Level Thread

Implementati tramite apposite librerie (tipo pthread.h).

Vantaggi
Svantaggi

Kernel-Level Thread

Ad ogni thread a livello utente corrisponde un singolo thread a livello kernel. Vari esempi sono Win32, Linux, Native thread di Java.

Vantaggi
Svantaggi

Approccio misto

Ad N thread a livello utente corrispondono MN thread a kernel level. Si ha un buon parallelismo e risulta meno costoso del full kernel-level. Un esempio è Solaris.

Libreria POSIX (pthread.h)

Per usare i pthreads in un programma C, è necessario includere la libreria <pthread.h>. Per compilare un programma che usa i pthreads occorre linkare la libreria libpthread, usando l’opzione -lpthread:

$> gcc prog.c -o prog -lpthread

Creazione di un thread

Un thread ha vari attributi, che possono essere cambiati, ad esempio:

Gli attributi di un thread sono contenuti in un oggetto di tipo pthread_attr_t. La syscall per inizializzare un thread è la seguente:

int pthread_attr_init(pthread_attr_t *attr);

Questa funzione inizializza con i valori di default un “contenitore di attributi” *attr, che potrà poi essere passato alla system call che crea un nuovo thread.

Un nuovo thread viene creato con la syscall intera pthread_create che accetta quattro argomenti:

Terminazione di un thread

Un thread termina quanto finisce il codice della routine specificata all’atto della creazione del thread stesso, oppure quando, nel codice della routine, si chiama la syscall di terminazione:

void pthread_exit(void *value_ptr);

Quando termina, il thread restituisce il “valore di return” specificato nella routine, oppure, se chiama la pthread_exit, il valore passato a questa syscall come argomento.

Sincronizzazione tra threads

Un thread può sospendersi, in attesa della terminazione di un altro thread chiamando la syscall:

int pthread_join(pthread_t thread, void **value_ptr);

Sono comunque disponibili diversi strumenti per sincronizzare fra loro i thread di un processo, fra questi anche i semafori. In realtà i semafori non fanno parte dell’ultima versione dello standard POSIX, ma della precedente. Tuttavia sono normalmente disponibili in tutte le versioni correnti dei pthread.
I pthread mettono anche a disposizione meccanismi di sincronizzazione strutturati, quali le variabili condizionali.

Esempio

#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
void *tbody(void *arg) {
    int j;
    printf(" ciao sono un thread, mi hanno appena creato\n");
    *(int *)arg = 10;
    sleep(2); /* faccio aspettare un pò il mio creatore, poi termino */
    pthread_exit((int *)50); /* oppure return ((int *)50); */
}

int main(int argc, char **argv) {
    int i;
    pthread_t mythread;
    void *result;
    printf("sono il primo thread, ora ne creo un altro \n");
    pthread_create(&mythread, NULL, tbody, (void *) &i);
    printf("ora aspetto la terminazione del thread che ho creato \n");
    pthread_join(mythread, &result);
    printf("Il thread creato ha assegnato %d ad i\n",i);
    printf("Il thread ha restituito %d \n", result);
}

Il thread main ha generato un secondo thread tbody. Poi main si è messo in attesa della terminazione del thread creato (con pthread_join), ed è terminato lui stesso.
È importante notare che i due thread condividono lo stesso spazio di indirizzamento, e quindi vedono le stesse variabili: se uno dei due modifica una variabile, la modifica è vista anche dall’altro thread.
Nel codice di t1, il main passa al thread tbody il puntatore alla variabile i dichiarata nel main. il thread tbody modifica la variabile, e questa modifica è vista da main. Nel caso dei processi tradizionali, una cosa simile è ottenibile solo usando esplicitamente un segmento di memoria condivisa.

Ovviamente i thread di un processo possono condividere variabili in maniera ancora più semplice, usando variabili globali. Tuttavia un thread può anche avere variabili proprie, viste solo dal thread stesso usando la classe di variabili thread_specific_data.

SO come processo

Sappiamo che il sistema operativo è un programma a tutti gli effetti, quindi può essere considerato esso stesso un processo?
Questo varia a seconda dell'implementazione, in cui le parti del SO possono o meno essere considerate dei processi.
Vediamo dunque le alternative disponibili:

Kernel Separato

Il kernel esegue al di fuori dei processi utente, in uno spazio "riservato" in memoria ed in esecuzione privilegiata. Essendo appunto "separato" le cose vanno implementate due volte e in maniera diversa. Inoltre il S.O. prende il controllo dell'intero sistema. Il concetto di processo è quindi applicabile solo ai processi utente. Questa modalità è tipica dei primi SO.

Kernel nei processi utente

In questa implementazione i servizi del SO sono procedure chiamabili da programmi utente, accessibili però solo in modalità protetta (kernel mode).
Ogni processo ha un kernel stack per gestire le chiamate a funzione e una porzione di codice/dati condiviso tra tutti i processi. Ciò velocizza le chiamate di sistema, visto che in caso di interrupt o trap durante l’esecuzione di un processo utente è sufficiente solo un cambio di modalità (mode switch). Ossia, il sistema passa da user mode a kernel mode e viene eseguita la parte di codice relativa al S.O. senza context switch (da processo utente a processo di SO).

Kernel come processo

A differenza della precedente implementazione, qui solo una parte del kernel (solitamente lo scheduler) esegue separatamente, al di fuori di tutti i processi. Per il resto, ogni parte del SO è un processo separato, eseguito in modalità protetta (kernel mode). Questo è molto vantaggioso nei sistemi multicore, che di solito riservano dei processori ad esclusivo utilizzo del SO.