I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff....

25
I processi e le funzioni di LINUX per la gestione dei processi Prof. Mauro Negri Prof. Giuseppe Pelagatti Dipartimento di Elettronica e Informazione (D.E.I.) 25 Novembre 2008

Transcript of I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff....

Page 1: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

I processi e le funzioni di LINUX

per la gestione dei processi

Prof. Mauro Negri

Prof. Giuseppe Pelagatti

Dipartimento di Elettronica e Informazione (D.E.I.)

25 Novembre 2008

Page 2: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 1

1 La necessità del parallelismo Un programma eseguibile, generato ad esempio da un programma C, è

rigorosamente sequenziale, nel senso che viene eseguito una istruzione alla volta e,

dopo l’esecuzione di un’istruzione, è univocamente determinata la prossima istruzione

da eseguire.

In base a quanto noto finora, l’esecuzione di N programmi da parte di un

calcolatore dovrebbe anch’essa essere rigorosamente sequenziale, in quanto, dopo

avere eseguito la prima istruzione di un programma dovrebbero essere eseguite tutte

le successive fino alla fine del programma prima di poter iniziare l’esecuzione della

prima istruzione del programma successivo. Questo modello sequenziale è molto

comodo per il programmatore, perchè nella scrittura del programma egli sa che non ci

saranno “interferenze” da parte di altri programmi, in quanto gli altri programmi che

verranno eseguiti dallo stesso esecutore saranno eseguiti o prima o dopo l’esecuzione

del programma considerato.

Tuttavia, il modello di esecuzione sequenziale non è adeguato alle esigenze

della maggior parte dei sistemi di calcolo; questa inadeguatezza è facilmente

verificabile pensando ai seguenti esempi:

• server WEB: un server WEB deve poter rispondere a molti utenti

contemporaneamente; non sarebbe accettabile che un utente dovesse

attendere, per collegarsi, che tutti gli altri utenti si fossero già scollegati

• calcolatore multiutente: i calcolatori potenti vengono utilizzati da molti

utenti contemporaneamente; in particolare, i calcolatori centrali dei Sistemi

Informativi (delle banche, delle aziende, ecc…) devono rispondere

contemporaneamente alle richieste di moltissimi utilizzatori

contemporanei

• applicazioni multiple aperte da un utente: quando un utente di un normale

PC tiene aperte più applicazioni contemporaneamente esistono diversi

programmi che sono in uno stato di esecuzione già iniziato e non ancora

terminato

In base ai precedenti esempi risulta necessario un modello più sofisticato del

Page 3: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 2

sistema; si osservi che tale modello deve garantirci due obiettivi tendenzialmente

contrastanti:

• fornire parallelismo, cioè permettere che l’esecuzione di un programma

possa avvenire senza attendere che tutti gli altri programmi in esecuzione

siano già terminati;

• garantire che ogni programma in esecuzione sia eseguito esattamente come

sarebbe eseguito nel modello sequenziale, cioè come se i programmi

fossero eseguiti uno dopo l’altro, evitando “interferenze” indesiderate tra

programmi diversi (vedremo che in alcuni casi esistono anche interferenze

“desiderate”, ma per ora è meglio non considerarle).

2 La nozione di processo Per ottenere un comportamento del sistema che soddisfa gli obiettivi indicati

sopra la soluzione più comune è quella rappresentata in figura 1, nella quale si vede

che sono stati creati tanti “esecutori” quanti sono i programmi che devono essere

eseguiti in parallelo. Nel contesto del sistema operativo LINUX (e in molti altri) gli

esecutori creati dinamicamente per eseguire diversi programmi sono chiamati

Processi. I processi devono essere considerati degli esecutori completi, e quindi la

struttura di figura 1 risponde in maniera evidente ad ambedue i requisiti contrastanti

definiti al precedente paragrafo: al primo requisito, perchè diversi processi eseguono

diversi programmi in parallelo, cioè senza che uno debba attendere la terminazione

degli altri, e al secondo requisito, perchè, essendo i diversi processi degli esecutori

indipendenti tra loro, non c’è interferenza tra i diversi programmi (come se fossero

eseguiti su diversi calcolatori).

I processi possono essere considerati come dei calcolatori o macchine

virtuali, nel senso che sono calcolatori realizzati dal software (sistema operativo) e

non esistono in quanto Hardware, anche se ovviamente il sistema operativo ha a sua

volta bisogno di essere eseguito da un calcolatore reale. Ogni processo deve possedere

ad ogni istante un unico programma in esecuzione; pertanto, la comunicazione tra due

processi coincide con la comunicazione tra i due corrispondenti programmi in

esecuzione.

Page 4: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 3

Figura 1 – Processi e programmi eseguiti dai processi

Tuttavia, la nozione di processo come macchina virtuale è più ampia e mette

in luce alcune importanti caratterisitche del processo che estendono la normale

nozione di programma in esecuzione; in particolare:

• il processo possiede delle risorse, per esempio una memoria, dei file aperti,

un terminale di controllo, ecc…

• il programma eseguito da un processo può essere sostituito senza annullare

il processo stesso, ovvero un processo può passare dall’esecuzione di un

programma all’esecuzione di un diverso programma (attenzione, questo

Programma 1 Programma 2 Programma 3

Processo 1 (esecutore)

Processo 2 (esecutore)

Processo 3 (esecutore)

Istruzioni

Istruzioni

Istruzioni

HARDWARE + SO

Virtualizzazione

Page 5: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 4

passaggio è sequenziale, cioè il primo programma viene totalmente

abbandonato per passare al nuovo);

Si osservi che, dato che il processo rimane lo stesso anche se cambia il programma da

esso eseguito, le risorse del processo non si annullano quando si passa all’esecuzione

di un nuovo programma e quindi il nuovo programma potrà utilizzarle; ad esempio, il

nuovo programma lavorerà sullo stesso terminale di controllo del programma

precedente.

Sorge però una domanda: come è possibile realizzare diversi processi che

procedono in parallelo se l’Hardware del calcolatore è uno solo ed è sequenziale? A

questa domanda si risponderà trattando la struttura interna del SO; per ora basta tenere

presente che il sistema operativo è in grado di virtualizzare diversi processi

indipendenti e che dal punto di vista del programmatore quello di figura 1 è il modello

di riferimento da utilizzare.

3 Caratteristiche generali dei processi Tutti i processi sono identificati da un apposito identificatore (PID = Process

Identifier).

Tutti i processi (ad eccezione del primo, il processo “init”, creato

all’avviamento dal SO) sono creati da altri processi. Ogni processo, a parte il processo

init, possiede quindi un processo padre che lo ha creato e può a sua volta creare molti

processi figli. La gerarchia delle relazioni di parentela dell’insieme dei processi creati

nel sistema può quindi essere rappresentata da un albero come mostrato in Figura 2.,

nel quale alla radice c’è in processo init che genera una serie di processi dedicati alla

gestione del Sistema Operativo. In particolare viene generato un processo per ogni

terminale (chiamato semplicemente “getty” in Figura 2) il quale si mette in attesa di

una richiesta di login da parte di un utente; quando avviene la richiesta il processo

attiva l’esecuzione del programma di login (in Figura 2 il processo “getty” che esegue

con successo il programma di login è chiamato “login”) il quale associa al processo le

caratteristiche dell’utente collegato (privilegi, limiti, ….) e genera un processo al

quale fa eseguire il programma dell’interprete comandi di default dell’utente (il

processo “shell” di Figura 2) che si mette in attesa di comandi da parte dell’utente.

Quando l’utente chiede di eseguire un proprio programma oppure un comando che

richieda l’esecuzione di un programma (ad esempio, un comando di copia di un file)

Page 6: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 5

l’interprete comandi genera un processo dedicato all’esecuzione del programma

richiesto e poi si mette in attesa della sua terminazione; il programma in esecuzione

può poi invocare le funzioni del sistema operativo per la gestione dei processi al fine

di gestire la creazione/distruzione di ulteriori processi.

Figura 2. La gerarchia dei processi

Per semplicità ci concentreremo sui processi creati dagli utenti anche se alcune

caratteristiche hanno validità generale. La memoria di ogni processo è costituita (dal

punto di vista del programmatore) da 3 parti (dette anche “segmenti”):

1. il segmento codice (text segment): contiene il codice eseguibile del

programma lanciato in esecuzione sul processo;

2. il segmento dati (user data segment): contiene tutti i dati del programma,

ossia sia i dati statici, sempre presenti, sia i dati dinamici, che a loro volta

si dividono in dati allocati automaticamente in una zona di memoria detta

pila (stack) nella quale sono allocati i record di attivazione delle funzioni

(variabili locali, indirizzo di ritorno e parametri delle funzioni) e dati

dinamici allocati esplicitamente dal programma tramite la funzione

“malloc( )” in una zona di memoria detta heap.

3. il segmento di sistema (system data segment): questo segmento contiene

dati inerenti il processo stesso (ad esempio, la “tabella dei files aperti”) che

tuttavia non sono gestiti esplicitamente dal programma in esecuzione ma

dal sistema operativo per suo conto. Si noti che esistono strutture dati

necessarie alla gestione del singolo processo che non sono allocate in

questa area, ma in strutture dati interne del sistema operativo (ad esempio,

il descrittore del processo nella tabella dei processi). Tuttavia, al fine di

init

P1(SO) Pn(SO)

… getty (SO)

login (SO)

shell (user)

Processo1 (comando)

Processo1 )Processo2

Page 7: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 6

semplificare la presentazione di quanto necessario per questo corso si

suppone che in questo segmento siano memorizzate tutte le informazioni

di sistema operativo necessarie alla comprensione della gestione

programmativa dei processi.

Per operare con i processi il sistema operativo, LINUX mette a disposizione

