Modo usuario, modo núcleo y llamadas al sistema - Procesos e Hilos
Volver a Procesos e Hilos Bloque 2 - Estados y cambio de contexto - Tema 10 de 43

Tema 10

Modo usuario, modo núcleo y llamadas al sistema

Un proceso no puede acceder directamente al hardware ni a las estructuras del núcleo. Para pedir servicios al sistema operativo, usa las llamadas al sistema, Que cambian el procesador de modo usuario a modo núcleo. En este tema veras por que existen dos modos, Como funcióna una syscall, Como seguirlas con strace, y que cuesta cruzar esa frontera.

1. Por que existen dos modos

El procesador tiene (al menos) dos modos de ejecución: modo usuario y modo núcleo (También llamado modo supervisor o modo privilegiado). En modo usuario, el proceso puede ejecutar instrucciones normales pero no puede acceder al hardware ni a las estructuras del núcleo. En modo núcleo, el código del sistema operativo tiene acceso completo a todo el hardware y a toda la memoria.

La razon de esta separacion es la Protección. Si un programa de usuario pudiera ejecutar cualquier instruccion, podria leer la memoria de otros procesos, modificar las tablas de páginas, acceder a los discos directamente, o deshabilitar las interrupciones. Un error o un programa malicioso podria destruir el sistema. Al restringir al proceso a modo usuario, el núcleo mantiene el control: el proceso sólo puede hacer lo que el núcleo le permite, pidiendoselo a través de las llamadas al sistema.

El hardware hace cumplir esta separacion: el procesador tiene un bit (o varios) Que indica el modo actual. Ciertas instrucciones sólo se pueden ejecutar en modo núcleo; Sí se intentan en modo usuario, el procesador genera una excepcion (trap) y el núcleo termina el proceso. Las tablas de páginas también tienen bits de protección que impiden al modo usuario acceder a páginas del núcleo.

La frontera entre usuario y núcleo

La separacion entre modo usuario y modo núcleo es la barrera fundamental de seguridad del sistema operativo. Todo lo que un proceso puede hacer pasa por el núcleo, Que valida la peticion y la ejecuta en nombre del proceso. Es el principio de minimo privilegio: el proceso tiene sólo los permisos que necesita, y para lo demás pide al núcleo.

2. Modo usuario

En modo usuario, el proceso ejecuta su propio código: las instrucciones del programa compilado, las funciónes de la biblioteca de C, los calculos, las manipulaciones de datos. Tiene acceso a su propio espacio de direcciónes (Código, datos, heap, stack) pero no a la memoria del núcleo ni a la de otros procesos.

Las restricciones del modo usuario son:

A pesar de estas restricciones, el modo usuario es dónde pasa la mayor parte del tiempo de ejecución: los programás hacen calculos, procesan datos, renderizan interfaces, etc. Solo cuándo necesitan un servicio del sistema (leer un archivo, crear un proceso, enviar datos por la red) cruzan a modo núcleo.

3. Modo núcleo

En modo núcleo, el código del sistema operativo se ejecuta con privilegios completos. El núcleo puede:

El núcleo se ejecuta en modo núcleo en tres situaciones:

En todos los casos, el núcleo se ejecuta en el contexto del proceso actual (o en un contexto especial para interrupciones). Cuando termina, retorna al modo usuario, restaurando el estado del proceso.

4. Qué es una llamada al sistema

Una llamada al sistema (system call o syscall) es el mecanismo por el que un proceso en modo usuario pide un servicio al núcleo. Es la unica forma legitima de cruzar la frontera entre modo usuario y modo núcleo. Cada syscall tiene un número (identificador) y unos argumentos, y devuelve un resultado.

Cuando un programa en C llama a read(fd, buf, n), la biblioteca de C no lee el disco directamente: empaqueta los argumentos, ejecuta la instruccion syscall (en x86-64) con el número de la syscall read, y el núcleo toma el control, valida los argumentos, ejecuta la operación, y devuelve el resultado.

Las syscalls son la API del núcleo. Todo lo que un programa puede pedirle al sistema operativo (leer archivos, crear procesos, enviar datos por la red, asignar memoria, etc.) se hace a través de syscalls. Hay unas 300-400 syscalls en linux (el número exacto depende de la arquitectura y la version del núcleo).

La syscall es la puerta del núcleo

Sin las syscalls, un proceso en modo usuario no podria hacer nada que requiera privilegios: ni leer un archivo, ni crear un proceso, ni enviar un paquete de red. La syscall es la unica puerta, y el núcleo valida cada peticion antes de ejecutarla.

