← Fundamentos de Sistemas Operativos Bloque 3 · El kernel y las llamadas al sistema · Tema 15 de 34

Tema 15

La biblioteca estándar de C y la API POSIX

Entre un programa y el kernel hay una capa de código que usamos en cada línea sin mirarla: la biblioteca de C. En este tema la abrimos. Seguimos a printf hasta que se convierte en un write, descubrimos el búfer que hay en medio, y vemos qué garantiza el estándar POSIX y cómo consultarlo en el manual.

1. La capa que faltaba

El tema 14 se ocupó de los envoltorios: funciones como write, read y getpid, que se corresponden una a una con las llamadas del kernel y no hacen más que realizarlas. Pero la mayoría de los programas en C no las usa directamente. Escribe con printf, lee con fgets y pide memoria con malloc, y ninguna de esas tres es una llamada al sistema.

Quedaron además dos preguntas abiertas. Al final del tema 14 advertimos que mezclar printf y write puede alterar el orden de la salida. Y en el tema 1 dijimos que printf guarda el texto y lo envía «cuando decide». ¿Cuándo decide, y con qué criterio?

Las dos respuestas están en la misma pieza. En el tema 10 la presentamos como uno de los componentes del sistema; ahora la miramos por dentro.

La biblioteca de C es el conjunto de funciones que todo programa en C tiene a su disposición. Incluye los envoltorios de las llamadas al sistema y, sobre ellos, funciones de más alto nivel que hacen trabajo propio en modo usuario. En Linux, la implementación habitual es glibc.

2. Tres clases de funciones

Desde el punto de vista del sistema operativo, lo que interesa de cada función de la biblioteca es una sola cosa: si entra al kernel, y cuántas veces. Según eso se dividen en tres grupos:

ClaseEjemplosLlamadas al sistema
Envoltorioswrite, read, getpid, closeExactamente una por cada invocación.
Funciones puras de usuariostrlen, strcpy, qsort, snprintf, sqrtNinguna. Trabajan solo con la memoria del programa.
Funciones que administran un recursoprintf, fgets, fopen, mallocAlgunas veces. Piden al kernel en cantidad y reparten por su cuenta.

El tercer grupo es el interesante, y su estrategia es siempre la misma: como cruzar al kernel es caro, la biblioteca pide mucho de una vez y luego atiende los pedidos pequeños del programa sin molestar al kernel. malloc lo hace con la memoria; printf, con la salida.

El siguiente programa permite ver las tres clases. Usa dos marcas escritas con write para delimitar tramos, de modo que en la traza se pueda ver qué llamadas ocurren entre una y otra:

clases.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static int comparar(const void *a, const void *b)
{
    return *(const int *)a - *(const int *)b;
}

int main(void)
{
    int numeros[] = { 42, 7, 19, 3, 88 };
    char texto[64];
    void *bloques[1000];
    size_t largo;

    write(2, "inicio\n", 7);

    qsort(numeros, 5, sizeof(int), comparar);           /* ordenar          */
    snprintf(texto, sizeof(texto), "el menor es %d", numeros[0]);
    largo = strlen(texto);                              /* medir una cadena */

    write(2, "medio\n", 6);

    for (int i = 0; i < 1000; i++)
        bloques[i] = malloc(100);                       /* mil pedidos de memoria */

    write(2, "fin\n", 4);

    printf("%s (%zu caracteres)\n", texto, largo);
    for (int i = 0; i < 1000; i++)
        free(bloques[i]);
    return 0;
}
terminal
$ gcc -Wall -Wextra -g clases.c -o clases
$ strace -e trace=write,brk -o traza.txt ./clases 2> /dev/null
el menor es 3 (13 caracteres)
$ grep -A4 inicio traza.txt
write(2, "inicio\n", 7)                 = 7
write(2, "medio\n", 6)                  = 6
brk(NULL)                               = 0x5581c7f6e000
brk(0x5581c7f8f000)                     = 0x5581c7f8f000
write(2, "fin\n", 4)                    = 4

Entre «inicio» y «medio» no hay nada: ordenar un arreglo, dar formato a un texto y medir una cadena no requieren al kernel. Entre «medio» y «fin» hubo mil llamadas a malloc y solo dos llamadas al sistema. Con la llamada brk, la biblioteca pidió al kernel que ampliara la memoria del proceso en un bloque de 132 KB, y de ese bloque fue entregando los mil trozos de 100 bytes. Novecientos noventa y nueve pedidos se resolvieron sin salir del modo usuario.

3. De printf a write

printf hace dos trabajos distintos, y ninguno de los dos es escribir en la pantalla:

  1. Da formato. Recorre la cadena de formato y convierte cada argumento en texto: el entero 30 se transforma en los caracteres '3' y '0'. Es cálculo puro, igual que snprintf.
  2. Guarda el resultado en un búfer. Los caracteres producidos se añaden a una zona de memoria que pertenece al programa.

La llamada write ocurre después, cuando la biblioteca resuelve vaciar ese búfer. Un programa con tres printf lo muestra con claridad:

unidos.c
#include <stdio.h>

int main(void)
{
    int edad = 30;

    printf("Nombre: %s\n", "Ana");
    printf("Edad:   %d\n", edad);
    printf("Total:  %.2f\n", 12.5);
    return 0;
}
terminal
$ gcc -Wall -Wextra -g unidos.c -o unidos
$ strace -e trace=write -o traza.txt ./unidos          # salida a la terminal
Nombre: Ana
Edad:   30
Total:  12.50
$ cat traza.txt
write(1, "Nombre: Ana\n", 12)           = 12
write(1, "Edad:   30\n", 11)            = 11
write(1, "Total:  12.50\n", 14)         = 14
+++ exited with 0 +++

$ strace -e trace=write -o traza.txt ./unidos > salida.txt   # salida a un archivo
$ cat traza.txt
write(1, "Nombre: Ana\nEdad:   30\nTotal:  12.50\n", 37) = 37
+++ exited with 0 +++

El mismo ejecutable hizo tres llamadas al sistema en un caso y una sola en el otro. En la segunda ejecución, los tres textos se acumularon en el búfer y salieron juntos al terminar el programa: 12 + 11 + 14 = 37 bytes en un único write. El kernel nunca supo que hubo tres printf.

Tres llamadas a printf depositan su texto en el búfer de stdout, dentro del proceso; al vaciarse, el búfer produce una sola llamada write que cruza al kernel ESPACIO DE USUARIO: EL PROCESO printf(…) printf(…) printf(…) búfer de stdout Nombre: Ana\n Edad: 30\n Total: 12.50\n write Kernel
El búfer está en la memoria del proceso. Llenarlo cuesta lo que una copia en memoria; vaciarlo cuesta una llamada al sistema.

La razón de este diseño es el costo que medimos en los temas 11 y 14. Una llamada al sistema cuesta unos cientos de nanosegundos, la misma cantidad sea cual sea el tamaño de los datos. Añadir caracteres a un arreglo cuesta casi nada. Juntar muchas escrituras pequeñas en una grande es la aplicación directa de la regla «pocas llamadas, y pedir mucho en cada una», y la biblioteca la aplica por nosotros.

4. El búfer y sus tres modos

Queda por explicar por qué en la terminal hubo tres write. Si la biblioteca esperara siempre a llenar el búfer, un programa interactivo sería inutilizable: haría una pregunta y el texto no aparecería hasta mucho después. Por eso el momento del vaciado depende del destino. La parte de la biblioteca que se ocupa de la entrada y la salida, llamada stdio por su archivo de cabecera, tiene tres modos:

ModoCuándo se vacía el búferSe aplica por defecto a
Por líneasAl escribir un salto de línea, o al llenarse.stdout, si está conectado a una terminal.
CompletoSolo al llenarse; típicamente, cada 4096 bytes.stdout, si está conectado a un archivo o a una tubería. Todos los archivos abiertos con fopen.
Sin búferEn cada operación.stderr, siempre.

En los tres modos, el búfer se vacía también al terminar el programa normalmente y cuando el programa lo pide de forma explícita con fflush.

La biblioteca elige el modo la primera vez que el programa escribe: pregunta al kernel qué hay detrás del descriptor 1 y actúa en consecuencia. Eso explica el experimento anterior. En la terminal, cada \n provocó un vaciado; con la salida redirigida, nada lo provocó hasta el final.

El modo por líneas tiene una consecuencia que desconcierta a quien la encuentra por primera vez:

demora.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    printf("Procesando...");            /* sin salto de línea */
    sleep(3);                           /* simula un trabajo largo */
    printf(" listo\n");
    return 0;
}
terminal
$ gcc -Wall -Wextra -g demora.c -o demora
$ ./demora
                                    # tres segundos sin que aparezca nada…
Procesando... listo                 # …y luego todo junto

El primer texto no termina en salto de línea, así que queda retenido en el búfer mientras el programa duerme. Hay tres maneras de corregirlo, y conviene conocer las tres:

SoluciónCódigoCuándo conviene
Vaciar a manofflush(stdout);Es la habitual: se pone después del texto que debe verse ya.
Usar la salida de erroresfprintf(stderr, "…");Para mensajes de diagnóstico y de progreso.
Cambiar el modosetvbuf(stdout, NULL, _IONBF, 0);Al comienzo del programa, si toda la salida debe ser inmediata.

5. Mezclar printf y write

Con lo anterior, la advertencia del tema 14 deja de ser un misterio. Este programa escribe tres líneas, la del medio con write:

mezcla.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    printf("uno (printf)\n");
    write(1, "dos (write)\n", 12);
    printf("tres (printf)\n");
    return 0;
}
terminal
$ gcc -Wall -Wextra -g mezcla.c -o mezcla
$ ./mezcla                          # a la terminal
uno (printf)
dos (write)
tres (printf)
$ ./mezcla | cat                    # a una tubería
dos (write)
uno (printf)
tres (printf)
$ strace -e trace=write -o traza.txt ./mezcla > /dev/null
$ cat traza.txt
write(1, "dos (write)\n", 12)           = 12
write(1, "uno (printf)\ntres (printf)\n", 27) = 27
+++ exited with 0 +++

En la terminal, el modo por líneas vacía el búfer en cada \n y el orden coincide con el del código. Con la salida en una tubería, el modo es completo: «uno» queda en el búfer, write envía «dos» al kernel de inmediato, «tres» se suma al búfer, y este se vacía al final. La traza lo muestra sin ambigüedad.

No hay ningún error en la biblioteca ni en el kernel. Hay dos caminos hacia el mismo descriptor, uno con sala de espera y otro sin ella. La regla práctica es no mezclarlos; y si hace falta, llamar a fflush(stdout) antes de cada write directo.

Un programa que cambia según quién lo mire

Este es uno de los comportamientos más traicioneros de la programación en C: el programa funciona bien mientras se lo prueba en la terminal y falla cuando su salida se guarda en un archivo o se pasa a otro comando. Cuando un programa «pierde» o desordena texto solo al redirigirlo, el búfer de stdio es el primer sospechoso.

6. Cuando el búfer se pierde: exit y _exit

El búfer está en la memoria del proceso. Si el proceso desaparece antes de vaciarlo, su contenido desaparece con él, sin que el kernel llegue a saber que existía. Por eso importa quién se encarga del vaciado final.

En C hay dos funciones para terminar un programa, con nombres casi iguales y comportamientos muy distintos:

exit(n)_exit(n)
Qué esUna función de la bibliotecaUn envoltorio de la llamada al sistema
Se declara enstdlib.hunistd.h
Vacía los búferes de stdioSíNo
Ejecuta las funciones registradas con atexitSíNo
FinalmenteLlama a _exit(n)Pide al kernel que finalice el proceso

Devolver un valor desde main equivale a llamar a exit con ese valor: el código de arranque que rodea a main, el que empieza en _start, lo hace por nosotros. Por eso en los programas anteriores el texto pendiente siempre apareció. Si se termina por la vía directa, no aparece:

perdido.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    printf("Este texto no llega a ninguna parte");
    _exit(0);                           /* termina sin vaciar el búfer */
}
terminal
$ gcc -Wall -Wextra -g perdido.c -o perdido
$ ./perdido
$ strace -e trace=write ./perdido
+++ exited with 0 +++

Ni una sola llamada write. El texto se formó, se guardó en el búfer y se perdió con el proceso.

Lo mismo ocurre cuando el programa no termina por su voluntad. Si un proceso es finalizado por el kernel a causa de un fallo, como los del tema 12, nadie llama a exit y el búfer no se vacía. Tiene una consecuencia práctica muy concreta: al depurar un programa que falla, los mensajes de printf anteriores al fallo pueden no aparecer, y hacen pensar que el programa murió antes de lo que realmente murió. Por eso los mensajes de diagnóstico se escriben en stderr, que no tiene búfer.

7. El búfer, medido

En el tema 14, copiar_1 copiaba un archivo de 10 MB leyendo y escribiendo de a un byte, y tardaba más de seis segundos. El siguiente programa también procesa un byte por vez, pero lo hace con funciones de stdio:

copiar_stdio.c
#include <stdio.h>

int main(void)
{
    int c;

    while ((c = getchar()) != EOF)      /* un byte por vuelta */
        putchar(c);
    return 0;
}
terminal
$ gcc -Wall -Wextra -g copiar_stdio.c -o copiar_stdio
$ time ./copiar_stdio < datos.bin > /dev/null
real	0m0.085s
user	0m0.081s
sys	0m0.004s
$ strace -c -o cuenta.txt ./copiar_stdio < datos.bin > /dev/null
$ grep -Ew 'read|write' cuenta.txt
 53.40    0.002105           0      2562           read
 45.12    0.001779           0      2560           write

Diez millones de llamadas a getchar y otras tantas a putchar produjeron las mismas cinco mil llamadas al sistema que la versión de copiar con búfer de 4096 bytes. getchar hace con la entrada lo que printf con la salida: la primera vez pide al kernel un bloque entero, y las siguientes 4095 veces entrega un byte de los que ya tiene.

ProgramaCómo trabajaLlamadas al sistemaTiempo
copiar_1read y write de 1 byte21 millones6,2 s
copiar_stdiogetchar y putchar, 1 byte5 mil0,085 s
copiarread y write de 4096 bytes5 mil0,004 s

La comparación entre las dos primeras filas es la diferencia entre una llamada al sistema y una llamada a función, vista en un programa completo: el mismo estilo de código, byte a byte, resulta setenta veces más rápido cuando cada byte no implica un cruce al kernel. La tercera fila muestra que el búfer propio sigue siendo lo más eficiente, porque evita además veinte millones de llamadas a función. Los valores exactos cambian con el equipo y con el sistema de archivos; las proporciones se mantienen.

8. Dos interfaces para lo mismo

Un programa en C dispone, entonces, de dos juegos de funciones para la entrada y la salida. No compiten: uno está construido sobre el otro.

Interfaz de descriptoresInterfaz de stdio
Un archivo abierto esUn int: el descriptorUn FILE *: un puntero a una estructura de la biblioteca
Los tres iniciales0, 1, 2stdin, stdout, stderr
Abrir y cerraropen, closefopen, fclose
Leer y escribirread, writefread, fgets, getchar, fwrite, printf, putchar
Trabaja conBytesBytes, líneas y texto con formato
Búfer en el procesoNoSí
Definida porPOSIXEl estándar del lenguaje C
Página del manualSección 2Sección 3

Una estructura FILE contiene, en esencia, un descriptor, un búfer y la cuenta de cuánto hay en él. La función fileno devuelve el descriptor que hay dentro de un FILE *, y fdopen hace lo inverso: construye un FILE * alrededor de un descriptor existente.

¿Cuál usar? Para texto, lectura por líneas y salida con formato, stdio es más cómoda y rinde bien sin esfuerzo. Los descriptores son necesarios cuando hace falta control exacto sobre cada llamada: con dispositivos, tuberías y conexiones de red, o cuando se necesita saber que un dato salió del proceso en un momento preciso. Los programas de sistema usan los dos, cada uno en su lugar.

9. Envoltorios que usan otra llamada

En el tema 14 dijimos que un envoltorio tiene el mismo nombre que la llamada al sistema que realiza. Es cierto en la mayoría de los casos, pero no en todos. Este programa abre un archivo, espera un segundo y termina:

nombres.c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    int fd = open("/etc/hostname", O_RDONLY);

    if (fd == -1) {
        perror("open");
        return 1;
    }
    close(fd);
    sleep(1);
    exit(0);
}
terminal
$ gcc -Wall -Wextra -g nombres.c -o nombres
$ strace -o traza.txt ./nombres
$ tail -5 traza.txt
openat(AT_FDCWD, "/etc/hostname", O_RDONLY) = 3
close(3)                                = 0
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, 0x7ffc5d0e3a10) = 0
exit_group(0)                           = ?
+++ exited with 0 +++

De las cuatro funciones que llamó el programa, solo close aparece en la traza con su nombre. Las otras tres se tradujeron a llamadas distintas:

Función de la bibliotecaLlamada al sistema que se ejecutaMotivo
openopenatUna llamada posterior y más general, que permite indicar un directorio de partida.
sleepclock_nanosleepEl kernel ofrece una espera en nanosegundos; la biblioteca convierte los segundos.
exitexit_groupFinaliza todos los hilos del proceso, y no solo el que llama.
forkcloneUna llamada más general para crear procesos e hilos.
signalrt_sigactionLa interfaz moderna para las señales.
timeNingunaSe resuelve en el vDSO, sin entrar al kernel.

En todos los casos sucedió lo mismo: con los años el kernel incorporó una llamada más capaz, y la biblioteca pasó a usarla conservando el nombre y los parámetros que los programas ya conocían. Un programa escrito en 1990 con open se compila hoy sin tocar una línea y usa openat sin saberlo.

Es la ventaja de tener dos interfaces en lugar de una, como anticipamos en el tema 10: la del kernel puede crecer y la de la biblioteca puede permanecer. Y es también la razón por la que, al leer una traza, no hay que buscar los nombres del código fuente, sino los de las llamadas que efectivamente llegan al kernel. Lo practicaremos en el tema 16.

10. glibc, el archivo

Todo el código que venimos describiendo está en un archivo concreto, el que ldd mostraba en los temas 9 y 10. Tiene una rareza: además de ser una biblioteca, se puede ejecutar, y al hacerlo se identifica.

terminal
$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.39-0ubuntu8.4) stable release version 2.39.
Copyright (C) 2024 Free Software Foundation, Inc.
…
$ ls -lhL /lib/x86_64-linux-gnu/libc.so.6
-rwxr-xr-x 1 root root 2.1M ene 28  2025 /lib/x86_64-linux-gnu/libc.so.6
$ nm -D --defined-only /lib/x86_64-linux-gnu/libc.so.6 | wc -l
3036

En Fedora el archivo está en /lib64/libc.so.6. Son dos megabytes con unos tres mil nombres públicos, entre funciones y variables: frente a las menos de cuatrocientas llamadas al sistema del kernel, da una idea de cuánto trabajo se hace en modo usuario. Como es una biblioteca compartida, existe una sola copia en memoria, que usan a la vez todos los procesos del sistema.

La versión importa más de lo que parece. Cada ejecutable registra qué versión de la biblioteca necesita para cada función que usa:

terminal
$ objdump -T unidos | grep GLIBC
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.34) __libc_start_main
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) printf

La primera línea es la función que llama a main; el ejecutable exige que la biblioteca sea, como mínimo, la 2.34. Si se lo copia a un sistema con una glibc anterior, el enlazador dinámico se niega a iniciarlo:

terminal, en un sistema más antiguo
$ ./unidos
./unidos: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./unidos)

Es uno de los errores más comunes al distribuir programas en Linux, y sigue una regla simple: un ejecutable funciona en sistemas con una glibc igual o más nueva que aquella con la que se compiló, pero no más vieja. Por eso los programas que se distribuyen ya compilados se construyen a propósito en una distribución antigua.

No es la única biblioteca de C

glibc es la más extendida, pero la interfaz es un estándar y admite otras implementaciones. La distribución Alpine, muy usada en contenedores, emplea musl, más pequeña y simple. Android usa Bionic. Un programa compilado contra una no funciona con otra, aunque el kernel sea el mismo: es el caso de Ubuntu y Android del tema 10.

11. POSIX: un contrato entre sistemas

En el tema 3 contamos que, hacia los años ochenta, Unix se había dividido en variantes incompatibles: un programa escrito para una no compilaba en otra. La respuesta fue un estándar, publicado en 1988 por el IEEE con el nombre POSIX, de Portable Operating System Interface; la X final recuerda su origen en Unix.

POSIX es una norma que especifica la interfaz que un sistema operativo debe ofrecer a los programas: qué funciones de C existen, qué parámetros reciben, qué devuelven y qué errores pueden informar. Un programa que usa solo lo que la norma define se puede compilar en cualquier sistema que la cumpla.

Conviene precisar su alcance, porque se presta a confusiones:

POSIX definePOSIX no define
Las funciones: open, read, write, fork, getpid y unas mil más.Cuáles de ellas son llamadas al sistema y cuáles funciones de biblioteca.
Los archivos de cabecera: unistd.h, fcntl.h, errno.h.Los números de las llamadas ni los registros en que se pasan.
Los nombres de los errores: EBADF, EACCES.Sus valores numéricos.
La shell y las utilidades: sh, ls, grep, cp.Cómo está construido el kernel.

La columna derecha explica por qué en el tema 14 recomendamos comparar errno con nombres y no con números. Y marca una distinción importante: POSIX garantiza la portabilidad del código fuente, no la de los ejecutables. El programa copiar.c se compila sin cambios en Linux, en macOS y en FreeBSD, pero el ejecutable producido en uno no sirve en los otros: cada sistema tiene su formato, su biblioteca y sus números de llamada.

SistemaRelación con POSIX
LinuxLo cumple casi por completo, sin certificación oficial.
macOSCertificado como UNIX.
FreeBSD, OpenBSDLo cumplen casi por completo.
WindowsNo. Su interfaz nativa es otra, la API Win32. WSL lo resuelve ejecutando un kernel Linux.

Un programa puede preguntar por la versión de la norma y por algunos límites del sistema. La función sysconf, también de POSIX, sirve para eso:

posix.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    printf("Version de POSIX:           %ld\n", (long)_POSIX_VERSION);
    printf("Tamano de pagina:           %ld bytes\n", sysconf(_SC_PAGESIZE));
    printf("Archivos abiertos, maximo:  %ld\n", sysconf(_SC_OPEN_MAX));
    printf("Tics de reloj por segundo:  %ld\n", sysconf(_SC_CLK_TCK));
    return 0;
}
terminal
$ gcc -Wall -Wextra -g posix.c -o posix
$ ./posix
Version de POSIX:           200809
Tamano de pagina:           4096 bytes
Archivos abiertos, maximo:  1024
Tics de reloj por segundo:  100
$ getconf PAGESIZE                  # lo mismo, desde la shell
4096

El número 200809 indica la revisión de septiembre de 2008 de la norma. El programa no contiene ningún valor propio de Linux: pregunta en lugar de suponer, y por eso funciona igual en un sistema donde las páginas de memoria miden 16 KB, como las Mac con procesador ARM. El último valor es el que usamos en el tema 12 para interpretar /proc/stat: las centésimas de segundo.

12. Tres círculos: ISO C, POSIX y extensiones

Las funciones que ofrece glibc provienen de tres fuentes, cada una más amplia que la anterior. Saber a cuál pertenece una función es saber en qué sistemas se podrá compilar el programa.

Tres conjuntos anidados de funciones: el estándar ISO C en el centro, POSIX alrededor y las extensiones de GNU y Linux en el exterior Extensiones de GNU y de Linux syscall gettid epoll_wait asprintf solo Linux POSIX open read write fork getpid sysconf sistemas tipo Unix ISO C: la biblioteca estándar del lenguaje printf fopen malloc strlen qsort exit cualquier sistema con un compilador de C, incluido Windows
Cuanto más afuera está una función, menos portable es el programa que la usa.
OrigenQuién lo defineCabeceras típicasTrata de
ISO CEl estándar del lenguajestdio.h, stdlib.h, string.h, math.hLo que tiene sentido en cualquier máquina: texto, memoria, cálculo, archivos como secuencias de bytes.
POSIXEl IEEE y The Open Groupunistd.h, fcntl.h, sys/stat.h, signal.hLo propio de un sistema operativo: procesos, descriptores, permisos, directorios, señales.
ExtensionesEl proyecto GNU y el kernel Linuxsys/syscall.h, sys/epoll.hFunciones adicionales y servicios que solo ofrece Linux.

La división coincide con algo que ya observamos: stdio.h existe en Windows y unistd.h no. El estándar de C no puede hablar de procesos ni de descriptores, porque debe servir también para un microcontrolador sin sistema operativo. De ahí que la «biblioteca estándar de C» en sentido estricto sea solo el círculo interior, y que glibc, que implementa los tres, sea bastante más que eso.

Las extensiones están ocultas por defecto. Para usarlas, el programa lo declara antes de incluir cualquier cabecera:

pedir las extensiones
#define _GNU_SOURCE             /* antes del primer #include */
#include <unistd.h>

pid_t hilo = gettid();          /* disponible solo con la línea anterior */

Esa línea es una señal útil al leer código ajeno: avisa de que el programa renuncia a la portabilidad y solo se compila en Linux con glibc.

13. Leer una página del manual

En el tema 4 presentamos man y sus secciones. Ahora que conocemos la diferencia entre el kernel, la biblioteca y el estándar, una página del manual dice mucho más. Para cualquier función, responde las tres preguntas de este tema: si es una llamada al sistema, qué estándar la define y en qué se aparta el envoltorio del kernel.

terminal
$ man -f write                      # ¿en qué secciones existe este nombre?
write (1)            - send a message to another user
write (2)            - write to a file descriptor
$ man -f printf
printf (1)           - format and print data
printf (3)           - formatted output conversion

El número entre paréntesis ya da la primera respuesta: write está en la sección 2 y es una llamada al sistema; printf está en la 3 y es una función de biblioteca. Por eso en los textos técnicos las funciones se citan con su sección: write(2), printf(3). Dentro de la página, cada apartado tiene un propósito fijo:

ApartadoQué contienePara qué sirve
SYNOPSISLas cabeceras necesarias y el prototipo.Saber qué incluir y con qué tipos llamar. Indica también si hace falta _GNU_SOURCE.
DESCRIPTIONQué hace la función y qué significa cada parámetro.—
RETURN VALUEQué devuelve en caso de éxito y de error.Escribir la comprobación correcta.
ERRORSLos valores posibles de errno y qué los causa.Interpretar un fallo.
STANDARDSLa norma que define la función.Saber si es portable. En páginas más antiguas se titula CONFORMING TO.
HISTORY / VERSIONSDesde qué versión del kernel o de glibc existe.Saber si funcionará en un sistema antiguo.
NOTESAdvertencias y detalles de implementación.Aquí suele estar C library/kernel differences.
SEE ALSOPáginas relacionadas.Descubrir funciones vecinas.

El subapartado C library/kernel differences es el que documenta los casos de la sección 9. En man 2 fork, por ejemplo, explica que el envoltorio de glibc no invoca la llamada fork del kernel sino clone. Cuando una traza de strace muestra un nombre que no esperabas, la explicación está casi siempre ahí.

Además de las páginas de cada función, hay algunas de conjunto que conviene conocer:

ComandoContenido
man 2 syscallsLa lista de todas las llamadas al sistema de Linux, con la versión del kernel en que apareció cada una.
man 2 introIntroducción a la sección 2: qué es una llamada al sistema y cómo se informan los errores.
man 3 errnoTodos los códigos de error, con su nombre y su significado.
man 3 stdioVisión general de las funciones de entrada y salida con búfer.
man 7 standardsLos estándares que menciona el manual: las versiones de C y de POSIX.
man 7 vdsoQué llamadas se resuelven sin entrar al kernel en cada arquitectura.

La sección 7 reúne textos explicativos sobre conceptos, a diferencia de las secciones 2 y 3, que son de referencia. Y man -k palabra busca en los títulos de todas las páginas, para cuando se sabe qué se quiere hacer pero no cómo se llama la función.

14. Resumen

15. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. Clasifica strlen, getpid y malloc según las llamadas al sistema que hacen.

strlen no hace ninguna: recorre memoria del programa. getpid es un envoltorio y hace exactamente una. malloc hace alguna de vez en cuando: pide memoria al kernel en bloques grandes y atiende la mayoría de los pedidos con lo que ya tiene.

2. ¿Cuáles son los dos trabajos de printf? ¿Cuál de ellos necesita al kernel?

Convertir los argumentos en texto según el formato, y añadir ese texto al búfer de stdout. Ninguno de los dos necesita al kernel. La llamada write ocurre aparte, cuando el búfer se vacía.

3. Un programa con tres printf hace tres write en la terminal y uno solo si se redirige a un archivo. ¿Por qué?

Porque el modo del búfer depende del destino. En una terminal es por líneas, y cada salto de línea provoca un vaciado. En un archivo es completo: los tres textos se acumulan y salen juntos al terminar el programa.

4. ¿Por qué stderr no usa búfer?

Porque se destina a mensajes de error y de diagnóstico, que deben verse en el acto y no deben perderse si el programa falla a continuación. El costo de una llamada por mensaje es aceptable a cambio de esa garantía.

5. En ./mezcla | cat, ¿por qué «dos» aparece antes que «uno»?

Con la salida en una tubería, el texto de printf queda en el búfer del proceso. El write directo no pasa por ese búfer y llega al kernel de inmediato. «uno» y «tres» salen después, juntos, cuando el búfer se vacía al terminar.

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

exit es una función de la biblioteca: vacía los búferes de stdio, ejecuta las funciones registradas con atexit y luego llama a _exit. _exit es el envoltorio de la llamada al sistema: finaliza el proceso de inmediato, y lo que hubiera en los búferes se pierde.

7. Un programa imprime «paso 3» con printf y falla enseguida, pero el mensaje no aparece. ¿Qué pudo pasar?

El mensaje quedó en el búfer. Al ser finalizado el proceso por el kernel, nadie llamó a exit y el búfer no se vació. Usando fprintf(stderr, …), o fflush(stdout) después del printf, el mensaje habría salido.

8. ¿Por qué copiar_stdio es mucho más rápido que copiar_1, si ambos procesan un byte por vez?

Porque getchar y putchar trabajan sobre búferes en la memoria del proceso y solo llaman al kernel cada 4096 bytes. copiar_1 hace una llamada al sistema por cada byte leído y otra por cada byte escrito.

9. El código llama a open y en la traza aparece openat. ¿Es un error?

No. La biblioteca implementa open mediante la llamada openat, más general. El nombre y los parámetros de la función se mantienen para los programas, y la llamada que llega al kernel es otra.

10. ¿Qué garantiza POSIX y qué no?

Garantiza que ciertas funciones existen, con parámetros, resultados y errores definidos, de modo que el código fuente se pueda compilar en cualquier sistema que cumpla la norma. No garantiza que un ejecutable funcione en otro sistema, ni dice cómo se implementan las funciones o qué números de llamada se usan.

11. ¿Por qué stdio.h existe en Windows y unistd.h no?

stdio.h pertenece al estándar del lenguaje C, que todo compilador debe ofrecer. unistd.h pertenece a POSIX, la interfaz de los sistemas tipo Unix, que Windows no implementa.

Para practicar

  1. En clases.c, cambia el tamaño de cada bloque de 100 a 1000 bytes y repite la traza. ¿Cuántas llamadas brk aparecen ahora? Prueba luego con un solo malloc de 1 MB y trazando también mmap.
  2. Ejecuta ./unidos | cat con strace -e trace=write -o traza.txt. ¿Se parece al caso de la terminal o al del archivo?
  3. Corrige demora.c de las tres maneras de la sección 4 y comprueba cada una.
  4. Añade fflush(stdout) en mezcla.c para que el orden sea correcto también con | cat. ¿Cuántas llamadas write hace ahora el programa?
  5. En perdido.c, añade \n al final del texto y ejecútalo en la terminal y con | cat. Explica la diferencia. Luego reemplaza _exit(0) por exit(0).
  6. Escribe un programa que imprima un mensaje sin salto de línea y a continuación lea de la dirección 0, como espiar.c del tema 12. Comprueba que el mensaje no aparece, y corrígelo.
  7. Ejecuta strace -e trace=read -o traza.txt ./copiar_stdio < datos.bin > /dev/null y mira las primeras líneas con head traza.txt. ¿De qué tamaño son las lecturas?
  8. Averigua la versión de glibc de tu sistema de tres formas: ejecutando el archivo, con ldd --version y con getconf GNU_LIBC_VERSION.
  9. Consulta man 2 getpid, man 3 printf y man 2 gettid. Para cada una, localiza el apartado que indica el estándar y determina a cuál de los tres círculos pertenece.
  10. Busca en man 2 _exit el apartado C library/kernel differences y relaciona lo que dice con la traza de nombres.c.