del programmatore un certo numero di servizi di sistema. I principali servizi

che tratteremo in questo testo consentono di:

• generare un processo figlio, che è copia del processo padre in esecuzione,

ossia che esegue lo stesso programma ;

• attendere la terminazione di un processo figlio;

• terminare un processo restituendo un eventuale codice di terminazione al

processo padre;

• sostituire il programma eseguito da un processo, cioè il segmento codice e

il segmento dati del processo, con il codice e i dati di un diverso

programma.

I servizi del sistema operativo sono messi a disposizione del programmatore

come funzioni in modo analogo alle operazioni di lettura e scrittura da terminale o

file. Queste funzioni sono diverse dalle normali funzioni implementate da un

programmatore perché esse non devono operare solo sui dati (segmento dati) del

programma, ma viceversa devono accedere alle strutture dati gestite dal sistema

operativo (ad esempio, per creare un processo nel sistema). Per ottenere questo

obiettivo esse richiamano il sistema operativo, demandandogli quelle operazioni che il

programma non è in grado di svolgere direttamente. Pertanto la comprensione del

comportamento reale di queste funzioni richiede di conoscere il funzionamento del

sistema operativo ed è per questo motivo che la descrizione del loro comportamento

sarà opportunamente semplificata.

4 Generazione e terminazione dei processi: le funzioni fork ed exit La funzione fork crea un processo figlio identico al processo padre all’istante

della fork come mostrato in Figura 3.

Prototipo della funzione fork.

pid_t fork( )

(pid_t è un tipo predefinito)

Page 8: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 7

Tutti i segmenti del padre sono duplicati nel figlio, quindi sia il codice e le

variabili (segmenti codice e dati), sia i file aperti utilizzati (segmento di sistema). Si

noti che il segmento codice è identico e quindi si potrebbe evitare la duplicazione, ma

questo aspetto non è considerato ulteriormente.

Programma 1void main(){fork(); … } …

System segment

(file aperti,tty)

terminale

Programma 1void main(){fork()… } …

system segment

(file aperti,tty)

terminale

creazione

(clone)

Processo padre Processo figlio

data segment data segment

virtualcopy

PC-> <-PC

Figura 3. Creazione di un processo

La duplicazione del segmento di sistema prodotto dalla fork copia la tabella

dei file aperti e pertanto, come mostrato in Figura 4, entrambi i processi possono

operare sullo stesso file aperto (file 1), mentre successivi file aperti da uno dei due

processi (file 2) dopo la fork saranno invece immessi nella tabella dei file aperti del

solo processo che ha eseguito l’apertura. Si noti che la tabella degli i-node e dei file

aperti globali sono invece strutture dati referenziate dalle tabelle locali ai processi, ma

sono strutture dati del sistema operativo.

Page 9: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 8

Tab. globale file aperti Tabella i_nodeProcesso 1 (system segment)

Tab. file aperti

file 1

Processo 2 (system segment)

Tab. file aperti

file 1

file 2

file 1

file 2

inode1

header file 1

block pointers

inode2header file 1block pointers

…..

Figura 4. La gestione dei file aperti

Il processo figlio eredita anche il valore del PC del processo padre, pertanto

entrambi i processi dopo la fork si trovano ad eseguire la stessa istruzione del

programma. Ciò significa che terminata l’esecuzione della fork entrambi i processi

proseguono ad eseguire la porzione dello stesso programma che segue l’istruzione di

invocazione della fork.

Tuttavia, come sarà illustrato in seguito, la creazione di un processo figlio avviene

spesso perché padre e figlio si distinguano eseguendo parti diverse di codice. Per

permettere ciò il sistema operativo restituisce, come valore ritornato dalla funzione

fork stessa, un valore diverso tra processo figlio e processo padre:

• nel processo padre la funzione fork restituisce un valore diverso da 0;

normalmente tale valore indica il pid del figlio, tranne -1, che indica un

errore e quindi che la fork non è stata eseguita;

• nel processo figlio la funzione fork restituisce il valore 0.

In questo modo dopo l’esecuzione di una fork è possibile sapere se siamo nel

processo padre oppure nel figlio interrogando tale valore all’interno dei due processi.

La funzione exit pone termine all’esecuzione del programma e provoca la

Page 10: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 9

distruzione del processo corrente (la descrizione dettagliata del funzionamento di

questa funzione è ripresa più avanti). Un programma può terminare anche senza una

invocazione esplicita della funzione exit; in tal caso exit viene invocata

automaticamente dal sistema al termine dell’esecuzione del programma (questo

comportamento è simile a quello del comando return in una funzione C: esso permette

la terminazione della funzione in un punto qualsiasi, ma una funzione può anche

terminare raggiungendo la fine, senza return esplicita).

#include <stdio.h> #include <sys/types.h> void main( ) { pid_t pid; pid=fork( ); if (pid==-1) {printf(“errore esecuzione fork”); exit();} else if (pid==0) {printf("sono il processo figlio\n"); exit( ); } else {printf("sono il processo padre\n"); exit( ); /* non necessaria */ } } a)il programma fork1

