Google+ Quarta Info B: Sistemi
Visualizzazione post con etichetta Sistemi. Mostra tutti i post
Visualizzazione post con etichetta Sistemi. Mostra tutti i post

domenica 12 maggio 2013

FILESYSTEM INODE

Un inode, solitamente utilizzato nei filesystem Unix, non è altro che una struttura di dati.
I filesystem inode (index node) consentono di creare dei blocchi con funzioni particolari sul disco, i quali gruppi di questi blocchi formano gli inode. Con questi filesystem, ogni file avrà un inode associato, con questo metodo però si rischia che i file superino il numero di inode disponibili. Nella directory radice (/) sarà presente una lista dove troveremo i riferimenti agli inode, che a loro volta punteranno a un file specifico.



Ogni inode è una serie di bit, che devono essere suddivisi per essere spartiti ad ogni informazione associata al file:

Un inode stesso può essere una struttura di inode, cioè può contenere dei riferimenti ad altri inode di livello più basso. Per capire meglio osserviamo questo schema:

Come si può notare con questo metodo, si riesce ad ovviare al problema di avere dei file troppo grandi  e un numero troppo basso di inode, creando appunto vari riferimenti e collegamenti fra gli inode.
Proviamo a fare un esercizio per capirne ancora meglio il meccanismo:

Indirezione tra gli inode:
L'inode che contiene elementi di una directory, verrà chiamato di livello -1. Chiameremo di livello -2 un inode con informazioni di una cartella che a sua volta punta ad un inode di livello -1. Ed infine di livello 0 l'inode che verrà puntato dagli inode di livello -1, proprio come una gerarchia a più livelli.s

FILESYSTEM FAT

Il filesystem FAT è uno dei primi della storia, venne utilizzato dalle prime versioni di winzozz da zio Bill. Questo si appoggia su una tabella apposita denominata FAT che contiene le informazioni che riguardano i file presenti su disco  ed i loro settori.
La FAT è la tabella dove troviamo le informazioni su quali settori sono allocati e a chi sono assegnati. Quando un elemento della FAT (File Allocation Table) punta a se stesso significa che quello è l'ultimo elemento della tabelle; questo ricorda molto il principio che sta alla base delle liste concatenata nelle quali troviamo però come ultimo elemento il NULL(cioè vuoto). Quando invece troviamo un elemento non nullo nella FAT, significa che il settore successivo sarà quell'elemento stesso. Oltre alla FAT, in questo tipo di filesystem è necessario conoscere anche la directory radice (/), la quale contiene tutti i file e il percorso associato. Inoltre è necessario sapere se il file preso in considerazione è veramente un file oppure una directory.
Quando viene creato un filesystem FAT, la prima partizione viene dedicata alla omonima tabella, una seconda serve per contenere la directory radice (/) (solitamente di qualche KB) e la terza partizione verrà invece utilizzata come spazio libero disponibile per l'allocazione dei vari file.

martedì 7 maggio 2013

I FILESYSTEM

Un filesystem deve far poter gestire al sistema operativo i vari file, ordinarli, rinominarli, assegnarli i permessi ecc. I file vengono utilizzati per archiviare istruzioni e informazioni(dati). Questi vengono archiviati in modo permanente, per poi poter essere riletti in un secondo momento. Un file può essere qualsiasi cosa, nella visione di Unix ogni cosa viene vista come un file, anche le stesse directory. Un file viene visto in generale come un insieme di blocchi di una partizione e naturalmente devono risiedere sul disco.
Esistono anche alcuni tipi di file, detti file speciali o di dispositivo, che non sono memorizzati in blocchi su disco, ma servono solamente per virtualizzare l'accesso a una periferica.
Nei file è presente un "Magic Number" (3 is a magic number :D ), che viene assegnato, solamente quando quel file è eseguibile o se appartiene ad una categoria di file, ad esempio file immagine o video. Una volta mandato in esecuzione quel file, il sistema operativo controlla che il Magic Number sia coincidente, se lo è allora verrà mandato in esecuzione (sempre che naturalmente abbia il flag X attivo).

