POSIXstud.netgroup.uniroma2.it/~lorenzo/fondinf2016/posix.pdf · 2017-01-26 · sistema finchè la...

Post on 25-Feb-2020

8 views 0 download

Transcript of POSIXstud.netgroup.uniroma2.it/~lorenzo/fondinf2016/posix.pdf · 2017-01-26 · sistema finchè la...

POSIX

LORENZO.BRACCIALE@UNIROMA2.IT

LE SYSTEM CALL •  Alcune operazioni possono essere

effettuate solo dal kernel del sistema opeativo •  Ad esempio operazione di I/O (scrivere/

leggere file) •  o allocare memoria…

•  I programmi devono quindi richiedere al kernel di effettuare queste operazioni •  Invocando funzioni implementate nel

kernel •  Queste richieste (chiamate) sono

denominate System Call •  sono quindi l’interfaccia tra il sistema

operativo ed il nostro programma Kernel

Programmi

Sys calls

SPERIMENTIAMO Strace: runs the specified command until it exits. It intercepts and records the system calls which are called by a process and the signals which are received by a process. (apt-get install strace)

root@aquilante:~# strace ls!execve("/bin/ls", ["ls"], [/* 17 vars */]) = 0!brk(0) = 0x9b48000!access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)!mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb778e000!access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)!open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3!fstat64(3, {st_mode=S_IFREG|0644, st_size=18086, ...}) = 0!mmap2(NULL, 18086, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7789000!close(3) = 0!…!

SYS CALLS E LIBRERIE •  Spesso le system call non vengono chiamate

direttamente ma mediante librerie •  Alcune funzioni di libreria fanno da “wrapper” per

le syscall •  Dal nostro programma invochiamo le funzioni di

libreria che a loro volta invocano le syscall •  Ad esempio le funzioni printf, malloc, open sono

implementate nella glibc attraverso delle chiamate alle system call

•  Spesso quindi non ci interessa utilizzare le system call direttamente, ma attraverso funzioni di libreria, ma…

•  Problema: queste funzioni che chiamiamo, sono disponibili per tutti i sistemi operativi?

•  O dobbiamo ri-scrivere il nostro programma per ogni sistema operativo su cui vogliamo farlo eseguire? Kernel

Applicazione

Sys calls

Librerie

Interfacce applicative (API)

QUANTI OS ESISTONO? http://en.wikipedia.org/wiki/List_of_operating_systems Più di 700 sistemi operativi diversi!

Serve uno standard!

CHE COS’E’ POSIX? •  POSIX (Portable Operating System Interface for Unix) è il

nome che indica la famiglia degli standard definiti dall’IEEE denominati formalmente IEEE 1003. •  Nome standard internazionale è ISO/IEC 9945. •  15 documenti, inizialmente rilasciati nel 1988

•  Specifica l'interfaccia comune del sistema operativo con utente e software a livello di: •  API: processi, timer, thread, file, segnali, terminale, rete … •  Shell: Korn Shell •  Utilities: awk, ed, echo …

SISTEMI OPERATIVI COMPLIANT POSIX A/UX AIX BSD/OS[citation needed] DSPnano[citation needed] HP-UX INTEGRITY IRIX LynxOS[citation needed] MPE/iX[citation needed] OS X[15] QNX[16] RTEMS (POSIX 1003.13-2003 Profile 52) Solaris Tru64 Unison RTOS[citation needed] UnixWare[17]

BeOS (and subsequently Haiku) FreeBSD[18] Contiki Darwin (core of OS X and iOS)

illumos GNU/Linux MINIX NetBSD Nucleus RTOS

OpenBSD OpenSolaris PikeOS RTOS RTEMS Sanos

SkyOS Syllable VSTa VxWorks

full compliant mostly compliant

src: wikipedia

su windows: cygwin

POSIX Posix definisce l’interfaccia tra l’applicazione e le librerie

In ogni sistema operativo, all’interno delle librerie, le funzioni possono essere implementate in modo diverso

Kernel

Applicazione

Sys calls

Librerie

Interfacce applicative (API)

C STDLIB E POSIX Le librerie POSIX sono un superset di quelle definite dall’ANSI C

Funzioni ANSI C

Funzioni POSIX

COSA VEDREMO •  Processi •  memoria condivisa •  Timers e gestione del tempo

apt­get install manpages­posix manpages­posix­dev#

SINCRONIZZAZIONE

INTRODUZIONE A INTERRUPT, POLLING E CALLBACKS E RIPASSO

PROBLEMA •  Dovete fare due cose contemporaneamente

1.  La pasta 2.  Il sugo

•  Problema: cucinare il sugo ma ricordarsi di scolare la pasta

•  Possibili approcci algoritmici:

•  Assaggiare la pasta ogni minuto per sapere se è cotta •  Impostare una sveglia

POLLING

