Difference between revisions of "Lezioni Anno Accademico 2017/18 I semestre"
(140 intermediate revisions by 19 users not shown) | |||
Line 2: | Line 2: | ||
== Lezione del 26 settembre 2017 == | == Lezione del 26 settembre 2017 == | ||
− | + | === W⁴ H Y === | |
* What: | * What: | ||
* Who: | * Who: | ||
+ | ---- | ||
+ | Noi studenti ed "entrambi" i professori. | ||
<br> | <br> | ||
* When: | * When: | ||
Line 18: | Line 20: | ||
<br> | <br> | ||
* How: | * How: | ||
+ | ---- | ||
+ | Il corso si svolgerà tramite: | ||
+ | * Lezioni frontali in aula. | ||
+ | * Attività di laboratorio. | ||
+ | * Esercitazioni. | ||
+ | |||
+ | Inoltre, durante l'ultimo periodo di lezioni del secondo semestre, vi sarà un periodo di ripasso del programma tramite esercizi, in vista dell'esame. | ||
+ | Durante la lezione del 3/10 sono state indicate alcune propedeuticità del corso. tra cui: programmazione, algoritmi e architettura degli elaboratori in primis. | ||
+ | Importanti sono anche i corsi di Analisi matematica e Algebra e geometria. Per farla breve, il primo anno è propedeutico al secondo. | ||
<br> | <br> | ||
* Why: | * Why: | ||
− | + | ||
− | + | === Fonti e strumenti del corso === | |
− | + | * wiki creata di gruppo, lezioni frontali e esercitazioni. | |
− | + | * Non esiste un vero e proprio testo consigliato. Tutte le informazioni sono date DURANTE il corso. | |
− | + | * Per domande specifiche scrivere su mailing list (so@cs.unibo.it); | |
− | + | * Per esercizi, appunti e programma svolto rivolgersi al wiki (so.v2.cs.unibo.it); | |
− | + | * Per live streaming (www.cs.unibo.it/~renzo/live/). | |
− | + | === Modalità di esame === | |
− | + | esame scritto (diviso in due parti: una parte generale e una di programmazione concorrente) + progetto + orale (facoltativo), possibilità di dare solo lo scritto per ottenere un massimo di 18. La modalità per studenti in "difficoltà" è disponibile solo fino agli appelli autunnali. | |
− | + | * Si parlerà del progetto indicativamente a partire da Dicembre 2017. | |
+ | * L'orale può essere sostenuto da chi vuole migliorare (peggiorare) il voto ottenuto, o da chi vuole ottenere la lode. | ||
+ | === Orario di ricevimento per il primo semestre (fino al 15/12/17) === | ||
+ | martedì alle 11:30. | ||
* Università = docenti + studenti. | * Università = docenti + studenti. | ||
* Informatica = come generare informazione automatica. | * Informatica = come generare informazione automatica. | ||
* Hardware, Software, Elaborazione, Comunicazione, Memorizzazione, Digitale/Analogico. | * Hardware, Software, Elaborazione, Comunicazione, Memorizzazione, Digitale/Analogico. | ||
+ | |||
+ | === Introduzione === | ||
La principale distinzione che facciamo tra '''dato''' e '''informazione''' è che il dato, da solo, è privo di significato. Se, tuttavia, viene interpretato in un particolare contesto allora può diventare informazione significativa per chi sta interpretando i dati. | La principale distinzione che facciamo tra '''dato''' e '''informazione''' è che il dato, da solo, è privo di significato. Se, tuttavia, viene interpretato in un particolare contesto allora può diventare informazione significativa per chi sta interpretando i dati. | ||
− | Un '''algoritmo''' è | + | Un '''algoritmo''' è una o più sequenze non ambigue di passi che, dato un problema, ci permette di risolverlo. |
Il nostro algoritmo, scritto in un qualche linguaggio formale, diventa un '''programma''' (sostanzialmente un testo, un insieme di istruzioni). | Il nostro algoritmo, scritto in un qualche linguaggio formale, diventa un '''programma''' (sostanzialmente un testo, un insieme di istruzioni). | ||
− | Un linguaggio | + | Un linguaggio è un entità software, ed è definito come una quadrupla (alfabeto, sintassi, lessico, semantica) dove: |
*l'''alfabeto'' è l'insieme di simboli che compone il linguaggio. | *l'''alfabeto'' è l'insieme di simboli che compone il linguaggio. | ||
+ | *il ''lessico'' si può vedere come una funzione che va dai simboli a un booleano e ci dice quali sono le frasi ben formate. | ||
*la ''sintassi'' determina quali delle sequenze scritte sono effettivamente corrette. | *la ''sintassi'' determina quali delle sequenze scritte sono effettivamente corrette. | ||
− | |||
*la ''semantica'' associa un significato alle parole ben formate. | *la ''semantica'' associa un significato alle parole ben formate. | ||
== Lezione del 29 settembre 2017 == | == Lezione del 29 settembre 2017 == | ||
− | + | === Cos'è un sistema operativo === | |
Un ''sistema operativo'' è un programma che gestisce i processi, le risorse e interfaccia le applicazioni con l'hardware dell'elaboratore. | Un ''sistema operativo'' è un programma che gestisce i processi, le risorse e interfaccia le applicazioni con l'hardware dell'elaboratore. | ||
Line 54: | Line 70: | ||
A cosa serve un sistema operativo? | A cosa serve un sistema operativo? | ||
Principalmente, ha i seguenti scopi: | Principalmente, ha i seguenti scopi: | ||
− | # | + | # Facilitare l'utilizzo del sistema (senza S.O. sarebbe a carico del programmatore conoscere tutti i linguaggi con cui si comunica con l'architettura della macchina); |
− | # | + | # Rendere affidabile, protetto e sicuro l'utilizzo del sistema (e.g. un processo potrebbe recar danno all'intero sistema se non controllato o gestito, potrebbe ignorare i permessi di visualizzazione di un file); |
− | # | + | # Astrarre l'hardware (e.g. filesystem); |
− | # | + | # Garantire l'efficienza (e.g. non tenere la CPU in idle adottando opportuni algoritmi di scheduling); |
− | # | + | # Assicurare portabilità ; |
− | Un approccio molto usato è il cosiddetto approccio a strati in cui ogni strato utilizza i servizi forniti al livello inferiore e ne fornisce di nuovi a quello superiore. | + | Un approccio molto usato è il cosiddetto approccio a strati ("a livelli") in cui ogni strato utilizza i servizi forniti al livello inferiore e ne fornisce di nuovi a quello superiore. |
− | Astraendo il nostro calcolatore possiamo piazzare al livello più basso l'hardware e, subito sopra, il sistema operativo. I due layer comunicano usando il linguaggio ISA (Instruction Set Architecture), nativo della CPU stessa, al quale si aggiungono quelli che permettono di comunicare con i vari dispositivi come la scheda di rete, la stampante, etc. Sopra il livello del sistema operativo possiamo collocare quello delle librerie e, infine, quello degli applicativi. | + | Astraendo il nostro calcolatore possiamo piazzare al livello più basso l'hardware e, subito sopra, il sistema operativo. I due layer comunicano usando il linguaggio ISA (Instruction Set Architecture), nativo della CPU stessa, al quale si aggiungono quelli che permettono di comunicare con i vari controllori dei dispositivi come la scheda di rete, la stampante, etc. Sopra il livello del sistema operativo possiamo collocare quello delle librerie e, infine, quello degli applicativi. |
− | *systemcall | + | * '''Systemcall''' = meccanismo usato da un processo per richiedere al SO una qualche funzionalità a livello kernel. Un esempio è la funzione printf() del linguaggio C (stampa a video) che utilizza la systemcall write() (una delle systemcall per la gestione dei dispositivi). |
+ | <br> | ||
− | + | === Storia dei sistemi operativi === | |
− | Nella generazione '''zero''' (1800 ca.) possiamo | + | Nella generazione '''zero''' (1800 ca.) possiamo includere Babbage con la sua macchina analitica e lady Ada Lovelace. [[File:290px-AnalyticalMachine Babbage London.jpg |200px|thumb|right|Macchina analitica di Babbage]] |
Nella generazione '''uno''' vediamo la comparsa delle valvole con tutti i problemi connessi. Non c'erano utilizzatori delle macchine, le stesse persone che le costruivano erano anche programmatori e fruitori delle stesse. | Nella generazione '''uno''' vediamo la comparsa delle valvole con tutti i problemi connessi. Non c'erano utilizzatori delle macchine, le stesse persone che le costruivano erano anche programmatori e fruitori delle stesse. | ||
− | Nella generazione '''due''' (fine anni '60) arriva il transistor. Quest'ultimo è molto più veloce e piccolo della valvola e, soprattutto, meno incline a guasti. Le macchine iniziano ad essere più economiche grazie alla possibilità di avere un'economia di scala e quindi accessibili al grande pubblico. Questo fa si che, in queste prime fasi, i costruttori non siano più gli unici utilizzatori. | + | Nella generazione '''due''' (fine anni '60) arriva il transistor. Quest'ultimo è molto più veloce e piccolo della valvola e, soprattutto, meno incline a guasti. Le macchine iniziano ad essere più economiche grazie alla possibilità di avere un'economia di scala e quindi accessibili al grande pubblico. Questo fa si che, in queste prime fasi, i costruttori non siano più gli unici utilizzatori. [[File:Transistors.jpg |300px|thumb|right|Diversi tipi di Transistor]] |
+ | |||
+ | ---- | ||
+ | |||
A questo punto poiché, pur essendo diventata più accessibile, una macchina costava ancora molto, nasce l'esigenza di non lasciare mai un processore senza lavoro (al fine di massimizzare i ricavi) e di condividere i dati. | A questo punto poiché, pur essendo diventata più accessibile, una macchina costava ancora molto, nasce l'esigenza di non lasciare mai un processore senza lavoro (al fine di massimizzare i ricavi) e di condividere i dati. | ||
In questa fase, infatti, abbiamo sistemi operativi di tipo ''batch'' (che collezionano tutto l'input all'inizio per calcolare e restituire l'output) dove l'inserimento di programmi e dati veniva fatto tramite schede perforate e non c'era interazione. Dai sistemi batch nasce lo ''SPOOL'' (Simultaneous Peripheral Operations On-Line). | In questa fase, infatti, abbiamo sistemi operativi di tipo ''batch'' (che collezionano tutto l'input all'inizio per calcolare e restituire l'output) dove l'inserimento di programmi e dati veniva fatto tramite schede perforate e non c'era interazione. Dai sistemi batch nasce lo ''SPOOL'' (Simultaneous Peripheral Operations On-Line). | ||
− | Si comincia a pensare di utilizzare i tempi di I/O per poter eseguire altri processi. Questo però solleva almeno due problematiche: da un lato serve un modo per sapere quando un input è davvero finito mentre, dall'altro, bisogna fare in modo che un processo che non richieda I/O occupi la CPU per un tempo indefinito. | + | Si comincia a pensare di utilizzare i tempi di I/O per poter eseguire altri processi. Questo però solleva almeno due problematiche: da un lato serve un modo per sapere quando un input è davvero finito mentre, dall'altro, bisogna fare in modo che un processo che non richieda I/O non occupi la CPU per un tempo indefinito. |
Per la prima problematica la soluzione è rappresentata dall'introduzione degli '''interrupt''', segnali elettrici inviati alla CPU alla fine di ogni input. | Per la prima problematica la soluzione è rappresentata dall'introduzione degli '''interrupt''', segnali elettrici inviati alla CPU alla fine di ogni input. | ||
Per la seconda, invece, è stato introdotto il cosiddetto '''interval timer''' che non è altro che un dispositivo che invia interrupt dopo un determinato quanto di tempo assicurando che un processo non occupi per troppo tempo il processore. | Per la seconda, invece, è stato introdotto il cosiddetto '''interval timer''' che non è altro che un dispositivo che invia interrupt dopo un determinato quanto di tempo assicurando che un processo non occupi per troppo tempo il processore. | ||
Line 85: | Line 105: | ||
[[File:Dds.png]] | [[File:Dds.png]] | ||
− | Nella '''quarta''' generazione (anni '70 ca.) vediamo la nascita di molte innovazioni importanti. Si riesce a rimpicciolire di molto i processori facendoli divenire a tutti gli effetti micro-processori. Nei laboratori Bell nasce Unix, un sistema operativo | + | Nella '''quarta''' generazione (anni '70 ca.) vediamo la nascita di molte innovazioni importanti. Si riesce a rimpicciolire di molto i processori facendoli divenire a tutti gli effetti micro-processori. Nei laboratori Bell nasce Unix, un sistema operativo rivoluzionario con time sharing e clonazione dei processi (un processo non viene mai creato dal nulla ma viene prima clonato da uno già esistente e poi, alla copia, viene fatto eseguire un programma). A causa di problemi di portabilità da PDP-9, macchina su cui Unix è nato, a PDP-11 nasce il linguaggio C e il suo compilatore (scritto anch'esso in C). |
Mentre Apple I "porta" una nuova idea di pc per tutte le famiglie, nascono molte versioni di Unix. | Mentre Apple I "porta" una nuova idea di pc per tutte le famiglie, nascono molte versioni di Unix. | ||
Per impedire una deriva proprietaria in cui ogni produttore offriva tutto quello che offrivano gli altri ma con qualche feature in più, Richard Stallman fonda il progetto GNU (GNU's Not Unix) per il quale scrive tutta una serie di utilities come il famoso compilatore gcc. | Per impedire una deriva proprietaria in cui ogni produttore offriva tutto quello che offrivano gli altri ma con qualche feature in più, Richard Stallman fonda il progetto GNU (GNU's Not Unix) per il quale scrive tutta una serie di utilities come il famoso compilatore gcc. | ||
Line 93: | Line 113: | ||
== Lezione del 3 ottobre 2017 == | == Lezione del 3 ottobre 2017 == | ||
+ | |||
+ | === Introduzione alla programmazione concorrente === | ||
+ | |||
+ | In programmazione concorrente le nozioni apprese di "algoritmo", "programma" e "processo" devono essere aggiornate. Questo perché non vi è più un programma con un'unica sequenza esecutiva [o meglio, un unico filo (= thread) di esecuzione]. Appare ovvio come servino dei meccanismi per organizzare e sincronizzare tali fili esecutivi, in modo da evitare il verificarsi di eventi non desiderati. | ||
+ | |||
+ | Quando parliamo di ''sistema concorrente'', ci riferiamo ad un sistema in cui due o più "attori" sono eseguiti parallelamente.<br> | ||
+ | |||
+ | Questi attori possono essere '''processi''' o '''thread'''. | ||
+ | La differenza sostanziale tra queste due tipologie di attori riguarda la condivisione della memoria e dei dati su cui operano.<br> | ||
+ | |||
+ | Più processi possono eseguire lo stesso programma ma, ognuno di essi, ha i propri dati su cui operare e il suo stato di avanzamento. In particolare: | ||
+ | * un processo è un'entità, un attore che ha delle proprietà e delle risorse associate; | ||
+ | * in ogni istante, il processo può essere interrotto.<br> | ||
+ | Più thread che vengono eseguiti parallelamente, invece, condividono gli stessi dati su cui operare.<br> | ||
+ | Codici di esempio: | ||
+ | |||
+ | === processi === | ||
+ | |||
+ | <syntaxhighlight lang=C> | ||
+ | #include <stdio.h> | ||
+ | #include <unistd.h> | ||
+ | |||
+ | int main(int argc, char *argv[]){ | ||
+ | if(fork()) printf("Yes"); | ||
+ | else printf("No"); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | L'output generato sarà "YesNo" poiché, usando la systemcall ''fork()'', abbiamo creato una nuova copia del processo (un processo figlio). Il processo padre, che ha eseguito con successo la fork(), stamperà "Yes" mentre il figlio stamperà "No". | ||
+ | |||
+ | === thread === | ||
+ | |||
+ | <syntaxhighlight lang=C> | ||
+ | #include <pthread.h> | ||
+ | #include <stdio.h> | ||
+ | #include <stdlib.h> | ||
+ | #include <assert.h> | ||
+ | |||
+ | #define NUM_THREADS 5 | ||
+ | #define MAX 1000000 | ||
+ | |||
+ | int sum; | ||
+ | |||
+ | void* perform_work(){ | ||
+ | int i; | ||
+ | |||
+ | for(i = 0; i < MAX; i++) sum = sum + 1; | ||
+ | /* optionally: insert more useful stuff here */ | ||
+ | return NULL; | ||
+ | } | ||
+ | |||
+ | int main( int argc, char** argv ){ | ||
+ | pthread_t threads[NUM_THREADS]; | ||
+ | int thread_args[NUM_THREADS]; | ||
+ | int result_code; | ||
+ | unsigned index; | ||
+ | |||
+ | sum = 0; | ||
+ | // create all threads one by one | ||
+ | for(index = 0; index < NUM_THREADS; ++index ){ | ||
+ | thread_args[index] = index; | ||
+ | printf("In main: creating thread %d\n", index); | ||
+ | result_code = pthread_create(&threads[index], NULL, perform_work, NULL); | ||
+ | assert(!result_code); | ||
+ | } | ||
+ | // wait for each thread to complete | ||
+ | for(index = 0; index < NUM_THREADS; ++index ){ | ||
+ | // block until thread 'index' completes | ||
+ | result_code = pthread_join(threads[index], NULL); | ||
+ | assert(!result_code); | ||
+ | printf("In main: thread %d has completed\n", index); | ||
+ | } | ||
+ | |||
+ | printf("In main: All threads completed successfully\n" ); | ||
+ | printf("Result: %d\n", sum); | ||
+ | exit( EXIT_SUCCESS ); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Prima di discutere l'output generato dall'esecuzione di questo codice, è opportuno introdurre, brevemente, gli strumenti utilizzati. | ||
+ | |||
+ | ---- | ||
+ | ==== Pthread (POSIX thread) ==== | ||
+ | |||
+ | Citazione dal Silberschatz, Galvin, Gagne: | ||
+ | ''Pthread si riferisce allo standard POSIX (IEEE 1003.1c) che definisce una API per la creazione e sincronizzazione dei thread.'' | ||
+ | |||
+ | Le istruzioni fondamentali sono: | ||
+ | * '''int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);''' che crea un thread; | ||
+ | * '''int pthread_join(pthread_t thread, void **retval);''' che aspetta che un thread sia completato prima di continuare l'esecuzione. | ||
+ | |||
+ | Da ricordare che, per utilizzare pthread, va incluso l'header ''<pthread.h>'' e durante la compilazione va inclusa la libreria esterna tramite l'opzione ''-pthread''. | ||
+ | ---- | ||
+ | |||
+ | Torniamo all'output generato dall'esecuzione del secondo codice.<br> | ||
+ | Nel codice vengono generati cinque thread ed ognuno somma MAX (=1000000) volte 1 a sum. Ci aspettiamo un output di 5000000 ma, in realtà, viene stampato un valore molto minore.<br> | ||
+ | |||
+ | Questo accade principalmente per due motivi: | ||
+ | *sommare una costante ad una variabile non è un'operazione atomica (richiede almeno tre istruzioni assembler: due mov ed una sum). | ||
+ | *l'interval timer può fermare l'esecuzione del thread mentre sta effettuando una delle tre operazioni richieste per aggiungere uno a sum e provocare un risultato errato. | ||
+ | |||
+ | Questo è uno dei possibili problemi della programmazione concorrente conosciuto come ''race condition''.<br> | ||
+ | Un sistema di processi multipli presenta una '''race condition''' qualora il risultato finale dell'esecuzione dipenda dalla temporizzazione, o dall'ordine con cui i processi vengono eseguiti. | ||
+ | |||
+ | Per risolvere questo problema possiamo adoperare ancora una volta pthread. | ||
+ | Nello specifico, ci venogono offerte istruzione per realizzare una '''mutua esclusione''' sulle risorse condivise in modo che l'operazione che prima non era atomiche,ora, lo sia e non porti più ad una computazione errata.<br> | ||
+ | Codice di esempio:<br> | ||
+ | === Mutex === | ||
+ | |||
+ | <syntaxhighlight lang=C> | ||
+ | #include <pthread.h> | ||
+ | #include <stdio.h> | ||
+ | #include <stdlib.h> | ||
+ | #include <assert.h> | ||
+ | |||
+ | #define NUM_THREADS 5 | ||
+ | #define MAX 1000000 | ||
+ | |||
+ | int sum; | ||
+ | pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; | ||
+ | |||
+ | void* perform_work(){ | ||
+ | int i; | ||
+ | |||
+ | for(i = 0; i < MAX; i++){ | ||
+ | pthread_mutex_lock(&mutex); | ||
+ | sum = sum + 1; | ||
+ | pthread_mutex_unlock(&mutex); | ||
+ | } | ||
+ | /* optionally: insert more useful stuff here */ | ||
+ | return NULL; | ||
+ | } | ||
+ | |||
+ | int main( int argc, char** argv ){ | ||
+ | pthread_t threads[NUM_THREADS]; | ||
+ | int thread_args[NUM_THREADS]; | ||
+ | int result_code; | ||
+ | unsigned index; | ||
+ | |||
+ | sum = 0; | ||
+ | // create all threads one by one | ||
+ | for(index = 0; index < NUM_THREADS; ++index ){ | ||
+ | thread_args[index] = index; | ||
+ | printf("In main: creating thread %d\n", index); | ||
+ | result_code = pthread_create(&threads[index], NULL, perform_work, NULL); | ||
+ | assert(!result_code); | ||
+ | } | ||
+ | // wait for each thread to complete | ||
+ | for(index = 0; index < NUM_THREADS; ++index ){ | ||
+ | // block until thread 'index' completes | ||
+ | result_code = pthread_join(threads[index], NULL); | ||
+ | assert(!result_code); | ||
+ | printf("In main: thread %d has completed\n", index); | ||
+ | } | ||
+ | |||
+ | printf("In main: All threads completed successfully\n" ); | ||
+ | printf("Result: %d\n", sum); | ||
+ | exit( EXIT_SUCCESS ); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | ---- | ||
+ | '''MUTEX''' | ||
+ | |||
+ | Un mutex è un dispositivo di MUTual EXclusion (mutua esclusione), ed è utile per proteggere le strutture dati condivise da modifiche concorrenti, implementare sezioni critiche e monitor. | ||
+ | |||
+ | Un mutex ha due possibili stati: sbloccato (non posseduto da nessun thread) e bloccato (posseduto da un thread). Un mutex non può mai essere posseduto da due thread contemporaneamente.<br>Se un thread provasse ad acquisire il mutex, mentre quest'ultimo è già in possesso di un altro thread, dovrebbe aspettare finché non viene rilasciato. | ||
+ | La variabile che realizza il mutex, di tipo ''pthread_mutex_t'', viene inizializzata staticamente assegnandogli PTHREAD_MUTEX_INITIALIZER. | ||
+ | '''pthread_mutex_lock''' acquisisce immediatamente il mutex, se libero. Altrimenti aspetta fino a trovarla, prima o poi, libera.<br> | ||
+ | '''pthread_mutex_unlock''' rilascia il mutex. | ||
+ | |||
+ | [da: https://sourceware.org/pthreads-win32/manual/pthread_mutex_init.html] | ||
+ | ---- | ||
+ | |||
+ | Con questa soluzione abbiamo raggiunto un risultato esatto pur avendo pagato in prestazioni: la computazione infatti è più lenta a causa del tempo richiesto dalla sincronizzazione tra i thread. | ||
+ | |||
+ | Consideriamo ora due scenari leggermente più complicati: | ||
+ | * nel primo abbiamo due processi, A e B, che devono accedere a due risorse, X e Y, contemporaneamente prima di poter terminare. Cosa succede se, mentre A acquisisce X, B acquisisce Y? | ||
+ | * nel secondo abbiamo tre processi, A, B e C, che devono accedere alla risorsa X. Che succede se A e B, essendo più veloci, occupano insistentemente la risorsa e non permettono a C di utilizzarla? | ||
+ | |||
+ | La prima condizione si chiama '''deadlock''', la seconda condizione si chiama '''starvation'''. Il deadlock (stallo) è un problema che non può scomparire autonomamente, mentre la starvation ("inedia") si può risolvere da sola. A tal proposito ci viene incontro l'assioma di '''finite progress''': "ogni processo che può avanzare, prima o poi lo farà". | ||
+ | <br><br> | ||
+ | In ogni caso, per parlare in maniera più approfondita di queste tematiche: | ||
+ | * avremo bisogno di un modello di riferimento | ||
+ | * avremo bisogno di paradigmi in grado di esprimere la concorrenza (alcuni paradigmi saranno rivolti alla concorrenza a memoria privata; altri al multithreading a risorse condivise) | ||
+ | * dovremo imparare a scrivere programmi | ||
+ | Nel nostro modello di riferimento l'assegnamento di costanti è un operazione atomica. Dagli esempi precedenti, infatti, si evince come ci sia bisogno di creare atomicità in sezioni critiche. Inoltre, il nostro modello dovrà rispettare le proprietà di safety e di liveness, ovvero: | ||
+ | * Tutti i processi danno la stessa risposta; | ||
+ | * Ogni processo corretto da come risposta una tra quelle proposte; | ||
+ | * Ogni processo prima o poi fornisce una risposta. | ||
+ | |||
== Lezione del 6 ottobre 2017 == | == Lezione del 6 ottobre 2017 == | ||
+ | === Il linguaggio C === | ||
+ | |||
+ | Il linguaggio C nasce nei primi anni 70 grazie al lavoro di Dennis Ritchie con lo scopo di scrivere il | ||
+ | sistema operativo Unix. Quali sono le caratteristiche di C e cosa lo rende un linguaggio adatto per scrivere un sistema | ||
+ | operativo? Un linguaggio per scrivere un sistema operativo deve: | ||
+ | * essere indipendente dalla macchina su cui viene scritto | ||
+ | * dare la possibilità di lavorare direttamente con la memoria | ||
+ | * permette di andare a modificare i singoli bit in memoria | ||
+ | * essere semplice da usare | ||
+ | * essere massimamente produttivo | ||
+ | * essere veloce, efficiente | ||
+ | C presenta le caratteristiche dette sopra e alcune altre: | ||
+ | * in C, dal punto di vista sintattico, tutto è funzione. Si può vedere un programma in C come un insieme di funzioni. La funzione principale è il '''main'''. L'unica cosa che la distingue è il nome, a parte quello il main è una funzione come le altre. Prende due argomenti: argcount e argvalue, che contengono rispettivamente il numero degli argomenti e un array contenente gli argomenti stessi. L'interfaccia del main è tipicamente: <syntaxhighlight lang=C> int main(int argc, char *argv[]) </syntaxhighlight> | ||
+ | * il costrutto fondamentale è l'espressione. ''expression'' ; = ''instruction'' | ||
+ | * sono previste istruzioni di struttura <syntaxhighlight lang=C>if, while, switch, etc.</syntaxhighlight> | ||
+ | * le variabili, per poter essere utilizzate, devono essere sempre definite | ||
+ | * il C è un linguaggio fortemente tipato, però con un certo numero di eccezioni: ad esempio, è possibile fare il casting esplicito di un puntatore void * in qualsiasi tipo, senza che il compilatore dia errori o warnings, con conseguente perdita di type safety | ||
+ | * C è un linguaggio "per adulti": non impone una struttura ordinata al codice, come ad esempio fa invece Python, ed è piena responsabilità del programmatore scrivere programmi ordinati/leggibili. Inoltre molte operazioni che in altri linguaggi non sarebbero permesse lo sono invece in C: anche in questo caso è responsabilità del programmatore fare corretto uso del linguaggio | ||
+ | * C è un linguaggio molto semplice. Come si sopperisce alle mancanze imposte da questa semplicità? Grazie alle librerie: in C tutte le funzionalità aggiuntive (non fornite dal linguaggio stesso, come l'allocazione di memoria, la stampa a schermo, la gestione di input e output) sono fornite dalle librerie | ||
+ | * C è anche sintatticamente semplice. Dove va a finire la complessità sintattica? Nel preprocessore: prima di essere compilato un programma in C passa attraverso un preprocessore, che produce codice in base alle direttive di preprocessione date nel codice stesso <syntaxhighlight lang=C>#ifdef VAR ... #endif, # e ## operator, __LINE__,etc</syntaxhighlight> | ||
+ | * in C vi sono fondamentalmente due tipi di dato: int e float. Tutti gli altri "tipi" di dato sono derivati da essi. Si hanno diversi modificatori, che vanno a modificare la quantità di memoria allocata per la variabile. Alcuni di questi sono <syntaxhighlight lang=C>long, short, char, etc</syntaxhighlight> Un dato di tipo long int ha, solitamente, la dimensione di una parola di memoria della macchina su cui è eseguito il programma. char alloca 8 bits. Se si prende una variabile di un certo tipo e lo si assegna ad una di tipo più ampio, il valore viene mantenuto. Viceversa quando si assegna una variabile di un tipo ad una di tipo meno ampio, una volta "superato il limite" si torna indietro ciclicamente al valore iniziale. Per queste situazioni il compilatore non dà né warnings né errors: sono perfettamente lecite in C | ||
+ | * in C si passa una varibile ad una funzione solo per valore; per passare "per riferimento" una variabile si passa per valore l'indirizzo della variabile | ||
+ | * in C si può fare uso di puntatori alla memoria. Dato un puntatore p, un'istruzione del tipo p+i va letta come: vai avanti di i posizioni della grandezza del tipo del puntatore ognuno. Un vettore in C non è altro che un puntatore che non può essere assegnato. Una scrittura del tipo array[i] è una abbreviazione di *(array + i). Non c'è controllo in C se vado oltre la memoria allocata per l'array. Perché? In Pascal, ad esempio, c'è un tale controllo. Il motivo per cui in C questo manca è che questo controllo genera ulteriore codice macchina. Dovendo C essere efficiente, non ci si può permettere un tale costo aggiuntivo. La responsabilità di non andare oltre è lasciata al programmatore | ||
+ | |||
+ | Esercizio svolto che prende in input due valori tramite i parametri del metodo '''main''', li converte da sequenze di caratteri ad interi, per poi stamparli utilizzando delle '''System Call'''. Lo scopo dell’esercizio è dimostrare come sia possibile utilizzare il linguaggio C senza l’utilizzo di librerie particolari (ad eccezione di quelle per le System Call). | ||
+ | |||
+ | <syntaxhighlight lang=C> | ||
+ | #define _GNU_SOURCE | ||
+ | #include <unistd.h> | ||
+ | #include <sys/syscall.h> | ||
+ | |||
+ | int _uatoi(char *s, int value){ | ||
+ | if (*s == 0) | ||
+ | return value; | ||
+ | else if (*s >= '0' && *s <= '9'){ | ||
+ | value = value * 10 + (*s - '0'); | ||
+ | return _uatoi(s+1, value); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | int uatoi(char *s){ | ||
+ | return _uatoi(s, 0); | ||
+ | } | ||
+ | |||
+ | char *uitoa(int val, char *buf, int len){ | ||
+ | int i; | ||
+ | for (len = len - 1; len >= 0 && val > 0; len--, val /= 10){ | ||
+ | buf[len] = '0' + (val %10); | ||
+ | } | ||
+ | |||
+ | return buf + len + 1; | ||
+ | } | ||
+ | |||
+ | #define MAXOUTLEN 10 | ||
+ | int main (int argc, char *argv[]) { | ||
+ | int tot = 0; | ||
+ | int i; | ||
+ | char buf[MAXOUTLEN]; | ||
+ | char *result; | ||
+ | for (i = 1; i < argc; i++) | ||
+ | tot += uatoi(argv[i]); | ||
+ | result = uitoa(tot, buf, MAXOUTLEN); | ||
+ | syscall(__NR_write, 1, result, MAXOUTLEN - (result - buf)); | ||
+ | syscall(__NR_write, 1, "\n", 1); | ||
+ | return tot; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
== Lezione del 10 ottobre 2017 == | == Lezione del 10 ottobre 2017 == | ||
+ | === Programmazione concorrente: notazione e un primo problema === | ||
+ | |||
+ | Esempio di possibile rappresentazione (che non useremo) di codice concorrente.<br> | ||
+ | '''''A'', ''B'', ''C'', ''D'', ''E''''' rappresentano porzioni di codice.<br> | ||
+ | '''''//''''' rappresenta esecuzione parallela | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | A | ||
+ | cobegin | ||
+ | B | ||
+ | // | ||
+ | C | ||
+ | // | ||
+ | D | ||
+ | coend | ||
+ | E | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Il codice sarà eseguito come: ''A'' -> ''BCD'' -> ''E''. ''E'' quindi verrà eseguito solo dopo la fine dell'esecuzione di ''BCD''. | ||
+ | |||
+ | Un esempio che si presta molto bene ad illustrare la programmazione concorrente è quello del '''merge sort''' concorrente: | ||
+ | <syntaxhighlight lang='c'> | ||
+ | leggi vettore | ||
+ | cobegin | ||
+ | ordina la prima metà del vettore | ||
+ | // | ||
+ | ordina la seconda metà del vettore | ||
+ | coend | ||
+ | merge (miscela) i risultati | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Versione errata del merge sort concorrente: | ||
+ | <syntaxhighlight lang='c'> | ||
+ | leggi vettore | ||
+ | cobegin | ||
+ | ordina la prima metà del vettore | ||
+ | // | ||
+ | ordina la seconda metà del vettore | ||
+ | // | ||
+ | merge (miscela) i risultati | ||
+ | coend | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Avendo un numero ''N'' di processi, vorremmo avere una notazione che ci permetta di definire una porzione di codice come atomica (la sezione critica). Come possiamo procedere?<br> | ||
+ | Teoricamente, vorremmo avere due funzioni, '''csenter()''' e '''csexit()''' che ci permettano di entrare e uscire dalla sezione critica del codice senza causare errori nella computazione.<br> | ||
+ | Quali sono le proprietà che queste funzioni debbono rispettare? | ||
+ | # Devono garantire la mutua esclusione (1 solo processo esegue la critical section alla volta). Contando ''n'' csenter() completate e ''m'' csexit() completate la differenza deve essere <=1 | ||
+ | # Non devono fare deadlock tra di loro | ||
+ | # Non devono fare starvation tra loro | ||
+ | # ''(no unnecessary delay)'' un processo che non sta usando la critical section {csenter()..csexit()} non deve bloccare un altro processo dall'entrarvi. | ||
+ | |||
+ | La soluzione a questo problema è nota come '''soluzione di Dekker''' (scritta e pubblicata da Dijkstra) che venne presentata in modo graduale attraverso l'utilizzo di quattro possibili, ma sbagliate, soluzioni. | ||
+ | === Soluzione di Dekker === | ||
+ | Vorremmo avere programmi che abbiano la seguente struttura: | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | process[i], i=1,...,N | ||
+ | |||
+ | while(1): | ||
+ | csenter() //entrata nella sezione critica (critical section) | ||
+ | critical code | ||
+ | csexit() //uscita dalla sezione critica | ||
+ | non-critical code | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Ad esempio, tornando al problema dei thread che sommavano 1 a ''sum'' MAX volte, avremo: | ||
+ | <syntaxhighlight lang='c'> | ||
+ | process A: | ||
+ | |||
+ | for (i = 0; i < MAX; i++) | ||
+ | csenter() | ||
+ | sum += 1 //atomicità descritta da csenter(), csexit() | ||
+ | // < sum += 1 > atomicità lasciata al processore | ||
+ | csexit() | ||
+ | |||
+ | process B: | ||
+ | |||
+ | for (i = 0; i < MAX; i++) | ||
+ | csenter() | ||
+ | sum += 1 | ||
+ | csexit() | ||
+ | </syntaxhighlight> | ||
+ | <br> | ||
+ | |||
+ | ==== Soluzione 1: A turni ==== | ||
+ | |||
+ | Qual è il problema?<br> | ||
+ | *'''mutex''' OK<br> | ||
+ | *'''deadlock''' OK<br> | ||
+ | *'''starvation''' OK<br> | ||
+ | *'''no unnecessary delay''' NO<br> | ||
+ | :: Perché se P ha bisogno spesso della critical section, P non deve aspettare che Q chieda la critical section e, all'uscita, lo autorizzi di nuovo ad usarla impostando ''turn = P''. Se Q una volta richiesta la sezione non la usa ma si ferma, P non può richiederla ancora perché appartiene ancora a Q. | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | |||
+ | turn=P | ||
+ | |||
+ | process P: | ||
+ | while(1) | ||
+ | while (turn != P) | ||
+ | ; | ||
+ | critical code | ||
+ | turn = Q | ||
+ | non-critical code | ||
+ | |||
+ | process Q: | ||
+ | while(1) | ||
+ | while (turn != Q) | ||
+ | critical code | ||
+ | turn = P | ||
+ | non-critical code | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Soluzione 2: con 2 variabili ==== | ||
+ | Cerchiamo di risolvere il problema dei turni | ||
+ | |||
+ | Finché Q usa la sezione P aspetta, quando Q ha finito P prende la sezione critica e non ci sono ritardi indesiderati. | ||
+ | |||
+ | *'''starvation''' OK | ||
+ | *'''deadlock''' OK | ||
+ | *'''no unnecessary delay''' OK | ||
+ | *'''mutex''' NO | ||
+ | :: Possono essere entrambi come false, escono insieme nel while, tutti e due mettono a true ed entrano assieme nel while. | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | |||
+ | inP = false; | ||
+ | inQ = false; | ||
+ | |||
+ | process P: | ||
+ | while(1) | ||
+ | while (inQ) | ||
+ | ; | ||
+ | inP = true | ||
+ | critical code | ||
+ | inP = false | ||
+ | non-critical code | ||
+ | |||
+ | process Q: | ||
+ | while(1) | ||
+ | while (inP) | ||
+ | ; | ||
+ | inQ = true | ||
+ | critical code | ||
+ | inQ = false | ||
+ | non-critical code | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Soluzione 3 ==== | ||
+ | '''Il problema di soluzione 2 è che i processi non sono al corrente dell'intenzione dell'altro di entrare''' nel while quindi si crea il problema. | ||
+ | *'''mutex''' OK | ||
+ | *'''deadlock''' NO (potrebbero entrambi richiedere, contemporaneamente, l'ingresso alla critical section) | ||
+ | *'''starvation''' OK | ||
+ | *'''non unnecessary delay''' OK | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | needP = false; | ||
+ | needQ = false; | ||
+ | |||
+ | process P: | ||
+ | while(1) | ||
+ | needP = true | ||
+ | while (needQ) | ||
+ | ; | ||
+ | critical code | ||
+ | needP = false | ||
+ | non-critical code | ||
+ | |||
+ | process Q: | ||
+ | while(1) | ||
+ | needQ = true | ||
+ | while (needP) | ||
+ | ; | ||
+ | critical code | ||
+ | needQ = false | ||
+ | non-critical code | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | ==== Soluzione 4 ==== | ||
+ | |||
+ | *'''mutex''' OK | ||
+ | *'''deadlock''' OK | ||
+ | *'''no unnecessary delay''' OK | ||
+ | *'''starvation''' NO | ||
+ | :: Perché può esistere il processo che prova sempre a chiedere nel momento sbagliato e non esegue mai. | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | inP = false; | ||
+ | inQ = false; | ||
+ | |||
+ | process P: | ||
+ | while(1) | ||
+ | inP = true | ||
+ | while (inQ){ | ||
+ | inP = false; | ||
+ | delay | ||
+ | inP = true; | ||
+ | } | ||
+ | critical code | ||
+ | inP = false | ||
+ | non-critical code | ||
+ | |||
+ | process Q: | ||
+ | while(1) | ||
+ | inQ = true | ||
+ | while (inQ){ | ||
+ | inQ = false; | ||
+ | delay | ||
+ | inQ = true; | ||
+ | } | ||
+ | critical code | ||
+ | inQ = false | ||
+ | non-critical code | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Soluzione finale ==== | ||
+ | Uniamo soluzione 1 (poco simmetrica) e soluzione 4 (troppo simmetrica) usando quello che ci serve (attraverso ''needP'' e ''needQ'') e, se dovessimo essere tutti e due contemporaneamente a voler entrare, usiamo i turni (''turn'') per entrare nella critical section (settando il need dell'altro processo a false). | ||
+ | |||
+ | *'''mutex''' OK | ||
+ | *'''deadlock''' OK | ||
+ | *'''starvation''' OK | ||
+ | *'''non unnecessary delay''' OK | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | needP = false; | ||
+ | needQ = false; | ||
+ | turn = P | ||
+ | |||
+ | process P: | ||
+ | while(1) | ||
+ | needP = true | ||
+ | while (needQ) | ||
+ | if (turn != P) { | ||
+ | needP = false; | ||
+ | while ( turn != P) | ||
+ | ; /* busy wait */ | ||
+ | needP = true; | ||
+ | } | ||
+ | /*critical code*/ | ||
+ | turn = Q | ||
+ | needP = false | ||
+ | /*non-critical code*/ | ||
+ | |||
+ | process Q: | ||
+ | while(1) | ||
+ | needQ = true | ||
+ | while (needP) | ||
+ | if (turn != Q) { | ||
+ | needQ = false; | ||
+ | while ( turn != Q) | ||
+ | ; /* busy wait */ | ||
+ | needQ = true; | ||
+ | } | ||
+ | /*critical code*/ | ||
+ | turn = P | ||
+ | needQ = false | ||
+ | /*non-critical code*/ | ||
+ | </syntaxhighlight> | ||
+ | |||
== Lezione del 13 ottobre 2017 == | == Lezione del 13 ottobre 2017 == | ||
+ | === Il linguaggio C: PREPROCESSORE === | ||
+ | |||
+ | Il '''preprocessore''' è uno strumento contenuto nella toolchain di '''gcc''' che esamina il codice sorgente prima che questo venga compilato, e "trasforma" il programma prima della compilazione; è propriamente definito come ''macro processor'', ovvero un esaminatore di '''macro'''. | ||
+ | Le '''macro''' sono delle abbreviazioni che in realtà definiscono delle strutture di codice più grandi, e tornano molto utili in diversi casi nello sviluppo di un programma in C (è buona norma scrivere l’identificatore completamente in '''MAIUSCOLO'''). Il preprocessore esamina le cosiddette '''direttive'''; per conoscere l’output del preprocessore è possibile digitare il comando <kbd>'''gcc -E''' ''{nomeDelSorgente}''</kbd>. | ||
+ | |||
+ | Un primo esempio è dato dalla direttiva '''<kbd>#define</kbd>''', la quale crea una ''macro'', ovvero l’associazione di un identificatore (con o senza parametri) con una stringa di ''token''. Le macro così definite possono assumere quindi diverse funzioni: | ||
+ | # condizione per l’inclusione o meno di codice nel file che verrà passato al compilatore; | ||
+ | # inserimento di '''costanti''' all’interno del codice (notare che questa direttiva nasce molto prima della keyword '''const''' del linguaggio C); | ||
+ | # creazione di abbreviazioni a porzioni di codice più lunghe. | ||
+ | |||
+ | ==== Caso 1 ==== | ||
+ | Il primo caso, che potrebbe essere definito come il più semplice, prevede la dichiarazione di un valore costante, su cui ci si baserà in seguito per includere o meno parti di codice. Per fare ciò occorre introdurre anche altre direttive del preprocessore: '''#ifdef''', '''ifndef''' ed '''endif''', le quali identificano un blocco di codice che deve essere incluso nel file da compilare solo se rispettivamente una macro è definita ''(#ifdef)'' o una macro non è definita ''(#ifndef)'', mentre la direttiva ''#endif'' indica la fine del blocco condizionale. | ||
+ | <syntaxhighlight lang="C"> | ||
+ | #include <stdio.h> | ||
+ | |||
+ | #define DEBUG | ||
+ | |||
+ | int main() { | ||
+ | #ifdef DEBUG | ||
+ | printf("In main\n"); | ||
+ | #endif | ||
+ | |||
+ | // la macro __linux__ è definita solamente nei sistemi Linux-based | ||
+ | #ifndef __linux__ | ||
+ | printf("WARNING: not a Linux system\n"); | ||
+ | #endif | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | :Da notare in questo caso l’utilizzo della macro '''__linux__''', una macro predefinita nei sistemi ''Linux-based'', che consente di discriminare il sistema operativo su cui il programma viene compilato, dando quindi la possibilità di compilare del codice diverso in base alla piattaforma utilizzata. <br> | ||
+ | ==== Caso 2 ==== | ||
+ | Il secondo caso prevede la presenza di un valore in seguito all’identificatore, che quindi verrà sostituito in ogni sua occorrenza all’interno del codice. È da far notare come questo meccanismo sia simile all’utilizzo della keyword '''const''' del linguaggio, ma quest’ultima nasce solo successivamente come alternativa ''type-safe'' (dal momento che le macro potrebbero essere utilizzate ovunque senza alcun tipo di controllo). Questo meccanismo torna ad esempio quando si vuole effettuare un’operazione un numero di volte prefissato; in questo modo, è sufficiente modificare il valore della macro nella sua definizione, per aggiornare tutto il programma che utilizzava quella macro al nuovo valore. | ||
+ | <syntaxhighlight lang="C"> | ||
+ | #include <stdio.h> | ||
+ | |||
+ | #define ITER_NUM 5 | ||
+ | |||
+ | int main() { | ||
+ | int i; | ||
+ | for (i = 0; i < ITER_NUM; i++) | ||
+ | printf("Iterazione numero %d\n", i); | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </syntaxhighlight> <br> | ||
+ | ==== Caso 3 ==== | ||
+ | L’ultimo caso analizzato è quello che può essere definito il più complesso tra i tre. L’identificatore della macro ora presenta anche dei parametri formali, all’interno di parentesi e separati da virgole. Tali parametri compaiono anche nella token string in cui vengono utilizzati (anche più volte) e su cui vengono eseguite delle operazioni. Questa particolarità è utile per permettere di ridurre porzioni di codice che vengono definite così una volta sola, e per essere utilizzate sarà sufficiente scrivere il nome dell’identificatore con i relativi parametri. Da notare come ci sia un numero considerevole di parentesi, che potrebbero quasi sembrare superflue. In realtà sono essenziali per assicurare il corretto ordine di esecuzione delle istruzioni una volta che la stringa di token sarà sostituita alle occorrenze dell’identificatore. | ||
+ | <syntaxhighlight lang="C"> | ||
+ | #include <stdio.h> | ||
+ | |||
+ | #define MAX(x, y) ((x) > (y) ? (x) : (y)) | ||
+ | |||
+ | int main() { | ||
+ | printf("%d\n", MAX(10, 20)); | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | :Che produrrà, in seguito all’esecuzione del preprocessore: | ||
+ | <syntaxhighlight lang="C"> | ||
+ | |||
+ | int main() { | ||
+ | printf("%d\n", ((10) > (20) ? (10) : (20))); | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | :Occorre far notare che quella effettuata dal preprocessore non è altro che una '''string substitution''', cioè si limita a sostituire le macro utilizzate nel codice con la rispettiva definizione (ovviamente includendo eventuali parametri formali), ma non effettua alcun tipo di controllo. Ciò significa che occorre prestare molta attenzione quando si utilizzano le macro come nell’ultimo caso illustrato, perché potrebbero comparire dei '''side-effect''' inaspettati. Se si modifica l’esempio precedente in questo modo | ||
+ | <syntaxhighlight lang="C"> | ||
+ | #include <stdio.h> | ||
+ | |||
+ | #define MAX(x, y) ((x) > (y) ? (x) : (y)) | ||
+ | |||
+ | int main() { | ||
+ | int i = 100; | ||
+ | printf("%d\n", MAX(10, i++)); | ||
+ | printf("%d\n", MAX(10, i)); | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | :si otterrà un risultato che in un primo momento potrebbe essere inaspettato: | ||
+ | <kbd> | ||
+ | 101<br>102 | ||
+ | </kbd> | ||
+ | :Ad un’analisi più approfondita tuttavia, è possibile accorgersi dell’errore: il preprocessore effettua una sostituzione testuale dei parametri al momento dell’utilizzo della macro; ciò significa che sostituisce ogni occorrenza del primo parametro nella ''token string'' con il primo valore fornito, e dualmente per il secondo parametro. Il codice che verrà sottoposto al compilatore conterrà quindi due incrementi della variabile <kbd>i</kbd>. Un esempio a grandi linee delle operazioni eseguite dal compilatore è la seguente (per capirlo è necessario aver ben compreso la nozione di '''post-incremento''' della variabile i): | ||
+ | #assegno alla variabile i il valore 100 | ||
+ | #10 è maggiore di i? | ||
+ | #NO, incremento i (ora i vale 101) | ||
+ | #scrivo i (101) | ||
+ | #incremento i (ora i vale 102) | ||
+ | #10 è maggiore di i? | ||
+ | #NO, scrivo i (102) | ||
+ | |||
+ | |||
+ | === Approfondimenti === | ||
+ | |||
+ | Esistono molti altri operatori che si possono utilizzare all’interno delle macro, due esempi sono: | ||
+ | * l’operatore '''#''' che, se anteposto al nome di un parametro nella stringa di token, viene riconosciuto dal processore, che rimpiazza il parametro con il simbolo '''#''' anteposto con il proprio valore letterale convertito in una stringa costante; | ||
+ | * l’operatore '''##''' "combina" due token in uno: unisce il token alla sinistra e quello alla destra dell’operatore per farlo diventare un’unico token. Questo torna molto utile se uno dei due token proviene da un parametro della macro.<br><br> | ||
+ | Per un esempio su questi ultimi due operatori: [http://so.v2.cs.unibo.it/wiki/index.php?title=Esercizi_di_%22lettura%22_di_in_linguaggio_C_2017/18#tables_and_preprocessor_tricks tables and preprocessors tricks] | ||
+ | ---- | ||
+ | <br> | ||
+ | |||
+ | Per ulteriori approfondimenti sul preprocessore del C: [https://gcc.gnu.org/onlinedocs/cpp/ Approfondimenti sul preprocessore del C] | ||
+ | |||
== Lezione del 17 ottobre 2017 == | == Lezione del 17 ottobre 2017 == | ||
+ | |||
+ | === Oltre la soluzione di Dekker === | ||
+ | |||
+ | Nella scorsa lezione di teoria abbiamo visto la soluzione di Dekker dove due processi, per poter entrare ed eseguire la critical section, manifestano prima questo loro bisogno (<code>needP/needQ = true</code>), controllano poi se anche l'altro processo ha comunicato questo bisogno e, in caso affermativo, verrà utilizzata una politica a turni per gestire l'entrata e l'uscita alla sezione critica. | ||
+ | |||
+ | P.S. Nei sistemi reali (specialmente multi-core) la soluzione di Dekker necessita di istruzioni aggiuntive come <code>__syn_synchronize()</code> e <code>sched_yeld()</code>: la prima sincronizza il contenuto delle cache (assicurando la correttezza del risultato) mentre la seconda fa sì che il thread rinunci al suo tempo di CPU permettendo a qualche altro processo di avere la priorità. | ||
+ | |||
+ | Questa soluzione ha però due limiti: | ||
+ | *non funziona con un numero di processi > 2 | ||
+ | *è relativamente complicato da capire, si può fare di meglio! | ||
+ | |||
+ | Un'evoluzione più lineare e facilmente adattabile a più di due processi è l''''algoritmo di Peterson''' che, nel caso base, ha la seguente struttura: | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | needP = false; | ||
+ | needQ = false; | ||
+ | turn; | ||
+ | |||
+ | process P: | ||
+ | while(1) | ||
+ | /* entry protocol */ | ||
+ | needP = true | ||
+ | turn = Q | ||
+ | while(needQ && turn != P) | ||
+ | ; /* busy wait */ | ||
+ | /* critical section */ | ||
+ | needP = false | ||
+ | /* non-critical section */ | ||
+ | |||
+ | process Q: | ||
+ | while(1) | ||
+ | /* entry protocol */ | ||
+ | needQ = true | ||
+ | turn = P | ||
+ | while(needP && turn != Q) | ||
+ | ; /* busy wait */ | ||
+ | /* critical section */ | ||
+ | needQ = false | ||
+ | /* non-critical section */ | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Questa soluzione è più snella poiché i processi, invece di assegnare la variabile <code>turn</code> all'altro processo come in Dekker, lo fanno prima.<br> | ||
+ | Possiamo vedere questa soluzione come se avesse una sorta di "anticamera" da cui bisogna necessariamente passare per poter accedere alla sezione critica.<br> | ||
+ | Entra nella critical section sempre l'ultimo processo a non essere entrato nell'anticamera.<br> | ||
+ | Questi concetti possono bessimo essere estesi nel caso in cui abbiamo più di due processi a patto di estendere l'anticamera con tanti "stage" quanti sono i processi. | ||
+ | |||
+ | Queste soluzioni funzionano in pratica ma, ci sono ancora tutti i difetti di prima: '''busy wait''' che spreca cicli di CPU e una grande difficoltà nello scrivere programmi concorrenti in questo modo. Ci farebbero davvero comodo dei paradigmi di più alto livello. | ||
+ | |||
+ | Davanti a noi si aprono principalmente due possibilità: | ||
+ | *l'hardware della macchina ci viene in aiuto mascherando gli interrupt (nei sistemi mono-core) | ||
+ | *l'hardware (e gli ingegneri) ci aiuta fornendoci un'apposita istruzione atomica: la '''TEST&SET''' | ||
+ | |||
+ | L'idea che sta alla base della T&S è quella di avere una variabile globale che ci dica se la critical section è libera (0) o occupata (1).<br> | ||
+ | La T&S viene invocata con due parametri, una variabile locale che serve per verificare lo stato precedente e la variabile globale, e realizza la seguente funzionalità: | ||
+ | |||
+ | '''<code>T&S(x,y) = <x = y; y = 1></code>''' | ||
+ | |||
+ | Quindi, intuitivamente, possiamo avere due casi: o la variabile globale è 0, e la T&S assegna 0 a x e setta la variabile globale a 1, o la variabile globale è a 1 e la funzione semplicemente immagazzina 1 in x. | ||
+ | Questa semplice funzione è abbastanza per realizzare i protocolli csenter() e csexit() e realizzare le critical section: | ||
+ | <syntaxhighlight lang='c'> | ||
+ | G = 0; /* global variable */ | ||
+ | csenter(): | ||
+ | L; | ||
+ | do{ | ||
+ | T&S(L, G) | ||
+ | }while(L == 1) | ||
+ | |||
+ | csexit(): | ||
+ | G = 0 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Questo approccio ci fornisce diversi vantaggi: è nativamente adatto a gestire un numero arbitrario di processi, è semplice e ci garantisce 3 delle 4 proprietà fondamentali delle critical section (starvation può accadere).<br> | ||
+ | Però c'è ancora busy wait (chiamata anche spin-lock in questo contesto), potrebbe esserci starvation in casi speciali ed è ancora una gestione di basso livello.<br> | ||
+ | Vorremo un paradigma che permetta di scrivere ancor più facilmente programmi concorrenti e che siano allo stesso tempo semplicemente implementabili. | ||
+ | |||
+ | Il primo paradigma che vediamo è quello dei '''semafori''', proposto da Dijksta nel 1965. (P.S. quasi tutti gli appunti di Dijkstra sono stati scansionati e sono scaricabili da qui: http://www.cs.utexas.edu/users/EWD).<br> | ||
+ | |||
== Lezione del 20 ottobre 2017 == | == Lezione del 20 ottobre 2017 == | ||
+ | |||
+ | === Complementi sul linguaggio C e cenni sulle librerie standard === | ||
+ | * strutture con vettore di dimensione nulla come ultimo campo. | ||
+ | * funzioni a numero variabile di parametri | ||
+ | * formattazione (printf/scanf), uso avanzato | ||
+ | * allocazione dinamica | ||
+ | |||
+ | === Makefile con azioni automatiche === | ||
+ | |||
== Lezione del 24 ottobre 2017 == | == Lezione del 24 ottobre 2017 == | ||
+ | |||
+ | === Implementazione dei semafori nei sistemi operativi === | ||
+ | |||
+ | Un '''costrutto''' è un'entità del linguaggio che implementa un'astrazione. La semantica del semaforo può essere in larga parte descritta dal suo invariante: | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | nP <= nV + init | ||
+ | init + nV - nP >= 0 | ||
+ | /* init + nV - nP si dice "valore del semaforo" */ | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | La V si può fare in qualsiasi momento, la P invece può non essere possibile da completare (nP = numero operazioni P completate) perché incrementandola potrebbe rendere la disuguaglianza non vera. | ||
+ | |||
+ | Se eseguo V su un semaforo di valore 0, il valore del semaforo = +1 se eseguissi P il valore del semaforo diventerebbe -1, violando l'invariante. Quindi P può essere eseguito solo se il valore del semaforo > 0 e ridurrà il valore del semaforo di 1. | ||
+ | |||
+ | |||
+ | Una sezione critica può essere realizzata con un semaforo. Espressa con solo l'invariante non evitiamo la starvation. | ||
+ | <syntaxhighlight lang='c'> | ||
+ | semaphore s(1) | ||
+ | |||
+ | s.P() | ||
+ | critical code | ||
+ | s.V() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Iniziamo con un semaforo inizializzato a 0, A entra ma non può eseguire P, quindi subentra B che esegue V e da il segnale ad A in modo che A possa proseguire: | ||
+ | <syntaxhighlight lang='c'> | ||
+ | semaphore synch(0) | ||
+ | |||
+ | Process A: | ||
+ | //wait 4 B | ||
+ | synch.P() | ||
+ | .... | ||
+ | |||
+ | Process B: | ||
+ | //signal B | ||
+ | synch.V() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | '''Da ricordare''': | ||
+ | |||
+ | * Con i semafori nessuna protezione di variabile condivisa è automatica (occorre inserire sezioni critiche!) | ||
+ | * I semafori hanno memoria, nel senso che se mandiamo una V adesso e una P tra mezz'ora il semaforo ricorderà di aver fatto una V. Se non abbiamo necessità di una determinata segnalazione la dobbiamo rimuovere. | ||
+ | * Una V autorizza l'attivazione di un processo bloccato. Se facciamo una V allora uno dei processi bloccati partirà, ma non sappiamo quando. | ||
+ | * Un programma con almeno un semaforo che abbia solo operazioni P o solo operazioni V è errato. | ||
+ | * Se un testo chiede di usare semafori si usano SOLO semafori! | ||
+ | |||
+ | ==== Producer Consumer con semafori generali ==== | ||
+ | (o anche binari, in questo esempio è equivalente) | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | #include<stdio.h> | ||
+ | #include<pthread.h> | ||
+ | #include<semaphore.h> | ||
+ | |||
+ | semaphore full; | ||
+ | semaphore empty; | ||
+ | volatile int buffer; | ||
+ | /*aspetto un tempo random con massimo 2s e genero un intero, a questo punto | ||
+ | semaphore_P è empty, facendo in questo modo il produttore scopre che il buffer | ||
+ | è vuoto e lo dichiara non più vuoto, in questo momento non è né vuoto né pieno, | ||
+ | mette il valore nel buffer condiviso e setta semaphore_V a full permettendo al | ||
+ | consumatore di leggere.*/ | ||
+ | void *producer(void *arg) { | ||
+ | while (1) { | ||
+ | int value; | ||
+ | usleep(random() % 2000000); | ||
+ | value = random() % 32768; | ||
+ | printf("produced: %d\n",value); | ||
+ | semaphore_P(empty); | ||
+ | buffer = value; | ||
+ | semaphore_V(full); | ||
+ | printf("sent: %d\n",value); | ||
+ | } | ||
+ | } | ||
+ | /*il consumatore arriva a quel semaphore_P che è pieno quando può va avanti, si | ||
+ | copia in una variabile locale il buffer e semaphore_P diventa non pieno e non | ||
+ | vuoto. Stampa il valore e si mette "a dormire" per un tempo arbitrario.*/ | ||
+ | void *consumer(void *arg) { | ||
+ | while (1) { | ||
+ | int value; | ||
+ | printf("\t\tconsumer ready\n"); | ||
+ | semaphore_P(full); | ||
+ | value = buffer; | ||
+ | semaphore_V(empty); | ||
+ | printf("\t\tconsume %d\n", value); | ||
+ | usleep(random() % 2000000); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | int main(int argc, char *argv[]) { | ||
+ | pthread_t prod_t; | ||
+ | pthread_t cons_t; | ||
+ | full=semaphore_create(0); | ||
+ | empty=semaphore_create(1); | ||
+ | srandom(time(NULL)); | ||
+ | pthread_create(&prod_t, NULL, producer, NULL); | ||
+ | pthread_create(&cons_t, NULL, consumer, NULL); | ||
+ | pthread_join(prod_t, NULL); | ||
+ | pthread_join(cons_t, NULL); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Se avessi più produttori e consumatori il codice funzionerebbe lo stesso in quanto i semafori valgono al più 1 quindi solo 1 produttore supera il blocco. Stessa cosa per quanto riguarda il consumatore. | ||
+ | |||
+ | === Semafori Binari === | ||
+ | |||
+ | SEMAFORO BINARIO USANDO I GENERALI (precisamente 2): | ||
+ | |||
+ | Sono semafori che soddisfano il seguente invariante: | ||
+ | <syntaxhighlight lang='c'> | ||
+ | 0 <= init + nV - nP <= 1 | ||
+ | /* init + nV - nP si dice "valore del semaforo" */ | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Dopo aver presentato il paradigma dei semafori binari e quello dei semafori generali ci si è posti la seguente domanda: | ||
+ | tutti i problemi che si risolvono usando i semafori generali si possono risolvere usando invece i semafri binari? viceversa tutti i problemi che si risolvono con i semafori binari si possono risolvere con quelli generali? | ||
+ | Quale fra i due paradigmi è più espressivo? | ||
+ | |||
+ | Si può dimostrare che i due paradimi hanno lo stesso potere espressivo. | ||
+ | Per poter fare uqesta dimostrazione si deve essere in grado di implementare un tipo di semaforo servendosi dell'altro e viceversa. | ||
+ | |||
+ | <syntaxhighlight lang= 'c'> | ||
+ | bsem { | ||
+ | gsem pblock; //pblock si usa per indicare il valore del semaforo per cui la P si blocca | ||
+ | gsem vblock; //vblock si usa per inidicare il valore del semaforo per cui la V si blocca | ||
+ | } | ||
+ | |||
+ | binit(v): | ||
+ | bsem = malloc(struct bsem) | ||
+ | bsem.pblock.ginit(v) | ||
+ | bsem.vblock.ginit(1-v) | ||
+ | |||
+ | bP(): //bP sblocca vblock e blocca pblock | ||
+ | bsem.pblock.gP() //si tenta di chiamare la P prendendo pblock come parametro | ||
+ | bsem.vblock.gV() | ||
+ | |||
+ | bV(): //bV sblocca pblock e blocca vblock | ||
+ | bsem.pblock.gV() | ||
+ | bsem.vblock.gP() | ||
+ | //pblock e vblock hanno sempre valore opposto | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Una generalizzazione della sezione critica è quella di inizializzare il semaforo ad un numero positivo anche maggiore di 1, ovvero consentiamo a "n" numero di processi che può prosegurie dopo una P ovvero: | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | semaphore s(4) | ||
+ | |||
+ | s.P() | ||
+ | use resource | ||
+ | s.V() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Introduzione al problema della cena dei filosofi. === | ||
+ | |||
== Lezione del 27 ottobre 2017 == | == Lezione del 27 ottobre 2017 == | ||
+ | |||
+ | ===Il Sistema Operativo UNIX=== | ||
+ | |||
+ | Unix, fin dalla sua nascita, presentava già le maggiori caratteristiche che sono tutt’ora presenti: | ||
+ | * scritto in C; | ||
+ | * multitasking; | ||
+ | * prevede un’interfaccia di '''System Call''' | ||
+ | * l’interprete dei comandi non è interno al '''kernel''', ma è un processo a parte, con lo scopo di ''semplificare'' il kernel mettendo tutto ciò che fosse possibile al di fuori di questo; | ||
+ | * sistema operativo multi-user | ||
+ | * presenza di una '''SHELL''' | ||
+ | |||
+ | ---- | ||
+ | |||
+ | === Shell UNIX === | ||
+ | |||
+ | La SHELL è un interprete di comandi ed è chiamata così (shell = conchiglia) proprio perché è la parte esterna della struttura. | ||
+ | |||
+ | '''Caratteristiche principali''' | ||
+ | |||
+ | * storicamente, la prima shell fu '''sh''', detta anche ''[https://en.wikipedia.org/wiki/Bourne_shell Bourne Shell]''; | ||
+ | * è possibile utilizzare il kernel tramite la SHELL; | ||
+ | * essa stessa rappresenta un linguaggio di scripting; | ||
+ | * è possibile quindi scrivere degli '''script''', contenenti anche strutture di controllo proprie dei linguaggi di programmazione (''if'', ''else'', ''while'', ecc..); | ||
+ | * il simbolo '''$''' (dollaro) come prompt di solito indica che si sta usando la shell come ''utente'', questo previene l’esecuzione accidentale di programmi che potrebbero arrecare danni al sistema; | ||
+ | * il successore di ''sh'' sarà '''bash''' (Bourne-Again SHell). | ||
+ | |||
+ | '''Struttura di uno script Shell''' | ||
+ | |||
+ | [comandi] [complementi] [complementi oggetto] | ||
+ | |||
+ | '''Alcuni comandi della shell e strutture particolari''' | ||
+ | * '''grep''': ricerca contenuti testuali; | ||
+ | * '''cat''': copia ''stdin'' in ''stdout'' oppure ogni file che viene elencato ordinatamente in stdout, può quindi essere utilizzato per fare copie; | ||
+ | * '''ls''': mostra il contenuto di una directory; | ||
+ | * '''more''': nato per primo rispetto a '''less''' (funzionamento simile), utile per leggere file particolarmente lunghi mostrando una schermata per volta del file; | ||
+ | * '''less''': versione più evoluta di ''more'', chiamata così ironicamente, consente anche di tornare ad una "pagina" precedente, cosa che con il comando ''more'' non era possibile costringendo a dover scorrere tutto il file a partire dall’inizio se si desiderava leggere una pagina precedente; | ||
+ | * '''file''': indica di che tipo è il file, usando una combinazione di evidenze del SO e di '''euristiche''': tenta di dare delle classificazioni più dettagliate (ad esempio i file con estensione '''.c''' sono definiti come ''C Source Code''); | ||
+ | * '''chmod''': cambia i permessi di accesso ad un file | ||
+ | **per fare ciò, si considera il campo dei permessi come un '''numero ottale'''; | ||
+ | **es. rw+ rw+ rw- = 664 (write ad utente corrente e gruppo corrente, accesso solamente in lettura a chiunque altro; | ||
+ | **''sticky bit'': bit meno significativo che sostanzialmente indica che ogni utente è libero di fare ciò che vuole, ma solamente con i propri file; | ||
+ | * '''touch''': se il file specificato non esiste lo crea, in caso contrario imposta la data di modifica del file a quella corrente; | ||
+ | * '''passwd''': cambia la password dell’utente corrente, questo comando viene considerato sempre come lanciato da '''root'''; | ||
+ | * '''man''' comando: mostra il manuale del comando specificato; | ||
+ | * '''echo''' [string]: mette in output i parametri specificati, utilizzato negli script per stampare | ||
+ | * '''EXEC BASH''': ''exec'' sostituisce al processo corrente l’esecuzione del nuovo comando. Esempio a seguire. | ||
+ | |||
+ | ==== Link Utili ==== | ||
+ | [http://www.mimante.net/doc/comandi.txt Lista comandi shell UNIX] | ||
+ | |||
+ | [http://man.cat-v.org Archivio storico dei manuali di Unix] | ||
+ | |||
+ | ---- | ||
+ | |||
+ | === System Call === | ||
+ | [[Il ''catalogo'' delle System Call]] | ||
+ | |||
+ | ==== Esempio di echo scritto tramite systemcall ==== | ||
+ | Proviamo a scrivere il seguente script shell tramite le system call: | ||
+ | |||
+ | <syntaxhighlight lang='bash'> | ||
+ | echo "this is a test please discard" > test | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | |||
+ | #include <stdio.h> | ||
+ | #include <unistd.h> | ||
+ | #include <stdlib.h> | ||
+ | #include <sys/types.h> | ||
+ | #include <sys/wait.h> | ||
+ | #include <fcntl.h> | ||
+ | |||
+ | char *echoargv[] = {"/bin/echo", "this is a test please discard", NULL}; | ||
+ | int main(int argc, char const *argv[]) { | ||
+ | int status; | ||
+ | int fd; | ||
+ | switch (fork()) { | ||
+ | case 0: | ||
+ | /*inserire qui le modifiche al programma che vado a lanciare | ||
+ | come ad esempio la reindirizzazione dell'output | ||
+ | |||
+ | fd = open("test", O_WRONLY | O_CREAT | O_TRUNC, 0666); | ||
+ | dup2(fd, STDOUT_FILENO); | ||
+ | close(fd); | ||
+ | */ | ||
+ | execve("/bin/echo", echoargv, NULL); | ||
+ | exit(0); | ||
+ | default: | ||
+ | wait(&status); | ||
+ | break; | ||
+ | case -1: | ||
+ | fprintf(stderr, "exec err\n", ); | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Cosa succede se il figlio termina subito ed il padre aspetta 10 secondi prima di terminare? | ||
+ | * Il processo che ha eseguito echo rimane come <defunct> ovvero un processo zombie. | ||
+ | E se invece succede il contrario? | ||
+ | * Tutti gli orfani vengono adottati da init o systemd e verranno eseguiti e chiusi tutti. | ||
+ | |||
== Lezione del 31 ottobre 2017 == | == Lezione del 31 ottobre 2017 == | ||
+ | === Recap sui semafori === | ||
+ | Cos'è un semaforo?<br> | ||
+ | Un dato astratto che serve per gestire l'accesso alle risorse condivise. | ||
+ | |||
+ | Semaforo generale:<br> | ||
+ | P blocca in caso il semaforo sia a 0 (riduce di 1 il semaforo) | ||
+ | V non bloccante ed incrementa di 1 il valore del semaforo | ||
+ | |||
+ | Semaforo binario:<br> | ||
+ | P e V bloccanti, P in caso il semaforo sia 0 V in caso il semaforo sia 1. | ||
+ | |||
+ | Il valore del semaforo non può essere negativo. | ||
+ | |||
+ | Potere espressivo di un paradigma:<br> | ||
+ | L'insieme dei problemi che si possono risolvere (programmi che si possono scrivere) | ||
+ | |||
+ | Se ho un problema risolvibile con un semaforo binario può essere implementato con un semaforo generale e viceversa. Quindi semafori binari e generali hanno lo stesso problema espressivo. | ||
+ | |||
+ | === Implementazione di un semaforo binario === | ||
+ | Trasformiamo l'implementazione di un semaforo generale (visto in precedenza) in uno che implementa un semaforo binario. | ||
+ | <syntaxhighlight lang='c'> | ||
+ | #include<stdio.h> | ||
+ | #include<pthread.h> | ||
+ | #include<stdlib.h> | ||
+ | #include<unistd.h> | ||
+ | #include<suspend.h> | ||
+ | #include<tlist.h> | ||
+ | #include<semaphore.h> | ||
+ | |||
+ | #define mutex_in(X) pthread_mutex_lock(X) | ||
+ | #define mutex_out(X) pthread_mutex_unlock(X) | ||
+ | |||
+ | struct semaphore { | ||
+ | volatile long value; | ||
+ | pthread_mutex_t lock; | ||
+ | struct tlist *q; | ||
+ | }; | ||
+ | |||
+ | semaphore semaphore_create(long initval) { | ||
+ | semaphore s = malloc(sizeof(*s)); | ||
+ | if (s) { | ||
+ | s->value = initval; | ||
+ | s->q = NULL; | ||
+ | pthread_mutex_init(&s->lock, NULL); | ||
+ | } | ||
+ | return s; | ||
+ | } | ||
+ | |||
+ | void semaphore_destroy(semaphore s) { | ||
+ | pthread_mutex_destroy(&s->lock); | ||
+ | free(s); | ||
+ | } | ||
+ | |||
+ | void semaphore_P(semaphore s) { | ||
+ | mutex_in(&s->lock); | ||
+ | if (s->value <= 0) { | ||
+ | tlist_enqueue(&s->q, pthread_self()); | ||
+ | mutex_out(&s->lock); | ||
+ | suspend(); | ||
+ | } else { | ||
+ | if (tlist_empty(s->q)) | ||
+ | s->value--; | ||
+ | else | ||
+ | wakeup(tlist_dequeue(&s->q)); | ||
+ | mutex_out(&s->lock); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | void semaphore_V(semaphore s) { | ||
+ | mutex_in(&s->lock); | ||
+ | if (s->value >= 1){ | ||
+ | tlist_enqueue(&s->q, pthread_self()); | ||
+ | mutex_out(&s->lock); | ||
+ | suspend(); | ||
+ | } else { | ||
+ | if (tlist_empty(s->q)) | ||
+ | s->value++; | ||
+ | else | ||
+ | wakeup(tlist_dequeue(&s->q)); | ||
+ | mutex_out(&s->lock); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | === Implementazione 5 filosofi === | ||
+ | ==== Versione Errata ==== | ||
+ | <syntaxhighlight lang='c'> | ||
+ | #include<stdio.h> | ||
+ | #include<stdint.h> | ||
+ | #include<pthread.h> | ||
+ | #include<semaphore.h> | ||
+ | |||
+ | semaphore stick[5]; | ||
+ | char philo_status[]="TTTTT"; | ||
+ | |||
+ | void *philo(void *arg) { | ||
+ | int i = (uintptr_t)arg; | ||
+ | printf("philo thinking: %d\n",i); | ||
+ | while (1) { | ||
+ | usleep(random() % 200000); //thinking | ||
+ | semaphore_P(stick[i]); | ||
+ | //quando prendo una bacchetta ho un ritardo nel prendere la seconda. | ||
+ | usleep(100000); | ||
+ | //Evidenzia il deadlock presente in questa implementazione | ||
+ | semaphore_P(stick[(i+1)%5]); | ||
+ | philo_status[i] = 'E'; | ||
+ | printf("philo eating: %d |%s|\n",i,philo_status); | ||
+ | usleep(random() % 200000); //eating | ||
+ | philo_status[i] = 'T'; | ||
+ | printf("philo thinking: %d |%s|\n",i,philo_status); | ||
+ | semaphore_V(stick[i]); | ||
+ | semaphore_V(stick[(i+1)%5]); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | int main(int argc, char *argv[]) { | ||
+ | int i; | ||
+ | pthread_t philo_t[5]; | ||
+ | srandom(time(NULL)); | ||
+ | for (i=0; i<5; i++) | ||
+ | stick[i]=semaphore_create(1); | ||
+ | for (i=0; i<5; i++) | ||
+ | pthread_create(&philo_t[i], NULL, philo, (void *)(uintptr_t) i); | ||
+ | for (i=0; i<5; i++) | ||
+ | pthread_join(philo_t[i], NULL); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Versione corretta ==== | ||
+ | Il problema fondamentale è che tutti i filosofi potrebbero prendere una bacchetta allo stesso tempo. Se inseriamo un filosofo "mancino" ovvero che si muove in senso contrario rispetto agli altri, risolviamo il problema di deadlock circolare. | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | #include<stdio.h> | ||
+ | #include<stdint.h> | ||
+ | #include<pthread.h> | ||
+ | #include<semaphore.h> | ||
+ | |||
+ | #define MIN(X,Y) ((X)<(Y)) ? (X) : (Y) | ||
+ | #define MAX(X,Y) ((X)>(Y)) ? (X) : (Y) | ||
+ | |||
+ | semaphore stick[5]; | ||
+ | char philo_status[]="TTTTT"; | ||
+ | |||
+ | void *philo(void *arg) { | ||
+ | int i = (uintptr_t)arg; | ||
+ | printf("philo thinking: %d\n",i); | ||
+ | while (1) { | ||
+ | usleep(random() % 200000); //thinking | ||
+ | semaphore_P(stick[MIN(i, (i + 1) % 5)]); | ||
+ | //quando prendo una bacchetta ho un ritardo nel prendere la seconda. | ||
+ | usleep(100000); | ||
+ | //Evidenzia il deadlock presente in questa implementazione | ||
+ | semaphore_P(stick[MAX(i, (i + 1) % 5)]); | ||
+ | philo_status[i] = 'E'; | ||
+ | printf("philo eating: %d |%s|\n", i, philo_status); | ||
+ | usleep(random() % 200000); //eating | ||
+ | philo_status[i] = 'T'; | ||
+ | printf("philo thinking: %d |%s|\n", i, philo_status); | ||
+ | semaphore_V(stick[i]); | ||
+ | semaphore_V(stick[(i+1)%5]); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | int main(int argc, char *argv[]) { | ||
+ | int i; | ||
+ | pthread_t philo_t[5]; | ||
+ | srandom(time(NULL)); | ||
+ | for (i=0; i<5; i++) | ||
+ | stick[i]=semaphore_create(1); | ||
+ | for (i=0; i<5; i++) | ||
+ | pthread_create(&philo_t[i], NULL, philo, (void *)(uintptr_t) i); | ||
+ | for (i=0; i<5; i++) | ||
+ | pthread_join(philo_t[i], NULL); | ||
+ | } | ||
+ | |||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Scrittura alternativa === | ||
+ | |||
+ | Proviamo a scrivere con la seguente scrittura una "non" soluzione della cena dei filosofi. | ||
+ | <syntaxhighlight lang='c'> | ||
+ | <Si> | ||
+ | |||
+ | <await (Ci) -> Ti> | ||
+ | |||
+ | S.P() | ||
+ | <await (S.value > 0) -> S.value --> | ||
+ | |||
+ | S.V() | ||
+ | <S.value++> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Soluzione basata sullo stato del precedente (quella che consente la congiura dei filosofi). | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | Philo(i): | ||
+ | while(1): | ||
+ | /*think*/ | ||
+ | <await(status[(i+4)%5] == 'T' && status[(i+1)%5] == 'T') -> status[i] = 'E'> | ||
+ | /*eat*/ | ||
+ | <status[i] = 'T'> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Come trasformo questa scrittura in un tentativo di soluzione con semafori?<br> | ||
+ | |||
+ | *Ingredienti: | ||
+ | **mutex | ||
+ | **1 await semaphore Si | ||
+ | **waiting procs counter per await Wi | ||
+ | |||
+ | *Quando incontriamo: | ||
+ | **<Si>: mutex.P(); | ||
+ | **Si : SIGNAL | ||
+ | |||
+ | Avremo quindi: | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | <await (Ci) -> Ti> : mutex.P(); | ||
+ | if(!Ci) { | ||
+ | Wi++; | ||
+ | mutex.P(); | ||
+ | Si.P(); | ||
+ | Wi--; | ||
+ | } | ||
+ | Ti; | ||
+ | SIGNAL; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | Com'è fatto SIGNAL? | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | if (C1 && W1 > 0) S1.W; | ||
+ | nondet_else (C2 && W2 > 0) S2.W; | ||
+ | nondet_else (C3 && W3 > 0) S3.W; | ||
+ | ... | ||
+ | else mutex.V(); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Pseudocodice: | ||
+ | <syntaxhighlight lang='c'> | ||
+ | |||
+ | void signal(void){ | ||
+ | int i; | ||
+ | for(i = 0; i <5; i++){ | ||
+ | if (status[(i+4)%5] != 'I' || status[(i+1)%5] != 'I' && waiting[i] > 0) { | ||
+ | semaphore_V(waitsem[i]); | ||
+ | return; | ||
+ | } | ||
+ | } | ||
+ | semaphore_V(mutex); | ||
+ | } | ||
+ | |||
+ | process philo(i): | ||
+ | while(1) { | ||
+ | semaphore_P(mutex); | ||
+ | if (status[(i+4)%5] != 'I' || status[(i+1)%5] != 'I') { | ||
+ | waiting[i]++; | ||
+ | semaphore_P(waitsem[i]); | ||
+ | waiting[i]--; | ||
+ | } | ||
+ | status[i] = 'E'; | ||
+ | signal(); | ||
+ | // eat | ||
+ | semaphore_P(mutex); | ||
+ | status[i] = 'I'; | ||
+ | signal(); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | [http://www.sciencedirect.com/science/article/pii/0167642389900130# Articolo di Gregory R. Andrews "A method for solving synchronization problems"]. Contiene la spiegazione del metodo '''passing le baton''' sopra illustrato più alcuni approfondimenti. | ||
+ | |||
== Lezione del 3 novembre 2017 == | == Lezione del 3 novembre 2017 == | ||
+ | |||
+ | System call : Creare l'astrazione di processo | ||
+ | Fork: crea processi senza eseguire programmi a somiglianza di quello generante | ||
+ | _exit : termina i processi immediatamente | ||
+ | |||
+ | Tutti i processi vengono uccisi o si "suicidano" (sistemi operativi è una materia triste) | ||
+ | |||
+ | Il main non è lo starting point di esecuzione dei programmi ma è questo "guscio" formato da: | ||
+ | |||
+ | set-up argc argv | ||
+ | ret_value = main(arc,argv) | ||
+ | exit(ret_value) | ||
+ | |||
+ | |||
+ | atexit(): caricare dei puntatori a funzioni che vengono richiamate quando il programma termina | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | int main (int argc, char *argv[]){ | ||
+ | pid_t pid; | ||
+ | int status; | ||
+ | switch (pid = fork()){ | ||
+ | case 0: //child | ||
+ | printf(child ) | ||
+ | int i = 0; | ||
+ | i = 10 /i; | ||
+ | _exit (42) //usa i bit di 42 come exit status | ||
+ | break; | ||
+ | default: | ||
+ | waitpid(pid,&status,0); | ||
+ | printf("exit status = %d\n",WEXITSTATUS(status)); | ||
+ | printf("natural death? = %d\n",WIFEEXITED(status)); //per vedere se un processo è morto "naturalmente" | ||
+ | printf("abort= %d\n",WIFSIGNALED(status)); | ||
+ | case -1: | ||
+ | printf("fork error\n") | ||
+ | break;} | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Segnali | ||
+ | 8 = floating point exception | ||
+ | 11 = segmentation fault | ||
+ | ----------------------------------------- | ||
+ | <syntaxhighlight lang='c'> | ||
+ | int main (int argc, char *argv[]){ | ||
+ | pid_t pid; | ||
+ | int status; | ||
+ | switch (pid = fork()){ | ||
+ | case 0: //child | ||
+ | printf("child\n" ) | ||
+ | int *i = (int *)1; | ||
+ | *i = *i +1; | ||
+ | _exit (42) //usa i bit di 42 come exit status | ||
+ | break; | ||
+ | default: | ||
+ | waitpid(pid,&status,0); | ||
+ | printf("exit status = %d\n",WEXITSTATUS(status)); | ||
+ | printf("natural death? = %d\n",WIFEEXITED(status)); //per vedere se è morto "naturalmente" | ||
+ | printf("abort? = %d\n",WIFSIGNALED(status)); | ||
+ | printf("signal? = %d\n",WTERMSIG(status)); | ||
+ | |||
+ | case -1: | ||
+ | printf("fork error\n") | ||
+ | break;} | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Vita dei processi : | ||
+ | -fork() | ||
+ | -exit() | ||
+ | -wait() | ||
+ | |||
+ | uso di file: open, close, read, write, lseek, fcntl, ioctl, pread, pwrite, readv, writev | ||
+ | |||
== Lezione del 7 novembre 2017 == | == Lezione del 7 novembre 2017 == | ||
+ | |||
+ | === Recap lezione precedente === | ||
+ | <syntaxhighlight lang='c'> | ||
+ | < T > | ||
+ | < await C -> S > | ||
+ | |||
+ | ----------------- | ||
+ | |||
+ | < Tj > ===> mutex.P(); | ||
+ | Tj | ||
+ | SIGNAL(); | ||
+ | |||
+ | < await Ci -> Ui > ===> mutex.P(); | ||
+ | if (!C) { | ||
+ | wi++; | ||
+ | mutex.V() | ||
+ | Si.P() | ||
+ | wi--; | ||
+ | } | ||
+ | Ui | ||
+ | SIGNAL | ||
+ | |||
+ | SIGNAL ===> if (C1 && V1 > 0) S1.V(); | ||
+ | nondet_else (C2 && V2 > 0) S1.V(); | ||
+ | . . . | ||
+ | else mutex.V(); | ||
+ | </syntaxhighlight> | ||
+ | === Problema Scrittori e Lettori === | ||
+ | ==== Pseudocodice ==== | ||
+ | Esistono processi scrittori e lettori. I lettori vogliono leffere una struttura dati, gli scrittori vogliono modificarla. Il problema nasce quando un processo vuole modificare la struttura. | ||
+ | <syntaxhighlight lang='c'> | ||
+ | while(1) | ||
+ | startread() | ||
+ | nr++ | ||
+ | READ | ||
+ | nr-- | ||
+ | endread() | ||
+ | |||
+ | while(1) | ||
+ | startwrite() | ||
+ | nw++ | ||
+ | WRITE | ||
+ | nw-- | ||
+ | endwrite() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Condizioni accettabili: | ||
+ | <syntaxhighlight lang='c'> | ||
+ | (nr == 0 && nw == 0) || (nr > 0 && nw == 0) || (nr == 0 && nw == 1) | ||
+ | |||
+ | oppure | ||
+ | (nw == 0 && nr >= 0) || (nr == 0 && nr == 1) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | //lettore | ||
+ | while (1) | ||
+ | <await nw == 0 -> nr++> | ||
+ | READ | ||
+ | <nr--> | ||
+ | |||
+ | //scrittore | ||
+ | while (1) | ||
+ | <await nr == 0 && nw == 0 -> nw++> | ||
+ | WRITE | ||
+ | <nw--> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Primo passaggio di trasformazione meccanica: | ||
+ | <syntaxhighlight lang='c'> | ||
+ | //lettore | ||
+ | while (1) | ||
+ | mutex.P() | ||
+ | if (nw > 0) { | ||
+ | wr++; //waiting readers | ||
+ | mutex.V(); | ||
+ | Sr.P(); | ||
+ | wr--; | ||
+ | } | ||
+ | nr++; | ||
+ | SIGNAL; | ||
+ | READ | ||
+ | mutex.P() | ||
+ | nr--; | ||
+ | SIGNAL | ||
+ | |||
+ | //scrittore | ||
+ | while (1) | ||
+ | mutex.P() | ||
+ | if (nr > 0 || nw > 0) { | ||
+ | ww++; //waiting writers | ||
+ | mutex.V(); | ||
+ | Sw.P(); | ||
+ | ww--; | ||
+ | } | ||
+ | nw++; | ||
+ | SIGNAL; | ||
+ | WRITE | ||
+ | mutex.P() | ||
+ | nw--; | ||
+ | SIGNAL; | ||
+ | |||
+ | SIGNAL: | ||
+ | if (nw == 0 && wr > 0) Sr.V(); | ||
+ | nondet_else (nw == 0 && nr == 0 && ww > 0) Sw.V(); | ||
+ | else mutex.V(); | ||
+ | |||
+ | mutex: | ||
+ | semaphore mutex(1); | ||
+ | semaphore Sr(0), Sw(0); | ||
+ | int nr, nw, wr, ww; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | //Starvation per i lettori che vanno sempre avanti e gli scrittori che attendono sempre. | ||
+ | SIGNAL: | ||
+ | if (nw == 0 && wr > 0) Sr.V(); | ||
+ | /*nondet_*/else (nw == 0 && nr == 0 && ww > 0) Sw.V(); | ||
+ | else mutex.V(); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | //Starvation per entrambi | ||
+ | SIGNAL: | ||
+ | if (nw == 0 && nr == 0 && ww > 0) Sw.V(); | ||
+ | /*nondet_*/else (nw == 0 && wr > 0) Sr.V(); | ||
+ | else mutex.V(); | ||
+ | </syntaxhighlight> | ||
+ | ==== Codice ==== | ||
+ | <syntaxhighlight lang='c'> | ||
+ | #include<stdio.h> | ||
+ | #include<stdint.h> | ||
+ | #include<pthread.h> | ||
+ | #include<semaphore.h> | ||
+ | |||
+ | #define NREADERS = 5; | ||
+ | #define NWRITERS = 5; | ||
+ | |||
+ | |||
+ | int nr, nw, wr, ww; | ||
+ | semaphore mutex; | ||
+ | semaphore ok2read; | ||
+ | semaphore ok2write; | ||
+ | |||
+ | void signal(void) { | ||
+ | if (nw == 0 && wr > 0) semaphore_V(ok2read); | ||
+ | if (nr == 0 && nw == 0 && ww > 0) semaphore_V(ok2write); | ||
+ | semaphore_V(mutex); | ||
+ | } | ||
+ | |||
+ | void startread(void) { | ||
+ | semaphore_P(mutex); | ||
+ | if (nw > 0) { | ||
+ | wr++; | ||
+ | semaphore_V(mutex); | ||
+ | semaphore_P(ok2read); | ||
+ | wr--; | ||
+ | } | ||
+ | nr++; | ||
+ | signal() | ||
+ | } | ||
+ | |||
+ | void endread(void) { | ||
+ | semaphore_P(mutex); | ||
+ | nr--; | ||
+ | signal() | ||
+ | } | ||
+ | |||
+ | void startwrite(void) { | ||
+ | semaphore_P(mutex); | ||
+ | if (nr > 0 || nw > 0) { | ||
+ | ww++; | ||
+ | semaphore_V(mutex); | ||
+ | semaphore_P(ok2write); | ||
+ | ww--; | ||
+ | } | ||
+ | nw++; | ||
+ | signal() | ||
+ | } | ||
+ | |||
+ | void endwrite(void) { | ||
+ | semaphore_P(mutex); | ||
+ | nw--; | ||
+ | signal(); | ||
+ | } | ||
+ | |||
+ | |||
+ | void *reader(void *arg) { | ||
+ | int i = (uintptr_t)arg; | ||
+ | while(1){ | ||
+ | usleep(random() %200000); | ||
+ | startreade(); | ||
+ | printf("nr %d nw %d -- wr %d ww %d \n", nr, nw, wr, ww ); | ||
+ | usleep(random() %200000); | ||
+ | /*READ*/ | ||
+ | endread(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | void *writer(void *arg) { | ||
+ | int i = (uintptr_t)arg; | ||
+ | while(1){ | ||
+ | usleep(random() %200000); | ||
+ | startwrite(); | ||
+ | printf("nr %d nw %d -- wr %d ww %d \n", nr, nw, wr, ww ); | ||
+ | /*READ*/ | ||
+ | usleep(random() %200000); | ||
+ | endwrite(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | int main(int argc, char *argv[]) { | ||
+ | int i; | ||
+ | pthread_t treader[NREADERS]; | ||
+ | pthread_t twriter[NWRITERS]; | ||
+ | srandom(time(NULL)); | ||
+ | mutex = semaphore_create(1); | ||
+ | ok2read = semaphore_create(0); | ||
+ | ok2write = semaphore_create(0); | ||
+ | for (i=0; i<NREADERS; i++) | ||
+ | pthread_create(&treader[i], NULL, treader, (void *)(uintptr_t) i); | ||
+ | for (i=0; i<NWRITERS; i++) | ||
+ | pthread_create(&treader[i], NULL, twriter, (void *)(uintptr_t) i); | ||
+ | for (i=0; i<5; i++) | ||
+ | pthread_join(treader[i], NULL); | ||
+ | for (i=0; i<5; i++) | ||
+ | pthread_join(twriter[i], NULL); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Programma che stampa ITALIA con semafori === | ||
+ | <syntaxhighlight lang='c'> | ||
+ | #include<stdio.h> | ||
+ | #include<stdint.h> | ||
+ | #include<pthread.h> | ||
+ | #include<semaphore.h> | ||
+ | |||
+ | |||
+ | char chars[] = {'I', 'T', 'A', 'L', '\n'}; | ||
+ | #define NPROC sizeof(chars) | ||
+ | char seq[] = {0, 1, 2, 3, 0, 2, 4}; | ||
+ | #define NSEQ sizeof(seq) | ||
+ | semaphore sem[NPROC]; | ||
+ | int k = 0; | ||
+ | |||
+ | void *printchar(void *argv) { | ||
+ | uintptr_t i = (uintptr_t) argv; | ||
+ | while (1){ | ||
+ | semaphore_P(sem[i]); | ||
+ | putchar(chars[i]); | ||
+ | k = (k + 1) %NSEQ; | ||
+ | semaphore_V(sem[seq[k]]); | ||
+ | usleep(random() %100000); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | int main(int argc, char *argv[]) { | ||
+ | int i; | ||
+ | pthread_t tchars[NPROC]; | ||
+ | srandom(time(NULL)); | ||
+ | sem[0] = semaphore_create(1); | ||
+ | for (i=1; i<NPROC; i++) | ||
+ | sem[i] = semaphore_create(0); | ||
+ | for (i=0; i<NPROC; i++) | ||
+ | pthread_create(&tchars[i], NULL, printchar, (void *)(uintptr_t) i); | ||
+ | for (i=0; i<NPROC; i++) | ||
+ | pthread_join(tchars[i], NULL); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Soluzione possibile esercizio c.1 2017-09-11 === | ||
+ | <syntaxhighlight lang='c'> | ||
+ | /* | ||
+ | esame 2017-09-11 | ||
+ | esercizio c.1 | ||
+ | */ | ||
+ | |||
+ | enum DIRECTION{N, E, S, W}; | ||
+ | |||
+ | int wv[4]; //waiting veicle | ||
+ | int nv; //numero veicoli | ||
+ | semaphore ok2enter[4]; | ||
+ | semaphore mutex; | ||
+ | |||
+ | crossing_enter(enum DIRECTION d){ | ||
+ | semaphore_P(mutex); | ||
+ | nv++; | ||
+ | if (nv > 0){ | ||
+ | wv[d]++; | ||
+ | semaphore_V(mutex); | ||
+ | semaphore_P(ok2enter[d]); | ||
+ | wv[d]--; | ||
+ | } | ||
+ | else | ||
+ | semaphore_V(mutex); | ||
+ | } | ||
+ | |||
+ | crossing_exit(enum DIRECTION d){ | ||
+ | semaphore_P(mutex); | ||
+ | nv--; | ||
+ | if (nv == 0) | ||
+ | semaphore_V(mutex); | ||
+ | else | ||
+ | for(i = 1; i < 5; i++){ | ||
+ | newdir = (d + i) % 4; | ||
+ | if (wv[newdir] > 0) { | ||
+ | semaphore_V(ok2enter[newdir]); | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | main() { | ||
+ | ok2enter[i] = semaphore_create(0); | ||
+ | mutex = semaphore_create(1); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
== Lezione del 10 novembre 2017 == | == Lezione del 10 novembre 2017 == | ||
+ | |||
+ | Lettura di directory: getdents (la syscall) e' scomodissima, quinndi si usa opendir/readdir/rewinddir/closedir oppure scandir | ||
+ | |||
+ | System call per il file system: mkdir, rmdir, chdir, getcwd, link, symlink, stat (fstat, lstat), chown (fchown, lchown), chmod (fchmod). | ||
+ | |||
+ | fchdir per cambiare temporaneamente dir. | ||
+ | |||
+ | System call per programmazione a eventi: select, poll | ||
+ | |||
+ | Script: sintassi sharp-bang (#!) per la scelta dell'interprete. | ||
+ | |||
+ | Variabili locale e di ambiente nelle shell. | ||
+ | |||
+ | Esercizio: | ||
+ | Un programma che dato come parametro una directory ritorni ciò che vi è all'interno | ||
+ | e nelle sottodirectory interne | ||
+ | |||
+ | Esempio da terminale : find /tmp -print | ||
+ | Tramite le seguanti system call: | ||
+ | |||
+ | * scandir() = Filtra gli elementi.Ritorna un intero,il numero delle entry. Ogni singolo elemento è in allocazione dinamica | ||
+ | * readdir() = Si accede a tutti gli elementi,li leggo uno alla volta | ||
+ | |||
+ | Come scriviamo il programma usando una funzione ricorsiva? | ||
+ | |||
+ | <syntaxhighlight lang='c'> | ||
+ | #include <stdio.h> | ||
+ | #include <stdlib.h> | ||
+ | #include <string.h> | ||
+ | #include <dirent.h> | ||
+ | |||
+ | int notdots(const struct dirent *d) { | ||
+ | if (strcmp(d->d_name,".") == 0) | ||
+ | return 0; | ||
+ | if (strcmp(d->d_name,"..") == 0) | ||
+ | return 0; | ||
+ | return 1; | ||
+ | } | ||
+ | |||
+ | void recscan(char *path) | ||
+ | { | ||
+ | struct dirent **namelist = NULL; | ||
+ | int i, n; | ||
+ | |||
+ | n = scandir(path, &namelist, notdots, alphasort); | ||
+ | if (n == -1) { | ||
+ | perror("scandir"); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | |||
+ | for (i=0; i<n; i++) { | ||
+ | size_t pathlen = strlen(path) + strlen(namelist[i]->d_name) + 2; | ||
+ | char newpath[pathlen]; | ||
+ | snprintf(newpath, pathlen, "%s/%s", path, namelist[i]->d_name); | ||
+ | printf("%s\n", newpath); | ||
+ | if (namelist[i]->d_type == DT_DIR) | ||
+ | recscan(newpath); | ||
+ | free(namelist[i]); | ||
+ | } | ||
+ | free(namelist); | ||
+ | } | ||
+ | |||
+ | int main(int argc, char *argv[]) { | ||
+ | char *path; | ||
+ | path = (argc > 1) ? argv[1] : "."; | ||
+ | recscan(path); | ||
+ | exit(EXIT_SUCCESS); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Per confronto, il programma seguente usa opendir/readdir/closedir al posto di scandir: | ||
+ | <syntaxhighlight lang='c'> | ||
+ | #include <dirent.h> | ||
+ | #include <stdio.h> | ||
+ | #include <stdlib.h> | ||
+ | #include <string.h> | ||
+ | |||
+ | int notdots(const struct dirent *d) { | ||
+ | if (strcmp(d->d_name,".") == 0) | ||
+ | return 0; | ||
+ | if (strcmp(d->d_name,"..") == 0) | ||
+ | return 0; | ||
+ | return 1; | ||
+ | } | ||
+ | |||
+ | void recreaddir(char *path) | ||
+ | { | ||
+ | DIR *d; | ||
+ | struct dirent *de; | ||
+ | int i, n; | ||
+ | |||
+ | d = opendir(path); | ||
+ | if (d == NULL) { | ||
+ | perror("opedir"); | ||
+ | exit(EXIT_FAILURE); | ||
+ | } | ||
+ | |||
+ | while ((de = readdir(d)) != NULL) { | ||
+ | if (notdots(de)) { | ||
+ | size_t pathlen = strlen(path) + strlen(de->d_name) + 2; | ||
+ | char newpath[pathlen]; | ||
+ | snprintf(newpath, pathlen, "%s/%s", path, de->d_name); | ||
+ | printf("%s\n", newpath); | ||
+ | if (de->d_type == DT_DIR) | ||
+ | recreaddir(newpath); | ||
+ | } | ||
+ | } | ||
+ | closedir(d); | ||
+ | } | ||
+ | |||
+ | int main(int argc, char *argv[]) { | ||
+ | char *path; | ||
+ | path = (argc > 1) ? argv[1] : "."; | ||
+ | recreaddir(path); | ||
+ | exit(EXIT_SUCCESS); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | System call per accedere ai file: | ||
+ | -Open() | ||
+ | -Close() | ||
+ | File descriptor: 0(stdin) 1(stderr) 2(stdout) | ||
+ | File: | ||
+ | -lseek() = Imposta l'indicatore d posizione del file | ||
+ | -ioctl() = Control device.Con i valori di tag si sceglie l'operazione voluta | ||
+ | |||
+ | File System: | ||
+ | -mkdir = Path e permessi di accesso | ||
+ | -rmdir = Cancella directory purchè sia vuota | ||
+ | -chmod = Modifica i permessi a file e directory.Richiede il pathname | ||
+ | -chown = Cambia il proprietario del file | ||
+ | -stat = Tutte le informazioni di un file(device su cui è memorizzato,tipo,quanti nodi ha il file,proprietario,blocco sui cuiè memorizzato,tre tempi : ultimo accesso,modificazione e cambiamento di stato[creazione del file]) | ||
+ | IL NOME NON FA PARTE DELLE INFO DI UN FILE!! | ||
+ | Inode number : rappresenta l'id di un file | ||
+ | |||
+ | Varianti con prefisso f : servono per i file già aperti (hanno come parametro il file descriptor e non già il pathname). | ||
+ | * fchmod | ||
+ | * fchown | ||
+ | |||
+ | Link simbolico: Un collegamento simbolico è un file contenente un percorso relativo od assoluto al file o directory a cui fa riferimento | ||
+ | |||
+ | Varianti con prefisso l sono definite come chiamate opache | ||
+ | Esempio | ||
+ | * lchown = Come una chown ma non deferenzia il link simbolico | ||
+ | |||
+ | Non esiste una system call per cancellare i file,ma solo quella per togliere il NOME al file,ovvero unlink() | ||
+ | Fino a quando il file è aperto,esso può esistere anche senza nome | ||
+ | es: file temporanei | ||
+ | |||
+ | * access = Chiede se si ha il permesso per eseguire una operazione di apertura di un file o directory (si può leggere, scrivere eseguire?) | ||
+ | * chdir = Cambia directory.La directory corrente è rappresentata dal parametro di un processo | ||
+ | * fchdir = Cambia directory usando come parametro un file aperto.Può servire,per esempio,per saltare in una directory e poi tornare indietro | ||
+ | |||
+ | * mount = Rendere accessibile un contenuto (Device).Unico albero di file sistem,deve venire esteso per i device che vogliamo "montare".Di solito l'operazione avviene in una cartella vuota. | ||
+ | * synch = Non ha parametri e sembra non fare nulla ma serve a svuotare le chache | ||
+ | * umask = Assegna il parametro per indicare la modalità di protezione dei file | ||
+ | * chroot = Ridefinisce la root. Crea una Security cage che non è annullabile. | ||
+ | * truncate = Decidere la lunghezza di un file | ||
+ | |||
+ | Programmazione ad eventi : main event loop. Si ferma attendendo un evento (es. click del mouse) e a seconda di esso agisce di conseguenza | ||
+ | Le system call per realizzare la programmazione ad eventi sono poll e select (con le varianti ppoll e pselect che vedremo in seguito). | ||
+ | |||
+ | Programma di chat tra due terminali | ||
+ | <syntaxhighlight lang='c'> | ||
+ | /* | ||
+ | Per eseguire il programma aprire due terminali ed eseguire i comandi | ||
+ | ./chat f1 f2 | ||
+ | ./chat f2 f1 | ||
+ | */ | ||
+ | |||
+ | #include <stdio.h> | ||
+ | #include <unistd.h> | ||
+ | #include <fcntl.h> | ||
+ | #include <poll.h> | ||
+ | #define BUFSIZE 1024; | ||
+ | char buf [BUFSIZE]; | ||
+ | |||
+ | int main (int arc char *argv[]){ | ||
+ | fd_in = open(argv[1], O_RDONLY | O_NONBLOCK); | ||
+ | fd_out = open(argv[2], O_WRONLY | O_NONBLOCK); | ||
+ | struct pollfd myfds[] = {{STDIN_FILENO, POLLIN}, {fd_in, POLLIN}}; | ||
+ | while (1){ | ||
+ | int n = poll(myfds, 2 , -1); | ||
+ | if (myfds[0].revents & POLLIN){ | ||
+ | int nn = read(STDIN_FILENO, buf, BUFSIZE); | ||
+ | write(fd_out, buf, nn); | ||
+ | } | ||
+ | if (myfds[1].revents & POLLIN){ | ||
+ | int nn = read(fd_in, buf, BUFSIZE); | ||
+ | write(STDOUT_FILENO, buf, nn); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | </syntaxhighlight> | ||
+ | |||
+ | * pipe() = Fa comunicare i processi tra loro.Restituisce due descrittori aperti (che sono gli estremi del "tubo") | ||
+ | |||
== Lezione del 14 novembre 2017 == | == Lezione del 14 novembre 2017 == | ||
+ | |||
+ | ===Definizione di Monitor=== | ||
+ | Un contenitore che contiene all'interno le variabili (del monitor) e delle funzioni, è simile ad una classe ma con delle differenze: | ||
+ | * le variabili del monitor sono normalmente accedute in mutua esclusione (i processi aspettano il loro turno per accedere) | ||
+ | * prevede le variabili di condizione, e due metodi: '''wait''' e '''signal'''; se un processo chiama wait viene sospeso, signal riattiva il processo | ||
+ | |||
+ | ===Soluzione al problema del buffer limitato con i monitor=== | ||
+ | <syntaxhighlight lang="c"> | ||
+ | monitor m { | ||
+ | variables... | ||
+ | condition c1, c2.... | ||
+ | procedure entry function... | ||
+ | function... | ||
+ | } | ||
+ | |||
+ | process producer(i). i = 1,...,N: | ||
+ | while(1) { | ||
+ | x = produce(); // | ||
+ | bb.enqueue(x); | ||
+ | } | ||
+ | |||
+ | process consumer(i). i = 1,...,M: | ||
+ | while(1) { | ||
+ | y = bb.dequeue(); // | ||
+ | consume(y); | ||
+ | } | ||
+ | |||
+ | monitor bb { | ||
+ | queue_of_T buffer; | ||
+ | condition ok2produce; // buffer.lenght() < SIZE | ||
+ | condition ok2consume; // buffer.lenght() > 0 | ||
+ | |||
+ | procedure entry void enqueue(T el) { | ||
+ | if (buffer.lenght() >= SIZE) | ||
+ | ok2produce.wait(); | ||
+ | buffer.enqueue(el); | ||
+ | ok2consume.signal(); | ||
+ | } | ||
+ | |||
+ | procedure entry Y dequeue(void) { | ||
+ | T el; | ||
+ | if (buffer.lenght() <= 0) | ||
+ | ok2consume.wait(); | ||
+ | el = buffer.dequeue(); | ||
+ | ok2produce.signal(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | ===Uso della libreria C fornita per le esercitazioni sui monitor=== | ||
+ | |||
+ | |||
+ | ===Dimostrazione dell'equivalenza di potere espressivo fra monitor e semafori=== | ||
+ | <syntaxhighlight lang="c"> | ||
+ | monitor semaphore { | ||
+ | int value; | ||
+ | condition ok2P; // value > 0 | ||
+ | |||
+ | procedure entry P() { | ||
+ | if (value <= 0) ok2P.wait(); | ||
+ | value --; | ||
+ | } | ||
+ | |||
+ | procedure entry V() { | ||
+ | value++; | ||
+ | ok2P.signal(); | ||
+ | } | ||
+ | |||
+ | semaphore(int i) { // constructor | ||
+ | value = i; | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | P e V dei semafori vs wait e signal: | ||
+ | * la wait è sempre bloccante la P solo se è 0 | ||
+ | * la signal se c'è qualcuno va avanti altrimenti va perduta la V | ||
+ | * con la V il processo ha il diritto di continuare quanto ne ha voglia, con la signal il processo viene ripreso immediatamente | ||
+ | |||
+ | |||
+ | ====Decalogo==== | ||
+ | * mai condizioni con solo signal o solo wait | ||
+ | * se monitor allora solo monitor | ||
+ | * busy wait in monitor = deadlock | ||
+ | * se tutte le procedure entry iniziano con wait → deadlock | ||
+ | * tutti i dati condivisi del monitor devono essere nel monitor (non condivise) per evitare race condition | ||
+ | |||
+ | |||
+ | Dati i semafori implementare i monitor: | ||
+ | <syntaxhighlight lang="c"> | ||
+ | class monitor { | ||
+ | semaphore mutex = 1; | ||
+ | stack_of_sem urgent; | ||
+ | enter(); | ||
+ | exit(); | ||
+ | } | ||
+ | |||
+ | class condition { | ||
+ | condition(monitor m); | ||
+ | monitor m; | ||
+ | queue_of_sem qs; | ||
+ | wait(); | ||
+ | signal(); | ||
+ | } | ||
+ | |||
+ | monitor::enter(){ | ||
+ | self.mutex.P(); | ||
+ | } | ||
+ | |||
+ | monitor::exit(){ | ||
+ | if (!self.urgent.empty()) | ||
+ | urgent.pop().V(); | ||
+ | else | ||
+ | self.mutex.V(); | ||
+ | } | ||
+ | |||
+ | condition::condition(monitor m) {self.m = m} | ||
+ | |||
+ | condition_wait(condition C){ | ||
+ | semaphore s = new semaphore; | ||
+ | self.qs.enqueue(s); | ||
+ | self.m.exit(); | ||
+ | s.P(); | ||
+ | } | ||
+ | |||
+ | condition_signal(condition C){ | ||
+ | if (! self.qs.empty()) { | ||
+ | semaphore s = new semaphore; | ||
+ | self.m.urgent.push(s); | ||
+ | self.qs.dequeue().V(); | ||
+ | s.P(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | ===Soluzione del problema dei 5 filosofi con i monitor=== | ||
+ | <syntaxhighlight lang="c"> | ||
+ | monitor philom { | ||
+ | bool busystick[5]; | ||
+ | condition okstick[i]; //busystick = false | ||
+ | |||
+ | procedure entry void starteat(int i) { | ||
+ | if (busystick[i]) okstick[i].wait(); | ||
+ | busystick[i] = true; | ||
+ | if (busystick[(i+1)%5]) okstick[i+1]%5].wait(); | ||
+ | busystick[(i+1) % 5] = true; | ||
+ | } | ||
+ | |||
+ | procedure entry void endeat(int i) { | ||
+ | busystick[i] = false | ||
+ | busystick[(i+1) % 5] = false; | ||
+ | okstick[i].signal; | ||
+ | okstick[(i+1)%5].signal(); | ||
+ | } | ||
+ | |||
+ | process philo(i), i=1,...,5 | ||
+ | while(1) | ||
+ | think() | ||
+ | philom.starteat(); | ||
+ | eat() | ||
+ | philom.endeat(); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | ===Soluzione del problema dei lettori/scrittori con i monitor=== | ||
+ | <syntaxhighlight lang="c"> | ||
+ | process reader[i] { | ||
+ | while (1) { | ||
+ | ... | ||
+ | rw.startread(); | ||
+ | read | ||
+ | rw.endread(); | ||
+ | } | ||
+ | |||
+ | process writer[i] { | ||
+ | while (1) { | ||
+ | ... | ||
+ | rw.startwrite(); | ||
+ | write | ||
+ | rw.endwrite(); | ||
+ | } | ||
+ | |||
+ | monitor rw { | ||
+ | int nr, nw; | ||
+ | condition ok2read; // nw == 0 | ||
+ | condition ok2write; // nw == 0 && nr == 0 | ||
+ | } | ||
+ | |||
+ | procedure entry startread(void) { | ||
+ | if (nw > 0 || ww > 0) ok2read.wait() | ||
+ | nr++; | ||
+ | ok2read.signal() | ||
+ | } | ||
+ | |||
+ | procedure entry endread(void) { | ||
+ | nr--; | ||
+ | if (nr == 0) ok2write.signal() | ||
+ | } | ||
+ | |||
+ | procedure entry startwrite(void){ | ||
+ | ww++; | ||
+ | if (nw > 0 || nr > 0) ok2write.wait() | ||
+ | ww-- | ||
+ | nw++; | ||
+ | } | ||
+ | |||
+ | procedure entry endwrite(void){ | ||
+ | nr--; | ||
+ | ok2read.signal() | ||
+ | if (nr == 0) ok2write.signal() | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
== Lezione del 17 novembre 2017 == | == Lezione del 17 novembre 2017 == | ||
+ | |||
+ | Segnali. | ||
+ | |||
+ | ===Reentrant Functions=== | ||
+ | |||
+ | [[File:reentrant_functions.PNG]] | ||
+ | |||
== Lezione del 21 novembre 2017 == | == Lezione del 21 novembre 2017 == | ||
+ | |||
+ | ===Message Passing=== | ||
+ | |||
+ | I semafori e i monitor sono pensati in un modello di concorrenza dove i processi hanno memoria condivisa. Se eliminiamo | ||
+ | questa ipotesi, ovvero pensiamo ad un modello a memoria privata (ogni processo ha la sua memoria) per poter fare delle | ||
+ | computazioni utilizzando più processi contemporaneamente è necessario che i processi si parlino: si devono utilizzare | ||
+ | meccanismi di message passing. | ||
+ | |||
+ | Le primitive principali di un sistema di message passing sono send e receive. Per scambiare i messaggi è necessario | ||
+ | sapere anche l'identità dei processi comunicanti, per destinare il messaggio al processo giusto. La send prenderà | ||
+ | l'identificativo del destinatario e il messaggio come argomenti, la receive avrà come parametro l'identificativo del | ||
+ | mittente e il suo valore di ritorno sarà il messaggio stesso (oppure memorizzerà il messaggio in una variabile passata | ||
+ | per riferimento; dipende dall'implementazione). | ||
+ | |||
+ | Occorre specificare la semantica del sistema. Si identificano tre tipi di scambio di messaggi: | ||
+ | * '''message passing asincrono''': la send non è bloccante, la receive sì. Il messaggio parte sempre. Per ricevere un messaggio è necessario che ce ne sia almeno uno nella coda dei messaggi ricevuti, altrimenti si aspetta che ne arrivi uno. Si parla di message passing con memoria (il modello teorico prevede una dimensione infinita per la coda dei messaggi in arrivo) | ||
+ | * '''message passing sincrono''': sia la send che la receive sono bloccanti. La send di un processo mittente e la receive del corrispondente destinatario si devono alternare perfettamente. Si parla di message passing a rendevouz o senza memoria. | ||
+ | * '''completamente asincrono (sottocaso del primo)''': né la send né la receive sono bloccanti. La send manda sempre e la receive ritorna un errore se non si hanno messaggi in casella. | ||
+ | |||
+ | I paradigmi che vediamo sono solo di unicast. In particolare non verranno considerate le problematiche legate a message | ||
+ | passing in sistemi broadcast e multicast (questo è argomento del corso di sistemi concorrenti). | ||
+ | |||
+ | È possibile implementare message passing asincrono con quello sincrono e viceversa? Da un lato sì, dall'altro nì. Il | ||
+ | message passing asincrono ha memoria, quindi implementare il servizio sincrono (senza memoria) è semplice. Inserire la | ||
+ | memoria invece con un sistema sincrono per simularne uno asincrono invece è più difficile. | ||
+ | |||
+ | ====Implementazione di message passing sincrono mediante message passing asincrono==== | ||
+ | |||
+ | <syntaxhighlight lang=python> | ||
+ | def ssend(dest, msg): | ||
+ | asend(dest, msg) | ||
+ | areceive(dest, ack) | ||
+ | |||
+ | def sreceive(sender, msg): | ||
+ | ret_value = areceive(sender, msg) | ||
+ | asend(sender,"") # Acknowledgement | ||
+ | return ret_value | ||
+ | |||
+ | # Nota: questo crea deadlock (colpa del modello) | ||
+ | |||
+ | </syntaxhighlight> | ||
+ | |||
+ | ====Implementazione di message passing asincrono mediante message passing sincrono==== | ||
+ | |||
+ | Come si crea la memoria? Con un altro processo che fa da demone nel mezzo. Tuttavia si perde l'equivalenza tra i due | ||
+ | paradigmi, perché non si sta implementando uno con l'altro mediante una libreria ma mediante un processo aggiuntivo. Il | ||
+ | demone continua a ciclare indefinitamente, prendendo le richieste. Per le richieste di tipo send le mette in una | ||
+ | struttura dati per il sender. Per le richieste di tipo receive prende i messaggi dalla struttura dati e li recapita al | ||
+ | richiedente. | ||
+ | |||
+ | <syntaxhighlight lang=python> | ||
+ | def asend(dest, msg): | ||
+ | ssend(SERVER, (SND,dest,msg)) | ||
+ | |||
+ | def areceive(sender, msg): | ||
+ | ssend(SERVER, (RCV,sender,"")) | ||
+ | ssend(SERVER, (sender, msg)) | ||
+ | |||
+ | # Nota: serve un check sulla parte di codice che segue (non sono sicuro di aver copiato tutto e bene) | ||
+ | |||
+ | server process: | ||
+ | waiting = [] | ||
+ | msgq = [] | ||
+ | while True: | ||
+ | srecv(sender, (tag, proc, msg)) | ||
+ | if tag == SND: | ||
+ | if (match(waiting[proc], sender)): | ||
+ | asend(proc, (sender,msg)) | ||
+ | match(waiting[proc], sender = None) | ||
+ | else: | ||
+ | msgq[proc].append((sender,msg)) | ||
+ | elif tag == RCV: | ||
+ | if (senx, msg) = msgq.get(proc, sender): | ||
+ | asend(proc, (senx,msg)) | ||
+ | else: | ||
+ | waiting[sender] = proc | ||
+ | </syntaxhighlight> | ||
+ | |||
== Lezione del 24 novembre 2017 == | == Lezione del 24 novembre 2017 == | ||
+ | |||
+ | creazione gruppi, awk, find, grep, xarg | ||
+ | |||
== Lezione del 28 novembre 2017 == | == Lezione del 28 novembre 2017 == | ||
+ | |||
+ | Esercizi Prog. Concorrente | ||
+ | |||
== Lezione del 1 dicembre 2017 == | == Lezione del 1 dicembre 2017 == | ||
+ | |||
+ | Esercizi (prove pratiche) | ||
+ | |||
== Lezione del 5 dicembre 2017 == | == Lezione del 5 dicembre 2017 == | ||
+ | |||
+ | Esercizi Prog. Concorrente | ||
+ | |||
== Lezione del 12 dicembre 2017 == | == Lezione del 12 dicembre 2017 == | ||
+ | |||
+ | la macchina emulata uARM | ||
+ | |||
+ | == Lezione del 14 dicembre 2017 == | ||
+ | |||
+ | Esercizi | ||
+ | |||
== Lezione del 15 dicembre 2017 == | == Lezione del 15 dicembre 2017 == | ||
+ | |||
+ | presentazione delle specifiche di phase1 |
Latest revision as of 18:20, 23 February 2018
scrivete qui idee, riassunti dei concetti espressi, commenti approfondimenti sulle lezioni.
Lezione del 26 settembre 2017
W⁴ H Y
- What:
- Who:
Noi studenti ed "entrambi" i professori.
- When:
Il corso ha durata annuale. Verrà svolto ogni martedì e venerdì dalle 15:30 alle 18:30.
- La lezione del martedì sarà dedicata alla programmazione concorrente;
- La lezione del venerdì sarà dedicata alla parte generale. Terminerà circa 15 minuti prima dell'orario stabilito.
- Where:
Sempre in Aula 1 Ercolani (E1): DISI, Scuole Ercolani, Mura Anteo Zamboni 2B.
- How:
Il corso si svolgerà tramite:
- Lezioni frontali in aula.
- Attività di laboratorio.
- Esercitazioni.
Inoltre, durante l'ultimo periodo di lezioni del secondo semestre, vi sarà un periodo di ripasso del programma tramite esercizi, in vista dell'esame.
Durante la lezione del 3/10 sono state indicate alcune propedeuticità del corso. tra cui: programmazione, algoritmi e architettura degli elaboratori in primis.
Importanti sono anche i corsi di Analisi matematica e Algebra e geometria. Per farla breve, il primo anno è propedeutico al secondo.
- Why:
Fonti e strumenti del corso
- wiki creata di gruppo, lezioni frontali e esercitazioni.
- Non esiste un vero e proprio testo consigliato. Tutte le informazioni sono date DURANTE il corso.
- Per domande specifiche scrivere su mailing list (so@cs.unibo.it);
- Per esercizi, appunti e programma svolto rivolgersi al wiki (so.v2.cs.unibo.it);
- Per live streaming (www.cs.unibo.it/~renzo/live/).
Modalità di esame
esame scritto (diviso in due parti: una parte generale e una di programmazione concorrente) + progetto + orale (facoltativo), possibilità di dare solo lo scritto per ottenere un massimo di 18. La modalità per studenti in "difficoltà" è disponibile solo fino agli appelli autunnali.
- Si parlerà del progetto indicativamente a partire da Dicembre 2017.
- L'orale può essere sostenuto da chi vuole migliorare (peggiorare) il voto ottenuto, o da chi vuole ottenere la lode.
Orario di ricevimento per il primo semestre (fino al 15/12/17)
martedì alle 11:30.
- Università = docenti + studenti.
- Informatica = come generare informazione automatica.
- Hardware, Software, Elaborazione, Comunicazione, Memorizzazione, Digitale/Analogico.
Introduzione
La principale distinzione che facciamo tra dato e informazione è che il dato, da solo, è privo di significato. Se, tuttavia, viene interpretato in un particolare contesto allora può diventare informazione significativa per chi sta interpretando i dati.
Un algoritmo è una o più sequenze non ambigue di passi che, dato un problema, ci permette di risolverlo.
Il nostro algoritmo, scritto in un qualche linguaggio formale, diventa un programma (sostanzialmente un testo, un insieme di istruzioni).
Un linguaggio è un entità software, ed è definito come una quadrupla (alfabeto, sintassi, lessico, semantica) dove:
- l'alfabeto è l'insieme di simboli che compone il linguaggio.
- il lessico si può vedere come una funzione che va dai simboli a un booleano e ci dice quali sono le frasi ben formate.
- la sintassi determina quali delle sequenze scritte sono effettivamente corrette.
- la semantica associa un significato alle parole ben formate.
Lezione del 29 settembre 2017
Cos'è un sistema operativo
Un sistema operativo è un programma che gestisce i processi, le risorse e interfaccia le applicazioni con l'hardware dell'elaboratore.
In particolare, il sistema operativo (laddove esiste) è il primo processo ad essere attivato e resta in vita fino allo spegnimento del calcolatore o al sopraggiungere di un errore fatale per il sistema.
A cosa serve un sistema operativo? Principalmente, ha i seguenti scopi:
- Facilitare l'utilizzo del sistema (senza S.O. sarebbe a carico del programmatore conoscere tutti i linguaggi con cui si comunica con l'architettura della macchina);
- Rendere affidabile, protetto e sicuro l'utilizzo del sistema (e.g. un processo potrebbe recar danno all'intero sistema se non controllato o gestito, potrebbe ignorare i permessi di visualizzazione di un file);
- Astrarre l'hardware (e.g. filesystem);
- Garantire l'efficienza (e.g. non tenere la CPU in idle adottando opportuni algoritmi di scheduling);
- Assicurare portabilità ;
Un approccio molto usato è il cosiddetto approccio a strati ("a livelli") in cui ogni strato utilizza i servizi forniti al livello inferiore e ne fornisce di nuovi a quello superiore. Astraendo il nostro calcolatore possiamo piazzare al livello più basso l'hardware e, subito sopra, il sistema operativo. I due layer comunicano usando il linguaggio ISA (Instruction Set Architecture), nativo della CPU stessa, al quale si aggiungono quelli che permettono di comunicare con i vari controllori dei dispositivi come la scheda di rete, la stampante, etc. Sopra il livello del sistema operativo possiamo collocare quello delle librerie e, infine, quello degli applicativi.
- Systemcall = meccanismo usato da un processo per richiedere al SO una qualche funzionalità a livello kernel. Un esempio è la funzione printf() del linguaggio C (stampa a video) che utilizza la systemcall write() (una delle systemcall per la gestione dei dispositivi).
Storia dei sistemi operativi
Nella generazione zero (1800 ca.) possiamo includere Babbage con la sua macchina analitica e lady Ada Lovelace.
Nella generazione uno vediamo la comparsa delle valvole con tutti i problemi connessi. Non c'erano utilizzatori delle macchine, le stesse persone che le costruivano erano anche programmatori e fruitori delle stesse.
Nella generazione due (fine anni '60) arriva il transistor. Quest'ultimo è molto più veloce e piccolo della valvola e, soprattutto, meno incline a guasti. Le macchine iniziano ad essere più economiche grazie alla possibilità di avere un'economia di scala e quindi accessibili al grande pubblico. Questo fa si che, in queste prime fasi, i costruttori non siano più gli unici utilizzatori.
A questo punto poiché, pur essendo diventata più accessibile, una macchina costava ancora molto, nasce l'esigenza di non lasciare mai un processore senza lavoro (al fine di massimizzare i ricavi) e di condividere i dati. In questa fase, infatti, abbiamo sistemi operativi di tipo batch (che collezionano tutto l'input all'inizio per calcolare e restituire l'output) dove l'inserimento di programmi e dati veniva fatto tramite schede perforate e non c'era interazione. Dai sistemi batch nasce lo SPOOL (Simultaneous Peripheral Operations On-Line).
Si comincia a pensare di utilizzare i tempi di I/O per poter eseguire altri processi. Questo però solleva almeno due problematiche: da un lato serve un modo per sapere quando un input è davvero finito mentre, dall'altro, bisogna fare in modo che un processo che non richieda I/O non occupi la CPU per un tempo indefinito. Per la prima problematica la soluzione è rappresentata dall'introduzione degli interrupt, segnali elettrici inviati alla CPU alla fine di ogni input. Per la seconda, invece, è stato introdotto il cosiddetto interval timer che non è altro che un dispositivo che invia interrupt dopo un determinato quanto di tempo assicurando che un processo non occupi per troppo tempo il processore. Questo consente di realizzare sistemi time sharing (il cui più semplice algoritmo è il round-robin) dove un processo può essere in uno dei seguenti tre stati:
- READY pronto per essere eseguito ma, il processore è già occupato;
- RUNNING in esecuzione;
- WAIT in attesa di I/O.
Nella quarta generazione (anni '70 ca.) vediamo la nascita di molte innovazioni importanti. Si riesce a rimpicciolire di molto i processori facendoli divenire a tutti gli effetti micro-processori. Nei laboratori Bell nasce Unix, un sistema operativo rivoluzionario con time sharing e clonazione dei processi (un processo non viene mai creato dal nulla ma viene prima clonato da uno già esistente e poi, alla copia, viene fatto eseguire un programma). A causa di problemi di portabilità da PDP-9, macchina su cui Unix è nato, a PDP-11 nasce il linguaggio C e il suo compilatore (scritto anch'esso in C). Mentre Apple I "porta" una nuova idea di pc per tutte le famiglie, nascono molte versioni di Unix. Per impedire una deriva proprietaria in cui ogni produttore offriva tutto quello che offrivano gli altri ma con qualche feature in più, Richard Stallman fonda il progetto GNU (GNU's Not Unix) per il quale scrive tutta una serie di utilities come il famoso compilatore gcc. A GNU, per essere operativo, manca però un kernel. Non volendo aspettare i tempi di sviluppo del kernel che Stallman aveva in mente, Linus Torlvalds scrive il kernel Linux da cui nasce il sistema operativo GNU/Linux.
Nei tempi più recenti nascono i processori multi-core.
Lezione del 3 ottobre 2017
Introduzione alla programmazione concorrente
In programmazione concorrente le nozioni apprese di "algoritmo", "programma" e "processo" devono essere aggiornate. Questo perché non vi è più un programma con un'unica sequenza esecutiva [o meglio, un unico filo (= thread) di esecuzione]. Appare ovvio come servino dei meccanismi per organizzare e sincronizzare tali fili esecutivi, in modo da evitare il verificarsi di eventi non desiderati.
Quando parliamo di sistema concorrente, ci riferiamo ad un sistema in cui due o più "attori" sono eseguiti parallelamente.
Questi attori possono essere processi o thread.
La differenza sostanziale tra queste due tipologie di attori riguarda la condivisione della memoria e dei dati su cui operano.
Più processi possono eseguire lo stesso programma ma, ognuno di essi, ha i propri dati su cui operare e il suo stato di avanzamento. In particolare:
- un processo è un'entità, un attore che ha delle proprietà e delle risorse associate;
- in ogni istante, il processo può essere interrotto.
Più thread che vengono eseguiti parallelamente, invece, condividono gli stessi dati su cui operare.
Codici di esempio:
processi
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]){
if(fork()) printf("Yes");
else printf("No");
}
L'output generato sarà "YesNo" poiché, usando la systemcall fork(), abbiamo creato una nuova copia del processo (un processo figlio). Il processo padre, che ha eseguito con successo la fork(), stamperà "Yes" mentre il figlio stamperà "No".
thread
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#define NUM_THREADS 5
#define MAX 1000000
int sum;
void* perform_work(){
int i;
for(i = 0; i < MAX; i++) sum = sum + 1;
/* optionally: insert more useful stuff here */
return NULL;
}
int main( int argc, char** argv ){
pthread_t threads[NUM_THREADS];
int thread_args[NUM_THREADS];
int result_code;
unsigned index;
sum = 0;
// create all threads one by one
for(index = 0; index < NUM_THREADS; ++index ){
thread_args[index] = index;
printf("In main: creating thread %d\n", index);
result_code = pthread_create(&threads[index], NULL, perform_work, NULL);
assert(!result_code);
}
// wait for each thread to complete
for(index = 0; index < NUM_THREADS; ++index ){
// block until thread 'index' completes
result_code = pthread_join(threads[index], NULL);
assert(!result_code);
printf("In main: thread %d has completed\n", index);
}
printf("In main: All threads completed successfully\n" );
printf("Result: %d\n", sum);
exit( EXIT_SUCCESS );
}
Prima di discutere l'output generato dall'esecuzione di questo codice, è opportuno introdurre, brevemente, gli strumenti utilizzati.
Pthread (POSIX thread)
Citazione dal Silberschatz, Galvin, Gagne: Pthread si riferisce allo standard POSIX (IEEE 1003.1c) che definisce una API per la creazione e sincronizzazione dei thread.
Le istruzioni fondamentali sono:
- int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); che crea un thread;
- int pthread_join(pthread_t thread, void **retval); che aspetta che un thread sia completato prima di continuare l'esecuzione.
Da ricordare che, per utilizzare pthread, va incluso l'header <pthread.h> e durante la compilazione va inclusa la libreria esterna tramite l'opzione -pthread.
Torniamo all'output generato dall'esecuzione del secondo codice.
Nel codice vengono generati cinque thread ed ognuno somma MAX (=1000000) volte 1 a sum. Ci aspettiamo un output di 5000000 ma, in realtà, viene stampato un valore molto minore.
Questo accade principalmente per due motivi:
- sommare una costante ad una variabile non è un'operazione atomica (richiede almeno tre istruzioni assembler: due mov ed una sum).
- l'interval timer può fermare l'esecuzione del thread mentre sta effettuando una delle tre operazioni richieste per aggiungere uno a sum e provocare un risultato errato.
Questo è uno dei possibili problemi della programmazione concorrente conosciuto come race condition.
Un sistema di processi multipli presenta una race condition qualora il risultato finale dell'esecuzione dipenda dalla temporizzazione, o dall'ordine con cui i processi vengono eseguiti.
Per risolvere questo problema possiamo adoperare ancora una volta pthread.
Nello specifico, ci venogono offerte istruzione per realizzare una mutua esclusione sulle risorse condivise in modo che l'operazione che prima non era atomiche,ora, lo sia e non porti più ad una computazione errata.
Codice di esempio:
Mutex
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#define NUM_THREADS 5
#define MAX 1000000
int sum;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* perform_work(){
int i;
for(i = 0; i < MAX; i++){
pthread_mutex_lock(&mutex);
sum = sum + 1;
pthread_mutex_unlock(&mutex);
}
/* optionally: insert more useful stuff here */
return NULL;
}
int main( int argc, char** argv ){
pthread_t threads[NUM_THREADS];
int thread_args[NUM_THREADS];
int result_code;
unsigned index;
sum = 0;
// create all threads one by one
for(index = 0; index < NUM_THREADS; ++index ){
thread_args[index] = index;
printf("In main: creating thread %d\n", index);
result_code = pthread_create(&threads[index], NULL, perform_work, NULL);
assert(!result_code);
}
// wait for each thread to complete
for(index = 0; index < NUM_THREADS; ++index ){
// block until thread 'index' completes
result_code = pthread_join(threads[index], NULL);
assert(!result_code);
printf("In main: thread %d has completed\n", index);
}
printf("In main: All threads completed successfully\n" );
printf("Result: %d\n", sum);
exit( EXIT_SUCCESS );
}
MUTEX
Un mutex è un dispositivo di MUTual EXclusion (mutua esclusione), ed è utile per proteggere le strutture dati condivise da modifiche concorrenti, implementare sezioni critiche e monitor.
Un mutex ha due possibili stati: sbloccato (non posseduto da nessun thread) e bloccato (posseduto da un thread). Un mutex non può mai essere posseduto da due thread contemporaneamente.
Se un thread provasse ad acquisire il mutex, mentre quest'ultimo è già in possesso di un altro thread, dovrebbe aspettare finché non viene rilasciato.
La variabile che realizza il mutex, di tipo pthread_mutex_t, viene inizializzata staticamente assegnandogli PTHREAD_MUTEX_INITIALIZER.
pthread_mutex_lock acquisisce immediatamente il mutex, se libero. Altrimenti aspetta fino a trovarla, prima o poi, libera.
pthread_mutex_unlock rilascia il mutex.
[da: https://sourceware.org/pthreads-win32/manual/pthread_mutex_init.html]
Con questa soluzione abbiamo raggiunto un risultato esatto pur avendo pagato in prestazioni: la computazione infatti è più lenta a causa del tempo richiesto dalla sincronizzazione tra i thread.
Consideriamo ora due scenari leggermente più complicati:
- nel primo abbiamo due processi, A e B, che devono accedere a due risorse, X e Y, contemporaneamente prima di poter terminare. Cosa succede se, mentre A acquisisce X, B acquisisce Y?
- nel secondo abbiamo tre processi, A, B e C, che devono accedere alla risorsa X. Che succede se A e B, essendo più veloci, occupano insistentemente la risorsa e non permettono a C di utilizzarla?
La prima condizione si chiama deadlock, la seconda condizione si chiama starvation. Il deadlock (stallo) è un problema che non può scomparire autonomamente, mentre la starvation ("inedia") si può risolvere da sola. A tal proposito ci viene incontro l'assioma di finite progress: "ogni processo che può avanzare, prima o poi lo farà".
In ogni caso, per parlare in maniera più approfondita di queste tematiche:
- avremo bisogno di un modello di riferimento
- avremo bisogno di paradigmi in grado di esprimere la concorrenza (alcuni paradigmi saranno rivolti alla concorrenza a memoria privata; altri al multithreading a risorse condivise)
- dovremo imparare a scrivere programmi
Nel nostro modello di riferimento l'assegnamento di costanti è un operazione atomica. Dagli esempi precedenti, infatti, si evince come ci sia bisogno di creare atomicità in sezioni critiche. Inoltre, il nostro modello dovrà rispettare le proprietà di safety e di liveness, ovvero:
- Tutti i processi danno la stessa risposta;
- Ogni processo corretto da come risposta una tra quelle proposte;
- Ogni processo prima o poi fornisce una risposta.
Lezione del 6 ottobre 2017
Il linguaggio C
Il linguaggio C nasce nei primi anni 70 grazie al lavoro di Dennis Ritchie con lo scopo di scrivere il sistema operativo Unix. Quali sono le caratteristiche di C e cosa lo rende un linguaggio adatto per scrivere un sistema operativo? Un linguaggio per scrivere un sistema operativo deve:
- essere indipendente dalla macchina su cui viene scritto
- dare la possibilità di lavorare direttamente con la memoria
- permette di andare a modificare i singoli bit in memoria
- essere semplice da usare
- essere massimamente produttivo
- essere veloce, efficiente
C presenta le caratteristiche dette sopra e alcune altre:
- in C, dal punto di vista sintattico, tutto è funzione. Si può vedere un programma in C come un insieme di funzioni. La funzione principale è il main. L'unica cosa che la distingue è il nome, a parte quello il main è una funzione come le altre. Prende due argomenti: argcount e argvalue, che contengono rispettivamente il numero degli argomenti e un array contenente gli argomenti stessi. L'interfaccia del main è tipicamente:
int main(int argc, char *argv[])
- il costrutto fondamentale è l'espressione. expression ; = instruction
- sono previste istruzioni di struttura
if, while, switch, etc.
- le variabili, per poter essere utilizzate, devono essere sempre definite
- il C è un linguaggio fortemente tipato, però con un certo numero di eccezioni: ad esempio, è possibile fare il casting esplicito di un puntatore void * in qualsiasi tipo, senza che il compilatore dia errori o warnings, con conseguente perdita di type safety
- C è un linguaggio "per adulti": non impone una struttura ordinata al codice, come ad esempio fa invece Python, ed è piena responsabilità del programmatore scrivere programmi ordinati/leggibili. Inoltre molte operazioni che in altri linguaggi non sarebbero permesse lo sono invece in C: anche in questo caso è responsabilità del programmatore fare corretto uso del linguaggio
- C è un linguaggio molto semplice. Come si sopperisce alle mancanze imposte da questa semplicità? Grazie alle librerie: in C tutte le funzionalità aggiuntive (non fornite dal linguaggio stesso, come l'allocazione di memoria, la stampa a schermo, la gestione di input e output) sono fornite dalle librerie
- C è anche sintatticamente semplice. Dove va a finire la complessità sintattica? Nel preprocessore: prima di essere compilato un programma in C passa attraverso un preprocessore, che produce codice in base alle direttive di preprocessione date nel codice stesso
#ifdef VAR ... #endif, # e ## operator, __LINE__,etc
- in C vi sono fondamentalmente due tipi di dato: int e float. Tutti gli altri "tipi" di dato sono derivati da essi. Si hanno diversi modificatori, che vanno a modificare la quantità di memoria allocata per la variabile. Alcuni di questi sono Un dato di tipo long int ha, solitamente, la dimensione di una parola di memoria della macchina su cui è eseguito il programma. char alloca 8 bits. Se si prende una variabile di un certo tipo e lo si assegna ad una di tipo più ampio, il valore viene mantenuto. Viceversa quando si assegna una variabile di un tipo ad una di tipo meno ampio, una volta "superato il limite" si torna indietro ciclicamente al valore iniziale. Per queste situazioni il compilatore non dà né warnings né errors: sono perfettamente lecite in C
long, short, char, etc
- in C si passa una varibile ad una funzione solo per valore; per passare "per riferimento" una variabile si passa per valore l'indirizzo della variabile
- in C si può fare uso di puntatori alla memoria. Dato un puntatore p, un'istruzione del tipo p+i va letta come: vai avanti di i posizioni della grandezza del tipo del puntatore ognuno. Un vettore in C non è altro che un puntatore che non può essere assegnato. Una scrittura del tipo array[i] è una abbreviazione di *(array + i). Non c'è controllo in C se vado oltre la memoria allocata per l'array. Perché? In Pascal, ad esempio, c'è un tale controllo. Il motivo per cui in C questo manca è che questo controllo genera ulteriore codice macchina. Dovendo C essere efficiente, non ci si può permettere un tale costo aggiuntivo. La responsabilità di non andare oltre è lasciata al programmatore
Esercizio svolto che prende in input due valori tramite i parametri del metodo main, li converte da sequenze di caratteri ad interi, per poi stamparli utilizzando delle System Call. Lo scopo dell’esercizio è dimostrare come sia possibile utilizzare il linguaggio C senza l’utilizzo di librerie particolari (ad eccezione di quelle per le System Call).
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
int _uatoi(char *s, int value){
if (*s == 0)
return value;
else if (*s >= '0' && *s <= '9'){
value = value * 10 + (*s - '0');
return _uatoi(s+1, value);
}
}
int uatoi(char *s){
return _uatoi(s, 0);
}
char *uitoa(int val, char *buf, int len){
int i;
for (len = len - 1; len >= 0 && val > 0; len--, val /= 10){
buf[len] = '0' + (val %10);
}
return buf + len + 1;
}
#define MAXOUTLEN 10
int main (int argc, char *argv[]) {
int tot = 0;
int i;
char buf[MAXOUTLEN];
char *result;
for (i = 1; i < argc; i++)
tot += uatoi(argv[i]);
result = uitoa(tot, buf, MAXOUTLEN);
syscall(__NR_write, 1, result, MAXOUTLEN - (result - buf));
syscall(__NR_write, 1, "\n", 1);
return tot;
}
Lezione del 10 ottobre 2017
Programmazione concorrente: notazione e un primo problema
Esempio di possibile rappresentazione (che non useremo) di codice concorrente.
A, B, C, D, E rappresentano porzioni di codice.
// rappresenta esecuzione parallela
A
cobegin
B
//
C
//
D
coend
E
Il codice sarà eseguito come: A -> BCD -> E. E quindi verrà eseguito solo dopo la fine dell'esecuzione di BCD.
Un esempio che si presta molto bene ad illustrare la programmazione concorrente è quello del merge sort concorrente:
leggi vettore
cobegin
ordina la prima metà del vettore
//
ordina la seconda metà del vettore
coend
merge (miscela) i risultati
Versione errata del merge sort concorrente:
leggi vettore
cobegin
ordina la prima metà del vettore
//
ordina la seconda metà del vettore
//
merge (miscela) i risultati
coend
Avendo un numero N di processi, vorremmo avere una notazione che ci permetta di definire una porzione di codice come atomica (la sezione critica). Come possiamo procedere?
Teoricamente, vorremmo avere due funzioni, csenter() e csexit() che ci permettano di entrare e uscire dalla sezione critica del codice senza causare errori nella computazione.
Quali sono le proprietà che queste funzioni debbono rispettare?
- Devono garantire la mutua esclusione (1 solo processo esegue la critical section alla volta). Contando n csenter() completate e m csexit() completate la differenza deve essere <=1
- Non devono fare deadlock tra di loro
- Non devono fare starvation tra loro
- (no unnecessary delay) un processo che non sta usando la critical section {csenter()..csexit()} non deve bloccare un altro processo dall'entrarvi.
La soluzione a questo problema è nota come soluzione di Dekker (scritta e pubblicata da Dijkstra) che venne presentata in modo graduale attraverso l'utilizzo di quattro possibili, ma sbagliate, soluzioni.
Soluzione di Dekker
Vorremmo avere programmi che abbiano la seguente struttura:
process[i], i=1,...,N
while(1):
csenter() //entrata nella sezione critica (critical section)
critical code
csexit() //uscita dalla sezione critica
non-critical code
Ad esempio, tornando al problema dei thread che sommavano 1 a sum MAX volte, avremo:
process A:
for (i = 0; i < MAX; i++)
csenter()
sum += 1 //atomicità descritta da csenter(), csexit()
// < sum += 1 > atomicità lasciata al processore
csexit()
process B:
for (i = 0; i < MAX; i++)
csenter()
sum += 1
csexit()
Soluzione 1: A turni
Qual è il problema?
- mutex OK
- deadlock OK
- starvation OK
- no unnecessary delay NO
- Perché se P ha bisogno spesso della critical section, P non deve aspettare che Q chieda la critical section e, all'uscita, lo autorizzi di nuovo ad usarla impostando turn = P. Se Q una volta richiesta la sezione non la usa ma si ferma, P non può richiederla ancora perché appartiene ancora a Q.
turn=P
process P:
while(1)
while (turn != P)
;
critical code
turn = Q
non-critical code
process Q:
while(1)
while (turn != Q)
critical code
turn = P
non-critical code
Soluzione 2: con 2 variabili
Cerchiamo di risolvere il problema dei turni
Finché Q usa la sezione P aspetta, quando Q ha finito P prende la sezione critica e non ci sono ritardi indesiderati.
- starvation OK
- deadlock OK
- no unnecessary delay OK
- mutex NO
- Possono essere entrambi come false, escono insieme nel while, tutti e due mettono a true ed entrano assieme nel while.
inP = false;
inQ = false;
process P:
while(1)
while (inQ)
;
inP = true
critical code
inP = false
non-critical code
process Q:
while(1)
while (inP)
;
inQ = true
critical code
inQ = false
non-critical code
Soluzione 3
Il problema di soluzione 2 è che i processi non sono al corrente dell'intenzione dell'altro di entrare nel while quindi si crea il problema.
- mutex OK
- deadlock NO (potrebbero entrambi richiedere, contemporaneamente, l'ingresso alla critical section)
- starvation OK
- non unnecessary delay OK
needP = false;
needQ = false;
process P:
while(1)
needP = true
while (needQ)
;
critical code
needP = false
non-critical code
process Q:
while(1)
needQ = true
while (needP)
;
critical code
needQ = false
non-critical code
Soluzione 4
- mutex OK
- deadlock OK
- no unnecessary delay OK
- starvation NO
- Perché può esistere il processo che prova sempre a chiedere nel momento sbagliato e non esegue mai.
inP = false;
inQ = false;
process P:
while(1)
inP = true
while (inQ){
inP = false;
delay
inP = true;
}
critical code
inP = false
non-critical code
process Q:
while(1)
inQ = true
while (inQ){
inQ = false;
delay
inQ = true;
}
critical code
inQ = false
non-critical code
Soluzione finale
Uniamo soluzione 1 (poco simmetrica) e soluzione 4 (troppo simmetrica) usando quello che ci serve (attraverso needP e needQ) e, se dovessimo essere tutti e due contemporaneamente a voler entrare, usiamo i turni (turn) per entrare nella critical section (settando il need dell'altro processo a false).
- mutex OK
- deadlock OK
- starvation OK
- non unnecessary delay OK
needP = false;
needQ = false;
turn = P
process P:
while(1)
needP = true
while (needQ)
if (turn != P) {
needP = false;
while ( turn != P)
; /* busy wait */
needP = true;
}
/*critical code*/
turn = Q
needP = false
/*non-critical code*/
process Q:
while(1)
needQ = true
while (needP)
if (turn != Q) {
needQ = false;
while ( turn != Q)
; /* busy wait */
needQ = true;
}
/*critical code*/
turn = P
needQ = false
/*non-critical code*/
Lezione del 13 ottobre 2017
Il linguaggio C: PREPROCESSORE
Il preprocessore è uno strumento contenuto nella toolchain di gcc che esamina il codice sorgente prima che questo venga compilato, e "trasforma" il programma prima della compilazione; è propriamente definito come macro processor, ovvero un esaminatore di macro. Le macro sono delle abbreviazioni che in realtà definiscono delle strutture di codice più grandi, e tornano molto utili in diversi casi nello sviluppo di un programma in C (è buona norma scrivere l’identificatore completamente in MAIUSCOLO). Il preprocessore esamina le cosiddette direttive; per conoscere l’output del preprocessore è possibile digitare il comando gcc -E {nomeDelSorgente}.
Un primo esempio è dato dalla direttiva #define, la quale crea una macro, ovvero l’associazione di un identificatore (con o senza parametri) con una stringa di token. Le macro così definite possono assumere quindi diverse funzioni:
- condizione per l’inclusione o meno di codice nel file che verrà passato al compilatore;
- inserimento di costanti all’interno del codice (notare che questa direttiva nasce molto prima della keyword const del linguaggio C);
- creazione di abbreviazioni a porzioni di codice più lunghe.
Caso 1
Il primo caso, che potrebbe essere definito come il più semplice, prevede la dichiarazione di un valore costante, su cui ci si baserà in seguito per includere o meno parti di codice. Per fare ciò occorre introdurre anche altre direttive del preprocessore: #ifdef, ifndef ed endif, le quali identificano un blocco di codice che deve essere incluso nel file da compilare solo se rispettivamente una macro è definita (#ifdef) o una macro non è definita (#ifndef), mentre la direttiva #endif indica la fine del blocco condizionale.
#include <stdio.h>
#define DEBUG
int main() {
#ifdef DEBUG
printf("In main\n");
#endif
// la macro __linux__ è definita solamente nei sistemi Linux-based
#ifndef __linux__
printf("WARNING: not a Linux system\n");
#endif
return 0;
}
- Da notare in questo caso l’utilizzo della macro __linux__, una macro predefinita nei sistemi Linux-based, che consente di discriminare il sistema operativo su cui il programma viene compilato, dando quindi la possibilità di compilare del codice diverso in base alla piattaforma utilizzata.
Caso 2
Il secondo caso prevede la presenza di un valore in seguito all’identificatore, che quindi verrà sostituito in ogni sua occorrenza all’interno del codice. È da far notare come questo meccanismo sia simile all’utilizzo della keyword const del linguaggio, ma quest’ultima nasce solo successivamente come alternativa type-safe (dal momento che le macro potrebbero essere utilizzate ovunque senza alcun tipo di controllo). Questo meccanismo torna ad esempio quando si vuole effettuare un’operazione un numero di volte prefissato; in questo modo, è sufficiente modificare il valore della macro nella sua definizione, per aggiornare tutto il programma che utilizzava quella macro al nuovo valore.
#include <stdio.h>
#define ITER_NUM 5
int main() {
int i;
for (i = 0; i < ITER_NUM; i++)
printf("Iterazione numero %d\n", i);
return 0;
}
Caso 3
L’ultimo caso analizzato è quello che può essere definito il più complesso tra i tre. L’identificatore della macro ora presenta anche dei parametri formali, all’interno di parentesi e separati da virgole. Tali parametri compaiono anche nella token string in cui vengono utilizzati (anche più volte) e su cui vengono eseguite delle operazioni. Questa particolarità è utile per permettere di ridurre porzioni di codice che vengono definite così una volta sola, e per essere utilizzate sarà sufficiente scrivere il nome dell’identificatore con i relativi parametri. Da notare come ci sia un numero considerevole di parentesi, che potrebbero quasi sembrare superflue. In realtà sono essenziali per assicurare il corretto ordine di esecuzione delle istruzioni una volta che la stringa di token sarà sostituita alle occorrenze dell’identificatore.
#include <stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main() {
printf("%d\n", MAX(10, 20));
return 0;
}
- Che produrrà, in seguito all’esecuzione del preprocessore:
int main() {
printf("%d\n", ((10) > (20) ? (10) : (20)));
return 0;
}
- Occorre far notare che quella effettuata dal preprocessore non è altro che una string substitution, cioè si limita a sostituire le macro utilizzate nel codice con la rispettiva definizione (ovviamente includendo eventuali parametri formali), ma non effettua alcun tipo di controllo. Ciò significa che occorre prestare molta attenzione quando si utilizzano le macro come nell’ultimo caso illustrato, perché potrebbero comparire dei side-effect inaspettati. Se si modifica l’esempio precedente in questo modo
#include <stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main() {
int i = 100;
printf("%d\n", MAX(10, i++));
printf("%d\n", MAX(10, i));
return 0;
}
- si otterrà un risultato che in un primo momento potrebbe essere inaspettato:
101
102
- Ad un’analisi più approfondita tuttavia, è possibile accorgersi dell’errore: il preprocessore effettua una sostituzione testuale dei parametri al momento dell’utilizzo della macro; ciò significa che sostituisce ogni occorrenza del primo parametro nella token string con il primo valore fornito, e dualmente per il secondo parametro. Il codice che verrà sottoposto al compilatore conterrà quindi due incrementi della variabile i. Un esempio a grandi linee delle operazioni eseguite dal compilatore è la seguente (per capirlo è necessario aver ben compreso la nozione di post-incremento della variabile i):
- assegno alla variabile i il valore 100
- 10 è maggiore di i?
- NO, incremento i (ora i vale 101)
- scrivo i (101)
- incremento i (ora i vale 102)
- 10 è maggiore di i?
- NO, scrivo i (102)
Approfondimenti
Esistono molti altri operatori che si possono utilizzare all’interno delle macro, due esempi sono:
- l’operatore # che, se anteposto al nome di un parametro nella stringa di token, viene riconosciuto dal processore, che rimpiazza il parametro con il simbolo # anteposto con il proprio valore letterale convertito in una stringa costante;
- l’operatore ## "combina" due token in uno: unisce il token alla sinistra e quello alla destra dell’operatore per farlo diventare un’unico token. Questo torna molto utile se uno dei due token proviene da un parametro della macro.
Per un esempio su questi ultimi due operatori: tables and preprocessors tricks
Per ulteriori approfondimenti sul preprocessore del C: Approfondimenti sul preprocessore del C
Lezione del 17 ottobre 2017
Oltre la soluzione di Dekker
Nella scorsa lezione di teoria abbiamo visto la soluzione di Dekker dove due processi, per poter entrare ed eseguire la critical section, manifestano prima questo loro bisogno (needP/needQ = true
), controllano poi se anche l'altro processo ha comunicato questo bisogno e, in caso affermativo, verrà utilizzata una politica a turni per gestire l'entrata e l'uscita alla sezione critica.
P.S. Nei sistemi reali (specialmente multi-core) la soluzione di Dekker necessita di istruzioni aggiuntive come __syn_synchronize()
e sched_yeld()
: la prima sincronizza il contenuto delle cache (assicurando la correttezza del risultato) mentre la seconda fa sì che il thread rinunci al suo tempo di CPU permettendo a qualche altro processo di avere la priorità.
Questa soluzione ha però due limiti:
- non funziona con un numero di processi > 2
- è relativamente complicato da capire, si può fare di meglio!
Un'evoluzione più lineare e facilmente adattabile a più di due processi è l'algoritmo di Peterson che, nel caso base, ha la seguente struttura:
needP = false;
needQ = false;
turn;
process P:
while(1)
/* entry protocol */
needP = true
turn = Q
while(needQ && turn != P)
; /* busy wait */
/* critical section */
needP = false
/* non-critical section */
process Q:
while(1)
/* entry protocol */
needQ = true
turn = P
while(needP && turn != Q)
; /* busy wait */
/* critical section */
needQ = false
/* non-critical section */
Questa soluzione è più snella poiché i processi, invece di assegnare la variabile turn
all'altro processo come in Dekker, lo fanno prima.
Possiamo vedere questa soluzione come se avesse una sorta di "anticamera" da cui bisogna necessariamente passare per poter accedere alla sezione critica.
Entra nella critical section sempre l'ultimo processo a non essere entrato nell'anticamera.
Questi concetti possono bessimo essere estesi nel caso in cui abbiamo più di due processi a patto di estendere l'anticamera con tanti "stage" quanti sono i processi.
Queste soluzioni funzionano in pratica ma, ci sono ancora tutti i difetti di prima: busy wait che spreca cicli di CPU e una grande difficoltà nello scrivere programmi concorrenti in questo modo. Ci farebbero davvero comodo dei paradigmi di più alto livello.
Davanti a noi si aprono principalmente due possibilità:
- l'hardware della macchina ci viene in aiuto mascherando gli interrupt (nei sistemi mono-core)
- l'hardware (e gli ingegneri) ci aiuta fornendoci un'apposita istruzione atomica: la TEST&SET
L'idea che sta alla base della T&S è quella di avere una variabile globale che ci dica se la critical section è libera (0) o occupata (1).
La T&S viene invocata con due parametri, una variabile locale che serve per verificare lo stato precedente e la variabile globale, e realizza la seguente funzionalità:
T&S(x,y) = <x = y; y = 1>
Quindi, intuitivamente, possiamo avere due casi: o la variabile globale è 0, e la T&S assegna 0 a x e setta la variabile globale a 1, o la variabile globale è a 1 e la funzione semplicemente immagazzina 1 in x. Questa semplice funzione è abbastanza per realizzare i protocolli csenter() e csexit() e realizzare le critical section:
G = 0; /* global variable */
csenter():
L;
do{
T&S(L, G)
}while(L == 1)
csexit():
G = 0
Questo approccio ci fornisce diversi vantaggi: è nativamente adatto a gestire un numero arbitrario di processi, è semplice e ci garantisce 3 delle 4 proprietà fondamentali delle critical section (starvation può accadere).
Però c'è ancora busy wait (chiamata anche spin-lock in questo contesto), potrebbe esserci starvation in casi speciali ed è ancora una gestione di basso livello.
Vorremo un paradigma che permetta di scrivere ancor più facilmente programmi concorrenti e che siano allo stesso tempo semplicemente implementabili.
Il primo paradigma che vediamo è quello dei semafori, proposto da Dijksta nel 1965. (P.S. quasi tutti gli appunti di Dijkstra sono stati scansionati e sono scaricabili da qui: http://www.cs.utexas.edu/users/EWD).
Lezione del 20 ottobre 2017
Complementi sul linguaggio C e cenni sulle librerie standard
- strutture con vettore di dimensione nulla come ultimo campo.
- funzioni a numero variabile di parametri
- formattazione (printf/scanf), uso avanzato
- allocazione dinamica
Makefile con azioni automatiche
Lezione del 24 ottobre 2017
Implementazione dei semafori nei sistemi operativi
Un costrutto è un'entità del linguaggio che implementa un'astrazione. La semantica del semaforo può essere in larga parte descritta dal suo invariante:
nP <= nV + init
init + nV - nP >= 0
/* init + nV - nP si dice "valore del semaforo" */
La V si può fare in qualsiasi momento, la P invece può non essere possibile da completare (nP = numero operazioni P completate) perché incrementandola potrebbe rendere la disuguaglianza non vera.
Se eseguo V su un semaforo di valore 0, il valore del semaforo = +1 se eseguissi P il valore del semaforo diventerebbe -1, violando l'invariante. Quindi P può essere eseguito solo se il valore del semaforo > 0 e ridurrà il valore del semaforo di 1.
Una sezione critica può essere realizzata con un semaforo. Espressa con solo l'invariante non evitiamo la starvation.
semaphore s(1)
s.P()
critical code
s.V()
Iniziamo con un semaforo inizializzato a 0, A entra ma non può eseguire P, quindi subentra B che esegue V e da il segnale ad A in modo che A possa proseguire:
semaphore synch(0)
Process A:
//wait 4 B
synch.P()
....
Process B:
//signal B
synch.V()
Da ricordare:
- Con i semafori nessuna protezione di variabile condivisa è automatica (occorre inserire sezioni critiche!)
- I semafori hanno memoria, nel senso che se mandiamo una V adesso e una P tra mezz'ora il semaforo ricorderà di aver fatto una V. Se non abbiamo necessità di una determinata segnalazione la dobbiamo rimuovere.
- Una V autorizza l'attivazione di un processo bloccato. Se facciamo una V allora uno dei processi bloccati partirà, ma non sappiamo quando.
- Un programma con almeno un semaforo che abbia solo operazioni P o solo operazioni V è errato.
- Se un testo chiede di usare semafori si usano SOLO semafori!
Producer Consumer con semafori generali
(o anche binari, in questo esempio è equivalente)
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
semaphore full;
semaphore empty;
volatile int buffer;
/*aspetto un tempo random con massimo 2s e genero un intero, a questo punto
semaphore_P è empty, facendo in questo modo il produttore scopre che il buffer
è vuoto e lo dichiara non più vuoto, in questo momento non è né vuoto né pieno,
mette il valore nel buffer condiviso e setta semaphore_V a full permettendo al
consumatore di leggere.*/
void *producer(void *arg) {
while (1) {
int value;
usleep(random() % 2000000);
value = random() % 32768;
printf("produced: %d\n",value);
semaphore_P(empty);
buffer = value;
semaphore_V(full);
printf("sent: %d\n",value);
}
}
/*il consumatore arriva a quel semaphore_P che è pieno quando può va avanti, si
copia in una variabile locale il buffer e semaphore_P diventa non pieno e non
vuoto. Stampa il valore e si mette "a dormire" per un tempo arbitrario.*/
void *consumer(void *arg) {
while (1) {
int value;
printf("\t\tconsumer ready\n");
semaphore_P(full);
value = buffer;
semaphore_V(empty);
printf("\t\tconsume %d\n", value);
usleep(random() % 2000000);
}
}
int main(int argc, char *argv[]) {
pthread_t prod_t;
pthread_t cons_t;
full=semaphore_create(0);
empty=semaphore_create(1);
srandom(time(NULL));
pthread_create(&prod_t, NULL, producer, NULL);
pthread_create(&cons_t, NULL, consumer, NULL);
pthread_join(prod_t, NULL);
pthread_join(cons_t, NULL);
}
Se avessi più produttori e consumatori il codice funzionerebbe lo stesso in quanto i semafori valgono al più 1 quindi solo 1 produttore supera il blocco. Stessa cosa per quanto riguarda il consumatore.
Semafori Binari
SEMAFORO BINARIO USANDO I GENERALI (precisamente 2):
Sono semafori che soddisfano il seguente invariante:
0 <= init + nV - nP <= 1
/* init + nV - nP si dice "valore del semaforo" */
Dopo aver presentato il paradigma dei semafori binari e quello dei semafori generali ci si è posti la seguente domanda: tutti i problemi che si risolvono usando i semafori generali si possono risolvere usando invece i semafri binari? viceversa tutti i problemi che si risolvono con i semafori binari si possono risolvere con quelli generali? Quale fra i due paradigmi è più espressivo?
Si può dimostrare che i due paradimi hanno lo stesso potere espressivo. Per poter fare uqesta dimostrazione si deve essere in grado di implementare un tipo di semaforo servendosi dell'altro e viceversa.
bsem {
gsem pblock; //pblock si usa per indicare il valore del semaforo per cui la P si blocca
gsem vblock; //vblock si usa per inidicare il valore del semaforo per cui la V si blocca
}
binit(v):
bsem = malloc(struct bsem)
bsem.pblock.ginit(v)
bsem.vblock.ginit(1-v)
bP(): //bP sblocca vblock e blocca pblock
bsem.pblock.gP() //si tenta di chiamare la P prendendo pblock come parametro
bsem.vblock.gV()
bV(): //bV sblocca pblock e blocca vblock
bsem.pblock.gV()
bsem.vblock.gP()
//pblock e vblock hanno sempre valore opposto
Una generalizzazione della sezione critica è quella di inizializzare il semaforo ad un numero positivo anche maggiore di 1, ovvero consentiamo a "n" numero di processi che può prosegurie dopo una P ovvero:
semaphore s(4)
s.P()
use resource
s.V()
Introduzione al problema della cena dei filosofi.
Lezione del 27 ottobre 2017
Il Sistema Operativo UNIX
Unix, fin dalla sua nascita, presentava già le maggiori caratteristiche che sono tutt’ora presenti:
- scritto in C;
- multitasking;
- prevede un’interfaccia di System Call
- l’interprete dei comandi non è interno al kernel, ma è un processo a parte, con lo scopo di semplificare il kernel mettendo tutto ciò che fosse possibile al di fuori di questo;
- sistema operativo multi-user
- presenza di una SHELL
Shell UNIX
La SHELL è un interprete di comandi ed è chiamata così (shell = conchiglia) proprio perché è la parte esterna della struttura.
Caratteristiche principali
- storicamente, la prima shell fu sh, detta anche Bourne Shell;
- è possibile utilizzare il kernel tramite la SHELL;
- essa stessa rappresenta un linguaggio di scripting;
- è possibile quindi scrivere degli script, contenenti anche strutture di controllo proprie dei linguaggi di programmazione (if, else, while, ecc..);
- il simbolo $ (dollaro) come prompt di solito indica che si sta usando la shell come utente, questo previene l’esecuzione accidentale di programmi che potrebbero arrecare danni al sistema;
- il successore di sh sarà bash (Bourne-Again SHell).
Struttura di uno script Shell
[comandi] [complementi] [complementi oggetto]
Alcuni comandi della shell e strutture particolari
- grep: ricerca contenuti testuali;
- cat: copia stdin in stdout oppure ogni file che viene elencato ordinatamente in stdout, può quindi essere utilizzato per fare copie;
- ls: mostra il contenuto di una directory;
- more: nato per primo rispetto a less (funzionamento simile), utile per leggere file particolarmente lunghi mostrando una schermata per volta del file;
- less: versione più evoluta di more, chiamata così ironicamente, consente anche di tornare ad una "pagina" precedente, cosa che con il comando more non era possibile costringendo a dover scorrere tutto il file a partire dall’inizio se si desiderava leggere una pagina precedente;
- file: indica di che tipo è il file, usando una combinazione di evidenze del SO e di euristiche: tenta di dare delle classificazioni più dettagliate (ad esempio i file con estensione .c sono definiti come C Source Code);
- chmod: cambia i permessi di accesso ad un file
- per fare ciò, si considera il campo dei permessi come un numero ottale;
- es. rw+ rw+ rw- = 664 (write ad utente corrente e gruppo corrente, accesso solamente in lettura a chiunque altro;
- sticky bit: bit meno significativo che sostanzialmente indica che ogni utente è libero di fare ciò che vuole, ma solamente con i propri file;
- touch: se il file specificato non esiste lo crea, in caso contrario imposta la data di modifica del file a quella corrente;
- passwd: cambia la password dell’utente corrente, questo comando viene considerato sempre come lanciato da root;
- man comando: mostra il manuale del comando specificato;
- echo [string]: mette in output i parametri specificati, utilizzato negli script per stampare
- EXEC BASH: exec sostituisce al processo corrente l’esecuzione del nuovo comando. Esempio a seguire.
Link Utili
Archivio storico dei manuali di Unix
System Call
Il ''catalogo'' delle System Call
Esempio di echo scritto tramite systemcall
Proviamo a scrivere il seguente script shell tramite le system call:
echo "this is a test please discard" > test
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
char *echoargv[] = {"/bin/echo", "this is a test please discard", NULL};
int main(int argc, char const *argv[]) {
int status;
int fd;
switch (fork()) {
case 0:
/*inserire qui le modifiche al programma che vado a lanciare
come ad esempio la reindirizzazione dell'output
fd = open("test", O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, STDOUT_FILENO);
close(fd);
*/
execve("/bin/echo", echoargv, NULL);
exit(0);
default:
wait(&status);
break;
case -1:
fprintf(stderr, "exec err\n", );
break;
}
}
Cosa succede se il figlio termina subito ed il padre aspetta 10 secondi prima di terminare?
- Il processo che ha eseguito echo rimane come <defunct> ovvero un processo zombie.
E se invece succede il contrario?
- Tutti gli orfani vengono adottati da init o systemd e verranno eseguiti e chiusi tutti.
Lezione del 31 ottobre 2017
Recap sui semafori
Cos'è un semaforo?
Un dato astratto che serve per gestire l'accesso alle risorse condivise.
Semaforo generale:
P blocca in caso il semaforo sia a 0 (riduce di 1 il semaforo)
V non bloccante ed incrementa di 1 il valore del semaforo
Semaforo binario:
P e V bloccanti, P in caso il semaforo sia 0 V in caso il semaforo sia 1.
Il valore del semaforo non può essere negativo.
Potere espressivo di un paradigma:
L'insieme dei problemi che si possono risolvere (programmi che si possono scrivere)
Se ho un problema risolvibile con un semaforo binario può essere implementato con un semaforo generale e viceversa. Quindi semafori binari e generali hanno lo stesso problema espressivo.
Implementazione di un semaforo binario
Trasformiamo l'implementazione di un semaforo generale (visto in precedenza) in uno che implementa un semaforo binario.
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>
#include<suspend.h>
#include<tlist.h>
#include<semaphore.h>
#define mutex_in(X) pthread_mutex_lock(X)
#define mutex_out(X) pthread_mutex_unlock(X)
struct semaphore {
volatile long value;
pthread_mutex_t lock;
struct tlist *q;
};
semaphore semaphore_create(long initval) {
semaphore s = malloc(sizeof(*s));
if (s) {
s->value = initval;
s->q = NULL;
pthread_mutex_init(&s->lock, NULL);
}
return s;
}
void semaphore_destroy(semaphore s) {
pthread_mutex_destroy(&s->lock);
free(s);
}
void semaphore_P(semaphore s) {
mutex_in(&s->lock);
if (s->value <= 0) {
tlist_enqueue(&s->q, pthread_self());
mutex_out(&s->lock);
suspend();
} else {
if (tlist_empty(s->q))
s->value--;
else
wakeup(tlist_dequeue(&s->q));
mutex_out(&s->lock);
}
}
void semaphore_V(semaphore s) {
mutex_in(&s->lock);
if (s->value >= 1){
tlist_enqueue(&s->q, pthread_self());
mutex_out(&s->lock);
suspend();
} else {
if (tlist_empty(s->q))
s->value++;
else
wakeup(tlist_dequeue(&s->q));
mutex_out(&s->lock);
}
}
Implementazione 5 filosofi
Versione Errata
#include<stdio.h>
#include<stdint.h>
#include<pthread.h>
#include<semaphore.h>
semaphore stick[5];
char philo_status[]="TTTTT";
void *philo(void *arg) {
int i = (uintptr_t)arg;
printf("philo thinking: %d\n",i);
while (1) {
usleep(random() % 200000); //thinking
semaphore_P(stick[i]);
//quando prendo una bacchetta ho un ritardo nel prendere la seconda.
usleep(100000);
//Evidenzia il deadlock presente in questa implementazione
semaphore_P(stick[(i+1)%5]);
philo_status[i] = 'E';
printf("philo eating: %d |%s|\n",i,philo_status);
usleep(random() % 200000); //eating
philo_status[i] = 'T';
printf("philo thinking: %d |%s|\n",i,philo_status);
semaphore_V(stick[i]);
semaphore_V(stick[(i+1)%5]);
}
}
int main(int argc, char *argv[]) {
int i;
pthread_t philo_t[5];
srandom(time(NULL));
for (i=0; i<5; i++)
stick[i]=semaphore_create(1);
for (i=0; i<5; i++)
pthread_create(&philo_t[i], NULL, philo, (void *)(uintptr_t) i);
for (i=0; i<5; i++)
pthread_join(philo_t[i], NULL);
}
Versione corretta
Il problema fondamentale è che tutti i filosofi potrebbero prendere una bacchetta allo stesso tempo. Se inseriamo un filosofo "mancino" ovvero che si muove in senso contrario rispetto agli altri, risolviamo il problema di deadlock circolare.
#include<stdio.h>
#include<stdint.h>
#include<pthread.h>
#include<semaphore.h>
#define MIN(X,Y) ((X)<(Y)) ? (X) : (Y)
#define MAX(X,Y) ((X)>(Y)) ? (X) : (Y)
semaphore stick[5];
char philo_status[]="TTTTT";
void *philo(void *arg) {
int i = (uintptr_t)arg;
printf("philo thinking: %d\n",i);
while (1) {
usleep(random() % 200000); //thinking
semaphore_P(stick[MIN(i, (i + 1) % 5)]);
//quando prendo una bacchetta ho un ritardo nel prendere la seconda.
usleep(100000);
//Evidenzia il deadlock presente in questa implementazione
semaphore_P(stick[MAX(i, (i + 1) % 5)]);
philo_status[i] = 'E';
printf("philo eating: %d |%s|\n", i, philo_status);
usleep(random() % 200000); //eating
philo_status[i] = 'T';
printf("philo thinking: %d |%s|\n", i, philo_status);
semaphore_V(stick[i]);
semaphore_V(stick[(i+1)%5]);
}
}
int main(int argc, char *argv[]) {
int i;
pthread_t philo_t[5];
srandom(time(NULL));
for (i=0; i<5; i++)
stick[i]=semaphore_create(1);
for (i=0; i<5; i++)
pthread_create(&philo_t[i], NULL, philo, (void *)(uintptr_t) i);
for (i=0; i<5; i++)
pthread_join(philo_t[i], NULL);
}
Scrittura alternativa
Proviamo a scrivere con la seguente scrittura una "non" soluzione della cena dei filosofi.
<Si>
<await (Ci) -> Ti>
S.P()
<await (S.value > 0) -> S.value -->
S.V()
<S.value++>
Soluzione basata sullo stato del precedente (quella che consente la congiura dei filosofi).
Philo(i):
while(1):
/*think*/
<await(status[(i+4)%5] == 'T' && status[(i+1)%5] == 'T') -> status[i] = 'E'>
/*eat*/
<status[i] = 'T'>
Come trasformo questa scrittura in un tentativo di soluzione con semafori?
- Ingredienti:
- mutex
- 1 await semaphore Si
- waiting procs counter per await Wi
- Quando incontriamo:
- <Si>: mutex.P();
- Si : SIGNAL
Avremo quindi:
<await (Ci) -> Ti> : mutex.P();
if(!Ci) {
Wi++;
mutex.P();
Si.P();
Wi--;
}
Ti;
SIGNAL;
Com'è fatto SIGNAL?
if (C1 && W1 > 0) S1.W;
nondet_else (C2 && W2 > 0) S2.W;
nondet_else (C3 && W3 > 0) S3.W;
...
else mutex.V();
Pseudocodice:
void signal(void){
int i;
for(i = 0; i <5; i++){
if (status[(i+4)%5] != 'I' || status[(i+1)%5] != 'I' && waiting[i] > 0) {
semaphore_V(waitsem[i]);
return;
}
}
semaphore_V(mutex);
}
process philo(i):
while(1) {
semaphore_P(mutex);
if (status[(i+4)%5] != 'I' || status[(i+1)%5] != 'I') {
waiting[i]++;
semaphore_P(waitsem[i]);
waiting[i]--;
}
status[i] = 'E';
signal();
// eat
semaphore_P(mutex);
status[i] = 'I';
signal();
}
Articolo di Gregory R. Andrews "A method for solving synchronization problems". Contiene la spiegazione del metodo passing le baton sopra illustrato più alcuni approfondimenti.
Lezione del 3 novembre 2017
System call : Creare l'astrazione di processo Fork: crea processi senza eseguire programmi a somiglianza di quello generante _exit : termina i processi immediatamente
Tutti i processi vengono uccisi o si "suicidano" (sistemi operativi è una materia triste)
Il main non è lo starting point di esecuzione dei programmi ma è questo "guscio" formato da:
set-up argc argv ret_value = main(arc,argv) exit(ret_value)
atexit(): caricare dei puntatori a funzioni che vengono richiamate quando il programma termina
int main (int argc, char *argv[]){
pid_t pid;
int status;
switch (pid = fork()){
case 0: //child
printf(child )
int i = 0;
i = 10 /i;
_exit (42) //usa i bit di 42 come exit status
break;
default:
waitpid(pid,&status,0);
printf("exit status = %d\n",WEXITSTATUS(status));
printf("natural death? = %d\n",WIFEEXITED(status)); //per vedere se un processo è morto "naturalmente"
printf("abort= %d\n",WIFSIGNALED(status));
case -1:
printf("fork error\n")
break;}
}
Segnali 8 = floating point exception 11 = segmentation fault
int main (int argc, char *argv[]){
pid_t pid;
int status;
switch (pid = fork()){
case 0: //child
printf("child\n" )
int *i = (int *)1;
*i = *i +1;
_exit (42) //usa i bit di 42 come exit status
break;
default:
waitpid(pid,&status,0);
printf("exit status = %d\n",WEXITSTATUS(status));
printf("natural death? = %d\n",WIFEEXITED(status)); //per vedere se è morto "naturalmente"
printf("abort? = %d\n",WIFSIGNALED(status));
printf("signal? = %d\n",WTERMSIG(status));
case -1:
printf("fork error\n")
break;}
}
Vita dei processi : -fork() -exit() -wait()
uso di file: open, close, read, write, lseek, fcntl, ioctl, pread, pwrite, readv, writev
Lezione del 7 novembre 2017
Recap lezione precedente
< T >
< await C -> S >
-----------------
< Tj > ===> mutex.P();
Tj
SIGNAL();
< await Ci -> Ui > ===> mutex.P();
if (!C) {
wi++;
mutex.V()
Si.P()
wi--;
}
Ui
SIGNAL
SIGNAL ===> if (C1 && V1 > 0) S1.V();
nondet_else (C2 && V2 > 0) S1.V();
. . .
else mutex.V();
Problema Scrittori e Lettori
Pseudocodice
Esistono processi scrittori e lettori. I lettori vogliono leffere una struttura dati, gli scrittori vogliono modificarla. Il problema nasce quando un processo vuole modificare la struttura.
while(1)
startread()
nr++
READ
nr--
endread()
while(1)
startwrite()
nw++
WRITE
nw--
endwrite()
Condizioni accettabili:
(nr == 0 && nw == 0) || (nr > 0 && nw == 0) || (nr == 0 && nw == 1)
oppure
(nw == 0 && nr >= 0) || (nr == 0 && nr == 1)
//lettore
while (1)
<await nw == 0 -> nr++>
READ
<nr-->
//scrittore
while (1)
<await nr == 0 && nw == 0 -> nw++>
WRITE
<nw-->
Primo passaggio di trasformazione meccanica:
//lettore
while (1)
mutex.P()
if (nw > 0) {
wr++; //waiting readers
mutex.V();
Sr.P();
wr--;
}
nr++;
SIGNAL;
READ
mutex.P()
nr--;
SIGNAL
//scrittore
while (1)
mutex.P()
if (nr > 0 || nw > 0) {
ww++; //waiting writers
mutex.V();
Sw.P();
ww--;
}
nw++;
SIGNAL;
WRITE
mutex.P()
nw--;
SIGNAL;
SIGNAL:
if (nw == 0 && wr > 0) Sr.V();
nondet_else (nw == 0 && nr == 0 && ww > 0) Sw.V();
else mutex.V();
mutex:
semaphore mutex(1);
semaphore Sr(0), Sw(0);
int nr, nw, wr, ww;
//Starvation per i lettori che vanno sempre avanti e gli scrittori che attendono sempre.
SIGNAL:
if (nw == 0 && wr > 0) Sr.V();
/*nondet_*/else (nw == 0 && nr == 0 && ww > 0) Sw.V();
else mutex.V();
//Starvation per entrambi
SIGNAL:
if (nw == 0 && nr == 0 && ww > 0) Sw.V();
/*nondet_*/else (nw == 0 && wr > 0) Sr.V();
else mutex.V();
Codice
#include<stdio.h>
#include<stdint.h>
#include<pthread.h>
#include<semaphore.h>
#define NREADERS = 5;
#define NWRITERS = 5;
int nr, nw, wr, ww;
semaphore mutex;
semaphore ok2read;
semaphore ok2write;
void signal(void) {
if (nw == 0 && wr > 0) semaphore_V(ok2read);
if (nr == 0 && nw == 0 && ww > 0) semaphore_V(ok2write);
semaphore_V(mutex);
}
void startread(void) {
semaphore_P(mutex);
if (nw > 0) {
wr++;
semaphore_V(mutex);
semaphore_P(ok2read);
wr--;
}
nr++;
signal()
}
void endread(void) {
semaphore_P(mutex);
nr--;
signal()
}
void startwrite(void) {
semaphore_P(mutex);
if (nr > 0 || nw > 0) {
ww++;
semaphore_V(mutex);
semaphore_P(ok2write);
ww--;
}
nw++;
signal()
}
void endwrite(void) {
semaphore_P(mutex);
nw--;
signal();
}
void *reader(void *arg) {
int i = (uintptr_t)arg;
while(1){
usleep(random() %200000);
startreade();
printf("nr %d nw %d -- wr %d ww %d \n", nr, nw, wr, ww );
usleep(random() %200000);
/*READ*/
endread();
}
}
void *writer(void *arg) {
int i = (uintptr_t)arg;
while(1){
usleep(random() %200000);
startwrite();
printf("nr %d nw %d -- wr %d ww %d \n", nr, nw, wr, ww );
/*READ*/
usleep(random() %200000);
endwrite();
}
}
int main(int argc, char *argv[]) {
int i;
pthread_t treader[NREADERS];
pthread_t twriter[NWRITERS];
srandom(time(NULL));
mutex = semaphore_create(1);
ok2read = semaphore_create(0);
ok2write = semaphore_create(0);
for (i=0; i<NREADERS; i++)
pthread_create(&treader[i], NULL, treader, (void *)(uintptr_t) i);
for (i=0; i<NWRITERS; i++)
pthread_create(&treader[i], NULL, twriter, (void *)(uintptr_t) i);
for (i=0; i<5; i++)
pthread_join(treader[i], NULL);
for (i=0; i<5; i++)
pthread_join(twriter[i], NULL);
}
Programma che stampa ITALIA con semafori
#include<stdio.h>
#include<stdint.h>
#include<pthread.h>
#include<semaphore.h>
char chars[] = {'I', 'T', 'A', 'L', '\n'};
#define NPROC sizeof(chars)
char seq[] = {0, 1, 2, 3, 0, 2, 4};
#define NSEQ sizeof(seq)
semaphore sem[NPROC];
int k = 0;
void *printchar(void *argv) {
uintptr_t i = (uintptr_t) argv;
while (1){
semaphore_P(sem[i]);
putchar(chars[i]);
k = (k + 1) %NSEQ;
semaphore_V(sem[seq[k]]);
usleep(random() %100000);
}
}
int main(int argc, char *argv[]) {
int i;
pthread_t tchars[NPROC];
srandom(time(NULL));
sem[0] = semaphore_create(1);
for (i=1; i<NPROC; i++)
sem[i] = semaphore_create(0);
for (i=0; i<NPROC; i++)
pthread_create(&tchars[i], NULL, printchar, (void *)(uintptr_t) i);
for (i=0; i<NPROC; i++)
pthread_join(tchars[i], NULL);
}
Soluzione possibile esercizio c.1 2017-09-11
/*
esame 2017-09-11
esercizio c.1
*/
enum DIRECTION{N, E, S, W};
int wv[4]; //waiting veicle
int nv; //numero veicoli
semaphore ok2enter[4];
semaphore mutex;
crossing_enter(enum DIRECTION d){
semaphore_P(mutex);
nv++;
if (nv > 0){
wv[d]++;
semaphore_V(mutex);
semaphore_P(ok2enter[d]);
wv[d]--;
}
else
semaphore_V(mutex);
}
crossing_exit(enum DIRECTION d){
semaphore_P(mutex);
nv--;
if (nv == 0)
semaphore_V(mutex);
else
for(i = 1; i < 5; i++){
newdir = (d + i) % 4;
if (wv[newdir] > 0) {
semaphore_V(ok2enter[newdir]);
break;
}
}
}
main() {
ok2enter[i] = semaphore_create(0);
mutex = semaphore_create(1);
}
Lezione del 10 novembre 2017
Lettura di directory: getdents (la syscall) e' scomodissima, quinndi si usa opendir/readdir/rewinddir/closedir oppure scandir
System call per il file system: mkdir, rmdir, chdir, getcwd, link, symlink, stat (fstat, lstat), chown (fchown, lchown), chmod (fchmod).
fchdir per cambiare temporaneamente dir.
System call per programmazione a eventi: select, poll
Script: sintassi sharp-bang (#!) per la scelta dell'interprete.
Variabili locale e di ambiente nelle shell.
Esercizio: Un programma che dato come parametro una directory ritorni ciò che vi è all'interno e nelle sottodirectory interne
Esempio da terminale : find /tmp -print Tramite le seguanti system call:
- scandir() = Filtra gli elementi.Ritorna un intero,il numero delle entry. Ogni singolo elemento è in allocazione dinamica
- readdir() = Si accede a tutti gli elementi,li leggo uno alla volta
Come scriviamo il programma usando una funzione ricorsiva?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
int notdots(const struct dirent *d) {
if (strcmp(d->d_name,".") == 0)
return 0;
if (strcmp(d->d_name,"..") == 0)
return 0;
return 1;
}
void recscan(char *path)
{
struct dirent **namelist = NULL;
int i, n;
n = scandir(path, &namelist, notdots, alphasort);
if (n == -1) {
perror("scandir");
exit(EXIT_FAILURE);
}
for (i=0; i<n; i++) {
size_t pathlen = strlen(path) + strlen(namelist[i]->d_name) + 2;
char newpath[pathlen];
snprintf(newpath, pathlen, "%s/%s", path, namelist[i]->d_name);
printf("%s\n", newpath);
if (namelist[i]->d_type == DT_DIR)
recscan(newpath);
free(namelist[i]);
}
free(namelist);
}
int main(int argc, char *argv[]) {
char *path;
path = (argc > 1) ? argv[1] : ".";
recscan(path);
exit(EXIT_SUCCESS);
}
Per confronto, il programma seguente usa opendir/readdir/closedir al posto di scandir:
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int notdots(const struct dirent *d) {
if (strcmp(d->d_name,".") == 0)
return 0;
if (strcmp(d->d_name,"..") == 0)
return 0;
return 1;
}
void recreaddir(char *path)
{
DIR *d;
struct dirent *de;
int i, n;
d = opendir(path);
if (d == NULL) {
perror("opedir");
exit(EXIT_FAILURE);
}
while ((de = readdir(d)) != NULL) {
if (notdots(de)) {
size_t pathlen = strlen(path) + strlen(de->d_name) + 2;
char newpath[pathlen];
snprintf(newpath, pathlen, "%s/%s", path, de->d_name);
printf("%s\n", newpath);
if (de->d_type == DT_DIR)
recreaddir(newpath);
}
}
closedir(d);
}
int main(int argc, char *argv[]) {
char *path;
path = (argc > 1) ? argv[1] : ".";
recreaddir(path);
exit(EXIT_SUCCESS);
}
System call per accedere ai file: -Open() -Close() File descriptor: 0(stdin) 1(stderr) 2(stdout) File: -lseek() = Imposta l'indicatore d posizione del file -ioctl() = Control device.Con i valori di tag si sceglie l'operazione voluta
File System: -mkdir = Path e permessi di accesso -rmdir = Cancella directory purchè sia vuota -chmod = Modifica i permessi a file e directory.Richiede il pathname -chown = Cambia il proprietario del file -stat = Tutte le informazioni di un file(device su cui è memorizzato,tipo,quanti nodi ha il file,proprietario,blocco sui cuiè memorizzato,tre tempi : ultimo accesso,modificazione e cambiamento di stato[creazione del file]) IL NOME NON FA PARTE DELLE INFO DI UN FILE!! Inode number : rappresenta l'id di un file
Varianti con prefisso f : servono per i file già aperti (hanno come parametro il file descriptor e non già il pathname).
- fchmod
- fchown
Link simbolico: Un collegamento simbolico è un file contenente un percorso relativo od assoluto al file o directory a cui fa riferimento
Varianti con prefisso l sono definite come chiamate opache Esempio
- lchown = Come una chown ma non deferenzia il link simbolico
Non esiste una system call per cancellare i file,ma solo quella per togliere il NOME al file,ovvero unlink() Fino a quando il file è aperto,esso può esistere anche senza nome es: file temporanei
- access = Chiede se si ha il permesso per eseguire una operazione di apertura di un file o directory (si può leggere, scrivere eseguire?)
- chdir = Cambia directory.La directory corrente è rappresentata dal parametro di un processo
- fchdir = Cambia directory usando come parametro un file aperto.Può servire,per esempio,per saltare in una directory e poi tornare indietro
- mount = Rendere accessibile un contenuto (Device).Unico albero di file sistem,deve venire esteso per i device che vogliamo "montare".Di solito l'operazione avviene in una cartella vuota.
- synch = Non ha parametri e sembra non fare nulla ma serve a svuotare le chache
- umask = Assegna il parametro per indicare la modalità di protezione dei file
- chroot = Ridefinisce la root. Crea una Security cage che non è annullabile.
- truncate = Decidere la lunghezza di un file
Programmazione ad eventi : main event loop. Si ferma attendendo un evento (es. click del mouse) e a seconda di esso agisce di conseguenza Le system call per realizzare la programmazione ad eventi sono poll e select (con le varianti ppoll e pselect che vedremo in seguito).
Programma di chat tra due terminali
/*
Per eseguire il programma aprire due terminali ed eseguire i comandi
./chat f1 f2
./chat f2 f1
*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#define BUFSIZE 1024;
char buf [BUFSIZE];
int main (int arc char *argv[]){
fd_in = open(argv[1], O_RDONLY | O_NONBLOCK);
fd_out = open(argv[2], O_WRONLY | O_NONBLOCK);
struct pollfd myfds[] = {{STDIN_FILENO, POLLIN}, {fd_in, POLLIN}};
while (1){
int n = poll(myfds, 2 , -1);
if (myfds[0].revents & POLLIN){
int nn = read(STDIN_FILENO, buf, BUFSIZE);
write(fd_out, buf, nn);
}
if (myfds[1].revents & POLLIN){
int nn = read(fd_in, buf, BUFSIZE);
write(STDOUT_FILENO, buf, nn);
}
}
}
- pipe() = Fa comunicare i processi tra loro.Restituisce due descrittori aperti (che sono gli estremi del "tubo")
Lezione del 14 novembre 2017
Definizione di Monitor
Un contenitore che contiene all'interno le variabili (del monitor) e delle funzioni, è simile ad una classe ma con delle differenze:
- le variabili del monitor sono normalmente accedute in mutua esclusione (i processi aspettano il loro turno per accedere)
- prevede le variabili di condizione, e due metodi: wait e signal; se un processo chiama wait viene sospeso, signal riattiva il processo
Soluzione al problema del buffer limitato con i monitor
monitor m {
variables...
condition c1, c2....
procedure entry function...
function...
}
process producer(i). i = 1,...,N:
while(1) {
x = produce(); //
bb.enqueue(x);
}
process consumer(i). i = 1,...,M:
while(1) {
y = bb.dequeue(); //
consume(y);
}
monitor bb {
queue_of_T buffer;
condition ok2produce; // buffer.lenght() < SIZE
condition ok2consume; // buffer.lenght() > 0
procedure entry void enqueue(T el) {
if (buffer.lenght() >= SIZE)
ok2produce.wait();
buffer.enqueue(el);
ok2consume.signal();
}
procedure entry Y dequeue(void) {
T el;
if (buffer.lenght() <= 0)
ok2consume.wait();
el = buffer.dequeue();
ok2produce.signal();
}
}
Uso della libreria C fornita per le esercitazioni sui monitor
Dimostrazione dell'equivalenza di potere espressivo fra monitor e semafori
monitor semaphore {
int value;
condition ok2P; // value > 0
procedure entry P() {
if (value <= 0) ok2P.wait();
value --;
}
procedure entry V() {
value++;
ok2P.signal();
}
semaphore(int i) { // constructor
value = i;
}
}
P e V dei semafori vs wait e signal:
- la wait è sempre bloccante la P solo se è 0
- la signal se c'è qualcuno va avanti altrimenti va perduta la V
- con la V il processo ha il diritto di continuare quanto ne ha voglia, con la signal il processo viene ripreso immediatamente
Decalogo
- mai condizioni con solo signal o solo wait
- se monitor allora solo monitor
- busy wait in monitor = deadlock
- se tutte le procedure entry iniziano con wait → deadlock
- tutti i dati condivisi del monitor devono essere nel monitor (non condivise) per evitare race condition
Dati i semafori implementare i monitor:
class monitor {
semaphore mutex = 1;
stack_of_sem urgent;
enter();
exit();
}
class condition {
condition(monitor m);
monitor m;
queue_of_sem qs;
wait();
signal();
}
monitor::enter(){
self.mutex.P();
}
monitor::exit(){
if (!self.urgent.empty())
urgent.pop().V();
else
self.mutex.V();
}
condition::condition(monitor m) {self.m = m}
condition_wait(condition C){
semaphore s = new semaphore;
self.qs.enqueue(s);
self.m.exit();
s.P();
}
condition_signal(condition C){
if (! self.qs.empty()) {
semaphore s = new semaphore;
self.m.urgent.push(s);
self.qs.dequeue().V();
s.P();
}
}
Soluzione del problema dei 5 filosofi con i monitor
monitor philom {
bool busystick[5];
condition okstick[i]; //busystick = false
procedure entry void starteat(int i) {
if (busystick[i]) okstick[i].wait();
busystick[i] = true;
if (busystick[(i+1)%5]) okstick[i+1]%5].wait();
busystick[(i+1) % 5] = true;
}
procedure entry void endeat(int i) {
busystick[i] = false
busystick[(i+1) % 5] = false;
okstick[i].signal;
okstick[(i+1)%5].signal();
}
process philo(i), i=1,...,5
while(1)
think()
philom.starteat();
eat()
philom.endeat();
Soluzione del problema dei lettori/scrittori con i monitor
process reader[i] {
while (1) {
...
rw.startread();
read
rw.endread();
}
process writer[i] {
while (1) {
...
rw.startwrite();
write
rw.endwrite();
}
monitor rw {
int nr, nw;
condition ok2read; // nw == 0
condition ok2write; // nw == 0 && nr == 0
}
procedure entry startread(void) {
if (nw > 0 || ww > 0) ok2read.wait()
nr++;
ok2read.signal()
}
procedure entry endread(void) {
nr--;
if (nr == 0) ok2write.signal()
}
procedure entry startwrite(void){
ww++;
if (nw > 0 || nr > 0) ok2write.wait()
ww--
nw++;
}
procedure entry endwrite(void){
nr--;
ok2read.signal()
if (nr == 0) ok2write.signal()
}
}
Lezione del 17 novembre 2017
Segnali.
Reentrant Functions
Lezione del 21 novembre 2017
Message Passing
I semafori e i monitor sono pensati in un modello di concorrenza dove i processi hanno memoria condivisa. Se eliminiamo questa ipotesi, ovvero pensiamo ad un modello a memoria privata (ogni processo ha la sua memoria) per poter fare delle computazioni utilizzando più processi contemporaneamente è necessario che i processi si parlino: si devono utilizzare meccanismi di message passing.
Le primitive principali di un sistema di message passing sono send e receive. Per scambiare i messaggi è necessario sapere anche l'identità dei processi comunicanti, per destinare il messaggio al processo giusto. La send prenderà l'identificativo del destinatario e il messaggio come argomenti, la receive avrà come parametro l'identificativo del mittente e il suo valore di ritorno sarà il messaggio stesso (oppure memorizzerà il messaggio in una variabile passata per riferimento; dipende dall'implementazione).
Occorre specificare la semantica del sistema. Si identificano tre tipi di scambio di messaggi:
- message passing asincrono: la send non è bloccante, la receive sì. Il messaggio parte sempre. Per ricevere un messaggio è necessario che ce ne sia almeno uno nella coda dei messaggi ricevuti, altrimenti si aspetta che ne arrivi uno. Si parla di message passing con memoria (il modello teorico prevede una dimensione infinita per la coda dei messaggi in arrivo)
- message passing sincrono: sia la send che la receive sono bloccanti. La send di un processo mittente e la receive del corrispondente destinatario si devono alternare perfettamente. Si parla di message passing a rendevouz o senza memoria.
- completamente asincrono (sottocaso del primo): né la send né la receive sono bloccanti. La send manda sempre e la receive ritorna un errore se non si hanno messaggi in casella.
I paradigmi che vediamo sono solo di unicast. In particolare non verranno considerate le problematiche legate a message passing in sistemi broadcast e multicast (questo è argomento del corso di sistemi concorrenti).
È possibile implementare message passing asincrono con quello sincrono e viceversa? Da un lato sì, dall'altro nì. Il message passing asincrono ha memoria, quindi implementare il servizio sincrono (senza memoria) è semplice. Inserire la memoria invece con un sistema sincrono per simularne uno asincrono invece è più difficile.
Implementazione di message passing sincrono mediante message passing asincrono
def ssend(dest, msg):
asend(dest, msg)
areceive(dest, ack)
def sreceive(sender, msg):
ret_value = areceive(sender, msg)
asend(sender,"") # Acknowledgement
return ret_value
# Nota: questo crea deadlock (colpa del modello)
Implementazione di message passing asincrono mediante message passing sincrono
Come si crea la memoria? Con un altro processo che fa da demone nel mezzo. Tuttavia si perde l'equivalenza tra i due paradigmi, perché non si sta implementando uno con l'altro mediante una libreria ma mediante un processo aggiuntivo. Il demone continua a ciclare indefinitamente, prendendo le richieste. Per le richieste di tipo send le mette in una struttura dati per il sender. Per le richieste di tipo receive prende i messaggi dalla struttura dati e li recapita al richiedente.
def asend(dest, msg):
ssend(SERVER, (SND,dest,msg))
def areceive(sender, msg):
ssend(SERVER, (RCV,sender,""))
ssend(SERVER, (sender, msg))
# Nota: serve un check sulla parte di codice che segue (non sono sicuro di aver copiato tutto e bene)
server process:
waiting = []
msgq = []
while True:
srecv(sender, (tag, proc, msg))
if tag == SND:
if (match(waiting[proc], sender)):
asend(proc, (sender,msg))
match(waiting[proc], sender = None)
else:
msgq[proc].append((sender,msg))
elif tag == RCV:
if (senx, msg) = msgq.get(proc, sender):
asend(proc, (senx,msg))
else:
waiting[sender] = proc
Lezione del 24 novembre 2017
creazione gruppi, awk, find, grep, xarg
Lezione del 28 novembre 2017
Esercizi Prog. Concorrente
Lezione del 1 dicembre 2017
Esercizi (prove pratiche)
Lezione del 5 dicembre 2017
Esercizi Prog. Concorrente
Lezione del 12 dicembre 2017
la macchina emulata uARM
Lezione del 14 dicembre 2017
Esercizi
Lezione del 15 dicembre 2017
presentazione delle specifiche di phase1