← Programación en C para Sistemas Operativos Bloque 9 · Errores, archivos y depuración · Tema 37 de 42

Tema 37

Manejo de errores con errno

Hasta aquí, casi todos los ejemplos asumían que las cosas salían bien. En un programa real, abrir un archivo falla porque no existe o no hay permiso, malloc falla cuando falta memoria, escribir falla cuando el disco se llena, y un programa que ignora esos casos no «sigue adelante»: sigue con datos equivocados, o cae más tarde en un lugar que no explica nada. C no tiene excepciones. El lenguaje deja el manejo de errores en manos del programador, y la biblioteca y el sistema ofrecen una convención bastante regular para avisar que algo salió mal: un valor de retorno especial y una variable, errno, que dice qué pasó. Este tema enseña esa convención con sus cuatro variantes, las trampas de errno (que solo vale en un instante concreto), cómo convertir un código en un mensaje útil con perror y strerror, y cómo terminar el programa con un código de salida que el shell, un Makefile o un script puedan interpretar. Cierra con un programa que copia archivos y comprueba cada paso, con todas las salidas verificadas.

1. Cómo avisa una llamada que falló

No hay una única forma. Según de dónde venga la función, el aviso de error toma una de cuatro formas. Conocerlas todas es lo primero, porque comprobar mal es peor que no comprobar: se interpreta un valor correcto como error, o al revés.

ConvenciónEjemplosCómo se comprueba
1. Devuelve -1 y fija errnoLlamadas al sistema: open, read, write, close, unlink…if (r == -1) y se lee errno.
2. Devuelve NULL y fija errnofopen, malloc, opendir, fdopen…if (p == NULL) y se lee errno.
3. Devuelve el código de error directamente (0 es éxito; errno no se toca)La familia pthread_*, posix_memalign, getaddrinfo (con su propio catálogo)int r = f(…); if (r != 0); r es el código.
4. Valor especial sin ser un error por sí solofgets y fread (fin de archivo o error), strtol (0 o LONG_MAX pueden ser válidos)Pregunta aparte: feof, ferror, errno puesto a 0 antes de llamar.

Un programa que prueba una de cada clase deja ver las diferencias:

conv.c
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    int fd = open("/no/existe", O_RDONLY);
    printf("open       -> %d, errno=%d (%s)\n", fd, errno, strerror(errno));

    errno = 0;
    FILE *f = fopen("/no/existe", "r");
    printf("fopen      -> %s, errno=%d (%s)\n", f ? "FILE*" : "NULL", errno, strerror(errno));

    void *p;
    errno = 0;
    int r = posix_memalign(&p, 3, 100);
    printf("memalign   -> %d (%s), errno=%d\n", r, strerror(r), errno);

    char *fin;
    errno = 0;
    long v = strtol("abc", &fin, 10);
    printf("strtol abc -> %ld, errno=%d, sobra \"%s\"\n", v, errno, fin);
    errno = 0;
    v = strtol("99999999999999999999", &fin, 10);
    printf("strtol big -> %ld, errno=%d (%s)\n", v, errno, strerror(errno));

    char buf[8];
    f = fopen("/dev/null", "r");
    char *s = fgets(buf, sizeof buf, f);
    printf("fgets /dev/null -> %s, feof=%d ferror=%d\n", s ? "texto" : "NULL", feof(f) != 0, ferror(f) != 0);
    fclose(f);
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -o conv conv.c && ./conv
open       -> -1, errno=2 (No such file or directory)
fopen      -> NULL, errno=2 (No such file or directory)
memalign   -> 22 (Invalid argument), errno=0
strtol abc -> 0, errno=0, sobra "abc"
strtol big -> 9223372036854775807, errno=34 (Numerical result out of range)
fgets /dev/null -> NULL, feof=1 ferror=0

Cada línea ilustra una cosa:

De ahí sale la primera regla práctica: antes de usar una función desconocida, leer en el manual (man 3 función, sección «RETURN VALUE») cómo avisa del error. No se puede deducir.

2. errno: qué es y cuándo vale