INTERRUPT

IN INFORMATICA… •  Dobbiamo scrivere sul disco

•  tempo di accesso ~ms •  velocità processore ~ Ghz

•  Strategie: •  Polling: leggere periodicamente un registro finchè lo status

non è cambiato •  Se lo facessimo all’interno di un driver, bloccheremmo tutto il

sistema finchè la scrittura non è andata a buon fine •  Alternativa: usare un timer di sistema (ma non siamo precisi e

comunque consumiamo risorse) •  Interrupt: l’hardware (ad es. l’hard disk o la scheda di rete) o

il software interrompono quello che il sistema operativo sta facendo, invocando una funzione del driver in modo asincrono

INTERRUPT •  Un interrupt hardware è

un segnale asincrono generato esternamente che richiede un interruzione del normale flusso (ad es. keystroke) mediante l’invio di un interrupt request (IRQ)

•  Un interrupt software è generato internamente (ad es. divisione per 0)

•  Ogni interrupt richide l’esecuzione di un particolare codice per gestire l’interrupt (handler)

lorenzo@ubuntu:~/fractal$ cat /proc/interrupts CPU0 CPU1 0: 44 0 IO-APIC-edge timer 1: 22 45187 IO-APIC-edge i8042 4: 36578 23 IO-APIC-edge 6: 0 3 IO-APIC-edge floppy 7: 0 0 IO-APIC-edge parport0 8: 0 0 IO-APIC-edge rtc0 9: 0 0 IO-APIC-fasteoi acpi 12: 10767 2537 IO-APIC-edge i8042 14: 0 0 IO-APIC-edge ata_piix 15: 11631 20 IO-APIC-edge ata_piix 16: 885 99 IO-APIC-fasteoi Ensoniq AudioPCI 17: 8612 31544 IO-APIC-fasteoi ehci_hcd:usb1, ioc0 18: 40 0 IO-APIC-fasteoi uhci_hcd:usb2 19: 8489 20 IO-APIC-fasteoi eth0

Approfondirete a Sistemi Operativi…

IRQ n° interrupts type name of device

CALLBACKS •  Anche all’interno dei programmi è necessario impostare

dei meccanismi asincroni di chiamata a funzione •  “callbacks”

•  Tra poco vedremo timer e threads…

•  Timer: chiama una certa funzione tra un certo tempo •  Thread: fai girare questa funzione contemporaneamente

•  Problema: Come scrivereste una libreria per implementare un timer? •  mio_timer( funzione_da_chiamare, tempo ) •  o per i thread: crea_thread( funzione_da chiamare )

•  E’ necessario ripassare i puntatori a funzione!

PUNTATORI A FUNZIONE (RIPASSO) Prototipo di una funzione C: tipo_restituito nome_funzione (parametro1, parametro2, ...) Puntatore alla funzione C: tipo_restituito ( * nome_ptr_a_funz ) (parametro1, parametro2, ...) Per assegnarlo: nome_ptr_a_funz = funzione_assegnata Per invocare la funzione: nome_ptr_a_funz (parametro1, parametro2, ...)

ESERCIZIO: DICHIARAZIONE •  Dichiarare un puntatore (nome: pippo) alla funzione:

•  int nome_funzione(int a, float b);

•  Soluzione

•  int (* pippo) (int a, float b);

ESERCIZIO: INVOCAZIONE Data una funzione: void print_hello (char *s){

printf(“Hello %s\n”, s);

}

Invocarla in un main mediante un puntatore a funzione

ESERCIZIO INVOCAZIONE: SOLUZIONE #include <stdio.h> void print_hello(char *s) { printf("Hello %s\n", s); } int main() { void (*print_ptr)(char *s); print_ptr = print_hello; print_ptr("world"); return 0; }

ESEMPI PIU’ OSCURI Funzione che ritorna un puntatore a funzione: int (*return_f())(char)

E’ una funzione chiamata “return_f” che non accetta argomenti. Ritorna un puntatore a funzione che ha come argomento un char e ritorna un intero.

ESERCIZIO: DICHIARAZIONE void (*signal(int signum, void (*handler)(int) ) )(int); Cos’è? Un prototipo di funzione! 1) La funzione signal vuole come parametri un intero signum, ed un puntatore handler ad una funzione che restituisce un void e che vuole come parametro un intero. 2) la funzione signal restituisce un puntatore ad una funzione che restituisce un void e che vuole come parametro un intero. If you're confused by the prototype at the top of this manpage, it may help to see it separated out thus: typedef void (*handler_type)(int); handler_type signal(int signum, handler_type handler);

src: http://www.cs.unibo.it/~ghini/didattica/sistemi1/c005.pdf

PROCESSI E THREADS

DOVE STUDIARE •  Gapil

•  http://users.lilik.it/~mirko/gapil/gapilch5.html#gapilse8.html