b)un possibile risultato dell’esecuzione di fork1

Figura 5

In figura 5.a è mostrato un programma che utilizza i servizi fork ed exit e il

risultato della sua esecuzione. Si noti che l’ordine nel quale sono state eseguite le due

printf è casuale; dopo una fork il processo figlio evolve indipendentemente dal padre

e quindi può essere eseguito per primo sia il processo padre che il processo figlio.

Costituisce un grave errore concettuale ipotizzare nella programmazione un preciso

ordine di esecuzione di due processi, perché essi evolvono in parallelo.

Page 11: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 10

A causa della struttura particolarmente semplice dell’esempio le due exit non

sarebbero necessarie, perché comunque il programma terminerebbe raggiungendo i

punti in cui sono le exit.

Si osservi che ambedue i processi scrivono tramite printf sullo stesso terminale

come mostrato in Figura 5.b. Questo fatto è dovuto alla duplicazione del segmento di

sistema nell’esecuzione di una fork: dato che la funzione printf scrive sullo standard

output, che è un file speciale, e dato che la tabella dei file aperti è replicata nei due

processi, le printf eseguite dai due processi scrivono sullo stesso standard output.

Infine si fa notare, con riferimento alla Figura 2, che il comando ./fork1 di Figura 5.b

è ricevuto dall’interprete comandi che crea un processo al quale associa l’esecuzione

del programma che a sua volta poi crea un ulteriore processo tramite la fork.

Possiamo arricchire l’esempio precedente stampando il valore del pid dei due

processi padre e figlio; a questo scopo possiamo utilizzare una funzione di libreria che

restituisce al processo che la invoca il valore del suo pid. Tale funzione si chiama

getpid ed ha il seguente prototipo:

pid_t getpid( )

In figura 6 sono riportati il testo ed il risultato dell’esecuzione del programma

forkpid1, che è simile al programma fork1 ma stampa i pid dei processi coinvolti. Si

osservi che le printf eseguite dai due processi sono mescolate tra loro in ordine

casuale, per i motivi discussi sopra. Per interpretare il risultato è quindi necessario

osservare bene il testo delle printf nel codice. #include <stdio.h> #include <sys/types.h> void main( ) { pid_t pid,miopid; pid=fork( ); if (pid==0) {miopid=getpid( ); printf("sono il processo figlio con pid: %i\n\n",miopid); exit( ); } else {printf("sono il processo padre\n"); printf("ho creato un processo con pid: %i\n", pid); miopid=getpid( ); printf("il mio pid e' invece: %i\n\n", miopid); exit( ); /* non necessaria */ } } a)il programma forkpid1

Page 12: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 11

b)un possibile risultato dell’esecuzione di forkpid1

Figura 6

Un processo può creare più di un figlio, e un figlio può a sua volta generare

dei figli (talvolta si usa la dizione di processi “nipoti” per i figli dei figli), ecc… Si

viene a creare in questo modo la struttura gerarchica tra processi, mostrata in Figura 2,

in cima alla quale è situato, come già detto, il primo processo generato dal sistema

operativo durante l’inizializzazione.

Quanto descritto sinora permette di creare processi, ma come evidenziato nella

descrizione della exit ogni processo viene distrutto quando termina di eseguire il

programma del proprio segmento di codice. Dato che una volta creati i processi, essi

procedono autonomamente non esiste un criterio generale che stabilisca chi termini

per primo, quindi è possibile che un processo figlio termini la propria esecuzione

prima del padre o viceversa. Diventa quindi necessario stabilire cosa accada ai

processi figli che rimangono “orfani” a causa della terminazione e quindi distruzione

del loro processo padre. La convenzione adottata da Linux, mostrata in Figura 7, è

quella di far adottare i processi figli rimasti orfani (processi 2 e 3) e tutta la loro

discendenza (processo 4) al processo init del sistema operativo.

a) b)

Figura 7. L’adozione dei processi orfani

init …

login

shell Processo1

Processo2 Processo3

Processo4

init

… login

shell

Processo2 Processo3

Processo4

Page 13: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 12

In figura 8 sono riportati il testo e il risultato dell’esecuzione del programma

forkpid2, che estende forkpid1 facendo generare al processo padre un secondo

processo figlio che eseguendo un ciclo infinito rimane orfano alla morte degli altri due

processi. Dato che i 3 processi scrivono sullo stesso terminale in ordine casuale, per

rendere il risultato più intelligibile tutte le stampe sono state numerate. Si noti che la

riallocazione del processo figlio nella gerarchia dei processi è eseguita

