Terminar un proceso - Procesos e Hilos
Volver a Procesos e Hilos Bloque 5 - Esperar y terminar - Tema 20 de 43

Tema 20

Terminar un proceso

Todo proceso termina. Puede terminar normalmente (con return o exit) o anormalmente (por una senal). En este tema veras las distintas formás de terminar un proceso, la diferencia entre exit y _exit, Como registrar funciónes de limpieza con atexit, y que significa el código de salida.

1. Cómo termina un proceso

Un proceso termina cuándo deja de ejecutarse y el núcleo libera sus recursos. La terminación puede ser normal (el proceso decide terminar) o anormal (el proceso es terminado por una senal o por un error fatal). En ambos casos, el núcleo guarda un estado de terminación Que el padre puede consultar con wait().

La terminación normal ocurre cuándo:

La terminación anormal ocurre cuándo:

En todos los casos, el núcleo marca el proceso como terminado, libera sus recursos (Memoria, archivos, descriptores), y notifica al padre. El PCB se conserva como zombi hasta que el padre lo recoge.

La terminación es inevitable

Todo proceso termina, ya sea normalmente o por una senal. Un programa que no termina nunca (bucle infinito) es un error, no una carácteristica. El sistema operativo siempre puede terminarlo con kill -9.

2. return en main

La forma más simple de terminar un proceso es retornar de main. Cuando main retorna, la biblioteca de C llama a exit() con el valor de retorno. Es equivalente a llamar a exit(valor).

return_main.c

#include 

int main(void)
{
    printf("Terminando con return 42\n");
    return 42;  /* equivalente a exit(42) */
}

Si main no tiene return explicito, el valor de retorno es 0 (en C99 y posteriores). En versiones anteriores de C, el valor era indeterminado.

El valor de retorno de main se convierte en el código de salida del proceso. El padre puede obtenerlo con WEXITSTATUS(status) Después de wait().

3. exit()

exit() termina el proceso inmediatamente, Sin retornar de main. Llama a las funciónes registradas con atexit, vacia los buferes de stdio, y luego llama a _exit() para terminar realmente.

C - firma de exit

#include 

void exit(int status);

El parámetro status es el código de salida. Por convencion, 0 Significa exito y cualquier otro valor significa error. Los valores tipicos son 0 (exito), 1 (error generico), 2 (uso incorrecto), 126 (comando encontrado pero no ejecutable), 127 (comando no encontrado).

exit() se puede llamar desde cualquier función, no sólo desde main. Esto es útil para terminar el proceso desde una función de error:

exit_error.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 

void error_fatal(const char *msg)
{
    perror(msg);
    exit(EXIT_FAILURE);  /* termina el proceso */
}

int main(void)
{
    int fd = open("/archivo/inexistente", O_RDONLY);
    if (fd < 0)
        error_fatal("open");  /* no retorna */

    /* Este código no se ejecuta sí open fallo */
    close(fd);
    return 0;
}

exit() es la forma más comun de terminar un proceso desde una función que no es main. Es segura y limpia: vacia los buferes y ejecuta las funciónes atexit.

4. _exit() y _Exit()

_exit() (POSIX) y _Exit() (C99) terminan el proceso inmediatamente, Sin llamar a las funciónes atexit ni vaciar los buferes de stdio. Solo llaman a la syscall exit directamente.

C - firmás de _exit y _Exit

#include    /* _exit */
#include    /* _Exit */

void _exit(int status);
void _Exit(int status);

_exit y _Exit son equivalentes en la práctica. _exit es POSIX, _Exit es C99. Ambos terminan el proceso sin limpieza.

_exit se usa en situaciones dónde no se quiere ejecutar la limpieza normal:

5. La diferencia entre exit y _exit

Accionexit()_exit() / _Exit()
Llamar a funciónes atexitSiNo
Vaciar buferes de stdioSiNo
Cerrar descriptores de archivoSi (el núcleo los cierra al terminar)Si (el núcleo los cierra al terminar)
Liberar memoriaSi (el núcleo la libera)Si (el núcleo la libera)
Notificar al padreSiSi
Syscall exitSi (al final)Si (inmediatamente)

