Spiega la funzione exec () e la sua famiglia


98

Qual è la exec()funzione e la sua famiglia? Perché viene utilizzata questa funzione e come funziona?

Per favore, qualcuno spieghi queste funzioni.


4
Prova a leggere di nuovo Stevens e chiarisci cosa non capisci.
vlabrecque

Risposte:


244

Semplicisticamente, in UNIX, hai il concetto di processi e programmi. Un processo è un ambiente in cui viene eseguito un programma.

La semplice idea alla base del "modello di esecuzione" UNIX è che ci sono due operazioni che puoi fare.

Il primo è a fork(), che crea un processo nuovo di zecca contenente un duplicato (principalmente) del programma corrente, incluso il suo stato. Ci sono alcune differenze tra i due processi che consentono loro di capire quale è il genitore e quale è il bambino.

Il secondo è quello di exec(), che sostituisce il programma nel processo corrente con un programma nuovo di zecca.

Da queste due semplici operazioni, è possibile costruire l'intero modello di esecuzione UNIX.


Per aggiungere qualche dettaglio in più a quanto sopra:

L'uso di fork()ed exec()esemplifica lo spirito di UNIX in quanto fornisce un modo molto semplice per avviare nuovi processi.

La fork()chiamata è quasi un duplicato del processo corrente, identico in quasi tutti i modi (non tutto viene copiato, ad esempio, i limiti delle risorse in alcune implementazioni, ma l'idea è di creare una copia più vicina possibile). Solo un processo chiama fork() ma due processi ritornano da quella chiamata: sembra strano ma è davvero piuttosto elegante

Il nuovo processo (chiamato figlio) ottiene un diverso ID processo (PID) e ha il PID del vecchio processo (genitore) come suo padre PID (PPID).

Poiché i due processi ora eseguono esattamente lo stesso codice, devono essere in grado di dire quale è quale - il codice di ritorno di fork()fornisce queste informazioni - il bambino ottiene 0, il genitore ottiene il PID del bambino (se fork()fallisce, no figlio viene creato e il genitore riceve un codice di errore).

In questo modo, il genitore conosce il PID del bambino e può comunicare con esso, ucciderlo, aspettarlo e così via (il bambino può sempre trovare il suo processo genitore con una chiamata a getppid()).

La exec()chiamata sostituisce l'intero contenuto corrente del processo con un nuovo programma. Carica il programma nello spazio di elaborazione corrente e lo esegue dal punto di ingresso.

Quindi, fork()e exec()sono spesso usati in sequenza per ottenere un nuovo programma in esecuzione come figlio di un processo corrente. Le shell in genere lo fanno ogni volta che si tenta di eseguire un programma come find- la shell fork, quindi il bambino carica il findprogramma in memoria, impostando tutti gli argomenti della riga di comando, l'I / O standard e così via.

Ma non devono essere usati insieme. È perfettamente accettabile che un programma chiami fork()senza un seguito exec()se, ad esempio, il programma contiene sia codice padre che codice figlio (è necessario fare attenzione a ciò che si fa, ogni implementazione potrebbe avere limitazioni).

Questo è stato usato abbastanza (e lo è ancora) per i demoni che semplicemente ascoltano su una porta TCP e fanno il fork di una copia di se stessi per elaborare una richiesta specifica mentre il genitore torna in ascolto. Per questa situazione, il programma contiene sia il codice padre che il codice figlio.

Allo stesso modo, i programmi che sanno di essere finiti e vogliono solo eseguire un altro programma non ne hanno bisogno fork(), exec()e quindi wait()/waitpid()per il bambino. Possono semplicemente caricare il bambino direttamente nel loro spazio di elaborazione corrente con exec().

Alcune implementazioni UNIX hanno un ottimizzato fork()che utilizza ciò che chiamano copy-on-write. Questo è un trucco per ritardare la copia dello spazio di elaborazione fork()fino a quando il programma non tenta di modificare qualcosa in quello spazio. Ciò è utile per quei programmi che utilizzano solo fork()e non exec()in quanto non devono copiare un intero spazio di elaborazione. Sotto Linux, fork()fa solo una copia delle tabelle delle pagine e una nuova struttura dei compiti, exec()farà il grosso lavoro di "separare" la memoria dei due processi.

Se exec viene chiamato following fork(e questo è ciò che accade principalmente), ciò causa una scrittura nello spazio del processo e viene quindi copiato per il processo figlio, prima che siano consentite le modifiche.

Linux ha anche un vfork(), ancora più ottimizzato, che condivide praticamente tutto tra i due processi. Per questo motivo, ci sono alcune restrizioni in ciò che il bambino può fare e il genitore si ferma finché il bambino non chiama exec()o _exit().

Il genitore deve essere fermato (e al figlio non è permesso tornare dalla funzione corrente) poiché i due processi condividono anche lo stesso stack. Questo è leggermente più efficiente per il classico caso d'uso di fork()seguito immediatamente da exec().

Si noti che non v'è una intera famiglia di execchiamate ( execl, execle, execvee così via), ma execin un contesto qui significa nessuna di esse.

Il diagramma seguente illustra l' fork/execoperazione tipica in cui la bashshell viene utilizzata per elencare una directory con il lscomando:

+--------+
| pid=7  |
| ppid=4 |
| bash   |
+--------+
    |
    | calls fork
    V
+--------+             +--------+
| pid=7  |    forks    | pid=22 |
| ppid=4 | ----------> | ppid=7 |
| bash   |             | bash   |
+--------+             +--------+
    |                      |
    | waits for pid 22     | calls exec to run ls
    |                      V
    |                  +--------+
    |                  | pid=22 |
    |                  | ppid=7 |
    |                  | ls     |
    V                  +--------+
+--------+                 |
| pid=7  |                 | exits
| ppid=4 | <---------------+
| bash   |
+--------+
    |
    | continues
    V

12
Grazie per la spiegazione così elaborata :)
Faizan

2
Grazie per il riferimento alla shell con il programma find. Esattamente quello che avevo bisogno di capire.
Utente

Perché execl'utilità viene utilizzata per reindirizzare l'IO del processo corrente? In che modo il caso "null", che esegue exec senza argomenti, è stato utilizzato per questa convenzione?
Ray

@ Ray, l'ho sempre pensato come un'estensione naturale. Se pensi che sia execil mezzo per sostituire il programma corrente (la shell) in questo processo con un altro, non specificare quell'altro programma con cui sostituirlo può semplicemente significare che non vuoi sostituirlo.
paxdiablo

Capisco cosa intendi se per "estensione naturale" intendi qualcosa sulla falsariga di "crescita organica". Sembra che il reindirizzamento sarebbe stato aggiunto per supportare la funzione di sostituzione del programma, e posso vedere questo comportamento rimanere nel caso degenerato di execessere chiamato senza un programma. Ma è un po 'strano in questo scenario poiché l'utilità originale del reindirizzamento per un nuovo programma - un programma che verrebbe effettivamente utilizzato exec- scompare e si dispone di un utile artefatto, reindirizzando il programma corrente - che non viene utilizzato execo avviato in alcun modo - invece.
Ray

36

Le funzioni nella famiglia exec () hanno comportamenti diversi:

  • l: gli argomenti vengono passati come un elenco di stringhe al main ()
  • v: gli argomenti vengono passati come un array di stringhe al main ()
  • p: percorso / i per cercare il nuovo programma in esecuzione
  • e: l'ambiente può essere specificato dal chiamante

Puoi mescolarli, quindi hai:

  • int execl (const char * percorso, const char * arg, ...);
  • int execlp (const char * file, const char * arg, ...);
  • int execle (const char * path, const char * arg, ..., char * const envp []);
  • int execv (const char * path, char * const argv []);
  • int execvp (const char * file, char * const argv []);
  • int execvpe (const char * file, char * const argv [], char * const envp []);

Per tutti loro l'argomento iniziale è il nome di un file che deve essere eseguito.

Per maggiori informazioni leggere la pagina man di exec (3) :

man 3 exec  # if you are running a UNIX system

