Problemas con fork(), pipe(), dup2() y exec() en C

11 minutos de lectura

avatar de usuario
rfgamaral

Aquí está mi código:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <wait.h>
#include <readline/readline.h>

#define NUMPIPES 2

int main(int argc, char *argv[]) {
    char *bBuffer, *sPtr, *aPtr = NULL, *pipeComms[NUMPIPES], *cmdArgs[10];
    int fdPipe[2], pCount, aCount, i, status, lPids[NUMPIPES];
    pid_t pid;

    pipe(fdPipe);

    while(1) {
        bBuffer = readline("Shell> ");

        if(!strcasecmp(bBuffer, "exit")) {
            return 0;
        }

        sPtr = bBuffer;
        pCount = -1;

        do {
            aPtr = strsep(&sPtr, "|");
            pipeComms[++pCount] = aPtr;
        } while(aPtr);

        for(i = 0; i < pCount; i++) {
            aCount = -1;

            do {
                aPtr = strsep(&pipeComms[i], " ");
                cmdArgs[++aCount] = aPtr;
            } while(aPtr);

            cmdArgs[aCount] = 0;

            if(strlen(cmdArgs[0]) > 0) {
                pid = fork();

                if(pid == 0) {
                    if(i == 0) {
                        close(fdPipe[0]);

                        dup2(fdPipe[1], STDOUT_FILENO);

                        close(fdPipe[1]);
                    } else if(i == 1) {
                        close(fdPipe[1]);

                        dup2(fdPipe[0], STDIN_FILENO);

                        close(fdPipe[0]);
                    }

                    execvp(cmdArgs[0], cmdArgs);
                    exit(1);
                } else {
                    lPids[i] = pid;

                    /*waitpid(pid, &status, 0);

                    if(WIFEXITED(status)) {
                        printf("[%d] TERMINATED (Status: %d)\n",
                            pid, WEXITSTATUS(status));
                    }*/
                }
            }
        }

        for(i = 0; i < pCount; i++) {
            waitpid(lPids[i], &status, 0);

            if(WIFEXITED(status)) {
                printf("[%d] TERMINATED (Status: %d)\n",
                    lPids[i], WEXITSTATUS(status));
            }
        }
    }

    return 0;
}

(El código se actualizó para reflejar los cambios propuestos por las dos respuestas a continuación, todavía no funciona como debería…)

Aquí está el caso de prueba donde esto falla:

nazgulled ~/Projects/SO/G08 $ ls -l
total 8
-rwxr-xr-x 1 nazgulled nazgulled  7181 2009-05-27 17:44 a.out
-rwxr-xr-x 1 nazgulled nazgulled   754 2009-05-27 01:42 data.h
-rwxr-xr-x 1 nazgulled nazgulled  1305 2009-05-27 17:50 main.c
-rwxr-xr-x 1 nazgulled nazgulled   320 2009-05-27 01:42 makefile
-rwxr-xr-x 1 nazgulled nazgulled 14408 2009-05-27 17:21 prog
-rwxr-xr-x 1 nazgulled nazgulled  9276 2009-05-27 17:21 prog.c
-rwxr-xr-x 1 nazgulled nazgulled 10496 2009-05-27 17:21 prog.o
-rwxr-xr-x 1 nazgulled nazgulled    16 2009-05-27 17:19 test
nazgulled ~/Projects/SO/G08 $ ./a.out 
Shell> ls -l|grep prog
[4804] TERMINATED (Status: 0)
-rwxr-xr-x 1 nazgulled nazgulled 14408 2009-05-27 17:21 prog
-rwxr-xr-x 1 nazgulled nazgulled  9276 2009-05-27 17:21 prog.c
-rwxr-xr-x 1 nazgulled nazgulled 10496 2009-05-27 17:21 prog.o

El problema es que debería volver a mi shell después de eso, debería ver “Shell>” esperando más información. También puede notar que no ve un mensaje similar a “[4804] TERMINADO (Estado: 0)” (pero con un pid diferente), lo que significa que el segundo proceso no terminó.

Creo que tiene algo que ver con grep, porque esto funciona:

nazgulled ~/Projects/SO/G08 $ ./a.out 
Shell> echo q|sudo fdisk /dev/sda
[4838] TERMINATED (Status: 0)

The number of cylinders for this disk is set to 1305.
There is nothing wrong with that, but this is larger than 1024,
and could in certain setups cause problems with:
1) software that runs at boot time (e.g., old versions of LILO)
2) booting and partitioning software from other OSs
   (e.g., DOS FDISK, OS/2 FDISK)

