Explique la función exec () y su familia

98

¿Cuál es la exec()función y su familia? ¿Por qué se utiliza esta función y cómo funciona?

Por favor, alguien explique estas funciones.

usuario507401
fuente
4
Intenta leer a Stevens de nuevo y aclara qué es lo que no entiendes.
vlabrecque

Respuestas:

244

De manera simplista, en UNIX, tiene el concepto de procesos y programas. Un proceso es un entorno en el que se ejecuta un programa.

La idea simple detrás del "modelo de ejecución" de UNIX es que hay dos operaciones que puede realizar.

La primera es to fork(), que crea un proceso completamente nuevo que contiene un duplicado (en su mayoría) del programa actual, incluido su estado. Hay algunas diferencias entre los dos procesos que les permiten determinar cuál es el padre y cuál es el hijo.

El segundo es para exec(), que reemplaza el programa en el proceso actual con un programa completamente nuevo.

A partir de esas dos operaciones simples, se puede construir todo el modelo de ejecución de UNIX.


Para agregar más detalles a lo anterior:

El uso fork()y exec()ejemplifica el espíritu de UNIX en el sentido de que proporciona una forma muy sencilla de iniciar nuevos procesos.

La fork()llamada hace casi un duplicado del proceso actual, idéntico en casi todos los sentidos (no todo se copia, por ejemplo, los límites de recursos en algunas implementaciones, pero la idea es crear una copia lo más cercana posible). Solo un proceso llama, fork() pero dos procesos regresan de esa llamada; suena extraño pero es realmente bastante elegante

El nuevo proceso (llamado hijo) obtiene un ID de proceso (PID) diferente y tiene el PID del proceso anterior (el padre) como su PID padre (PPID).

Debido a que los dos procesos ahora están ejecutando exactamente el mismo código, necesitan poder decir cuál es cuál - el código de retorno de fork()proporciona esta información - el niño obtiene 0, el padre obtiene el PID del niño (si fork()falla, no se crea el hijo y el padre recibe un código de error).

De esa manera, el padre conoce el PID del niño y puede comunicarse con él, matarlo, esperarlo, etc. (el niño siempre puede encontrar su proceso padre con una llamada a getppid()).

La exec()llamada reemplaza todo el contenido actual del proceso con un nuevo programa. Carga el programa en el espacio de proceso actual y lo ejecuta desde el punto de entrada.

Por lo tanto, fork()y a exec()menudo se usan en secuencia para ejecutar un nuevo programa como hijo de un proceso actual. Los shells suelen hacer esto cada vez que intenta ejecutar un programa como find: el shell se bifurca, luego el niño carga el findprograma en la memoria, configurando todos los argumentos de la línea de comandos, E / S estándar, etc.

Pero no es necesario que se usen juntos. Es perfectamente aceptable que un programa llame fork()sin seguimiento exec()si, por ejemplo, el programa contiene tanto código principal como secundario (debe tener cuidado con lo que hace, cada implementación puede tener restricciones).

Esto se usó bastante (y todavía lo es) para demonios que simplemente escuchan en un puerto TCP y bifurcan una copia de sí mismos para procesar una solicitud específica mientras el padre vuelve a escuchar. Para esta situación, el programa contiene tanto el código principal como el secundario.

Del mismo modo, los programas que saben que están terminados y solo quieren ejecutar otro programa no necesitan hacerlo fork(), exec()y luego wait()/waitpid()para el niño. Simplemente pueden cargar al niño directamente en su espacio de proceso actual con exec().

Algunas implementaciones de UNIX tienen un sistema optimizado fork()que usa lo que ellos llaman copia en escritura. Este es un truco para retrasar la copia del espacio de proceso fork()hasta que el programa intente cambiar algo en ese espacio. Esto es útil para aquellos programas que usan solo fork()y no exec()porque no tienen que copiar un espacio de proceso completo. Bajo Linux, fork()solo hace una copia de las tablas de páginas y una nueva estructura de tareas, exec()hará el trabajo duro de "separar" la memoria de los dos procesos.

Si exec se llama siguiente fork(y esto es lo que sucede principalmente), eso provoca una escritura en el espacio del proceso y luego se copia para el proceso hijo, antes de que se permitan las modificaciones.

Linux también tiene un vfork(), aún más optimizado, que comparte casi todo entre los dos procesos. Por eso, existen ciertas restricciones en lo que puede hacer el niño, y el padre se detiene hasta que el niño llama exec()o _exit().

El padre debe detenerse (y el hijo no puede regresar de la función actual) ya que los dos procesos incluso comparten la misma pila. Esto es un poco más eficiente para el caso de uso clásico de fork()seguido inmediatamente por exec().

Tenga en cuenta que hay toda una familia de execllamadas ( execl, execle, execveetc.), pero execen el contexto aquí significa cualquiera de ellos.

El siguiente diagrama ilustra la fork/execoperación típica donde bashse usa el shell para listar un directorio con el 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
paxdiablo
fuente
12
Gracias por una explicación tan elaborada :)
Faizan
2
Gracias por la referencia de shell con el programa de búsqueda. Exactamente lo que necesitaba entender.
Usuario
¿Por qué se executiliza la utilidad para redirigir la E / S del proceso actual? ¿Cómo se usó el caso "nulo", ejecutando exec sin un argumento, para esta convención?
Ray
@Ray, siempre lo he considerado una extensión natural. Si piensa que execes el medio para reemplazar el programa actual (el shell) en este proceso por otro, entonces no especificar ese otro programa con el que reemplazarlo puede significar simplemente que no desea reemplazarlo.
paxdiablo
Entiendo lo que quiere decir si por "extensión natural" se refiere a algo parecido al "crecimiento orgánico". Parece que la redirección se habría agregado para admitir la función de reemplazo del programa, y ​​puedo ver que este comportamiento permanece en el caso degenerado de execser llamado sin un programa. Pero es un poco extraño en este escenario, ya que la utilidad original de redirigir a un nuevo programa, un programa que en realidad se execeliminaría, desaparece y tiene un artefacto útil, que redirige el programa actual, que no se está execejecutando o iniciando. de cualquier manera, en su lugar.
Ray
36

Las funciones de la familia exec () tienen diferentes comportamientos:

  • l: los argumentos se pasan como una lista de cadenas al main ()
  • v: los argumentos se pasan como una matriz de cadenas al main ()
  • p: ruta / s para buscar el nuevo programa en ejecución
  • e: la persona que llama puede especificar el entorno

Puedes mezclarlos, por lo tanto tienes:

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

Para todos ellos, el argumento inicial es el nombre de un archivo que se va a ejecutar.

Para obtener más información, lea la página del manual de exec (3) :

man 3 exec  # if you are running a UNIX system
T04435
fuente
1
Curiosamente, se perdió execve()de su lista, que está definida por POSIX, y agregó execvpe(), que no está definida por POSIX (principalmente por razones de precedentes históricos; completa el conjunto de funciones). De lo contrario, una explicación útil de la convención de nomenclatura para la familia, un complemento útil de paxdiablo , una respuesta que explica más sobre el funcionamiento de las funciones.
Jonathan Leffler
Y, en su defensa, veo que la página de manual de Linux para execvpe()(et al) no aparece execve(); tiene su propia página de manual separada (al menos en Ubuntu 16.04 LTS); la diferencia es que las otras exec()funciones de la familia se enumeran en la sección 3 (funciones) mientras que execve()se enumeran en la sección 2 (llamadas al sistema). Básicamente, todas las demás funciones de la familia se implementan en términos de una llamada a execve().
Jonathan Leffler
18

La execfamilia de funciones hace que su proceso ejecute un programa diferente, reemplazando el programa anterior que estaba ejecutando. Es decir, si llamas

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

luego, el lsprograma se ejecuta con la identificación del proceso, el directorio de trabajo actual y el usuario / grupo (derechos de acceso) del proceso que llamó execl. Posteriormente, el programa original ya no se ejecuta.

