1. Abrir la caja
El bloque 2 terminó con una observación: en cada tema de hardware apareció alguien que organiza, protege y decide. Ese alguien es el sistema operativo, y a partir de ahora lo estudiamos por dentro.
En el tema 1 distinguimos dos sentidos de la expresión. En sentido estricto, el sistema operativo es el kernel. En sentido amplio, es todo lo que viene instalado para que el equipo sea utilizable. Un kernel solo no sirve de nada: no ofrece ninguna forma de escribir un comando, de copiar un archivo o de conectarse a una red. Para eso hacen falta otros programas, que se ejecutan sobre el kernel y que, junto con él, forman el sistema completo.
Este tema recorre esas piezas. La división más importante entre ellas ya la conocemos:
- El espacio de kernel es el kernel mismo, ejecutándose en el modo privilegiado de la CPU.
- El espacio de usuario es todo lo demás: programas comunes, en modo restringido, que solo pueden pedir servicios al kernel mediante llamadas al sistema.
Una idea que conviene asumir pronto: la mayor parte de lo que llamamos «el sistema operativo» está en el espacio de usuario y, desde el punto de vista del kernel, no se diferencia en nada de un programa escrito por ti.
2. El mapa de componentes
| Componente | Qué es | Dónde se ejecuta |
|---|---|---|
| Kernel | El núcleo: administra el hardware y aísla los programas. | Espacio de kernel |
| Bibliotecas del sistema | Código compartido que da a los programas una interfaz cómoda sobre el kernel. | Espacio de usuario, dentro de cada programa |
| Shell | El programa que recibe órdenes de la persona y las lleva a cabo. | Espacio de usuario |
| Utilidades | Programas pequeños para las tareas básicas. | Espacio de usuario |
| Servicios | Programas que funcionan en segundo plano, sin intervención de nadie. | Espacio de usuario |
| Entorno gráfico | Ventanas, escritorio y manejo del ratón. | Espacio de usuario, en Linux |
3. El kernel
El kernel es el único componente imprescindible y el único que se ejecuta con privilegios. Todo lo que exige controlar el hardware o arbitrar entre programas pasa por él. Internamente se divide en subsistemas, que se corresponden con los servicios del tema 1:
- Gestión de procesos: crea y finaliza los procesos, y decide cuál usa la CPU en cada momento.
- Gestión de memoria: da a cada proceso su espacio de direcciones y administra la memoria física, la caché de disco y el intercambio.
- Sistemas de archivos: convierte los bloques de un disco en archivos y directorios.
- Red: implementa los protocolos de comunicación.
- Drivers: un programa por cada tipo de controlador de hardware, como vimos en el tema 8.
En disco, el kernel de Linux es un archivo como cualquier otro, que el cargador de arranque lleva a memoria al encender el equipo:
$ ls -lh /boot/vmlinuz-$(uname -r)
-rw------- 1 root root 15M sep 12 09:14 /boot/vmlinuz-6.8.0-45-generic
Quince megabytes para el programa que gobierna toda la máquina. En WSL este archivo no está visible, porque el kernel lo aporta Windows. Dedicamos el tema 11 entero al kernel; aquí seguimos con las piezas que lo rodean.
4. Las bibliotecas del sistema
Un programa podría pedir todo al kernel directamente, pero sería incómodo. La interfaz del kernel es mínima a propósito: ofrece, por ejemplo, escribir bytes en un descriptor, pero no dar formato a un número ni buscar un texto dentro de otro. Las bibliotecas del sistema cubren esa distancia. Son código ya escrito y probado que los programas incorporan en lugar de reescribirlo.
La principal es la biblioteca de C, que en casi todas las distribuciones de Linux es glibc. Cumple tres funciones:
- Envuelve las llamadas al sistema. Funciones como
write,openogetpidson pequeños envoltorios que preparan los registros y ejecutan la instrucción que cruza al kernel. Permiten hacer una llamada al sistema como si fuera una función común de C. - Añade funcionalidad que no necesita al kernel.
strlen,qsortosqrtse resuelven por completo en espacio de usuario. - Combina ambas cosas.
printfda formato al texto por su cuenta y luego usawrite;mallocadministra la memoria que pidió antes al kernel en bloques grandes.
En el tema 9 vimos que esta biblioteca es un archivo aparte, libc.so.6, que se carga junto con cada programa. Prácticamente todo lo que se ejecuta en el sistema depende de ella, incluidos los programas escritos en otros lenguajes, cuyos intérpretes están hechos en C:
$ ldd /bin/bash | grep libc
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007a4e1c200000)
$ ldd $(which python3) | grep libc
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000073d5e9a00000)
Aunque no está en el kernel, la biblioteca de C se considera parte del sistema operativo porque es la cara que el sistema presenta a los programadores: quien escribe un programa casi nunca trata con el kernel, sino con ella. La estudiamos en el tema 15.
5. La shell
La shell, o intérprete de comandos, es el programa que conversa con la persona. Su tarea es un bucle: mostrar el prompt, leer una línea, entender qué se pide, pedir al kernel que ejecute el programa correspondiente, esperar a que termine y volver a empezar. El nombre significa «caparazón»: es la capa externa que rodea al kernel, que significa «núcleo».
Lo que la hace interesante para este curso es una decisión de diseño de Unix que ya mencionamos en el tema 3: la shell es un programa común. No tiene privilegios ni está integrada en el kernel. Por eso existen varias, y cada usuario elige la suya:
$ echo $SHELL # la shell configurada para tu usuario
/bin/bash
$ cat /etc/shells # las shells instaladas
/bin/sh
/bin/bash
/usr/bin/bash
/bin/dash
/usr/bin/dash
$ ps -p $$ # $$ es el PID de la shell actual
PID TTY TIME CMD
3120 pts/0 00:00:00 bash
El último comando lo muestra con claridad: la shell es un proceso más, con su PID, igual que los programas que lanzamos desde ella. Recuerda que en el tema 4 el «padre» de nuestro programa entorno era precisamente este proceso.
Casi todos los comandos que escribes son programas independientes, pero unos pocos los resuelve la shell por sí misma. Se llaman comandos internos, y type permite distinguirlos:
$ type cd pwd cp
cd is a shell builtin
pwd is a shell builtin
cp is /usr/bin/cp
cd tiene que ser interno por una razón que entenderemos del todo al estudiar los procesos: el directorio actual es una propiedad de cada proceso, y un programa externo solo podría cambiar el suyo, no el de la shell.
6. Las utilidades
Las utilidades son los programas pequeños que acompañan al sistema para las tareas básicas: listar archivos, copiarlos, ver los procesos, configurar la red. Cada comando que no es interno de la shell es una utilidad, y cada utilidad es un archivo ejecutable ELF como los del tema 9:
$ ls /usr/bin | wc -l # cuántos programas hay en ese directorio
1648
$ file /usr/bin/ls
/usr/bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, …
| Familia | Ejemplos | Qué hacen |
|---|---|---|
| Archivos | ls, cp, mv, rm, mkdir, chmod | Manipular archivos y directorios. |
| Texto | cat, grep, sort, head, wc | Leer, filtrar y transformar texto. |
| Procesos | ps, top, kill | Observar y controlar los procesos. |
| Sistema | mount, lsblk, dmesg, free | Consultar y configurar el propio sistema. |
| Red | ip, ping, ss | Configurar y diagnosticar la red. |
Dos observaciones sobre las utilidades. La primera es que siguen la filosofía de Unix que vimos en el tema 3: cada una hace una sola cosa, y se combinan mediante tuberías para tareas más complejas. La segunda es que no tienen ningún poder especial. ps no «ve» los procesos por ser parte del sistema: le pregunta al kernel, igual que podría hacerlo un programa tuyo. De hecho, el proyecto final de este curso consiste en escribir una utilidad de ese tipo.
7. Los servicios
No todos los programas del sistema esperan a que alguien los invoque. Los servicios, llamados tradicionalmente demonios (daemons) en Unix, se inician con el equipo y permanecen en segundo plano, sin terminal ni usuario delante, atendiendo tareas que deben estar siempre disponibles. Muchos se reconocen porque su nombre termina en d.
$ systemctl list-units --type=service --state=running --no-pager
UNIT LOAD ACTIVE SUB DESCRIPTION
cron.service loaded active running Regular background program processing daemon
dbus.service loaded active running D-Bus System Message Bus
NetworkManager.service loaded active running Network Manager
systemd-journald.service loaded active running Journal Service
systemd-logind.service loaded active running User Login Management
systemd-udevd.service loaded active running Rule-based Manager for Device Events and Files
…
| Servicio | De qué se ocupa |
|---|---|
systemd-journald | Recoge y guarda los mensajes de registro del kernel y de los demás servicios. |
systemd-udevd | Reacciona cuando el kernel detecta un dispositivo nuevo: crea su entrada en /dev y aplica la configuración. |
systemd-logind | Gestiona los inicios de sesión de los usuarios. |
NetworkManager | Configura las conexiones de red. |
cron | Ejecuta tareas programadas a horas determinadas. |
sshd | Acepta conexiones remotas de terminal. |
Hay un servicio que ocupa un lugar aparte: el que los inicia a todos. Es el primer proceso que el kernel crea al arrancar, y por eso tiene siempre el PID 1. En la mayoría de las distribuciones actuales es systemd:
$ ps -p 1 -o pid,comm
PID COMMAND
1 systemd
Lo estudiamos en el tema 22, dentro del bloque sobre el arranque. Los servicios muestran una tendencia del diseño de sistemas operativos: sacar del kernel todo lo que pueda hacerse fuera. Configurar la red o registrar mensajes son tareas del sistema, pero no necesitan privilegios de hardware, así que se resuelven con programas comunes. Si uno falla, se reinicia sin afectar al resto.
8. La interfaz gráfica
Las ventanas, los menús y el escritorio son, en Linux, otro conjunto de programas de usuario, organizados a su vez en capas: un servidor gráfico (Wayland o el más antiguo X11) que reparte la pantalla y distribuye los eventos del teclado y del ratón, y sobre él un entorno de escritorio, como GNOME o KDE, que aporta el aspecto, los paneles y las aplicaciones básicas.
Como es un componente separado, puede cambiarse por otro o no instalarse en absoluto. Un servidor suele prescindir de él, y sigue teniendo un sistema operativo completo. Es la demostración práctica de lo dicho en el tema 1: la interfaz gráfica no es la parte esencial.
Otros sistemas tomaron una decisión distinta. En Windows, parte del sistema de ventanas se ejecuta dentro del kernel, lo que mejora el rendimiento gráfico a costa de que un fallo en esa parte pueda detener el sistema completo. Es un ejemplo del equilibrio entre rendimiento y protección del tema 2.
9. De dónde viene cada pieza
En Linux hay una circunstancia poco habitual: los componentes del sistema operativo los desarrollan proyectos independientes, con equipos y calendarios propios. La distribución es quien los reúne, los prueba en conjunto y los entrega como un producto.
| Componente | Proyecto habitual | Paquete en Ubuntu |
|---|---|---|
| Kernel | Linux | linux-image-… |
| Biblioteca de C | GNU C Library (glibc) | libc6 |
| Shell | GNU Bash | bash |
| Utilidades de archivos y texto | GNU Coreutils | coreutils |
| Utilidades de procesos | procps | procps |
| Utilidades del sistema | util-linux | util-linux, mount |
| Inicio y servicios | systemd | systemd |
| Compilador | GNU Compiler Collection | gcc |
El gestor de paquetes, que es en sí otro componente del sistema, sabe a qué paquete pertenece cada archivo instalado:
$ dpkg -S $(which ls) $(which ps) $(which mount)
coreutils: /usr/bin/ls
procps: /usr/bin/ps
mount: /usr/bin/mount
En Fedora, el comando equivalente es rpm -qf. La tabla anterior explica también una discusión de nombres que quizá hayas encontrado: como buena parte del espacio de usuario proviene del proyecto GNU, algunos prefieren llamar al conjunto «GNU/Linux».
10. Los mismos componentes en otros sistemas
La división en componentes no es propia de Linux. Todos los sistemas operativos de propósito general tienen las mismas piezas, con otros nombres:
| Componente | Ubuntu | Android | macOS | Windows |
|---|---|---|---|---|
| Kernel | Linux | Linux | XNU | NT |
| Biblioteca del sistema | glibc | Bionic | libSystem | ntdll, kernel32 |
| Shell | Bash | mksh | zsh | PowerShell, cmd |
| Inicio y servicios | systemd | init propio | launchd | Administrador de servicios |
| Entorno gráfico | GNOME | SurfaceFlinger | Aqua | Escritorio de Windows |
Las dos primeras columnas son la mejor ilustración de este tema. Ubuntu y Android usan el mismo kernel y casi nada más en común: Android reemplaza la biblioteca de C, la shell, el sistema de inicio y el entorno gráfico por los suyos. Un programa compilado para Ubuntu no funciona en Android, aunque el procesador sea el mismo y el kernel también, porque depende de una biblioteca que allí no existe. Lo que para un usuario define «un sistema operativo» está, en gran medida, en el espacio de usuario.
11. Las interfaces entre componentes
Que las piezas sean independientes y reemplazables es posible porque entre ellas hay interfaces bien definidas: acuerdos sobre cómo se pide algo y qué se recibe. En un sistema operativo hay tres que conviene tener presentes:
| Interfaz | Entre quiénes | En qué consiste |
|---|---|---|
| De usuario | La persona y el sistema | La línea de comandos o el entorno gráfico. |
| De programación (API) | El programa y las bibliotecas | Las funciones disponibles, con sus parámetros y resultados. La norma POSIX define la de los sistemas tipo Unix. |
| De llamadas al sistema | El espacio de usuario y el kernel | El conjunto de servicios del kernel y la forma exacta de solicitarlos. |
La tercera es la frontera que da nombre a este bloque, y tiene una propiedad notable en Linux: es extraordinariamente estable. Los desarrolladores del kernel se imponen la regla de no romper nunca los programas existentes, de modo que un ejecutable compilado hace veinte años sigue funcionando sobre un kernel actual. Las interfaces de arriba, en cambio, cambian con más libertad.
Las interfaces explican también por qué se puede cambiar una pieza sin tocar las demás. Mientras el kernel mantenga sus llamadas al sistema, puede reescribirse por dentro; mientras la biblioteca mantenga su API, los programas no se enteran de qué kernel hay debajo. Es la idea de abstracción del tema 2, aplicada a la construcción del propio sistema operativo.
12. Un comando los atraviesa a todos
Para ver los componentes colaborando, sigamos lo que ocurre al escribir ls y pulsar Enter. Junto a cada paso se indica el componente que actúa:
- El emulador de terminal recibe las teclas y se las entrega a la shell. entorno gráfico
- La shell lee la línea, comprueba que
lsno es un comando interno y lo busca en los directorios del sistema hasta encontrar/usr/bin/ls. shell - La shell pide al kernel que cree un proceso nuevo y que ejecute en él ese archivo. shell → kernel
- El kernel carga el ejecutable, como vimos en el tema 9, y el enlazador dinámico incorpora la biblioteca de C. kernel
lsempieza a ejecutarse y llama a funciones de la biblioteca para abrir el directorio actual y leer su contenido. utilidad → biblioteca- La biblioteca hace las llamadas al sistema correspondientes. biblioteca → kernel
- El kernel resuelve el pedido: el subsistema de archivos localiza el directorio y, si sus datos no están en la caché, el driver del disco los trae. kernel
lsordena los nombres, los escribe en su salida y termina. El texto llega al emulador de terminal, que lo dibuja. utilidad- El kernel avisa a la shell de que el proceso terminó, y la shell muestra de nuevo el prompt. kernel → shell
La parte central del recorrido, entre los pasos 4 y 8, se puede observar. Le pedimos a strace que muestre solo cuatro tipos de llamada:
$ strace -e trace=execve,openat,getdents64,write ls
execve("/usr/bin/ls", ["ls"], 0x7ffc1a2b3c40 /* 36 vars */) = 0
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
…
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
getdents64(3, 0x5f0a3c8e1b40 /* 5 entries */, 32768) = 144
getdents64(3, 0x5f0a3c8e1b40 /* 0 entries */, 32768) = 0
write(1, "componentes.sh version version.c\n", 35) = 35
+++ exited with 0 +++
Las primeras líneas son la carga del programa y de la biblioteca. Las resaltadas son el trabajo propio de ls, y caben en cuatro llamadas: abrir el directorio actual, pedir sus entradas con getdents64 —la primera vez devuelve cinco, la segunda ninguna, señal de que no hay más— y escribir el resultado. Las cinco entradas son los tres archivos del ejemplo más las dos que todo directorio contiene, . y ... Reconstruimos este recorrido con mucho más detalle en el tema 32.
13. Inventario de tu sistema
El siguiente script reúne en una pantalla los componentes que hemos visto, tal como están en tu equipo:
#!/bin/bash
# Inventario de los componentes del sistema operativo.
echo "Kernel: $(uname -sr)"
echo "Biblioteca C: $(ldd --version | head -1)"
echo "Shell: $SHELL ($BASH_VERSION)"
echo "Utilidades: $(ls /usr/bin | wc -l) programas en /usr/bin"
echo "Proceso 1: $(ps -p 1 -o comm=)"
echo "Procesos: $(ps -e --no-headers | wc -l) en ejecucion"
echo "Escritorio: ${XDG_CURRENT_DESKTOP:-ninguno}"
$ chmod +x componentes.sh
$ ./componentes.sh
Kernel: Linux 6.8.0-45-generic
Biblioteca C: ldd (Ubuntu GLIBC 2.39-0ubuntu8.3) 2.39
Shell: /bin/bash (5.2.21(1)-release)
Utilidades: 1648 programas en /usr/bin
Proceso 1: systemd
Procesos: 287 en ejecucion
Escritorio: ubuntu:GNOME
Cada línea corresponde a un componente con su propia versión, porque cada uno proviene de un proyecto distinto. Un programa en C puede comprobar esa independencia consultando por separado a la biblioteca y al kernel:
#include <stdio.h>
#include <gnu/libc-version.h>
#include <sys/utsname.h>
int main(void)
{
struct utsname kernel;
if (uname(&kernel) == -1) {
perror("uname");
return 1;
}
printf("Biblioteca de C: glibc %s\n", gnu_get_libc_version());
printf("Kernel: %s %s\n", kernel.sysname, kernel.release);
return 0;
}
$ gcc -Wall -Wextra -g version.c -o version
$ ./version
Biblioteca de C: glibc 2.39
Kernel: Linux 6.8.0-45-generic
$ strace -e trace=uname ./version > /dev/null
uname({sysname="Linux", nodename="mi-equipo", ...}) = 0
+++ exited with 0 +++
El programa hace dos preguntas, y strace registra una sola llamada al sistema. La versión de la biblioteca la responde la propia biblioteca, que es código cargado dentro del proceso: no hay que cruzar ninguna frontera. La versión del kernel solo puede darla el kernel, y para eso hay que llamar al sistema. Es la diferencia entre las dos capas, medida con una herramienta.
14. Resumen
- Un sistema operativo completo se compone del kernel y de un conjunto de programas de espacio de usuario que lo hacen utilizable.
- El kernel es el único componente privilegiado. Se organiza en subsistemas: procesos, memoria, archivos, red y drivers.
- Las bibliotecas del sistema, con la biblioteca de C a la cabeza, ofrecen a los programas una interfaz cómoda sobre las llamadas al sistema.
- La shell es un programa común que interpreta comandos; puede sustituirse por otra.
- Las utilidades son programas pequeños para tareas básicas, sin ningún privilegio especial: obtienen su información preguntando al kernel.
- Los servicios funcionan en segundo plano desde el arranque. El proceso con PID 1 los inicia.
- En Linux cada componente proviene de un proyecto distinto y la distribución los integra. Ubuntu y Android comparten kernel y casi nada más.
- Las piezas se comunican mediante interfaces estables: la de usuario, la de programación y la de llamadas al sistema.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué componentes de un sistema operativo se ejecutan en espacio de kernel y cuáles en espacio de usuario?
En espacio de kernel, solo el kernel, con sus subsistemas y drivers. En espacio de usuario, todo lo demás: las bibliotecas, la shell, las utilidades, los servicios y, en Linux, el entorno gráfico.
2. ¿Por qué la biblioteca de C se considera parte del sistema operativo si no está en el kernel?
Porque es la interfaz que el sistema ofrece a los programadores. Envuelve las llamadas al sistema y casi todos los programas dependen de ella, de modo que sin ella el sistema no sería utilizable, y sustituirla por otra hace incompatibles los programas.
3. ¿Qué consecuencia tiene que la shell sea un programa común y no parte del kernel?
Que puede reemplazarse: hay varias shells y cada usuario elige la suya. Además, un fallo en la shell no afecta al kernel, y cualquiera puede escribir una nueva sin tocar el sistema.
4. ¿Tiene el comando ps algún privilegio especial para ver los procesos?
No. Es un programa de usuario como cualquier otro. Obtiene la información pidiéndosela al kernel, y un programa escrito por ti podría hacer lo mismo.
5. ¿Qué es un servicio o demonio, y cuál es el primero en iniciarse?
Es un programa que se ejecuta en segundo plano de forma permanente, sin terminal asociada, para ofrecer una función del sistema. El primero es el proceso con PID 1, que en la mayoría de las distribuciones es systemd y se encarga de iniciar a los demás.
6. Android usa el kernel de Linux. ¿Por qué un programa compilado para Ubuntu no funciona en Android?
Porque casi todo el espacio de usuario es distinto. En particular, el programa depende de la biblioteca glibc, y Android usa otra, Bionic. Compartir el kernel no basta para compartir los programas.
7. En version.c se obtienen dos datos y strace muestra una sola llamada al sistema. ¿Por qué?
La versión de glibc la devuelve una función de la propia biblioteca, que se ejecuta en espacio de usuario dentro del proceso. La versión del kernel solo la conoce el kernel, así que hace falta una llamada al sistema, uname.
8. ¿Qué ventaja tiene sacar del kernel una tarea como la configuración de la red y dejarla en un servicio?
El servicio se ejecuta sin privilegios de hardware y aislado: si falla, el sistema lo reinicia sin que el resto se vea afectado. Dentro del kernel, el mismo error podría detener toda la máquina. Además puede actualizarse o reemplazarse sin tocar el kernel.
Para practicar
- Ejecuta
componentes.shy completa una tabla con cada componente de tu sistema, su versión y el proyecto del que proviene. - Con
type, clasifica como internos o externos estos comandos:echo,kill,exit,grep,export,cat,history. Luego pruebatype -a echoy explica el resultado. - Averigua con
dpkg -S(orpm -qf) a qué paquete pertenecengrep,free,ip,lsblkygcc. - Ejecuta
ps -e -o pid,ppid,comm | head -20. ¿Qué procesos tienen como padre al PID 1? ¿Cuáles terminan end? - Repite el
stracede la sección 12 conls -l. ¿Qué llamadas nuevas aparecen al quitar el filtro-e? ¿Qué archivos de/etcconsulta y para qué crees que los necesita?