TL; DR : Perché questo è il metodo ottimale per creare nuovi processi e mantenere il controllo nella shell interattiva
fork () è necessario per processi e tubi
Per rispondere alla parte specifica di questa domanda, se grep blabla foo
dovesse essere chiamato exec()
direttamente tramite genitore, il genitore si impadronirebbe di esistere e il suo PID con tutte le risorse verrebbe rilevato da grep blabla foo
.
Tuttavia, parliamo in generale di exec()
e fork()
. Il motivo principale di tale comportamento è perché fork()/exec()
è il metodo standard per creare un nuovo processo su Unix / Linux, e questa non è una cosa specifica; questo metodo è stato applicato sin dall'inizio e influenzato da questo stesso metodo da sistemi operativi già esistenti del tempo. Parafrasare in qualche modo la risposta di goldilocks su una domanda correlata, fork()
per creare un nuovo processo è più semplice poiché il kernel ha meno lavoro da fare per quanto riguarda l'allocazione delle risorse e molte proprietà (come descrittori di file, ambiente, ecc.) essere ereditato dal processo parent (in questo caso da bash
).
In secondo luogo, per quanto riguarda le shell interattive, non è possibile eseguire un comando esterno senza biforcazione. Per avviare un eseguibile che risiede sul disco (ad esempio /bin/df -h
), è necessario chiamare una delle exec()
funzioni familiari, come ad esempio execve()
, che sostituirà il genitore con il nuovo processo, assumerà il suo PID e i descrittori di file esistenti, ecc. Per la shell interattiva, si desidera che il controllo ritorni all'utente e lasciare che la shell interattiva padre continui. Pertanto, il modo migliore è quello di creare un sottoprocesso tramite fork()
e lasciare che tale processo venga acquisito tramite execve()
. Quindi il PID 1156 della shell interattivo genererebbe un figlio tramite fork()
PID 1157, quindi chiamerebbe execve("/bin/df",["df","-h"],&environment)
, che viene /bin/df -h
eseguito con PID 1157. Ora la shell deve solo attendere che il processo termini e restituisca il controllo.
Nel caso in cui sia necessario creare una pipe tra due o più comandi, ad esempio df | grep
, è necessario un modo per creare due descrittori di file (ovvero leggere e scrivere end of pipe che provengono da pipe()
syscall), quindi in qualche modo lasciare che due nuovi processi li ereditino. Questo viene fatto biforcando un nuovo processo e quindi copiando l'estremità di scrittura della pipe tramite dup2()
call sul suo stdout
aka fd 1 (quindi se end di scrittura è fd 4, lo facciamo dup2(4,1)
). Quando exec()
per deporre le uova df
avviene il processo figlio penserà nulla del suo stdout
e scrivere senza essere a conoscenza (a meno che attivamente controlli) che la sua uscita va in realtà un tubo. Stesso processo succede grep
, tranne che fork()
, prendere fine di lettura di tubo con fd 3 e dup(3,0)
prima deposizione delle uova grep
conexec()
. Per tutto questo tempo il processo padre è ancora lì, in attesa di riprendere il controllo una volta completata la pipeline.
Nel caso di comandi integrati, generalmente la shell no fork()
, ad eccezione del source
comando. Le sottostrutture richiedono fork()
.
In breve, questo è un meccanismo necessario e utile.
Svantaggi del fork e delle ottimizzazioni
Ora, questo è diverso per le shell non interattive , come ad esempio bash -c '<simple command>'
. Nonostante fork()/exec()
sia un metodo ottimale in cui devi elaborare molti comandi, è uno spreco di risorse quando hai un solo comando. Per citare Stéphane Chazelas da questo post :
Il fork è costoso, in termini di tempo di CPU, memoria, descrittori di file allocati ... Avere un processo shell che sta aspettando solo un altro processo prima di uscire è solo uno spreco di risorse. Inoltre, rende difficile riportare correttamente lo stato di uscita del processo separato che eseguirà il comando (ad esempio, quando il processo viene interrotto).
Pertanto, molte shell (e non solo bash
) usano exec()
per consentire che questo bash -c ''
venga assunto da quel singolo semplice comando. Ed esattamente per i motivi sopra indicati, è meglio ridurre al minimo le pipeline negli script di shell. Spesso puoi vedere i principianti fare qualcosa del genere:
cat /etc/passwd | cut -d ':' -f 6 | grep '/home'
Certo, questo sarà fork()
3 processi. Questo è un semplice esempio, ma considera un file di grandi dimensioni, nel raggio di Gigabyte. Sarebbe molto più efficiente con un processo:
awk -F':' '$6~"/home"{print $6}' /etc/passwd
Lo spreco di risorse in realtà può essere una forma di attacco Denial of Service, e in particolare le bombe a forcella vengono create tramite funzioni shell che si autodefiniscono in pipeline, che crea più copie di se stesse. Al giorno d'oggi, questo viene mitigato limitando il numero massimo di processi nei cgroup su systemd , che Ubuntu utilizza anche dalla versione 15.04.
Ovviamente ciò non significa che il fork sia solo male. È ancora un meccanismo utile come discusso in precedenza, ma nel caso in cui è possibile cavarsela con meno processi e consecutivamente meno risorse e quindi prestazioni migliori, è necessario evitare fork()
se possibile.
Guarda anche