Le chiamate a sistema e i servizi che il sistema operativo mette a disposizione dell'utente sono:
-Apertura
-Chiusura
-Lettura
-Scrittura
-Spostamento ( fseek() )
Un sistema operativo ha dei limiti sul numero di file che può aprire contemporaneamente, altrimenti si rischia di perderne i dati. È necessario che ci sia un protocollo che metta in comunicazione il file con il processo (l'utente). Naturalmente quando un file viene aperto in scrittura un altro processo non potrà aprirlo in lettura, tanto meno in scrittura; anche se alcuni sistemi operativi, non sono così rigidi e consentono che avvenga.
Per identificare un determinato utente, gli si associa un UID (user identifier) e per un gruppo un GID (group identifier). Questi (UID & GID) sono solomente dei numeri interi che ricordano molto PID, utili appunto per distinguere tra loro due cose dello stesso tipo.

Informazioni di un File:

giovedì 11 aprile 2013

TABELLE DELLE PAGINE A PIÙ LIVELLI

Le tabelle delle pagine a più livelli consistono nell'utilizzare tabelle delle pagine di livello superiore, che contengono un riferimento ad altre tabelle di livello inferiore e così via. In poche parole, se abbiamo che nella tabella di 1° livello (non è un sayan) ci sono 10 elementi, ognuno di questi servirà come riferimento per puntare ad una tabella specifica di secondo livello, dove quest'ultima potrà contenere anch'essa riferimenti ad altre tabelle, oppure potra essere di "ultimo livello", cioè contenere riferimenti a pagine fisiche e terminare così la catena di tabelle.
Il vantaggio di utilizzare questo metodo per gestire le pagine, sta nel fatto che quando un processo viene mandato in esecuzione, si caricano in memoria solo le tabelle necessarie alle pagine di quel processo, lasciando quelle che non servono fuori dalla memoria. In questo modo, come si può notare, si ha un aumento di velocità nel raggiungere la pagina fisica ed anche un risparmio di spazio occupato, in memoria, dalle tabelle.



MEMORIA SEGMENTATA


Fino ad ora abbiamo visto una memoria virtuale con un singolo spazio di indirizzamento che parte dall’indirizzo 0 fino ad arrivare all’indirizzo massimo N.
Con la memoria segmentata, invece, viene divisa la memoria in segmenti.
Ogni segmento parte dell’indirizzo 0 a può arrivare fino ad una grandezza massima n anche se non è detto che ci arrivi: il segmento ha il grande vantaggio di essere espandibile e contraibile a seconda delle necessità.
Lo stack, ad esempio, può espandersi se si scrive una variabile al suo interno e si contrae se si preleva una variabile.
Ogni segmento è completamente indipendente dagli altri: in questo modo si può espandere e contrarre come vuole senza andare ad intaccare o “disturbare” altri segmenti di memoria (cosa che succede con una memoria non segmentata portando anche gravi problemi e sprechi di tempo dovuti alla continua rilocazione delle pagine).
Per accedere ad un’istruzione l’indirizzo di una memoria segmentata deve essere diviso in numero di segmento e offset.
Siccome i segmenti sono distini e separati tra di loro, essi sono molto comodi: immaginiamo di dover trovare un alunno in una scuola con 10.000 iscritti (in questo caso l’alunno è una procedura, una funzione così via...): la ricerca sarebbe molto più veloce se conoscessimo (prima del nome e del cognome dell’alunno) anche la classe dello studente.
Basterebbe collegare l’informazione della classe e dell’alunno e andare a pescare la persona giusta, cosa che sarebbe molto più laboriosa se non conoscessimo la classe perchè dovremmo andare a cercare il nome tra 10.000 studenti.
Un’altro vantaggio, molto importante su grossi sistemi o durante la progettazione di grossi programmi, è la possibilità di compilare singole procedure e non tutto il programma. Se in un nostro programma modifichiamo un procedura, non andrà ricompilato tutto il codice, perchè sono stati modificati gli indirizzi di memoria solo della procedura che appartiene ad un segmento, mentre tutti gli altri segmenti sono rimasti invariati.
Un altro vantaggio della memoria segmentata è sicuramente la facilitazione della protezione: siccome ogni segmento contiene una serie di oggetti dello stesso tipo (un segmento contiene, ad esempio, solo dati o stack, ma non entrambi), posso specificare anche che tipo di protezione tenere per quel segmento specifico specificando permessi di lettura/scrittura/esecuzione.
In un sistema segmentato, la protezione ha senso per il semplice motivo che il programmatore è conscio dell’esistenza dei segmenti e sa con precisone cosa ci sia all’intero di ogniuno, cosa che non succede con un sistema a singolo spazio di indirizzamento paginato.
Le librerie grafiche di un computer, che solitamente sono molto estese e pesanti, verranno messe in un segmeno con protezione bassa in modo da poterle condividere con altri programmi (in questo modo gli altri programmi eviteranno di compilare le proprie librerie grafiche perchè utilizzano quelle del computer).
La sostanziale differenza tra paginazione e segmentazione è che i segmenti non hanno una dimensione fissa.
Quando i segmenti vengono caricati o scaricati dal disco molte volte, si può creare una frammentazione esterna e per questo, ogni tanto, il sistema operativo procederà alla compattazione.

WORKING SET


Una cosa molto interessante da notare è che ogni proceso fa riferimento ad una piccola quantità di pagina e non a moltissime pagina tutte insieme.
Un compilatore a più passate, per esempio, fa riferimento a poche pagine di memoria per ogni passata.
L’insieme della pagine che vengono utilizzate correntemente da un processo si chiama working set (o zona di lavoro).
Il working set dovrebbe essere caricato tutto in memoria in modo da evitare dei fault di pagina continui che porterebbero al rallentaento della pagina (thrashing).
Cosa succede su un processo viene scaricato in memoria e poi deve riprendere l’esecuzione?
In teoria basterebbe semplicemente non far nulla. Il processo genererà moltissimi fault di pagina fino a che il suo working set non sia stato caricato in memoria per poi riprendere l’esecuzione.
Il problema sta nel fatto che il computer potrebbe metterci alcuni millisecondi per gestire un fault di pagina. E se abbiamo migliaia di fault?
La soluzione è implementata in molti sistemi operativi moderni: viene tenuta traccia dei working set di ogni processo e non si permette al processo di ricominciare a lavorare prima che il suo working set non sia stato caricato in memoria in modo da evitare fault di pagina (questa procedura si chiama prepaging).
Il working set è sostanzialmente l’insieme di pagine usate nei k riferimenti di memoria più recenti.

ALGORITMI DI RIMPIAZZAMENTO DELLE PAGINE


Quando si genera un fault di pagina, il Sistema Operativo deve scegliere quale pagina scaricare dalla memoria.
È evidente che la scelta non può essere casuale perchè si rischierebbero rallentamente pesanti nella macchina.
I progettisti di computer hanno individuato un algortimo ottimale (che non può essere realizzato) per poi tentare di riprodurlo.
L’algoritmo ottimale dice che ogni pagina deve essere contrassegnata con il numero di istruzioni macchina che vengono eseguite prima che a quella pagina si faccia riferimento.
La pagina con il numero più alto viene scaricata.
Ovviamente questo algoritmo ha un implementazione impossibile perchè è impossibile determiare il numero di istruzioni che occorrono prima del riferimento alla pagina.

I progettisti hanno allora cercato degli algortimi che potessero aiutare veramente durante la progettazione di un computer:

Il primo algoritmo chiamato NRU ha bisogno, per essere utilizzato, di due bit: un bit R che viene messo a 1 quando si fa un riferimento alla pagina (lettura o scrittura) e un bit M che viene messo a 1 quando si fa una scrittura alla pagina.
Periodicamente, il bit R viene rimesso a 0 dal sistema operativo mentre l’M viene mantenuto ad 1 se la pagina è stata modificata.
Il sistema operativo, in questo modo, è in grado di classificare ogni pagina in 4 categorie:
Classe 0: non usata e non modificata (0,0)
Classe 1:non usata, modificata (0,1)
Classe 2: usata, non modificata (1,0)
Classe 3:usata,modificata(1,1)
Quando si genera un fault di pagina, il sistema operativo scarica dalla RAM una pagina casuale presa dalla classe più bassa non vuota.

Un altro algoritmo di rimpiazzamento è detto algoritmo FIFO.
É un algoritmo di facile implementazione, infatti, quando si genera un fault di pagina, viene scaricata dalla RAM la pagina che è presente da più tempo in memoria.
Il problema di questo algoritmo è evidente: anche se una pagina è vecchia, cioè da molto tempo in memoria, non vuol dire che sia inutile.

Una combinazione tra NRU e FIFO è l’algoritmo della seconda opportunità: quando si va in contro ad un fault di pagina, si va a cercare l’elemento più vecchio, cioè quello in coda alla lista.
Una volta che abbiamo trovato l’elemento più vecchio, si controlla il suo bit R. Se è a 0, vuol dire che la pagina è vecchia e non usata e allora la pagina viene scaricata su disco.
Se il bit R è a 1, viene messo a 0 e la pagina viene spostata in testa alla lista come se fosse stata appena caricata.
A questo punto si cotinua a cercare una pagina in fondo alla lista con bit R a 0.

L’algorimo dell’orologio consiste in questo: si mantiene una lista circolare di pagina con la lancetta che punta su una pagina. Se ad un fault di pagina la lancetta punta su una pagina con bit R a 0, la pagina viene scaricata, viene portata la lancetta avanti di uno e la pagina che deve entrare in memoria viene sostituita alla pagina appena scaricata.
Se il bit R e a 1, viene messo a 0 e la lancetta spostata in avanti fino a quando non si trova una pagina con bit 0.

Una buona approssimazione dell’algoritmo ottimale e quella offertaci dall’algoritmo LRU.
Questo algoritmo consiste nello scaricare dalla memoria la pagina non usata da più lungo tempo.
Un modo per implementare la LRU e quello di avere un contatore a 64 bit e che ogni pagina contenga abbastanza spazio per memorizzare il contenuto del contatore.
Dopo ogni riferimento in memoria, il valore del contatore viene memorizzato nella pagina a cui si è fatto riferimento.
Quando si va in contro ad un fault di pagina, viene scaricata dalla memoria la pagina con il valore del contatore più basso (la pagina corrispondente al contatore più basso e la pagina usata meno di recente).
Un altro metodo per implementare la LRU e quello di mantenere una tabella n*n (n numero delle pagine fisiche). Quando si fa un riferimento ad un pagina k , viene messa a 1 tutta la riga k e a 0 la colonna k.
Quando si genera un fault di pagina si controlla il valore binario di ogni riga: la riga con il valore binario più basso è quella usata meno di recente.
Queste implementazioni della LRU vengono eseguite via hardware. Il problema è che nessuna macchina possiede tale hardware.

Per questo si è cercato di simulare la LRU via software e si è riusciti con l’NFU.
Ad ogni pagina è associato un contatore e ad ogni interruzione di clock viene sommato il contatore al bit R della pagina.
Quando si verifica un fault di pagina, viene scelta la pagina con il contatore più basso.
Dopo varie simulazioni si è notato che anche questo algoritmo non è il massimo dell’efficienza, perchè non dimentica mai nulla e potrebbe essere che vengano scaricate su disco delle pagine che servano nell’immediato futuro.
Una modifica dell’NFU, chiamata algoritmo dell’invecchiamento, permette una resa ottimale e una buona approssimazione all’algoritmo ideale.
Come implementare tutto ciò?
Ogi pagina ha una serie di bit (8) che all’inizio vengono messi tutti a 0. Dopo ogni ciclo di clock, gli 8 bit vengono shiftati a destra di una posizione e viene messa nella posizione ora vuota il bit R.
Quando si genera un fault di pagina viene scaricata la pagina con la serie di bit più bassa.

TLB (Memoria Associativa)


Osservando l’utilizzo della memoria di un calcolatore, potremmo notare che la maggior parte dei programmi accede a poche pagine di memoria molto di frequente e non a molte pagine poco frequentemente.
Per sfruttare questa condizione vantaggiosa, i progettisti hanno creato una sorta di MMU ridotta ad accesso parallelo chiamata TLB.
All’interno della TLB vengono salvati alcuni dati relativi ad ogni pagina virtuale usata spesso.
Quando arriva alla MMU un indirizzo virtuale, prima di fare un controllo sulla tabella della pagine, viene controllato ogni elemento della TLB simultaneamente e se nella TLB è presente questo elemento, viene data subito la pagina fisica, senza bisogno di accedere alla lenta tabella delle pagine.
Se invece la pagina virtuale non è presente nella TLB, si genera una page miss, viene cercata nella tabella delle pagine la pagina virtuale, viene scaricata una pagina dalla TLB e rimpiazzata con la pagina appena trovata sulla tabella delle pagine.

MEMORIA VIRTUALE E MMU


Il ragionamento dietro alla memoria virtuale consiste nel fatto che un programma può essere più grande della memoria fisica.
Il programma viene diviso in modulo (overlay) e solo quelli necessari all’esecuzione vengono caricati in RAM mentre le restanti parti restano sul disco.
Un programma da 4 GB può girare tranquillamente su una memoria da 2GB scegliendo accuratamente quali 2GB mandare in esecuzione.
La memoria virtuale ora introduce anche il concetto di paginazione: la memoria viene divisa in partizioni dette pagine e ogni pagina è grande solitamentre 4 KByte.
In un sistema a 32 bit, ci sono 12 bit che servono per “muoversi” all’interno della pagina (2^12 = 4KB) e il restante 20 bit servono ad indicare la pagina a cui facciamo riferimento.
Da questo deduciamo che appunto con 12 bit di offset le pagine posso essere grandi massimo 4KB e che ci possono essere al massimo 2^20 pagine (1 milione di pagine).
(1 milione di pagine per 4KB l’una sono 4 giga).
Ora però l’indirizzo della pagina virtuale non corrisponde a quello della pagina reale.
Come risolvo questo problema? Sulla scheda madre c’è un componente chiamato MMU che fa da traduttore: prende l’indirizzo della pagina virtuale e lo trasfororma in indirizzo reale.
La MMU contiene appunto una tabella delle pagine che è compilata dal gestore della memoria. Il gestore della memoria, infatti, tiene conto delle pagine libere e è in grado di dire dove una pagina virtuale deve puntare sulla memoria reale.
Su un sistema a 32 bit, 12 bit vanno direttamente in memoria e 20 bit, invece, si fermano sulla MMU. Questi 20 bit compongono l’indirizzo virtuale della pagina richiesta, ma prima che la scheda di memoria onori la richiesta, questo indirizzo virtuale deve essere trasformato in indirizzo reale tramite la MMU.
Ovviamente in memoria virtuale abbiamo, ad esempio, 100 pagine virtuali, mentre in memoria reale sono 10 pagine.
Capiamo quindi che sono 10 pagine virtuali avranno un corrispettivo in memoria reale. Ogni pagina virtuale ha quindi un bit che che messo a 0 vuol dire che la pagina non è mappata, se è a 1 vuol dire che è mappata.
Cosa succede ora se per caso si deve accedere ad una pagina virtuale che non ha un corrispettivo nella memoria fisica?
In una situazione di questo genere si crea un fault di pagina: viene prelevata una pagina (algortimi di rimpiazzamento della pagine dopo) dalla memoria fisica, se è stata modificata viene riscritta su disco, altrimenti no.
Sulla sua pagina virtuale, viene messo un bit a 0 che sta a significare che quella pagina virtuale non ha più un corrispettivo nella memoria reale.
Ora viene presa la pagina virtuale a cui si deve accedere e viene scritta sopra la precedente pagina; si modifica la MMU per far puntare quella pagina virtuale a quella pagina fisica e viene messo un bit a 1 per dire che quella pagina virtuale ha un corrispettivo nella memoria reale.
Con un sistema a 32 bit, ci sono 2^20 pagine virtuali che corrispondono a oltre un milione di pagine virtuali.
L’indirizzo della pagina virtuale viene usato come indice della tabella delle pagine e il contenuto della corrispondente cella è l’indirizzo di memoria fisica.
In un sistema a 64 bit, il numero di pagine virtuali è 2^52 che sono moltissime.
Con così tante pagine virtuali, la velocità e la stabilità dal sistema sono seriamente compromesse e per questo si utilizzano metodi diversi per l’accesso alle istruzioni.
Un metodo è la tabella delle pagine inverse.
Nella tabella delle pagine normale, ci sono tante celle quante sono le pagine virtuali.
Nella tabella della pagine invertite, ci sono tante celle quante sono le pagine fisiche: la CPU emette un indirizzo a 32 bit, 12 di offset e 20 di pagina virtuale.
Al posto di andare all’indice dettato da 20 bit, si cerca nel contenuto delle celle il corrispettivo numero dei 20 bit e quando lo si trova si prende l’indice e quella è la pagina fisica.
Se non si trova l’elemento abbiamo un fault di pagina.
In sostanza le pagine fisiche fanno da indici per la tabella e quelle reali da contenuto.


POLITICHE DI GESTIONE DELLA MEMORIA


Quando la memoria viene gestita e allocata dinamicamente, è sempre bene tenere conto della memoria utilizzata e di quella non utilizzata.
Il primo modo di tracciare l’utilizzo della memoria è quello con le bitmap.
Viene creata una grande matrice formata da 0 e 1. La memoria viene divisa in piccole partizione e a ogni partizione viene associata una casella della bitmap. Se c’è 1 nella bitmap, la partizione è occupata, altrimenti è libera.
Il secondo metodo per tenere traccia dell’utilizzo della memoria è il metodo a liste: il sistema operativo tiene una lista concatenata di spazi liberi e occupati in memoria con le informazioni riguardanti la dimensione.
In ogni nodo della lista sono presenti tre informazioni: se la regione di memoria è libera, l’indirizzo di inizio e di fine di quella regione.
Quando spazi e buchi vengono ordinati nella lista in ordine di indirizzo, possiamo poi usare diversi algoritmi per allocarle la memoria ad un processo.
Il primo metodo è detto di first feet, cioè il gestore della memoria trova il primo buco sufficientemente grande per contenere il processo, divide il buco in 2 parti: uno spazio per il processo e uno spazio che diventa un altro buco più piccolo. Altri algoritmi sono il next feet, il best feet, worst feet e il quick feet.

SWAPPING


Lo swapping consiste nel caricare e scaricare in RAM un processo a seconda delle esigenze. Se un processo deve essere eseguito sul processore, Il processo viene mosso dal disco alla memoria, eseguito e poi riportato su disco per permettere ad altri processi di occupare quell’area di memoria.
Assistiamo quindi ad una gestione dinamica della memoria: le partizioni non hanno mai stesse dimensioni e la stessa posizione.
La gestione dinamica della memoria utilizzata nello swapping consente una maggiore flessibilità, ma implica anche una maggiore difficoltà a causa della continua rilocazione delle partizioni.
Una cosa molto importante è anche la quantità di memoria che va allocata per ogni processo: se ogni processo avesse una dimensione fissa, basterebbe allocare la quantità di memoria che serve, ne più ne meno.
Il problema sta nel fatto che spesso i processi si modificano in dimensioni e quindi è sempre meglio allocare una dimensione di memoria superiore per evitare problemi di segmentation fault.

RILOCAZIONE E PROTEZIONE


La multiprogrammazione porta con se anche due grandi problemi: la rilocazione e la protezione.
Quando compiliamo un programma viene generato un file binario contenente delle istruzioni. Supponiamo che la prima istruzione sia una chiamata ad una funzione in posizione 100 e che la partizione di memoria del processo inizi in posizione 350. Il processo dovrebbe eseguire l’istruzione in posizione 350 + 100 e non in posizione 100. Quindi il problema consiste nel trasformare l’indirizzo di memoria assoluto, in quello relativo alla proprio partizione di memoria. Il problema viene comunemente risolto semplicemente aggiungengo 350 durante la chiamata a quella istruzione. Questo però non risolve il problema della protezione, infatti si potrebbero creare delle istruzioni che utilizzino direttamente indirizzi assoluti e non relativi.
IBM risolse questo problema con 4 bit che definivano cosa si potesse fare in una determinata sezione di memoria. Il file che conteneva questi 4 bit si chiamava PSW.
Un’altro modo per risolvere entrambi i problemi era i registri di base e limite. In quello di base, era contenuto l’indirizzo dove iniziava la partizione e in quello di limite dove finiva la partizione: se durante la trasformazione da indirizzo assoluto a indirizzo relativo veniva sforato il limite sia positivamente che negativamente, il processo veniva killato.
Inoltre, ad ogni chiamata ad un indirizzo, veniva sommato all’indirizzo il registro base per trasformare da indirizzo assoluto a indirizzo relativo.

ORGANIZZAZIONE DELLA MEMORIA


L’organizzazione della memoria più avvenire così in due modi: con diverse code o in singola coda.
Nel primo caso tutti i processi “piccoli” si accodano dove c’è la partizione più piccola che possa contenere il singolo processo e lo stesso vale per i processi “grandi”. Il problema di questo modo di usare la memoria è che potrebbe accadere che la coda dei processi piccoli sia piena, mentre la coda dei processi grandi sia vuota: in questo caso i processi piccoli in fondo alla coda aspettano di essere caricati in memoria, mentre in realtà la memoria è quasi totalmente vuota perchè le partizioni grandi che dovrebbero contenere processi grandi sono vuote.
La coda singola è diversa, cioè viene creata una singola coda contenete processi grandi e piccoli.
Quando viene liberata una partizone di memoria, viene cercato nella coda il processo che sprechi meno memoria mettendosi in quella partizione.
Il problema di questo tipo di gestione della memoria è che i processi piccoli vengono discriminati mentre in realtà dovrebbero essere privilegiati.

GESTIONE DELLA MEMORIA (Introduzione)


Come sappiamo, all’interno di un computer esistono diversi tipi di memorie diverse per velocità, capacità e prezzo.
In un computer ideale, tutte le memorie sarebbero enormi, molto economiche e velocissime, ma dato che ciò non è possibile, i progettisti hanno deciso di creare diversi tipi di memoria: la Cache, molto veloce, molto piccola e molto costosa, la RAM, abbastanza capiente, abbastanza veloce e mediamente costosa e la memoria di massa, lenta, molto capiente e economica.
Il gestore della memoria ha il compito di gestire e organizzare le memorie nel modo più efficiente possibile, tenendo conto delle pagine di memoria usate e quelle non usate, compilando la tabella delle pagine nella MMU e svolgendo molti altri compiti che vedremo più avanti.

Nel caso di un sistema monoprogrammato (cioè che può eseguire un processo alla volta), la gestione della memoria è molto semplice, infatti, è possibile caricare in RAM un processo alla volta e fino a quando non viene portato a termine, esso non verrà scaricato dalla RAM.
In un sistema monoprogrammato ci sono tre diversi modi di gestire la memoria: il SO nella parte bassa della RAM, il SO in ROM, oppure il SO nella parte bassa della RAM e il gestore delle periferiche in ROM.

Ai giorni nostri, però, la monoporgrammazione non è più contemplata e si è passati alla multiprogrammazione, dove più processi risiedono in RAM in attesa di essere caricati ed eseguiti sulla CPU.
Il modo più semplice per realizzare la multiprogrammazione è dividere la memoria in blocchi fissi, magari di dimensioni diverse.
Quando un processo viene caricato in RAM, viene cercata la partizione più piccola disponibile che possa contenere il processo.

giovedì 21 febbraio 2013

Multiprogrammazione

Per varie applicazioni è utile avere in singolo processo svariati thread in esecuzione.  Questi condivideranno il segmento di codice e il segmento dati ma avranno un segmento di stack personale.
I threads verranno schedulati automaticamente per essere mandati in esecuzione all'interno del processo stesso. Questi thread possono avere delle zone di memoria condivise ( possono essere delle variabili globali contenute nel data segment oppure dei file comuni). Se i thread dovessero solo leggere il dato / file non ci sarebbe nessun problema, ma se dovessero anche modificarlo?  Basti pensare al caso di un conto corrente:

Thread 1Thread 2Bilancio
Legge Bilancio: $1000$1000
Legge Bilancio: $1000$1000
Deposito $200$1000
Deposito $200$1000
Aggiorna Bilancio $1000+$200$1200
Aggiorna Bilancio $1000+$200$1200


Il primo thread legge il bilancio attuale (1000€) così come il secondo che esegue un deposito di 200€.
Viene eseguito un deposito da parte del primo thread, che va ad aggiornare il bilancio.
Il secondo thread non avrà il bilancio aggiornato (1200€) ma avrà il bilancio letto in precedenza (1000€), quindi quando si andrà ad aggiornare il valore si avrà solo l'ultima modifica fatta dal secondo thread e non quella fatta dal primo.
Questo esempio è uno dei tanti e fa capire quanto sia importante la gestione di thread o processi quando devono andare a scrivere su dei dati condivisi.

Per risolvere questo problema utilizziamo  le variabili mutex o le variabili semaforo.

Nella gestione di più processi o thread che eseguono delle corse critiche abbiamo 3 problemi ricorrenti, che sono i seguenti:
- Le cinque filosofe a tavola
- Il barbiere che dorme
- Lettore e scrittore
- Produttore e consumatore

Il primo problema è il seguente: 5 filosofe sono ad un tavolo rotondo per mangiare degli spaghetti. Ogni filosofa dispone di una sola forchetta ma sfortunatamente gli spaghetti sono così scivolosi che per mangiarli bisogna utilizzare per forza 2 forchette insieme.
Le nostre filosofe (cioè i nostri processi) possono essere in 3 stati distinti: pensante , affamata  e mangiante .
Come possiamo far mangiare il maggior numero di filosofe contemporaneamente senza creare nessuna situazione di stallo?

Utilizzeremo un vettore di interi state di un numero uguale a quello delle filosofe; in ognuno di queste posizioni andremo ad inserire lo stato della nostra filosofa (pensante, affamata e mangiante).
In totale avremo 6 mutex: 5 per ogni filosofa mentre ne avremo una globale.
Ogni filosofa può iniziare a mangiare solo se nessuna delle 2 filosofe ai suoi fianchi non sta mangiando.


Il codice per risolvere questo problema è il seguente:


#define N 5
#define THINKING 0
#define HUNGRY 1
#define EATING 2

typedef int semaphore;
int state[N];
semaphore Mutex=1; /*Master Mutex*/
semaphore S[N]; /*Philosopher's Mutex, Inizializzato come? tutto a zero, ovvero bloccato*/
/*down circa= lock. up circa= unlock. */

void philosopher(int i) //Codice eseguito dalle 5 filosofe
{
while (1)
{
think(); // La filosofa sta pensando
take_forks(i); // la filosofa è affamata, quindi cercherà di prendere le forchette
eat(); // entra nella sezione critica, inizia a mangiare
put_forks(i); // posa le forchette e ricomincia a pensare
}
}

void take_forks(int i)  //Funzione prendi forchette
{
down(&Mutex);  //Cerca di entrare nella zona critica
state[i]=HUNGRY; //Si modifica il suo stato rendendolo da pensante ad affamata
test(i); // controlla che le forchette siano disponibili, quindi che le due filosofe da parte non stiano mangiando
up(&Mutex); //Esce dalla regione critica
down(&S[i]); // Se la funzione test è andata a buon fine, quindi la filosofa ha ottenuto le forchette essa inizierà a mangiare, altrimenti si bloccherà aspettando che una delle filosofe ai suoi lati la sblocchi, così che possa iniziare a mangiare
}

void put_forks(int i) //Funzione posa forchette
{
down (&Mutex);  //Cerca di entrare nella zona critica
state[i]=THINKING; //Ha finito di mangiare, quindi il suo stato viene modificato in pensante
test( Left(i) ) ; // Controlla che la filosofa alla sua sinistra possa mangiare
test( Right(i) ); // Controlla che la filosofa alla sua destra possa mangiare
up(&Mutex); //Esce dalla regione critica
}

void test(i)  //Funzione che controlla che le filosofe possano iniziare a mangiare
{
if(state[i]== HUNGRY && state[ Left(i) ]!=EATING && state[ Right(i) ]!=EATING )
        //Se la filosofa è affamata e nessuna delle filosofe vicine sta mangiando, allora la filosofa può mangiare
{
state[i]=EATING; // Si mette nello stato mangiante
up(&S[i]); //fa una unlock sulla sua mutex, così che quando va a fare la down nella funzione take_forks(i) essa non verrà bloccata e quindi potrà mangiare
}
}

int Right(int i) /*Nome filosofa di destra*/
{
return (i-1) % N;
}

int Left(int i) /*Di sinistra */
{
return (i+1) % N;
}


Per riassumere.
Una filosofa quando smette di pensare sarà affamata, per poter mangiare dovrà avere le due filosofe ai fianchi che non stiano mangiando, altrimenti aspetterà fino a quando non saranno disponibili le forchette.
Il problema delle cinque filosofe è molto utile per risolvere problemi dove dei processi devono competere per l'accesso a un numero limitato di risorse, come i dispositivi di input/output.


Il secondo problema invece riguarda quello di un barbiere che ha una sola postazione per il taglio dei capelli e un numero di sedie per far attendere i clienti. Se non c'è nessun cliente nel negozio il barbiere inizierà a dormire. Quando arriverà un cliente si sveglierà e inizierà a tagliargli i capelli; se intanto arrivano altri clienti mentre il barbiere è occupato essi attenderanno sulle sedie, se un cliente vuole fare il taglio dei capelli ma ci sono tutte le sedie per attendere occupato questo se ne va.


Il codice è il seguente:

#define SEDIE 5 //Il numero di sedie utilizzate per far aspettare i clienti

typedef int semaforo; //La nostra variabile semaforo

semaforo clienti = 0;  //Numero dei clienti in attesa
semaforo barbieri = 0;  //Numero dei barbieri in attesa dei clienti
semaforo mutex = 1; //Mutex che utilizziamo per la mutua esclusione
int in_attesa = 0;  //Numero dei clienti in attesa (non durante il taglio dei capelli)

void barbiere(void)
{
     while(1)
     {
             down(&clienti); //Il barbiere inizierà a dormire se non c'è nessun cliente
             down(&mutex); //Entra nella sezione critica perché va a modificare una variabile globale ...
             in_attesa = in_attesa -1;  //... che è in_attesa
             up(&barbieri); //Un barbiere è pronto per tagliare i capelli al cliente
             up(&mutex); //Esco dalla zona critica
             taglia_capelli(); //Taglio i capelli al cliente
     }
}

void cliente(void)
{
     down(&mutex); //Entra nella zona critica
     if(in_attesa < SEDIE) //Se i clienti in attesa sono meno delle sedie per attendere...
     {
                  in_attesa = in_attesa +1;  //...anche questo cliente si metterà in attesa
                  up(&clienti); //Incrementa in numero di clienti in attesa
                  up(&mutex); //Esce dalla zona critica
                  down(&barbieri); //Blocca l'esecuzione del cliente se non c'è nessun barbiere disponibile per il taglio dei capelli
                  vai_al_taglio(); //Taglio dei capelli
     }
     else //Se tutte le sedie sono occupate
     {
         up(&mutex); // Allora esco direttamente dalla zona critica, senza eseguire il codice che mi serve
     }
}


Per fare un esempio, questo problema è ricorrente nei server web.
Ogni volta che un utente vorrà andare su una pagina web il server gli invierà il file .html della pagina corrispondente tramite un codice simile a quello del barbiere. Il server infatti avrà naturalmente più di un "barbiere" che potrà servire i vari utenti (perché se potesse inviare la pagina ad un solo utente alla volta sarebbe improponibile, basti pensare a pagine molto visitate) che richiedono la pagina internet; per gestire i più barbieri semplicemente li creerà come thread da un thread master. Il thread master sarà quello in ascolto sulla porta 80, cioè quella che riguarda le pagine web; ogni volta che un cliente arriva darà la richiesta del cliente al thread barbiere, rimettendosi immediatamente in ascolto per nuove richieste.













venerdì 15 febbraio 2013

Mutex e semafori.

Come già parlato nei post dei thread, per gestire le corse critiche utilizziamo le mutex.
Le mutex sono delle speciali variabili a livello kernel che ci permettono di gestire l'ingresso in una zona critica (Cioè una zona di istruzioni che interessa la modifica di variabili globali o di un file condiviso che deve essere eseguita da un solo thread o processo alla volta, dall'inizio alla fine) da parte di 2 o più thread / processi.
Le mutex, essendo variabili in kernel space possono essere modificate solo tramite delle chiamate di sistema particolari.

