ARCHITETTURA INTERNA E
PRIMITIVE DI UNIX
  1. Processi e file
  2. Cenni alla gestione dei file
  3. Cenni alla gestione dei processi
  4. System call per Processi, thread  e Signal
  5. System call per File System
  6. System call per Gestione Tempi

Processi e File( Indice )

La figura 3.1 da' una visione logica dell'architettura Unix. In realta' i vari moduli interagiscono maggiormente. Sono indicati 3 livelli: kernel, user, hardware.

Le chiamate di sistema sono funzioni appartenenti ad una o piu' libreria. Possono, e devono, essere chiamate all'interno di un programma, esattamente come normali funzioni. La loro esecuzione tuttavia e' diversa dall'esecuzione di una funzione "normale" in quanto vengono eseguite in modo kernel .

Per poter passare in modo kernel e' necessario eseguire una trap. I parametri di chiamata di una primitiva sono inseriti al top dello stack utente, come per una normale procedura. dopo la trap i  parametri sono ricopiati al top dello stack del kernel.

Tutte queste operazioni sono effettuate in modo trasparente all'utente, che usa le funzioni di sistema esattamente come normali procedure di libreria.

Figura 1

Cenni alla gestione dei file( Indice )

Unix vede "ogni cosa" (o quasi) come file. L'unificazione del paradigma dei dati semplifica anche l'interfaccia delle funzioni di sistema. Ad esempio, la funzione open di Unix puo' essere usata per contesti diversi, a seconda del tipo di dato visto come file: i driver di I/O e i canali di comunicazione sono file, cosi' come la memoria virtuale. Nelle versioni UNIX che supportano il file system /proc l'istruzione

fd=open("/proc/1234", O_RDWR);
consente di accedere, se se ne hanno i privilegi, alla memoria del processo 1234 utilizzando il descrittore fd (e' il caso di un programma di debugger).

Per avere un confronto, Windows 2000 segue invece la programmazione ad oggetti, per cui file, processi, semafori, caselle di posta, connessioni HTTP ecc., sono tutti oggetti. Un processo puo' acquisire un handle su di un oggetto (cioe' il suo indirizzo e i privilegi di accesso), e accedere ad esso.

Ogni file Unix ha un i-node contenente la descrizione "fisica" del file. Ogni file ha un solo i-node, ma puo' avere piu' nomi (attraverso il meccanismo di link). Il nome e' il path-name. Per accedere occorre avere i permessi per ogni directory del cammino.

La Kernel File Table e' unica per il kernel. Ogni file effettivamente aperto o creato da un processo ha un entry che contiene: un reference counter per contare i riferimenti (+1 dopo ogni fork), il tipo di accesso (R/W) del file, un puntatore (offset) interno al file per il posizionamento in lettura/scrittura e un puntatore che lo collega all'i-node del file.

La File Descriptor Table e' unica per ogni processo. Ogni entry rappresenta un file accessibile dal processo, e contiene il nome usato dall'utente per quel file e un puntatore all'entry relativa nella Kernel File Table.

Posizioni riservate                             0 ==>   standard input
della File Description Table              1 ==>   standard output
                                                            2 ==> standard error

Figura 2

Cenni alla Gestione dei processi( Indice )

Modulo gestione memoria.
Scambi fra memoria centrale secondaria (swapping o demand paging). Lo swapper realizza lo scheduling a lungo termine.

Struttura dati per processi
Un processo puo' essere in escuzione in 2 modi: kernel e utente (si tratta di un sistema "orientato alle procedure"). Un processo ha almeno 3 regioni: codice, dati e stack (allocato dinamicamente). Unix usa 2 tipi di stack, user e kernel, a seconda del modo di esecuzione. Il passaggio da user a kernel avviene tramite trap (o software interrupt). Ogni processo e' rappresentato dal kernel nella Tabella dei Processi (l'area U mantiene informazioni sui processi).
Il codice e' in genere unico per piu' processi.

Figura 3

Tavola dei Processi (per context switch):

 
stato, size           memoria 
uid                   user identifier, per gestione signal 
pid                   anche parentele fra processi 
parametri di schedulazione    
lista segnali         ricevuti ma non ancora gestiti 
contatori             tempo CPU, risorse ... 
puntatore area U
Contesto del processo: contenuto dello spazio utente, registri hardware, strutture dati del kernel relativi a quel processo.

System Call per Processi, thread e Signal ( Indice )

1. Meccanismo di biforcazione                            proc_id = fork()

E' la funzione che consente di creare un nuovo processo.
Dopo la fork, se ha avuto successo, esitono due processi: il genitore e il figlio. Il figlio condivide il codice con il genitore, mentre la memoria, i registri e le informazioni sui files (tabelle) vengono duplicate(vedi figura), ma hanno diversi pid. Poiche' il program counter e' lo stesso, entrambi i processi ritengono di aver eseguito la funzione fork. Entrambi ricevono un valore di ritorno, ma diverso:

       proc_id =      > 0 per processo padre
                      0 per il figlio
                      < 0 per il padre (errore)


Figura 4

Esempio:

#include <stdio.h>
main()
{
int pid;
pid=fork();
if (pid<0)
{
perror ("fork fallita");
exit(1);
}
 if (pid == 0) // eseguito SOLO dal figlio
{
printf(“Sono il figlio ! (pid: %u)\n”, getpid());
}
// codice eseguito da padre e da figlio
printf(“Sono il padre: pid di mio figlio: %d\n”, pid);
ecc.
}

Esempio di OUTPUT:
Sono il figlio ! (pid: 3765)
Sono il padre: pid di mio figlio: 3765
Sono il padre: pid di mio figlio: 0

Altro codice:
#include <stdio.h>
main()
{
int pid;
pid=fork();
if (pid<0)
{
perror ("fork fallita");
exit(1);
}
 if (pid == 0) // eseguito SOLO dal figlio
{
printf(“Sono il figlio ! (pid: %u)\n”, getpid());
}
else
// codice eseguito solo dal padre
printf(“Sono il padre: pid di mio figlio: %d\n”, pid);
// codice eseguito sia dal figlio che dal padre
ecc.
}
In questo caso un output possibile e':
Sono il padre: pid di mio figlio: 3765)
Sono il figlio ! (pid: 3765)


