La memoria más rápida de la computadora son los registros, ubicados en cada uno de los núcleos de cada CPU. Las arquitecturas tipo RISC (Reduced Instruction Set Computer) sólo permiten la ejecución de instrucciones entre registros (excepto, claro, las de carga y almacenamiento a memoria primaria). Los primeros CPU trabajaban con pocos registros, muchos de ellos de propósito específico, se regían más bien con una lógica de registro acumulador. Por ejemplo, el MOS 6502 (en el cual se basaron las principales computadoras de ocho bits) tenía un acumulador de ocho bits (A), dos registros índices de ocho bits (X e Y), un registro de estado del procesador de ocho bits (P), un apuntador al stack de ocho bits (S), y un apuntador al programa de 16 bits (PC). En contraposición con proceso, en un sistema por lotes se habla de tareas, una tarea requiere mucha menos estructura, típicamente basta con guardar la información relacionada con la contabilidad de los recursos empleados. Una tarea no es interrumpida en el transcurso de su ejecución. Ahora bien, esta distinción no es completamente objetiva y se pueden encontrar muchos textos que emplean indistintamente una u otra nomenclatura. Si bien el sistema brinda la ilusión de que muchos procesos se están ejecutando al mismo tiempo, la mayor parte de ellos típicamente está esperando para continuar su ejecución en un momento determinado sólo puede estar ejecutando sus instrucciones un número de procesos igual o menor al número de procesadores que tenga el sistema. Todos los sistemas de cómputo están compuestos por al menos una unidad de proceso junto con dispositivos que permiten ingresar datos (teclado, mouse, micrófono, etc.) y otros que permiten obtener resultados (pantalla, impresora, parlantes, etc.). Como se vio anteriormente, una de las funciones del sistema operativo es la de abstraer el hardware de la computadora y presentar al usuario una versión unificada y simplificada de los dispositivos. En este capítulo se verá la relación que mantiene el sistema operativo con el hardware, las funciones que cumplen y algunas abstracciones comunes utilizadas en sistemas operativos modernos. memoria de la siguiente instrucción a ejecutar. La arquitectura von Neumann fue planteada, obviamente, sin considerar la posterior diferencia entre la velocidad que adquiriría el CPU y la memoria. En 1977, John Backus presentó al recibir el premio Turing un artículo describiendo el cuello de botella de von Neumann. Los procesadores son cada vez más rápidos (se logró un aumento de 1 000 veces tanto entre 1975 y 2000 tan sólo en el reloj del sistema), pero la memoria aumentó su velocidad a un ritmo mucho menor; aproximadamente un factor de 50 para la tecnología en un nivel costo- beneficio suficiente para usarse como memoria primaria. Una respuesta parcial a este problema es la creación de una jerarquía de almacenamiento, yendo de una pequeña área de memoria mucho más cara pero extremadamente rápida y hasta un gran espacio de memoria muy económica, aunque mucho más lenta, como lo ilustran la figura 2.1 y el cuadro 2.1. En particular, la relación entre las capas superiores está administrada por hardware especializado de modo que su existencia resulta transparente al programador. Ahora bien, aunque la relación entre estos medios de almacenamiento puede parecer natural, para una computadora tiene una realidad completamente distinta: los registros son parte integral del procesador, y la memoria está a sólo un paso de distancia (el procesador puede referirse a ella directamente, de forma transparente, indicando la dirección desde un programa). Para efectos prácticos, el caché no se maneja explícitamente: el procesador no hace referencia directa a él, sino que es manejado por los controladores de acceso a memoria. El compilador2 busca realizar muchas operaciones que deben ocurrir reiteradamente, donde la rapidez es fundamental, con sus operadores cargados en los registros. El estado del CPU a cada momento está determinado por el contenido de los registros. El contenido de la memoria, obviamente, debe estar sincronizado con lo que ocurre dentro de éste — pero el estado actual del CPU, lo que está haciendo, las indicaciones respecto a las operaciones recién realizadas que se deben entregar al programa en ejecución, están todas representadas en los registros. Se debe mantener esto en mente cuando posteriormente se habla de todas las situaciones en que el flujo de ejecución debe ser quitado de un proceso y entregado a otro. La relación de la computadora y del sistema operativo con la memoria principal será abordada en el capítulo 5. LAS TERMINALES: excepciones resultan en una señal enviada al proceso, y este último es el encargado de tratar la excepción. En otros casos la falla o excepción son irrecuperables (una instrucción inválida o un error de bus) ante la cual el sistema operativo terminará el proceso que la generó. En el capítulo 5 se cubre con mucho mayor detalle un tipo de excepción muy importante que debe tratar el sistema operativo: el fallo de paginación. En los sistemas operativos modernos es común referirse al emulador de terminal, un programa especializado, ya sea para tener múltiples instancias de una terminal, o para ejecutar una terminal de texto dentro de una interfaz gráfica. Estos programas se denominan de esta forma dado que sólo replican el comportamiento de las terminales (que eran originalmente equipos independientes), siendo únicamente un programa que recibe la entrada del usuario por medio del teclado enviándola al sistema operativo como un flujo de datos, y recibe otro flujo de datos del sistema operativo, presentándolo de forma adecuada al usuario. Los dispositivos de almacenamiento (discos, memorias flash, cintas) pueden ser vistos como una región donde la computadora lee y escribe una serie de bytes que preservarán su valor, incluso luego de apagada la computadora. Para el hardware el sistema operativo no accede al dispositivo de almacenamiento byte por byte, sino que éstos se agrupan en bloques de tamaño fijo. El manejo de estos bloques (administración de bloques libres, lectura y escritura) es una tarea fundamental del sistema operativo, que asimismo se encarga de presentar abstracciones como la de archivos y directorios al usuario. Hoy en día, el acomodo más frecuente6 de estos buses es por medio de una separación en dos chips: el puente norte (Northbridge), conectado directamente al CPU, encargado de gestionar los buses de más alta velocidad y que, además, son fundamentales para el más básico inicio de la operación del sistema: la memoria y el reloj. La comunicación con algunas tarjetas de video se incorpora al puente norte a través del canal dedicado AGP (Advanced Graphics Port, Puerto Gráfico Avanzado). Al puente norte se conecta el puente sur (Southbridge), que controla el resto de los dispositivos del sistema. Normalmente se ven aquí las interfaces de almacenamiento (SCSI, SATA, IDE), de expansión interna (PCI, PCIe) y de expansión externa (USB, Firewire, puertos heredados seriales y paralelos). Un proceso, a lo largo de su vida, alterna entre diferentes estados de ejecución. Éstos son: Nuevo Se solicitó al sistema operativo la creación de un proceso, y sus recursos y estructuras están siendo creadas. Listo Está listo para iniciar o continuar su ejecución, pero el sistema no le ha asignado un procesador. En ejecución El proceso está siendo ejecutado en este momento. Sus instrucciones están siendo procesadas en algún procesador. Bloqueado En espera de algún evento para poder continuar su ejecución (aun si hubiera un procesador disponible, no podría avanzar). Zombi El proceso ha finalizado su ejecución, pero el sistema operativo debe realizar ciertas operaciones de limpieza para poder eliminarlo de la lista.1 Terminado El proceso terminó de ejecutarse; sus estructuras están a la espera de ser limpiadas por el sistema operativo. Equipo de trabajo al iniciar la porción multadillos del proceso, se crean muchos hilos idénticos, que realizarán las mismas tareas sobre diferentes datos. Este modelo es frecuentemente utilizado para cálculos matemáticos (p. ej.: criptografía, rendir, álgebra lineal). Puede combinarse con un estilo jefe/trabajador para irle dando al usuario una pre visualización del resultado de su cálculo, dado que éste se irá ensamblando progresivamente, pedazo por pedazo. Su principal diferencia con el patrón jefe/trabajador consiste en que el trabajo a realizar por cada uno de los hilos se plantea desde principio, esto es, el paso de división de trabajo no es un hilo más, sino que prepara los datos para que éstos sean lanzados en paralelo. Estos datos no son resultado de eventos independientes (como en el caso anterior), sino partes de un solo cálculo. Aunque una de las tareas principales de los sistemas operativos es dar a cada proceso la ilusión de que se está ejecutando en una computadora dedicada, de modo que el programador no tenga que pensar en la competencia por recursos, a veces un programa requiere interactuar con otros: parte del procesamiento puede depender de datos obtenidos en fuentes externas, y la cooperación con hilos o procesos externos es fundamental. Se verá que pueden aparecer muchos problemas cuando se estudia la interacción entre hilos del mismo proceso, la sincronización entre distintos procesos, la asignación de recursos por parte del sistema operativo a procesos simultáneos, o incluso cuando interactúan usuarios de diferentes computadoras de una red —se presentarán distintos conceptos relacionados con la concurrencia utilizando uno de esos escenarios, pero muchos de dichos conceptos en realidad son independientes del escenario: más bien esta sección se centra en la relación entre procesos que deben compartir recursos o sincronizar sus tareas. Para presentar la problemática y los conceptos relacionados con la concurrencia suelen utilizarse algunos problemas clásicos, que presentan casos particulares muy simplificados, y puede encontrárseles relación con distintas cuestiones que un programador enfrentará en la vida real. Cada ejemplo presenta uno o más conceptos. Se recomienda comprender bien el ejemplo, el problema y la solución y desmenuzar buscando los casos límite como ejercicio antes de pasar al siguiente caso. También podría ser útil imaginar en qué circunstancia un sistema operativo se encontraría en una situación similar, Para profundizar más en este tema, se recomienda el libro The little book of semaphores (Downey 2008). En este libro (de libre descarga) se encuentran muchos ejemplos que ilustran el uso de semáforos, no sólo para resolver problemas de sincronización, sino como un mecanismo simple de comunicación entre procesos. También se desarrollan distintas soluciones para los problemas clásicos (y no tan clásicos). Por otro lado, uno podría pensar (con cierta cuota de razón) que la secuencia de eventos propuesta es muy poco probable: usualmente un sistema operativo ejecuta miles de instrucciones antes de cambiar de un proceso a otro. De hecho, en la práctica este problema es muy frecuentemente ignorado y los programas funcionan muy bien la mayoría de las veces. Esto permite ver una característica importante de los programas concurrentes: es muy usual que un programa funcione perfectamente la mayor parte del tiempo, pero de vez en cuando puede fallar. Subsecuentes ejecuciones con los mismos argumentos producen nuevamente el resultado correcto. Esto hace que los problemas de concurrencia sean muy difíciles de detectar y más aún de corregir. Es importante (y mucho más efectivo) realizar un buen diseño inicial de un programa concurrente en lugar de intentar arreglarlo cuando se detecta alguna falla. También es interesante notar que, dependiendo del sistema, puede ser que alguna de las instrucciones sea muy lenta, en el caso de un sistema de reserva de asientos de aviones, las operaciones pueden durar un tiempo importante (p. ej.: desde que el operador muestra los asientos disponibles hasta que el cliente lo elige) haciendo mucho más probable que ocurra una secuencia no deseada. Si bien este análisis presenta aparentemente una problemática específica al planteamiento en cuestión es fácil ver que la misma circunstancia podría darse en un sistema de reserva de vuelos (p. ej.: puede que dos operadores vean un asiento vacío en su copia local de los asientos y ambos marquen el mismo asiento como ocupado) o con dos procesos que decidan cambiar simultáneamente datos en un archivo. Aquí las operaciones ya no son necesariamente internas de la máquina. Otra cuestión que puede parecer artificiosa es que en el ejemplo hay un solo procesador o núcleo. Sin embargo, tener más de un procesador no sólo no soluciona el problema, sino que lo empeora: ahora las operaciones de lectura o escritura pueden ejecutarse directamente en paralelo y aparecen nuevos problemas de coherencia de caché. En la siguiente discusión muchas veces se presupone que hay un solo procesador, sin que eso invalide la discusión para equipos multiprocesadores. Manipulación de datos que requiere la garantía de que se ejecutará como una sola unidad de ejecución, o fallará completamente, sin resultados o estados parciales observables por otros procesos o el entorno. Esto no necesariamente implica que el sistema no retirará el flujo de ejecución en medio de la operación, sino que el efecto de que se le retire el flujo no llevará a un comportamiento inconsistente. Dado que el sistema no tiene forma de saber cuáles instrucciones (o áreas del código) deben funcionar de forma atómica, el programador debe asegurar la atomicidad de forma explícita, mediante la sincronización de los procesos. El sistema no debe permitir la ejecución de parte de esa área en dos procesos de forma simultánea (sólo puede haber un proceso en la sección crítica en un momento dado).