In questa sezione
• Come funziona la gestione dei processi
in UNIX e quali interfacce UNIX fornisce
per poter operare con i processi
• Esempi in C sul funzionamento delle più
importati system call per la gestione dei
processi.
Nicola Gessa
Gestione dei processi
Domanda:
Come definire un processo?
Nicola Gessa
Gestione dei processi
Come definire un processo?
Un processo è un’istanza in esecuzione di un
programma.
La gestione dei processi include:
•creazione del processo.
•esecuzione del processo e controllo del suo ciclo di
vita.
•terminazione del processo.
•Il comando ps -A visualizza una lista di tutti i processi e
dei loro PID al momento in esecuzione.
•Il comando pstree mostra i processi in esecuzione
secondo la loro struttura ad albero
Nicola Gessa
Esempio di lista dei processi
PID TTY
TIME CMD
1?
00:00:04 init
2?
00:00:00 keventd
3?
00:00:00 kapmd
4?
00:00:00 ksoftirqd_CPU0
9?
00:00:00 bdflush
5?
00:00:00 kswapd
6?
00:00:00 kscand/DMA
7?
00:00:00 kscand/Normal
8?
00:00:00 kscand/HighMem
10 ?
00:00:00 kupdated
641 pts/0 00:00:00 bash
2671 pts/1 00:00:00 bash
2710 ?
00:00:07 gedit
2883 ?
00:00:00 cupsd
3117 pts/1 00:00:00 ps
Nicola Gessa
Stato di avanzamento dei
processi
SOSPENSIONE
IN
PRERILASCIO
ESECUZIONE
ASSEGNAZIONE
IN
PRONTO
ATTESA
RIATTIVAZIONE
Nicola Gessa
Quali funzioni per gestire i
processi?
L'impiego di funzioni (primitive di controllo di
processo) per:
• Creare nuovi processi (fork, vfork) ;
• Attivare nuovi programmi (famiglia exec);
• Attendere la terminazione di un processo (wait,
waitpid);
• Terminare un processo (exit, _exit,return).
Nicola Gessa
Identificatore di processi
• Ogni processo ha assegnato un unico ID che lo
identifica, un intero non negativo.
• Sull’ID del processo vengono poi costruiti altri
identificativi che devono essere unici (come ad
esempio nomi di file temporanei).
• Solitamente il processo 0 è lo scheduler, che fa
parte del kernel del SO.
• Il processo con ID 1 è di solito il processo INIT
invocato alla fine della fase di bootstrap. Questo
processo non termina mai.
• Il processo con ID 2 è il pagedaemon, che si
occupa della gestione della memoria virtuale.
Nicola Gessa
Identificatore di processi
• Il pid viene assegnato in forma progressiva ogni volta
che un nuovo processo viene creato, fino ad un certo
limite. Oltre questo valore l'assegnazione riparte dal
numero più basso disponibile a partire da un minimo
fissato, che serve a riservare i pid più bassi ai processi
eseguiti dal direttamente dal kernel.
• 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 da programma
usando le funzioni:
pid_t getpid(void)
restituisce il pid del processo corrente.
pid_t getppid(void)
restituisce il pid del padre del processo corrente.
Nicola Gessa
Tabella dei processi
• Il kernel mantiene una tabella dei processi attivi,
la cosiddetta process table ; per ciascun processo
viene mantenuta una voce nella tabella dei processi
costituita da una struttura task_struct , che contiene
tutte le informazioni rilevanti per quel processo.
Tutte le strutture usate a questo scopo sono
dichiarate in un header file (es: linux/sched.h)
Nicola Gessa
Tabella dei processi
Nicola Gessa
Alcune definizione per la tabella…
• Stato di un processo:
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
……
• Politiche di scheduling
#define SCHED_OTHER 0
#define SCHED_FIFO 1
#define SCHED_RR 2
• Time slice
#define DEF_PRIORITY (20*HZ/100) /* 200 ms time
slices */
Nicola Gessa
Tabella dei processi, cosa
contiene?
La struttura dei processi task_struct contiene
informazioni come:
• Stato del processo
volatile long state;
• Tipo di errore generato
int errno;
• Priorità statica
long priority;
• Codice di terminazione e segnale che ha causato
la terminazione
int exit_code, exit_signal;
Nicola Gessa
Tabella dei processi, cosa
contiene?
• Identificatori per il processo:
int pid; int pgrp;
• Collegamenti ai processi parenti…
struct task_struct *p_opptr, *p_pptr,
*p_cptr, *p_ysptr,
*p_osptr;
• Informazioni sul filsystem
struct fs_struct *fs;
• Informazioni file aperti
struct files_struct *files;
• Memoria utilizzata dal processo
struct mm_struct *mm;
Nicola Gessa
Creazione di un processo
• L’unico modo per poter creare un processo è
tramite la chiamata alla funzione fork. Questo non si
applica solo ai processi speciali - scheduler, init e
pagedaemon - che vengono creati al momento del
bootstrap.
pid_t fork(void)
• Il processo creato tramite la funzione fork è
chiamato child process
Nicola Gessa
Creazione di un processo
• La funzione fork è chiamata una volta ma, una
volta avvenuta la creazione del nuovo processo,
ritorna “due volte”, una nel processo padre (il valore
restituito è l’ID del figlio) e una nel processo figlio (il
valore restituito è 0).
• Il padre memorizza l’ID del figlio ricevuto al
termine della chiamata alla funzione fork mentre il
processo figlio lo puo’ calcolare tramite la funzione
getppid().
• Sia il padre che il figlio continuano di seguito
l’esecuzione del codice che segue la fork: il figlio è
una copia del padre, non condividono la memoria.
Nicola Gessa
Cosa avviene nella tabella dei
processi?
• Alloca memoria per un nuovo task_struct da
associare al nuovo processo
• Cerca un elemento libero nella tabella dei
processi
• Copia le informazioni del processo padre nel
nuovo processo
• Imposta correttamente i puntatori del processo
padre e del processo figlio
Nicola Gessa
Creazione di un processo
• In generale dopo il ritorno dalla funzione fork non
è possibile sapere se verrà eseguito prima il padre o
il figlio.
• I motivi principali per cui la fork può fallire e non
può essere quindi creato un nuovo processo nel
sistema:
– troppi processi attivi nel sistema.
– troppi processi attivi per l’utente che la sta
eseguendo.
Nicola Gessa
Creazione di processo
• Usi tipici della creazione di un nuovo processo
sono:
– quando un processo vuole duplicare se stesso
così che il processo padre e il processo figlio
possano eseguire differenti sezione del codice:
ad esempio nel modello client-server i server
possono creare processi nuovi per gestire le
richieste mentre il processo principale aspetta
di riceverne di nuove.
– quando un processo desidera eseguire
programmi diversi: è questo il caso delle shell.
In questo caso il processo figlio esegue un exec
immediatamente dopo il ritorno dalla funzione
fork.
Nicola Gessa
Environment di un processo
• Ogni processo riceve una lista con i parametri di
ambiente.
• Questa lista è un array di puntatori a carattere che
contengono gli indirizzi di una stringa C terminata
con un carattere null. La lunghezza stessa dell’array
non è fissa: l’array è terminato da un puntatore
nullo.
• L’indirizzo dell’array di puntatori è memorizzato in
una variabile global environ.
extern char ** environ
• Per convenzione le stringhe rappresentano delle
coppie name=value.
• L’accesso alle variabili d’ambiente è possibile
anche tramite le funzioni getenv and putenv.
Nicola Gessa
Environment di un processo
Esempio di struttura di variabili d’ambiente
Nicola Gessa
Environment di un processo
• L’uso delle variabili d’ambiente è riservata alle
applicazioni e ad alcune funzioni di libreria; in genere
esse costituiscono un modo comodo per definire un
comportamento specifico senza dover ricorrere all'uso
di opzioni a linea di comando o di file di configurazione.
• La shell, ad esempio, ne usa molte per il suo
funzionamento (come PATH per la ricerca dei comandi)
e alcune di esse (come HOME , USER , etc.) sono
definite al login. In genere, è cura dell'amministratore
definire le opportune variabili di ambiente in uno script
di avvio. Alcune servono poi come riferimento generico
per molti programmi (come EDITOR, che indica l'editor
preferito da invocare in caso di necessità).
Nicola Gessa
Esempio con la fork
#include <sys/types>
int glob=6;
char buf[]=“stampa su stdout\n”;
int main(void){
int var; pid_t pid;
var = 88;
if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!= sizeof(buf)-1)
err_sys(“errore nelle stampa!”);
printf(“prima della fork\n”);
if((pid=fork())< 0) err_sys(“errore nella fork”);
else if (pid==0){ glob++; var++; //il figlio modifica le variabili
}else sleep(2);
printf(“pid=%d,glod=%d,var=%d\n”,getpid(), glob, var);
exit(0);
}
Nicola Gessa
Output dell’esempio precedente
$a.out
stampa su stdout
prima della fork
pid = 430, glob = 7, var = 89 //esecuzione del figlio
pid = 429, glob = 6, var = 88 //esecuzione del padre
//le variabili non sono modificate
$a.out > temp.out
$cat temp.out
stampa su stdout
prima della fork
pid = 433, glob = 7, var = 89
prima della fork
pid = 432, glob = 6, var = 88
Nicola Gessa
Output dell’esempio precedente
$a.out
stampa su stdout
prima della fork
pid = 430, glob = 7, var = 89 //esecuzione del figlio
pid = 429, glob = 6, var = 88 //esecuzione del padre
//le variabili non sono modificate
$a.out > temp.out
$cat temp.out
stampa su stdout
prima della fork
pid = 433, glob = 7, var = 89
prima della fork
pid = 432, glob = 6, var = 88
?
Nicola Gessa
Risultato dell’esempio precedente
• La funzione write NON è bufferizzata, così la prima stampa
è eseguita solo una volta.
• La funzione printf invece è bufferizzata quindi
– eseguendo il programma in maniera interattiva si
ottiene una copia della stampa “prima della fork” perchè
il carattere di newline “\n” esegue il flush del buffer di
standard output
– ridirigendo lo standard output del programma verso un
file si ottengono due copie della stampa “prima della
fork” perché in questo caso la stringa rimane nel buffer
anche alla chiamata della fork. Il buffer è copiato nel
processo figlio e in questo buffer è inserita la stringa
stampata nella seconda printf. Il buffer è quindi
stampato quando il processo termina e ne viene fatto il
flush.
Nicola Gessa
Terminazione di un processo
• Ci sono tre metodi per la terminazione normale del
processo:
– ritornando dalla funzione principale main eseguendo
la funzione return.
– chiamando la funzione exit. Questa funzione è
definita nello standard ANSI C e comporta la
chiamata di tutti gestori della exit che sono stati
registrati con la funzione atexit e la chiusura di tutti
gli stream di I/O.
– chiamando la funzione _exit. Il processo termina
immediatamente senza eseguire nessun tipo di
gestione degli stream o chiamata ad altre funzioni.
Nicola Gessa
La funzione atexit
• E’ possibile registrare delle funzioni che vengono in
seguito chiamate automaticamente dalla funzione exit()
per la gestione della chiusura del processo.
• Questa registrazione è fatto utilizzando la funzione
atexit:
int atexit(void (*func)(void));
• Questa dichiarazione specifica che deve essere passato
l’indirizzo della funzione da richiamare come parametro
della atexit().
• La funzione exit() chiama le funzioni che sono state
registrate in ordine inverso rispetto alla loro registrazione
e tante volte quante sono state registrate
Nicola Gessa
Esempio con atexit
#include <sys/types>
static void my_exit1(void), my_exit2(void)
int main(void){
if(atexit(my_exit2) !=0 ) err_sys(“impossibile registrare
my_exit2!”);
if(atexit(my_exit1) !=0 ) err_sys(“impossibile registrare
my_exit1!”);
if(atexit(my_exit1) !=0 ) err_sys(“impossibile registrare
my_exit1!”);
print(“main terminato”);
return(0);
}
static void my_exit1(void){ print(“primo exit handler\n”); }
static void my_exit2(void){ print(“secondo exit handler\n”); }
RISULTATO
$a.out
main terminato
primo exit handler
primo exit handler
secondo exit handler
Nicola Gessa
Terminazione di un processo
• Ci sono due metodi per la terminazione anomala di un
processo:
– chiamando la funzione abort, che genera un segnale
di SIGABRT.
– quando un processo riceve un certo segnale. I segnali
possono essere generati dallo stesso processo, da altri
processi o dal kernel ( ad esempio quando un
processo cerca di fare dei riferimenti a zone di
memoria che non sono nel suo spazio di memoria)
• La terminazione di un processo comporta la chiusura di
tutti i suoi descrittori e il rilascio della memoria.
•
Può darsi che la terminazione (anomala) di un
processo provochi il core dump (scarico della memoria). In
pratica si ottiene la creazione di un file nella working
directory, contenente l’immagine del processo interrotto.
Questi file servono soltanto a documentare un incidente di
funzionamento ed a permetterne l’analisi attraverso
strumenti diagnostici opportuni (debugger).
Nicola Gessa
Terminazione di un processo
• Il processo che termina deve essere in grado di
informare della modalità di terminazione il suo processo
padre. Utilizzando le funzioni exit, _exit o return questo è
consentito passando alle due funzioni un argomento. In
caso di terminazione anomala, il kernel genera uno stato
di terminazione che indica i motivi della terminazione
anormale del processo.
• Se il processo termina chiamando le funzioni di exit o
return senza parametro, lo stato di uscita rimane
indefinito
• Il padre del processo può ottenere lo stato della
terminazione utilizzando le funzioni wait e waitpid
Nicola Gessa
Terminazione di un processo
• Cosa capita quando il processo padre termina prima del
figlio? In questo caso il processo init diventa il nuovo
padre: il processo figlio è stato ereditato da init. In pratica
quando un processo termina viene controllato se questo
aveva dei figli, e nel caso ne vengano trovati, l’ID del loro
padre diventa 1.
• Cosa capita se il processo figlio termina prima del
padre? In questo caso il padre potrebbe non essere in
grado di ottenere il suo stato di terminazione. Il sistema
allora mantiene un certo numero di informazioni sul
processo terminato in modo da poterle passare al
processo padre quando questi ne fa richiesta ( tramite le
funzioni wait o waitpid). Queste informazioni
comprendono l’ID del processo, lo stato di terminazione e
il tempo di CPU impiegato. Processi terminati il cui padre
non ha ancora eseguito le funzioni come wait sono detti
zombie.
Nicola Gessa
Terminazione dei processi
• Quando un processo termina, sia normalmente o in
maniera anomala, il padre viene avvisato dal kernel
tramite il segnale SIGCHLD.
• Questo segnale è la notifica asincrona del kernel della
morte del processo figlio. Quindi il processo può definire
un gestore di questo segnale per poter gestire la
terminazione dei processi figli. Nel caso tale gestore non
sia definito il segnale viene ignorato.
• Per ottenere informazioni sulla terminazione di un
processo figlio si utilizzano le funzioni wait e waitpid:
pid_t wait(int *statloc)
pid_t waitpid(pid_t pid, int *statloc, int option);
Nicola Gessa
Terminazione dei processi
• La chiamata alle funzioni wait e waitpid può
– bloccare il processo che l’esegue se tutti i suoi figli
sono ancora in esecuzione.
– ritornare immediatamente restituendo il codice di
terminazione del figlio se un figlio ha già terminato e
si aspetta che il suo stato di terminazione sia
registrato.
– ritornare un errore, se il processo non ha nessun figlio.
• Le differenze fra le funzioni wait e waitpid sono:
– la wait è bloccante mentre la waitpid ha delle opzioni
per evitare di bloccare il processo padre.
– la waitpid riceve dei parametri per specificare di quale
processo aspettare la terminazione.
Nicola Gessa
La funzione waitpid
pid_t waitpid(pid_t pid, int *statloc, int
option);
• L’argomento pid_t permette di specificare l’ID del
processo di cui si vuole attendere la terminazione.
• La funzione ritorna l’ID del processo che ha
terminato e nella variabile statloc lo stato di
terminazione.
• Ritorna errore se si specifica un ID che non
appartiene all’insieme dei figli del processo.
• Consente una versione non bloccante della
funzione wait tramite l’uso dell’argomento option.
Nicola Gessa
Esempio di creazione di zombie
Cosa succede se il padre non ha verifica la terminazione del figlio?
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void){
pid_t pid;
if ( (pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0)
/* figlio */
exit(0);
/* padre */
sleep(4);
sleep(4);
exit(0);
system("ps");
system("ps");
}
Nicola Gessa
Output dell’esempio precedente
$a.out
PID TTY
3150 pts/1
11448 pts/1
11449 pts/1
11450 pts/1
PID TTY
3150 pts/1
11448 pts/1
11449 pts/1
11451 pts/1
TIME CMD
00:00:00 bash
00:00:00 a.out
00:00:00 a.out <defunct>
00:00:00 ps
TIME CMD
00:00:00 bash
00:00:00 a.out
00:00:00 a.out <defunct>
00:00:00 ps
•La vita del processo termina solo quando la notifica della
sua conclusione viene ricevuta dal processo padre, a quel
punto tutte le risorse allocate nel sistema ad esso
Nicola Gessa
associate vengono rilasciate
Esempio con wait() - 1
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void){
pid_t pid; int status;
if ( (pid = fork()) < 0) err_sys("fork error");
else if (pid == 0) /* child */ exit(7);
if (wait(&status) != pid) /* wait for child */ err_sys("wait
error");
pr_exit(status); /* and print its status */
/* CONTINUA….. */
Nicola Gessa
Esempio con wait() - 2
if ( (pid = fork()) < 0) err_sys("fork error");
else if (pid == 0)/* figlio */abort(); /* generates SIGABRT */
if (wait(&status) != pid)/* wait for child */err_sys("wait error");
pr_exit(status); /* and print its status */
if ( (pid = fork()) < 0)err_sys("fork error");
else if (pid == 0) /* figlio */status /= 0;/* divide by 0 generates
SIGFPE */
if (wait(&status) != pid)/* wait for child */err_sys("wait error");
pr_exit(status);
/* and print its status */
exit(0);
}
Nicola Gessa
Output dell’esercizio precedente
$ a.out
normale, stato = 7
anormale, stato = 6
anormale, stato = 8
Nicola Gessa
Esempio con waitpid(2)
#include <sys/types>
#include <sys/wait.h>
#include <unistd.h>
main(void)
{ pid_t pid;
if ( (pid = fork()) < 0) err_sys("fork error");
else if (pid == 0) {
/* primo figlio */
if ( (pid = fork()) < 0) err_sys("fork error");
else if (pid > 0) exit(0);
/* primo figlio */
sleep(2); printf(“secondo figlio, parent pid = %d\n", getppid());
printf(“muore il processo 3\n”);
exit(0);
}
if (waitpid(pid, NULL, 0) != pid) /* aspetta il primo figlio */
err_sys("waitpid error");
else printf(“muore il processo 1\n”); exit(0);
}
Nicola Gessa
Output dell’esempio precedente (2)
$a.out
muore il processo1
$ secondo figlio, parent pid = 1;
muore il processo 3
Aspetta 2 secondi
printf
Processo 3
Processo
1
Processo 2
Exit()
printf
Exit()
Nicola Gessa
Ordinamento nell’esecuzione dei
processi
•Poiché non si conosce a priori l’ordine di esecuzione dei
processi padre e figli, il risultato potrebbe essere
imprevedibile, e inoltre la situazione di errore risultare
difficilmente riproducibile in fase di debugging.
•Il processo padre può attendere la fine del processo figlio
tramite la funzione waitpid.
•Il processo figlio potrebbe attendere la fine dell’esecuzione
del processo padre ?
Nicola Gessa
Ordinamento nell’esecuzione dei
processi
•Poiché non si conosce a priori l’ordine di esecuzione dei
processi padre e figli, il risultato potrebbe essere
imprevedibile, e inoltre la situazione di errore risultare
difficilmente riproducibile con difficoltà in fase di
debugging.
•Il processo padre può attendere la fine del processo figlio
tramite la funzione waitpid.
•Il processo figlio potrebbe attendere la fine dell’esecuzione
del processo padre con un codice come
while(getppid()!=1) sleep(1);
dove si riconosce la fine del padre dal fatto che il PID e
quello del processo init.
E’ una buona soluzione ?.
Nicola Gessa
Ordinamento nell’esecuzione dei
processi
•Poiché non si conosce a priori l’ordine di esecuzione dei
processi padre e figli, il risultato potrebbe essere
imprevedibile, e inoltre la situazione di errore risultare
difficilmente riproducibile con difficoltà in fase di
debugging.
•Il processo padre può attendere la fine del processo figlio
tramite la funzione waitpid.
•Il processo figlio potrebbe attendere la fine dell’esecuzione
del processo padre con un codice come
while(getppid()!=1) sleep(1);
dove si riconosce la fine del padre dal fatto che il PID e
quello del processo init.Questa soluzione è dispendiosa e
non sempre applicabile (attesa attiva).
Nicola Gessa
Ordinamento nell’esecuzione dei
processi
static void charatatime(char *); /* funzione per la
stampa di caratteri uno alla volta*/
int main(void)
{
pid_t pid;
if((pid=fork())< 0) err_sys(“errore nella fork”);
else if (pid==0){charatatime(“output dal figlio\n”);
}else{charatatime(“output dal padre\n”);}
exit(0);
}
static void charatatime(char *str){
char *ptr;
int c;
setbuf(stdout,NULL); /* output unbuffered */
for(ptr=str;c = *ptr++;) putc(c,stdout);
}
Nicola Gessa
Risultato dell’esempio precedente
•A seconda delle condizioni di esecuzione, o degli
algoritmi di scheduling adottati, i risultati ottenuti
con il programma precedente possono essere anche
molto diversi:
$ a.out
output from child
output from parent
$ a.out
oouuttppuutt ffrroomm cphairlednt
$ a.out
ooutput from child
utput from parent
Nicola Gessa
La funzione exec
• Quando un processo chiama la funzione exec, quel
processo inizia l’esecuzione del codice del nuovo
programma specificato, e il nuovo programma inizia
la sua esecuzione partendo dalla sua funzione
main().
• NON viene creato un nuovo processo, quindi l’ID
non cambia.
• Con la fork quindi si creano nuovi processi, mentre
con la exec si avviano nuovi programmi.
• Le funzioni exit, wait e waitpid sono usate sempre
per gestire la terminazione dei processi.
Nicola Gessa
Le funzioni exec
• int execl(const char *pathname, const char
*arg0,.,/*(char *)0*/);
• int execv(const char *pathname, char *const argv[]);
• int execle(const char *pathname, const char
*arg0,.,/*(char *)0, char *const envp[]*/);
• int execve(const char *pathname, char *const argv[],
char *const envp[]);
• int execlp(const char *filename, const char
*arg0,.,/*(char *)0*/);
• int execvp(const char *filename, char *const argv[]);
ritornano -1 in caso di errore.
Nicola Gessa
La funzione exec
• La funzione exec prende come parametri anche i
parametri da passare al programma da eseguire; tali
argomenti possono essere passati come lista oppure come
array.
• Normalmente un processo consente di propagare il suo
ambiente di esecuzione ai processi figli, ma è possibile
specificare anche particolari ambienti di esecuzione.
• Nel sistema si possono fissare dei limiti alla dimensione
degli argomenti e alla lista delle variabili d’ambiente.
• Ogni descrittore di file ha associato un flag che
consente di forzare la chiusura dei del descrittore del file
quando viene eseguita una chiamata ad una exec.
Nicola Gessa
Esempio con la execle
#include <sys/types>
char *env_init[]={“USER=unknown”,PATH=“/tmp”,NULL}
int main(void){
pid_t pid;
if((pid=fork())< 0) err_sys(“errore nella fork”);
else if (pid==0){
/* figlio*/
if(execle(“/home/bin/echoall”,”echoall”,”arg1”,”arg2”,
(char *) 0,env_init)<0) errsys(“errore nella execle”);}
if(waitpid(pid,NULL,0)<0) err_sys(“errore nella wait”);
if((pid=fork())< 0) err_sys(“errore nella fork”);
else if (pid==0){ if(execlp(“echoall”,”echoall”,”newarg”,
(char*)0,env_init)<0)
errsys(“errore nella execle”);}
exit(0);
}
Nicola Gessa
Risultato dell’esempio
precedente
$ a.out
argv[0]:echoall
argv[1]:arg1
argv[2]:arg2
USER=unknown
PATH=/tmp
argv[0]:echoall
$ argv[1]:newarg
USER=stevens
HOME=/home/myhome
…..
EDITOR=/usr/vi
Nicola Gessa
Ancora sull’esempio con la execle
#include <sys/types>
char *env_init[]={“USER=unknown”,PATH=“/tmp”,NULL}
int main(void){
Manca qualcosa?
pid_t pid;
if((pid=fork())< 0) err_sys(“errore nella fork”);
else if (pid==0){
/* figlio*/
if(execle(“/home/bin/echoall”,”echoall”,”arg1”,”arg2”,
(char *) 0,env_init)<0) errsys(“errore nella execle”);}
if(waitpid(pid,NULL,0)<0) err_sys(“errore nella wait”);
if((pid=fork())< 0) err_sys(“errore nella fork”);
else if (pid==0){ if(execlp(“echoall”,”echoall”,”newarg”,
(char*)0,env_init)<0)
errsys(“errore nella execle”);}
exit(0);
}
Nicola Gessa
Ancora sull’esempio con la execle
#include <sys/types>
char *env_init[]={“USER=unknown”,PATH=“/tmp”,NULL}
int main(void){
pid_t pid;
if((pid=fork())< 0) err_sys(“errore nella fork”);
else if (pid==0){
/* figlio*/
if(execle(“/home/bin/echoall”,”echoall”,”arg1”,”arg2”,
(char *) 0,env_init)<0) errsys(“errore nella execle”);}
if(waitpid(pid,NULL,0)<0) err_sys(“errore nella wait”);
if((pid=fork())< 0) err_sys(“errore nella fork”);
else if (pid==0){ if(execlp(“echoall”,”echoall”,”newarg”,
(char*)0,env_init)<0)
errsys(“errore nella execle”);}
exit(0);
}
NO!
Sarebbe inutile inserire il comando di exit poiché la dalla funzione exec(a
meno di errori nella sua esecuzione), non ritorna mai
Nicola Gessa
Esecuzione dei programmi
CODICE DELLA
SHELL
Caricamento
PROCESSO CHE
ESEGUE LA SHELL
Terminazione
CODICE DEL
PROGRAMMA
Caricamento
PROCESSO CHE
ESEGUE IL
PROGRAMMA
PROCESSI
PROGRAMMI
Nicola Gessa
Duplicazione
Esempio di programma shell-like
Come esempio sul funzionamento della exec
vediamo un programma che riprende la struttura
base di una shell. I punti fondamentali sono:
•Si usa la funzione gets per leggere dentro ad un
ciclo infinito una linea per volta dallo standard input.
L’inserimento come primo carattere di un NULL fa
terminare l’esecuzione.
•Calcoliamo la lunghezza della stringa e sostituiamo
l’ultimo carattere di newline con un byte null per
poter passare questa stringa come argomento della
successiva funzione execlp
•Chiamiamo la funzione fork per creare un nuovo
processo che sarà la copia del padre.
Nicola Gessa
Esempio di programma shell-like
•Nel processo figlio chiamiamo la funzione execpl
per eseguire il comando inserito dall’utente dallo
standard input. Rimpiazziamo quindi il processo
figlio con il nuovo programma.
•Il processo padre rimane in attesa della
terminazione del processo figlio chiamando la
funzione waitpid.
•In questo caso non riusciamo a passare argomenti
al comando eseguito. Per far questo dovremmo
parserizzare l’input dell’utente e separare gli
argomenti contenuti nella stringa per poterli poi
passare alla funzione execpl.
Nicola Gessa
Esempio di programma shell-like
int main(void){
char buf[MAXLINE]; pid_t pid; int status;
print (“%%”);
while(fgets(buf,MAXLINE,stdin)!=NULL){
buf[strlen(buf)-1]=0;
if((pid=fork())< 0) err_sys(“errore nella fork”);
else if (pid==0){
if(execlp(buf,buf,(char *) 0)<0) errsys(“errore nella execlp”);
}
if((pid=waitpid(pid,&status,0))<0) err_sys(“errore nella wait”);
print (“%%”);
}
}
exit(0);
Nicola Gessa
Risultato
$a.out
%date
comandi letti dalla
mia shell
Fri Jun 10 10:30:23 MST 2002
%pwd
/home/students/rossi
%ls
a.out……………….
% ^D
$
Nicola Gessa
Schema del ciclo di vita di un
processo
User process
_exit
User
Exit handler
_exit
return
…………...
call
exit
Main
Exit
function
Exit handler
return
function
call
call
return
call
exit
call
return
function
exit
_exit
return
Standard I/O
cleanup
C start-up
routine
exec
kernel
Nicola Gessa
Tempo di esecuzione di un
processo
Unix registra 3 valori relativi al tempo di esecuzione di un
processo:
•clock time: rappresenta il tempo che un processo ha
impiegato per essere eseguito e il suo valore dipende dal
numero di processi totali che sono in esecuzione
•user CPU time: è il tempo di CPU che stato impiegato
dalle istruzioni utente
•system CPU time: è il tempo di CPU impiegato alle
esecuzioni delle istruzioni eseguite dal kernel per conto
dell’utente
La somma di user CPU time e system CPU time è detta
CPU time
Questi valori sono ottenuti con la funzione time
Nicola Gessa
La funzione system
Può essere conveniente eseguire un comando
dall’interno di un programma: la funzione system
fornisce l’interfaccia per questa operazione.
int system(const char *cmdstring)
•Il parametro cmdstring specifica il comando da
eseguire.
•La funzione system è implementata chiamando le
funzioni fork, exec e waitpid. Il valore di ritorno
dipende da quale di queste funzioni fallisce.
•Il vantaggio di usare la funzione system invece
delle tre funzioni risiede nel fatto che questa
funzione gestisce sia gli errori che i segnali richiesti.
Nicola Gessa
Esempio con la funzione system
#include
#include
#include
<sys/types.h>
<sys/wait.h>
"ourhdr.h"
int main(void){
int
}
status;
if ( (status = system("date")) < 0)
err_sys("system() error");
pr_exit(status);
if ( (status = system("nosuchcommand")) < 0)
err_sys("system() error");
pr_exit(status);
if ( (status = system("who; exit 44")) < 0)
err_sys("system() error");
pr_exit(status);
exit(0);
Nicola Gessa
Risultato dell’esempio
precedente
$a.out
Thu Aug 29 13.23.21 MST 2001
normal termination, exit status = 0
sh: nosuchcommand: not found
normal termination, exit status = 1
stevens console Aug 25 10.25
stevens ttyp0
Aug 29 05:23
stevens ttyp1
Aug 29 05:24
stevens ttyp2
Aug 29 05:25
normal termination, exit status = 44
Nicola Gessa
Scarica

Come definire un processo?