Effetti della fork()

    * Allocazione di una nuova entry nella tavola dei processi per il figlio
    * Allocazione di una nuova user structure relativa al figlio nella quale viene copiata la user structure del padre
    * Allocazione dei segmenti di dati e stack delfiglio nei quali vengono copiati dati e stack del padre
    * Aggiornamento della text structure del codice eseguito (condiviso col padre)

Di conseguenza:

Cos'e' un signal?

E' una interruzione "simulata"di un processo.

Nella Tavola dei processi, per ogni entri di processo o di thread e' specificato:

Puo' essere inviato:

  • da kernel a processo
  • da processo a processo

  • Funzionamento
    Nota1: essendoci una singola signal handler array, il comportamento a fronte di una signal e' unico per tutto il processo, ma una signal puo' anche essere inviata ad un singolo thread e non fare effetto sugli altri (vedi thread dopo)
    Nota2: non è safe chiamare tipiche funzioni della libreria standard, come la printf(), la scanf() o altre funzioni definite all’interno del programma.  Usare altre funzioni tipo read e write di cui e' garantita l'atomicita'.

    Caso A: Esempi

       ctr C   --------->  interruzione hardware
                           esecuzione di procedura kernel
                           seleziona processo destinatario
       processo <--------  signal

       errore   --------->  interruzione software
                            esecuzione di procedura kernel
                            seleziona processo destinatario
       processo <---------  signal

    Caso B: esempi Si passa comunque sempre per il kernel.

    Il processo destinatario di ua signal non e' immediatamente risvegliato o interrotto: il kernel marca con un flag il processo ricevente per avvisarlo che e' arrivato un signal con un certo numero, in modo che alla successiva schedulazione il kernel sara' al corrente della situazione. Si posssono memorizzare piu' signal, ma solo uno per ogni tipo (gli altri sono persi).

    Processo ricevente
    Per ogni tipo (numero) di signal l'utente puo' associare una procedura (all'interno del processo ricevente).
    Viene allora definita dal kernel una tavola di corrispondenze fra numero signal e indirizzo della procedura associata. Tale procedura puo' pero' anche mancare.

    Possibili azioni per gestione signal:

    Alcuni valori (i piu' bassi) sono predefiniti dal sistema (riservati perche' gestiti dal kernel), altri 2 (di norma) possono essere usati dall'utente per inviare signal con significati particolari.
    In alcuni casi assieme alla terminazione del processo il kernel esegue anche un dump della memoria (vedere file core presente successivamente la terminazione).

    In caso di terminazione di un processo con core dump, ricordarsi di cancellarlo (occupa molta memoria). Si puo' fare una ricerca dei core nel proprio file system con il comando
    find . -name core

    2. Funzione kill                                         s = kill(pid, signal)

    pid e' l'identificatore del processo ricevente.
    signal e' il numero del segnale da inviare.

    Consente di inviare un signal al processo specificato, se si hanno i privilegi per farlo.

    Nel file signal sono elencati i signal predefiniti in Linux.
    I numeri corrispondenti ai signal non sono gli stessi per tutti i sistemi Unix, quindi e' opportuno usare i nome simbolici.

    ESEMPIO

    /* forever.c */

    #include <fcntl.h>
    int  pid;

    /* sono creati due processi che vivono per sempre,
       anche dopo la morte del padre.
       Si possono killare ad esempio con il comando kill
       Sono elencati con lo stesso nome dell'eseguibile
       del padre (ad esempio a.out) */

    main ()
    {  
       if ((pid=fork())==0) 
         { for (;;) ;}
       printf("1st son pid = %d \n",pid);
       if ((pid=fork())==0) 
         { for (;;) ;}
       printf("2nd son pid = %d \n",pid);
       exit(0);
    }

    3. Definizione della funzione associata    oldf = signal(segnale, funzione)
  • oldf: puntatore vecchia funzione (definita precedentemente o null)
  • segnale: numero (tipo) del segnale
  • funz.: puntatore alla nuova funzione handler associata a "segnale".

  • Se funz=SIG_IGN il signal e' ignorato.
    Se funz.=SIG_DFL si torna alla situazione precedente.
    Descrizione della funzione signal per Linux:
    libra:/> man signal

    SIGACTION(2)        Linux Programmer's Manual        SIGACTION(2)
    NAME
           signal - ANSI C signal handling.
    SYNOPSIS
           #include <signal.h>
           void (*signal(int signum, void (*handler)(int)))(int);
    DESCRIPTION
           The  signal  system call installs a new signal handler for
           signal signum.  The signal handler is set to handler which
           may be a user specified function, or one of the following:
                  SIG_IGN
                         Ignore the signal.
                  SIG_DFL
                         Reset the signal to its default behavior.
    RETURN VALUE
           signal returns the previous value of the  signal  handler,
           or SIG_ERR on error.
    NOTES
           Signal handlers cannot be set for SIGKILL or SIGSTOP.
           Unlike BSD systems, signals under Linux are reset to their
           default behavior when raised.
           If you're confused by the prototype at  the  top  of  this
           manpage, it may help to see it separated out thus:
           typedef void (*sighandler_t)(int);
           sighandler_t signal(int signum, sighandler_t handler);
    SEE ALSO
           kill(1),  kill(2),  killpg(2),  pause(2), raise(3), sigac-
           tion(2), signal(7), sigsetops(3), sigvec(2)

    4. definizione congiunta:    int sigaction ( int signum, const struct sigaction* act, struct sigaction* oldact )

    Valore di ritorno:   0 successo, -1 errore (sets errno)
    act : struttura che definisce il nuovo trattamento del segnale signum.
    Con act a NULL si prende solo il vecchio gestore
    oldact : ritorna il contenuto precedente del signal handler array.

    Signal di Linux:

    Signal Name Number Description
    SIGHUP 1 Hangup (POSIX)
    SIGINT 2 Terminal interrupt (ANSI)
    SIGQUIT 3 Terminal quit (POSIX)
    SIGILL 4 Illegal instruction (ANSI)
    SIGTRAP 5 Trace trap (POSIX)
    SIGIOT 6 IOT Trap (4.2 BSD)
    SIGBUS 7 BUS error (4.2 BSD)
    SIGFPE 8 Floating point exception (ANSI)
    SIGKILL 9 Kill(can't be caught or ignored) (POSIX)
    SIGUSR1 10 User defined signal 1 (POSIX)
    SIGSEGV 11 Invalid memory segment access (ANSI)
    SIGUSR2 12 User defined signal 2 (POSIX)
    SIGPIPE 13 Write on a pipe with no reader, Broken pipe (POSIX)
    SIGALRM 14 Alarm clock (POSIX)
    SIGTERM 15 Termination (ANSI)
    SIGSTKFLT 16 Stack fault
    SIGCHLD 17 Child process has stopped or exited, changed (POSIX)
    SIGCONT 18 Continue executing, if stopped (POSIX)
    SIGSTOP 19 Stop executing(can't be caught or ignored) (POSIX)
    SIGTSTP 20 Terminal stop signal (POSIX)
    SIGTTIN 21 Background process trying to read, from TTY (POSIX)
    SIGTTOU 22 Background process trying to write, to TTY (POSIX)
    SIGURG 23 Urgent condition on socket (4.2 BSD)
    SIGXCPU 24 CPU limit exceeded (4.2 BSD)
    SIGXFSZ 25 File size limit exceeded (4.2 BSD)
    SIGVTALRM 26 Virtual alarm clock (4.2 BSD)
    SIGPROF 27 Profiling alarm clock (4.2 BSD)
    SIGWINCH 28 Window size change (4.3 BSD, Sun)
    SIGIO 29 I/O now possible (4.2 BSD)
    SIGPWR 30 Power failure restart (System V)

    Si possono vedere esempi d'uso nel file sigEsempi. ESEMPIO 1
    #include <stdio.h>
    #include <signal.h>

    int status;
    void intr()  {printf /"ricevuto \n");
                  return;
                 }
    main()
    { int Spid;
      pid=getpid;
      signal(SIGUSR1, intr);
      if (fork() == 0)  { kill(SIGUSR1, pid);
                          sleep(5);
                          kill(SIGUSR1, pid);
                          exit(1); 
                          }
      wait(&status);
    }
    In Unix SystemV la funzione signal ha effetto una volta sola. Va quindi rifatta per le successive (Riassume valore default).

    ESEMPIO 2

    Sostituire il codice di intr() dell'esempio 1 con:

    void intr()
    { printf("ricevuto \n");
      signal (SIGUSR1, intr);
      return;
    }
    Nota: lo sleep(5) dovrebbe essere sufficiente a lasciare il tempo di eseguire la seconda chiamata a signal, ma non e' BUONA programmazione. Eliminandolo ed eseguendo ripetutamente il programma si potrebbe avere una race condition.
    Altri Unix: unica signal con lista di segnali associati.

    La funzione kill consente l'invio di un segnale anche ad un gruppo di processi con queste convenzioni:

    Fallisce se non ci sono i privilegi richiesti.

    Gestione della Maschera dei segnali pendenti:
    – con la fork() viene messa a 0 (nessun segnale pendente)
    – rimane la stessa del thread che ha invocato la exec()
    – viene azzerata dalla pthread_create()

    Esistono varie funzioni per assegnare i valori della maschera

    4. Gruppi di processi                                        grp = setgrp()

    Piu' processi possono condividere lo stesso numero di gruppo. Un processo eredita il gruppo del padre al momento della fork(). La chiamata di sistema grp assegna ad un processo un numero di gruppo uguale al suo pid.

    5. Terminazione sincrona di processo                  exit(status)

    Un programma puo' terminare con diverse modalita': con una chiamata alla funzione exit()  (che esegue le funzioni registrate per l'uscita e chiude gli stream), o semplicamente con il ritorno dalla funzione main (equivalente alla chiamata di exit).  Questo in caso di terminazione "normale".

    In ogni caso il kernel esegue una serie di operazioni: chiude tutti i file aperti, rilascia la memoria che stava usando, e così via. In particolare sono chiusi i file descriptor, viene memorizzato lo stato di terminazione del processo, i processi figli sono ereditati da init (nella process structure di ogni figlio al pid del processo padre viene assegnato il valore del pid di init (normalmente =1), viene inviato il segnale SIGCHLD al processo padre. Altre operazioni sono eseguite se il processo e' leader di sessione.

    Inoltre se il processo termina senza che il padre ne abbia rilevato la terminazione attraverso la system call wait, il processo passa nello stato zombie.  pero' che il padre sappia come la terminazione è avvenuta:  il meccanismo scelto consiste nel riportare lo stato di terminazione (il cosiddetto termination status) al processo padre.  Questo valore e' caratterizzato tramite il cosiddetto exit status (o dal valore di ritorno per main). Ma se il processo viene concluso in maniera anomala il programma non può specificare nessun exit status, ed è il kernel che deve generare autonomamente il termination status per indicare le ragioni della conclusione anomala.

    Tale scelta comporta alcune complicazioni: se alla sua creazione è scontato che ogni nuovo processo ha un padre, non è detto che sia così alla sua conclusione, dato che il padre potrebbe essere già terminato (processo orfano). Ma in questo caso l'adozione da parte di init consente di risolvere il problema.

    E' anche possibile che un processo termini prima del padre, ma senza che il padre raccolga la terminazione con la funzione wait(). Il kernel deve comunque conservare una certa quantità di informazioni riguardo ai processi che sono terminati.
    Questo viene fatto mantenendo attiva la voce nella tabella dei processi, e memorizzando alcuni dati essenziali, come il pid, i tempi di CPU usati dal processo e lo stato di terminazione, mentre la memoria in uso ed i file aperti vengono rilasciati immediatamente. I processi che sono terminati, ma il cui stato di terminazione non è stato ancora ricevuto dal padre sono chiamati zombie, essi restano presenti nella tabella dei processi ed in genere possono essere identificati dall'output di ps per la presenza di una Z nella colonna che ne indica lo stato. Quando il padre effettuerà la lettura dello stato di uscita anche questa informazione, non più necessaria, verrà scartata e la terminazione sara' conclusa.

    6. Attesa di terminazione figlio                           pid = wait(&status)

    Il processo chiamante può avere figli in esecuzione:

    I figli la cui terminazione e' rilevata  sono cancellati dalla tavola dei processi.
    Viene ritornato il valore del pid del figlio terminato.

    main()
    {
    int pid, status;
    pid=fork();
    if (pid==0)
    {printf(“figlio”);
    exit(0);
    }
    else{ pid=wait(&status);
    printf(“terminato processo figlio n.%d”, pid);
    if ((char)status==0)
    printf(“term. volontaria con stato %d”, status>>8);
    else printf(“terminazione involontaria per segnale %d\n”, (char)status);
    }
    }
    Valori di ritorno dello status:
                             byte alto                byte basso
                             ----------               -----------
    figlio: exit  .........parametro exit  .........      0
    padre: wait   .........       0        ......... parametro exit

    Figura 5

    7. Esecuzione di programmi                          out = exec()

    E' possibile mandare in esecuzione altri programmi all'interno di un processo.
    "exec" e' una famiglia di funzioni. Esistono ad esempio:

        out = execve (nomefile, argv, envp)
        out = execl  (nomefile, arg1, arg2,...argn, 0)
        ecc.
    dove:
    nomefile e' il nome del file che contiene l'eseguibile 
    argv e' il puntatore ad un array di puntatori a caratteri
    envp e' l'environment definibile tramite shell 
         (lista di assegnazione di variabili d'ambiente)..
    execve e' la primitiva di base Il nuovo programma si sostituisce interamente, come dati e codice , a quello vecchio, che non e' piu' "raggiungibile". mantre restano inalterate le tavole file (file aperti, posizionamento all'interno di essi, WD, relazioni con altri processi ecc.).

    ESEMPIO 1
    Pipeline di programmi separati, ad esempio per esigenze di (scarsa) memoria:

    parte di codice del programma prog1
      ............
      execl("prog2", arg1, arg2,...,0);
      printf("errore chiamata prog2 \n");
      ..................
    }
    parte di codice di prog2
      .............
      execl("prog3, arg1,arg2,....,0);
      printf("errore chiamata prog3 \n");
    }
    eccetera.
    Le funzioni printf vengono eseguite solo in caso di errore della funzione exec, cioe' solo se l'operazione fallisce, per cui il processo continua con lo stesso programma.

    ESEMPIO 2

    Exec puo' essere associata ad una fork:

      ..............
      if (fork(0)== 0)  { /* figlio */
                          execl("filename", arg1,...,0);
                          pritf("errore \n");
                          exit(1); }
      /* padre */
      wait (&status);
      ............
     
    Il meccanismo di creazione di processi con fork e exec e' utilizzato dal sistema operativo stesso per generare i processi Unix iniziali.

    Il processo dopo l’exec

    Figura 6

    8. Esecuzione comando di shell             system (string)

    sistem() esegue il comando specificato nella stringa argomento chiamando "/bin/sh -c string", e ritorna dopo che il comando e' stato completato.

    ESEMPIO
    system("ls -l > file.dir");

    crea il file "file.dir" contenente la directory lunga della WD.

    Puo' essere utilizzata al posto di una operazione fork con exec nel figlio, se non e' necessario "condividere" tabelle file con il genitore.

    9. funzione di allarme               alarm(numero_secondi)

    Il kernel invia il signal SIGALRM dopo (circa) il numero di secondi indicato. All'arrivo del signal viene eseguita la procedura associata dall'utente all'allarme. Se non ne sono state assegnate il processo termina.

    ESEMPIO

      alarm (10);
      ............                passano solo 3 secondi
     
    alarm (20);
    La seconda richiesta annulla la prima.
    alarm(0) e' ignorato.
    10. sospensione del processo          s = pause()

    Il processo e' sospeso (sempre presente nella process table ma non piu' schedulato) finche' non arriva un qualunque signal che non sia "ignorato" (in questo caso ovviamente il processo termina).

    11. sospensione del processo  (tempo)        s = sleep(nsec)

    Sospende l'esecuzione dle processo per il numero di secondi specificato.

    ESEMPIO (script per shell)
         for i
         do
         sleep 240; echo $i
         sleep 240; echo $i
         sleep 240; echo $i
         sleep 240; echo $i
         sleep 240; echo $i
         done
    12. Per conoscere il proprio pid               pid = getpid()

    A questo punto possiamo riassumere i modi con cui 2 processi Unix possono comunicare informazioni fra di loro:

    Occorre considerare anche una forma implicita di scambio di informazioni: quando si effettua una fork, il processo figlio eredita informazioni sull'ambiente del genitore.

    System Call per thread
    Sono presenti nella specifica POSIX, ma possono essere implementate nel kernel o nello spazio utente, a scelta.


    Condivisioni fra thread dello stesso processo:


    Perche' usare i thread ?
    • processi paralleli che condividono lo spazio di indirizzamento dei dati
    • la creazione di un thread e' “100” volte piu' veloce della creazione di un processo
    • thread con calcolo intensivo thread con molto i/o
    • sono utili su multi processori

      esempio di Web Server multithreaded

    Linux ha un'implementazione non completamente POSIX-like e implementa i thread nel kernel.

    Differenza fra implementazione dei threads a livello utente e implementazione dei threads a livello kernel.

    1. Nel primo caso il S.O. non è a conoscenza dell'esistenza dei threads.  Questi  vengono gestiti completamente da un sistema run-time che esegue in modalità utente (ovvero non si richiede l'intervento del sistema operativo per creare, terminare, schedulare per l'esecuzione, sospendere o risvegliare threads); in questo tipo di organizzazione, lo scheduler della CPU gestisce i processi mentre il sistema run-time si occupa di assegnare un po' del tempo di CPU concesso ad un processo P a ciascuno dei suoi threads pronti ad eseguire.
    2. Nel secondo caso invece i threads sono direttamente gestiti dal S.O., pertanto esisteranno opportune system call per creare, terminare, sospendere e risvegliare un thread, inoltre sarà il S.O. che si occuperà della schedulazione dei threads (le entità gestite dallo scheduler della CPU saranno threads anziché processi).
    Qualche concetto sull'implementazione dei Thread in spazio utente

    • Realizzati da una librerie di procedure che girano in modo utente tipo:
             thread_create(), thread _exit(), thread_wait()...
    • Ogni processo ha una thread table gestita dal run time support della libreria
    • I thread devono rilasciare esplicitamente la CPU per permettere allo scheduler di esuguire un altro thread

    Funzione di sistema utilizzabile:     thread _yeld ()
    Se un thread si blocca (per un errore di programmazione, perche' e' in attesa di un evento), si blocca tutto il processo

    Implementazione dei thread nel kernel


    • Thread table unica (nel kernel)
    • Le primitive che lavorano sui thread sono system call:
        thread_create(), thread _exit(), thread_wait()...
    • Non è necessario che un thread rilasci esplicitamente la CPU
    • Le system call possono bloccarsi senza bloccare tutti i thread di quel processo

    Vantaggi:
    1. Visibilità dei dati globali: condivisione di oggetti semplificata.
    2. Più flussi di esecuzione.
    3. Gestione semplice di eventi asincroni (I/O per esempio)
    4. Comunicazioni veloci. Tutti i thread di un processo condividono lo stesso spazio di indirizzamento, quindi le comunicazioni tra thread sono più semplici delle comunicazioni tra processi.
    5. Context switch veloce. Nel passaggio da un thread ad un altro di uno stesso processo viene mantenuto buona parte dell’ambiente.

    Svantaggi:

    1. Concorrenza invece di parallelismo: gestire la mutua esclusione
    2. Routine di libreria devono essere rientranti (thread safe call): i thread di un programma usano il s.o. mediante system call che usano dati e tabelle di sistema dedicate al processo. Le syscall devono essere costruite in modo da poter essere utilizzate da più threadcontemporaneamente. Es: la funzione char *inet_ntoa() scrive il proprio risultato in una variabile di sistema (del processo) e restituisce al chiamante un puntatore a tale variabile. Se due thread di uno stesso processo eseguono "nello stesso istante"  la chiamata a due inet_ntoa() ognuno setta la variabile con un valore. Cosa leggono i due chiamanti dopo che le chiamate sono terminate?  
    Funzioni per la gestione dei thread kernel

    Thread management: funzioni per creare, eliminare, attendere la fine dei pthread
    Mutex: funzioni per supportare un tipo di sincronizzazione semplice chiamata “mutex” (abbreviazione di mutua esclusione). Comprende funzioni per creare e eliminare la struttura per mutua esclusione di una risorsa, acquisire e rilasciare tale risorsa.
    Condition variable: funzioni a supporto di una sincronizzazione  più complessa, dipendente dal valore di variabili, secondo i modi definite dal programmatore. Comprende funzioni per creare e eliminare la struttura per la sincronizzazione, per attendere e segnalare le modifiche delle variabili.

    14. Creazione di un thread          err = pthread_create(&tid, attr, function, arg);

    tid e' il thread identifier,
    function e' la funzione eseguita dal nuovo thread, che ha arg come primo parametro,
    attr e' un argomento che specifica gli attributi del thread. Si possono settare usando la funzione pthread_attr_init, oppure usando quelli definiti per default (attr=NULL in Linux). Nell'ultimo caso il thread e' "joinable" ed e' schedulato in modo non real-time e senza priorita' particolari.

    15. Terminazione di un thread          err = pthread_exit(&status)

    Un thread termina con una chiamata esplicita a pthread_exit, ooppure effettuando un normale return dalla procedura eseguita. Lo status contiene l'exit status, come per i processi. Non effettua nessun tipo di clean-up, ad esempio non sono chiusi i file aperti, che sono visibili anche da altri thread. viene invece liberato lo stack, che fra l'altro contiene anche le variabili locali, che sono perse.

    16. Attesa per terminazione thread       err = pthread_join (tid, &status)

    Sospende l'esecuzione del thread chiamante finche' il thread in questione e' terminato.
    status e' impostato dal thread tramite pthread_exit().

    Recupero delle risorse di un thread      err = pthread_detach(pthread_t thread);

    Un thread occupa delle risorse che non sono recuperate nel momento in cui il thread termina.   Per liberarle, e renderle nuovamente utilizzabili, o un altro thread effettua un pthread_join oppure il thread in questione esegue un thread_detach prima di terminare.   Un thread detached non e' piu' joinable.

    Esempi:   main.c  racecondition.c  p_hello.c    (file Ese1.pdf)

    Mutex Variables

    I thread si possono sincronizzare tramite un  mutex cioe' un semaforo con i soli valori 0 e 1.
    Tramite i mutex si possono accedere in mutua esclusione aree dati condivise. Normalmente si tratta di accessi veloci, tipo incremento di un puntatore.

    Mutex à l’abbreviazione di “mutua esclusione”. Una variabile mutex (più formalmente, di tipo pthread_mutex_t) è una variabile che serve per regolare l’accesso a dei dati che debbono essere protetti dall’accesso contemporaneo da parte di più thread. Ogni thread, prima di accedere a tali dati, deve effettuare una operazione di lock su una stessa variabile mutex. L’operazione detta “lock” di una variabile mutex blocca l’accesso da parte di altri thread.
    Infatti, se più thread eseguono l’operazione di lock su una stessa variabile mutex, solo uno dei thread termina la lock() e prosegue l’esecuzione, gli altri rimangono bloccati nella lock. In tal modo, il processo che continua l’esecuzione può accedere ai dati (protetti mediante la mutex).

    Finito l’accesso, il thread effettua un’operazione detta “unlock” che libera la variabile mutex. Un’altro thread che ha precedentemente eseguito eseguito la lock della mutex potrà allora terminare la lock ed accedere a sua volta ai dati.

    La tipica sequenza d’uso diuna mutex è quindi:
    • creare ed inizializzare la mutex
    • più thread cercano di accedere alla mutex chiamando la lock
    • un solo thread termina la lock e diviene proprietario della mutex, gli altri sono bloccati.
    • il thread proprietario accede ai dati, o esegue funzioni.
    • il thread proprietario libera la mutex eseguendo la unlock
    • un’altro thread termina la lock e diventa proprietario di mutex
    • .... e cosi via
    • al termine la mutex è distrutta.

    17. creazione di un mutex           pthread_mutex_init (pthread_mutex_t   *mutex,
                                                        const pthread_mutexattr_t *mutexattr);

    Il parametro degli attributi puo' essere NULL.
     

    18. distruzione di un mutex         pthread_mutex_destroy (pthread_mutex_t *mutex)
     

    19. lock di un mutex                      pthread_mutex_lock (pthread_mutex_t *mutex)
     

    20. unlock di un mutex                  pthread_mutex_unlock (pthread_mutex_t *mutex)

    21.probe per lock (non bloccante)    int pthread_mutex_trylock (pthread_mutex_t *mutex);
    è come la lock(), ma se si accorge che la mutex è già in possesso di altro thread ( e quindi si rimarrebbe bloccati) restituisce immediatamente il controllo al chiamante con risultato EBUSY In caso la chiamata vada a buon fine e si ottenga la proprietà della mutex, restituisce 0.
     
    Esempi:   main2.c  mutexex.c   p_greetings.c
    (file Ese2.pdf)

    Sono possibili errori che bloccano il programma.  d esempio dimenticarsi di fare l'unlock di un thread, oppure eseguire 2 volte il lock.  Altri che invece fanno fallire il sistema di sicurezza, ad esempio accedere alla variabile condivisa dimenticandosi di fare il lock prima.

    Per situazioni piu' complesse si possono utilizzare le condition variable, utilizzate per consentire lo sblocco di un thread in attesa da parte di un altro thread ed associate ad un mutex.
    Tipicamente un programma usa 3 oggetti:

    Attenzione: e' possibile avere deadlock se mal usati, come per i mutex!  

    21. creazione di una condition variable         pthread_cond_init (&condition, attr)

    cond e' l'id. della condition variable di tipo pthread_cond_t.
    Attenzione: mutex e condition variable sono implementati sullo stack del thread che li crea, quindi sono persi quando il thread termina.

    22. distruzione di una condition variable       pthread_cond_destroy (&cond)

    23. attesa su una condition variable               pthread_cond_wait (pthread_cond_t  *cond,
                                                                                                pthread_mutex_t *mutex)

    Il mutex che protegge la condizione deve essere bloccato prima di chiamare questa funzione. La funzione atomicamente sblocca il mutex e blocca il thread chiamante finche' la condizione non e' segnalata.
    Quando la funzione ritorna (perche' ha ricevuto il signal), il mutex e' nuovamente bloccato.
     

    24. signal di una condition variable               pthread_cond_signal (pthread_cond_t *cond)

    Si sblocca almeno un thread (dipende dall'implementazione) in attesa sulla variabile, in base alla priorita' scelta dallo scheduler, in genere quello da piu' tempo in attesa. Se non esistono thread in attesa il signal viene perso (non funzionano come i semafori).

    25. signal di una condition variable               pthread_cond_broadcast (pthread_cond_t *cond)

    Si sbloccano tutti i thread in attesa.

    26. Identificatore di thread                               th=pthread_self()

    Restituisce l'identificatore del thread chiamante.

    Esempi:    bounded.c    threadBlocchiDisco.c (singoloThread.c)   barrier.c (file Ese3.pdf)

    Chi conosce il Visual C++ puo' vedere il programma letters.cpp (attenzione, in questo caso la funzione Sleep blocca solo il thread, non l'intero processo).
     
     

    System Call per File System( Indice )

    1. Apertura file          fd = open(filename, flag, modo)

    Esempio di open
    Processo A:
             .....
    (3)      fd1 = open ("/etc/passwd", O_RDONLY);
    (4)      fd2 = open ("/etc/passwd", O_WRONLY);
    (5)      fd3 = open ("/etc/local", O_RDWR);
             .....
    Processo B:
             .....
    (3)      fd1 = open ("/etc/passwd", O_RDONLY);
    (4)      fd2 = open ("privato", O_WRONLY);

    Figura 7

    Attenzione: nelle entry della kernel table manca il valore di offset (posizionamento).

    2. Creazione file          fd = creat(filename, protezioni)

    ESEMPIO   fd = creat ("pippo", 0750)

    Il valore 750 e' il binario 111 (owner)   101 (group)   000 (world)
    Se pippo esiste gia', e' cancellato. Il file rimane aperto in scrittura (non occorre chiamare open).

    3. Lettura da file num = read(fd, buffer,count)

    L'accesso all'i-node del file e' sospeso fino alla fine dell'operazione.

    4. Scrittura del file              num = write(fd, buffer, count)

    Si puo' scrivere dopo la fine del file (diventa piu' lungo). L'accesso all'i-node e' sospeso.
    Esempi d'uso si trovano nel file sigWrite.
     

    Gestione del pathname in Unix (eseguito dal sistema).

    function name2inode(FileName): inode;
      /* Analizza un componente del pathname del file alla volta, dalla radice se
      l'indirizzo e' assoluto, dalla WD se e' relativo (WD nell'area U).
      Controlla che le directory esistano e che ci ci siano i privilegi richiesti.
      Restituisce l'inode che rimane bloccato (file non accessibile) o errore. */

    function open(FileName, flag, mode): file_descriptor;
    begin
      inodePtr:=name2inode(FileName);
      if error then return(Error);
      kf:=nuova entry in KernelFileTable;
         inizializza counter, offset, flag;
         kf.pi:=inodePtr;
      fd:=nuova entry in FileDescrTable;
         fd.pk:=kf;
      sblocca inodePtr;    /* file accessibile */
      return(fd)
    end;

    function read(fd, buffer, counter);
    begin
      controlla se file accessibile in lettura;
      inodePtr:=fd.pk.pi;
      blocca inodePtr;
      partendo dall'offset in kf legge (al massimo)
        counter byte dal file (fd) in buffer;
      sblocca inodePtr;
      memorizza nuovo offset in fd.pk;
      return (numero byte letti)
    end;

    function creat(FileName, mode): fd;
    begin
      inodePtr:=name2inode(FileName);
      if error then return(Error);
      if file esistente then 
         if scrittura non permessa then 
            rilascia inodoPtr;
            return(error)
         else libera blocchi disco  /* cancella file */
         /* i permessi e l'owner rimangono gli stessi,
            non e' necessario avere permesso di scrittura 
            nella directory padre */
      else   /* file non esistente */
        blocca directory del padre;
        if scrittura non permessa in directory padre then
          sblocca directory padre;
          return (error);
        assegna inodePtr libero e lo blocca;
        crea una nuova entry nella directory del padre;
        sblocca directory padre;
      kf:=nuova entry in KernelFileTable;
         inizializza counter, offset, flag;
         kf.pi:=inodePtr;
      fd:=nuova entry in FileDescrTable;
         fd.pk:=kf;
      sblocca inodePtr;    /* file accessibile */
      return(fd)
    end;
    5. Chiusura del file                s = close (fd)

    6. Creazione di file speciali             fd = mknod (pathname, tipo, dispositivo)

    Usato per creare pipe e directory.

    fd = mknod("/dev/tty2", 02 0744, Ox 04 02);
    Dove 02 indica il tipo carattere, 0744 e' la protezione, 04 e 02 sono rispettivamente il major e il minor number.

    protezione: rwx rwx rwx per owner, gruppo e world.

    7. Modifica posizionamento nel file           pos = lseek (fd, offset, riferimento)

    ESEMPIO
         #include <fcntl.h>
         main (argv, argc)   /* argv[1]=nome del file */
         int  argc;
         char *argv[];
         { int fd, pos;
           char c;
           if (argc != 2) exit(1);
           if (( fd=open(argv[1], O_RDONLY))==-1) exit(1);
           while ((pos = read (fd, &c, 1))==1)
             {
              printf ("char %c \n \n", c);
              pos = lseek (fd, 1023L, 1);
              printf ("nuovo valore pos %d \n", pos);
             }
         }
    8. Informazioni sullo stato di un file          s = stat (pathname, bufferstato)
                                                                 sf= stat (fd, bufferstato)

    Il secondo parametro e' una struttura in cui sono fornite le informazioni (vedere manuale per il tipo di struttura).

    9. Copie di descrittori di file newfd = dup (fd)

    Viene creata una nuova entry nella File Descriptor Table (newfd) contenente una replica del descrittore nello slot fd (incrementa anche i corrispondenti reference counter).
    newfd e' l'entry piu' bassa di quelle disponibili.

    Le prime 3 posizione nella File Descriptor Table sono inizializzate dall shell quando si esegue un programma, e contengono rispettivamente:

    Esempio
         #include <fcntl.h>
         #define  STD_O  1  /* standard output */
         main ()
         { int fd1, fd2;
           fd1 = dup(STD_O);   /* memorizza STD_O */
           close (STD_O);      /* libera entry STD_O */
           fd2 = open ("file_out", O_WRONLY);  /* occupa entry STD_O */
           if (fork()==0)
              {
               execve("progr2", .....);
               perror("no exec \m");
               exit(1);
              }
           close (fd2);     /* libera entry STD_O */
           fd2 = dup(fd1);  /* rimette a posto lo standard output */
           close (fd1);
           ........
         }
    10. Modifica tasti di controllo           s= ioctl (fd, request, argp)

    Solo per file speciali di tipo carattere.
    cooked mode - i tasti del, erase, ctr S, ctr Q, ctr \, ctr D, ctr Z ecc. hanno un significato particolare, e sono gestiti da Unix,

    Vedere il manuale.

    11. Comunicazione fra processi s = pipe (fdpipe)

    Figura 8

    Due processi possono comunicare solo se hanno un antenato in comune che ha aperto il pipe.

    Figura 9

    Dopo essere stato letto un campo e' cancellato (non rileggibile).
    Il pipe non e' permanenente. Gestione FIFO.

    ESEMPIO 1 (Produttore-Consumatore)

         ...
         main ()  
         { int dati[2], status;
           pipe(dati);
           if (fork() == 0) 
              { close (dati[0];
                execl ("prod", itoa(dati[1]), 0);
                exit (-1); }
           if (fork() == 0)
              { close (dati[1];
                execl ("cons", itoa(dati[0]), 0);
                exit (-2); }
           wait (&status);
           wait (&status);
         }
    Il file descriptor da usare e' passato come parametro per l'output e l'input.
    Il pipe ha una lunghezza finita.
    Se un buffer da scrivere e' piu' lungo, viene spezzato, quindi eventualmente uno stesso messaggio viene decomposto in pezzi non consecutivi.

    Casi particolari:

    Esempi si trovano nel file pipeEsempi.

    11. Connessione fra file (link fisico)                 s = link (source, destination)

    Come funzione e' possibile fare solo un hard link. I link software sono gestiti dalla shell. I 2 file possono anche essere in directory diverse. Solo il sistema (superuser) puo' linkare directory, per evitare possibili loop. Il file destination non deve gia' esistere. L'effetto e' quello di inserire il nuovo nome del file dentro la directory specificata, e di incrementare di uno il reference counter dell'i-node del file.

    12. Cancellazione file          s = unlink (nome file)

    In Unix non esiste una funzione di cancellazione di un file, in quanto si cancella un link, non il file. Cioe' si cancella l'identificatore di file indicato nei parametri, e si decrementa il reference counter del'i-node del file fisico. Quando diventa =0, il file e' effettivamente cancellato, cioe' diventa non piu' raggiungibile (esiste ancora un "fantasma" di file, in quanto i blocchi disco non sono cancellati, almeno finche' non sono soprascritti.

    ESEMPIO

    #include <fcntl.h>
    /* Il file "file.log" deve esistere, 
       "prova.in" e' creato come link fisico a "file.log" */
    main (argc, argv)
    int     argc;
    {  int fd1;
       char  buf[60];
       link("file.log", "prova.in");
       fd1 = open("prova.in", O_RDONLY);
       read(fd1, buf, sizeof(buf));
       printf("%s \n",buf);
    }
    Dopo l'esecuzione:
    krypton > ls -ln
    total 87
    ...............
    -rw-r--r--   2 113      15            11 set  8 13:23 file.log
    ...............
    -rw-r--r--   2 113      15            11 set  8 13:23 prova.in
    ...............
    Dopo il comando "rm prova.in"
    krypton > ls -ln
    total 86
    ...............
    -rw-r--r--   1 113      15            11 set  8 13:23 file.log
    ...............

    krypton >
    Una directory appena creata contiene gia' due file con nome "." e ".." Il primo e' il link a se stessa,il secondo alla directory genitore.

    13. Montaggio di un filesystem         s = mount (nome file spec, nome directory, flag).

    Un file system logico occupa un intero supporto fisico o una sua partizione.
    Mount connette il File system con una gerarchia esistente di file system. La W.D. da cui e' dato il comando ne' la directory da smontare, ne' un suo discendente.

    mount ("/dev/ps0", "/usr/users", 0)
    Dove "/dev/ps0" e' l'identificatore del file speciale a blocchi.

    Se flag e' dispari, e' consentita solo la lettura. L'eventuale contenuto della directory /usr/users e' parso. Il trattamento di link che attraversano file system diversi dipende dal tipo di Unix:
    Solo superuser. Il kernel ha una "tavola mount" con una entry per ogni file system caricato. Operazioni eseguite dal sistema Unix.

    /* Il kernel ha una TAVOLA DI MOUNT, con una entry per ogni file system
       montato. Una entry contiene:
       a. numero logico del device caricato;
       b. puntatore ad un buffer contenente il superblocco del file system;
       c. puntatore all'inode della radice del file system;
       d. puntatore all'inode della directory  su cui il file system
          e' montato
    */

    function mount(DeviceFileName, DirFileName, flag): void;
    begin
      if non superuser return(error);
      inodePtr1:=name2inode(DeviceFileName);
      controlla legalita'  /* device blocchi, accesso possibile /*
      inodePtr2:=name2inode(DirFileName);
      if (non directory) or (link ref. counter>1) then
         rilascia inodePtr1;
         rilascia inodePtr2;
         return(error);
      pm:=nuova entry in MountTable; 
      pm.dl:=device identifier;
      apre driver di DevFileName;
      alloca spazio buffer in memoria;
      legge superblocco(DevFileName) in buffer;
      pm.pb:=puntatore a buffer;
      inizializza campi superblocco;
      inodePtr3:=inode della radice DevFileName;
      pm.in:=inodePtr3;
      pm.d:=inodePtr2; 
      /* per consentire l'accesso alle directory antenate */
      sblocca inodePtr1;
      sblocca inodePtr2;
    end;

    Figura 10

    Durante la mount viene implicitamente chiamata la open della directory di mount e del file device. Sono di conseguenza aggiornati i reference counter degli i-node.
    Nota: la directory ".." non e' modificata. Quindi nella radice del FS montato c'e' il riferimento al genitore della directory "." di mount.

    ESEMPIO

       krypton > pwd
         /usr/users/
       krypton > mkdir common
       krypton > mount /dev/ps0  common
       krypton > cd common/new
       krypton > pwd
         /usr/users/common/new
    *  krypton > cd ../../..
       krypton > pwd
         /usr

    Figura 11

    La WD prima del comando etichettato * ha come i-node 5.
    La directory ".." e' presente nella directory new, ed ha i-node 4,  che non ha definito "..".
    Guardando la Tavola Mount, si trova l'entry con il device relativo e si arriva alla directory users, con i-node number = 1.
    Di qui si risale, attraverso ".." alla directory "usr" finale.

    14. Smontaggio di un file system                s = umount (nome file spec.)

    Si puo' chiamare solo da una WD "superiore".
    Si recupera (non garantito) l'eventuale contenuto della directory su cui era montato il FS.

    15. Memorizzazione della cache                 s = sync

    Scrive nei file le cache block che sono state modificate.
    Viene normalmente effettuato dal demone update ogni 30 secondi.

    16. Cambio di directory e di root         s = chdir (dirname)
                                                           s = chroot (dirname)

    Il primo processo creato ha la radice come WD. La WD si puo' cambiare, ma solo il superuser puo' cambiare la root.

    Gestione delle Protezioni

    17. Modifica protezione file              s = chmod (nome_file, modo)

    ESEMPIO

    chmod("/usr/users/mio", 0644).

    Si possono cambiare solo gli ultimi 9 bit di protezione dei file di cui si e' owner.

    18. Modifica per l'utente (gruppo) effettivo
                                     s = setuid(uid)            s = setpgid(gid)

    Per ogni processo ci sono 2 id_user:

    Il secondo puo' assegnare proprieta' ai file creati, controllare permessi per inviare segnali, controllare permessi di accesso ai file.
    Un processo cambia il suo setuid quando:
    Puo' essere eseguita solo dal superuser.

    ESEMPIO
    Per creare una directory si usa la funzione mknod che pero' puo' essere usata solo dal superuser. L'utente che vuole creare una directory puo' allora chiamare il programma mkdir (l'owner e' superuser, modo =04755) che internamente chiama la mknod. Idem per setgid.

    19. Informazioni
                   uid = getuid()     uid = geteuid()       gid = getgid()      git = getegid()

    per conoscere i valori attuali.

    20. Cambiamento owner             s = chown (nome_file, owner, gruppo)

    solo superuser.
    Allo stesso modo si puo' cambiare il gruppo di appartenenza con chgroup

    ESEMPIO
    chown("/usr/suo", owner_id, grp_id)

    21. Maschere per protezione             oldmask = umask(mode)

    Costruisce una maschera usata successivamente per filtrare i bit di protezione dei file creati. Mette a zero i bit corrispondenti a 1 nel parametro mode.

    ESEMPIO

         .....
         umask(022);
         ........
         creat("nome", 0777):
               /* crea un file con protezione 0755=0777 and not 022 */

    System call per gestione tempi( Indice )

    Si tratta di un insieme di funzioni che consentono di avere informazioni sui tempi di esecuzione. La struttura dati restituita e il loro formato dipende dall'implementazione Unix usata.