errno es un entero, declarado en <errno.h>, donde las funciones de la clase 1 y 2 dejan el código del último error. Cada código tiene un nombre simbólico (ENOENT, EACCES, EINVAL…) y se comparan los nombres, no los números, que pueden variar entre sistemas. En el tema 14 vimos que errno no es una variable sino una macro: en Linux se expande a (*__errno_location ()), una función que devuelve la dirección de la variable real. Hay una razón: cada hilo tiene su propio errno. Si fuera una sola variable global, dos hilos que fallan a la vez se pisarían. Se comprueba comparando la dirección de errno en el hilo principal y en otro hilo:

hilo2.c
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <pthread.h>
#include <stdio.h>

static void *dir_hilo;

static void *trabajo(void *arg)
{
    (void)arg;
    dir_hilo = &errno;
    return NULL;
}

int main(void)
{
    pthread_t t;
    pthread_create(&t, NULL, trabajo, NULL);
    pthread_join(t, NULL);
    printf("direccion de errno en main y en el hilo: %s\n",
           (void *)&errno == dir_hilo ? "la misma" : "distintas");
    return 0;
}

$ gcc -std=c17 -Wall -Wextra -pthread -o hilo2 hilo2.c && ./hilo2
direccion de errno en main y en el hilo: distintas

(Los hilos se estudian a fondo en el curso Procesos e Hilos; aquí solo importa que errno está a salvo de ellos.) Las reglas de uso son pocas, pero todas importan:

Regla 1: solo vale justo después de un fallo

Una función que tiene éxito puede cambiar errno, aunque no hubo error, y ninguna función lo pone en cero. El estándar solo garantiza que errno tiene un valor significativo inmediatamente después de una llamada que informó de un fallo. Esto se ve, por ejemplo, con pthread_create, que tuvo éxito:

exito.c (extracto) y terminal
errno = 7;
pthread_create(&t, NULL, nada, NULL);
printf("tras pthread_create (exitoso): errno=%d\n", errno);
errno = 7;
pthread_join(t, NULL);
printf("tras pthread_join (exitoso):   errno=%d\n", errno);

$ gcc -std=c17 -Wall -Wextra -pthread -o exito exito.c && ./exito
tras pthread_create (exitoso): errno=22
tras pthread_join (exitoso):   errno=7
tras fopen (exitoso):          errno=7
tras fread (exitoso):          errno=7

La llamada a pthread_create tuvo éxito y aun así dejó errno=22 (EINVAL, por alguna operación interna que falló y se manejó). Nada de eso es un error: simplemente errno no dice nada si la función no falló. El código correcto comprueba primero el valor de retorno y solo entonces mira errno.

Regla 2: cualquier llamada posterior puede pisarlo

El caso típico: tras detectar el error, el código llama a otras funciones (cerrar un archivo, liberar, escribir un registro) antes de mostrar el mensaje, y alguna de ellas falla y cambia errno:

pisa.c y terminal · la limpieza llama a close(-1), que falla con EBADF
static void limpiar(void)
{
    close(-1);
}

int main(int argc, char **argv)
{
    (void)argv;
    FILE *f = fopen("/no/existe", "r");
    if (!f) {
        int guardado = errno;
        limpiar();
        if (argc > 1)
            errno = guardado;
        fprintf(stderr, "no se pudo abrir: %s\n", strerror(errno));
    }
    return 0;
}

$ ./pisa
no se pudo abrir: Bad file descriptor
$ ./pisa guardar
no se pudo abrir: No such file or directory

Sin guardarlo, el mensaje dice Bad file descriptor, el error de la limpieza, y no el del fopen que originó el problema. La defensa es copiar errno a una variable local en cuanto se detecta el fallo y usar la copia (o restaurarlo si hay que seguir llamando a perror). Esta regla es la más vulnerada en código real, sobre todo en rutas de error con varias funciones de limpieza.

Regla 3: ponerlo en cero antes de las funciones ambiguas

Para las de la clase 4, como strtol o strtod (donde el valor de retorno no distingue error de resultado legítimo), la forma segura es errno = 0; inmediatamente antes de la llamada y comprobarlo después, como se hizo en el tema 22. Nunca hay que asignar errno = 0 «por si acaso» al comienzo del programa y comprobarlo al final.

Regla 4: no se asigna, salvo en los casos anteriores

Salvo para ponerlo a cero o para restaurar un valor guardado, no se escribe en errno. Tampoco se debe declarar a mano (extern int errno; era una práctica antigua que hoy rompe la versión por hilo): siempre con #include <errno.h>.

