← Fundamentos de Sistemas Operativos Bloque 2 · La computadora por dentro · Tema 5 de 34

Tema 05

Arquitectura de una computadora

Un sistema operativo administra hardware, así que antes de estudiarlo hay que conocer lo que administra. En este tema trazamos el mapa general de la máquina: el modelo de von Neumann, sus tres componentes y los buses que los unen. Los temas siguientes del bloque amplían cada parte.

1. Por qué empezar por el hardware

En el tema 3 apareció varias veces la misma regla: lo que un sistema operativo puede hacer depende de lo que el hardware le permite. La multiprogramación necesitó interrupciones; la protección, un procesador con modo privilegiado. Por eso este segundo bloque del curso recorre la máquina antes de volver al software.

No hace falta saber electrónica. Nos interesa el hardware tal como lo ve un sistema operativo: qué componentes hay, qué sabe hacer cada uno y cómo se comunican. Este tema da la visión de conjunto, y los cuatro siguientes entran en detalle:

2. El modelo de von Neumann

Casi todas las computadoras construidas desde finales de los años cuarenta, desde un mainframe hasta un teléfono, siguen el mismo esquema general. Se lo conoce como arquitectura de von Neumann, por el matemático John von Neumann, que lo describió en un informe de 1945. Consta de tres componentes conectados entre sí:

Modelo de von Neumann: CPU, memoria y dispositivos de entrada y salida conectados por un bus CPU Unidad de control Unidad aritmético-lógica Registros Memoria principal 0instrucción 1instrucción 2dato 3dato … Bus: direcciones · datos · control Disco Teclado y pantalla Red
La CPU ejecuta, la memoria guarda instrucciones y datos, los dispositivos comunican con el exterior, y el bus los conecta.

Los tres se enlazan mediante buses, conjuntos de conexiones por los que viajan las señales. Antes de ver cada parte, conviene detenerse en la idea que hace especial a este diseño.

3. El programa almacenado

Las primeras computadoras electrónicas, como ENIAC, se programaban recableándolas: cambiar de programa significaba mover cables e interruptores durante días. El programa era parte del hardware.

La propuesta de von Neumann y sus colegas fue tratar las instrucciones como un dato más:

En una computadora de programa almacenado, las instrucciones se codifican como números y se guardan en la misma memoria que los datos. Cambiar de programa es cambiar el contenido de la memoria.

Parece un detalle técnico y es, en realidad, la idea que hace posible todo el software. De ella se derivan cuatro consecuencias que usaremos constantemente:

  1. Un programa puede cargar a otro. Si un programa es un conjunto de números, otro programa puede leerlos del disco, copiarlos a la memoria y pedir a la CPU que los ejecute. Es exactamente lo que hace el sistema operativo cuando escribes ./entorno.
  2. Un programa puede fabricar a otro. Un compilador como gcc lee texto y escribe números que son instrucciones. Para él, el programa que genera es su dato de salida.
  3. La memoria no distingue. Un mismo grupo de bytes puede ser un número entero, cuatro letras o una instrucción; no hay nada en ellos que lo indique. Lo que son depende de cómo se los use.
  4. Los datos pueden ejecutarse por error, o a propósito. Si la CPU llega a una zona de datos, intentará ejecutarlos como instrucciones. Muchos ataques informáticos consisten en lograr precisamente eso, y buena parte de las protecciones del sistema operativo existen para impedirlo.
La alternativa: arquitectura Harvard

Existe otro diseño, llamado Harvard, con dos memorias separadas: una para las instrucciones y otra para los datos. Se usa en muchos microcontroladores, donde el programa queda grabado y no cambia. Las computadoras de propósito general siguen el modelo de von Neumann.

4. La CPU

La CPU es el único componente que hace algo por iniciativa propia: los demás esperan sus órdenes. Internamente se distinguen tres partes:

Su funcionamiento se reduce a un ciclo que repite sin descanso mientras tiene energía: tomar la siguiente instrucción de la memoria, ejecutarla, y volver a empezar. Las instrucciones son muy elementales —copiar un valor, sumar dos números, comparar, saltar a otra posición—, y la CPU ejecuta miles de millones por segundo.

Conviene retener desde ahora una limitación: la CPU no sabe qué es un programa, ni un usuario, ni un archivo. Solo ve una instrucción a continuación de otra. Que esas instrucciones pertenezcan a un navegador o al kernel es una organización que el sistema operativo construye encima.

