Lanciare programmi

Si tratta, ovviamente, di lanciare programmi dall'interno di altri programmi. E' una possibilità la cui utilità dipende largamente non solo dagli scopi del programma stesso, ma anche e soprattutto dalle caratteristiche del sistema operativo. E' facile intuire che un sistema in grado di dare supporto all'elaborazione multitasking (il riferimento a Unix, ancora una volta, è voluto e non casuale) offre interessanti possibilità al riguardo (si pensi, ad esempio, ad un gruppo di programmi elaborati contemporaneamente, tutti attivati e controllati da un unico programma gestore); tuttavia, anche in ambienti meno evoluti si può fare ricorso a tale tecnica (vedere un esempio, anche se non esaustivo).

Per brevità, adottando la terminologia Unix, il programma chiamante si indica d'ora in poi con il termine parent, mentre child è il programma chiamato.

La libreria C

La libreria C fornisce supporto al lancio di programmi esterni mediante un certo numero di funzioni, che possono essere considerate standard entro certi limiti (più precisamente, lo sono in ambiente DOS); per approfondimenti circa la portabilità dei sorgenti che ne fanno uso vedere oltre.

Come al solito, per i dettagli relativi alla sintassi, si rimanda alla manualistica specifica del compilatore utilizzato; qui si intende semplicemente mettere in luce alcune interessanti caratteristiche di dette funzioni e i loro possibili ambiti di utilizzo.

system()

La funzione system() costituisce forse il mezzo più semplice per raggiungere lo scopo: essa deve infatti essere invocata passandole come unico parametro una stringa contenente, né più né meno, il comando che si intende eseguire. Ad esempio, il codice:


#include <stdlib.h>
#include <stdio.h>	// per fprintf()
#include <errno.h>	// per errno
...
    if(system("comando param1 param2") == -1)
        fprintf(stderr,"errore %d in system()\n", errno);

lancia il programma comando passandogli i parametri param1 e param2. Dal momento che, nell'esempio, per comando non è specificato un path, il sistema utilizza la variabile di environment PATH [1] (non è necessario specificare l'estensione). Dall'esempio si intuisce che system() restituisce -1 in caso di errore; tuttavia va sottolineato che la restituzione di 0 non implica che comando sia stato effettivamente eseguito secondo le intenzioni del programmatore. Infatti system() esegue il comando ricevuto come parametro attraverso l'interprete dei comandi: in altre parole, essa non fa altro che lanciare una copia dell'interprete stesso e scaricargli il barile, proprio come se fosse stata digitata la riga


command -c "comando param1 param2"

Ne segue che system() si limita a restituire 0 nel caso in cui sia riuscita a lanciare correttamente l'interprete, e non si preoccupa di come questo se la cavi poi con il comando specificato: pertanto, non solo non è possibile conoscere il valore restituito al sistema dal child, ma non è neppure possibile sapere se questo sia stato effettivamente eseguito.

Se, da una parte, ciò appare come un pesante limite, dall'altra la system() consente di gestire anche comandi interni DOS, proprio perché in realtà è l'interprete a farsene carico. Ad esempio è possibile richiedere


    system("dir /p");

e system() restituisce -1 solo se non è stato possibile lanciare l'interprete dei comandi. Inoltre, è possibile eseguire i file batch. Ancora,


    system("command");

esegue un'istanza dell'interprete, mettendo il prompt del DOS a disposizione dell'utilizzatore: digitando


exit

al prompt la shell viene chiusa e l'elaborazione del programma parent riprende [2].

Infine, system() può essere utilizzata semplicemente per verificare se l'interprete dei comandi è disponibile:


    system(NULL);

restituisce un valore diverso da 0 se è possibile lanciare l'interprete dei comandi.

E' superfluo (speriamo!) chiarire che l'argomento di system() non deve necessariamente essere una costante stringa, come si è assunto per comodità negli esempi precedenti, ma è sufficiente che esso sia di tipo char *: ciò consente la costruzione dinamica della riga di comando, ad esempio mediante l'utilizzo di funzioni atte ad operare sulle stringhe [3] (strcpy(), strcat(), sprintf(), etc.).

spawn...()