La diferencia clave es que exit() ejecuta la limpieza de la biblioteca de C (atexit, buferes), mientras que _exit() va directamente al núcleo. En la mayoria de los casos, se usa exit(). _exit() se usa sólo en casos especiales (hijo de fork, fallo de exec).

Usar _exit en el hijo después de fork

Despues de fork, el hijo debe usar _exit() en lugar de exit(). Si usa exit(), ejecutara las funciónes atexit del padre (Que pueden no tener sentido en el hijo) y vaciara los buferes de stdio (Que ya se vaciaron en el padre, causando duplicacion).

6. atexit: Funciónes de limpieza

atexit() registra una función que se ejecutara cuándo el proceso termine con exit() (o return en main). Es la forma de registrar limpieza (cerrar archivos, liberar memoria, guardar estado) Que se ejecuta automaticamente al terminar.

C - firma de atexit

#include 

int atexit(void (*func)(void));

La función registrada no recibe argumentos ni devuelve valor. Se puede registrar multiples funciónes; se ejecutan en orden inverso al de registro (LIFO: la ultima registrada se ejecuta primero).

atexit_demo.c

#include 
#include 

void limpiar1(void) { printf("Limpieza 1\n"); }
void limpiar2(void) { printf("Limpieza 2\n"); }
void limpiar3(void) { printf("Limpieza 3\n"); }

int main(void)
{
    atexit(limpiar1);
    atexit(limpiar2);
    atexit(limpiar3);

    printf("main: terminando\n");
    return 0;  /* ejecuta atexit en orden inverso */
}
terminal - ejecutar atexit_demo

$ gcc -std=c17 -Wall -Wextra -o atexit_demo atexit_demo.c
$ ./atexit_demo
main: terminando
Limpieza 3
Limpieza 2
Limpieza 1

Las funciónes se ejecutan en orden inverso (3, 2, 1). Esto es importante porque las dependencias se registran en orden y se liberan en orden inverso.

atexit puede fallar sí se registran demasiadas funciónes (el limite es 32 en POSIX, pero puede ser mayor en la implementación). Si falla, devuelve un valor distinto de cero.

atexit no se ejecuta con _exit

Las funciónes registradas con atexit sólo se ejecutan cuándo el proceso termina con exit() o return en main. Si se llama a _exit() o _Exit(), no se ejecutan. Esto es importante en el hijo después de fork: usar _exit() evita ejecutar las funciónes atexit del padre.

7. on_exit: la variante de glibc

glibc ofrece on_exit(), una variante de atexit que permite pasar un argumento a la función de limpieza y conocer el código de salida:

C - firma de on_exit

#define _GNU_SOURCE
#include 

int on_exit(void (*func)(int status, void *arg), void *arg);

La función registrada recibe el código de salida y el argumento pasado a on_exit. Esto es útil para funciónes de limpieza que necesitan saber como terminó el proceso.

on_exit_demo.c

#define _GNU_SOURCE
#include 
#include 

void limpiar(int status, void *arg)
{
    printf("Limpieza: status=%d, arg=%s\n", status, (char *) arg);
}

int main(void)
{
    on_exit(limpiar, "mi_argumento");
    printf("main: terminando con 42\n");
    return 42;
}
terminal - ejecutar on_exit_demo

$ gcc -std=gnu17 -Wall -Wextra -o on_exit_demo on_exit_demo.c
$ ./on_exit_demo
main: terminando con 42
Limpieza: status=42, arg=mi_argumento

on_exit es una extension GNU, no POSIX. Para portabilidad, usar atexit.

8. El código de salida

El Código de salida (exit status) es un número entero (0-255) Que el proceso devuelve al terminar. Por convencion:

CodigoSignificado
0Exito.
1Error generico.
2Uso incorrecto (argumentos inválidos).
126Comando encontrado pero no ejecutable.
127Comando no encontrado.
128 + NTerminado por la senal N (por ejemplo, 130 = SIGINT, 137 = SIGKILL).

El código de salida se pasa a exit() o return. El núcleo lo guarda en el PCB y el padre lo obtiene con WEXITSTATUS(status) Después de wait().

El código de salida es sólo 8 bits (0-255). Si se pasa un valor mayor, se trunca a los 8 bits bajos. Por ejemplo, exit(256) produce código de salida 0.

