¿Cómo se hizo el primer programa?
El primer programa se hizo en lenguaje de maquina. En unos y ceros. No había otra forma. Había que tener a mano la tabla de códigos de máquina (el código en 1s y 0s de: sumar, comparar, mover, etc.), y había que ir construyendo (en una hoja de papel) otra tabla, la tabla de las direcciones de memoria que asociaríamos a las variables de nuestro programa.
Con esa técnica, tabla de códigos y tabla de símbolos, se hizo el primer Ensamblador, un programa que convertía códigos mnemotécnicos, como ADD, COMPARE, MOVE... en códigos de máquina: 00001, 00010, 00110... Y que convertía símbolos (nombres de variables) en direcciones. Fue una gran ayuda, no se requería ya de consultar los códigos de instrucción ni anotar las direcciones de memoria.
Disponiendo de ese primer ensamblador se hizo un programa más complicado y ambicioso, un Compilador, que traduce expresiones escritas en notación matemática (como z = x + y) a secuencias de instrucciones de máquina que las implementa.
Con un primer compilador para un lenguaje modesto se hizo compiladores para lenguajes más ambiciosos, con nuevas e ingeniosas abstracciones. Los 1s y 0s fueron quedando muy atrás. Se comenzó a escribir los programas en base a abstracciones de alto nivel: iteraciones, decisiones, subprogramas, funciones, recursión, clases, objetos, vectores, matrices, tablas, stacks, colas, árboles.
Bien, ya tenemos traductores -ensambladores para lenguajes simbólicos y compiladores para lenguajes de alto nivel- pero, ¿cómo cargar esos traductores en memoria de modo de poder usarlos para traducir nuestros programas de aplicación? Un programa de miles de instrucciones, como lo son los traductores, no vamos a cargarlos bit a bit a través de un panel de switches... Surge el Cargado Básico, un programa simple que lee byte a byte por algún dispositivo de entrada, como un lector de cinta de papel, y va colocando esos bytes en memoria a partir de cierta dirección dada. El Cargado Básico consta apenas de una docena de instrucciones y ése sí lo cargamos manualmente en memoria, vía un panel de switches, y lo dejamos allí, listo para cargar los traductores.
Ya con el traductor en memoria, lo usaremos para traducir nuestro programa. La salida del traductor, nuestra aplicación ya en ejecutable, podría quedar en otra área de memoria, si es que hay suficiente memoria (recordar que está el traductor ocupando una buena parte). Si no hay tanta memoria (como no solía haberla), la salida del traductor será más bien perforada en cinta de papel y luego la cargamos, con el utilísimo Cargador Básico, eventualmente sobrescribiendo el traductor.
El Cargador Básico viene -desde hace ya algún tiempo- en una memoria ROM, de modo que no hay que cargar nada vía panel de switches. Ya no usamos cintas de papel ni tarjetas perforadas, porque disponemos de discos magnéticos u otros dispositivos de almacenamiento secundario de alta capacidad. Además de los traductores, utilizamos diversas herramientas, como editores de texto y manejadores de archivos. La memoria principal se ha hecho tan grande que pueden coexistir allí multitud de programas de aplicación. Los procesadores se han hecho tan rápidos que pueden atender concurrentemente multitud de aplicaciones. Esto requiere otro programa, un director de orquesta, que asigne memoria y otros recursos a los diversos programas, que coordine el uso del procesador central y de los dispositivos de e/s. Surge entonces el Sistema Operativo. Así que ahora, al encender la máquina, arranca un cargador básico que carga un cargador más elaborado que a su vez carga el sistema operativo, una secuencia de pasos que han denominado bootstrapping, cargarse a sí mismo.
Vamos a concluir cambiando la pregunta original, aquella de "¿Cómo se hizo el primer programa? por esta otra ¿Cuántas instrucciones de máquina se ejecutan desde que encendemos el computador, hasta que arranca la primera aplicación que queremos utilizar?
Todo comienza con la docena de instrucciones del Cargador Básico, viene ahora un cargador más elaborado que va a inicializar el hardware, va a cargar el núcleo del SO y el Shell (un conjunto de herramientas de uso general). Hace tiempo que los SO nos ofrecen una interfaz gráfica, pues bien en el proceso de arranque se va a inicializar este manejo de interfaz gráfica. Hay diversos dispositivos de E/S, se va a cargar los drivers para cada dispositivo. Hay puertos de red, se va a arrancar diversos servicios que usan puertos de red.
Digamos que la primera aplicación que deseamos usar es el correo. Pues se abrirá el navegador por defecto, damos el url que se refiere al servicio de correo, se resuelve ese url mediante el servicio de nombres de dominio (DNS), se arranca el protocolo de transmisión de hipertexto (HTTP), se cargan diversos scripts, se construye la imagen que nos ofrezca el cliente de correo y comienza nuestra interacción con la bandeja de entrada.
Es difícil estimar cuantas instrucciones estarán involucradas en esta secuencia de procesos, pero si observamos el tiempo que se necesitó desde el encendido hasta que comienza nuestra interacción con la bandeja de entrada, podremos hacer algunos estimados. Se tarda unos 2 o 3 segundos para bootstraping, unos 15 segundos la carga de Windows (en un procesador moderno, digamos un core-i5), unos 5 segundos para inicio de sesión de usuario, unos 3 segundos para el arranque del navegador (configurado para que vaya al grano, sin publicidad ni una plétora de extensiones), otros 5 segundos para que cargue el correo (con unas bandejas moderadas).
Considerando un procesador de 4 núcleos, que ejecuta unos 4 mil millones de instrucciones por segundo, en 30 segundo ejecutamos 120 mil millones de instrucciones.
¡Es una cifra asombrosa! Lo que estamos haciendo no parece nada complicado, recibir correo electrónico, recibir un mensaje de texto. Estamos pagando un enorme overhead. Hay, sin dudas, mucho software de por medio: cargadores, SO, controladores, aplicaciones.
El problema es que no estamos en una máquina dedicada a recibir correos, estamos dentro de un sistemas operativo que está manteniendo un mix de multiprogramación, haciendo frecuentes context switches (pasando ordenadamente el CPU de un proceso a otro), está administrando memoria, haciendo swapping de cache y de memoria virtual, atendiendo el más mínimo movimiento del mouse, recibiendo cada tecla pulsada en el teclado, abriendo y cerrando ventanas superpuestas, recibiendo múltiples mensajes por los puertos de red. Cada caracter en un dispositivo de e/s pasa varios niveles de software hasta que llega al correspondiente driver. En cualquier momento puede ocurrir excepciones que se deben manejar. El sistema está vigilando el nivel de carga de la batería, la temperatura del procesador, la inserción de nuevos dispositivos (pendrives, mouse, otros), el antivirus interviene con frecuencia, el navegador atiende varias pestañas, el sistema maneja varios usuarios y sesiones por lo que debe confirmar permisos e identificación... Cada una de esas tareas requiere miles y miles de instrucciones.
Todo esto podría ser un 15% más corto bajo Linux, pero igual son miles de millones de instrucciones., sigue siendo un increíble overhead. Hay que reconocer que buena parte de ese resultado se debe al crecimiento desordenado de los sistemas operativos, a la acumulación de protocolos que en aras de mantener compatibilidad con sistemas anteriores no se han simplificado, al contrario se amplían cada vez más para manejar situaciones que no se había considerado. La mayoría del software no se optimiza, simplemente se dispone de mucha memoria y de mucha velocidad, y eso se aprovecha alegremente. Hay mucho Fatware. Pero, hay también mucho trabajo que hacer en cada segundo de máquina, en ese incesante trabajo se basa la increíble flexibilidad y conveniencia del computador.
Comentarios
Publicar un comentario