5. Cómo funcióna una syscall

El mecanismo de una syscall varia según la arquitectura. En x86-64 (la más comun), el proceso usa la instrucción syscall:

  1. El proceso pone el número de la syscall en el registro RAX: cada syscall tiene un número (por ejemplo, read es la 0, write es la 1, open es la 2).
  2. El proceso pone los argumentos en los registros RDI, RSI, RDX, R10, R8, R9: hasta 6 argumentos, en ese orden.
  3. El proceso ejecuta la instrucción syscall: el procesador cambia a modo núcleo, guarda el contador de programa (en RCX) y los flags (en R11), y salta al código del núcleo (el entry point, Que esta en el registro MSR_LSTAR).
  4. El núcleo identifica la syscall: lee RAX, busca en la tabla de syscalls la función correspondiente, y la llama con los argumentos de los registros.
  5. El núcleo ejecuta la operación: valida los argumentos, accede al hardware o a las estructuras internas, y produce un resultado.
  6. El núcleo devuelve el resultado: pone el valor de retorno en RAX (negativo sí hay error, con el código de error en errno).
  7. El núcleo ejecuta sysret: restaura RCX en el contador de programa y R11 en los flags, y vuelve a modo usuario.
  8. El proceso continua: desde la instruccion siguiente a syscall, con el resultado en RAX.

En arquitecturas más antiguas (x86 de 32 bits), se usaba la interrupción 0x80 (int $0x80) en lugar de la instrucción syscall. El mecanismo era más lento porque las interrupciones son más costosas que la instrucción syscall.

Flujo de una llamada al sistema en x86-64. El proceso pone el número en RAX y los argumentos en registros, ejecuta syscall, el núcleo identifica y ejecuta la operación, devuelve el resultado en RAX y vuelve a modo usuario con sysret. Modo usuario 1. RAX = Número de syscall 2. RDI, RSI, RDX = argumentos 3. syscall (instrucción) ... 8. resultado en RAX 9. Continuar ejecución Modo núcleo 4. identificar syscall (RAX) 5. ejecutar operación (validar, acceder HW) 6. resultado en RAX 7. sysret (volver a usuario) syscall sysret
Flujo de una syscall en x86-64. La instrucción syscall cambia a modo núcleo; sysret vuelve a modo usuario.

6. La tabla de syscalls

El núcleo mantiene una tabla de syscalls: un arreglo dónde cada entrada es un puntero a la función que implementa esa syscall. El número de la syscall es el índice en esa tabla. En x86-64, la tabla tiene unas 350 entradas.

Las syscalls más comunes son:

NumeroNombrePara que sirve
0readLeer de un descriptor de archivo.
1writeEscribir en un descriptor de archivo.
2openAbrir un archivo.
3closeCerrar un descriptor de archivo.
9mmapMapear memoria.
12brkCambiar el limite del monticulo.
39getpidObtener el PID del proceso.
56cloneCrear un proceso o hilo.
57forkCrear un proceso (envoltura de clone).
59execveEjecutar un programa.
60exitTerminar el proceso.
61wait4Esperar a un hijo.
62killEnviar una senal.

La tabla completa esta en el código fuente del núcleo (arch/x86/entry/syscalls/syscall_64.tbl) y se puede consultar con el comando ausyscall o en la documentacion del núcleo.

7. strace: seguir las syscalls

strace es la herramienta que permite ver todas las syscalls que hace un programa. Es indispensable para entender que hace un programa por debajo y para depurar problemás de rendimiento o de acceso a recursos.

terminal - strace basico