Come la system(), anche le funzioni della famiglia spawn...() consentono di lanciare programmi esterni come se fossero subroutine del parent; tuttavia esse non fanno ricorso all'interprete dei comandi, in quanto si basano sul servizio 4Bh dell'int 21h [4]: di conseguenza, non è possibile utilizzarle per invocare comandi interni DOS né file batch, tuttavia si ha un controllo più ravvicinato sull'esito dell'operazione. Esse infatti restituiscono -1 se l'esecuzione del child non è riuscita; in caso contrario restituiscono il valore che il programma child ha restituito a sua volta.

Tutte le funzioni spawn...() richiedono come primo parametro un intero, di solito dichiarato nei prototipi con il nome mode, che indica la modalità di esecuzione del programma child: in PROCESS.H sono definite le costanti manifeste P_WAIT (il child è eseguito come una subroutine) e P_OVERLAY (il child è eseguito sostituendo in memoria il parent, proprio come se fosse chiamata la corrispondente funzione della famiglia exec...()). Come osservato riguardo system() (vedere oltre), anche le funzioni spawn...() non possono essere utilizzate per lanciare shell permanenti o programmi TSR; tuttavia l'utilizzo del valore P_OVERLAY per il parametro mode consente un'eccezione, in quanto il parent scompare senza lasciare traccia di sé e, in uscita dal child, la sua esecuzione non può mai riprendere.

Il secondo parametro, di tipo char *, è invece il nome del programma da eseguire: esso, diversamente da quanto visto circa la system(), deve essere completo di estensione; inoltre, se non è specificato il path, solo le funzioni spawnlp(), spawnlpe(), spawnvp() e spawnvpe() utilizzano la variabile di environment PATH (la lettera "p" presente nel suffisso finale dei nomi delle funzioni indica proprio detta caratteristica).

Funzioni del gruppo "l"

Le funzioni del gruppo "l" si distinguono grazie alla presenza, nel suffisso finale del loro nome, della lettera "l", la quale indica che gli argomenti della riga di comando del child sono accettati dalla funzione spawnl...() come una lista di parametri, di tipo char *, conclusa da un puntatore nullo.

Ad esempio, per eseguire il comando


myutil -a -b 5 arg1 arg2

si può utilizzare la funzione spawnl():


#include <process.h>
...
    spawnl(P_WAIT,"myutil.exe","myutil","-a","-b","5","arg1","arg2",NULL);

Si noti che il nome del programma è passato due volte a spawnl(): la prima stringa indica il programma da eseguire, mentre la seconda rappresenta il primo parametro ricevuto dal programma child: essa deve essere comunque passata alla funzone spawnl...() e, per convenzione, è uguale al nome del programma stesso (il valore di argv[0], se questo è stato a sua volta scritto in linguaggio C). Il programma myutil è ricercato solo nella directory corrente; la funzione spawnlp(), la cui sintassi è identica a quella di spawnl(), effettua la ricerca in tutte le directory specificate dalla variabile di environment PATH.

Il processo child eredita l'ambiente del parent: in altre parole, le variabili di environment del child sono una copia di quelle del programma chiamante. I due environment sono pertanto identici, tuttavia il child non può accedere a quello del parent, né tantomeno modificarlo. Se il parent ha la necessità di passare al child un environment diverso dal proprio, può farlo mediante le funzioni spawnle() e spawnlpe(), che, pur essendo analoghe alle precedenti, accettano un ulteriore parametro dopo il puntatore nullo che chiude la lista degli argomenti:


    static char *newenv[] = {"USER=Pippo","PATH=C:\\DOS",NULL);
    ...
    spawnle(P_WAIT,"myutil","myutil","-a","-b","5","arg1","arg2",NULL,newenv);

lancia myutil in un environment che comprende le sole [5] variabili USER e PATH, valorizzate come evidente nella dichiarazione dell'array di stringhe (o meglio, di puntatori a stringa, o, meglio ancora, di puntatori a puntatori a carattere) newenv. Il processo parent, qualora abbia necessità di passare al child una copia modificata del proprio environment, deve arrangiarsi a costruirla utilizzando le funzioni di libreria getenv() e putenv() e la variabile globale environ [6], dichiarate in DOS.H.

Funzioni del gruppo "v"