Command (m for help): 
[4839] TERMINATED (Status: 0)

Puede ver fácilmente dos mensajes de “terminar”…

Entonces, ¿qué tiene de malo mi código?

  • Parece que grep no llegó al final. No pudo imprimir la tercera línea coincidente (“prog.o”).

    – tíoO

    27 de mayo de 2009 a las 17:27

  • Imprimió la tercera línea coincidente, simplemente no copié todo correctamente. Ya he editado la publicación para solucionarlo.

    – rfgamaral

    27 mayo 2009 a las 17:40

  • Tengo un pequeño ejemplo de shell (999LOC) escrito para una tarea universitaria hace años. patch-tag.com/r/xsh En particular, job.c#job_run y process.c#process_run contienen la configuración de canalización, y main.c#waitpid_wrapper contiene el control de espera.

    – efímero

    27 mayo 2009 a las 22:10

  • Gracias, pero prefiero seguir intentando hacerlo yo mismo y publicar mis problemas cuando los tengo, así aprendo más y mejor. Además, su código es mucho más de lo que necesito saber por el momento.

    – rfgamaral

    27 mayo 2009 a las 23:50

  • Mi código es mucho menos para leer que Bash 🙂 Probablemente tendrá que descubrir cómo lidiar con setpgid y tcsetpgrp eventualmente, y toda la otra diversión que viene con el control de trabajos y el diseño histórico de sesiones UNIX y grupos de procesos de terminal …

    – efímero

    28 mayo 2009 a las 20:30

avatar de usuario
efímero

Incluso después de que sale el primer comando de su canalización (y por lo tanto se cierra stdout=~fdPipe[1]), el padre todavía tiene fdPipe[1] abierto.

Por lo tanto, el segundo comando de la tubería tiene un stdin=~fdPipe[0] eso nunca obtiene un EOF, porque el otro punto final de la tubería todavía está abierto.

Necesitas crear un nuevo pipe(fdPipe) para cada |y asegúrese de cerrar ambos extremos en el padre; es decir

for cmd in cmds
    if there is a next cmd
        pipe(new_fds)
    fork
    if child
        if there is a previous cmd
            dup2(old_fds[0], 0)
            close(old_fds[0])
            close(old_fds[1])
        if there is a next cmd
            close(new_fds[0])
            dup2(new_fds[1], 1)
            close(new_fds[1])
        exec cmd || die
    else
        if there is a previous cmd
            close(old_fds[0])
            close(old_fds[1])
        if there is a next cmd
            old_fds = new_fds
if there are multiple cmds
    close(old_fds[0])
    close(old_fds[1])

Además, para estar más seguro, debe manejar el caso de fdPipe y {STDIN_FILENO,STDOUT_FILENO} superposición antes de realizar cualquiera de las close y dup2 operaciones. Esto puede suceder si alguien logró iniciar su shell con stdin o stdout cerrados, y generará una gran confusión con el código aquí.

Editar

   fdPipe1           fdPipe3
      v                 v
cmd1  |  cmd2  |  cmd3  |  cmd4  |  cmd5
               ^                 ^
            fdPipe2           fdPipe4

Además de asegurarme de cerrar los puntos finales de la tubería en el padre, estaba tratando de señalar que fdPipe1, fdPipe2etc no poder ser el mismo pipe().

/* suppose stdin and stdout have been closed...
 * for example, if your program was started with "./a.out <&- >&-" */
close(0), close(1);

/* then the result you get back from pipe() is {0, 1} or {1, 0}, since
 * fd numbers are always allocated from the lowest available */
pipe(fdPipe);

close(0);
dup2(fdPipe[0], 0);

Sé que no usas close(0) en su código actual, pero el último párrafo le advierte que tenga cuidado con este caso.

Editar

La siguiente mínimo el cambio en su código lo hace funcionar en el caso de falla específico que mencionó:

@@ -12,6 +12,4 @@
     pid_t pid;

-    pipe(fdPipe);
-
     while(1) {
         bBuffer = readline("Shell> ");
@@ -29,4 +27,6 @@
         } while(aPtr);

+        pipe(fdPipe);
+
         for(i = 0; i < pCount; i++) {
                 aCount = -1;
@@ -72,4 +72,7 @@
         }