Los procesadores actuales contienen varios núcleos, cada uno de los cuales es una CPU completa capaz de ejecutar su propia secuencia de instrucciones al mismo tiempo que los demás. Estudiamos la CPU por dentro en el tema 6.

5. La memoria principal

La memoria principal, o RAM, tiene una estructura muy simple: es una larga fila de celdas numeradas. Cada celda guarda un byte, y el número que la identifica es su dirección. Solo admite dos operaciones: leer el contenido de una dirección y escribir un valor en una dirección.

Para un programador de C, la memoria es un único arreglo gigantesco de bytes, y una dirección es un índice de ese arreglo. Un puntero no es más que una variable que guarda uno de esos índices.

Las unidades

UnidadQué esValores posibles
BitLa unidad mínima: un 0 o un 1.2
Byte8 bits. Es la unidad con dirección propia.256 (de 0 a 255)
PalabraLa cantidad de bits que la CPU maneja de una vez: 64 en los procesadores actuales.264

Cuando se dice que un procesador o un sistema es «de 64 bits» se habla del tamaño de palabra: sus registros tienen 64 bits y, por lo tanto, puede manejar direcciones de hasta 64 bits. Como un valor de más de un byte no cabe en una celda, ocupa varias celdas consecutivas, y su dirección es la de la primera.

Las cantidades de memoria se expresan en potencias de 2: un kibibyte (KiB) son 1024 bytes; un mebibyte (MiB), 1024 KiB; un gibibyte (GiB), 1024 MiB. En el uso cotidiano se los llama, de forma imprecisa, KB, MB y GB.

Dos propiedades que importan al sistema operativo

6. La memoria vista desde C

C permite mirar la memoria casi sin intermediarios, y es una de las razones por las que se usa para escribir sistemas operativos. El siguiente programa guarda un número en una variable y luego recorre, uno por uno, los bytes que esa variable ocupa.

memoria.c
#include <stdio.h>

int main(void)
{
    int numero = 0x11223344;
    unsigned char *byte = (unsigned char *)&numero;

    printf("Tamanios: char=%zu  int=%zu  long=%zu  puntero=%zu bytes\n",
           sizeof(char), sizeof(int), sizeof(long), sizeof(void *));

    printf("La variable numero ocupa %zu bytes a partir de %p\n\n",
           sizeof(numero), (void *)&numero);

    for (size_t i = 0; i < sizeof(numero); i++)
        printf("  direccion %p  contenido 0x%02x\n",
               (void *)(byte + i), byte[i]);

    return 0;
}

El valor 0x11223344 está escrito en hexadecimal para que cada byte sea reconocible: 11, 22, 33 y 44. La segunda línea de main toma la dirección de la variable y la trata como dirección de bytes sueltos, lo que permite examinarlos por separado.

terminal
$ gcc -Wall -Wextra -g memoria.c -o memoria
$ ./memoria
Tamanios: char=1  int=4  long=8  puntero=8 bytes
La variable numero ocupa 4 bytes a partir de 0x7ffd3c1a5b44

  direccion 0x7ffd3c1a5b44  contenido 0x44
  direccion 0x7ffd3c1a5b45  contenido 0x33
  direccion 0x7ffd3c1a5b46  contenido 0x22
  direccion 0x7ffd3c1a5b47  contenido 0x11

La salida confirma lo dicho en la sección anterior y añade una sorpresa:

El orden de los bytes no afecta a un programa que trata sus variables como números. Importa cuando los bytes salen de la máquina tal cual están en memoria: al guardarlos en un archivo binario o al enviarlos por la red a un equipo que podría ordenarlos de otro modo.

Las direcciones cambian

La dirección que veas será distinta de la del ejemplo y distinta en cada ejecución, por la carga en posiciones aleatorias que mencionamos en el tema 2. Además, es una dirección virtual: no indica una celda física de la RAM, sino una posición en el espacio de direcciones del proceso. Para el programa, la diferencia es invisible.

7. Los dispositivos de entrada y salida

Una CPU con memoria puede calcular, pero no puede recibir datos ni mostrar resultados, y lo pierde todo al apagarse. Los dispositivos de entrada y salida la conectan con el mundo. Se pueden agrupar por su función:

TipoEjemplosQué aportan
AlmacenamientoDisco de estado sólido, disco rígido, memoria USBConservan los datos sin energía. Mucha capacidad, mucho más lentos que la RAM.
ComunicaciónTarjeta de red, Wi-Fi, BluetoothIntercambian datos con otros equipos.
Interfaz con personasTeclado, ratón, pantalla, audio, cámaraReciben órdenes y muestran resultados.
Del propio sistemaReloj, temporizador, sensores de temperaturaMiden el tiempo y el estado de la máquina.

Todos comparten dos rasgos que condicionan el diseño de cualquier sistema operativo:

La CPU no trata directamente con el dispositivo, sino con su controlador de hardware: un circuito que sabe manejarlo y que se presenta ante el resto de la máquina como un pequeño conjunto de registros donde se escriben órdenes y se leen resultados. Lo vemos en detalle en el tema 8.

8. Los buses

Un bus es un canal de comunicación compartido entre componentes. En el modelo clásico, la CPU usa tres grupos de líneas para comunicarse con la memoria y los dispositivos:

BusQué transportaSentido
De direccionesLa dirección de la celda o del dispositivo con que se quiere operar.Desde la CPU
De datosEl valor que se lee o se escribe.En ambos sentidos
De controlLas señales que coordinan: si es lectura o escritura, cuándo el dato es válido, pedidos de interrupción.En ambos sentidos

Una lectura de memoria, por ejemplo, ocurre así: la CPU coloca la dirección en el bus de direcciones y activa la señal de lectura en el bus de control; la memoria responde poniendo el contenido de esa celda en el bus de datos; la CPU lo recoge y lo guarda en un registro.

El ancho del bus de direcciones

La cantidad de líneas del bus de direcciones fija cuánta memoria puede usar la máquina. Con n líneas se pueden formar 2n direcciones distintas, y cada una identifica un byte:

Bits de direcciónMemoria direccionableEjemplo histórico
1664 KiBMicroprocesadores de 8 bits; el mundo de CP/M
201 MiBLa primera IBM PC; el límite de MS-DOS
324 GiBEl Intel 80386 y los sistemas de 32 bits
48256 TiBDirecciones virtuales en la mayoría de los x86-64 actuales

La tabla explica por qué un sistema de 32 bits no puede aprovechar más de 4 GiB de memoria, y adelanta un dato que veremos enseguida en la terminal: los procesadores de 64 bits no usan realmente los 64 bits para direcciones, porque nadie fabrica equipos con tanta memoria.

9. Una computadora actual

El diagrama de la sección 2 muestra un único bus compartido por todos. Así eran las computadoras de los años setenta. Hoy la idea se mantiene, pero un solo canal sería un cuello de botella, de modo que cada tipo de componente tiene su propia conexión, dimensionada según su velocidad:

ConexiónQué uneCaracterística
Canales de memoriaLa CPU con los módulos de RAMLa conexión más rápida y directa.
PCI ExpressLa CPU con la tarjeta gráfica, los discos NVMe y las tarjetas de redEnlaces dedicados de alta velocidad para cada dispositivo.
SATADiscos rígidos y de estado sólido tradicionalesPensada para almacenamiento.
USBTeclado, ratón, memorias, cámarasDispositivos externos que se conectan y desconectan en marcha.

En una PC, un circuito de la placa base llamado chipset agrupa las conexiones más lentas y las comunica con la CPU. En un teléfono o en una placa pequeña, casi todo —la CPU, el procesador gráfico, los controladores de dispositivos— está integrado en un único chip, llamado SoC (system on a chip).

Para el sistema operativo estas diferencias son de detalle. El esquema lógico sigue siendo el de von Neumann: una CPU que ejecuta instrucciones tomadas de una memoria y que conversa con dispositivos a través de sus controladores.

10. Explorar tu hardware desde Linux

El kernel reconoce el hardware al arrancar y mantiene un inventario que podemos consultar. Empecemos por la CPU:

terminal (extracto)
$ lscpu
Architecture:             x86_64
  CPU op-mode(s):         32-bit, 64-bit
  Address sizes:          39 bits physical, 48 bits virtual
  Byte Order:             Little Endian
CPU(s):                   8
Model name:               Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
  Thread(s) per core:     2
  Core(s) per socket:     4
  Socket(s):              1

Las dos líneas resaltadas conectan con lo que vimos. Byte Order confirma el orden que observamos con memoria.c. Address sizes indica el ancho real de las direcciones: este procesador puede direccionar 239 bytes de memoria física, es decir, 512 GiB, y ofrece a cada programa direcciones virtuales de 48 bits. La diferencia entre ambos tipos de dirección es el tema de la gestión de memoria.

Veamos ahora la memoria y el almacenamiento:

terminal
$ head -3 /proc/meminfo          # la memoria, según el kernel
MemTotal:       16252416 kB
MemFree:         7234180 kB
MemAvailable:   10931548 kB

$ lsblk                          # dispositivos de almacenamiento
NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
nvme0n1     259:0    0 476,9G  0 disk
├─nvme0n1p1 259:1    0     1G  0 part /boot/efi
└─nvme0n1p2 259:2    0 475,9G  0 part /

Y los dispositivos conectados a los buses PCI y USB, cada uno con su controlador:

terminal (extracto)
$ lspci
00:00.0 Host bridge: Intel Corporation Xeon E3-1200 v6/7th Gen Core Processor Host Bridge
00:02.0 VGA compatible controller: Intel Corporation UHD Graphics 620
00:14.0 USB controller: Intel Corporation Sunrise Point-LP USB 3.0 xHCI Controller
00:1f.3 Audio device: Intel Corporation Sunrise Point-LP HD Audio
02:00.0 Network controller: Intel Corporation Wireless 8265 / 8275
03:00.0 Non-Volatile memory controller: Samsung Electronics NVMe SSD Controller

$ lsusb
Bus 001 Device 003: ID 046d:c077 Logitech, Inc. Mouse
Bus 001 Device 002: ID 04f2:b5ce Chicony Electronics Co., Ltd Integrated Camera
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Si lspci o lsusb no están instalados, se obtienen con sudo apt install pciutils usbutils. Observa en la salida de lspci que casi todas las líneas dicen «controller»: lo que el kernel ve en el bus no es la pantalla ni el disco, sino sus controladores.

En WSL y en máquinas virtuales

Lo que verás es el hardware virtual que el programa de virtualización presenta a Linux, no el de tu equipo. lscpu y /proc/meminfo mostrarán los procesadores y la memoria asignados a la máquina virtual, y lspci y lsusb listarán pocos dispositivos, o ninguno en WSL. Es en sí mismo una buena demostración: el kernel de Linux no puede distinguir un hardware simulado de uno real.

11. Ver el programa almacenado

Afirmamos en la sección 3 que las instrucciones son números guardados en la misma memoria que los datos. Podemos comprobarlo: el siguiente programa toma la dirección de su propia función main y lee como bytes las instrucciones que hay allí.

almacenado.c
#include <stdio.h>

int global = 2026;

int main(void)
{
    unsigned char *codigo = (unsigned char *)main;

    printf("Instrucciones de main en %p\n", (void *)main);
    printf("Variable global en       %p\n\n", (void *)&global);

    printf("Primeros bytes de main: ");
    for (int i = 0; i < 8; i++)
        printf("%02x ", codigo[i]);
    printf("\n");
    return 0;
}
terminal
$ gcc -Wall -Wextra -g almacenado.c -o almacenado
$ ./almacenado
Instrucciones de main en 0x5d3f2a6c1149
Variable global en       0x5d3f2a6c4010

Primeros bytes de main: f3 0f 1e fa 55 48 89 e5

El nombre de una función, en C, es la dirección de memoria donde empiezan sus instrucciones. El programa muestra que esa dirección y la de la variable global pertenecen a la misma memoria —están a poca distancia una de otra— y que en la primera hay bytes, igual que en la segunda. Para saber qué instrucciones representan esos bytes, le pedimos a la herramienta objdump que los traduzca:

terminal
$ objdump -d almacenado | grep -A3 '<main>:'
0000000000001149 <main>:
    1149:	f3 0f 1e fa          	endbr64
    114d:	55                   	push   %rbp
    114e:	48 89 e5             	mov    %rsp,%rbp

Son los mismos ocho bytes: forman las tres primeras instrucciones de main, que ocupan cuatro, uno y tres bytes. Lo que para la CPU es «guardar un registro en la pila», para nuestro printf fue simplemente el número 0x55. Los valores concretos dependen del procesador y del compilador; en un equipo ARM serán otros.