Le funzioni del gruppo "v" si distinguono grazie alla presenza, nel suffisso finale del loro nome, della lettera "v" (in luogo della lettera "l"), la quale indica che gli argomenti della riga di comando del child sono accettati dalla funzione spawnv...() come un puntatore ad un array di stringhe, il cui ultimo elemento deve essere un puntatore nullo.

Riprendendo l'esempio precedente, il comando


myutil -a -b 5 arg1 arg2

viene gestito mediante la funzione spawnv() come segue:


#include <process.h>
...
    char *childArgv[] = {"myutil","-a","-b","5","arg1","arg2",NULL};
    ...
    spawnv(P_WAIT,"myutil.exe",childArgv);

Si intuisce facilmente che la spawnvp() cerca il comando da eseguire in tutte le directory definite nella variabile di ambiente PATH (qualora il suo path non sia specificato esplicitamente), mentre spawnv() lo ricerca solo nella directory corrente.

Si noti che il primo elemento dell'array childArgv[] punta, per convenzione, al nome del child medesimo (del resto il nome scelto per l'array nell'esempio dovrebbe suggerire che esso viene ricevuto dal child come parametro argv di main()).

Infine, le funzioni spawnve() e spawnvpe(), analogamente a spawnle() e spawnlpe(), accettano come ultimo parametro un puntatore ad un array di stringhe, che costituiranno l'environment del child.

exec...()

Le funzioni della famiglia exec...(), a differenza delle spawn...(), non trattano il child come una subroutine del parent: esso, al contrario, viene caricato in memoria ed eseguito in luogo del parent, sostituendovisi a tutti gli effetti.

I nomi e la sintassi delle funzioni exec...() sono strettamente analoghi a quelli delle spawn...(): esistono otto funzioni exec...(), ciascuna delle quali può essere posta in corrispondenza biunivoca con una spawn...(): a seconda della presenza delle lettere "l", "v", "p" ed "e" il comportamento di ciascuna exec...() è assolutamente identico a quello della corrispondente spawn...() chiamata con il parametro mode uguale a P_OVERLAY (le funzioni exec...() non accettano il parametro mode; il loro primo parametro è sempre il nome del programma da eseguire).

Se si desidera che il solito comando degli esempi precedenti sostituisca in memoria il parent e sia eseguito in luogo di questo, è del tutto equivalente utilizzare


    spawnv(P_OVERLAY,"myutil.exe",childArgv);

oppure


    execv("myutil.exe",childArgv);

ad eccezione di quanto specificato in tema di portabilità.

Tabella sinottica

Di seguito si presenta una tabella sinottica delle funzioni spawn...() ed exec...().

SINTASSI E CARATTERISTICHE DELLE FUNZIONI spawn...() E exec...()
Modo
Nome del child
Argomenti del child
Environment del child
spawnl() int:
P_WAIT,
P_OVERLAY
char *lista di char *
il primo è = child
l'ultimo è NULL
spawnlp() int:
P_WAIT,
P_OVERLAY
char *
(utilizza PATH)
lista di char *
il primo è = child
l'ultimo è NULL
spawnle() int:
P_WAIT,
P_OVERLAY
char *lista di char *
il primo è = child
l'ultimo è NULL
char **Env
spawnlpe() int:
P_WAIT,
P_OVERLAY
char *
(utilizza PATH)
lista di char *
il primo è = child
l'ultimo è NULL
char **Env
spawnv() int:
P_WAIT,
P_OVERLAY
char *char **Argv
Argv[0] =
child
Argv[ultimo] = NULL
spawnvp() int:
P_WAIT,
P_OVERLAY
char *
(utilizza PATH)
char **Argv
Argv[0] =
child
Argv[ultimo] = NULL
spawnve() int:
P_WAIT,
P_OVERLAY
char *char **Argv
Argv[0] =
child
Argv[ultimo] = NULL
char **Env
spawnvpe() int:
P_WAIT,
P_OVERLAY
char *
(utilizza PATH)
char **Argv
Argv[0] =
child
Argv[ultimo] = NULL
char **Env
execl() char * lista di char *
il primo è = child
l'ultimo è NULL
execlp() char *
(utilizza PATH)
lista di char *
il primo è = child
l'ultimo è NULL
execle() char * lista di char *
il primo è = child
l'ultimo è NULL
char **Env
execlpe() char *
(utilizza PATH)
lista di char *
il primo è = child
l'ultimo è NULL
char **Env
execv() char * char **Argv
Argv[0] =
child
Argv[ultimo] = NULL
execp() char *
(utilizza PATH)
char **Argv
Argv[0] =
child
Argv[ultimo] = NULL
execve() char * char **Argv
Argv[0] =
child
Argv[ultimo] = NULL
char **Env
execvpe() char *
(utilizza PATH)
char **Argv
Argv[0] =
child
Argv[ultimo] = NULL
char **Env

