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

Tema 11

El kernel: el núcleo del sistema

Llevamos diez temas nombrándolo. Ahora lo miramos de frente: qué clase de programa es el kernel, cuándo se ejecuta, de qué se ocupa, qué información guarda y dónde está, en el disco y en la memoria de un sistema Linux.

1. Qué es el kernel

En el tema 10 ubicamos el kernel en el mapa de componentes. Ahora podemos definirlo con precisión:

El kernel es el programa que se ejecuta en el modo privilegiado de la CPU, permanece en memoria desde el arranque hasta el apagado y tiene el control exclusivo del hardware. Todos los demás programas acceden a los recursos de la máquina a través de él.

Tres rasgos lo distinguen de cualquier otro software:

La palabra «kernel» significa núcleo, o semilla: la parte central alrededor de la cual se construye el resto. Es, además, software como cualquier otro. En Linux está escrito casi íntegramente en C, se compila con gcc y contiene funciones, estructuras y bucles como los que escribes tú. Lo que lo hace especial no es su código, sino la posición desde la que se ejecuta.

2. Un programa que solo responde

La primera intuición sobre el kernel suele ser errónea. Se lo imagina como un programa que corre en paralelo a los demás, vigilándolos. No es así, y ya lo adelantamos en el tema 1: en un procesador se ejecuta una sola cosa a la vez, y mientras un programa de usuario tiene la CPU, el kernel no ejecuta ni una instrucción.

El kernel no es un proceso. No tiene una función main con un bucle que gire sin parar. Después de arrancar, es un conjunto de rutinas que esperan a ser invocadas, y solo hay tres acontecimientos que pueden invocarlas:

Las tres entradas al kernel: llamada al sistema, interrupción y excepción Llamada al sistema un programa pide un servicio Interrupción un dispositivo requiere atención Excepción una instrucción no puede completarse Kernel atiende el acontecimiento y devuelve la CPU a un programa
Fuera de estos tres casos, el kernel no se ejecuta.
EntradaQuién la provocaEjemploTema
Llamada al sistemaEl programa en ejecución, a propósito.write, open, getpid13
InterrupciónUn dispositivo, en cualquier momento.Llegó un paquete de red; venció el temporizador.17
ExcepciónEl programa en ejecución, sin quererlo.División por cero; acceso a memoria inválida.18

En los tres casos ocurre lo mismo: la CPU deja lo que hacía, pasa a modo privilegiado y salta a una rutina del kernel. El kernel atiende el asunto y devuelve la CPU, al mismo programa o a otro. Por eso conviene pensar en él menos como un programa y más como una biblioteca con privilegios combinada con un conjunto de manejadores de eventos.

Si el kernel solo se ejecuta cuando algo lo invoca, la multitarea parece imposible: un programa en un bucle infinito, que no hace llamadas al sistema ni provoca excepciones, nunca lo activaría. La pieza que falta es el temporizador, un dispositivo que interrumpe a la CPU a intervalos regulares precisamente para garantizar que el kernel recupere el control. Lo vemos en el tema 19.

3. Medir el tiempo dentro del kernel

Cuando un programa hace una llamada al sistema, el kernel trabaja en nombre de ese programa, y lleva la cuenta del tiempo que emplea. El comando time lo muestra separado en tres cifras:

El siguiente programa tiene dos modos de trabajo. Con el argumento calcular hace mil millones de sumas, sin pedir nada a nadie. Con pedir hace cinco millones de llamadas al sistema, escribiendo un byte cada vez en /dev/null, el archivo especial que descarta todo lo que recibe.

tiempos.c
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (argc == 2 && strcmp(argv[1], "calcular") == 0) {
        volatile unsigned long suma = 0;

        for (unsigned long i = 0; i < 1000000000UL; i++)
            suma += i;                      /* solo instrucciones propias */

    } else if (argc == 2 && strcmp(argv[1], "pedir") == 0) {
        char byte = 'x';
        int fd = open("/dev/null", O_WRONLY);

        if (fd == -1) {
            perror("open");
            return 1;
        }
        for (long i = 0; i < 5000000; i++)
            if (write(fd, &byte, 1) != 1)   /* una llamada al sistema por vuelta */
                break;
        close(fd);

    } else {
        fprintf(stderr, "uso: %s calcular|pedir\n", argv[0]);
        return 1;
    }
    return 0;
}
terminal
$ gcc -Wall -Wextra -g tiempos.c -o tiempos
$ time ./tiempos calcular
real	0m2.104s
user	0m2.101s
sys	0m0.002s

$ time ./tiempos pedir
real	0m1.612s
user	0m0.358s
sys	0m1.251s

Los valores exactos dependen del equipo, pero el patrón es siempre el mismo:

Hay además una cuenta interesante: cinco millones de escrituras en 1,6 segundos son unos 320 nanosegundos por llamada, y eso que escribir en /dev/null no hace nada. Ese es, aproximadamente, el costo de cruzar la frontera hacia el kernel y volver: el equivalente a cientos de sumas. Explica por qué en el tema 7 recomendamos leer y escribir en bloques grandes.

4. Sus responsabilidades

El kernel se organiza en subsistemas, uno por cada tipo de recurso que administra. Para cada uno indicamos qué abstracción ofrece a los programas, con qué llamadas al sistema se usa y dónde puede observarse:

SubsistemaQué administraAbstracciónLlamadas típicasDónde se observa
ProcesosLa CPUProceso, hilofork, execve, exit, wait/proc/<pid>, ps
MemoriaLa RAM y el intercambioEspacio de direccionesmmap, brk/proc/meminfo, free
ArchivosLos dispositivos de almacenamientoArchivo, directorioopen, read, write, close/proc/mounts, df
RedLas interfaces de redSocketsocket, connect, send/proc/net, ss
DispositivosTodos los controladores de hardwareArchivo de dispositivoioctl, read, write/sys, /dev

A esas cinco áreas se suman tres tareas que las atraviesan a todas:

Cada fila de la tabla es un curso de la ruta que sigue a este. En el bloque 7 damos una primera vuelta por todas.

5. El kernel como contable

Buena parte del trabajo del kernel consiste en llevar registros. Para administrar algo hay que saber qué existe, a quién pertenece y en qué estado está, de modo que el kernel mantiene en su memoria grandes tablas con esa información:

Estos registros no son nada exótico: son estructuras de C. La que describe a un proceso en Linux se llama task_struct. Tiene cientos de campos; este es un extracto simplificado:

del código fuente de Linux (extracto simplificado)
struct task_struct {
    unsigned int         __state;     /* en ejecución, en espera, detenido… */
    pid_t                pid;         /* identificador del proceso */
    struct task_struct  *parent;      /* el proceso que lo creó */
    struct mm_struct    *mm;          /* su espacio de direcciones */
    struct files_struct *files;       /* sus archivos abiertos */
    const struct cred   *cred;        /* usuario y permisos */
    char                 comm[16];    /* nombre del programa */
    /* … y varios cientos de campos más … */
};

Un proceso, desde el punto de vista del kernel, es una de estas estructuras. Crear un proceso es reservar una y completarla; finalizarlo es liberarla. Los programas no pueden leer la memoria del kernel, pero este publica parte del contenido de sus tablas en forma de archivos. El siguiente comando muestra varios campos del registro de la shell actual, cuyo PID es $$:

terminal
$ grep -E '^(Name|State|Pid|PPid|Uid|VmRSS):' /proc/$$/status
Name:	bash
State:	S (sleeping)
Pid:	3120
PPid:	3098
Uid:	1000	1000	1000	1000
VmRSS:	    5632 kB

Cada línea corresponde a un campo de la estructura: Name a comm, State a __state, PPid al identificador de parent, Uid a cred y VmRSS, la memoria física que usa, a mm. Un detalle revelador: la shell figura como sleeping, dormida, porque en ese instante está esperando a que termine el grep que ella misma lanzó.

Estos archivos no existen en ningún disco. El kernel genera su contenido en el momento en que alguien los lee, a partir de sus tablas. Así obtienen sus datos ps, top y free, y así los obtendremos nosotros en el tema 31.

6. Dónde vive: en el disco

Antes de ejecutarse, el kernel es un archivo. En una instalación de Linux se encuentra en el directorio /boot, junto con algunos archivos auxiliares:

terminal
$ ls -lh /boot | grep $(uname -r)
-rw-r--r-- 1 root root 284K sep 12 09:14 config-6.8.0-45-generic
-rw-r--r-- 1 root root  68M sep 20 08:02 initrd.img-6.8.0-45-generic
-rw------- 1 root root 8,6M sep 12 09:14 System.map-6.8.0-45-generic
-rw------- 1 root root  15M sep 12 09:14 vmlinuz-6.8.0-45-generic
ArchivoContenido
vmlinuz-…El kernel. Es la imagen ejecutable, comprimida; de ahí la z final. El cargador de arranque la lleva a memoria.
initrd.img-…Un pequeño sistema de archivos temporal que el kernel usa durante el arranque. Lo vemos en el tema 21.
config-…Las miles de opciones con que se compiló este kernel: qué funciones incluye y cuáles no.
System.map-…La tabla de símbolos del kernel: el nombre y la dirección de cada una de sus funciones.

Quince megabytes no alcanzan para contener los drivers de todo el hardware existente, y tampoco tendría sentido cargarlos en memoria si no se usan. Por eso la mayor parte del kernel de Linux se distribuye en módulos: archivos de código que el kernel incorpora en marcha, solo cuando hacen falta. Están en /lib/modules:

terminal
$ ls /lib/modules/$(uname -r)/kernel
arch  block  crypto  drivers  fs  kernel  lib  mm  net  sound
$ find /lib/modules/$(uname -r) -name '*.ko*' | wc -l    # módulos disponibles
6213
$ lsmod | head -5                                         # módulos cargados ahora
Module                  Size  Used by
nvme                   61440  2
nvme_core             212992  3 nvme
snd_hda_intel          61440  4
iwlwifi               598016  1 iwlmvm

Hay miles de módulos disponibles y solo unas decenas cargados: los que corresponden al hardware de este equipo. Entre ellos está nvme, el driver del disco que encontramos en el tema 8. Los nombres de los subdirectorios reflejan los subsistemas de la sección 4: fs para los sistemas de archivos, mm para la memoria, net para la red, drivers para los dispositivos.

Un módulo cargado no es un programa aparte: pasa a formar parte del kernel, con sus mismos privilegios. Volvemos sobre ellos en el tema 24, al comparar los diseños de kernel.

En WSL

El directorio /boot está vacío y hay pocos módulos, o ninguno: el kernel lo proporciona Windows desde fuera de la distribución, y viene con casi todo incorporado. Los comandos de las secciones siguientes sí funcionan.

7. Dónde vive: en la memoria

Una vez cargado, el kernel ocupa una parte de la memoria física y no la abandona. Pero la pregunta interesante es en qué direcciones se lo encuentra, y la respuesta es una de las decisiones de diseño más ingeniosas del sistema.

Cada proceso tiene su propio espacio de direcciones, como vimos con ilusion.c en el tema 2. En Linux, ese espacio se divide en dos mitades:

División del espacio de direcciones de un proceso en x86-64: espacio de kernel arriba y espacio de usuario abajo 0xffffffffffffffff 0xffff800000000000 0x00007fffffffffff 0x0000000000000000 Espacio de kernel el mismo en todos los procesos direcciones no válidas Espacio de usuario propio de cada proceso pila bibliotecas heap · datos · código
El kernel está presente en el espacio de direcciones de todos los procesos, pero solo es accesible en modo kernel.

Las direcciones que hemos visto hasta ahora confirman el esquema: el código de nuestros programas aparecía en torno a 0x55… o 0x5d…, las bibliotecas en 0x79… y la pila en 0x7ffc…, justo por debajo del límite superior del espacio de usuario. Las funciones del kernel están en el otro extremo:

terminal
$ sudo grep -w __x64_sys_write /proc/kallsyms
ffffffff9a4b2c10 T __x64_sys_write

El archivo /proc/kallsyms lista los símbolos del kernel en ejecución. La función que atiende la llamada al sistema write está en una dirección que empieza por ffffffff, en lo más alto del espacio de direcciones. Sin sudo, el kernel muestra ceros en lugar de las direcciones; en un equipo ARM, la función se llama __arm64_sys_write.

¿Por qué colocar el kernel dentro del espacio de direcciones de cada proceso? Por eficiencia. Cuando un programa hace una llamada al sistema, la CPU no necesita cambiar de espacio de direcciones: el código del kernel ya está ahí, a un salto de distancia. Y el kernel puede leer y escribir directamente la memoria del proceso que lo llamó, por ejemplo para copiar los datos de un read.

Que esté presente no significa que sea accesible. Las páginas del espacio de kernel están marcadas como exclusivas del modo privilegiado, y si un programa de usuario intenta leer una de esas direcciones, la CPU lo impide y el resultado es el conocido «Segmentation fault».

8. Versiones y dimensiones

El comando que más hemos usado tiene ahora una lectura completa:

terminal
$ uname -r
6.8.0-45-generic
$ cat /proc/version
Linux version 6.8.0-45-generic (buildd@lcy02-amd64-075) (x86_64-linux-gnu-gcc-13
(Ubuntu 13.2.0-23ubuntu4) 13.2.0, GNU ld 2.42) #45-Ubuntu SMP PREEMPT_DYNAMIC
ParteSignificado
6.8La versión del kernel publicada por el proyecto Linux. Sale una nueva cada dos meses, aproximadamente.
.0El nivel de correcciones sobre esa versión.
-45La revisión de la distribución: Ubuntu aplica sus propios parches y los numera.
genericLa variante: aquí, la de uso general. Hay otras para servidores en la nube o para tiempo real.

La segunda salida añade un dato que enlaza con el tema 9: el kernel se compiló con gcc y se enlazó con ld, las mismas herramientas que usamos para nuestros programas.

Algunas cifras ayudan a dimensionarlo. El código fuente de Linux supera los 35 millones de líneas, lo que lo convierte en uno de los proyectos de software más grandes que existen. Lo desarrollan miles de personas de cientos de empresas, coordinadas todavía por Linus Torvalds, y se publica bajo una licencia libre en kernel.org. Como dijimos en el tema 8, la mayor parte de ese código son drivers, y el kernel de un equipo concreto solo compila y carga una fracción pequeña.

9. Los hilos del kernel

Dijimos que el kernel no es un proceso, y hay que matizarlo. Aunque casi siempre trabaja en respuesta a un acontecimiento, tiene tareas de mantenimiento que no puede resolver en ese momento y que deben hacerse «cuando haya tiempo». Para ellas crea hilos del kernel: flujos de ejecución que el planificador trata igual que a los procesos, pero que se ejecutan íntegramente en modo kernel y no tienen programa de usuario detrás.

Aparecen en la lista de procesos. Todos descienden del proceso con PID 2, kthreadd:

terminal (extracto)
$ ps -e -o pid,ppid,comm | awk '$2 == 2'
     16       2 ksoftirqd/0
     18       2 migration/0
     52       2 kworker/0:1
    104       2 kswapd0
    412       2 jbd2/nvme0n1p2-8
    …
HiloDe qué se ocupa
ksoftirqd/NCompleta el trabajo que las interrupciones dejaron pendiente en el procesador N.
kworker/…Ejecuta tareas diferidas de cualquier parte del kernel; por ejemplo, escribir en disco los datos modificados.
kswapd0Libera memoria cuando escasea, descartando caché o intercambiando al disco.
migration/NMueve procesos de un procesador a otro para equilibrar la carga.
jbd2/…Mantiene el registro de transacciones de un sistema de archivos ext4.

Varios de ellos son los responsables de mecanismos del tema 7: los datos «sucios» de la caché de disco los escribe un kworker, y el intercambio lo decide kswapd. En la salida de ps aux se los reconoce porque su nombre aparece entre corchetes, como [kswapd0], y porque no consumen memoria de usuario.

10. Escuchar al kernel

El kernel no puede usar printf: no tiene terminal ni biblioteca de C. Para informar de lo que hace, escribe mensajes en una zona circular de su memoria, el búfer de mensajes. El comando dmesg lo muestra. Cada línea lleva delante los segundos transcurridos desde el arranque:

terminal (líneas recortadas)
$ sudo dmesg | head -3
[    0.000000] Linux version 6.8.0-45-generic (buildd@lcy02-amd64-075) …
[    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-6.8.0-45-generic root=UUID=… ro quiet
[    0.000000] KERNEL supported cpus:

$ sudo dmesg | tail -3              # tras conectar una memoria USB
[ 8123.401222] usb 1-2: new high-speed USB device number 7 using xhci_hcd
[ 8123.553871] usb-storage 1-2:1.0: USB Mass Storage device detected
[ 8124.580113] sd 2:0:0:0: [sdb] Attached SCSI removable disk

Las primeras líneas son lo primero que dijo el kernel al arrancar. Las últimas muestran la enumeración del tema 8 en directo: el kernel detecta el dispositivo, le asigna el driver usb-storage y lo registra como el disco sdb. Cuando algo falla con el hardware, dmesg es el primer lugar donde mirar.

Además de los mensajes, el kernel expone su estado interno en dos directorios que ya hemos usado sin presentarlos formalmente:

terminal
$ cat /proc/uptime               # segundos desde el arranque (y segundos de CPU ociosa)
8431.77 61210.42
$ cat /proc/loadavg              # carga media; procesos en ejecución/total; último PID
0.42 0.31 0.27 1/1187 9214

Les dedicamos el tema 31.

11. Programar dentro del kernel

El kernel es código C, pero se escribe en condiciones muy distintas a las de un programa común. Conocerlas ayuda a entender muchas decisiones de diseño:

Programa de usuarioCódigo del kernel
BibliotecasDispone de la biblioteca de C.No tiene ninguna. Usa sus propias funciones; por ejemplo, printk en lugar de printf.
Un acceso inválido a memoriaEl kernel finaliza el programa. El resto sigue.No hay nadie por encima que lo contenga. Puede detener el sistema.
PilaVarios megabytes.Unos pocos kilobytes, de tamaño fijo.
ConcurrenciaOpcional.Inevitable: el mismo código puede ejecutarse a la vez en varios procesadores y ser interrumpido en cualquier punto.
Depuracióngdb, strace, reiniciar el programa.Mucho más difícil: un fallo puede obligar a reiniciar la máquina.

La segunda fila es la decisiva. Cuando el kernel detecta en sí mismo un error del que no puede recuperarse, se detiene deliberadamente para no corromper datos. Es el kernel panic de Linux, equivalente a la «pantalla azul» de Windows. Si el error es menos grave y afecta solo a la operación en curso, el kernel emite un aviso llamado oops, finaliza el proceso implicado e intenta seguir.

De aquí se desprende un principio que ya vimos aplicado en el tema 10: todo lo que pueda hacerse fuera del kernel conviene hacerlo fuera. Cada línea de código que se añade al kernel es una línea que se ejecuta con todos los privilegios y cuyo fallo afecta a todo el sistema. Hasta dónde llevar ese principio es la discusión entre kernels monolíticos y microkernels, que abordamos en el tema 24.

12. Resumen

13. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Cuáles son las tres maneras de que el kernel pase a ejecutarse después del arranque?

Una llamada al sistema, cuando un programa le pide un servicio; una interrupción, cuando un dispositivo requiere atención; y una excepción, cuando la instrucción en curso no puede completarse, por ejemplo por un acceso inválido a memoria.

2. ¿Por qué se dice que el kernel no es un proceso?

Porque no tiene un flujo de ejecución propio que avance continuamente. Es un conjunto de rutinas que se ejecutan en respuesta a acontecimientos, casi siempre en nombre del proceso que estaba en la CPU. La excepción son los hilos del kernel, que realizan tareas de mantenimiento.

3. Un programa muestra user 0m0.1s y sys 0m4.0s. ¿Qué indica sobre lo que hace?

Que casi todo su tiempo de CPU lo consumió el kernel atendiendo sus pedidos: hace muchas llamadas al sistema, probablemente de entrada y salida, y muy poco cálculo propio. Reducir la cantidad de llamadas, por ejemplo agrupando los datos, lo aceleraría.

4. ¿Qué es task_struct y qué relación tiene con /proc/<pid>/status?

Es la estructura de C con la que el kernel de Linux representa a cada proceso: su identificador, su estado, su memoria, sus archivos, su dueño. El archivo status es una vista en texto de varios de esos campos, generada por el kernel en el momento de leerlo.

5. ¿Por qué el kernel de Linux usa módulos en lugar de incluir todos los drivers en vmlinuz?

Porque hay miles de drivers y cada equipo necesita solo unos pocos. Cargarlos todos desperdiciaría memoria y haría el kernel enorme. Con módulos, el kernel incorpora en marcha únicamente los que corresponden al hardware presente.

6. ¿Qué ventaja tiene que el kernel esté mapeado en el espacio de direcciones de cada proceso?

Una llamada al sistema no exige cambiar de espacio de direcciones: basta con cambiar de modo y saltar. Además, el kernel puede acceder directamente a la memoria del proceso que lo llamó. La protección se mantiene porque esas direcciones solo son accesibles en modo kernel.

7. En la salida de ps aparece [kswapd0]. ¿Qué es y por qué su nombre va entre corchetes?

Es un hilo del kernel, que se ocupa de liberar memoria cuando escasea. Va entre corchetes porque no tiene un programa de usuario ni una línea de comandos asociados: se ejecuta por completo dentro del kernel.

8. ¿Por qué un error de programación es más grave en un driver que en una aplicación?

La aplicación se ejecuta aislada en modo usuario: si falla, el kernel la finaliza y el resto del sistema continúa. El driver se ejecuta dentro del kernel, con acceso a todo; un error suyo puede corromper la memoria del kernel o provocar un kernel panic que detenga la máquina.

Para practicar

  1. Ejecuta tiempos en sus dos modos y calcula el costo por llamada al sistema en tu equipo. Luego modifica el modo pedir para escribir 4096 bytes por llamada y hacer mil veces menos llamadas. ¿Cómo cambia sys?
  2. Usa time con comandos reales y clasifícalos según predomine user o sys: ls -R /usr > /dev/null, sha256sum /boot/vmlinuz-* (con sudo), dd if=/dev/zero of=/dev/null bs=1 count=1000000.
  3. Lee /proc/self/status completo con cat. ¿De qué proceso es? Identifica al menos cinco campos y explica qué recurso describe cada uno.
  4. Con lsmod, busca los módulos de tu tarjeta de red y de tu tarjeta de sonido. Consulta uno con modinfo y localiza su archivo .ko.
  5. Ejecuta sudo dmesg -w, que queda esperando mensajes nuevos, y conecta o desconecta un dispositivo USB. Sal con Ctrl+C.
  6. Cuenta los hilos del kernel de tu sistema con ps -e -o ppid | grep -c -w 2. ¿Cuántos hay por cada procesador?