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 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:
- No puede ejecutar instrucciones privilegiadas: Instrucciones que controlan el hardware (deshabilitar interrupciones, cambiar tablas de páginas, acceder a puertos de E/S) generan una excepcion.
- No puede acceder a la memoria del núcleo: las páginas del núcleo tienen el bit de usuario desactivado en las tablas de páginas, por lo que cualquier intento de acceso genera una violacion de segmento.
- No puede acceder a la memoria de otros procesos: cada proceso tiene su propio espacio de direcciónes.
- No puede manipular directamente los dispositivos: el acceso a discos, red, terminal, etc. requiere pasar por el núcleo.
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:
- Acceder a toda la memoria: incluyendo la memoria de todos los procesos y las estructuras internas del núcleo.
- Ejecutar instrucciones privilegiadas: cambiar tablas de páginas, deshabilitar interrupciones, acceder a puertos de E/S.
- Manipular el hardware: leer y escribir en los registros de los dispositivos, controlar el DMA, programar temporizadores.
- Gestionar procesos: crear, destruir, planificar, cambiar de contexto.
El núcleo se ejecuta en modo núcleo en tres situaciones:
- Cuando un proceso hace una llamada al sistema: el proceso ejecuta una instruccion especial (syscall) Que cambia a modo núcleo y salta al código del núcleo.
- Cuando ocurre una interrupcion: un dispositivo (disco, red, temporizador) interrumpe al procesador, Que salta al manejador de interrupciones del núcleo.
- Cuando ocurre una excepcion: un error (Division por cero, Violacion de segmento, Página no presente) hace que el procesador salte al manejador de excepciones del núcleo.
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).
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:
- 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).
- El proceso pone los argumentos en los registros RDI, RSI, RDX, R10, R8, R9: hasta 6 argumentos, en ese orden.
- 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). - 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.
- El núcleo ejecuta la operación: valida los argumentos, accede al hardware o a las estructuras internas, y produce un resultado.
- 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).
- El núcleo ejecuta
sysret: restaura RCX en el contador de programa y R11 en los flags, y vuelve a modo usuario. - 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.
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:
| Numero | Nombre | Para que sirve |
|---|---|---|
| 0 | read | Leer de un descriptor de archivo. |
| 1 | write | Escribir en un descriptor de archivo. |
| 2 | open | Abrir un archivo. |
| 3 | close | Cerrar un descriptor de archivo. |
| 9 | mmap | Mapear memoria. |
| 12 | brk | Cambiar el limite del monticulo. |
| 39 | getpid | Obtener el PID del proceso. |
| 56 | clone | Crear un proceso o hilo. |
| 57 | fork | Crear un proceso (envoltura de clone). |
| 59 | execve | Ejecutar un programa. |
| 60 | exit | Terminar el proceso. |
| 61 | wait4 | Esperar a un hijo. |
| 62 | kill | Enviar 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.
$ 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:
-e trace=read,write: filtrar por syscalls especÃficas.-e trace=file: Sólo syscalls relacionadas con archivos.-e trace=process: Sólo syscalls relacionadas con procesos.-e trace=network: Sólo syscalls de red.-c: mostrar un resumen con el tiempo y la cantidad de cada syscall.-p PID: adjuntarse a un proceso en ejecución.-f: seguir a los hijos (fork).-T: mostrar el tiempo que tardo cada syscall.-o archivo: guardar la salida en un archivo.
$ 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:
- Portabilidad: la misma función de C Funcióna en distintos sistemás operativos, aunque las syscalls subyacentes sean distintas.
- Manejo de errores: la biblioteca convierte los códigos de error del núcleo (Números negativos) en errno y devuelve -1, Siguiendo la convencion de C.
- Bufer: Funciónes como printf usan un bufer en espacio de usuario para reducir el número de syscalls (write).
- Compatibilidad: la biblioteca puede emular funciónalidades que el núcleo no ofrece 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:
#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:
| Componente | costo tipico | Por que |
|---|---|---|
| Cambio de modo (syscall/sysret) | ~0.1-0.5 us | Cambio de anillo de privilegio, guardar/restaurar registros. |
| Validacion de argumentos | ~0.1-0.5 us | El núcleo valida que los punteros sean válidos, Que los descriptores existan, etc. |
| Ejecucion de la operación | Variable | Depende de la syscall: getpid es trivial, read de disco puede tardar milisegúndos. |
| Cambio de contexto (Sí se bloquea) | ~5-10 us | Si 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.
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:
- gettimeofday: obtener la hora actual.
- clock_gettime: obtener el tiempo con alta resolución.
- getcpu: obtener el CPU actual y el nodo NUMA.
- time: obtener el tiempo en segúndos.
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]:
$ 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:
- Validacion de punteros: el núcleo verifica que los punteros que el proceso pasa como argumentos apunten a memoria valida del proceso, no a memoria del núcleo o de otros procesos.
- Validacion de permisos: el núcleo verifica que el proceso tenga permiso para acceder al recurso (archivo, dispositivo, proceso).
- Validacion de limites: el núcleo verifica que los valores esten dentro de los limites permitidos (tamano de bufer, Número de descriptores, etc.).
- Aislamiento: el núcleo asegura que un proceso no pueda ver ni modificar la memoria de otros procesos ni del núcleo.
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:
#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:
$ 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
- El procesador tiene dos modos: usuario (restringido) y núcleo (privilegiado).
- La separación protege al sistema: un proceso en modo usuario no puede acceder al hardware ni a la memoria del núcleo.
- Las llamadas al sistema (syscalls) son la unica forma de cruzar de modo usuario a modo núcleo.
- Una syscall pone el número en RAX y los argumentos en registros, ejecuta la instrucción syscall, el núcleo ejecuta la operación y devuelve el resultado con sysret.
- Hay unas 350 syscalls en linux, numeradas en una tabla.
- strace permite ver todas las syscalls que hace un programa.
- La biblioteca de C envuelve las syscalls, anadiendo portabilidad, manejo de errores y bufer.
- El costo base de una syscall es ~0.5 us; el costo total depende de la operación.
- vDSO permite ejecutar algunas syscalls (gettimeofday, clock_gettime) en modo usuario sin cambio de modo.
- El núcleo valida cada syscall para garantizar la seguridad: punteros, permisos, limites.
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
- Ejecuta strace ls /tmp y observa las syscalls que hace.
- Usa strace -c para ver un resumen de las syscalls de un programa.
- Usa strace -e trace=file para ver sólo las syscalls de archivos.
- Compila y ejecuta observar_syscalls.c con strace y compara la salida con el código.
- Usa strace -T para ver cuánto tarda cada syscall.
- Investiga la syscall vDSO con strace -e trace=gettimeofday ./programa y observa que no aparece (porque usa vDSO).