Para iniciar un nuevo proceso, forkse utiliza la llamada al sistema. Para ejecutar un programa sin tener que reemplazar el original, es necesario fork, a continuación exec.

Fred Foo
fuente
Gracias, eso fue realmente útil. Actualmente estoy haciendo un proyecto que requiere que usemos exec () y su descripción solidificó mi comprensión.
TwilightSparkleTheGeek
7

cuál es la función ejecutiva y su familia.

La execfamilia es la función de todas las funciones que se utilizan para ejecutar un archivo, tales como execl, execlp, execle, execv, y execvp.Son todos los interfaces a execvey proporcionar diferentes métodos de llamarlo.

¿Por qué se usa esta función?

Las funciones Exec se utilizan cuando desea ejecutar (lanzar) un archivo (programa).

Y, cómo funciona.

Funcionan sobrescribiendo la imagen del proceso actual con la que inició. Reemplazan (al finalizar) el proceso que se está ejecutando actualmente (el que llamó al comando exec) con el nuevo proceso que se ha iniciado.

Para más detalles: vea este enlace .

Reese Moore
fuente
7

execse usa a menudo junto con fork, lo que vi que también preguntó, por lo que discutiré esto con eso en mente.

execconvierte el proceso actual en otro programa. Si alguna vez viste Doctor Who, entonces esto es como cuando se regenera: su cuerpo viejo es reemplazado por uno nuevo.

La forma en que esto sucede con su programa y execes que muchos de los recursos que el kernel del sistema operativo verifica para ver si el archivo al que está pasando execcomo argumento del programa (primer argumento) es ejecutable por el usuario actual (ID de usuario del proceso haciendo la execllamada) y, si es así, reemplaza el mapeo de memoria virtual del proceso actual con una memoria virtual del nuevo proceso y copia los datos argvy envpque se pasaron en la execllamada en un área de este nuevo mapa de memoria virtual. Varias otras cosas también pueden suceder aquí, pero los archivos que estaban abiertos para el programa que llamó exectodavía estarán abiertos para el nuevo programa y compartirán el mismo ID de proceso, pero el programa que llamó execcesará (a menos que el ejecutivo falle).

La razón por la que esto se hace de esta manera es que al separar la ejecución de un nuevo programa en dos pasos como este, puede hacer algunas cosas entre los dos pasos. Lo más común es asegurarse de que el nuevo programa tenga ciertos archivos abiertos como ciertos descriptores de archivo. (recuerde aquí que los descriptores de archivo no son lo mismo que FILE *, pero son intvalores que el kernel conoce). Haciendo esto puedes:

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 */

Esto logra ejecutar:

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

desde el shell de comandos.

nategoose
fuente
5

Cuando un proceso usa fork (), crea una copia duplicada de sí mismo y este duplicado se convierte en el hijo del proceso. El fork () se implementa usando la llamada al sistema clone () en Linux, que regresa dos veces desde el kernel.

  • Se devuelve al padre un valor distinto de cero (ID de proceso del hijo).
  • Se devuelve un valor de cero al niño.
  • En caso de que el niño no se cree correctamente debido a problemas como poca memoria, se devuelve -1 a la bifurcación ().

Entendamos esto con un ejemplo:

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");

En el ejemplo, asumimos que exec () no se usa dentro del proceso hijo.

Pero un padre y un hijo difieren en algunos de los atributos de PCB (bloque de control de proceso). Estos son:

  1. PID: tanto el niño como el padre tienen una ID de proceso diferente.
  2. Señales pendientes: el niño no hereda las señales pendientes de los padres. Estará vacío para el proceso hijo cuando se cree.
  3. Bloqueos de memoria: el niño no hereda los bloqueos de memoria de sus padres. Los bloqueos de memoria son bloqueos que se pueden usar para bloquear un área de memoria y luego esta área de memoria no se puede intercambiar en el disco.
  4. Bloqueos de registros: el hijo no hereda los bloqueos de registros de sus padres. Los bloqueos de registros están asociados con un bloque de archivos o con un archivo completo.
  5. La utilización de recursos del proceso y el tiempo de CPU consumido se establecen en cero para el niño.
  6. El niño tampoco hereda los temporizadores del padre.

Pero, ¿qué pasa con la memoria infantil? ¿Se crea un nuevo espacio de direcciones para un niño?

Las respuestas en el no. Después de la bifurcación (), tanto el padre como el hijo comparten el espacio de direcciones de memoria del padre. En Linux, estos espacios de direcciones se dividen en varias páginas. Solo cuando el niño escribe en una de las páginas de la memoria principal, se crea un duplicado de esa página para el niño. Esto también se conoce como copia al escribir (copiar las páginas principales solo cuando el niño escribe en ellas).

Comprendamos copia sobre escritura con un ejemplo.

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;
}

Pero, ¿por qué es necesaria la copia por escrito?

La creación de un proceso típico se lleva a cabo mediante la combinación fork () - exec (). Primero entendamos qué hace exec ().

El grupo de funciones Exec () reemplaza el espacio de direcciones del niño con un nuevo programa. Una vez que se llama a exec () dentro de un niño, se creará un espacio de direcciones separado para el niño que es totalmente diferente al del padre.

Si no hubiera copia en el mecanismo de escritura asociado con fork (), se habrían creado páginas duplicadas para el niño y todos los datos se habrían copiado en las páginas del niño. Asignar nueva memoria y copiar datos es un proceso muy costoso (requiere tiempo del procesador y otros recursos del sistema). También sabemos que, en la mayoría de los casos, el niño llamará a exec () y eso reemplazaría la memoria del niño con un nuevo programa. Así que la primera copia que hicimos habría sido un desperdicio si la copia en escritura no estuviera allí.

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.

¿Por qué el padre espera el proceso del hijo?

  1. El padre puede asignar una tarea a su hijo y esperar hasta que complete su tarea. Entonces puede realizar algún otro trabajo.
  2. Una vez que el niño termina, todos los recursos asociados con el niño se liberan excepto el bloque de control de proceso. Ahora, el niño está en estado zombi. Usando wait (), el padre puede preguntar sobre el estado del hijo y luego pedirle al núcleo que libere la PCB. En caso de que el padre no use la espera, el niño permanecerá en estado zombi.

¿Por qué es necesaria la llamada al sistema exec ()?

No es necesario usar exec () con fork (). Si el código que ejecutará el hijo está dentro del programa asociado con el padre, no se necesita exec ().

Pero piense en casos en los que el niño tiene que ejecutar varios programas. Tomemos el ejemplo del programa shell. Soporta múltiples comandos como find, mv, cp, date, etc. ¿Será correcto incluir el código de programa asociado con estos comandos en un programa o hacer que el niño cargue estos programas en la memoria cuando sea necesario?

Todo depende de su caso de uso. Tiene un servidor web que dio una entrada x que devuelve 2 ^ x a los clientes. Para cada solicitud, el servidor web crea un nuevo hijo y le pide que calcule. ¿Escribirá un programa separado para calcular esto y usará exec ()? ¿O simplemente escribirás código de cálculo dentro del programa principal?

Por lo general, la creación de un proceso implica una combinación de llamadas fork (), exec (), wait () y exit ().

Shivam Mitra
fuente
4

Las exec(3,3p)funciones reemplazan el proceso actual por otro. Es decir, el proceso actual se detiene y en su lugar se ejecuta otro, tomando algunos de los recursos que tenía el programa original.

Ignacio Vázquez-Abrams
fuente
6
No exactamente. Reemplaza la imagen de proceso actual con una nueva imagen de proceso. El proceso es el mismo proceso con el mismo pid, el mismo entorno y la misma tabla de descriptores de archivos. Lo que ha cambiado es toda la memoria virtual y el estado de la CPU.
JeremyP
@JeremyP "El mismo descriptor de archivo" fue importante aquí, ¡explica cómo funciona la redirección en shells! ¡Estaba desconcertado sobre cómo puede funcionar la redirección si el ejecutivo sobrescribe todo! Gracias
FUD