3. De un número a un mensaje

Un número como 2 no le dice nada al usuario. La biblioteca ofrece varias formas de convertirlo en texto:

FunciónQué hace
char *strerror(int e)Devuelve el texto del código e (de <string.h>). Para un número desconocido: Unknown error N.
void perror(const char *prefijo)Escribe en stderr: prefijo: texto de errno. Con NULL o cadena vacía, solo el texto.
int strerror_r(int e, char *buf, size_t n)Versión que escribe el texto en un búfer propio (la recomendable en programas con hilos).
%m en printfExtensión de glibc: se reemplaza por strerror(errno).
strerrorname_np(e)Extensión de glibc (2.32+): el nombre simbólico, "ENOENT".
msg.c (usa _GNU_SOURCE para strerrorname_np) y terminal
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    int lista[] = {EPERM, ENOENT, EACCES, EEXIST, EINVAL, ENOSPC, 999};

    for (size_t i = 0; i < sizeof lista / sizeof lista[0]; i++) {
        const char *n = strerrorname_np(lista[i]);
        printf("%3d %-8s %s\n", lista[i], n ? n : "(?)", strerror(lista[i]));
    }
    return 0;
}

$ gcc -std=c17 -Wall -Wextra -o msg msg.c && ./msg
  1 EPERM    Operation not permitted
  2 ENOENT   No such file or directory
 13 EACCES   Permission denied
 17 EEXIST   File exists
 22 EINVAL   Invalid argument
 28 ENOSPC   No space left on device
999 (?)      Unknown error 999

Y las dos formas de mostrar el error del momento. En ambos casos el mensaje sale en inglés (el catálogo de la biblioteca) salvo que el programa use setlocale y haya traducciones instaladas:

msg2.c (extracto) y terminal · perror y %m con errno = ENOENT
errno = ENOENT;
perror("abrir config");
errno = ENOENT;
perror(NULL);
errno = ENOENT;
fprintf(stderr, "con %%m: %m\n");

$ ./msg2
abrir config: No such file or directory
No such file or directory
con %m: No such file or directory

Las dos primeras líneas son perror("abrir config") y perror(NULL); la tercera, fprintf(stderr, "con %%m: %m\n"). perror siempre escribe en stderr, que es donde van los mensajes de error: así no se mezclan con la salida normal del programa en una redirección o una tubería.

strerror tiene una pega: devuelve un puntero a un búfer que la biblioteca puede reutilizar, y el estándar no exige que sea segura entre hilos. strerror_r escribe en un búfer del llamador. Con las opciones de este curso (_POSIX_C_SOURCE=200809L) es la variante POSIX, que devuelve un entero (0 o un código), y si el búfer es pequeño, trunca el texto y devuelve ERANGE:

terminal · strerror_r (variante POSIX)
$ ./sr
strerror_r -> 0, "No such file or directory"
buffer chico -> 34 (ERANGE), "No such"

(Con _GNU_SOURCE la misma función cambia de firma: devuelve char *. Es una de las incompatibilidades más conocidas de glibc; si el código debe compilar en ambos modos, se fija con la macro de características que se use y se comprueba la firma.) Para un mensaje de un solo uso en un programa de un solo hilo, strerror y perror bastan.

Los códigos más comunes

NombreValorSignificado habitual
EPERM1Operación no permitida (no se tiene el privilegio).
ENOENT2No existe el archivo o directorio.
EINTR4La llamada fue interrumpida por una señal (sección 4).
EIO5Error de entrada/salida (fallo del dispositivo).
EBADF9Descriptor de archivo inválido.
EAGAIN11No hay datos aún o el recurso está ocupado; en Linux EWOULDBLOCK vale lo mismo (11).
ENOMEM12Sin memoria.
EACCES13Permiso denegado.
EEXIST17El archivo ya existe.
EISDIR / ENOTDIR21 / 20Es un directorio / no es un directorio.
EINVAL22Argumento inválido.
ENOSPC28No queda espacio en el dispositivo.
ERANGE34Resultado fuera de rango (o búfer demasiado chico).

Los valores de la tabla son los de Linux en x86-64; el estándar de C solo define tres (EDOM, EILSEQ y ERANGE), y POSIX agrega el resto de los nombres. Por eso se escribe siempre el nombre en el código.