automaticamente dal sistema operativo. #include <stdio.h> #include <sys/types.h> void main( ) { pid_t pid,miopid; int i=0; pid=fork( ); if (pid==0) {miopid=getpid( ); printf("1)sono il primo processo figlio con pid: %i\n",miopid); while (i==0) continue; exit( ); } else { printf("2)sono il processo padre\n"); printf("3)ho creato un processo con pid: %i\n", pid); miopid=getpid( ); printf("4)il mio pid e' invece: %i\n", miopid); pid=fork( ); if (pid==0) {miopid=getpid( ); printf("5)sono il secondo processo figlio con pid: %i\n",miopid); exit(); } else {printf("6)sono il processo padre\n"); printf("7)ho creato un secondo processo con pid: %i\n", pid); exit( ); /* non necessaria */ } } } a)il programma forkpid2 b)un possibile risultato dell’esecuzione di forkpid2

Figura 8

Page 14: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 13

5 Attesa della terminazione e stato restituito da un processo figlio: la

funzione wait e i parametri della funzione exit Nella precedente sezione i processi creati evolvevano indipendentemente,

tuttavia un processo crea un altro processo per svolgere un particolare compito e si

pone spesso in attesa del suo risultato prima di proseguire. La sincronizzazione tra il

processo padre e i processi figli avviene tramite la funzione wait che permette al

processo padre di sospendersi in attesa di un processo figlio e la funzione exit che

permette ad un processo figlio di informare un processo padre della sua terminazione

e di passare eventualmente un codice di errore. Poiché le due funzioni sono inserite in

programmi eseguiti in processi diversi che evolvono in modo indipendente le funzioni

saranno presentate considerando prima una sincronizzazione tra i due processi e poi il

reale comportamento asincrono dei processi.

La funzione wait sospende l’esecuzione del processo che la esegue ed attende

la terminazione di un qualsiasi processo figlio

Prototipo della funzione wait

pid_t wait(int *)

Esempio di uso: pid_t pid;

int stato;

pid = wait(&stato);

Dopo l’esecuzione la variabile pid assume il valore del pid del figlio

terminato; la variabile stato assume il valore del codice di terminazione del processo

figlio. Tale codice contiene una parte (gli 8 bit superiori) che può essere assegnato

esplicitamente dal programmatore tramite la funzione exit nel modo descritto sotto; la

parte restante è assegnata dal sistema operativo per indicare particolari condizioni di

terminazione (ad esempio quando un processo viene terminato a causa di un errore).

Dato che il valore restituito dalla exit è contenuto negli 8 bit superiori, lo stato

ricevuto dalla wait è lo stato della exit diviso per 256.

Prototipo della funzione exit

void exit(int);

Esempio: exit(5)

termina il processo e restituisce il valore 5 al padre.

Page 15: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 14

Si noti che il processo figlio che emette una exit ha completato l’esecuzione

del programma ad esso associato e pertanto il processo dopo aver trasferito il codice

di stato al processo padre viene distrutto dal sistema operativo.

In Figura 9 sono riportati il testo e il risultato dell’esecuzione del programma

forkwait1, che crea un processo figlio e pone il processo padre in attesa della

terminazione di tale figlio. Il processo figlio a sua volta termina con una exit

restituendo il valore di stato 5. Dopo la terminazione del figlio il padre riprende

l’esecuzione e stampa l’informazione ottenuta dalla wait. Si osservi che per stampare

correttamente il valore dello stato restituito dal figlio è necessario dividerlo per 256 e

che il padre riceve anche il pid del figlio terminato; quest’ultimo dato è utile quando

un processo padre ha generato molti figli e quindi ha bisogno di sapere quale dei figli

è quello appena terminato. #include <stdio.h> #include <sys/types.h> void main( ) { pid_t pid, miopid; int stato_exit, stato_wait; pid=fork( ); if (pid==0) { miopid=getpid( ); printf("sono il processo figlio con pid %i \n", miopid); printf("termino \n\n"); stato_exit=5; exit(stato_exit); } else { printf("ho creato un processo figlio \n\n"); pid=wait (&stato_wait); printf("terminato il processo figlio \n"); printf("il pid del figlio e' %i, lo stato e' %i\n",pid,stato_wait/256); } } a)il programma forkwait1

b)risultato dell’esecuzione di forkwait1

Figura 9

Page 16: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 15

La Figura 10 mostra una possibile evoluzione temporale dei programmi

eseguiti dai due processi compatibile con l’esito di Figura 9.b.

Figura 10. Esecuzione sincronizzata

La situazione ottimale descritta in Figura 10 non deve tuttavia trarre in

inganno poiché l’evoluzione temporale mostrata non è garantita dal sistema operativo

come mostrato dalla Figura 11 nella quale il processo figlio termina la propria

esecuzione prima ancora che il processo padre si metta in attesa della sua

terminazione.

Figura 11

Per supportare l’evoluzione asincrona dei due processi si deve quindi

complicare il comportamento delle funzioni wait e exit:

- funzione exit(stato). Il sistema operativo memorizza il valore di stato nella

parte di sistema operativo dedicata al processo, chiude tutti i file aperti