+        close(fdPipe[0]);
+        close(fdPipe[1]);
+
         for(i = 0; i < pCount; i++) {
                 waitpid(lPids[i], &status, 0);

Esto no funcionará para más de un comando en la canalización; para eso, necesitarías algo como esto: (no probado, ya que también tienes que arreglar otras cosas)

@@ -9,9 +9,7 @@
 int main(int argc, char *argv[]) {
     char *bBuffer, *sPtr, *aPtr = NULL, *pipeComms[NUMPIPES], *cmdArgs[10];
-    int fdPipe[2], pCount, aCount, i, status, lPids[NUMPIPES];
+    int fdPipe[2], fdPipe2[2], pCount, aCount, i, status, lPids[NUMPIPES];
     pid_t pid;

-    pipe(fdPipe);
-
     while(1) {
         bBuffer = readline("Shell> ");
@@ -32,4 +30,7 @@
                 aCount = -1;

+                if (i + 1 < pCount)
+                    pipe(fdPipe2);
+
                 do {
                         aPtr = strsep(&pipeComms[i], " ");
@@ -43,11 +44,12 @@

                         if(pid == 0) {
-                                if(i == 0) {
-                                        close(fdPipe[0]);
+                                if(i + 1 < pCount) {
+                                        close(fdPipe2[0]);

-                                        dup2(fdPipe[1], STDOUT_FILENO);
+                                        dup2(fdPipe2[1], STDOUT_FILENO);

-                                        close(fdPipe[1]);
-                                } else if(i == 1) {
+                                        close(fdPipe2[1]);
+                                }
+                                if(i != 0) {
                                         close(fdPipe[1]);

@@ -70,4 +72,17 @@
                         }
                 }
+
+                if (i != 0) {
+                    close(fdPipe[0]);
+                    close(fdPipe[1]);
+                }
+
+                fdPipe[0] = fdPipe2[0];
+                fdPipe[1] = fdPipe2[1];
+        }
+
+        if (pCount) {
+            close(fdPipe[0]);
+            close(fdPipe[1]);
         }

  • Veo lo que quieres decir, pero me resulta bastante difícil entender ese pseudocódigo y todo lo que está haciendo. Eso de la tubería fds vieja y nueva me está confundiendo. Además, no estoy seguro de lo que quieres decir con el último párrafo.

    – rfgamaral

    27 de mayo de 2009 a las 20:19

  • Ignorando lo de old_fds/new_fds, si cambio su código para ejecutar pipe() dentro del ciclo readline y cierro (fdPipe[]) antes de esperar, funciona en el sentido inmediato de que la tubería que falla en la pregunta ya no falla. El resto es una advertencia, ya que *otros cosas que podrían estar mal a medida que avanzas.

    – efímero

    27 de mayo de 2009 a las 20:39

  • Veo que probablemente no te diste cuenta en el código, pero solo estaba considerando el caso con 2 comandos y una tubería sería suficiente. Aún así, no sabía que necesitaba más de uno para más de 2 comandos, por lo que será útil. Pero por ahora, me concentraré en solo dos, trate de entender eso y avance al siguiente paso. Todavía no entiendo todo lo que dijiste además de eso y que necesito cerrarlos en los padres. Creo que es mejor arreglar esto para dos recomendaciones y luego publicar mi código y si estoy haciendo algo mal o me falta algo, con suerte, lo señalarás.

    – rfgamaral

    27 de mayo de 2009 a las 23:54

  • Esta es la mejor respuesta para aceptar: estoy de acuerdo. Y la generalización a canalizaciones de múltiples etapas será importante eventualmente. Creo que algunos shells, probablemente bash entre ellos, bifurcan un solo proceso para ejecutar toda la tubería, y ese primer hijo finalmente ejecuta el último proceso en la tubería, después de arreglar las tuberías entre las otras etapas. El niño también se convierte en un líder de grupo de procesos y otras cosas para que el control del trabajo funcione con sensatez.

    –Jonathan Leffler

    28 de mayo de 2009 a las 5:08

  • Entonces, la sugerencia es cerrar los extremos de la tubería en el primer proceso después del bucle que hace la bifurcación y antes del bucle que espera. Estos extremos abiertos impiden que ctrl-D (EOF) que grep necesita para terminar. Las menciones efímeras de peligro ocurrirían si tuviera más de dos comandos, es decir, más de una tubería. Si cerró una tubería antes de crear la siguiente, se reutilizarían los mismos fds, lo que causaría problemas.

    – tíoO

    28 de mayo de 2009 a las 19:35

Debería tener una salida de error después de execvp(); fallará en algún momento.

exit(EXIT_FAILURE);

Como señala @uncleo, la lista de argumentos debe tener un puntero nulo para indicar el final:

cmdArgs[aCount] = 0;

No me queda claro si deja que ambos programas se ejecuten libremente; parece que necesita que el primer programa en la tubería finalice antes de iniciar el segundo, lo cual no es una receta para el éxito si el primer programa se bloquea porque la tubería está llena.

  • No agregué ningún tipo de manejo de errores a propósito, no es necesario hacer que el código sea aún más grande para la prueba… Como dije anteriormente, ¿strsep() no maneja eso? El último argumento siempre será NULL porque strsep() lo hace por sí mismo.

    – rfgamaral

    27 de mayo de 2009 a las 17:57

  • Tiene razón sobre correr libre. Usted bifurca un proceso y espera a que termine antes de bifurcar el siguiente. Quizás grep se está estancando con una tubería llena.

    – tíoO

    27 de mayo de 2009 a las 18:04

  • Ninguna de esas sugerencias resolvió el problema… Acabo de probar todo y sucede lo mismo.

    – rfgamaral

    27 de mayo de 2009 a las 18:39

  • @Nazgulled: entiendo que no hay manejo de errores, pero aún así pondría una salida() después de execvp(). Tal vez estés lo suficientemente seguro, eso te asegura. Además, su lista de ‘ls’ es lo suficientemente corta como para no llenar ningún búfer de tubería; afortunadamente, está probando en un entorno pequeño. Veo que strsep() le dará un valor nulo: está usando do { } while ().

    –Jonathan Leffler

    27 de mayo de 2009 a las 19:02

  • Tenga en cuenta que strsep() no está disponible en todas partes, específicamente, no en Solaris 10.

    –Jonathan Leffler

    27 de mayo de 2009 a las 19:05

avatar de usuario
tioO

Jonathan tiene la idea correcta. Confías en el primer proceso para bifurcar todos los demás. Cada uno tiene que ejecutarse hasta completarse antes de que se bifurque el siguiente.

En su lugar, bifurque los procesos en un ciclo como lo está haciendo, pero espérelos fuera del ciclo interno (en la parte inferior del ciclo grande para el indicador de shell).

loop //for prompt
    next prompt
    loop //to fork tasks, store the pids
        if pid == 0 run command
        else store the pid
    end loop
    loop // on pids
        wait
    end loop
end loop

  • ¿No acabas de decir que “yo” tenía razón en el comentario de arriba? No sentí que me estuvieras hablando diciendo que Jonathan tenía razón, sino al revés. Tal vez te malinterprete…

    – rfgamaral

    27 de mayo de 2009 a las 18:34

  • Acabo de actualizar el código en la pregunta para reflejar los cambios sugeridos.

    – rfgamaral

    27 de mayo de 2009 a las 19:24

Creo que sus procesos bifurcados seguirán ejecutándose.

Intente:

  • Cambiándolo a ‘return execvp’
  • Agregue ‘salida (1);’ después de execvp

Un problema potencial es que cmdargs puede tener basura al final. Se supone que debes terminar esa matriz con un puntero nulo antes de pasarla a execvp().

Sin embargo, parece que grep está aceptando STDIN, por lo que podría no estar causando ningún problema (todavía).

  • Creo que strsep() se ocupa de eso, digamos que el comando es “cmd arg1 arg2”, cmdArgs será: cmdArgs[0] = “comando”; cmdArgs[1] = “arg1”; cmdArgs[2] = “arg2”; cmdArgs[3] = NULO;

    – rfgamaral

    27 de mayo de 2009 a las 17:46

avatar de usuario
sentarse

los descriptores de archivo de la canalización se cuentan como referencia y se incrementan con cada bifurcación. para cada bifurcación, debe emitir un cierre en ambos descriptores para reducir el recuento de referencia a cero y permitir que la tubería se cierre. Estoy adivinando.

  • Creo que strsep() se ocupa de eso, digamos que el comando es “cmd arg1 arg2”, cmdArgs será: cmdArgs[0] = “comando”; cmdArgs[1] = “arg1”; cmdArgs[2] = “arg2”; cmdArgs[3] = NULO;

    – rfgamaral

    27 de mayo de 2009 a las 17:46

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad