Herramientas de usuario

Herramientas del sitio


procesos

Procesos

Un proceso se crea con la llamada al sistema fork(2). Esto crea un clon del proceso actual, pero cada proceso resultante sabe si él es el padre o el hijo.

  • Al hijo se le asigna un pid (process id) único.
  • La llamada fork() retorna 2 veces: una vez en el padre y otra en el hijo.
  • En el hijo, fork retorna 0.
  • En el padre, fork retorna el pid del hijo.
  • Para el hijo se crea un nuevo espacio de direcciones de memoria que parte inicialmente con una copia de toda la memoria del padre. De ahí en adelante las modificaciones que haga el hijo en su memoria no se verán reflejadas en la memoria del padre, y viceversa. Son espacios de direcciones independientes.
  • El hijo hereda todos los archivos abiertos por el padre. Los fds (file descriptors) del padre siguen siendo válidos en el hijo (y también en el padre).
  • El tiempo de ejecución del proceso hijo parte de 0.
  • El hijo parte sin alarmas ni señales pendientes.

Esquema típico:

  #include <unistd.h>
  
  pid_t child= fork();
  if (child==0) {
    // aquí se ejecuta el hijo
    ... cuerpo del hijo ...
    exit(...); // termina el hijo entregando un código de retorno
  }
  else {
    // aquí se ejecuta el padre
    // la variable "child" contiene el pid del hijo
    ... código que se ejecuta en paralelo con el cuerpo del hijo ...
    int status;
    waitpid(child, &status, 0);  // espera hasta que el hijo termine
    int rc= WEXITSTATUS(status); // código de retorno del hijo
  }

Formas de terminar un proceso

mecanismo notas
return del main Si el valor retornado de 0 se considera que el fin fue exitoso
exit(value); Esta función no retorna
abort(); El efecto de esta última llamada es indefinido, pero puede generar un core

Para matar a otro proceso se usa:

  #include <sys/types.h>
  #include <signal.h>

  pid_t pid;
  kill(pid, SIGKILL);

Esperar el término de un proceso

Para esperar que un proceso termine se usa la llamada al sistema wait(2):

  #include <sys/types.h>
  #include <sys/wait.h>
  
  int status;
  wait(&status);

entonces el padre espera hasta que algún hijo muera y retorna el pid de ese hijo. A la variable status (int) se le asigna la causa del deceso (ver manual).

Si en el momento de ejecutar wait ya había algún proceso que ya había muerto (“zombie”), la llamada retorna inmediatamente.

Un wait también termina si llega una señal que cause la terminación del proceso o la invocación de una función de manejo de señales.

También existe una función que permite un manejo más específico:

  #include <sys/types.h>
  #include <sys/wait.h>
  
  int status;
  pit_t pid;
  int flags= ...;
  waitpid(pid, &status, flags);

Si pid==-1 se espera a cualquier proceso (como hace wait). Si pid>0 se espera al proceso que posee ese pid.

Los flags incluyen como valor posible WNOHANG que hace que la llamada no se bloquee.

Deberes del padre

En Unix, un padre es completamente independiente de sus hijos, pero debe cumplir con una responsabilidad fundamental: enterrarlos cuando mueren. Para esto, basta con que invoque la función wait en cualquier momento, pero es indispensable que sea informado que su hijo murió para que Linux pueda deshacerse por completo de él. Un hijo muerto pero no enterrado (cuyo padre nunca ha sido informado de su muerte vía wait) es un Zombie: un proceso que ya no existe pero no puede reciclarse por completo.

Siempre deben evitar generar Zombies en el sistema y es su responsabilidad hacer wait de todos sus hijos muertos. Para observar lo que sucede mientras el padre no ha invocado wait compile y ejecute este ejemplo.

Pipes

Un pipe es un canal de comunicación entre dos procesos. Se comporta como una cola (FIFO) en donde lo que se escribe por un extremo se lee por el otro. Fue el primer mecanismo que tuvo Unix para comunicar procesos y todavía se usa cuando desde el shell Ud. invoca por ejemplo ls | more.

Para crear un pipe se invoca:

  #include <unistd.h>
  
  int fds[2];
  pipe(fds);

en donde fds es un arreglo de tamaño 2. Después de ejecutar esta llamada, se obtienen 2 fds:

      fds[0] permite leer
      fds[1] permite escribir