presenti nella tabella dei file aperti del segmento di sistema del processo e

passa il processo dallo stato di “attivo” allo stato di “zombie”.

In Figura 11 il processo figlio dopo l’exit rimane quindi in vita, ma solo per

t

padre

figlio fork()

wait(..)

miopid=getpid( ); … exit(stato_exit);

printf("ho creato…"); ……. …... …

t

Processo figlio fork() printf("ho creato…"); wait(..)

miopid=getpid( ); … … exit(stato_exit);

printf("terminato…");

Processo padre

Page 17: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 16

aspettare che il processo padre possa recuperare lo stato;

- funzione wait (&stato). Il sistema operativo verifica se esiste uno dei suoi

processi figli che abbia eseguito la exit. In caso affermativo (esempio di

Figura 11) il sistema operativo sblocca immediatamente il processo padre

all’atto dell’esecuzione della wait, preleva il valore dello stato dal processo

figlio e permette al processo padre di ripartire nell’esecuzione del proprio

programma e infine distrugge definitivamente il processo figlio. In caso

negativo (esempio di Figura 10) sospende il processo padre in attesa che un

processo figlio eseguendo una exit() lo metta in condizione di attivare le

operazioni conseguenti.

Si analizzano quindi alcuni casi particolari:

a) Un processo padre che non ha generato processi figli esegue una wait. In questo

caso il sistema operativo restituisce il codice di errore -1 e non pone in attesa il

processo padre.

b) Un processo padre esegue una wait in presenza di un processo figlio che non

esegue mai una exit (ad esempio per un ciclo infinito); in questo caso il processo

padre rimane sospeso all’infinito. Questa situazione richiede un intervento esterno

per forzare la terminazione di entrambi i processi.

c) Un processo padre termina l’esecuzione del proprio programma, provocando la

propria distruzione senza eseguire una wait, in presenza di uno o più processi figli

attivi. In questo caso il sistema operativo prende tutti i processi rimasti orfani dalla

morte del processo padre e li fa adottare al processo init. Quando questi processi

figli eseguono l’exit passano allo stato zombie senza avere più un padre che li

aspetti. Si noti che periodicamente il processo init esegue una wait proprio al fine

di eliminare i processi zombie inutilmente presenti nel sistema.

Una variante di wait: la funzione waitpid

La funzione wait mette un processo padre in attesa della terminazione di un

qualsiasi processo figlio, mentre in taluni casi potrebbe essere più significativo porlo

in attesa di uno specifico processo figlio. Esiste una variante di wait che permette di

sospendere l’esecuzione del processo padre in attesa della terminazione di uno

specifico processo figlio, di cui viene fornito il pid come parametro. Questa possibilità

non è ulteriormente analizzata.

Page 18: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 17

6 Sostituzione del programma in esecuzione: la funzione exec La funzione exec permette di sostituire il programma in esecuzione in un

processo con un altro programma indicato dai parametri della funzione exec.

Esistono molte varianti sintattiche della funzione exec che si differenziano

nella modalità di scrittura dei parametri passati al nuovo programma.

La forma più semplice è la execl, descritta di seguito.

Prototipo della funzione execl

int execl (char *nome_programma, char *arg0, char *arg1, …NULL );

Il parametro nome_programma è una stringa che deve contenere l’identificazione

completa (con tutto il pathname) di un file eseguibile contenente il nuovo programma

da lanciare in esecuzione.

I parametri arg0, arg1, ecc… sono puntatori a stringhe che verranno passate al

main del nuovo programma lanciato in esecuzione; l’ultimo puntatore deve essere

NULL per indicare la fine dei parametri; per convenzione, il parametro argv[0]

contiene sempre il nome del programma stesso questa volta senza pathname.

La funzione ritorna -1 in caso di errore.

Modalità di passaggio dei parametri al main da parte di exec

Per ricevere i parametri la funzione main del programma da eseguire è definita

con l’intestazione:

int main(int argc, char * argv[ ])

dove:

il parametro argc è un intero che indica il numero di parametri ricevuti e il

parametro argv[ ] è un vettore di puntatori a stringhe.

La Figura 12 descrive iol comportamento della funzione exec. La funzione

invoca il sistema operativo, il quale esegue le seguenti operazioni:

• copia in un’area di memoria (buffer) del sistema operativo i dati

indirizzati da tutti i parametri della funzione;

• verifica che il file del nuovo programma sia un eseguibile esistente e

accessibile;

• dealloca lo spazio di memoria dedicato al segmento codice e dati (di

Page 19: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 18

utente) del processo corrente. Si noti che il segmento di sistema rimane

allocato e quindi, ad esempio, i file aperti rimangono disponibili);

• alloca un segmento codice e un segmento dati adatti a contenere il nuovo

programma eseguibile con i propri dati;

• copia il vettore dei parametri della funzione exec dal buffer del sistema

allo stack del segmento dati del nuovo programma. L’indirizzo del vettore