PROCESSI Definizione: istanza di un programma in esecuzione in modo sequenziale. Ogni processo ha un ID (Process ID – PID)

top

ARCHITETTURA •  Su linux qualunque processo può a sua volta generarne altri

(child process)

•  ogni processo è sempre generato da un altro (parent process)

•  una sola eccezione: dato che ci deve essere un punto di partenza esiste un processo speciale (che normalmente è /sbin/init), che viene lanciato dal kernel alla conclusione della fase di avvio; ha sempre il pid uguale a 1 e non è figlio di nessun altro processo

ARCHITETTURA •  I processi hanno una relazione

padre/figlio:

•  organizzazione gerarchica ad albero

•  Provare con “pstree”

UNA PANORAMICA SULLE FUNZIONI FONDAMENTALI •  In un sistema unix-like i processi vengono sempre creati da altri processi

tramite la funzione fork; il nuovo processo (che viene chiamato figlio) creato dalla fork è una copia identica del processo processo originale (detto padre), ma ha un nuovo pid e viene eseguito in maniera indipendente

•  Se si vuole che il processo padre si fermi fino alla conclusione del processo figlio questo deve essere specificato dopo la fork chiamando la funzione wait o la funzione waitpid

•  Quando un processo ha concluso il suo compito o ha incontrato un errore non risolvibile esso può essere terminato con la funzione exit

•  Normalmente si genera un secondo processo per affidargli l'esecuzione di un compito specifico (ad esempio gestire una connessione dopo che questa è stata stabilita), o fargli eseguire (come fa la shell) un altro programma. Per quest'ultimo caso si usa la seconda funzione fondamentale per programmazione coi processi che è la exec

PROCESS ID •  Ogni processo viene identificato dal sistema da un numero

identificativo univoco, il process ID o pid •  Il pid viene assegnato in forma progressiva ogni volta che un

nuovo processo viene creato, fino ad un limite che, essendo il pid un numero positivo memorizzato in un intero a 16 bit, arriva ad un massimo di 32768

•  Tutti i processi inoltre memorizzano anche il pid del genitore da cui sono stati creati, questo viene chiamato in genere ppid (da parent process ID). Questi due identificativi possono essere ottenuti usando le due funzioni getpid e getppid

GETPID E GETPPID !

#include <sys/types.h>!

#include <unistd.h>!!

pid_t getpid(void) !

Restituisce il pid del processo corrente.!

!

pid_t getppid(void)!Restituisce il pid del padre del processo corrente.!

!

Entrambe le funzioni non riportano condizioni di errore.!

LA FUNZIONE FORK !

#include <sys/types.h>!

#include <unistd.h>!

#

pid_t fork(void)#

!

Crea un nuovo processo.!

!

In caso di successo restituisce il pid del figlio al padre e zero al figlio; ritorna -1 al padre (senza creare il figlio) in caso di errore; errno può assumere i valori:!

EAGAIN non ci sono risorse sufficienti per creare un altro processo (per allocare la tabella delle pagine e le strutture del task) o si è esaurito il numero di processi disponibili!

ENOMEM non è stato possibile allocare la memoria per le strutture necessarie al kernel per creare il nuovo processo.!

LA FUNZIONE FORK •  Dopo il successo dell'esecuzione di una fork sia il processo padre

che il processo figlio continuano ad essere eseguiti normalmente a partire dall'istruzione successiva alla fork

•  Il processo figlio è però una copia del padre, e riceve una copia dei segmenti di testo, stack e dati ed esegue esattamente lo stesso codice del padre •  la memoria è copiata, non condivisa, pertanto padre e figlio

vedono variabili diverse •  La differenza che si ha nei due processi è che nel processo

padre il valore di ritorno della funzione fork è il pid del processo figlio, mentre nel figlio è zero •  un processo infatti può avere più figli, ed il valore di ritorno di

fork è l'unico modo che gli permette di identificare quello appena creato

•  al contrario un figlio ha sempre un solo padre, per cui si usa il valore nullo, che non è il pid di nessun processo

ESEMPIO

LA FUNZIONE FORK •  Normalmente la chiamata a fork può fallire solo per due ragioni, o

ci sono già troppi processi nel sistema (il che di solito è sintomo che qualcos'altro non sta andando per il verso giusto) o si è ecceduto il limite sul numero totale di processi permessi all'utente

•  L'uso di fork avviene secondo due modalità principali •  Si creano processi figli cui viene affidata l'esecuzione di una

certa sezione di codice, mentre il processo padre ne esegue un'altra.

•  È il caso tipico dei programmi server in cui il padre riceve ed accetta le richieste da parte dei programmi client, per ciascuna delle quali pone in esecuzione un figlio che è incaricato di fornire il servizio.

•  Il processo vuole eseguire un altro programma •  questo è ad esempio il caso della shell. In questo caso il

processo crea un figlio la cui unica operazione è quella di fare una exec subito dopo la fork.

LA FUNZIONE FORK •  Non si può dire quale processo fra il padre ed il figlio venga eseguito per

primo •  In generale l'ordine di esecuzione dipenderà, oltre che dall'algoritmo

di scheduling usato dal kernel, dalla particolare situazione in cui si trova la macchina al momento della chiamata, risultando del tutto impredicibile

•  Non si può fare nessuna assunzione sulla sequenza di esecuzione delle istruzioni del codice fra padre e figli, né sull'ordine in cui questi potranno essere messi in esecuzione. •  Se è necessaria una qualche forma di precedenza occorrerà

provvedere ad espliciti meccanismi di sincronizzazione, pena il rischio di incorrere nelle cosiddette race condition

•  Essendo i segmenti di memoria utilizzati dai singoli processi completamente separati, le modifiche delle variabili nei processi figli sono visibili solo a loro (ogni processo vede solo la propria copia della memoria), e non hanno alcun effetto sul valore che le stesse variabili hanno nel processo padre (ed in eventuali altri processi figli che eseguano lo stesso codice)

LA FUNZIONE FORK •  la lista dettagliata delle proprietà che padre e figlio hanno in comune dopo l'esecuzione di

una fork è la seguente: •  i file aperti e gli eventuali flag di close-on-exec impostati •  Gli identificatori per il controllo di accesso: l'user-ID reale, il group-ID reale, l'user-ID

effettivo, il group-ID effettivo ed i group-ID supplementari •  gli identificatori per il controllo di sessione: il process group-ID e il session id ed il

terminale di controllo •  la directory di lavoro e la directory radice •  la maschera dei permessi di creazione •  la maschera dei segnali bloccati e le azioni installate •  i segmenti di memoria condivisa agganciati al processo •  i limiti sulle risorse •  le variabili di ambiente

•  le differenze fra padre e figlio dopo la fork invece sono: •  il valore di ritorno di fork •  il pid (process id) •  il ppid (parent process id), quello del figlio viene impostato al pid del padre •  i valori dei tempi di esecuzione della struttura tms che nel figlio sono posti a zero. •  i lock sui file, che non vengono ereditati dal figlio. •  gli allarmi ed i segnali pendenti, che per il figlio vengono cancellati.

CHIUSURA DI UN PROCESSO •  Qualunque sia la modalità di conclusione di un processo, il kernel esegue comunque una

serie di operazioni: chiude tutti i file aperti, rilascia la memoria che stava usando, e così via; l'elenco completo delle operazioni eseguite alla chiusura di un processo è il seguente:

•  tutti i file descriptor sono chiusi. •  viene memorizzato lo stato di terminazione del processo. •  ad ogni processo figlio viene assegnato un nuovo padre (in genere init). •  viene inviato il segnale SIGCHLD al processo padre •  se il processo è un leader di sessione ed il suo terminale di controllo è quello della

sessione viene mandato un segnale di SIGHUP a tutti i processi del gruppo di foreground e il terminale di controllo viene disconnesso

•  se la conclusione di un processo rende orfano un process group ciascun membro del gruppo viene bloccato, e poi gli vengono inviati in successione i segnali SIGHUP e SIGCONT

•  è però necessario poter disporre di un meccanismo ulteriore che consenta di sapere come la terminazione è avvenuta: dato che in un sistema unix-like tutto viene gestito attraverso i processi, il meccanismo scelto consiste nel riportare lo stato di terminazione (il cosiddetto termination status) al processo padre.

•  quello che contraddistingue lo stato di chiusura del processo e viene riportato attraverso le funzioni wait o waitpid

LA FUNZIONE WAIT #include <sys/types.h>!

#include <sys/wait.h>!

!

pid_t wait(int *status)!

!

Sospende il processo corrente finché un (qualunque) figlio non è uscito, o finché un segnale termina il processo o chiama una funzione di gestione.!

!

La funzione restituisce il pid del figlio in caso di successo e -1 in caso di errore; errno può assumere i valori:!

EINTR la funzione è stata interrotta da un segnale.!

!

LA FUNZIONE WAITPID #include <sys/types.h>!

#include <sys/wait.h>!

!

pid_t waitpid(pid_t pid, int *status, int options)!

!

Attende la conclusione di un processo figlio con id “pid”.!

!

La funzione restituisce il pid del processo che è uscito, 0 se è stata specificata l'opzione WNOHANG e il processo non è uscito e -1 per un errore, nel qual caso errno assumerà i valori:!

!

EINTR se non è stata specificata l'opzione WNOHANG e la funzione è stata interrotta da un segnale.!

ECHILD il processo specificato da pid non esiste o non è figlio del processo chiamante.!

!

ESERCIZIO •  Creare un programma che esegua la fork.

•  Il padre deve aspettare l’uscita del figlio e riportare lo stato di uscita

•  Il figlio deve dormire 10 secondi

CHIUSURA DI UN PROCESSO •  Solitamente lo stato di chiusura di un figlio viene riportato al

padre, ma ci possono essere due eccezioni importanti

•  Il padre è già terminato

•  In questo caso il figlio è un processo orfano. In questo caso viene “adottato” da init.

•  Il figlio termina prima del padre ma lo stato di terminazione non è stato ricevuto

•  In questo caso il processo figlio è chiamato zombie, e rimane presente nella tabella dei processi.

TEST PROCESSI ORFANI E ZOMBIE •  Per generare processi orfani basta nel programma di test

imponendo a ciascun processo figlio due secondi di attesa prima di uscire

•  Sarà adottato da init o da “init --user” •  possiamo controllare con ps –efl (per vedere anche PPID)

•  Per generare processi zombie indichiamo al processo padre di aspettare 10 secondi prima di uscire

•  in questo caso, usando ps sullo stesso terminale (prima dello scadere dei 10 secondi)

LA FUNZIONE WAITPID •  La terminazione di un processo figlio è chiaramente un evento

asincrono rispetto all'esecuzione di un programma e può avvenire in un qualunque momento

•  Per questo motivo, una delle azioni prese dal kernel alla conclusione di un processo è quella di mandare un segnale di SIGCHLD al padre.

•  L'azione predefinita per questo segnale è di essere ignorato, ma la sua generazione costituisce il meccanismo di comunicazione asincrona con cui il kernel avverte il processo padre che uno dei suoi figli è terminato.

•  In genere in un programma non si vuole essere forzati ad attendere la conclusione di un processo per proseguire, specie se tutto questo serve solo per leggerne lo stato di chiusura (ed evitare la presenza di zombie)

•  la modalità più usata per chiamare queste funzioni è quella di utilizzarle all'interno di un signal handler. In questo caso infatti, dato che il segnale è generato dalla terminazione di un figlio, avremo la certezza che la chiamata a wait non si bloccherà.

LA FUNZIONE EXEC •  una delle modalità principali con cui si utilizzano i processi in

Unix è quella di usarli per lanciare nuovi programmi: questo viene fatto attraverso una delle funzioni della famiglia exec

•  Quando un processo chiama una di queste funzioni esso viene completamente sostituito dal nuovo programma; il pid del processo non cambia, dato che non viene creato un nuovo processo, la funzione semplicemente rimpiazza lo stack, lo heap, i dati ed il testo del processo corrente con un nuovo programma letto da disco.

•  Ci sono sei diverse versioni di exec (per questo la si è chiamata famiglia di funzioni) che possono essere usate per questo compito, in realtà (come mostrato in fig. 3.4), sono tutte un front-end a execve

LA FUNZIONE EXEC #include <unistd.h>!

int execve(const char *filename, char *const argv[], char *const envp[])!

!

La funzione exec esegue il file o lo script indicato da filename, passandogli la lista di argomenti indicata da argv e come ambiente la lista di stringhe indicata da envp; entrambe le liste devono essere terminate da un puntatore nullo. I vettori degli argomenti e dell'ambiente possono essere acceduti dal nuovo programma quando la sua funzione main è dichiarata nella forma main(int argc, char *argv[], char *envp[]) La funzione ritorna solo in caso di errore, restituendo -1; nel qual caso errno può assumere i valori: EACCES il file non è eseguibile, oppure il filesystem è montato in noexec, oppure non è un file

ESERCIZIO •  Creare una funzione che stampi i numeri primi tra X1 e X2

•  X1 = 990 milioni •  X2 = 1.1 miliardi

•  Dividere il carico tra più processi

•  N processi, ognuno elabora 1/n del set di numeri •  Vedere il tempo di esecuzione e l’impiego delle CPU (per i

sistemi multicore)

La gestione del segnale SIGCHLD

SEGNALI •  I segnali sono usati per notificare ad un processo l'occorrenza di un

qualche evento •  un breve elenco di possibili cause per l'emissione di un segnale è il

seguente: •  un errore del programma, come una divisione per zero o un

tentativo di accesso alla memoria fuori dai limiti validi. •  la terminazione di un processo figlio. •  la scadenza di un timer o di un allarme. •  il tentativo di effettuare un'operazione di input/output che non può

essere eseguita. •  una richiesta dell'utente di terminare o fermare il programma. In

genere si realizza attraverso un segnale mandato dalla shell in corrispondenza della pressione di tasti del terminale come C-c o C-z.1

•  l'esecuzione di una kill

TIPI DI SEGNALE •  I segnali sono identificati da MACRO definite in signal.h

Lista di alcuni segnali

HANDLER DI SEGNALI •  I segnali sono eventi asincroni che possono essere generati in

qualunque momento da cause interne ed esterne al processo •  Per poter gestire un segnale bisogna definire una funzione

chiamata handler •  Questo viene fatto con la seguente chiamata (esempio nel caso

del segnale SIGINT)

#include <signal.h>!signal(SIGINT, sig_handler);!!Dove la sig_handler è una funzione che deve essere definita nel programma e che deve accettare un intero!!!!

GESTIONE DEL SIGCHLD •  Come già detto non è sempre possibile che un processo padre

possa fermarsi ad attendere lo stato di terminazione dei suoi figli •  Per questo motivo risolta opportuno chiamare la funzione wait

all’interno dell’handler del segnale SIGCHLD •  All’avvio del processo settiamo l’handler per il segnale SIGCHLD •  Il processo padre può quindi continuare la sua esecuzione (per

esempio attendere input da tastiera, attendere nuove connessioni, ecc….)

•  Quando il padre ricede il segnale, viene quindi chiamata l’handler •  Una volta completata la funzione di handler, il padre ritorna al

punto in cui è stato interrotto

ESERCIZIO •  Utilizzare i segnali per accettare lo status di uscita di un

figlio senza bloccare il padre

GESTIONE DEL SIGCHLD

ESERCIZIO •  Utilizzare i segnali far richiedere la conferma dell’uscita da

un programma a seguito di Ctrl-C (SIGINT)

#include <stdio.h> #include <signal.h> void INThandler(int); void main(void) { signal(SIGINT, INThandler); while (1) pause(); } void INThandler(int sig) { char c; signal(sig, SIG_IGN); printf("OUCH, did you hit Ctrl-C?\n" "Do you really want to quit? [y/n] "); c = getchar(); if (c == 'y' || c == 'Y') exit(0); else signal(SIGINT, INThandler); }

http://www.csl.mtu.edu/cs4411.ck/www/NOTES/signal/install.html

Intercettiamo la chiusura di un programma

Comunicazione tra processi attraverso POSIX shared memory

LA COMUNICAZIONE TRA PROCESSI •  Sia nel caso di processi padre figlio, sia nel caso di processi

“indipendenti”, possiamo aver bisogno di un meccanismo di comunicazione

•  Esistono molti meccanismi diversi

•  Pipe, named pipe, socket locali, SYSV IPC, POSIX IPC •  Per poter utilizzare la shared memory POSIX bisogna

compilare il programma con l’opzione -lrt

APERTURA DI UN SEGMENTO DI MEMORIA CONDIVISA #include <mqueue.h>!int shm_open(const char *name, int oflag, mode_t mode)! Apre un segmento di memoria condivisa. La funzione restituisce un file descriptor positivo in caso di successo e -1 in caso di errore; nel quel caso errno assumerà gli stessi valori riportati da open La funzione è del tutto analoga ad open ed analoghi sono i valori che possono essere specificati per oflag, che deve essere specificato come maschera binaria comprendente almeno uno dei due valori O_RDONLY e O_RDWR E’ possibile verificare la creazione del segmento di shared memory all’interno della directory /dev/shm/!

APERTURA DI UN SEGMENTO DI MEMORIA CONDIVISA Possibili valori di oflag: O_RDONLY Apre il file descriptor associato al segmento di memoria condivisa per l'accesso in sola lettura. O_RDWR Apre il file descriptor associato al segmento di memoria condivisa per l'accesso in lettura e scrittura. O_CREAT Necessario qualora si debba creare il segmento di memoria condivisa se esso non esiste; in questo caso viene usato il valore di mode per impostare i permessi, che devono essere compatibili con le modalità con cui si è aperto il file. O_EXCL Se usato insieme a O_CREAT fa fallire la chiamata a shm_open se il segmento esiste già, altrimenti esegue la creazione atomicamente. O_TRUNC Se il segmento di memoria condivisa esiste già, ne tronca le dimensioni a 0 byte.

APERTURA DI UN SEGMENTO DI MEMORIA CONDIVISA •  Chiamate multiple a shm_open usando lo stesso nome da più

processi restituiranno file descriptor associati allo stesso segmento (così come, nel caso di file di dati, essi sono associati allo stesso inode)

•  In questo modo è possibile effettuare una chiamata ad mmap sul file descriptor restituito da shm_open ed i processi vedranno lo stesso segmento di memoria condivisa

•  Quando il nome non esiste il segmento può essere creato specificando O_CREAT; in tal caso il segmento avrà (così come i nuovi file) lunghezza nulla.

•  Dato che un segmento di lunghezza nulla è di scarsa utilità, per impostarne la dimensione si deve usare ftruncate prima di mapparlo in memoria con mmap

MAPPATURA DI UN SEGMENTO DI MEMORIA CONDIVISA •  Una volta creato un segmento di memoria condivisa, questa

memoria deve essere “mappata” all’interno della memoria del processo che ha chiamato la funzione shm_open

•  Questo viene effettuato attraverso la funzione mmap

#include <sys/mman.h>!!void *mmap(void *start, size_t length, int prot, !

! ! !int flags, int fd, off_t offset);!