Después de invocar pipe, se duplica el proceso mediante fork. Uno de los procesos cierra el fd de lectura y el otro cierra el de escritura. Este pipe sirve para comunicar unidireccionalmente ambos procesos. El sistema garantiza que un write de tamaño menor o igual a PIPE_BUF es indivisible.

Ejemplo 1: quicksort paralelo

El siguiente programa ordena un arreglo por medio de 2 procesos. Después de llamar a particionar se llama a fork. La parte inferior del arreglo se ordena en el proceso hijo y la parte superior en el padre. El ejemplo completo se encuentra acá.

int leer(int fd, void *buf, int n) {
  if (n==0)
    return 0;
  do {
    int rc= read(fd, buf, n);
    if (rc<=0)
      return 1; /* fracaso: error o fin del archivo/pipe/socket */
    n-= rc; /* descontamos los bytes leídos */
    buf= (char*)buf + rc; /* avanzamos el buffer */
  } while (n>0); /* mientras no leamos todo lo que esperamos */
  return 0; /* exito */
}

void pquicksort(int a[], int i, int j) {
  if (i<j) {
    int h= particionar(a, i, j);
    int fd[2];
    pipe(fd);
    pid_t pid= fork();
    if (pid==0) {
      close(fd[0]);
      quicksort(a, i, h-1);
      write(fd[1], &a[i], (h-i)*sizeof(int));
      exit(0);
    }
    else {
      close(fd[1]);
      quicksort(a, h+1, j);
      leer(fd[0], (char*)&a[i], (h-i)*sizeof(int));
      close(fd[0]);
      waitpid(pid, NULL, 0);
    }
  }
}

Observe que si se reemplazara la llamada de la función leer por read, podría funcionar en algunas plataformas, pero sería incorrecto. La llamada al sistema read no siempre entregará la cantidad de bytes pedidos. Puede entregar menos bytes cuando se piden más bytes que la capacidad del buffer que utiliza un pipe. Por otra parte, se garantiza que write siempre escribirá la cantidad de bytes pedidos. Si se escribe menos es porque ocurrió un error. Yo hice el experimento en Linux y read siempre leyó la cantidad de bytes pedidos.

Cuando user fork nunca olvide:

  1. Invocar exit para terminar el hijo
  2. Invocar waitpid en el padre para enterrar al hijo
  3. Padre e hijo usan espacios de direcciones independientes así es que el padre no verá los cambios que el hijo haya hecho en la memoria. Use un pipe para que el hijo entregue sus resultados al padre.

Ejercicio

Resuelva la pregunta 1 partes a y b del control 3 de 2014/2. Pruebe su solución con el archivo mult.zip.

Cambiar el archivo ejecutable

Una vez que el hijo se inicia, frecuentemente lo que hace es invocar a algún comando, lo que hace llamando a una función de la familia exec. Esto no crea un proceso nuevo, sino que sustituye el binario del proceso. Un exec que funciona no retorna, sino que inicia un nuevo archivo ejecutable, sin cambiar la identificación del proceso (pid).

Supongamos que se tienen las siguientes declaraciones previas:

  #include <unistd.h>
  
  char *filename, *path, *s1, *s2, ...;
  char *argv[], *environ[];

La siguiente tabla muestra las distintas funciones para cambiar el archivo ejecutable.

Llamada notas
execl(path, s1, s2, …, NULL); ejecuta el programa indicado por path con parámetros s1, s2, …
execle(path, s1, s2, .., NULL, environ ); igual al anterior + se traspasa ambiente indicado en environ
execlp(filename, s1, s2, …, NULL); si filename no empieza por “/”, se le busca en el PATH
execv(path, argv); el último elemento de argv[] debe ser NULL
execve(path, argv, environ); Llamada al sistema (sección 2)
execvp(filename, argv); si filename no empieza por “/”, se le busca en el PATH
execvpe(filename, argv, environ); si filename no empieza por “/”, se le busca en el PATH

El archivo ejecutable debe corresponder a un programa con encabezamiento:

  int main(int argc, char *argv[], char *envp[]);

Pero usualmente no se declara el parámetro envp, porque también existe la función getenv para consultar las variables de ambiente (ver más abajo).