4. Errores que se reintentan: EINTR

No todo fallo es un fracaso definitivo. Una llamada al sistema que se queda esperando (leer de una tubería o del teclado, por ejemplo) puede ser interrumpida por una señal: si llega una señal para la que el programa instaló un manejador, la llamada vuelve con -1 y errno=EINTR. No hubo error real, y la solución es llamar de nuevo. Se puede provocar a propósito con una alarma de un segundo y un read que espera datos de una tubería lenta:

eintr.c (extracto) y terminal
alarm(1);
if (argc > 1) {
    do
        n = read(STDIN_FILENO, buf, sizeof buf);
    while (n < 0 && errno == EINTR);
    printf("con reintento: read -> %zd\n", n);
} else {
    n = read(STDIN_FILENO, buf, sizeof buf);
    printf("sin reintento: read -> %zd, errno=%d (%s)\n", n, errno, strerror(errno));
}

$ sleep 3 | ./eintr
sin reintento: read -> -1, errno=4 (Interrupted system call)
$ (sleep 2; echo fin) | ./eintr reintentar
con reintento: read -> 4

Sin el bucle, la alarma interrumpe el read al segundo, y el programa lo tomaría por un fallo de lectura. Con el bucle do … while (n < 0 && errno == EINTR), el programa sigue esperando y lee los cuatro bytes de «fin\n» cuando llegan. (El manejador se instaló sin SA_RESTART, la bandera que pide al sistema reanudar solo las llamadas; con ella la llamada no se interrumpiría. Las señales se estudian en el curso Procesos e Hilos.) Es un patrón común para read, write, open y close en programas que usan señales. El otro código «temporal» es EAGAIN (igual a EWOULDBLOCK): en un descriptor no bloqueante significa «todavía no hay nada, vuelve a intentarlo». No es un error y se maneja según el caso.

5. Códigos de salida del programa

Cuando un programa termina, deja un número entre 0 y 255, su código de salida (exit status), que el proceso padre puede leer. El shell lo guarda en $?; make lo usa para decidir si detiene la construcción (tema 35); if y && en un script lo consultan. La convención universal: 0 es éxito; cualquier otro valor es fallo.

Un programa lo fija de dos maneras: return n; desde main o exit(n) desde cualquier punto (son equivalentes en main). <stdlib.h> define EXIT_SUCCESS (0) y EXIT_FAILURE (1), los únicos valores que el estándar de C garantiza portables. Un programa de prueba muestra qué le pasa al valor:

salir.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void al_salir(void) { puts("[atexit]"); }

int main(int argc, char **argv)
{
    int n = argc > 1 ? atoi(argv[1]) : 0;
    atexit(al_salir);
    printf("sin salto de linea");
    if (argc > 2)
        _exit(n);
    exit(n);
}
terminal
$ for n in 0 1 2 255 256 300 -1; do ./salir $n >/dev/null; echo "exit($n) -> \$?=$?"; done
exit(0) -> $?=0
exit(1) -> $?=1
exit(2) -> $?=2
exit(255) -> $?=255
exit(256) -> $?=0
exit(300) -> $?=44
exit(-1) -> $?=255
$ ./salir 3 | cat; echo "rc=${PIPESTATUS[0]}"
sin salto de linea[atexit]
rc=3
$ ./salir 3 x | cat; echo "rc=${PIPESTATUS[0]}"
rc=3

Hay tres cosas que aprender de esto:

CódigoQuién lo produceSignificado
0El programaÉxito.
1El programaFallo genérico (EXIT_FAILURE).
2Muchos programas y el shellUso incorrecto o error de sintaxis.
64–78Convención de sysexits.hErrores clasificados (abajo).
126El shellEl archivo existe pero no se puede ejecutar (permiso).
127El shellOrden no encontrada.
128 + nEl shellEl proceso murió por la señal n.
terminal · 126, 127 y muertes por señal
$ ./noexiste; echo "no existe -> $?"
bash: ./noexiste: No such file or directory
no existe -> 127
$ : > sin_permiso; ./sin_permiso; echo "sin permiso -> $?"
bash: ./sin_permiso: Permission denied
sin permiso -> 126
$ ./aborta 2>/dev/null; echo "abort -> $?"
abort -> 134
$ ./segv 2>/dev/null; echo "segv -> $?"
segv -> 139
$ sleep 10 & pid=$!; kill -TERM $pid; wait $pid; echo "SIGTERM -> $?"
SIGTERM -> 143