•  Senza entrare nei dettagli, per mappare un segmento shm si effettua la seguente chiamata

mmap(NULL, shm_size, PROT_WRITE|PROT_READ, MAP_SHARED, fd, 0); !

Dove, fd è il file descriptor dell’inode che rappresenta il segmento shm e shm_size è la grandezza del segmento

RIMOZIONE DI UN SEGMENTO DI MEMORIA CONDIVISA •  Come per i file, quando si vuole effettivamente rimuovere

segmento di memoria condivisa, occorre usare la funzione shm_unlink

#include <mqueue.h>!int shm_unlink(const char *name)!Rimuove un segmento di memoria condivisa.!!La funzione restituisce 0 in caso di successo e -1 in caso di errore!

SEMPLICE INTERFACCIA ALLE CHIAMATE POSIX SHM

SEMPLICE INTERFACCIA ALLE CHIAMATE POSIX SHM

SEMPLICE INTERFACCIA ALLE CHIAMATE POSIX SHM

SEMPLICE INTERFACCIA ALLE CHIAMATE POSIX SHM

I Thread

DEFINZIONE •  Un thread è una suddivisione di un processo in due o più

sottoprocessi, che vengono eseguiti concorrentemente da un sistema di elaborazione monoprocessore (multithreading) o multiprocessore.