Condivisione dei file

I processi child lanciati con spawn...() e exec...() condividono i file aperti dal parent. In altre parole, entrambi i processi possono accedere ai file aperti dal parent, per ciascuno dei quali il sistema operativo mantiene un unico puntatore: ciò significa che le operazioni effettuate da uno dei processi (spostamento lungo il file, lettura, scrittura) influenzano l'altro processo; tuttavia se il child chiude il file, questo rimane aperto per il parent. Vediamo un esempio:

Il seguente frammento di codice, che si ipotizza appartenere al parent, apre il file C:\AUTOEXEC.BAT, effettua un'operazione di lettura e lancia il child, passandogli il puntatore allo stream.


...
#define   MAX    128
...
    char sPrtStr[10], line[MAX];
    FILE *inP;
    ...
    inP = fopen("C:\\AUTOEXEC.BAT","r");
    printf(fgets(line,MAX,inP));
    sprintf(sPtrStr,"%p",inP);
    spawnl(P_WAIT,"child","child",sPtrStr,NULL);
    printf(fgets(line,MAX,inP));
    ...

Se si eccettua la mancanza del pur necessario codice per la gestione degli eventuali errori, tralasciato per brevità, il listato appare piuttosto banale: l'unica particolarità è rappresentata dalla chiamata alla funzione sprintf(), con la quale si converte in stringa il valore contenuto nella variabile inP (l'indirizzo della struttura che descrive lo stream aperto dalla fopen()). Come si può vedere, il parent passa al child proprio detta stringa (è noto che i parametri ricevuti da un programma sulla riga di comando sono necessariamente stringhe), alla quale esso può accedere attraverso il proprio argv[1]. Ecco un frammento del child:


...
#define   MAX    128
...
int main(int argc,char **argv)
{
    ...
    FILE *inC;
    ...
    sscanf(argv[1],"%p",&inC);
    printf(fgets(line,MAX,inC));
    fclose(inC);
    ....
}

Il child memorizza in inC l'indirizzo della struttura che descrive lo stream aperto dal parent ricavandolo da argv[1] mediante la sscanf(), effettua un'operazione di lettura e chiude lo stream; tuttavia, il parent è ancora in grado di effettuare operazioni di lettura dopo il rientro dalla spawnl(): l'effetto congiunto dei due programmi consiste nel visualizzare le prime tre righe del file C:\AUTOEXEC.BAT.

Va sottolineato che è necessario compilare entrambi i programmi per un modello di memoria che gestisca i dati con puntatori a 32 bit (medium, large, huge): è infatti molto verosimile (per non dire scontato) che il child non condivida il segmento dati del parent, nel quale è allocata la struttura associata allo stream: l'utilizzo di indirizzi a 16 bit, che esprimono esclusivamente offset rispetto all'indirizzo del segmento dati stesso, condurrebbe inevitabilmente il child a utilizzare quel medesimo offset rispetto al proprio data segment, accedendo così ad una locazione di memoria ben diversa da quella desiderata.

Portabilità

Date le differenti caratteristiche del supporto fornito dai diversi sistemi operativi (DOS e Unix in particolare), sono necessarie alcune precisazioni relative alla portabilità del codice tra i due ambienti.

La funzione system() può essere considerata portabile: essa è infatti implementata nelle librerie standard dei compilatori in entrambi i sistemi.