134 es 128 + 6 (SIGABRT, de abort o de una aserción fallida), 139 es 128 + 11 (SIGSEGV, el segmentation fault del tema 27) y 143 es 128 + 15 (SIGTERM). Si un script ve un 139, ya sabe que el programa no terminó con un exit sino que murió por un acceso inválido a memoria.

Para dar al shell algo más informativo que «1», los sistemas tipo Unix tienen una convención antigua, <sysexits.h>, que clasifica los errores con valores del 64 al 78:

terminal · algunos valores de /usr/include/sysexits.h
$ grep -E '#define EX_(OK|USAGE|DATAERR|NOINPUT|CANTCREAT|IOERR|SOFTWARE|CONFIG)\b' /usr/include/sysexits.h
#define EX_OK		0	/* successful termination */
#define EX_USAGE	64	/* command line usage error */
#define EX_DATAERR	65	/* data format error */
#define EX_NOINPUT	66	/* cannot open input */
#define EX_SOFTWARE	70	/* internal software error */
#define EX_CANTCREAT	73	/* can't create (user) output file */
#define EX_IOERR	74	/* input/output error */
#define EX_CONFIG	78	/* configuration error */

No es un estándar de C ni de POSIX, pero está en glibc y en los BSD, y es una forma barata de que un script distinga «uso incorrecto» de «no se pudo leer la entrada» sin inventar números. Lo importante, en todo caso, es ser consistente y documentar los códigos que usa un programa.

6. Un programa que comprueba todo

Juntemos todo en un programa de uso real: copiar origen destino. Cada llamada que puede fallar se comprueba; cada mensaje dice qué se intentaba y por qué falló; los errores salen por stderr; y el código de salida distingue las categorías con sysexits.h. Una función auxiliar centraliza el aviso y la salida:

copiar.c
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>

static const char *prog = "copiar";

static void fallar(int codigo, const char *fmt, ...)
{
    int e = errno;
    va_list ap;

    fprintf(stderr, "%s: ", prog);
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    if (e)
        fprintf(stderr, ": %s", strerror(e));
    fputc('\n', stderr);
    exit(codigo);
}

int main(int argc, char **argv)
{
    if (argc != 3) {
        errno = 0;
        fallar(EX_USAGE, "uso: %s origen destino", prog);
    }

    FILE *in = fopen(argv[1], "rb");
    if (!in)
        fallar(EX_NOINPUT, "no se puede abrir '%s'", argv[1]);

    FILE *out = fopen(argv[2], "wb");
    if (!out)
        fallar(EX_CANTCREAT, "no se puede crear '%s'", argv[2]);

    char buf[4096];
    size_t n;
    while ((n = fread(buf, 1, sizeof buf, in)) > 0)
        if (fwrite(buf, 1, n, out) != n)
            fallar(EX_IOERR, "error al escribir '%s'", argv[2]);
    if (ferror(in))
        fallar(EX_IOERR, "error al leer '%s'", argv[1]);

    fclose(in);
    if (fclose(out) != 0)
        fallar(EX_IOERR, "error al cerrar '%s'", argv[2]);
    return EX_OK;
}

La función fallar aplica lo aprendido: copia errno al principio, antes de que lo pise cualquier otra llamada; si es cero (error propio del programa, como el uso incorrecto), omite el texto del sistema; y escribe todo en stderr. Las seis pruebas posibles:

terminal
$ echo hola > a.txt
$ ./copiar; echo "rc=$?"
copiar: uso: copiar origen destino
rc=64
$ ./copiar a.txt b.txt; echo "rc=$?"; cat b.txt
rc=0
hola
$ ./copiar noexiste.txt b.txt; echo "rc=$?"
copiar: no se puede abrir 'noexiste.txt': No such file or directory
rc=66
$ ./copiar a.txt /no/existe/b.txt; echo "rc=$?"
copiar: no se puede crear '/no/existe/b.txt': No such file or directory
rc=73
$ mkdir dir; ./copiar dir b.txt; echo "rc=$?"
copiar: error al leer 'dir': Is a directory
rc=74
$ ./copiar a.txt /dev/full; echo "rc=$?"
copiar: error al cerrar '/dev/full': No space left on device
rc=74

Dos casos merecen comentario. El de dir: en Linux, fopen de un directorio para lectura funciona (el error aparece en la primera lectura, EISDIR), y por eso el programa comprueba ferror(in) al terminar el bucle: fread devuelve 0 igual por fin de archivo que por error. El de /dev/full es un dispositivo especial de Linux donde toda escritura falla con «no hay espacio», ideal para probar el camino de error de la escritura. Lo notable: el fallo aparece en fclose, no en fwrite, porque stdio guarda los datos en un búfer y no los entrega al sistema hasta cerrar o vaciar. Si se omite la comprobación del fclose:

terminal · la misma copia con "fclose(out);" sin comprobar
$ ./copiar2 a.txt /dev/full; echo "rc=$?"
rc=0

El programa dice «éxito» y no copió nada. Es un error que ocurre en la vida real con discos llenos, cuotas excedidas o sistemas de archivos de red que se caen, y que solo se descubre comprobando el valor de retorno de fclose (o de fflush). La regla: en un archivo abierto para escribir, fclose es una operación que puede fallar y hay que comprobarla. Los detalles de los búferes, y de las llamadas open, read y write que están debajo, son el tema siguiente.

Una última mejora, que el ejemplo omite por brevedad, es qué hacer con un destino a medio escribir cuando algo falla: lo habitual es borrarlo con remove (guardando antes el errno original), o escribir primero a un archivo temporal y renombrarlo al final, de modo que nadie vea nunca una copia incompleta. En programas más largos, el patrón de limpieza ante errores es un goto hacia una etiqueta al final de la función que libera los recursos en orden inverso al de adquisición: uno de los pocos usos de goto que casi todo el mundo acepta en C.

7. Ayudas de glibc: err.h y error.h

La función fallar del apartado anterior es tan común que la biblioteca de C de GNU y los BSD ya la traen escrita. <err.h> ofrece warn, warnx, err y errx; <error.h> (solo GNU) ofrece error. Todas anteponen el nombre del programa y escriben en stderr:

FunciónQué hace
warn(fmt, …)Mensaje + : strerror(errno). Sigue ejecutando.
warnx(fmt, …)Mensaje sin errno. Sigue ejecutando.
err(código, fmt, …)Como warn, y luego exit(código).
errx(código, fmt, …)Como warnx, y luego exit(código).
error(status, errnum, fmt, …)GNU. Imprime errnum si no es 0; sale si status no es 0.
erh.c y terminal
#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <error.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    (void)argv;
    if (argc == 1) {
        errno = ENOENT;
        warn("no se pudo leer %s", "datos.txt");
        warnx("aviso sin errno");
        error(0, ENOENT, "error() con status 0");
        errno = EACCES;
        err(3, "err() termina el programa");
    }
    errx(4, "errx() sin errno: valor %d", 42);
}

$ ./erh; echo "rc=$?"
erh: no se pudo leer datos.txt: No such file or directory
erh: aviso sin errno
./erh: error() con status 0: No such file or directory
erh: err() termina el programa: Permission denied
rc=3
$ ./erh x; echo "rc=$?"
erh: errx() sin errno: valor 42
rc=4

Una diferencia sutil se ve en la salida: warn y err usan solo el nombre del programa (erh), mientras que error usa argv[0] tal como se invocó (./erh). Para un programa pequeño, err y errx ahorran la función de ayuda; para proyectos que deben compilar en sistemas que no son Linux ni BSD, conviene la propia, que es portable, como la de copiar.c. Y para bibliotecas la regla es otra: una biblioteca nunca debe llamar a exit ni a err, sino devolver el error a quien la llama, que es quien sabe si puede recuperarse.

8. Errores frecuentes

