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:
- Tema 6: cómo ejecuta la CPU una instrucción tras otra.
- Tema 7: por qué hay varios tipos de memoria y almacenamiento.
- Tema 8: cómo se conversa con un dispositivo.
- Tema 9: cómo llega un programa en C a ser instrucciones en memoria.
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í:
- La unidad central de proceso (CPU, o procesador) ejecuta las instrucciones.
- La memoria principal guarda las instrucciones que se van a ejecutar y los datos con que trabajan.
- Los dispositivos de entrada y salida (E/S) comunican la máquina con el exterior: el disco, el teclado, la pantalla, la red.
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:
- 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. - Un programa puede fabricar a otro. Un compilador como
gcclee texto y escribe números que son instrucciones. Para él, el programa que genera es su dato de salida. - 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.
- 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.
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:
- La unidad de control dirige el trabajo: trae cada instrucción de la memoria, averigua qué pide y activa lo necesario para cumplirla.
- La unidad aritmético-lógica (ALU) hace los cálculos: sumas, restas, comparaciones y operaciones lógicas entre bits.
- Los registros son unas pocas celdas de almacenamiento dentro de la propia CPU, de acceso inmediato. Guardan los valores con los que se está trabajando en este instante.
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
| Unidad | Qué es | Valores posibles |
|---|---|---|
| Bit | La unidad mínima: un 0 o un 1. | 2 |
| Byte | 8 bits. Es la unidad con dirección propia. | 256 (de 0 a 255) |
| Palabra | La 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
- Es volátil. Al cortarse la energía, su contenido se pierde. Todo lo que deba perdurar tiene que guardarse en un dispositivo de almacenamiento, y por eso existen los archivos.
- Es limitada y compartida. El kernel y todos los programas en ejecución están en la misma memoria física. Repartirla e impedir que unos lean o pisen lo de otros es una de las tareas centrales del 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.
#include <stdio.h>
int main(void)
{
int numero = 0x11223344;
unsigned char *byte = (unsigned char *)№
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.
$ 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:
- La variable ocupa cuatro celdas consecutivas. Las direcciones aumentan de a uno, y la dirección «de la variable» es la de su primer byte.
- Un puntero ocupa 8 bytes, es decir, 64 bits: el tamaño de palabra de la máquina.
- Los bytes están «al revés». El byte menos significativo,
0x44, se guarda en la dirección más baja. Este orden se llama little endian y es el que usan los procesadores x86-64 y, en la práctica, también los ARM. El orden contrario, big endian, se usa por convención en los protocolos de red.
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.
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:
| Tipo | Ejemplos | Qué aportan |
|---|---|---|
| Almacenamiento | Disco de estado sólido, disco rígido, memoria USB | Conservan los datos sin energía. Mucha capacidad, mucho más lentos que la RAM. |
| Comunicación | Tarjeta de red, Wi-Fi, Bluetooth | Intercambian datos con otros equipos. |
| Interfaz con personas | Teclado, ratón, pantalla, audio, cámara | Reciben órdenes y muestran resultados. |
| Del propio sistema | Reloj, temporizador, sensores de temperatura | Miden el tiempo y el estado de la máquina. |
Todos comparten dos rasgos que condicionan el diseño de cualquier sistema operativo:
- Son lentos. Comparados con la CPU, incluso los más rápidos tardan una eternidad. Mientras un disco atiende un pedido, la CPU podría ejecutar millones de instrucciones. De ahí nació la multiprogramación.
- Son distintos entre sí. Cada tipo de dispositivo, y a veces cada modelo, se maneja de una manera propia. De ahí nace la necesidad de abstracciones.
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:
| Bus | Qué transporta | Sentido |
|---|---|---|
| De direcciones | La dirección de la celda o del dispositivo con que se quiere operar. | Desde la CPU |
| De datos | El valor que se lee o se escribe. | En ambos sentidos |
| De control | Las 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ón | Memoria direccionable | Ejemplo histórico |
|---|---|---|
| 16 | 64 KiB | Microprocesadores de 8 bits; el mundo de CP/M |
| 20 | 1 MiB | La primera IBM PC; el límite de MS-DOS |
| 32 | 4 GiB | El Intel 80386 y los sistemas de 32 bits |
| 48 | 256 TiB | Direcciones 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ón | Qué une | Característica |
|---|---|---|
| Canales de memoria | La CPU con los módulos de RAM | La conexión más rápida y directa. |
| PCI Express | La CPU con la tarjeta gráfica, los discos NVMe y las tarjetas de red | Enlaces dedicados de alta velocidad para cada dispositivo. |
| SATA | Discos rígidos y de estado sólido tradicionales | Pensada para almacenamiento. |
| USB | Teclado, ratón, memorias, cámaras | Dispositivos 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:
$ 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:
$ 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:
$ 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.
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í.
#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;
}
$ 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:
$ 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 hardware | Qué le permite al sistema operativo | Tema |
|---|---|---|
| Modos de ejecución | Reservarse las instrucciones peligrosas y negárselas a los programas. | 12 |
| Interrupciones | Enterarse de que un dispositivo terminó, sin tener que preguntar constantemente. | 17 |
| Temporizador | Recuperar la CPU aunque el programa en ejecución no la ceda. | 19 |
| Unidad de gestión de memoria | Dar 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
- Casi todas las computadoras siguen el modelo de von Neumann: una CPU, una memoria principal y dispositivos de entrada y salida, unidos por buses.
- En una máquina de programa almacenado, las instrucciones son números guardados en la misma memoria que los datos. Por eso un programa puede cargar o generar a otro.
- La CPU repite un ciclo: tomar una instrucción, ejecutarla y pasar a la siguiente. No sabe nada de programas, usuarios ni archivos.
- La memoria es un arreglo de bytes con dirección. Es volátil y la comparten el kernel y todos los programas.
- Un valor de varios bytes ocupa celdas consecutivas; en x86-64 se guardan en orden little endian.
- Los dispositivos son lentos y diversos, y se manejan a través de sus controladores.
- El ancho de las direcciones limita la memoria utilizable: 32 bits alcanzan para 4 GiB.
- El modelo básico no ofrece protección. Los modos de ejecución, las interrupciones, el temporizador y la unidad de gestión de memoria son los añadidos que hacen posible un sistema operativo.
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
- Ejecuta
lscpuy 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. - Modifica
memoria.cpara examinar los bytes de una variablelongcon el valor1. ¿En qué dirección queda el byte que vale 1? - Añade a
memoria.cuna cadenachar texto[] = "Hola";y recorre sus bytes. ¿Cuántos ocupa? ¿Se aplica el orden little endian a una cadena? - En
almacenado.c, agrega la líneacodigo[0] = 0;antes del primerprintf. Compila, ejecuta y explica el resultado con lo visto en la sección 12. - Compara la salida de
free -hcon el contenido de/proc/meminfo. ¿De dónde sacafreesus datos? Compruébalo constrace -e trace=openat free.