•  E’ la più piccola sequenza di istruzioni che può essere gestita in modo indipendente dallo scheduler (wikipedia)

DIFFERENZA PROCESSI E THREADS •  Quando creiamo due processi (ad es. con fork() ),

vengono duplicati i programmi (memoria e spazio di indirizzamento e codice)

•  I thread invece condividono tutto e sono semplicemente “funzioni” che vengono eseguite in modo parallelo

DIFFERENZA PROCESSI E THREADS •  La programmazione multithread è considerata “difficile”

perché va gestita correttamente la concorrenza •  Ad es: un thread scrive in una variabile e l’altro la

legge; la lettura potrebbe essere “sporca” •  Esistono soluzioni come mutex o semafori che

garantiscono l’atomicità di operazioni •  Ma se non gestite correttamente possono creare

problemi gravi (ad es. deadlocks) •  La creazione di thread è quindi però più veloce

•  Miglior utilizzo delle risorse, condivisione di memoria semplificata

ESERCITAZIONE IN CLASSE Estendiamo la console per l’esecuzione degli algoritmi di sorting come segue:

1)  l’algoritmo di sorting viene eseguito in un processo figlio. Il controllo ritorna alla console

2)  È possibile controllare da console la percentuale di esecuzione dell’algoritmo di sorting attraverso la lettura di una area di memoria condivisa