1
È interessante notare che ti sei perso execve()dalla tua lista, che è definita da POSIX, e hai aggiunto execvpe(), che non è definita da POSIX (principalmente per ragioni di precedenti storici; completa l'insieme di funzioni). Altrimenti, un'utile spiegazione della convenzione di denominazione per la famiglia - un'utile aggiunta a paxdiablo ', una risposta che spiega di più sul funzionamento delle funzioni.
Jonathan Leffler

E, a tua difesa, vedo che la pagina man di Linux per execvpe()(et al) non elenca execve(); ha la sua pagina man separata (almeno su Ubuntu 16.04 LTS) - con la differenza che le altre exec()funzioni della famiglia sono elencate nella sezione 3 (funzioni) mentre execve()sono elencate nella sezione 2 (chiamate di sistema). In sostanza, tutte le altre funzioni della famiglia sono implementate in termini di chiamata a execve().
Jonathan Leffler

18

La execfamiglia di funzioni fa in modo che il processo esegua un programma diverso, sostituendo il vecchio programma che era in esecuzione. Cioè, se chiami

execl("/bin/ls", "ls", NULL);

quindi il lsprogramma viene eseguito con l'id del processo, la directory di lavoro corrente e l'utente / gruppo (diritti di accesso) del processo che ha chiamato execl. Successivamente, il programma originale non è più in esecuzione.

Per avviare un nuovo processo, forkviene utilizzata la chiamata di sistema. Per eseguire un programma senza sostituire l'originale, è necessario fork, quindi exec.


Grazie che è stato davvero utile. Attualmente sto facendo un progetto che ci richiede di usare exec () e la tua descrizione ha rafforzato la mia comprensione.
TwilightSparkleTheGeek

7

qual è la funzione exec e la sua famiglia.

La execfamiglia funzione è tutte le funzioni utilizzate per eseguire un file, ad esempio execl, execlp, execle, execv, e execvp.They sono tutti i frontend per execvee fornire diversi metodi di chiamarla.

perché viene utilizzata questa funzione

Le funzioni Exec vengono utilizzate quando si desidera eseguire (avviare) un file (programma).

E come funziona.

Funzionano sovrascrivendo l'immagine del processo corrente con quella avviata. Sostituiscono (terminando) il processo attualmente in esecuzione (quello che ha chiamato il comando exec) con il nuovo processo che è stato avviato.

Per maggiori dettagli: vedere questo collegamento .


7

execè spesso usato insieme a fork, di cui ho visto che hai chiesto anche tu, quindi ne discuterò tenendo presente questo.

exectrasforma il processo corrente in un altro programma. Se hai mai guardato Doctor Who, allora è come quando si rigenera: il suo vecchio corpo viene sostituito con un nuovo corpo.

Il modo in cui ciò accade con il tuo programma ed execè che molte delle risorse che il kernel del sistema operativo controlla per vedere se il file che stai passando execcome argomento del programma (primo argomento) è eseguibile dall'utente corrente (id utente del processo effettuando la execchiamata) e in tal caso sostituisce la mappatura della memoria virtuale del processo corrente con una memoria virtuale del nuovo processo e copia i dati argve envpche sono stati passati nella execchiamata in un'area di questa nuova mappa della memoria virtuale. Molte altre cose possono anche accadere qui, ma i file che erano aperti per il programma che ha chiamato execsaranno ancora aperti per il nuovo programma e condivideranno lo stesso ID di processo, ma il programma che ha chiamato execcesserà (a meno che exec non abbia fallito).

Il motivo per cui questo viene fatto in questo modo è che separando l' esecuzione di un nuovo programma in due passaggi come questo è possibile eseguire alcune operazioni tra i due passaggi. La cosa più comune da fare è assicurarsi che il nuovo programma abbia determinati file aperti come determinati descrittori di file. (ricorda qui che i descrittori di file non sono gli stessi di FILE *, ma sono intvalori che il kernel conosce). In questo modo puoi:

int X = open("./output_file.txt", O_WRONLY);

pid_t fk = fork();
if (!fk) { /* in child */
    dup2(X, 1); /* fd 1 is standard output,
                   so this makes standard out refer to the same file as X  */
    close(X);

    /* I'm using execl here rather than exec because
       it's easier to type the arguments. */
    execl("/bin/echo", "/bin/echo", "hello world");
    _exit(127); /* should not get here */
} else if (fk == -1) {
    /* An error happened and you should do something about it. */
    perror("fork"); /* print an error message */
}
close(X); /* The parent doesn't need this anymore */

Ciò consente di eseguire:

/bin/echo "hello world" > ./output_file.txt

dalla shell dei comandi.


5

Quando un processo usa fork (), crea una copia duplicata di se stesso e questo duplicato diventa il figlio del processo. Il fork () è implementato usando la chiamata di sistema clone () in linux che ritorna due volte dal kernel.

  • Al genitore viene restituito un valore diverso da zero (ID processo del figlio).
  • Al bambino viene restituito un valore pari a zero.
  • Nel caso in cui il bambino non venga creato con successo a causa di problemi come la memoria insufficiente, viene restituito -1 al fork ().

Capiamolo con un esempio:

pid = fork(); 
// Both child and parent will now start execution from here.
if(pid < 0) {
    //child was not created successfully
    return 1;
}
else if(pid == 0) {
    // This is the child process
    // Child process code goes here
}
else {
    // Parent process code goes here
}
printf("This is code common to parent and child");

Nell'esempio, abbiamo assunto che exec () non viene utilizzato all'interno del processo figlio.

Ma un genitore e un figlio differiscono in alcuni degli attributi PCB (blocco di controllo del processo). Questi sono:

  1. PID - Sia il figlio che il genitore hanno un ID processo diverso.
  2. Segnali in sospeso - Il bambino non eredita i segnali in sospeso del genitore. Sarà vuoto per il processo figlio quando viene creato.
  3. Blocchi della memoria: il bambino non eredita i blocchi della memoria del genitore. I blocchi di memoria sono blocchi che possono essere utilizzati per bloccare un'area di memoria e quindi questa area di memoria non può essere scambiata su disco.
  4. Record Locks: il bambino non eredita i record lock del genitore. I blocchi dei record sono associati a un blocco di file oa un intero file.
  5. L'utilizzo delle risorse di processo e il tempo impiegato dalla CPU sono impostati su zero per il figlio.
  6. Il bambino inoltre non eredita i timer dal genitore.

Ma per quanto riguarda la memoria del bambino? È stato creato un nuovo spazio indirizzi per un bambino?

Le risposte in no. Dopo il fork (), sia il genitore che il figlio condividono lo spazio degli indirizzi di memoria del genitore. In Linux, questi spazi degli indirizzi sono suddivisi in più pagine. Solo quando il bambino scrive su una delle pagine di memoria padre, viene creato un duplicato di quella pagina per il bambino. Questo è anche noto come copia in scrittura (copia le pagine genitore solo quando il bambino scrive su di esso).

Comprendiamo la copia su scrittura con un esempio.

int x = 2;
pid = fork();
if(pid == 0) {
    x = 10;
    // child is changing the value of x or writing to a page
    // One of the parent stack page will contain this local               variable. That page will be duplicated for child and it will store the value 10 in x in duplicated page.  
}
else {
    x = 4;
}

Ma perché è necessaria la copia su scrittura?

Una tipica creazione di un processo avviene tramite la combinazione fork () - exec (). Per prima cosa capiamo cosa fa exec ().

Il gruppo di funzioni Exec () sostituisce lo spazio degli indirizzi del bambino con un nuovo programma. Una volta che exec () viene chiamato all'interno di un figlio, verrà creato uno spazio di indirizzi separato per il figlio che è completamente diverso da quello del genitore.

Se non ci fosse un meccanismo di copia su scrittura associato a fork (), le pagine duplicate sarebbero state create per il bambino e tutti i dati sarebbero stati copiati nelle pagine del bambino. Allocare nuova memoria e copiare i dati è un processo molto costoso (richiede tempo al processore e altre risorse di sistema). Sappiamo anche che nella maggior parte dei casi, il bambino chiamerà exec () e questo sostituirà la memoria del bambino con un nuovo programma. Quindi la prima copia che abbiamo fatto sarebbe stata uno spreco se la copia su scrittura non fosse presente.

pid = fork();
if(pid == 0) {
    execlp("/bin/ls","ls",NULL);
    printf("will this line be printed"); // Think about it
    // A new memory space will be created for the child and that   memory will contain the "/bin/ls" program(text section), it's stack, data section and heap section
else {
    wait(NULL);
    // parent is waiting for the child. Once child terminates, parent will get its exit status and can then continue
}
return 1; // Both child and parent will exit with status code 1.

Perché il genitore attende un processo figlio?

  1. Il genitore può assegnare un'attività a suo figlio e attendere che completi la sua attività. Quindi può portare qualche altro lavoro.
  2. Una volta che il figlio termina, tutte le risorse associate al figlio vengono liberate tranne il blocco di controllo del processo. Ora, il bambino è in uno stato di zombi. Usando wait (), il genitore può informarsi sullo stato del figlio e poi chiedere al kernel di liberare il PCB. Nel caso in cui il genitore non usi l'attesa, il bambino rimarrà nello stato zombi.

Perché è necessaria la chiamata di sistema exec ()?

Non è necessario usare exec () con fork (). Se il codice che il bambino eseguirà è all'interno del programma associato a parent, exec () non è necessario.

Ma pensa ai casi in cui il bambino deve eseguire più programmi. Prendiamo l'esempio del programma shell. Supporta più comandi come find, mv, cp, date ecc. Sarà giusto includere il codice del programma associato a questi comandi in un programma o fare in modo che il bambino carichi questi programmi nella memoria quando richiesto?

Tutto dipende dal tuo caso d'uso. Hai un server web che ha dato un input x che restituisce 2 ^ x ai client. Per ogni richiesta, il server web crea un nuovo figlio e gli chiede di eseguire il calcolo. Scriverai un programma separato per calcolarlo e userai exec ()? O scriverai semplicemente il codice di calcolo all'interno del programma genitore?

Di solito, la creazione di un processo implica una combinazione di chiamate fork (), exec (), wait () ed exit ().


4

Le exec(3,3p)funzioni sostituiscono il processo corrente con un altro. Cioè, il processo corrente si interrompe e un altro viene eseguito al suo posto, assumendo alcune delle risorse del programma originale.


6
Non proprio. Sostituisce l' immagine di processo corrente con una nuova immagine di processo. Il processo è lo stesso processo con lo stesso pid, lo stesso ambiente e la stessa tabella del descrittore di file. Ciò che è cambiato è l'intera memoria virtuale e lo stato della CPU.
JeremyP

@JeremyP "Lo stesso descrittore di file" era importante qui, spiega come funziona il reindirizzamento nelle shell! Ero perplesso su come il reindirizzamento possa funzionare se exec sovrascrive tutto! Grazie
FUD
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.