La sintassi delle mutex è la seguente:

-La creazione ed inizializzazione di una variabile mutex.

pthread_mutex_t    mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t è la variabile con qui si dichiarano le mutex, PTHREAD_MUTEX_INITIALIZER invece serve per inizializzare la nostra variabile mutex

-Le due chiamate di sistema

pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);

pthread_mutex_lock e pthread_mutex_unlock servono rispettivamente, come fanno intuire i loro nomi, a bloccare e sbloccare la variabile mutex.

Il funzionamento di queste variabili è semplice: quando un thread entra nella zona critica blocca la variabile mutex, perciò quando un altro thread cercherà di entrare nella zona critica (eseguendo anch'esso una pthread_mutex_unlock(&mutex) ) il thread si bloccherà fino a quando l'altro thread non uscirà dalla zona critica, sbloccando la mutex.
Nel caso ci fossero più thread bloccati sulla stessa mutex sarò il S.O. a scegliere quale thread mandare in esecuzione.
Da notare -> il S.O. quando viene eseguita una pthread_mutex_unlock va a controllare se ci sono dei thread bloccati su quella variabile: se ci sono manderà in esecuzione uno dei thread bloccati (senza modificare il valore della mutex), altrimenti sbloccherà definitivamente la mutex.

Ce un altro tipo di variabile che può fare lo stesso lavoro delle mutex: sono le variabili semaphore, cioè semaforo.
I semafori per bloccare e sbloccare hanno anch'essi 2 chiamate di sistema, che noi chiameremo molto semplicemente up e down.

Queste tipo di variabile è come se fosse un intero: parte da 1 (che rappresenta lo stato iniziale del semaforo, cioè che si può accedere a quella determinata zona critica) e ogni volta che un thread cerca di entrare nella zona critica il semaforo si decrementa di uno. Quando un thread esce dalla zona critica dovrà eseguire lo sblocco, cioè una up.

Praticamente non ci sono differenze tra l'utilizzo di una variabile mutex o una semaforo per la gestione tra thread.







lunedì 4 febbraio 2013

Socket, IP e altro

Per prima cosa, come avviene il collegamento tra generalmente due computer? Per collegare tra di loro 2 computer abbiamo bisogno di una socket, cioè un canale virtuale che permette a due processi su due macchine distinte di scambiarsi dei dati.
Per gestire correttamente questo scambio di dati ho bisogno di un protocollo (cioè un insieme di regole) per far sì che la comunicazione tra i due processi avvenga correttamente. 
Uno dei protocolli fondamentali è l' IPv4 (Internet Protocoll version 4) , cioè un protocollo che associa un indirizzo di rete al computer.
Nel caso dell' IPv4 l'indirizzo è composto da 4 numeri (che vanno da 0 a 255) separati da dei punti.
Un esempio può essere il seguente:  192.168.1.254
Già da tempo il protocollo IPv4 doveva essere sostituito dall'IPv6, visto l'esaurimento degli indirizzi IPv4.
Insieme all'indirizzo IP (prenderemo in considerazione gli indirizzi IPv4, per comodità essendo quello più diffuso) si utilizza anche una mask, un maschera.
La maschera suddivide l'IP della rete in tre classi distinte:
-Classe c ->Si utilizza come maschera 255.255.255.0
-Classe b ->Si utilizza come maschera 255.255.0.0
-Classe a ->Si utilizza come maschera 255.0.0.0
 Questa maschera ha lo scopo di dividere l'indirizzo IP nell'indirizzo vero e proprio della macchina è l'indirizzo della rete dove è collegato il computer, questo avviene facendo una AND bit per bit tra indirizzo IP e maschera.

IP = 192.168.1.254    Mask = 255.255.255.0

    1100000 | 10101000 | 00000001 | 11111110     Facendo la AND bit a bit 
    1111111 | 11111111 | 11111111 | 00000000
=      192           168               1                 0 

Come si vede, quel che ne risulta è l'indirizzo della rete ( o net address) , mentre l'indirizzo del computer non viene visualizzato. L'host address invece (che è 254) è nascosto.
Quindi, se 192.168.1.0 identifica una rete, l'host address 254 identifica quel determinato computer in quella rete.
Gli host utilizzabili vanno da 1 a 254, il 255 è un broadcast, cioè se invio dei dati all'host 255 è come se lo inviassi a tutti gli host presenti nella rete.
Oltre a questi esistono indirizzi di rete specifici che sono privati come 192.168.0.0 , 172.16.0.0 , 10.0.0.0 
Gli indirizzi privati si possono vedere tra di loro solo se sono nella stessa rete.
Il router permette il coesistere sia le reti private sia le reti pubbliche, il router ha un IP che si chiama gateway.
Altri protocolli che il S.O. utilizza per inviare i dati sono il TCP (Transfer Control Protocol) e l'UDT.

Per creare una socket devo avere:
- L'indirizzo di rete di entrambi i computer
- Le porte dove i processi sono in ascolto

Le porte sono valori a 64 bit (quindi può avere valori da 0 a 65535), però le porte da 0 a 1023 sono riservate al S.O.

Una socket può essere indicata così
8.8.8.8:1015 | 1.2.5.7:10

8.8.8.8 e 1.2.5.7 sono gli IP dei due computer, mentre 1015 e 10 sono le porte dove i processi sono in ascolto.
Altra cosa, l'S.O. tramite la scheda di rete è quello che gestisce tutte le socket.

domenica 9 dicembre 2012

LE LISTE CONCATENATE

Una lista concatenata è una particolare struttura dati utilizzata nella programmazione informatica, in particolar modo nel linguaggio C, C++ e Java.
Alla base delle liste concatenate troviamo i puntatori, i link, particolari riferimenti che puntano ad un nodo, cioè un particolare dispositivo hardware, contenuto nel sistema, in grado di comunicare con gli altri dispositivi presenti nella rete. La lista concatenata consiste in una sequenza di questi nodi, con al loro interno dei dati specifici e dei link che collegano tra loro i vari nodi.

Le liste linearmente concatenate si suddividono in 2 grandi categorie:
-Liste semplicemente concatenate: Il collegamento (link) punta al nodo successivo, che a sua volta punterà a quello dopo e così via, terminando quando si avrà un valore vuoto o nullo(NULL).
-Liste doppiamente cocatenate: Il collegamento di ogni nodo punta sia al nodo precedente che a quello successivo. Se il precedente è vuoto o NULL allora quello è il primo nodo della lista; se, invece, il successivo è vuoto o NULL allora quello è l'ultimo nodo.
Una lista concatenata può ingrandire o diminuire la sua dimensione a piacere.


Le politiche di gestione delle liste concatenate sono principalmente 3:
-FIFO(First In First Out): Il primo nodo che richiede di entrare nella lista viene messo in testa a questa, in modo da essere il primo ad essere eseguito.
-LIFO(Last In First Out): L'ultimo nodo che richiede di entrare nella lista viene messo in testa a questa, in modo che nonostante sia l'ultimo ad entrare, sia il primo ad essere eseguito.
-Crescente o Decrescente: Queste due politiche funzionano in maniera contraria, cioè nella prima i nodi vengono messi in ordine dall'ultimo che fa la rpichiesta al primo, invece nella seconda avviene l'esatto opposto, cioè il primo che fa la richiesta, sarà anche il primo ad andare in esecuzione.

Per poter cancellare un nodo all'interno di una lista concatenata, va eliminato tale nodo e poi va creato un collegamento tra il nodo precedente a quello cancellato e quello successivo.                 B-->next=C-->next(D)

Ecco il codice per poter cancellare un nodo:

void cancella(nodo*t,nodo*p)
{
          nodo*a;
          if(t!=NULL)
         {
                    a=t;
                    while(a-->next!=NULL && a-->next!=p)
                    {
                               a=a-->next;
                     }
                     if(a-->next!=NULL)
                    {
                               a-->next=p-->next;
                               free(p);
                    }
          }
}

domenica 2 dicembre 2012

LO SCHEDULER


File:Scheduler.png

Lo scheduler è un software contenuto all'interno del sistema operativo che svolge il compito di gestire i processi, decidendo come amministrare la loro esecuzione.
Uno scheduler si affida a dei parametri contenuti all'interno del suo algoritmo di schedulazione, questi vengono comunemente chiamati politiche di scheduling.
I tipi di algoritmi che possiamo trovare in uno scheduler sono:
-FCFS(first come first served) manda in esecuzione il primo processo che ha fatto la richiesta in modo sequenziale.
-RR(round robin) simile al FCFS, ma con la differenza che quest'ultimo stabilisce un tempo massimo di esecuzione di ogni singolo processo e il tempo che manca ad un processo troppo lungo, per finire la sua esecuzione, verrà dato dopo che gli altri processi in coda abbiano finito.
-SNPF(shortest next process first) viene mandato in esecuzione il processo con il minor tempo di esecuzione tra quelli in attesa.

Un buono scheduler deve sottoporsi a delle regole ben precise per garantire l'efficienza del sistema, queste sono:
-Equità: ogni processo deve andare in esecuzione, senza lasciarne nessuno in memoria con un'attesa infinita.
-Rapidità: deve essere veloce nella scelta dei processi da mandare in esecuzione ed il suo tempo non deve dipendere dal numero di processi in attesa.
-Velocità del processore: la CPU deve essere attiva il più possibile, riducendo i tempi morti, in modo da essere sempre pronta.
-Rispetto gerarchia: lo scheduler dovrà assegnare le giuste priorità ad ogni processo che arriva. Un processo kernel avrà per forza la priorità più alta rispetto ad un processo utente.

martedì 27 novembre 2012

Sistemi: i threads

I threads, detti anche processi leggeri o fili di esecuzione, sono una parte di un processo. I thread condividono tra di loro il code (o text) segment e il data segment, mentre possiedono uno stack segment personale a ogni thread. Condividendo il code segment è sconsigliabile utilizzare comandi come quelli della famiglia dell'exec, visto i risultati inaspettati che possono accadere visto il code segment condiviso.
I principali vantaggi dei thread è quello che nei processori moderni sono capaci di elaborare 2 thread contemporaneamente, rispetto ad un singolo processo, questo perché posseggono il doppio dei registri.

Da notare. È importante all'interno della funzione che verrà eseguita dal main non dichiarare variabili con lo stesso nome di quelle globali. Altrimenti verranno prese in considerazione solo quelle globali.

Esempio:
int a=2;
int main() {...}
Codice thread:
{ 
           int a=15;
printf(“%i\n”,a);
}

In questo caso, sempre che stampi, verrà stampato il valore 2 in quanto a è una variabile globale


Ci sono 3 modi per gestire i thread:
- Vengono gestiti direttamente dal processo padre, simulando la concorrenza (cioè in user space) :
  Non dovendo eseguire chiamate di sistema per la gestione dei thread,  essa risulterà molto più rapida. Il padre dovrà gestire la sincronizzazione dei vari thread e non la monopolizzazione da parte di un thread della CPU
- Creando i thread in kernel space
Creando i thread in kernel space i thread saranno considerati come dei processi, perciò sarà lo stesso S.O. (o più precisamente lo scheduler) a gestire i thread. Purtroppo questo metodo è più lento rispetto alla sua controparte in user space perché ci saranno più chiamate di sistema per la gestione dei thread.
-  Creo i thread in user mode, ma li faccio gestire dal S.O.
L'ideologia di gestione di thread che andremo a considerare.
Il padre ( si intende il thread master, cioè il main), per poter gestire la concorrenza tra i vari thread deve utilizzare dei protocolli: un protocolli sono un insieme di regole che devono essere soddisfatte per affinché la comunicazione, o in questo caso la concorrenza tra thread sia gestita in maniera corretta.
Il S.O. invece entrerà in gioco con le variabili Mutex: queste variabili sono in kernel space e per modificarle bisogna utilizzare delle apposite chiamate di sistema. Queste variabili, dette anche variabili di lock, possono essere utilizzate come semafori per l'accedere alle zone critiche del programma.


Condizione critica: Lettura e scrittura di una variabile globale contemporaneamente tra 2 o più thread
Regione critica: zone di istruzioni che devono essere eseguite da un unico processo dall'inizio alla fine, uno alla volta

Le quattro regole per una buona programmazione concorrente:
- Non ci sia più di un processo nella stessa zona critica
- Il programma deve funzionare indipendentemente dal numero di processori e dalla loro velocità
- I processi che non stanno eseguendo operazioni critiche non devono poter bloccare nessun processo.
- Nessun processo deve avere un'attesa infinita per entrare in una regione critica
(Queste regole valgono anche per i thread)

Suddivisione della memoria da parte di un processo:

Il code segment contiene le istruzioni da eseguire



Nel data segment sono contenute le variabili globali (dette anche statiche, perché dichiarate all'inizio del programma)  e dalle variabili mallocate, cioè quelle dichiarate dinamicamente.





Nello stack sono contenute tutte le variabili del proprio ambiente più altre informazioni indispensabili come lo stato del processore, il process counter ecc...
Queste informazioni sono indispensabili per poter cambiare l'esecuzione da un processo all'altro: Se il processo, dopo esser stato messo nello stato di ready torna ad essere eseguito deve sapere a che istruzione è arrivato, che stato aveva il processore a quell'istruzione, il valori delle variabili locali ecc... Quindi senza queste informazioni il processo non può continuare.


Per poter creare i thread bisogna includere una libreria che si chiama pthread.h ( #include <pthread.h>). Quando si compilerà con il gcc bisognerà implementare il parametro -pthread
Il comando per creare un thread è il ptread_create, che avrà bisogno di 4 parametri per poter essere eseguita.
Il primo parametro è il puntatore di una variabile pthread_t : in questa variabile sarà contenuto il valore del tid ( thread identifier ; è come il pid per i processi), il secondo parametro è utilizzato per creare un thread con determinati attributi, passandogli il parametro NULL creeremo un thread con gli attributi di default, il terzo parametro dovrà avere il nome di una funzione void*. Questa funzione dovrà avere un unico parametro che dovrà essere anch'esso un puntatore void. Il quarto parametro sarà il puntatore della funzione che abbiamo passato come 4 parametro.
La pthread_create fa ritornare un valore


Da notare. E qui sorge un problema: se dobbiamo passare più variabili, ma abbiamo solo un puntatore da poter passare al thread, come possiamo fare? Per risolvere questo problema si può utilizzare una struttura (una struct) contenente tutti i parametri che ci servono passare al thread, passandogli solamente il puntatore della struct.

La funzione che fa aspettare il thread master la fine dei thread figli è il comando pthread_join. Questo comando, molto simile alla waitpid. Infatti, ci sono 2 parametri da passare: uno sarà una variabile pthread_t che conterrà il tid del thread, il secondo parametro è un puntatore, che noi gli passeremo NULL).

(Guardare anche sul manuale la spiegazione di pthread_create e _join, scrivendo da terminale man phtread_create e pthread_join)

Ringraziate Tasca che mi ha aiutato per completare gli appuntolli