3)  È possibile interrompere l’esecuzione dell’algoritmo tramite console

4)  La console permette di lanciare un solo algoritmo alla volta

POSSIBILI ESTENSIONI PER CASA 1)  Percentuale di completamento dell’algoritmo merge sort

2)  Esecuzione diversi algoritmi in parallelo

3)  Permettere l’esecuzione solo di un’istanza dello algoritmo contemporaneamente

IL TEMPO

LORENZO.BRACCIALE@UNIROMA2.IT

DOVE STUDIARE •  Gapil

•  http://users.lilik.it/~mirko/gapil/gapilse26.html#gapilsu125.html •  Linux

•  http://www.linuxsa.org.au/tips/time.html

IL FUSO ORARIO

UTC (Tempo Universale Coordinato) anche chiamato GMT (Greenwich Mean Time)

In Italia: GMT +1 (CET) GMT +2 (CEST, ora solare)

NEL COMPUTER

•  Due posti in cui viene memorizzato il tempo •  BIOS (“hardware clock”) •  Sistema operativo

•  che usa hardware clock al boot •  Sui sistemi linux

•  in /etc/localtime viene memorizzato il fuso orario corrente •  selezionabile da /usr/share/zoneinfo/ •  Il comando date ci permette di leggere /settare il clock di sistema •  il comando /usr/sbin/hwclock ci permette di leggere/settare il clock

hardware

DUE MISURE PER IL TEMPO •  Calendar Time

•  Numero di secondi dal 1/1/1970 (Epoch time) •  Tempo utilizzato dal kernel ad es. per le modifiche dei file •  Memorizzato nel tipo “time_t” (tempo assoluto) •  tipicamente un intero con segno

•  Process Time •  Tempo di processore •  Misurato in “clock ticks” •  Un tempo misurava gli interrupt per secondo

•  oggi POSIX richiede che sia pari alla costante CLOCKS_PER_SEC = 1000000

