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

Tema 10

Componentes de un sistema operativo

Un sistema operativo completo no es un único programa, sino un conjunto de piezas que colaboran. En este tema las identificamos una por una —kernel, bibliotecas, shell, utilidades y servicios—, vemos qué hace cada una y las localizamos en un sistema Linux real. Es el mapa del bloque que empieza.

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:

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

Componentes de un sistema operativo: en el espacio de usuario, shell, utilidades, servicios, entorno gráfico y aplicaciones sobre las bibliotecas del sistema; debajo, el kernel con sus subsistemas, y el hardware ESPACIO DE USUARIO Shell Utilidades Servicios Entornográfico Aplicaciones Bibliotecas del sistema (biblioteca de C y otras) interfaz de llamadas al sistema ESPACIO DE KERNEL Kernel Procesos Memoria Archivos Red Drivers Hardware
Todo lo que está por encima de la línea punteada son programas comunes. Las aplicaciones, con borde discontinuo, no forman parte del sistema operativo.
ComponenteQué esDónde se ejecuta
KernelEl núcleo: administra el hardware y aísla los programas.Espacio de kernel
Bibliotecas del sistemaCódigo compartido que da a los programas una interfaz cómoda sobre el kernel.Espacio de usuario, dentro de cada programa
ShellEl programa que recibe órdenes de la persona y las lleva a cabo.Espacio de usuario
UtilidadesProgramas pequeños para las tareas básicas.Espacio de usuario
ServiciosProgramas que funcionan en segundo plano, sin intervención de nadie.Espacio de usuario
Entorno gráficoVentanas, 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:

En disco, el kernel de Linux es un archivo como cualquier otro, que el cargador de arranque lleva a memoria al encender el equipo:

terminal
$ 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:

  1. Envuelve las llamadas al sistema. Funciones como write, open o getpid son 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.
  2. Añade funcionalidad que no necesita al kernel. strlen, qsort o sqrt se resuelven por completo en espacio de usuario.
  3. Combina ambas cosas. printf da formato al texto por su cuenta y luego usa write; malloc administra 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:

terminal
$ 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:

terminal
$ 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:

terminal
$ 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:

terminal
$ 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, …
FamiliaEjemplosQué hacen
Archivosls, cp, mv, rm, mkdir, chmodManipular archivos y directorios.
Textocat, grep, sort, head, wcLeer, filtrar y transformar texto.
Procesosps, top, killObservar y controlar los procesos.
Sistemamount, lsblk, dmesg, freeConsultar y configurar el propio sistema.
Redip, ping, ssConfigurar 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.

terminal (extracto)
$ 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
  …
ServicioDe qué se ocupa
systemd-journaldRecoge y guarda los mensajes de registro del kernel y de los demás servicios.
systemd-udevdReacciona cuando el kernel detecta un dispositivo nuevo: crea su entrada en /dev y aplica la configuración.
systemd-logindGestiona los inicios de sesión de los usuarios.
NetworkManagerConfigura las conexiones de red.
cronEjecuta tareas programadas a horas determinadas.
sshdAcepta 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:

terminal
$ 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.

ComponenteProyecto habitualPaquete en Ubuntu
KernelLinuxlinux-image-…
Biblioteca de CGNU C Library (glibc)libc6
ShellGNU Bashbash
Utilidades de archivos y textoGNU Coreutilscoreutils
Utilidades de procesosprocpsprocps
Utilidades del sistemautil-linuxutil-linux, mount
Inicio y serviciossystemdsystemd
CompiladorGNU Compiler Collectiongcc

El gestor de paquetes, que es en sí otro componente del sistema, sabe a qué paquete pertenece cada archivo instalado:

terminal (Ubuntu y Debian)
$ 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:

ComponenteUbuntuAndroidmacOSWindows
KernelLinuxLinuxXNUNT
Biblioteca del sistemaglibcBioniclibSystemntdll, kernel32
ShellBashmkshzshPowerShell, cmd
Inicio y serviciossystemdinit propiolaunchdAdministrador de servicios
Entorno gráficoGNOMESurfaceFlingerAquaEscritorio 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:

InterfazEntre quiénesEn qué consiste
De usuarioLa persona y el sistemaLa línea de comandos o el entorno gráfico.
De programación (API)El programa y las bibliotecasLas funciones disponibles, con sus parámetros y resultados. La norma POSIX define la de los sistemas tipo Unix.
De llamadas al sistemaEl espacio de usuario y el kernelEl 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:

  1. El emulador de terminal recibe las teclas y se las entrega a la shell. entorno gráfico
  2. La shell lee la línea, comprueba que ls no es un comando interno y lo busca en los directorios del sistema hasta encontrar /usr/bin/ls. shell
  3. La shell pide al kernel que cree un proceso nuevo y que ejecute en él ese archivo. shell → kernel
  4. El kernel carga el ejecutable, como vimos en el tema 9, y el enlazador dinámico incorpora la biblioteca de C. kernel
  5. ls empieza a ejecutarse y llama a funciones de la biblioteca para abrir el directorio actual y leer su contenido. utilidad → biblioteca
  6. La biblioteca hace las llamadas al sistema correspondientes. biblioteca → kernel
  7. 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
  8. ls ordena los nombres, los escribe en su salida y termina. El texto llega al emulador de terminal, que lo dibuja. utilidad
  9. 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:

terminal (salida abreviada)
$ 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:

componentes.sh
#!/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}"
terminal
$ 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:

version.c
#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;
}
terminal
$ 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

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

  1. Ejecuta componentes.sh y completa una tabla con cada componente de tu sistema, su versión y el proyecto del que proviene.
  2. Con type, clasifica como internos o externos estos comandos: echo, kill, exit, grep, export, cat, history. Luego prueba type -a echo y explica el resultado.
  3. Averigua con dpkg -S (o rpm -qf) a qué paquete pertenecen grep, free, ip, lsblk y gcc.
  4. Ejecuta ps -e -o pid,ppid,comm | head -20. ¿Qué procesos tienen como padre al PID 1? ¿Cuáles terminan en d?
  5. Repite el strace de la sección 12 con ls -l. ¿Qué llamadas nuevas aparecen al quitar el filtro -e? ¿Qué archivos de /etc consulta y para qué crees que los necesita?