$ strace ls /tmp
execve("/usr/bin/ls", ["ls", "/tmp"], 0x7ffd... /* 45 vars */) = 0
brk(NULL)                          = 0x55a3e4c8a000
arch_prctl(0x3001, 0x7ffd...)      = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f...
access("/etc/ld.so.preload", R_OK)  = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_Size=...}) = 0
mmap(NULL, 16478, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f...
close(3)                           = 0
...
write(1, "archivo1  archivo2\n", 19) = 19
close(1)                           = 0
exit_group(0)                      = ?
+++ exited with 0 +++

Cada linea muestra una syscall, sus argumentos y el valor de retorno. Las syscalls que fallan muestran el error (por ejemplo, ENOENT). Esto permite ver exactamente que archivos abre un programa, Que memoria pide, Que senales envia, etc.

Las opciones más útiles de strace:

terminal - strace con resumen

$ strace -c ls /tmp
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0         1           read
  0.00    0.000000           0         1           write
  0.00    0.000000           0         8           openat
  0.00    0.000000           0         8           close
  0.00    0.000000           0         5           fstat
  0.00    0.000000           0         7           mmap
  0.00    0.000000           0         4           mprotect
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         2           set_tid_address
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000           0        42         1 total

8. La biblioteca de C y las syscalls

Los programás en C no llaman a las syscalls directamente. Usan las funciónes de la biblioteca de C (glibc), Que son envolturas (wrappers) alrededor de las syscalls. Por ejemplo, printf() llama internamente a write(), Que es una envoltura de la syscall write. malloc() llama a brk() o mmap(), Que son envolturas de las syscalls correspondientes.

La biblioteca de C ofrece varias ventajas sobre llamar a las syscalls directamente:

Sin embargo, a veces es necesario llamar a una syscall directamente, por ejemplo cuándo se quiere usar una syscall nueva que la biblioteca aun no soporta. En linux se puede usar la función syscall() de la biblioteca:

syscall_directa.c

#define _GNU_SOURCE
#include 
#include 
#include 

int main(void)
{
    /* Llamar a getpid via syscall() directamente */
    pid_t pid = (pid_t) syscall(SYS_getpid);
    printf("PID: %d\n", (int) pid);

    /* Llamar a write via syscall() directamente */
    const char msg[] = "Hola desde syscall directa\n";
    syscall(SYS_write, 1, msg, Sizeof msg - 1);

    return 0;
}

Esto es útil para syscalls nuevas (Como gettid en versiones antiguas de glibc) o para experimentar. En la práctica, la biblioteca de C es la forma recomendada de interactuar con el núcleo.

9. El costo de una syscall

Una syscall tiene un costo que se puede descomponer en varias partes:

Componentecosto tipicoPor que
Cambio de modo (syscall/sysret)~0.1-0.5 usCambio de anillo de privilegio, guardar/restaurar registros.
Validacion de argumentos~0.1-0.5 usEl núcleo valida que los punteros sean válidos, Que los descriptores existan, etc.
Ejecucion de la operaciónVariableDepende de la syscall: getpid es trivial, read de disco puede tardar milisegúndos.
Cambio de contexto (Sí se bloquea)~5-10 usSi la syscall bloquea al proceso (read en disco, sleep), se produce un cambio de contexto.

El costo base de una syscall (el cambio de modo) es de unas centenas de nanosegúndos. Pero el costo total depende de la operación: una syscall como getpid es casí instantanea (microsegúndos), mientras que una como read de disco puede tardar milisegúndos.

Por eso, la optimización más importante en programás que hacen muchas syscalls es reducir el número de syscalls: usar buferes grandes (para hacer fewer reads/writes), usar mmap en lugar de read/write, y usar vDSO Cuándo sea posible.

Las syscalls son la frontera de rendimiento

Cada syscall tiene un costo base de ~0.5 us. Un programa que hace millones de syscalls por segúndo (por ejemplo, un servidor web que hace read/write por cada conexion) puede gastar una fraccion significativa de su tiempo en cruzar la frontera usuario/Núcleo. Reducir el número de syscalls es una de las optimizaciones más efectivas.

10. vDSO: syscalls sin cambio de modo

Algunas syscalls son tan frecuentes y tan simples que el núcleo las optimiza con el vDSO (virtual dynamic shared object): una pequena biblioteca que el núcleo mapea en el espacio de direcciónes de cada proceso, y que contiene versiones de ciertas syscalls que se ejecutan en modo usuario, Sin cruzar a modo núcleo.

Las syscalls que se optimizan con vDSO son:

Estas syscalls se ejecutan leyendo datos que el núcleo actualiza en una página compartida (la vvar), Sin necesidad de cambiar de modo. Esto las hace mucho más rápidas: de ~0.5 us a ~0.01 us.

El vDSO aparece en /proc/PID/maps como [vdso]:

terminal - ver el vDSO en /proc

$ grep vdso /proc/self/maps
7ffff7fc8000-7ffff7fca000 r-xp 00000000 00:00 0 [vdso]

La biblioteca de C usa el vDSO automaticamente cuándo esta disponible. El programa no necesita hacer nada especial: Simplemente llama a gettimeofday() o clock_gettime() y la biblioteca usa el vDSO Sí puede.

11. Modo usuario y seguridad

La separación entre modo usuario y modo núcleo es la base de la seguridad del sistema operativo. El núcleo valida cada syscall antes de ejecutarla:

Si un proceso intenta hacer algo que no esta permitido (acceder a memoria del núcleo, ejecutar una instrucción privilegiada, abrir un archivo sin permiso), el núcleo genera una excepción (SIGSEGV, SIGILL, EACCES) y termina o rechaza la operación.

Esta validacion tiene un costo: cada syscall incluye varias comprobaciones. Pero es esencial para la seguridad: Sin ella, un programa malicioso podria tomar control del sistema.

12. Un programa que observa sus syscalls

El siguiente programa hace operaciónes simples y muestra como se pueden observar las syscalls que genera con strace:

observar_syscalls.c

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

int main(void)
{
    /* Abrir un archivo: syscall open */
    int fd = open("/etc/hostname", O_RDONLY);
    if (fd < 0) { perror("open"); return 1; }

    /* Leer: syscall read */
    char buf[256];
    ssize_t n = read(fd, buf, Sizeof buf - 1);
    if (n > 0) {
        buf[n] = 0;
        printf("Leido: %s", buf);
    }

    /* Cerrar: syscall close */
    close(fd);

    /* Obtener PID: syscall getpid */
    printf("PID: %d\n", (int) getpid());

    /* Escribir: syscall write (a través de printf) */
    /* Terminar: syscall exit */
    return 0;
}

Al ejecutarlo con strace, veras las syscalls que genera cada operación:

terminal - strace del programa

$ strace ./observar_syscalls
execve("./observar_syscalls", ...) = 0
...
openat(AT_FDCWD, "/etc/hostname", O_RDONLY) = 3
read(3, "mi-equipo\n", 255) = 10
write(1, "Leido: mi-equipo\n", 17) = 17
close(3) = 0
getpid() = 1234
write(1, "PID: 1234\n", 10) = 10
exit_group(0) = ?
+++ exited with 0 +++

Cada linea de strace corresponde a una syscall. Puedes ver que open se traduce a openat (la version moderna), Que printf genera una write, y que return 0 en main genera exit_group.

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. Por que existen dos modos de ejecución?

Para proteger al sistema: en modo usuario, el proceso no puede acceder al hardware ni a la memoria del núcleo. Solo el núcleo (en modo núcleo) tiene acceso completo.

2. Qué es una llamada al sistema?

Es el mecanismo por el que un proceso en modo usuario pide un servicio al núcleo. Es la unica forma legitima de cruzar la frontera entre modo usuario y modo núcleo.

3. Cómo se identifica una syscall en x86-64?

Por un número que se pone en el registro RAX. Los argumentos van en RDI, RSI, RDX, R10, R8, R9. La instrucción syscall cambia a modo núcleo.

4. Para que sirve strace?

Para ver todas las syscalls que hace un programa, con sus argumentos y valores de retorno. Es indispensable para depurar y entender el comportamiento de un programa.

5. Qué es vDSO?

Una biblioteca que el núcleo mapea en el espacio de direcciónes de cada proceso, con versiones de ciertas syscalls (gettimeofday, clock_gettime) Que se ejecutan en modo usuario sin cambio de modo.

6. Por que la biblioteca de C es preferible a llamar a syscalls directamente?

Por portabilidad, manejo de errores (errno), bufer (printf), y compatibilidad. Las syscalls directas se usan sólo para syscalls nuevas o experimentacion.

7. Qué costo tiene una syscall?

El cambio de modo cuesta ~0.1-0.5 us. El costo total depende de la operación: getpid es trivial, read de disco puede tardar milisegúndos.

8. Qué valida el núcleo en cada syscall?

Los punteros (Que apunten a memoria valida del proceso), los permisos (Que el proceso pueda acceder al recurso), y los limites (tamano de bufer, Número de descriptores, etc.).

9. Qué es exit_group?

La syscall que termina un proceso y todos sus hilos. Es la que se llama cuándo main retorna o se llama a exit().

10. Cómo se puede llamar a una syscall directamente en C?

Usando la función syscall() de la biblioteca de C, con el número de la syscall (SYS_xxx) y los argumentos. Ejemplo: syscall(SYS_getpid).

Para prácticar

  1. Ejecuta strace ls /tmp y observa las syscalls que hace.
  2. Usa strace -c para ver un resumen de las syscalls de un programa.
  3. Usa strace -e trace=file para ver sólo las syscalls de archivos.
  4. Compila y ejecuta observar_syscalls.c con strace y compara la salida con el código.
  5. Usa strace -T para ver cuánto tarda cada syscall.
  6. Investiga la syscall vDSO con strace -e trace=gettimeofday ./programa y observa que no aparece (porque usa vDSO).