Read in English · Lire en français · Auf Deutsch lesen
Sección 0.1 de 0.8 · Principiante · 15 min de lectura
Programas, código y cómo los ejecuta una computadora
Videolección
4 min · narrado en inglés, con subtítulosSolo escuchar (audio)
Leer la transcripción
Bienvenido a Be Software Architecture Wise. En esta primera lección empezamos por el principio de todo: qué es el software y qué ocurre realmente cuando una computadora ejecuta un programa. No necesitas ninguna formación técnica. Al final, conocerás el puñado de ideas en las que se apoya cada capítulo posterior.
Empecemos con dos palabras que vas a oír constantemente. El hardware es la máquina física: el procesador, la memoria, el almacenamiento y la tarjeta de red. El software es el conjunto de instrucciones y datos que le dicen a ese hardware qué hacer. Una imagen útil es la cocina de un restaurante. El chef es el procesador, y el software son las recetas que sigue el chef.
Un programa es una lista de instrucciones precisas, y la computadora las sigue al pie de la letra, palabra por palabra. Nunca adivina lo que querías decir. Por eso los errores en las instrucciones se convierten en bugs. La computadora no se está poniendo difícil; está haciendo exactamente lo que se le dijo. Los programadores escriben esas instrucciones como código fuente, en lenguajes como Python, Java o Go, que las personas pueden leer y modificar.
Pero el procesador no sabe leer Python ni Java. Su propio idioma es el código máquina: instrucciones muy sencillas, específicas de un tipo de procesador. Así que el código fuente tiene que traducirse. Hay dos rutas principales. En la primera ruta, un compilador traduce todo el programa a código máquina antes de lanzarlo. Los lenguajes como C, Rust y Go funcionan así. En la segunda ruta, el código se compila a una forma intermedia llamada bytecode, y un programa llamado máquina virtual lo ejecuta, traduciendo a código máquina sobre la marcha las partes más usadas. Java, Python y C sharp funcionan así.
Entre tus programas y el hardware está el sistema operativo, como Windows, macOS, Linux o Android. Las aplicaciones, como un navegador web o una aplicación de reparto de comida, no tocan el hardware directamente. Le piden al sistema operativo lo que necesitan, mediante lo que se llaman llamadas al sistema. El sistema operativo reparte el procesador, la memoria, los archivos y la red entre todos los programas que se están ejecutando, y evita que interfieran entre sí. En nuestra cocina, es el jefe de cocina que decide qué receta recibe la atención del chef.
Cuando un programa se ejecuta, el sistema operativo crea un proceso para él, con su propia memoria privada. Dentro de un proceso puede haber varios hilos, cada uno haciendo una parte del trabajo al mismo tiempo. Los hilos comparten la memoria del proceso. Eso es útil, pero si dos hilos modifican los mismos datos a la vez, aparecen algunos de los bugs más difíciles de encontrar. Y fíjate en la diferencia entre la memoria y el disco. La memoria, o RAM, es rápida, pero se borra cuando el programa se detiene o la máquina se reinicia. Todo lo que no deba perderse tiene que guardarse en un almacenamiento duradero, normalmente una base de datos.
Juntémoslo todo con un pequeño programa que calcula el precio de un pedido. Cuando lo inicias, el sistema operativo crea un proceso e inicia dentro de él el intérprete de Python. El intérprete lee el archivo y lo convierte en bytecode. Si hay un error de escritura, como un paréntesis que falta, se notifica ahora, antes de que se ejecute nada. El programa te pregunta cuántos artículos quieres, y espera. Mientras espera, no usa nada de tiempo de procesador. Después viene el cálculo: tres artículos a doce libras con cincuenta son treinta y siete libras con cincuenta, y al sumar el veinte por ciento de IVA salen cuarenta y cinco libras. Por último, se imprime el resultado y el proceso termina. Su memoria se devuelve y, como no se guardó nada en el disco, la respuesta desaparece.
¿Por qué importa esto para la arquitectura de software? Porque todo sistema que diseñes, por grande que sea, está hecho de programas que se ejecutan como procesos sobre hardware compartido. Las decisiones sobre qué vive en memoria y qué debe almacenarse de forma duradera aparecen una y otra vez, en todos los capítulos. Y como muchas cosas se ejecutan a la vez y las máquinas se reinician, los arquitectos tienen que planificar la concurrencia y los fallos desde el principio.
Esa es la base. La sección completa incluye el ejemplo de código entero y más detalles sobre el sistema operativo y los bugs. A continuación, en la sección cero punto dos, veremos cómo organizan los datos los programas. Nos vemos allí.
Lo que necesitas saber
- El hardware es la máquina física (CPU, RAM, almacenamiento, tarjeta de red); el software es el conjunto de instrucciones y datos que le dicen qué hacer.
- Un programa es una lista de instrucciones precisas; la computadora hace exactamente lo que dice, así que los errores en las instrucciones se convierten en bugs.
- Los programadores escriben código fuente en lenguajes de alto nivel, que los compiladores, los intérpretes o las máquinas virtuales de bytecode convierten en código máquina que la CPU puede ejecutar.
- El sistema operativo reparte la CPU, la memoria, los archivos y la red entre los programas y aplica la seguridad; los programas le piden servicios mediante llamadas al sistema.
- Un programa en ejecución es un proceso con su propia memoria; los hilos dentro de un proceso comparten esa memoria, lo cual es útil pero puede causar bugs de concurrencia.
- La RAM es rápida pero pierde su contenido al reiniciar; todo lo que no deba perderse tiene que escribirse en un almacenamiento duradero, normalmente una base de datos.
Hardware y software
Una computadora es una máquina que sigue instrucciones muy deprisa. Las partes físicas que podrías tocar son el hardware; las instrucciones que les dicen a esas partes qué hacer son el software. Una computadora portátil, un teléfono, el servidor que ejecuta el sitio web de tu banco y el chip dentro de un termostato inteligente son todos computadoras construidas con los mismos cuatro tipos de componentes.
La CPU (unidad central de procesamiento, o procesador) ejecuta las instrucciones. Un procesador moderno tiene varios núcleos, cada uno capaz de procesar su propio flujo de instrucciones, y cada núcleo ejecuta miles de millones de pasos sencillos por segundo. La RAM (memoria de acceso aleatorio, normalmente llamada simplemente memoria) contiene las instrucciones y los datos con los que la CPU está trabajando en este momento. Es rápida, pero volátil: si se corta la corriente, su contenido desaparece. El almacenamiento (una unidad de estado sólido o un disco duro giratorio) conserva los archivos cuando no hay corriente, pero acceder a él es mucho más lento. Una tarjeta de red (por cable o Wi-Fi) envía y recibe datos hacia y desde otras computadoras.
Piensa en la cocina de un restaurante. El chef es la CPU: el único que realmente cocina. La encimera es la RAM: todo lo que el chef está usando en este momento está dispuesto allí, al alcance de la mano, pero se despeja al final de la noche. La despensa y el congelador son el almacenamiento: grandes, permanentes y a un paseo de distancia. La ventanilla que da al comedor es la tarjeta de red, por la que llegan los pedidos y salen los platos. Las recetas son el software.
Las cuatro partes que tiene toda computadora
| Parte | Qué hace | Analogía de la cocina | Qué ocurre si es el cuello de botella |
|---|---|---|---|
| CPU (procesador) | Ejecuta instrucciones: aritmética, comparaciones, movimiento de datos | El chef | El trabajo se acumula; las páginas y los informes se vuelven lentos aunque sobre memoria |
| RAM (memoria) | Contiene los programas en ejecución y los datos que están usando | La encimera | El sistema empieza a trasladar datos al disco y se ralentiza bruscamente, o se detienen programas |
| Almacenamiento (SSD o disco) | Guarda archivos y bases de datos de forma permanente | La despensa y el congelador | Guardar y cargar datos se vuelve lento; un disco lleno puede detener una base de datos |
| Tarjeta de red | Envía y recibe datos a otras máquinas | La ventanilla que da al comedor | Las solicitudes esperan para enviarse o recibirse; los usuarios notan retrasos |
Qué es un programa
Un programa es una lista de instrucciones precisas que una computadora sigue para hacer un trabajo: calcular una factura, mostrar una página web, enviar un pago. La palabra que importa es precisas. Un cocinero humano puede arreglárselas con "sal al gusto"; una computadora no. Hace exactamente lo que se le dice, exactamente en el orden en que se le dice, y nada más. Si una instrucción dice que se divida entre un número que resulta ser cero, la computadora no se encoge de hombros y sigue adelante: el programa se detiene con un error, a menos que el programador haya escrito más instrucciones que digan qué hacer en ese caso.
En el nivel más bajo, la CPU repite un ciclo mientras está encendida: obtener la siguiente instrucción de la memoria, decodificar lo que significa, ejecutarla y pasar a la siguiente. Cada instrucción es diminuta: sumar dos números, comparar dos valores, copiar un valor de un lugar de la memoria a otro, saltar a otra instrucción si una comparación resultó verdadera. Todo lo que hace una computadora, desde reproducir una película hasta entrenar un modelo de IA, se construye con miles de millones de estos pequeños pasos.
Software es el término más amplio: los programas junto con los datos, la configuración y la documentación que necesitan. Una aplicación (o app) es software que hace algo útil para una persona, como una aplicación bancaria. El software de sistema, como el sistema operativo que se describe más abajo, existe principalmente para ejecutar otro software.
Capas: las aplicaciones, el sistema operativo y el hardware
El código fuente y los lenguajes de programación
Las CPU solo entienden código máquina: instrucciones codificadas como números, específicas de una familia de procesadores. Los chips de Intel y AMD de la mayoría de las computadoras portátiles y servidores usan una familia (x86-64); los chips de los teléfonos, de los Mac recientes y de muchos servidores en la nube usan otra (Arm). El código máquina que funciona en una no funciona en la otra. Escribir código máquina a mano es lento, propenso a errores y casi nunca se hace hoy en día.
En su lugar, las personas escriben código fuente en un lenguaje de programación: una notación definida con precisión que se lee en parte como el inglés y en parte como las matemáticas. Python, JavaScript, Java, C#, Go, Rust y C son ejemplos muy usados. Se llaman lenguajes de alto nivel porque permiten a un programador decir "suma los precios de este carrito" en una o dos líneas en lugar de decenas de instrucciones máquina, y porque ocultan los detalles de la CPU concreta. El código fuente es texto plano, guardado en archivos, y es lo que los programadores leen, escriben, revisan y guardan en el control de versiones (un sistema que conserva el historial de cada cambio, explicado en la última sección de este capítulo).
Los lenguajes se diferencian en lo que facilitan. Python se valora por su legibilidad y domina el trabajo de ciencia de datos e IA; JavaScript es el lenguaje que ejecutan todos los navegadores web; Java y C# son habituales en los grandes sistemas empresariales; Go es popular para los servicios de red; C y Rust se usan donde cada microsegundo y cada byte importan, como en los sistemas operativos y las bases de datos. Ningún lenguaje es el mejor en todo, y elegir uno es en parte una decisión técnica y en parte una cuestión de qué habilidades tiene el equipo.
Del código fuente a un programa en ejecución: compiladores, intérpretes y máquinas virtuales
El código fuente tiene que convertirse en algo que la CPU pueda ejecutar. Hay tres enfoques generales, y los lenguajes reales a menudo los combinan.
Un compilador es un programa que traduce todo el código fuente de un programa a otra forma antes de que se ejecute. Los lenguajes como C, C++, Rust y Go suelen compilarse de antemano a código máquina para una CPU y un sistema operativo concretos. El resultado es un archivo ejecutable que arranca rápido y funciona deprisa, pero hay que compilar una versión distinta para cada tipo de máquina que quieras admitir, y el compilador rechaza el programa si encuentra ciertos tipos de error.
Un intérprete es un programa que lee el código fuente (o algo muy parecido) y va ejecutando sus instrucciones sobre la marcha, paso a paso. La interpretación facilita probar cosas y ejecutar el mismo programa en cualquier máquina que tenga el intérprete, a costa de la velocidad.
Muchos lenguajes populares se sitúan en un punto intermedio. El código fuente de Java lo compila una herramienta llamada javac a bytecode: instrucciones compactas para una computadora imaginaria e idealizada, en lugar de para una CPU real. La Máquina Virtual de Java (JVM) ejecuta después ese bytecode en cualquier máquina real en la que esté instalada, y así es como un mismo programa Java funciona sin cambios en Windows, macOS y Linux. La JVM observa qué partes del programa se ejecutan con más frecuencia y las compila a código máquina mientras el programa está en marcha, una técnica llamada compilación just-in-time (JIT). C# en .NET funciona de forma parecida. La implementación estándar de Python, CPython, también compila tus archivos .py a bytecode (guardando en caché el bytecode de los módulos importados en archivos .pyc) y después ejecuta ese bytecode en su propio bucle de intérprete.
Dos rutas del código fuente a la CPU
Comparación de los enfoques
| Enfoque | Lenguajes típicos | Ventajas | Costos |
|---|---|---|---|
| Compilación de antemano a código máquina | C, C++, Rust, Go | Arranque y ejecución rápidos; un único archivo autónomo que desplegar | Hay que compilar una versión distinta por CPU y sistema operativo; el ciclo de editar y probar es más lento |
| Bytecode más una máquina virtual con JIT | Java, Kotlin, C# | Una misma compilación funciona en muchas plataformas; el JIT puede hacer muy rápidos los servicios que se ejecutan durante mucho tiempo | Hay que instalar el runtime; arranque más lento y memoria adicional mientras el JIT entra en calor |
| Bytecode más un intérprete | Python (CPython), Ruby | Rápido de escribir y probar; muy portable | Suele ser más lento en cálculos pesados; las partes críticas para la velocidad a menudo se escriben en C y se llaman desde Python |
| Compilación JIT en el navegador o el servidor | JavaScript (V8 en Chrome y Node.js) | Funciona en cualquier lugar donde funcione un navegador; lo bastante rápido para la mayor parte del trabajo web | El rendimiento puede ser menos predecible que el del código compilado |
La función del sistema operativo
Cuando enciendes una computadora, el primer programa importante que se inicia es el sistema operativo (SO): Windows, macOS, Linux, Android o iOS. La mayoría de los servidores de internet ejecutan Linux. El sistema operativo tiene cuatro funciones principales.
Primero, reparte la CPU. Una computadora portátil puede tener unas cuantas decenas de programas en marcha pero solo un puñado de núcleos, así que el SO alterna cada núcleo entre los programas muchas veces por segundo, lo bastante rápido como para que todos parezcan ejecutarse a la vez. Segundo, gestiona la memoria: da a cada programa su propia región e impide que un programa lea o sobrescriba la de otro. Tercero, proporciona archivos y dispositivos: los programas piden al SO que abra un archivo, dibuje en la pantalla o envíe datos por la red, y el SO, mediante pequeños programas llamados drivers, se ocupa del hardware concreto. Cuarto, aplica la seguridad: usuarios, permisos y la regla de que una aplicación no puede leer tus fotos a menos que se lo permitas.
Los programas hacen estas solicitudes mediante llamadas al sistema, un conjunto fijo de servicios que ofrece el SO. Este es el primer ejemplo del curso de una idea arquitectónica importante: una capa que oculta los detalles engorrosos tras un conjunto estable de solicitudes. Quien escribe una aplicación de reparto de comida no necesita saber de qué marca es el chip de red del teléfono.
Procesos, hilos, memoria y disco
Un archivo de programa guardado en el disco no es más que instrucciones almacenadas. Cuando lo inicias, el SO crea un proceso: una instancia en ejecución del programa con su propia memoria privada, su propia identidad y su propia parte del tiempo de CPU. Abre dos ventanas del navegador y puede que tengas varios procesos; inicia un servidor web y puede ejecutarse como un solo proceso o como muchos.
Dentro de un proceso puede haber varios hilos. Un hilo es una secuencia de instrucciones que se está ejecutando. Un proceso con varios hilos puede hacer varias cosas a la vez, por ejemplo atender las solicitudes de dos clientes al mismo tiempo, y todos sus hilos comparten la memoria del proceso. Compartirla es cómodo pero arriesgado: si dos hilos modifican el mismo dato en el mismo momento sin coordinarse, el resultado puede ser erróneo de formas que solo aparecen de vez en cuando. Estos bugs de concurrencia están entre los más difíciles de encontrar, y esa es una de las razones por las que muchos sistemas prefieren procesos separados, o diseños en los que cada solicitud se atiende de forma independiente.
La diferencia entre la memoria y el disco explica gran parte del diseño de software. Acceder a los datos en RAM es rápido, aproximadamente una décima de microsegundo, pero desaparecen cuando el proceso termina, falla o la máquina se reinicia. Los datos en disco se conservan, pero acceder a ellos lleva mucho más tiempo: decenas de microsegundos o más en una unidad de estado sólido, y varios milisegundos en un disco giratorio. Por eso, cualquier información que no deba perderse, como un pedido o un pago, tiene que escribirse en un almacenamiento duradero, normalmente a través de una base de datos, antes de que el sistema le diga al cliente "listo".
Un proceso, sus hilos, su memoria y el disco
PRICE_PER_ITEM = 12.50 # pounds
VAT_RATE = 0.20 # 20 per cent
def total_with_vat(quantity):
subtotal = quantity * PRICE_PER_ITEM
return subtotal * (1 + VAT_RATE)
quantity = int(input("How many items? "))
print("Total to pay:", round(total_with_vat(quantity), 2))Ejemplo práctico
Ejemplo práctico: qué ocurre cuando se ejecuta el programa
Supón que el código anterior está guardado como order.py y escribes python order.py, y después introduces 3.
| 1. Inicio | El sistema operativo crea un proceso nuevo, le asigna memoria e inicia dentro de él el intérprete de Python (que a su vez es un programa compilado). |
|---|---|
| 2. Compilación | CPython lee order.py del disco, comprueba que es Python válido y lo compila a bytecode. Un paréntesis o unos dos puntos que falten se notificarían aquí como un error de sintaxis, antes de que se ejecute nada. |
| 3. Definición | El intérprete ejecuta el bytecode desde el principio. Guarda 12.5 y 0.2 en la memoria con los nombres PRICE_PER_ITEM y VAT_RATE, y registra la función total_with_vat sin ejecutarla todavía. |
| 4. Entrada | input() pide al SO que lea del teclado. El proceso espera, sin usar CPU, hasta que escribes 3 y pulsas Intro. int() convierte el texto "3" en el número 3. |
| 5. Cálculo | La función se ejecuta: 3 por 12.5 es 37.5, y después 37.5 por 1.2 es 45.0. Cada paso es un puñado de instrucciones de bytecode, y cada una de ellas se convierte en muchas instrucciones máquina en la CPU. |
| 6. Salida | print() pide al SO que muestre Total to pay: 45.0 en la pantalla. |
| 7. Fin | El programa llega a su última línea, el proceso termina y el SO recupera su memoria. No se guardó nada en el disco, así que el resultado se pierde a menos que lo hayas copiado. |
Incluso este programa de nueve líneas depende del sistema operativo para la memoria, el teclado y la pantalla, y de un runtime del lenguaje para traducirlo para la CPU.
Si escribieras three en lugar de 3, int() fallaría con un error en tiempo de ejecución y el programa se detendría: el código nunca dijo qué hacer con una entrada incorrecta.
El programa guarda el dinero en un número de coma flotante para ser breve. Los sistemas financieros reales guardan peniques enteros o usan un tipo decimal, por razones que explica la sección siguiente.
Bugs: cuando las instrucciones están mal
Un bug es un error en el software que hace que se comporte de forma distinta a la prevista. Como las computadoras siguen las instrucciones al pie de la letra, casi todos los bugs son una diferencia entre lo que el programador quería decir y lo que realmente escribió, o una situación en la que nadie pensó.
Conviene distinguir tres tipos. Un error de sintaxis rompe la gramática del lenguaje (un paréntesis que falta), así que el compilador o el intérprete se niega a ejecutar el programa; estos se detectan de inmediato. Un error en tiempo de ejecución ocurre mientras el programa se ejecuta, como dividir entre cero o intentar leer un archivo que no existe; el programa suele detenerse con un mensaje de error. Un error lógico es el más peligroso: el programa se ejecuta sin problemas y da la respuesta equivocada, por ejemplo cobrando el IVA dos veces. Nada falla de forma visible, así que solo las pruebas, una revisión cuidadosa o un cliente descontento lo pondrán al descubierto.
Los equipos profesionales cuentan con que habrá bugs y construyen defensas: pruebas automatizadas, revisión de código, lanzamientos graduales y una monitorización que detecta cuándo aumentan las tasas de error. Muchas de estas defensas son arquitectónicas, y aparecen a lo largo de todo este curso.
Por qué esto importa para la arquitectura
La arquitectura trata de las grandes decisiones que son costosas de cambiar, y varias de ellas se basan en las ideas de esta sección. La elección del lenguaje y del runtime afecta al tiempo de arranque, al uso de memoria, a qué bibliotecas puedes usar y a quién puedes contratar. La división entre memoria y disco decide qué debe almacenarse de forma duradera y cómo se recupera un sistema después de un fallo. Los procesos y los hilos son las unidades que se ejecutan, fallan y escalan: un capítulo posterior plantea si un sistema debe ser un solo proceso o muchos que cooperan. Y el diseño en capas del sistema operativo, que oculta el hardware tras un conjunto estable de solicitudes, es el primer ejemplo de un principio que volverás a encontrar como ocultación de información.
La sección siguiente examina aquello con lo que realmente trabajan los programas: los datos, y las estructuras y algoritmos que hacen que trabajar con ellos sea rápido o lento.
Comprueba lo que has entendido
¿Por qué desaparecen los datos de un programa cuando falla, a menos que se hayan guardado?
Mientras un programa se ejecuta, sus datos están en la RAM, que pertenece al proceso y se borra cuando el proceso termina o la máquina se reinicia. Solo sobreviven los datos escritos en un almacenamiento como un disco o una base de datos.
¿Cuál es la diferencia entre un compilador y un intérprete?
Un compilador traduce todo el programa a otra forma (código máquina o bytecode) antes de que se ejecute; un intérprete ejecuta las instrucciones a medida que las lee. Muchos lenguajes, como Java y Python, compilan primero a bytecode y después ejecutan ese bytecode en una máquina virtual.
¿Por qué un programa Java compilado en una computadora portátil con Windows suele poder ejecutarse sin cambios en un servidor Linux?
Java se compila a bytecode, no a código máquina para una CPU concreta. Cualquier máquina que tenga instalada una Máquina Virtual de Java puede ejecutar ese bytecode.
¿Por qué los programas piden al sistema operativo que lea un archivo o envíe datos, en lugar de controlar ellos mismos el disco o la tarjeta de red?
El sistema operativo reparte el hardware de forma segura entre los programas, aplica los permisos y oculta los detalles de cada dispositivo tras las llamadas al sistema, de modo que las aplicaciones no necesitan saber en qué hardware se están ejecutando.
Fuentes y lecturas recomendadas
¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.
Pregunta: “Si Python es más lento que C, ¿por qué tantas empresas construyen sus sistemas de backend en Python?”