La Evolución del Sistema de tiempo de cómputo compartido UNIX
Por Dennis M. Ritchie
Ritchie y Thompson fueron los principales desarrolladores del Sistema UNIX y coautores del artículo original que lo describía, publicado en 1974 en Comm. Assoc. Compo Mach.
Paper original "The evolution of the UNIX Timesharing System" como aparece en Microsystems, octubre de 1984.
ABSTRACT
Este paper presenta una breve historia del desarrollo primigenio del sistema operativo Unix. Se concentra en la evolución del sistema de ficheros, el mecanismo de control de procesos, y la idea de comandos entubados. Cierta atención se presta a las condiciones sociales existentes durante el desarrollo del sistema.
Durante los pocos años que han pasado, el sistema operativo UNIX ha llegado al uso difundido. Tan amplio ha sido que su propio nombre se ha convertido en una marca registrada de los Laboratorios Bell. Sus importantes características se han vuelto conocidas a muchas personas. Desde la primer publicación en 1974 que lo describía, ha sido objeto de mucha reescritura y reordenamiento, ha recibido escasos cambios fundamentales. Sin embargo, UNIX se originó en 1969, no en 1974, y la descripción de su desarrollo conforma una historia poco conocida y tal vez instructiva. Este paper representa la historia técnica y social de la evolución del sistema.
ORIGENES
Para la ciencia del cómputo en los Laboratorios Bell, el período 1968-1969 constituyó algo relativamente incómodo. El motivo principal para dicha incomodidad fue el repliegue lento, aunque claramente inevitable, de los Laboratorios del proyecto Multics. Para la comunidad de cómputo de los Laboratorios en un todo, el problema era la cada vez mayor obviedad del fracaso de Multics para ofrecer un tipo de sistema cualquiera que fuese utilizable rápidamente (mucho menos la panacea computacional que de él se había visionado). Durante gran parte de este período, el Centro de Cómputo de Murray Hill (Nueva Jersey) operaba también una costosa máquina General Electric GE-645, que simulaba inadecuadamente una GE-635. La separación organizativa entre los servicios de cómputo y la investigación del cómputo fue otra inconveniencia que ocurrió en dicho período.
Desde el punto de vista del grupo mayormente involucrado en los comienzos de UNIX (K. Thomson, Ritchie, M. D. McIlroy, J. Ossana) el declive y caída de Multics había hecho sentir su efecto de manera directa. Nos encontrábamos entre los últimos reductos de los Laboratorios Bell que realmente trabajaban con Multics, de modo que aún reteníamos cierto interés en su éxito. Más importante, el conveniente servicio interactivo de cómputo que Multics había prometido a una comunidad entera estaba disponible de hecho para nuestro limitado grupo, desde un comienzo bajo la égida del sistema CTSS empleado para el desarrollo de Multics [N.d.T. c.1963], y mas adelante bajo Multics en sí [N.d.T. c.1966]. Incluso aunque Multics se presentara incapaz de soportar muchos usuarios, podía darnos soporte a nosotros (a pesar de hacerlo a un costo exorbitante). No deseábamos perder el agradable nicho que ocupábamos, puesto que no existían otros similares disponibles; incluso el servicio de tiempo de cómputo compartido que luego se ofrecería bajo el sistema operativo de General Electric por entonces no existía aún. Lo que nosotros queríamos preservar era no sólo un buen ambiente de programación, sino uno alrededor del cual también pudiese formarse una comunidad. Sabíamos por experiencia que la esencia del cómputo comunal - como aquella provista por el acceso remoto a máquinas de tiempo compartido - no equivale meramente a mecanografiar programas en un terminal teletipo en lugar de introducirlos a través de conmutadores, sino el promover una comunicación cercana.
Por estos motivos comenzamos durante 1969 la búsqueda de una alternativa a Multics. La investigación tomó varios rumbos. A lo largo de 1969 nosotros (principalmente Ossanna, Thompson, Ritchie) presionamos intensamente para obtener la compra de una máquina de escala media, para la cual prometimos escribir un sistema operativo; la máquina sugerida fue la DEC PDP-10 y mas adelante la SDS (luego Xerox) Sigma 7. Dicho esfuerzo fue frustrante, pues nuestras propuestas jamás fueron rechazadas, pero aún así nunca fueron claramente aceptadas. En varias ocasiones pareció encontrarnos próximos al éxito. El golpe final a dichos esfuerzos arribó al presentar una propuesta exquisitamente rebuscada y diseñada para minimizar la erogacón financiera, la cual involucraba cierta compra directa, algún leasing a terceros, y un plan para convertir un procesador DEC KA-10 en el próximo a anunciarse y más capacitado KI-10 [N.d.T. distintas versiones de CPU de la DEC PDP-10]. Tal propuesta fue rechazada, y los rumores que pronto llegaron tendían a que W. O. Baker (por entonces vicepresidente de Investigaciones) había reaccionado con el comentario “¡los Laboratorios Bell no hacen negocios de esta manera!”. Restrospectivamente, resulta perfectamente obvio (y debería haberlo sido entonces) que estábamos proponiendo a los Laboratorios gastar mucho dinero en muy pocas personas y con un plan muy vago. Además, me encuentro bastante seguro que en dicho momento el área de sistemas Operativos no resultaba lo suficientemente atractiva como para que la Administración apoyara nuestra labor. La misma se encontraban en el proceso de extraerse a sí misma no sólo de los esfuerzos de desarrollo de sistemas operativos que habían fracasado, sino incluso de gestionar un Centro de Cómputo Local. Por lo tanto, puede haber parecido que la compra de una máquina como la que habíamos sugerido podía buscar la concreción de otro Multics más, o de otra manera, que si producíamos algo útil, deberían montar otro centro de cómputo más, del cual habrían de hacerse responsables.
Junto a estas agitaciones financieras ocurridas en 1969, también deben nombrarse rispideces técnicas. Thompson, R, H Cadanady y Ritchie llevaron a cabo (en pizarras y blocks de notas) el diseño de un sistema de archivo básico que se convertiría luego en el corazón de UNIX. La mayoría del diseño era obra de Thompson, como también lo fue el impuso de concebir los sistemas de archivaje en sí, pero creo que yo contribuí con la idea de los ficheros de dispositivos. Durante este período el impulso de Thompson para la creación de un sistema operativo tuvo varias formas; también escribió una simulación bastante detallada (en Multics) de la performace del diseño propuesto para el sistema de archivos, y el comportamiento de los programas de paginado. Por demás, comenzó a trabajar en un sistema operativo nuevo para la GE-645, incluso llegando a escribir un ensamblador para dicha máquina, junto a un núcleo rudimentario para un sistema operativo, cuyo mayor logro - según creo recordar - era escribir un mensaje de bienvenida. La complejidad de la máquina era tal que obtener un simple mensaje era ya un logro bastante notable, pero cuando se volvió claro que la vida útil de la GE645 en los Laboratorios Bell se mediría en meses, dicha labor fue abandonada.
También durante 1969, Thompson desarrolló un juego del “Viaje Espacial” [N.d.T. Space Travel], primero escrito en Multics, transliterado luego a FORTRAN para GECOS (el sistema operativo para la General Electric, luego Honeywell 635), que no era menos que una simulación del movimiento de los cuerpos mayores del Sistema Solar, con un jugador que guiaba una nave de aquí a allá, observando el escenario, e intentando aterrizar en varios planetas y lunas. La versión GECOS resultó insatisfactoria en dos aspectos importantes: primero, la presentación del estado del juego era atolondrada y difícil de controlar puesto que uno debía mecanografiar comandos para hacerlo funcionar, y en segundo lugar, una partida costaba unos 75 dólares de tiempo de CPU en un gran computadora. Por lo tanto no pasó mucho tiempo hasta que Thompson encontrara un procesador poco utilizado; un sistema al completo que se utilizaba como terminal Graphic-II. Él y yo reescribimos Space Travel para que pudiese correr en dicha máquina. La labor fue más ambiciosa de lo que podría parecer; puesto que tomamos la actitud de despreciar todo software existente, debimos forzosamente reescribir un paquete de aritmética de coma flotante, la especificación de puntos para los caracteres gráficos del trazador presentador, y un subsistema de depuración que mostrara de manera continua los contenidos de las localizaciones de memoria tipeadas en un costado de la pantalla. Todo esto fue escrito en lenguaje ensamblador para un compilador cruzado que corría bajo GECOS y producía cintas de papel perforado que podían ser llevadas a la PDP-7.
Aunque era un juego muy atractivo, Space Travel principalmente sirvió como una introducción a la torpe tecnología de preparar programas para la PDP-7. Un sistema de archivos sin una manera de ejercitar es una proposición estéril, de modo que pronto Thompson comenzó a implementar el sistema de archivo en papel (una descripción más precisa tal vez sea “sistema de archivo de tiza”). que anteriormente había diseñado. Procedió a concretarlo junto con otras funcionalidades necesarias para obtener un sistema operativo funcional (en particular la noción de procesos). Luego se afrontó un pequeño conjunto de utilidades de nivel usuario: esto implica una manera de copiar, imprimir, borrar y editar ficheros, y por supuesto un intérprete de comandos (shell) simple. Hasta este momento todos los programas habían sido escritos empleando GECOS y los archivos se transferían a la PDP-7 en cinta de papel; pero una vez que completamos un ensamblador, el sistema fue capaz de auto-soportarse. El sistema operativo como lo conocemos hoy nació, aunque no sería hasta bien entrado 1970, que Brian Kerningham sugirió - como una broma algo traicionera sobre “Multics” - el nombre “UNIX”.
El sistema de archivos del UNIX para la PDP-7
Estructuralmente, el sistema de archivos del UNIX para la PDP-7 era casi idéntico al de hoy. Tenía:
- Una i-list: un arreglo lineal de i-nodos, cada uno describiendo un fichero. Un i-nodo contenía menos de lo que tiene ahora, pero la información esencial era la misma: el modo de protección del fichero, su tipo y tamaño, y un listado de bloques físicos que daban soporte a sus contenidos.
- Directorios. Un tipo de fichero especial, que contenía una secuencia de nombres y el número-i asociado.
- Ficheros especiales que describían dispositivos. No se contenía explícitamente una descripción de dispositivo en el i-nodo, pero en su lugar se codificaba la cantidad, los números-i específicos correspondían a ficheros específicos.
Desde este momento también se encontraban presentes las llamadas de sistema importantes. read, rite, open, creat (sic), close: con una excepción muy importante tratada abajo, eran las mismas que encontramos hoy día. Una diferencia menor era que la unidad de E/S era el Word, no el Byte, puesto que la PDP-7 era una máquina de direccionamiento de Word. En la práctica, esto significaba simplemente que todos los programas que hacían cargo de cadenas de caracteres ignoraban los caracteres nulos (ya que estos se usaban para redondear un fichero a un numero par de caracteres). Otra diferencia menor y ocasionalmente molesta era la falta de borrado y matado de procesos para las terminales. Las terminales, en efecto, siempre estaban configuradas en modo en bruto. Tan sólo unos pocos programas - notablemente la shell y el editor - se molestaban en implementar el procesado de borrado y matado.
A pesar de sus considerables similitudes con el sistema de ficheros actual, el sistema de archivos de la PDP-7 era de una manera destacablemente diferente: carecía de nombres de ruta, y cada argumento de nombre de fichero en el sistema estaba constituído por un nombre simple (sin “/”) tomada en relación al directorio actual. Los enlaces existían en el sentido usual de UNIX. Junto con un conjunto elaborado de convenciones, eran los medios principales por los cuales la falta de nombres de ruta se volvían aceptables.
La llamada link guardaba el formato:
link(dir, file, newname)
donde dir era un fichero de directorio en el directorio actual, file era una entrada existente en dicho directorio, y newname era el nombre del enlace (el cual estaba asociado al directorio actual). Puesto que dir necesitaba encontrarse en el directorio actual, resulta evidente que la actual prohibición contra los enlaces a directorios por entonces no se forzaba; el sistema de archivos del PDP-7 UNIX guardaba la forma de un gráfico general dirigido.
[Se ilustra Fig. Gráfica del sistema de archivaje original]
De modo que todo usuario no necesitaba mantener un enlace a todos los directorios de interés, existía un directorio llamadodd que contenía entradas para el directorio de cada usuario. por lo tanto, para hacer un enlace a un fichero x en el directorio ken, podría hacer
ln dd ken ken
ln ken xx
rm ken
Este esquema volvía a los directorios lo suficientemente difíciles de usar como para hacer que en la práctica no se los utilizara. Otra barrera importante era que no existía manera de crear un directorio mientras el sistema estaba en ejecución; todos ellos se fabricaban durante la recreación del sistema de archivos desde la cinta de papel perforado, y como tal los directorios eran en práctica y efecto un recurso no renobable.
La convención dd hacía al comando chdir relativamente conveniente. Aceptaba múltiples argumentos, y conmutaba al directorio actual a cada directorio nombrado. Por tanto,
chdir dd ken
…movería al directorio ken (incidentemente, chdir se escribía ch; porqué expandimos esto cuando pasamos a la PDP-11, no lo recuerdo).
La inconveniencia más seria de la implementación del sistema de archivos - además de la falta de nombres de ruta - era la dificultad en cambiar su configuración; como se ha mencionado, los directorios y los ficheros especiales se hacían tan solo cuando se recreaba el disco. La instalación de un nuevo dispositivo se volvía muy problemática, ya que el código para el dispositivo se repartía ampliamente a través de todo el sistema; por ejemplo, existían varios bucles que visitaban cada dispositivo a la vez. No resulta sorprendente que no existiese noción de montar un pack de disco duro removible, puesto que la máquina contaba únicamente con un disco de cabezal fijo.
El código del sistema operativo que implementaba este sistema de archivo era una versión drásticamente simplificada del esquema actual. Una de las simplificaciones importantes seguidas del hecho de que el sistema no era multiprogramado; sólo un programa se encontraba en memoria por vez, y el control se pasaba entre los procesos únicamente cuando había un intercambio explícito. Por ejemplo, había una rutina iget que volvía disponible un i-nodo nombrado, pero dejaba el i-nodo en una constante, una locación estática en lugar de devolver un puntero a una gran tabla de i-nodos activos. Un percusor del mecanismo de búfer actual estaba presente (con algo así como 4 bufers) pero esencialmente no había sobrepaso a la E/S del disco con computación. Esto se evitaba no meramente por simplicidad. El disco asociado a la PDP-7 era rápido para su tiempo, transfería una palabra de 18 bits cada 2 microsegundos. Por otro lado, la PDP-7 en sí tenía un ciclo de memoria de 1 microsegundo, y la mayoría de las instrucciones ocupaban dos ciclos (uno para la instrucción en si, y otro para su operando). Sin embargo, las instrucciones indirectamente direccionada requerían tres ciclos, y la indirección era bastante común, porque la máquina no tenía registros índice. Finalmente, el controlador DMA era incapaz de acceder a la memoria durante una instrucción. El problema era que el disco incurriría en errores de sobrepasaje si cualquiera de las instrucciones indirectamente direccionadas se ejecutaban mientras estaba transfiriendo. Por lo tanto el control no podía ser devuelto al usuario, ni de hecho podía el código general del sistema ser ejecutado con el disco en funcionamiento. Las rutinas de interrupción para el reloj y las terminales, que necesitaban estar corriendo todo el tiempo, tenían que ser codificados en una manera muy extraña para evitar la indirección.
Control de procesos
Por “control de procesos” quiero significar el mecanismo por el cual los procesos son creados y utilizados; hoy las llamadas de sistema fork, exec, wait y exit implementan tales mecanismos. A diferencia del sistema de archivos, que existe en casi su manera presente en los primeros días, el esquema de control de procesos se sometió a una notación considerable luego de que el PDP-7 UNIX ya se encontraba en uso. (La introducción de nombres de ruta en los sistemas PDP-11 fue ciertamente un avance notarial considerable, pero no un cambio en la estructura fundamental).
Hoy, la manera en la cual los comandos son ejecutados por la shell pueden resumirse como sigue:
- La shell lee una línea de comandos del terminal.
- crea un proceso hijo a través de fork.
- El proceso hijo usa exec para llamar al comando desde un fichero.
- Mientras tanto, la shell padre usa wait para esperar que el proceso del (comando) hijo termine llamando a exit.
- El shell padre vuelve al paso 1.
Los procesos (entidades de ejecución independientemente)) existían muy al comienzo del UNIX de PDP-7. De hecho había precisamente dos de ellos, uno para cada uno de las dos terminales conectadas a la máquina. No había fork, wait o exec. Había un exit, pero su significado era bastante diferente, como se verá. El bucle principal para la shell se producía de la siguiente manera:
- La shell cerraba todos sus ficheros abiertos, luego abría el fichero especial de terminal para la entrada y salida estándar (descriptores de fichero 0 y 1).
- Leía una línea de comandos de la terminal.
- Enlazaba al fichero especificando el comando, abría el fichero, y removía el enlace. Luego copiaba un programa de inicialización pequeño a la parte superior de memoria y saltaba a él; este programa inicializador leía el fichero sobre el código de la shell, luego saltaba a la primer localización del comando (en efecto, un exec).
- El comando hacía su trabajo, luego, terminaba llamando exit. La llamada exit causaba que el sistema leyera una copia fresca de la shell sobre el comando terminado, luego para saltar a su inicio (y por lo tanto, en efecto iba al paso 1).
La cosa más interesante sobre esta implementación primitiva es el grado en el cual anticipaba los temas que se desarrollarían más adelante. Ciertamente, no podía dar soporte a procesos en segundo plano ni a ficheros de comandos de shell (mucho menos caños y filtros), pero la redirección de E/S (a través de “<” y “>”) pronto estuvo incorporada, y se discute a continuación. La implementación de redirección fue bastante sencilla; en el paso tres indicado arriba, la shell simplemente reemplazaba su entrada estándar o salida con el fichero apropiado. Crucial para el desarrollo subsiguiente fue la implementación de la shell como un programa de nivel de usuario almacenado en un fichero, en lugar de ser parte del sistema operativo.
La estructura de este esquema de control de procesos, con un proceso por terminal, es similar a la de muchos sistemas interactivos, por ejemplo CTSS, Multics, Honeywell TSS, y los TSS y TSO de IBM. En general tales sistemas requieren mecanismos especiales para implementar facilidades útiles tales como computaciones desconectadas y ficheros de comando; UNIX en ficha etapa no se molestó en suplir tales mecanismos especiales. También exhibía algunos problemas de idiosincrasias irritantes. Por ejemplo, una shell recientemente recreada debía cerrar todos sus ficheros abiertos tanto para deshacerse de cualquier fichero abierto dejado por el comando que acababa de ejecutar, y para rescindir previas redirecciones de E/S. Luego debía reabrir el fichero especial correspondiente a su terminal, de forma de poder leer una nueva línea de comandos. No había directorio /dev (porque no había nombres de ruta); además, la shell no podía retener memoria a lo largo de distintos comandos, porque se re-ejecutaba nuevamente después de cada comando. Por lo tanto, se requería una convención de sistema de archivo adicional: cada dirección debía contener una entrada tty para un fichero especial que refería a la terminal del proceso que la había abierto. Si por accidente uno cambiaba a algún directorio que carecía de esta entrada, la shell podría quedar en un bucle insalvable; casi siempre el único remedio era reiniciar. (Algunas veces el enlace faltante podía ser concretado a partir de la otra terminal).
El proceso de control en su forma moderna fue diseñado e implementado en un par de días. Es sorprendente cuán fácilmente se incorporó al sistema existente; al mismo tiempo, es fácil ver cómo algunos de las funcionalidades ligeramente inusuales de diseño están presentes precisamente porque representaron cambios pequeños, fácilmente programados a lo que ya existía. Un buen ejemplo es la separación de las funciones de fork y exec. El modelo más común para la creación de nuevos procesos involucra especificar un programa para el proceso a ejecutar; en UNIX, un proceso bifurcado continúa ejecutando el mismo programa que su padre hasta que desarrolla un exec explícito. La separación de las funciones ciertamente no es única a UNIX, y de hecho estaba presente en el sistema de cómputo de tiempo compartido de Berkeley, que era bien conocido por Thompson. Aún así, parece razonable suponer que existe en UNIX principalmente por la facilidad con la cual fork pudo ser implementado sin cambiar muchas otras cosas más. El sistema ya manejaba múltiples (por ejemplo, dos) procesos; había una tabla de procesos, y los procesos se intercambiaban entre la memoria principal y el disco. La implementación inicial de fork sólo requirió:
- Expandir la tabla de procesos
- Agregar una llamada fork que copiara el proceso actual al área de intercambio de disco, usando las primitivas de intercambio de E/S ya existentes; y hacer algunos ajustes a la tabla de procesos.
De hecho, la llamada fork de la PDP-7 requería precisamente 27 líneas de código ensamblador. Por supuesto, otros cambios en el sistema operativo y los programas de usuarios fueron requeridos, y algunos de ellos fueron bastante interesantes e inesperados. Pero un fork-exec combinado podría haber sido considerablemente más complicado, si sólo porque exec como tal no existía; su función ya era realizada -usando E/S explícita- por la shell.
La llamada de sistema exit, la cual previamente leía una nueva copia de la shell (realmente una especie de exec automático pero sin argumentos) simplificada considerablemente; en la nueva versión, un proceso sólo tenía que limpiar su entrada de tabla de procesos, y ceder el control.
Curiosamente, las primitivas que se convirtieron en wait eran considerablemente más generales que en el esquema actual. un par de primitivas enviaban mensajes de una word entre procesos declarados:
smes (pid, mensaje)
(pid, mensaje)=rmes()
El proceso destino de smes no necesitaba tener ninguna relación ancestral con el receptor, aunque el sistema no proveía un mecanismos explícito para comunicar IDs de procesos, excepto que fork devolvía a cada uno de los padres e hijos la ID de su pariente. Los mensajes no eran enviados a cola; un remitente se retrasaba hasta que el receptor leía el mensaje.
La facilidad de mensajes fue utilizada de la siguiente manera: la shell padre -luego de crear un proceso para ejecutar un comando- envía un mensaje a los nuevos procesos por smes; cuando el comando terminaba (asumiendo que no intentaba leer ningún mensaje) el smes bloqueado de la shell se volvía -en efecto- el equivalente de wait.
Un protocolo diferente, que sacaba ventaja más de la generalidad ofrecida por el mensaje, se usaba entre el programa de inicialización y las shells para cada terminal. El proceso de inicialización, cuya ID era entendida como 1, creaba una shell para cada una de las terminales, y luego enviaba rmses; cada shell, cuando leían el final de su fichero de entrada, usaba smes para enviar un mensaje convencional “estoy finalizados” al proceso de inicialización, el cual recreaba un nuevo proceso de shell para dicho terminal.
No puedo recordar otro uso de mensajes. Esto explica porqué la facilidad fue reemplazada con la llamada wait en el sistema actual, la cual es menos general, pero más directamente aplicable al propósito deseado. Posiblemente relevante también sea el bug evidente el el mecanismo: si un proceso de comando intentaba usar mensajes para comunicarse con otros procesos, interrumpiría la sincronización de la shell. La shell dependía de enviar un mensaje que nunca fue recibido; su un comando ejecutaba rms, recibiría el mensaje falso de la shell, y causaría que la shell leyera otra línea de entrada de la misma manera que si el comando hubiese finalizado. Si se hubiera manifestado por si misma una necesidad para mensajes generales, el bug probablemente hubiese sido reparado.
En cualquier caso, el esquema de control de procesos nuevos instantáneamente volvió algunas funcionalidades muy valiosas en triviales de implementar; por ejemplo, los procesos abandonados (con “&”) y el uso recursivo de la shell como un comando. La mayoría de los sistemas deben suplir algún tipo de “envío de trabajo por lotes” y un intérprete de comandos especial para los ficheros distintos a los que se usan interactivamente.
A pesar de que la idea de múltiples procesos se introdujo muy fácilmente, hubieron algunos efectos posteriores que no fueron anticipados. El más memorable de todos estos se hizo evidente pronto tras que se puso en funcionamiento el nuevo sistema y aparentemente funcionaba. En el medio de nuestro júbilo, se descubrió que el comando chdir (cambiar directorio actual) había dejado de funcionar. Hubo mucha lectura de código fuente e introspección ansiosa sobre cómo el agregado de fork podría haber roto la llamada chdir. Finalmente apareció la verdad: en el viejo sistema chdir era un comando ordinario; ajustaba el directorio actual del (único) proceso asociado a terminal. Bajo el nuevo sistema, el comando chdir correctamente cambiaba el directorio actual del proceso creado para ejecutarlo, pero este proceso rápidamente terminaba y no tenía efecto alguno sobre la shell padre! Fue necesario hacer de chdir un comando especial, ejecutado internamente dentro de la shell. Resultó que varias funciones similares a comandos tenían la misma propiedad, por ejemplo login.
Otro desencuentro entre el sistema como había sido y el nuevo esquema de control de procesos llevaron más tiempo en volverse evidentes. Originalmente, el puntero read/write asociado con cada fichero abierto se almacenaba dentro del proceso que abría le fichero (este puntero indica dónde en el archivo la siguiente lectura o escritura tendrá lugar). El problema con esta organización se volvió evidente sólo cuando intentamos usar ficheros de comando. Supongamos un fichero de comando simple que contiene
ls
who
y se ejecuta como sigue:
sh comfile>output
La secuencia de eventos era:
- La shell principal crea un proceso nuevo, que abre outfile para recibir la salida estándar y ejecuta la shell recursivamente.
- La nueva shell crea otro proceso para ejecutar ls, el cual correctamente escribe en el fichero output y luego finaliza.
- Otro proceso es creado para ejecutar el siguiente comando. Sin embargo, el puntero de E/S para la salida es copiado desde aquella a la shell, y aún es 9, porque la shell nunca ha escrito sobre su salida, y los punteros E/S están asociados con procesos. El efecto es que la salida de who sobrescribe y destruye la salida del comando ls precedente.
La solución a este problema requirió la creación de una nueva tabla de sistema para contener los punteros dee E/S para abrir ficheros independientemente del proceso en el cual fueron abiertos.
Redirección de E/S
La muy conveniente notación para redirección de E/S, usando los caracteres “>” y “<”, no estaba presente desde el comienzo del sistema PDP-7 UNIX, pero apareció bastante al principio. Como mucho más en UNIX, estuvo inspirada por una idea de Multics. Multics tenía un mecanismo de E/S bastante general que involucraba cadenas de E/S designadas que podían ser redireccionadas dinámicamente a varios dispositivos, ficheros e incluso a través de módulos de procesamientos de cadenas especiales. Incluso en la versión de Multics.
Incluso en la versión de Multics que conocíamos hace una década, existía un comando para redirigir la salida posterior, normalmente destinada a la terminal, a un archivo, y otro para volver a enviarla a la terminal. Mientras que en UNIX se podría usar:
ls>xx
para obtener una lista de los nombres de los archivos en xx, en Multics la notación era:
iocall attach user_output
file xx list
iocall attach user_output
syn user-i/o
Aunque esta secuencia tan engorrosa se usaba con frecuencia en la época de Multics y habría sido muy fácil integrarla en la shell de Multics, la idea no se nos ocurrió ni a nosotros ni a nadie más en aquel momento. Supongo que la razón fue la enorme envergadura del proyecto Multics: los desarrolladores del sistema de entrada/salida estaban en Bell Labs en Murray Hill, mientras que la shell se desarrolló en el MIT. No nos planteamos modificar el intérprete de comandos (era su programa); por consiguiente, es posible que los responsables del intérprete ni siquiera conocieran la utilidad, aunque algo torpe, de iocall. (El manual de Multics de 1969 cataloga iocall como un comando «mantenido por el autor», es decir, no estándar). Dado que tanto el sistema de E/S de UNIX como su intérprete de comandos estaban bajo el control exclusivo de Thompson, cuando finalmente surgió la idea adecuada, su implementación solo llevó una hora aproximadamente.
La llegada del PDP-11
A principios de 1970, UNIX para PDP-7 ya estaba en funcionamiento. Si bien para los estándares actuales era primitivo, aún ofrecía un entorno de programación más agradable que sus alternativos.
Sin embargo, se hacía evidente que la PDP-7 - una máquina que ni siquiera poseíamos - se encontraba cursando su obsolescencia, y sus sucesoras de la misma serie ofrecían poco interés. A principios de 1970 propusimos la adquisición de una PDP-11, que Digital acababa de lanzar. En cierto modo, esta propuesta fue simplemente la última de una serie de intentos realizados durante el año anterior. Se diferenciaba en dos aspectos importantes. Primero, la cantidad de dinero solicitada (unos 65.000 dólares) era mucho menor a las anteriores; segundo, el objetivo ya no era simplemente desarrollar un sistema operativo no especificado, sino elaborar uno diseñado específicamente para editar y formatear texto (lo que hoy se denominaría un procesador de textos). La iniciativa surgió principalmente de J. F. Ossanna, quien, desde entonces y hasta el final de su vida, se interesó por el procesamiento de textos. Si nuestras primeras propuestas habían sido demasiado vagas, esta quizás era demasiado específica. Fue recibida con recelo en un principio también, sin embargo, se lograron conseguir los fondos poco después gracias a la gestión de L. E. McMahon, y en mayo se realizó un pedido de una PDP-11.
El procesador llegó a finales del verano, pero el PDP-II era un producto tan nuevo que no hubo disco disponible hasta diciembre. Mientras tanto, se escribió una versión rudimentaria de UNIX, solo para el núcleo, utilizando un ensamblador cruzado en el PDP-7. La mayor parte del tiempo, la máquina permanecía en un rincón, enumerando todos los recorridos cerrados del caballo en un tablero de ajedrez de 6x8, una tarea que duró tres meses.
El primer sistema PDP-11
Una vez que llegó el disco, el sistema se completó rápidamente. En su estructura interna, la primera versión de UNIX para el PDP-11 representó un avance relativamente menor con respecto al sistema PDP-7; su escritura fue en gran medida una cuestión de transliteración. Por ejemplo, no había multiprogramación; solo un programa de usuario estaba presente en el núcleo en cada momento. Por otro lado, se introdujeron cambios importantes en la interfaz de usuario: se implementó la estructura de directorios actual, con rutas completas, junto con la forma moderna de exec y wait, y funciones prácticas como el borrado de caracteres y el bloqueo de líneas para terminales.
Quizás lo más interesante del sistema era su reducido tamaño: contaba con 24 KB de memoria principal (16 KB para el sistema y 8 KB para programas de usuario) y un disco con bloques de 1 KB (totalizando 512 KB). Los archivos estaban limitados a 63 KB. Al realizar el pedido del PDP-11, parecía lógico - o quizás conveniente - prometer un sistema dedicado al procesamiento de textos. Durante la prolongada espera del hardware, la creciente utilidad de UNIX para PDP-7 justificó la creación de UNIX para PDP-11 como herramienta de desarrollo, que se utilizaría para escribir el sistema más específico. Para la primavera de 1971, existía un consenso general de que nadie tenía el menor interés en abandonar UNIX. Por lo tanto, transcribimos el formateador de texto roff al lenguaje ensamblador PDP-11, partiendo de la versión PDP-7 que había sido transcrita de la versión BCPL de McIlroy en Multics, que a su vez se había inspirado en el programa runoff de J. Saltzer en CTSS. A principios del verano, con el editor y el formateador en mano, nos sentimos preparados para cumplir con nuestro cometido al ofrecer un servicio de procesamiento de texto al departamento de Patentes para la preparación de solicitudes de patentes. En ese momento, estaban evaluando un sistema comercial para este propósito; las principales ventajas que ofrecíamos (además de la dudosa de participar en un experimento interno) eran dos: primero, admitíamos terminales Teletype modelo 37, que, con una caja de tipos ampliada, podían imprimir la mayoría de los símbolos matemáticos necesarios. En segundo lugar, rápidamente dotamos a Roff de la capacidad de generar páginas numeradas por línea, requisito indispensable para el responsable de patentes y que el otro sistema no podía gestionar.
Durante la segunda mitad de 1971, apoyamos a tres mecanógrafas del departamento de patentes, quienes dedicaban el día a escribir, editar y formatear solicitudes de patentes, mientras intentábamos continuar con nuestro propio trabajo. UNIX tiene fama de ofrecer servicios interesantes en hardware modesto, y este periodo podría marcar un punto álgido en la relación beneficio/equipo; en una máquina sin protección de memoria y con un único disquete de 0,5 MB, cada prueba de un nuevo programa requería cuidado y audacia, ya que podía provocar fácilmente un fallo del sistema, y cada pocas horas de trabajo de las mecanógrafas implicaban transferir más información a DECtape, debido al reducido tamaño del disquete.
El experimento fue agotador, pero exitoso. El departamento de patentes no solo adoptó UNIX, convirtiéndose así en el primero de muchos grupos en los Laboratorios en ratificar nuestro trabajo, sino que logramos la credibilidad suficiente para convencer a nuestra propia dirección de adquirir uno de los primeros sistemas PDP-11/45 fabricados. Desde entonces, hemos acumulado mucho hardware y trabajado continuamente en el software, pero dado que la mayor parte del trabajo interesante ya se ha publicado (por ejemplo, sobre el propio sistema 1,5,6 y las aplicaciones de procesamiento de texto), parece innecesario repetirlo aquí.
Tuberías
Una de las contribuciones más admiradas de UNIX a la cultura de los sistemas operativos y los lenguajes de comandos es la tubería, utilizada en una secuencia de comandos. Por supuesto, la idea fundamental no era nueva; la tubería es simplemente una forma específica de corrutina.
Incluso la implementación no carecía de precedentes, aunque no lo sabíamos en aquel momento; los “archivos de comunicación” del Sistema de Tiempo Compartido de Dartmouth 10 hacían prácticamente lo mismo que las tuberías de UNIX, aunque parece que no se explotaron al máximo. Las tuberías aparecieron en UNIX en 1972, mucho después de que la versión del sistema para PDP-11 ya estuviera en funcionamiento, por sugerencia (o quizás insistencia) de M. D. McIlroy, un defensor desde hace mucho tiempo del flujo de control no jerárquico que caracteriza a las corrutinas. Algunos años antes de que se implementaran las tuberías, sugirió que los comandos se concibieran como operadores binarios, cuyos operandos izquierdo y derecho especificaban los archivos de entrada y salida. Así, una utilidad de “copia” se comandaría mediante:
inputfile copy outputfile
Para crear una tubería, se podían apilar operadores de comandos. De este modo, para ordenar la entrada, paginarla correctamente e imprimir el resultado sin conexión, se escribiría:
input sort paginate offprint
En el sistema actual, esto correspondería a:
sort input|pr|opr
La idea, explicada una tarde en una pizarra, nos intrigó, pero no desencadenó ninguna acción inmediata. Hubo varias objeciones a la idea tal como se planteó: la notación infija parecía demasiado radical (estábamos demasiado acostumbrados a escribir ‘cp x y’ para copiar x a y); y no veíamos cómo distinguir los parámetros de comando de los archivos de entrada o salida. Además, el modelo de ejecución de comandos de una entrada y una salida parecía demasiado restrictivo. ¡Qué falta de imaginación!
Tiempo después, gracias a la persistencia de McIlroy, finalmente se instalaron las tuberías en el sistema operativo (una tarea relativamente sencilla) y se introdujo una nueva notación. Utilizaba los mismos caracteres que para la redirección de E/S. Por ejemplo, la tubería anterior podría haberse escrito así:
sort input>pr>opr>
La idea es que después de ‘>’ puede ir un archivo, para especificar la redirección de la salida a ese archivo, o un comando al que se dirige la salida del comando anterior como entrada. El ‘>’ final era necesario en el ejemplo para especificar que la salida (inexistente) de opr debía dirigirse a la consola; de lo contrario, el comando opr no se habría ejecutado en absoluto. En su lugar, se habría creado un archivo opr.
La nueva funcionalidad fue recibida con entusiasmo, y pronto se acuñó el término «filtro». Muchos comandos se modificaron para que pudieran utilizarse en tuberías. Por ejemplo, nadie había imaginado que alguien querría que la utilidad sort o pr ordenara o imprimiera su entrada estándar sin argumentos explícitos.
Pronto se hicieron evidentes algunos problemas con la notación. El más molesto era un problema léxico trivial: la cadena después de > estaba delimitada por espacios en blanco, por lo que, para pasar un parámetro a pr en el ejemplo, había que citar:
En segundo lugar, en un intento por lograr generalidad, la notación de tubería aceptaba < como una redirección de entrada de forma equivalente a >; esto significaba que la notación no era única. También se podría escribir, por ejemplo:
opr<pr<"sort input"<
o incluso
pr<sort input"<>opr>
La notación de tubería con ‘<’ y ‘>’ solo duró un par de meses; fue reemplazada por la actual, que utiliza un operador único para separar los componentes de una tubería. Si bien la notación antigua tenía cierto encanto y coherencia interna, la nueva es sin duda superior. Claro que también tiene limitaciones. Es claramente lineal, aunque existen ciertas situaciones que constituyen excepciones, en las que se requieren múltiples entradas y salidas redirigidas.
Por ejemplo, ¿cuál es la mejor manera de comparar las salidas de dos programas? ¿Cuál es la notación apropiada para invocar un programa con dos flujos de salida paralelos? En la sección sobre redirección de E/S, mencioné que Multics proporcionaba un mecanismo para dirigir los flujos de E/S a través de módulos de procesamiento hacia (o desde) el dispositivo o archivo que actuaba como origen o destino. Por lo tanto, podría parecer que la segmentación de flujos en Multics fue el precursor directo de las tuberías de UNIX, al igual que la redirección de E/S de Multics lo fue para su versión de UNIX. De hecho, no creo que esto sea cierto, o lo es solo en un sentido limitado. Las corrutinas no solo eran bien conocidas, sino que su implementación como módulos de E/S segmentables de Multics requería que estos módulos se codificaran de forma especial para que no pudieran utilizarse para ningún otro propósito. La genialidad de la tubería de UNIX reside precisamente en que se construye a partir de los mismos comandos que se utilizan constantemente de forma simplex. El salto mental necesario para vislumbrar esta posibilidad e inventar la notación fue considerable.
Lenguajes de alto nivel
Cada programa para el sistema UNIX original del PDP-7 se escribía en lenguaje ensamblador, y era lenguaje ensamblador puro; por ejemplo, no había macros. Además, no existía un cargador ni un editor de enlaces, por lo que cada programa debía ser completo en sí mismo. El primer lenguaje interesante que apareció fue una versión de TMG11 de McClure, implementada por McIlroy. Poco después de que TMG estuviera disponible, Thompson decidió que no podíamos pretender ofrecer un servicio informático real sin Fortran, así que se puso a escribir una versión de Fortran en TMG. Si mal no recuerdo, la intención de trabajar con Fortran duró aproximadamente una semana. En cambio, produjo una definición y un compilador para el nuevo lenguaje B. B estuvo muy influenciado por el lenguaje BCPL; otras influencias fueron el gusto de Thompson por la sintaxis espartana y el reducido espacio en el que debía caber el compilador. El compilador producía código interpretado sencillo; Aunque tanto el programa como los que generaba eran bastante lentos, hicieron la vida mucho más agradable. Una vez que se dispuso de interfaces para las llamadas al sistema habituales, volvimos a disfrutar de las ventajas de usar un lenguaje razonable para escribir lo que normalmente se denomina «programas de sistema»: compiladores, ensambladores y similares. (Si bien algunos podrían considerar que el PL/I que usábamos con Multics era poco razonable, era mucho mejor que el lenguaje ensamblador). Entre otros programas, el compilador cruzado B para PDP-7 para el PDP-11 se escribió en B, y con el tiempo, el compilador B para el propio PDP-7 se transcribió de TMG a B.
Cuando llegó el PDP-11, B se trasladó a él casi de inmediato. De hecho, una versión del programa de calculadora de escritorio de precisión múltiple fue uno de los primeros programas en ejecutarse en el PDP-11, mucho antes de la llegada del disquete. Sin embargo, B no se impuso de inmediato. Solo se consideró superficialmente la posibilidad de reescribir el sistema operativo en B en lugar de ensamblador, y lo mismo ocurrió con la mayoría de las utilidades. Incluso el ensamblador se reescribió en ensamblador. Este enfoque se adoptó principalmente debido a la lentitud del código interpretado. De menor importancia, pero no por ello menos relevante, fue la incompatibilidad entre el lenguaje B, orientado a palabras, y el PDP-11, que utilizaba direcciones de byte.
Así pues, en 1971, se comenzó a trabajar en lo que se convertiría en el lenguaje C. La historia del desarrollo de lenguajes, desde BCPL hasta C, pasando por B, se narra en otro lugar y no es necesario repetirla aquí.
Quizás el punto de inflexión más importante se produjo en 1973, cuando el núcleo del sistema operativo se reescribió en C. Fue entonces cuando el sistema adquirió su forma moderna; el cambio de mayor alcance fue la introducción de la multiprogramación. Hubo pocos cambios visibles externamente, pero la estructura interna del sistema se volvió mucho más racional y general. El éxito de este esfuerzo nos convenció de que C era útil como una herramienta casi universal para la programación de sistemas, en lugar de ser simplemente un juguete para aplicaciones sencillas.
Hoy en día, el único programa importante de UNIX que aún se escribe en ensamblador es el propio ensamblador; prácticamente todos los programas de utilidad están escritos en C, al igual que la mayoría de las aplicaciones, aunque también existen sitios con muchos programas en Fortran, Pascal y Algol 68. Parece indudable que gran parte del éxito de UNIX se debe a la legibilidad, la modificabilidad y la portabilidad de su software, lo cual, a su vez, se deriva de su expresión en lenguajes de alto nivel.
Conclusión
Una de las cosas reconfortantes de los viejos recuerdos es su tendencia a idealizarlos. El entorno de programación que ofrecían las primeras versiones de UNIX, al describirlo aquí, parece extremadamente duro y primitivo. Estoy seguro de que si me viera obligado a volver al PDP-7, lo encontraría intolerablemente limitado y carente de comodidades. Sin embargo, en aquel momento no lo parecía; la memoria se centra en lo bueno y lo que perduró, y en la alegría de contribuir a crear las mejoras que hicieron la vida más fácil. Dentro de 10 años, espero que podamos recordar con nostalgia.
La misma impresión mixta de progreso y continuidad.
Agradecimientos
Agradezco a S. P. Morgan, K. Thompson y M. D. McIlroy por proporcionar documentos iniciales y rescatar recuerdos.
Dado que mi principal interés es describir la evolución de las ideas, este artículo atribuye ideas y trabajo a individuos solo cuando parece más relevante.
En general, el lector no se equivocará si interpreta cada aparición de «nosotros» con antecedente poco claro como «Thompson, con cierta ayuda mía».
Dennis Ritchie, Centro de Investigación en Ciencias de la Computación, Laboratorios Bell de AT&T, 600 Mountain Ave., Murray Hill, NJ 07974.