ErrorQué pasaCorrección
No comprobar el resultado de fopen, malloc, open…El programa sigue con NULL o -1 y cae más tarde (SIGSEGV), lejos de la causa.Comprobar cada llamada que pueda fallar, en el mismo punto.
Leer errno sin que la función haya falladoValor viejo o basura: una llamada exitosa puede dejarlo cambiado (visto con pthread_create).Comprobar primero el valor de retorno; mirar errno solo si indica fallo.
Llamar a otras funciones antes de usar errnoUna función de limpieza falla y el mensaje habla de otro error.int e = errno; de inmediato y usar e.
Mirar errno tras pthread_* o posix_memalignSiempre 0 o basura: estas funciones devuelven el código.El valor de retorno es el código: strerror(r).
No poner errno = 0 antes de strtolUn valor viejo de ERANGE hace creer que el número no cabe.errno = 0; justo antes de la llamada.
Tomar NULL de fgets/fread por un errorUn archivo vacío o el fin normal se tratan como fallo.Distinguir con feof y ferror.
No comprobar fclose/fflush en un archivo de salidaDisco lleno o error de red pasan desapercibidos: el programa dice «éxito» y los datos no están.Comprobar su resultado y dar un código de salida distinto de cero.
Mensajes de error por stdoutSe mezclan con la salida normal en tuberías y redirecciones.stderr (perror, fprintf(stderr, …)).
Mensaje sin contexto: solo perror("error")«error: No such file or directory»: ¿qué archivo?Incluir qué se intentaba y con qué dato: perror(nombre).
Terminar siempre con exit(1) sin distinguirUn script no puede saber qué pasó.Códigos distintos para causas distintas (sysexits.h), documentados.
Códigos de salida fuera de 0–255, o calculadosexit(256) es éxito (0).Limitar al rango; no usar contadores sin acotar.
Usar _exit o abort como salida normalSe pierden los búferes de stdio y no corren las funciones de atexit.exit o return desde main.
Tomar EINTR por un errorUn read interrumpido por una señal aborta el programa sin motivo.Reintentar mientras errno == EINTR.
Llamar a exit o err dentro de una bibliotecaEl programa que la usa pierde el control de su propia terminación.Devolver el error (código o NULL más errno) al llamador.

9. Resumen

10. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿De cuántas formas avisa una función de C que falló? Da un ejemplo de cada una.

De cuatro: (1) devuelve -1 y fija errno (open, read); (2) devuelve NULL y fija errno (fopen, malloc); (3) devuelve directamente el código de error, con éxito = 0 (pthread_create, posix_memalign); (4) devuelve un valor especial que no basta por sí solo y exige consultar feof/ferror o poner errno a cero antes (fgets, strtol).

2. ¿Por qué mirar errno después de un pthread_mutex_lock fallido no sirve?

Porque las funciones pthread_* no usan errno: devuelven el código de error como valor de retorno. En el ejemplo de posix_memalign se vio un retorno 22 (EINVAL) con errno aún en 0. El código correcto es int r = f(…); if (r != 0) … strerror(r).

3. ¿Por qué errno es una macro y no una variable global simple?

Porque cada hilo necesita el suyo; si fuera una única variable, dos hilos con errores simultáneos se pisarían. En glibc se expande a (*__errno_location ()), una llamada que devuelve la dirección del errno del hilo actual. Se comprobó que la dirección es distinta en el hilo principal y en otro hilo.

4. ¿Cuándo tiene significado el valor de errno?

Solo inmediatamente después de una llamada cuyo valor de retorno indicó fallo. Una llamada exitosa puede cambiarlo (como pthread_create, que dejó 22 sin error), y una llamada posterior que falle lo pisa. Por eso se comprueba primero el retorno y se copia errno a una variable local de inmediato.

5. El código muestra «Bad file descriptor» al fallar un fopen de un archivo inexistente. ¿Qué pasó?

Entre el fopen y el mensaje, el programa llamó a una función de limpieza (aquí close(-1)) que falló y cambió errno a EBADF. Hay que guardar int e = errno; justo después del fallo y usar e (o restaurar errno) para el mensaje.

6. ¿Por qué hay que poner errno = 0 antes de llamar a strtol?

Porque strtol solo lo modifica al fallar (ERANGE), nunca lo borra, y un resultado como LONG_MAX o 0 puede ser legítimo. Con errno = 0 justo antes, un valor distinto de cero después prueba que ocurrió un error en esa llamada y no antes.

7. ¿En qué se diferencian perror, strerror y strerror_r?

perror(prefijo) escribe «prefijo: texto» directamente en stderr usando errno. strerror(e) devuelve el texto de cualquier código, en un búfer que la biblioteca puede reutilizar (no segura entre hilos). strerror_r(e, buf, n) escribe el texto en un búfer del llamador (apta para hilos); si no cabe, trunca y devuelve ERANGE.