Ejemplo 2: invocar ls

  #include <stdio.h>
  #include <unistd.h>
  #include <stdlib.h>
  #include <sys/types.h>
  #include <sys/wait.h>

  /*
   * Ejemplo trivial de un fork/exec/wait
   */

  int main() {
    pid_t pid;
    int status;

    printf("voy al ls\n");
    printf("---------------\n");
    fflush(stdout);  /* Para asegurarme que lo anterior ya salga por la salida
                      * prueben comentando el fflush y redirijan la salida a un archivo.
                      */

    pid = fork();
    if (pid < 0) {
        fprintf(stderr, "falla fork!\n");
        exit(1);
    }

    if (pid == 0) { /* soy el hijo */
        execl("/bin/ls", "ls", "-l", 0);
        fprintf(stderr, "nunca debio ocurrir!\n");
        exit(1);
    }
    else {
      waitpid(pid, &status, 0); /* espera la muerte del proceso pid */
      printf("---------------\n");
      return 0;
    }
  }

Ejemplo 3: more

El siguiente programa filtra la salida por medio de more:

  /* Esto se requiere para poder usar fdopen sin warnings */
  #define _POSIX_C_SOURCE 1

  #include <stdio.h>
  #include <sys/types.h>
  #include <stdlib.h>
  #include <unistd.h>
  #include <sys/types.h>
  #include <sys/wait.h>
  #include <stdarg.h>

  void error(char *fmt, ...) {
    va_list args;

    va_start(args, fmt);
    fprintf(stderr, "Error: ");
    vfprintf(stderr, fmt, args);
    fprintf(stderr, "\n");
    va_end(args);

    exit(1);
  }

  int main() {
    int fd[2];
    pid_t pid;
    FILE *out;
    int status;
    int i;
            
    if (pipe(fd)!=0)
      error("Can't create pipe");
    if ((pid= fork()) == -1)
      error("Can't fork");
            
    if (pid==0) { /* es el hijo */
      /* el siguiente truco pone al extremo del lectura del pipe
         como el stdin del nuevo proceso */
      close(STDIN_FILENO);
      dup(fd[0]); /* aprovechamos que se usa el mismo fd recien liberado */
      close(fd[0]);
      close(fd[1]);
            
      execlp("more", "more", NULL);
      fprintf(stderr, "nunca debio ocurrir!\n");
      exit(1);
    }
    else {  
      /* este es el padre */
      close(fd[0]); /* no va a leer del pipe */
            
      out= fdopen(fd[1], "w"); /* simula que el extremo de lectura del pipe
                                  fue abierto con fopen */
      /* ahora generamos el output */
            
      for(i= 1; i<=200; ++i)
        fprintf(out, "Linea numero %d\n", i);
            
      fclose(out);
            
      wait(&status); /* esperamos que el hijo muera */
      return 0;
    }
  }

Ejercicio: ls | more

  • Modifique la función pquicksort de modo que reciba el parámetro adicional p, correspondiente al número de procesos que se usarán para el ordenamiento.
  • Escriba la función ls_pipe_more() que lanza el comando ls de manera que su salida estándar alimente la entrada estándar del comando more. Esto es equivalente a invocar desde el shell de comandos ls | more. Nota: La solución de este ejercicio aparece en esta página.

La función system

Una función la biblioteca estándar de C es system que permite invocar comandos como si los invocara el shell de comandos sh. Use man 3 system para obtener información de esta función. Una implementación muy simple de esta función aparece en esta página.

Con la función system es trivial implementar ls_pipe_more. La desventaja es que en ese caso se crea un proceso adicional para sh, el cual no es un proceso barato.

Variables de ambiente

El puntero environ en las funciones execle, execve y execvpe apunta a un arreglo de strings de la forma:

  name=value

terminando con un puntero NULL.

Ejemplo de variables de ambiente:

HOME
PATH
TERM
TZ
LOGNAME

Para buscar un nombre de variable en el environment se usa:

  #include <stdlib.h>
  char *p;
  
  p= getenv(name);

El parámetro name apunta al nombre de la variable y p recibe el puntero a la línea name=value respectiva, o bien NULL si no existe.

Para el resto de las funciones de la familia exec se preservan las mismas variables de ambiente que regían antes de la llamada a exec. En una llamada a fork, el proceso hijo hereda las mismas variables de ambiente del proceso hijo. Por esta razón al invocar un comando desde el shell, el proceso que se crea heredará las variables de ambiente del shell.

Información disponible durante la ejecución

  #include <unistd.h>
  #include <sys/types.h>

  pid_t pid, ppid;
  char *s;
  uid_t uid;
  gid_t guid;
Llamada Descripción
pid=getpid(); retorna el pid propio
ppid=getppid(); retorna el pid del padre
s=getlogin(); retorna puntero al nombre del usuario, o NULL
uid=getuid(); retorna real UID
uid=geteuid(); retorna effective UID
gid=getgid(); real group ID
gid=getegid(); effective group ID
s=ctermid(NULL); puntero al nombre del terminal en un buffer estático
(void)ctermid(termname); copia nombre del terminal a termname[L_ctermid]