Analoghe considerazioni valgono per le funzioni exec...(), ma con prudenza: in ambiente Unix, solitamente, non sono implementate le funzioni execlpe() e execvpe(). Inoltre, le funzioni execlp() e execvp() in versione Unix sono in grado di eseguire anche shell script (analoghi ai file batch del DOS). Tutte le funzioni exec...() in Unix, infine, accettano come nome del child il nome di un file ASCII che a sua volta, con una particolare sintassi, specifica qual è il programma da eseguire (ed eseguono quest'ultimo).

Le funzioni spawn...() non sono implementate in ambiente Unix. La modalità di gestione dei child, in questo caso, si differenzia profondamente proprio perché Unix è in grado di eseguire più processi contemporaneamente: pertanto un child non è necessariamente una subroutine del parent; i due programmi possono essere eseguiti in parallelo. Un modo per emulare le spawn...() consiste nell'uso congiunto delle funzioni fork() (assente nelle librerie C in DOS) ed exec...(): la fork() crea una seconda istanza del parent; di conseguenza, essa fa sì che coesistano in memoria due processi identici, l'esecuzione di entrambi i quali riprende in uscita dalla fork() stessa.. Dall'esame del valore restituito dalla fork() è possibile distinguere l'istanza parent dall'istanza child, in quanto fork() restituisce 0 al child, mentre al parent restituisce il PID [7] del child stesso. L'istanza child può, a questo punto, utilizzare una delle exec...() per eseguire il programma desiderato, mentre il parent, tramite la funzione waitpid() (anch'essa non implementata nel C in DOS) può attendere la terminazione del child e esaminarne il valore restituito mediante la macro WEXITSTATUS(). A puro titolo di esempio si riporta di seguito un programma, compilabile in ambiente Unix, che utilizza la tecnica descritta.


#include <stdio.h>	/* printf(), puts(), fprintf(), stderr */
#include <unistd.h>	/* fork(), execlp(), pid_t */
#include <errno.h>	/* errno */
#include <sys/wait.h>	/* waitpid(), WEXITSTATUS() */

int main(void);
void child(void);
void parent(pid_t pid);

int main(void)
{
    pid_t pid;

    puts("Il child elenchera' i files presenti nella directory /etc.");
    switch(pid = fork()) {
        case 0:
            child();
        case -1:
            fprintf(stderr,"Errore %d in fork().\n",errno);
            exit(errno);
        default:
            parent(pid);
    }
    return(0);
}

void child(void)
{
    if(execlp("ls","ls","-la","/etc",NULL) == -1) {
        fprintf(stderr,"Errore %d in execlp().\n",errno);
        exit(errno);
    }
}

void parent(int pid)
{
    int status;

    if(waitpid(pid,&status,0) <= 0) {
        printf("Errore %d in waitpid().\n");
        exit(errno);
    }
    printf("Il child ha restituito %d.\n",WEXITSTATUS(status));
}

In uscita dalla fork() entrambe le istanze del programma effettuano il test sul valore da questa restituito, e solo in base al risultato del test medesimo esse si differenziano, eseguendo parent() oppure child(). E' ovvio che l'istanza child non deve necessariamente eseguire una exec...() e annullarsi: essa può eseguire qualunque tipo di operazione (comprese ulteriori chiamate a fork()), come del resto l'istanza parent non ha l'obbligo di attendere la terminazione del child, ma, al contrario, può eseguire altre elaborazioni in parallelo a quello e verificarne lo stato solo in un secondo tempo.

La libreria C in ambiente Unix implementa altre funzioni (assenti sotto DOS) per il controllo dei processi child: ad esempio la popen(), che, con una sintassi del tutto analoga alla fopen() (vedere quanto detto sugli stream), consente di lanciare un programma e al tempo stesso rende disponibile uno stream di comunicazione, detto pipe, mediante il quale il parent può leggere dallo standard output o scrivere sullo standard input del child. Ancora, la pipe() apre una pipe (questa volta non collegata a standard input e standard output) che può essere utilizzata come un file virtuale in condivisione tra processi parent e child.

Come strumento di comunicazione interprocess, in DOS si può ricorrere alla condivisione dei file. Trattandosi di file reali, il metodo è certo meno efficiente della pipe, ma ha il vantaggio di risultare portabile tra i due sistemi. Per utilizzare in DOS aree di memoria in condivisione (tecnica in qualche modo paragonabile alla shared memory supportata da Unix) si può ricorrere, rinunciando alla portabilità, a qualche stratagemma.

Per approfondimenti circa le problematiche di portabilità dipendenti dai sistemi operativi si veda il paragrafo dedicato.


OK, andiamo avanti a leggere il libro... 

Non ci ho capito niente! Ricominciamo...