En la shell, el código de salida del último comando se guarda en $?:

terminal - Código de salida en la shell

$ ls /tmp; echo $?
archivo1  archivo2
0
$ ls /noexiste; echo $?
ls: cannot access '/noexiste': No such file or directory
2
$ sleep 100 & kill %1; wait %1; echo $?
143

143 = 128 + 15 (SIGTERM). El sleep fue terminado por SIGTERM.

9. WIFEXITED y WEXITSTATUS

Cuando el padre recoge el estado de un hijo con wait(), obtiene un entero (status) Que codifica como terminó el hijo. Las macros de POSIX permiten interpretarlo:

MacroSignificado
WIFEXITED(status)Verdadero sí el hijo terminó normalmente (con exit o return).
WEXITSTATUS(status)El código de salida (0-255). Solo válido sí WIFEXITED es verdadero.
WIFSIGNALED(status)Verdadero sí el hijo terminó por una senal.
WTERMSIG(status)El número de la senal que terminó al hijo. Solo válido sí WIFSIGNALED es verdadero.
WIFSTOPPED(status)Verdadero sí el hijo esta detenido (Sólo con WUNTRACED).
WSTOPSIG(status)El número de la senal que detuvo al hijo.
WIFCONTINUED(status)Verdadero sí el hijo se reanudo (Sólo con WCONTINUED).

El patron de interpretacion es:

interpretar_status.c

int status;
waitpid(pid, &status, 0);