Dado un uid, es posible obtener un record de información asociada al usuario, tomada de lo que aparece en el archivo /etc/passwd:

  #include <pwd.h>
  #include <sys/types.h>
        
  struct passwd *pwptr;
        
  pwptr=getpwuid(uid);

La estructura a la cual apunta pwptr es:

  /* The passwd structure.  */
  struct passwd {
    char *pw_name;                /* Username.  */
    char *pw_passwd;              /* Password (encriptada). */
    __uid_t pw_uid;               /* User ID.  */
    __gid_t pw_gid;               /* Group ID.  */
    char *pw_gecos;               /* Real name.  */
    char *pw_dir;                 /* Home directory.  */
    char *pw_shell;               /* Shell program.  */
  };

También existe la función

  pwptr=getpwnam(username);

que busca por nombre y retorna un record al puntero respectivo. En ambos casos se retorna NULL si el usuario no existe.

El shell de comandos

Los shells de comandos (sh o csh) usan intensivamente fork/exec para lanzar los comandos ingresados por el usuario. También invocan pipe cuando se ingresan pipes de comandos. Casi todos los comandos que se ejecutan desde un shell son archivos ejecutables, pero existen excepciones. La más notable es el comando cd que debe ser reconocido como un comando interno que cambia la variable de ambiente PWD. Si fuese un archivo ejecutable que se lanza con fork/exec, cambiar la variable PWD cambiaría la variable del proceso que se creó para cd, y no el del shell, lo que lo haría completamente inútil.

Otros ejemplos de comandos internos del shell son setenv (en csh) y set.

Hora y Fecha

  #include <time.h>
  
  time_t t; /* usualmente unsigned long */
  
  t=time(NULL); /* Almacena en t el número de segundos desde 1/1/1970 */
  
  (void)time(&t); /* forma alternativa */

La función anterior basta para calcular todo lo que se necesita, pero las siguientes funciones son útiles para simplificar esta tarea:

  struct tm {
    int tm_sec;                   /* Seconds.     [0-60] (1 leap second) */
    int tm_min;                   /* Minutes.     [0-59] */
    int tm_hour;                  /* Hours.       [0-23] */
    int tm_mday;                  /* Day.         [1-31] */
    int tm_mon;                   /* Month.       [0-11] */
    int tm_year;                  /* Year - 1900.  */
    int tm_wday;                  /* Day of week. [0-6] */
    int tm_yday;                  /* Days in year.[0-365] */
    int tm_isdst;                 /* DST.         [-1/0/1]*/
         
    long int tm_gmtoff;           /* Seconds east of UTC.  */
    const char *tm_zone;          /* Timezone abbreviation.  */
  };
  
  struct tm *t;
  time_t now;
  
  now=time(NULL);
  t=localtime(&now);
  t=gmtime(&now);
  
  seconds=mktime(&t); /* conversion inversa */
  /* Los campos tm_wday y tm_yday se ignoran y se actualizan en t */
  
  s=ctime(&now); /* Genera string de la forma "Wed Jun 30 21:49:08 1993\n" */
  
  s=asctime(&t); /* Genera string de la forma "Wed Jun 30 21:49:08 1993\n" */

La función strftime es una especie de sprintf para formatear tiempos (ver manual).

Para medir intervalos de tiempo de procesador se usa:

  #include <time.h>
        
  ticks=clock(); /* Tiempo medido desde algún instante de referencia */
                 /* Para transformar a segundos dividir por CLOCKS_PER_SEC */

Ejemplo 4: mostrar la hora

hora.c

  #include <stdio.h>
  #include <time.h>
        
  int main() {
    time_t now;
    struct tm *t;
        
    now=time(NULL);
    t=localtime(&now);
        
    printf("Son las %02d:%02d:%02d del %d/%d/%d\n",
          t->tm_hour, t->tm_min, t->tm_sec,
          t->tm_mon+1, t->tm_mday, t->tm_year+1900);
        
    printf("Dia de la semana: %d\n", t->tm_wday+1);
    printf("Dia del anno: %d\n", t->tm_yday+1);
    printf(t->tm_isdst>0?"Horario de Verano\n":"Horario Normal\n");
    return 0;
  }
procesos.txt · Última modificación: 2018/07/12 11:00 por lmateu