(parametro argv della funzione main) viene memorizzato nel record di

attivazione della funzione main allocato nello stack. Si noti che argv[0]

corrisponderà quindi al parametro arg0 della funzione exec, argv[1] ad

arg1 e così via. Nel record di attivazione è anche memorizzato il numero

dei parametri della funzione exec (paramtro argc della funzione main);

• il PC viene quindi caricato con l’indirizzo della prima istruzione

eseguibile del nuovo programma.

A questo punto il processo che è rimasto lo stesso e con lo stesso pid si trova

ad eseguire il nuovo programma.

Programma 1void main(){PC -> exec(programma2 )… } …

segmento sistema

(file aperti,tty)

PC, SP,…

Programma 2void main(){… … }…

segmento sistema

(file aperti,tty)

PC, SP,…

Sistema operativo

Processo prima dell’exec

segmento dati Segmento dati

Processo dopo l’exec

Codice eseguibile

Nuovo programma

PC

argc, argv

Sistema operativo

BUFFER

Figura 12 Esecuzione della funzione exec

Si consideri la Figura 13 nella quale il programma main1 stampa il numero di

parametri ricevuti (argc) e i parametri stessi e il programma exec1 che lancia in

esecuzione il precedente programma main1 passandogli alcuni parametri.

Page 20: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 19

#include <stdio.h> void main (int argc, char *argv[ ] ) { int i; printf("\nsono il programma main1\n"); printf("ho ricevuto %i parametri\n", argc); for (i=0; i<argc; i++) printf("il parametro %i è: %s\n", i, argv[i]); } a)il programma main1 #include <stdio.h> #include <sys/types.h> void main( ) { char P0[ ]="main1"; char P1[ ]="parametro 1"; char P2[ ]="parametro 2"; printf("sono il programma exec1\n"); execl("/home/pelagatt/esempi/main1", P0, P1, P2, NULL); printf("errore di exec"); /*normalmente non si arriva qui!*/ } b)il programma exec1

Figura 13

In figura 14 è mostrato il risultato dell’esecuzione del programma exec1. Si

noti che main1 scrive sullo stesso terminale di exec1, perché lo standard output è

rimasto identico, trattandosi dello stesso processo. Si noti che l’aver passato come

parametro P0 il nome del programma invocato “main1” non è una convenzione

richiesta dalla funzione exec.

Figura 14. Risultato dell’esecuzione di exec1

Il servizio exec assume particolare rilevanza in collaborazione con il servizio

Page 21: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 20

fork, perchè utilizzando entrambi i servizi è possibile per un processo far eseguire un

programma e poi riprendere la propria esecuzione, come fanno ad esempio gli

interpreti di comandi. In Figura 15.a è mostrato a questo scopo lo pseudocodice di un

interprete comandi semplificato (programma simpleshell) che legge da terminale un

comando, procede a creare un processo figlio dedicato all’esecuzione del comando,

mentre il processo padre ne attende la sua terminazione prima di ripetere la richiesta

di un altro comando.

#include <stdio.h> #include <sys/types.h> #define fine “logout” #define prompt “simpleshell:” void main( ) { pid_t pid; int stato_wait;

…. while (not logout dell’utente) { printf (“%s”,prompt); //lettura riga di comando e identificazione componenti del comando

pid=fork( ); if (pid==0) {execl(comando, arg0, arg1, … argn, NULL); printf("errore di exec"); /*normalmente non si arriva qui!*/ exit( ); } else wait(&stato_wait ); } exit( ); } a)il programma simpleshell

b)risultato dell’esecuzione di simpleshell senza parametri

simpleshell: ./main1 sono il programma main1 ho ricevuto 1 parametri il parametro 0 è: ./main1 simpleshell:

Page 22: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 21

c)risultato dell’esecuzione di simpleshell con tre parametri

Figura 15

Supponendo che il programma simpleshell sia in esecuzione, le Figure 15.b e 15.c

mostrano l’esecuzione del comando che invoca il programma main1 di Figura 13.a

con zero e tre parametri rispettivamente. Si noti che il programma simpleshell invoca

la funzione exec passando come primo parametro il nome del file poiché questa è la

convenzione che viene adottata dagli interpreti comandi; per averne una riprova è

sufficiente lanciare in esecuzione il programma main1 di Figura 13 direttamente da

interprete comandi.

Altre versioni della funzione exec

Esistono altre versioni della funzione exec che differiscono tra loro nel modo

in cui vengono passati i parametri al programma lanciato in esecuzione. In particolare

la execv permette di sostituire la lista di stringhe dalla execl con un puntatore a un

vettore di stringhe, in maniera analoga al modo in cui i parametri sono ricevuti nel

main; altre 2 versioni (execlp e execvp) permettono di sostituire il nome completo

(pathname) dell’eseguibile con il semplice nome del file e utilizzano il direttorio di

default per cercare tale file; infine 2 ulteriori versioni (execle e execve) hanno un

parametro in più che si riferisce all’ambiente di esecuzione (environment) del

processo.

simpleshell: ./main1 par1 par2 par3 sono il programma main1 ho ricevuto 4 parametri il parametro 0 è: ./main1 il parametro 1 è: par1 il parametro 2 è: par2 il parametro 3 è: par3 simpleshell:

Page 23: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 22

7. Esecuzione parallela e valore delle variabili L’esecuzione parallela dei processi può rendere impossibile la definizione

dell’ordine temporale con il quale le variabili sono modificate nei differenti processi.

Per comprendere il problema si consideri il programma di Figura 16 e le tre tabelle,

una per il processo padre e una per ognuno dei due processi figli, aventi la struttura

mostrata in Figura 17, indicando, negli istanti di tempo specificati, il valore delle

variabili i, j, k, pid1 e pid2, e utilizzando le seguenti convenzioni:

• nel caso in cui al momento indicato la variabile non esista (in quanto non

esiste il processo) riportare NE;

• se la variabile esiste ma non se ne conosce il valore o non è determinabile

con certezza riportare U; a causa del parallelismo può essere impossibile

stabilire se gli altri processi hanno eseguito certe istruzioni e quindi se

hanno modificato certe variabili;

• si suppone che tutte le istruzioni fork abbiano successo e che il sistema

operativo assegni ai processi figli creati valori di pid pari a 500, 501.

Attenzione: la frase “dopo l’istruzione x” definisce l’istante di tempo

immediatamente successivo all’esecuzione dell’istruzione x da parte di un processo

(tale processo è univoco, data la struttura del programma);

La soluzione è riportata in Figura 18. Nel processo padre le variabili esistono

sempre, il valore di pid1 dopo l’istruzione 6 è assegnato a 500 e non cambia nelle

istruzioni successive, il valore di pid2 è sempre indeterminato, le variabili j e k

mantengono sempre il loro valore iniziale. Più complessa è la valutazione dello stato

della variabile i dopo le istruzioni 9 e 11, perchè queste istruzioni sono eseguite dai

processi figli e quindi non possiamo sapere se il processo padre ha già eseguito in

questi istanti l’istruzione 18, che modifica i. questo è il motivo per l’indicazione U

nelle corrispondenti caselle. Dopo l’istruzione 19 ovviamente i vale 11.

Nel primo processo figlio l’ultima riga vale NE perchè sicuramente in

quell’istante tale processo è sicuramente terminato. I valori di pid1, pid2, j e k sono

ovvi. Il valore di i è indeterminato sostanzialmente per lo stesso motivo indicato per il

processo padre.

Nel secondo processo figlio le motivazioni sono derivabili da quelle fornite

per i casi precedenti.

Page 24: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 23

01: main() 02: { 03: int i, j, k, stato; 04: pid_t pid1, pid2; 05: i=10; j=20; k=30; 06: pid1 = fork(); /*creazione del primo figlio / 07: if(pid1 == 0) { 08: j=j+1; 09: pid2 = fork(); /*creazione del secondo figlio */ 10: if(pid2 == 0) { 11: k=k+1; 12: exit();} 13: else { 14: wait(&stato); 15: exit(); } 16: } 17: else { 18: i=i+1; 19: wait(&stato); 20: exit(); } 21: }

Figura 16

Valore delle variabili Istante

pid1 pid2 I j k

Dopo l’istruzione 6

dopo l’istruzione 9

Dopo l’istruzione 11

Dopo l’istruzione 19

figura 17 - Struttura delle 3 tabelle da compilare

Page 25: I processi e le funzioni di LINUX per la gestione dei processi · PROCESSI E FUNZIONI LINUX Proff. Mauro Negri, Giuseppe Pelagatti 1 1 La necessità del parallelismo Un programma

PROCESSI E FUNZIONI LINUX

Proff. Mauro Negri, Giuseppe Pelagatti 24

Valore delle variabili Istante

pid1 pid2 I j k

Dopo l’istruzione 6 500 U 10 20 30

dopo l’istruzione 9 500 U U 20 30

Dopo l’istruzione 11 500 U U 20 30

Dopo l’istruzione 19 500 U 11 20 30

1. Valore delle variabili nel processo padre.

Valore delle variabili Istante

pid1 pid2 I j k

dopo l’istruzione 6 0 U 10 20 30

dopo l’istruzione 9 0 501 10 21 30

Dopo l’istruzione 11 0 501 10 21 30

Dopo l’istruzione 19 NE NE NE NE NE

2. Valore delle variabili nel primo processo figlio.

Valore delle variabili Istante

pid1 pid2 I j k

dopo l’istruzione 6 NE NE NE NE NE

dopo l’istruzione 9 0 0 10 21 30

Dopo l’istruzione 11 0 0 10 21 31

Dopo l’istruzione 19 NE NE NE NE NE

3. Valore delle variabili nel secondo processo figlio

Figura 18 – Soluzione dell’esercizio