if (WIFEXITED(status)) {
    printf("Termino normalmente, Código %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
    printf("Termino por senal %d (%s)\n", WTERMSIG(status), strsignal(WTERMSIG(status)));
} else if (WIFSTOPPED(status)) {
    printf("Detenido por senal %d\n", WSTOPSIG(status));
}

10. Terminar por una senal

Un proceso puede terminar por una senal de dos formas:

Cuando un proceso termina por una senal, no hay código de salida (WEXITSTATUS no es válido). En su lugar, WTERMSIG da el número de la senal. Y el código de salida en la shell es 128 + Número de senal.

terminal - terminar por senal

$ sleep 100 &
[1] 1234
$ kill -TERM 1234
$ wait 1234; echo $?
143    # 128 + 15 (SIGTERM)
$ kill -KILL 1235
$ wait 1235; echo $?
137    # 128 + 9 (SIGKILL)

143 = 128 + 15 (SIGTERM). 137 = 128 + 9 (SIGKILL). La convencion de 128 + senal es la que usa la shell para reportar la terminación por senal.

11. El estado de terminación completo

El estado de terminación (status) Que devuelve wait() Codifica toda la información sobre como terminó el hijo. Se puede decodificar con las macros de POSIX:

interpretar_completo.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 
#include 
#include 

static void interpretar(int status)
{
    if (WIFEXITED(status)) {
        printf("Termino normalmente, Código %d\n", WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        int sig = WTERMSIG(status);
        printf("Termino por senal %d (%s)\n", Sig, strsignal(Sig));
        #ifdef WCOREDUMP
        if (WCOREDUMP(status))
            printf("  (genero core dump)\n");
        #endif
    } else if (WIFSTOPPED(status)) {
        printf("Detenido por senal %d (%s)\n", WSTOPSIG(status), strsignal(WSTOPSIG(status)));
    } else if (WIFCONTINUED(status)) {
        printf("Continuo\n");
    } else {
        printf("Estado desconocido\n");
    }
}

int main(void)
{
    /* Probar diferentes terminaciones */

    /* 1. Terminacion normal */
    pid_t pid = fork();
    if (pid == 0) _exit(42);
    int status; waitpid(pid, &status, 0);
    printf("Hijo 1: "); interpretar(status);

    /* 2. Terminacion por senal */
    pid = fork();
    if (pid == 0) { kill(getpid(), SIGTERM); _exit(0); }
    waitpid(pid, &status, 0);
    printf("Hijo 2: "); interpretar(status);

    /* 3. Abort (genera core) */
    pid = fork();
    if (pid == 0) abort();
    waitpid(pid, &status, 0);
    printf("Hijo 3: "); interpretar(status);

    return 0;
}
terminal - ejecutar interpretar_completo

$ gcc -std=c17 -Wall -Wextra -o interpretar_completo interpretar_completo.c
$ ./interpretar_completo
Hijo 1: Termino normalmente, Código 42
Hijo 2: Termino por senal 15 (Terminated)
Hijo 3: Termino por senal 6 (Aborted)

12. Un programa que demuestra la terminación

El siguiente programa demuestra las diferentes formás de terminar y como se interpretan:

Terminacion.c

#define _POSIX_C_SOURCE 200809L
#include 
#include 
#include 
#include 

static void mostrar(const char *desc, int status)
{
    printf("%-30s: ", desc);
    if (WIFEXITED(status))
        printf("exit(%d)\n", WEXITSTATUS(status));
    else if (WIFSIGNALED(status))
        printf("senal %d\n", WTERMSIG(status));
    else
        printf("otro\n");
}

static void crear_hijo(void (*func)(void), const char *desc)
{
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return; }
    if (pid == 0) { func(); _exit(0); }
    int status;
    waitpid(pid, &status, 0);
    mostrar(desc, status);
}

static void normal(void) { exit(42); }
static void abortar(void) { abort(); }
static void segfault(void) { int *p = NULL; *p = 1; }
static void division_cero(void) { int x = 1 / 0; (void) x; }

int main(void)
{
    crear_hijo(normal, "exit(42)");
    crear_hijo(abortar, "abort()");
    crear_hijo(segfault, "segfault");
    crear_hijo(Division_cero, "Division por cero");
    return 0;
}
terminal - ejecutar terminacion

$ gcc -std=c17 -Wall -Wextra -o terminacion terminacion.c
$ ./Terminacion
exit(42)                      : exit(42)
abort()                       : senal 6
segfault                      : senal 11
Division por cero             : senal 8

El programa demuestra las diferentes formás de terminar: normal (exit), por senal (abort genera SIGABRT, segfault genera SIGSEGV, division por cero genera SIGFPE). El padre interpreta cada estado con las macros de POSIX.

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. Qué diferencia hay entre exit y _exit?

exit ejecuta las funciónes atexit y vacia los buferes de stdio. _exit termina inmediatamente sin limpieza.

2. Por que usar _exit en el hijo después de fork?

Para evitar ejecutar las funciónes atexit del padre y vaciar los buferes de stdio dos veces (causando duplicacion).

3. Qué es atexit?

Una función que registra otra función para que se ejecute cuándo el proceso termine con exit() o return en main. Las funciónes se ejecutan en orden inverso.

4. Qué es el código de salida?

Un número de 0 a 255 Que el proceso devuelve al terminar. 0 Significa exito, otros valores significan error.

5. Qué macros interpretan el estado de terminación?

WIFEXITED/WEXITSTATUS (terminación normal), WIFSIGNALED/WTERMSIG (terminación por senal), WIFSTOPPED/WSTOPSIG (detencion).

6. Qué código de salida reporta la shell cuándo un proceso termina por senal?

128 + Número de senal. Por ejemplo, 130 para SIGINT (128+2), 137 para SIGKILL (128+9).

7. Qué es el código 127?

El código estandar de unix para "comando no encontrado". Se usa cuándo exec falla.

8. Qué es el código 126?

El código estandar de unix para "comando encontrado pero no ejecutable".

9. atexit se ejecuta con _exit?

No. Las funciónes registradas con atexit sólo se ejecutan con exit() o return en main. Con _exit() o _Exit() no se ejecutan.

10. Qué es on_exit?

Una variante de atexit de glibc que permite pasar un argumento a la función de limpieza y conocer el código de salida.

Para prácticar

  1. Compila y ejecuta atexit_demo.c. Observa el orden de ejecución (inverso).
  2. Compila y ejecuta terminacion.c. Observa los diferentes códigos de salida.
  3. Ejecuta un comando que termina por senal y verifica el código de salida en la shell ($?).
  4. Escribe un programa que use atexit para cerrar un archivo al terminar.
  5. Modifica terminacion.c para agregar un caso de terminación normal con código 0.
  6. Usa strace para ver las syscalls que se generan al llamar a exit().