Qué significan esas instrucciones lo veremos en el tema 6, y cómo llegaron a estar en el archivo ejecutable, en el tema 9.

12. Lo que el modelo no tiene

El modelo de von Neumann describe una máquina que ejecuta un programa, con acceso total a toda la memoria y a todos los dispositivos. No contempla que haya varios programas, ni que alguno pueda ser defectuoso. Con esa máquina sola, un sistema operativo no podría defenderse: cualquier programa podría sobrescribirlo o no devolverle nunca el control.

Para que un sistema operativo sea posible, el hardware añade al modelo básico cuatro mecanismos. Son los que hicieron falta, uno tras otro, en la historia del tema 3, y ocupan buena parte del resto del curso:

Mecanismo de hardwareQué le permite al sistema operativoTema
Modos de ejecuciónReservarse las instrucciones peligrosas y negárselas a los programas.12
InterrupcionesEnterarse de que un dispositivo terminó, sin tener que preguntar constantemente.17
TemporizadorRecuperar la CPU aunque el programa en ejecución no la ceda.19
Unidad de gestión de memoriaDar a cada programa su propio espacio de direcciones e impedirle salir de él.27

Ya vimos uno en acción sin nombrarlo. Si en almacenado.c intentaras escribir sobre los bytes de main, el programa terminaría con «Segmentation fault»: la unidad de gestión de memoria permite leer y ejecutar esa zona, pero no modificarla. En una máquina de von Neumann pura, la escritura se habría hecho sin protestar.

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué significa «programa almacenado» y qué había antes?

Significa que las instrucciones se guardan en la memoria, codificadas como números, igual que los datos. Antes, el programa se establecía con cables e interruptores, y cambiarlo exigía modificar físicamente la máquina.

2. ¿Por qué el concepto de programa almacenado es imprescindible para que exista un sistema operativo?

Porque permite que un programa trate a otro como dato. El sistema operativo lee un ejecutable del disco, copia sus bytes a la memoria y hace que la CPU los ejecute. Sin esa posibilidad no podría cargar programas.

3. Un procesador tiene direcciones de 32 bits. ¿Cuánta memoria puede direccionar y por qué?

232 bytes, es decir, 4 GiB. Con 32 bits se pueden formar 232 números distintos y cada uno identifica un byte.

4. En una máquina little endian se guarda el entero 0x0A0B0C0D en la dirección 100. ¿Qué byte hay en la dirección 100? ¿Y en la 103?

En la 100 está 0x0D, el byte menos significativo, y en la 103 está 0x0A, el más significativo.

5. ¿Por qué la memoria principal no alcanza para guardar los documentos de un usuario?

Porque es volátil: pierde su contenido al apagar el equipo. Lo que debe perdurar se guarda en un dispositivo de almacenamiento, como un disco.

6. ¿Qué función cumple cada uno de los tres buses clásicos durante una escritura en memoria?

El bus de direcciones lleva la dirección de la celda; el bus de datos lleva el valor que se va a guardar; el bus de control lleva la señal que indica que la operación es una escritura y cuándo debe realizarse.

7. ¿Qué le falta al modelo de von Neumann para poder proteger al sistema operativo de los programas?

Un modo de ejecución privilegiado, interrupciones, un temporizador y una unidad de gestión de memoria. En el modelo básico, cualquier programa accede a toda la memoria y a todos los dispositivos, y nada lo obliga a devolver la CPU.

Para practicar

  1. Ejecuta lscpu y anota la arquitectura, la cantidad de procesadores, el orden de bytes y los tamaños de dirección. Calcula cuánta memoria física podría direccionar tu procesador.
  2. Modifica memoria.c para examinar los bytes de una variable long con el valor 1. ¿En qué dirección queda el byte que vale 1?
  3. Añade a memoria.c una cadena char texto[] = "Hola"; y recorre sus bytes. ¿Cuántos ocupa? ¿Se aplica el orden little endian a una cadena?
  4. En almacenado.c, agrega la línea codigo[0] = 0; antes del primer printf. Compila, ejecuta y explica el resultado con lo visto en la sección 12.
  5. Compara la salida de free -h con el contenido de /proc/meminfo. ¿De dónde saca free sus datos? Compruébalo con strace -e trace=openat free.