8. ¿Por qué los mensajes de error deben salir por stderr?

Porque así no se mezclan con la salida normal del programa cuando ésta se redirige a un archivo o se pasa por una tubería: el usuario y los scripts ven los errores aunque la salida estándar vaya a otro lugar.

9. ¿Qué código de salida produce exit(256)? ¿Y exit(300)?

0 y 44: el código se reduce a 8 bits (módulo 256). Un exit(256) parecería un éxito; por eso los códigos calculados deben acotarse a 1–255.

10. ¿Qué diferencia hay entre exit y _exit?

exit ejecuta las funciones registradas con atexit y vacía y cierra los flujos de stdio antes de terminar. _exit termina de inmediato: los datos pendientes en los búferes de stdio se pierden (en la prueba, el texto sin salto de línea no apareció). Se usa solo en casos como un proceso hijo tras fork.

11. Un script ve que un programa terminó con código 139. ¿Qué significa?

128 + 11: el proceso murió por la señal 11, SIGSEGV (violación de segmento). No terminó con exit. De igual modo 134 es SIGABRT (abort o una aserción fallida) y 143 es SIGTERM.

12. ¿Qué significan los códigos 126 y 127 del shell?

127: orden no encontrada (el archivo no existe o no está en el PATH). 126: el archivo existe, pero no se puede ejecutar (por ejemplo, sin permiso de ejecución). Los produce el shell, no el programa.

13. ¿Por qué copiar a.txt /dev/full da éxito si se ignora el resultado de fclose?

Porque stdio acumula lo escrito en un búfer y no lo entrega al sistema hasta cerrar o vaciar; fwrite «tiene éxito» al copiar al búfer, y el error ENOSPC aparece recién al volcarlo en fclose. Si no se comprueba, el programa termina con 0 sin haber copiado nada.

14. ¿Cuándo es EINTR un error y cuándo no?

No es un error del recurso: significa que una señal interrumpió una llamada bloqueante. Lo normal es reintentar: do { n = read(…); } while (n < 0 && errno == EINTR);. Solo se trata como fallo si, por diseño, el programa quiere abandonar la espera al recibir la señal.

15. ¿Por qué una biblioteca no debe llamar a exit ante un error?

Porque quita al programa que la usa la posibilidad de recuperarse, reintentar o limpiar sus propios recursos. La biblioteca debe informar el error por su valor de retorno (y, si corresponde, errno) y dejar que el llamador decida.

Para practicar

  1. Revisa un programa tuyo anterior y anota qué llamadas no se comprueban. Añade las comprobaciones, con mensajes que digan qué se intentaba y con qué dato, y provoca cada error a propósito (archivo inexistente, directorio sin permisos, disco lleno con /dev/full).
  2. Escribe una función int leer_entero(const char *texto, long *salida) que use strtol correctamente (con errno = 0, endptr y rango) y devuelva 0 o un código errno (EINVAL, ERANGE), al estilo de la clase 3.
  3. Reproduce el ejemplo de pisa.c con otra función de limpieza (unlink sobre un archivo inexistente) y comprueba que el mensaje cambia. Arréglalo con una variable local.
  4. Haz que copiar borre el destino si falla a mitad de la copia (guardando el errno original antes de llamar a remove). Pruébalo con un destino que se llena (/dev/full no sirve: ¿por qué? Usa un archivo en un sistema de archivos pequeño o un límite con ulimit -f).
  5. Reescribe copiar con err/errx de <err.h> y compara el código de ambas versiones. ¿Qué te da la versión propia que la de la biblioteca no?
  6. Escribe un programa con un bucle de read sobre la entrada estándar que reintente con EINTR y termine con EXIT_SUCCESS en fin de archivo. Pruébalo con (sleep 2; echo hola) | ./prog y una alarma de un segundo.
  7. Haz una tabla propia de códigos de salida para una herramienta de varios errores posibles (no se puede leer la configuración, formato incorrecto, error interno…) usando sysexits.h, y documéntala en un comentario al inicio del archivo.
  8. En un script de shell, ejecuta tu programa y usa $? con case para mostrar un mensaje distinto según el código. ¿Cómo distingues un exit(139) explícito de una violación de segmento real?