•  Memorizzato nel tipo “clock_t” (tempo relativo) •  Usato ad es. per valutare le performance di un programma

DOMANDA •  Calendar time:

•  “Tempo utilizzato dal kernel…” •  “secondi dall’epoch time (1/1/1970)” •  su molte macchine (unix, 32 bit) sizeof(time_t) = 4

•  tipicamente un intero con segno

CANDY CRUSH

Il wait period, su dispositivi la cui data era modificata artificialmente a prima del 18 gennaio 2038 faceva crashare l’app

TEMPI DI PROCESSO Per ciascun processo il kernel mantiene: •  clock time: tempo reale passato dall’avvio del processo •  user time: tempo passato ad eseguere le istruzioni del

processo (user space) •  system time: tempo passato ad eseguire le syscall per

conto del processo

•  user time + system time = CPU time

•  è quello che ritorna il comando time nome_eseguibile •  ritornato anche dalla funzione “clock()”

ESEMPIO: MISURIAMO IL TEMPO DI ESECUZIONE DI UN PROCESSO

TEMPI DI CALENDARIO

•  Esiste una analoga funzione stime per settare l’orologio di sistema •  La funzione time_t time(time_t *t) ha una scarsa precisione (secondo) •  Per questo, queste funzioni sono sostituite da gettimeofday e settimeofday

GETTIMEOFDAY #include <sys/time.h> #include <time.h> int gettimeofday(struct timeval *tv, struct timezone *tz)

struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */

};

ritorna 0 in caso di successo, -1 in caso di errore

•  il parametro timezone è obsoleto (settarlo sempre a NULL) •  esiste la funzione analoga settimeofday

LE DATE •  Dato un calendar time, come stampare ore, minuti e

secondi? •  Funzioni di libreria per il “broken down time”

struct tm { int tm_sec; /* seconds */ int tm_min; /* minutes */

int tm_hour; /* hours */ int tm_mday; /* day of the month */ int tm_mon; /* month */ int tm_year; /* year */ int tm_wday; /* day of the week */ int tm_yday; /* day in the year */ int tm_isdst; /* daylight saving time */ long int tm_gmtoff; /* Seconds east of UTC. */ const char *tm_zone; /* Timezone abbreviation. */

};

FUNZIONI #include <time.h> char *asctime(const struct tm *tm) Produce una stringa con data e ora partendo da un valore espresso in broken-down time. char *ctime(const time_t *timep) Produce una stringa con data e ora partendo da un valore espresso in in formato time_t. struct tm *gmtime(const time_t *timep) Converte il calendar time dato in formato time_t in un broken-down time espresso in UTC. struct tm *localtime(const time_t *timep) Converte il calendar time dato in formato time_t in un broken-down time espresso nell'ora locale. time_t mktime(struct tm *tm) Converte il broken-down time in formato time_t.

#include <time.h> size_t strftime(char *s, size_t max,

const char *format,

const struct tm *tm)

Stampa il tempo tm nella stringa s

secondo il formato format.

SCRIVERE IL TEMPO

LE FUNZIONI DI SLEEP #include <unistd.h> unsigned int sleep(unsigned int seconds) Pone il processo in stato di sleep per un certo numero di secondi int nanosleep(const struct timespec *req, struct timespec *rem) Pone il processo in stato di sleep per il tempo specificato da req. In caso di interruzione restituisce il tempo restante in rem.

struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */

};

ESEMPIO #include <stdio.h> #include <unistd.h>

int main() {

printf("wait\n");

sleep(3);

printf("time elapsed\n"); return 0;

}

Cosa otteniamo se misuriamo il process time?

ESERCIZIO 1.  Stampare il tempo corrente ogni secondo

1.  utilizzare la funzione sleep 2.  stampare secondi e us correnti

2.  Modificare il programma per non andare out of sync

1.  supponiamo di dover fare un orologio che deve scrivere il tempo per anni

TIMEOUT Problema: •  Voglio eseguire una certa funzione tra X secondi senza

mettere il mio processo in stato di sleep •  Ad esempio stampare “Hurry up!” se l’utente ci mette più di

10 secondi a fare una mossa a tris

Soluzione

•  Utilizzare un timer

•  Su linux, far generare al sistema operativo un segnale allo scadere di un certo lasso di tempo

•  Il segnale in questione è SIGALERT

TIMEOUT: PROCEDURE 1.  Definire una funzione da chiamare allo scadere del

timeout 1.  handler del segnale SIGALARM 2.  prototipo: void nome_funzione(int sig);

2.  Impostare il timer

1.  Funzione alarm 2.  prototipo: unsigned int alarm(unsigned int seconds) 3.  “returns the number of seconds remaining until any

previously scheduled alarm was due to be delivered” 3.  Scrivere il nostro main

TIMEOUT