Sistemas Operativos Modernos PDF
Document Details
Uploaded by DesirableFantasy4067
Tags
Summary
Este documento analiza los diseños de sistemas operativos modernos, centrándose en los desafíos relacionados con la eficiencia y el diseño de microkernels. El texto explora ejemplos de sistemas operativos como Symbian y los problemas asociados con la administración dinámica de la memoria en un nanokernel.
Full Transcript
958 CASO DE ESTUDIO 3: SYMBIAN OS CAPÍTULO 12 2. Haga una lista de tres mejoras en la eficiencia debido a un diseño de microkernel. 3. Haga una lista de tres problemas de eficiencia debido a un diseño de microkernel. 4. Symbian...
958 CASO DE ESTUDIO 3: SYMBIAN OS CAPÍTULO 12 2. Haga una lista de tres mejoras en la eficiencia debido a un diseño de microkernel. 3. Haga una lista de tres problemas de eficiencia debido a un diseño de microkernel. 4. Symbian OS dividió el diseño de su kernel en dos niveles: el nanokernel y el kernel de Symbian OS. Los servicios como la administración dinámica de la memoria se consideraban demasiado complicados para el nanokernel. Describa los componentes complicados de la administración di- námica de la memoria y por qué no podrían funcionar en un nanokernel. 5. Analizamos los objetos activos como una forma de mejorar la eficiencia del procesamiento de la E/S. ¿Cree usted que una aplicación podría utilizar varios objetos activos al mismo tiempo? ¿Cómo reaccionaría el sistema cuando varios eventos de E/S requirieran acción? 6. ¿La seguridad en Symbian OS se enfoca en la instalación y en que Symbian firme las aplica- ciones? ¿Podría haber un caso en el que una aplicación se colocara en almacenamiento para eje- cutarla sin ser instalada? (Sugerencia: Piense en todos los posibles puntos de entrada de datos para un teléfono móvil). 7. En Symbian OS, la protección de los recursos compartidos basada en servidor se utiliza en for- ma extensa. Liste tres ventajas que tiene este tipo de coordinación de los recurso en el entorno de microkernel. Haga sus conjeturas en cuanto a la forma en que cada una de sus ventajas po- dría afectar a una arquitectura de kernel distinta. 13 DISEÑO DE SISTEMAS OPERATIVOS En los 12 capítulos anteriores tratamos muchos temas y analizamos gran cantidad de conceptos y ejemplos relacionados con los sistemas operativos. El diseño de estos presenta características espe- cíficas. En este capítulo daremos un vistazo rápido a algunas de las cuestiones y sacrificios que los diseñadores de sistemas operativos deben considerar al diseñar e implementar un nuevo sistema. Hay cierta cantidad de folclore sobre lo que es bueno y lo que es malo en las comunidades de los sistemas operativos, pero es sorprendente que se haya escrito tan poco sobre ello. Tal vez el libro más importante sea The Mythical Man Month (El mítico hombre-mes), una obra clásica de Fred Brooks en la que relata sus experiencias en el diseño y la implementación del OS/360 de IBM. La edición del 20 aniversario revisa parte de ese material y agrega cuatro nuevos capítulos (Brooks, 1995). Tres documentos clásicos sobre el diseño de sistemas operativos son “Hints for Computer Sys- tem Design” (Lampson, 1984), “On Building Sistems That Hill Fail” (Corbató, 1991) y “End-to- End Arguments in System Design” (Saltzer y colaboradores, 1984). Al igual que el libro de Brooks, los tres documentos han sobrevivido el paso de los años extremadamente bien; la mayoría de sus ideas siguen siendo tan válidas ahora como la primera vez que se publicaron. Este capítulo se basa en estas fuentes y en la experiencia personal del autor como diseñador o co-diseñador de tres sistemas: Amoeba (Tanenbaum y colaboradores, 1990), MINIX (Tanenbaum y Woodhull, 1997) y Globe (Van Steel y colaboradores, 1999a). Como no existe un consenso en- tre los diseñadores de sistemas operativos sobre la mejor forma de diseñar un sistema operativo, este capítulo será por lo tanto más personal, especulativo y sin duda más controversial que los an- teriores. 959 960 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 13.1 LA NATURALEZA DEL PROBLEMA DE DISEÑO El diseño de sistemas operativos es más como un proyecto de ingeniería que una ciencia exacta. Es mucho más difícil establecer objetivos claros y cumplirlos. Vamos a empezar con estos puntos. 13.1.1 Objetivos Para poder diseñar un sistema operativo exitoso, los diseñadores deben tener una clara idea de lo que quieren. La falta de un objetivo dificulta de manera considerable el proceso de tomar las decisiones subsiguientes. Para aclarar más este punto, es instructivo dar un vistazo a dos lenguajes de progra- mación: PL/I y C. IBM diseñó PL/I en la década de 1960, debido a que era una molestia dar sopor- te tanto a FORTRAN como a COBOL, y era vergonzoso que los académicos empezaran a parlotear en trasfondo que Algol era mejor que ambos. Por lo tanto, se estableció un comité para producir un lenguaje que cumpliera con las necesidades de todas las personas: PL/I. Tenía un poco de FOR- TRAN, un poco de COBOL y un poco de Algol. Fracasó debido a que carecía de una visión unifica- dora. Era tan sólo una colección de características en guerra unas con otras, y demasiado voluminoso como para poder compilarse con eficiencia, para iniciarse. Ahora consideremos C. Este lenguaje fue diseñado por Dennis Ritchie con un propósito: la pro- gramación de sistemas. Fue un enorme éxito, en gran parte debido a que Ritchie sabía lo que que- ría y lo que no. Como resultado, se sigue utilizando de manera extensa más de tres décadas después de su aparición. Tener una visión clara de lo que uno quiere es algo imprescindible. ¿Qué desean los diseñadores de sistemas operativos? Esto sin duda varía de un sistema a otro; de los sistemas embebidos (incrustados) a los sistemas de servidor. Sin embargo, para los sistemas operativos de propósito general hay que tener en cuenta cuatro puntos principales: 1. Definir las abstracciones. 2. Proveer operaciones primitivas. 3. Asegurar el aislamiento. 4. Administrar el hardware. A continuación analizaremos cada uno de estos puntos. La tarea más importante (y tal vez la más difícil) de un sistema operativo es definir las abstrac- ciones correctas. Algunas de ellas, como los procesos, los espacios de direcciones y los archivos, han estado presentes tanto tiempo que pueden parecer obvias. Otras, como los hilos, son más re- cientes y, en consecuencia, menos maduras. Por ejemplo, si un proceso multihilo que tiene un hilo bloqueado en espera de la entrada del teclado realiza una bifurcación, ¿hay un hilo en el nuevo pro- ceso también en espera de la entrada del teclado? Otras abstracciones se relacionan con la sincro- nización, las señales, el modelo de memoria, el modelado de la E/S y muchas otras áreas. SECCIÓN 13.1 LA NATURALEZA DEL PROBLEMA DE DISEÑO 961 Cada una de las abstracciones se puede instanciar en forma de estructuras de datos concretas. Los usuarios pueden crear procesos, archivos, semáforos, etc. Las operaciones primitivas manipu- lan estas estructuras de datos. Por ejemplo, los usuarios pueden leer y escribir archivos. Las opera- ciones primitivas se implementan en la forma de llamadas al sistema. Desde el punto de vista del usuario, el corazón del sistema operativo se forma mediante las abstracciones y las operaciones dis- ponibles en ellas, a través de las llamadas al sistema. Como varios usuarios pueden tener una sesión abierta en una computadora al mismo tiempo, el sistema operativo necesita proveer mecanismos para mantenerlos separados. Un usuario no pue- de interferir con otro. El concepto de proceso se utiliza ampliamente para agrupar recursos para fines de protección. Por lo general también se protegen los archivos y otras estructuras de datos. Asegurar que cada usuario pueda realizar sólo operaciones autorizadas con datos autorizados es un objetivo clave del diseño de sistemas. Sin embargo, los usuarios también desean compartir da- tos y recursos, por lo que el aislamiento tiene que ser selectivo y controlado por el usuario. Esto lo hace más difícil incluso. El programa de correo electrónico no debe ser capaz de hacer que fa- lle el navegador Web. Incluso cuando sólo haya un usuario, los distintos procesos necesitan estar aislados. La necesidad de aislar las fallas está muy relacionada con este punto. Si alguna parte del sis- tema falla (lo más frecuente es un proceso de usuario), no se debe permitir que falle el resto del sistema. El diseño debe asegurarse de que las diversas partes estén aisladas unas de otras. En teo- ría, las partes del sistema operativo también se deben aislar unas de otras para permitir fallas in- dependientes. Por último, el sistema operativo tiene que administrar el hardware. En especial tiene que cui- dar todos los chips de bajo nivel, como los controladores de interrupciones y el bus. También tiene que proporcionar un marco de trabajo para permitir que los drivers de dispositivos administren los dispositivos de E/S más grandes, como los discos, las impresoras o la pantalla. 13.1.2 ¿Por qué es difícil diseñar un sistema operativo? La Ley de Moore enuncia que el hardware de computadora mejora por un factor de 100 cada déca- da, pero no hay ley alguna que declare el mejoramiento de los sistemas operativos en un factor de 100 cada diez años… o que siquiera mejoren. De hecho, se puede considerar que algunos de ellos son peores, en ciertos sentidos clave (como la confiabilidad), que la versión 7 de UNIX, de la dé- cada de 1970. ¿Por qué? La mayor parte de las veces la inercia y el deseo de obtener compatibilidad inver- sa tienen la culpa, y también el no poder adherirse a los buenos principios de diseño. Pero hay más que eso. Los sistemas operativos son esencialmente distintos en ciertas formas de los pequeños programas de aplicación que se venden en las tiendas por 49 (dólares). Vamos a ver ocho de las cuestiones que hacen que sea mucho más difícil diseñar un sistema operativo que un programa de aplicación. En primer lugar, los sistemas operativos se han convertido en programas extremadamente extensos. Ninguna persona se puede sentar en una PC y escribir un sistema operativo serio en unos cuantos meses. Todas las versiones actuales de UNIX sobrepasan los 3 millones de líneas de 962 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 código. Windows Vista tiene más de 5 millones de líneas de código del kernel (y más de 70 millo- nes de líneas de código en total). Nadie puede entender de 3 a 5 millones de líneas de código, mu- cho menos 70 millones. Cuando tenemos un producto que ninguno de los diseñadores puede esperar comprender por completo, no debe sorprender que los resultados estén con frecuencia muy aleja- dos de lo óptimo. Los sistemas operativos no son los más complejos de todos. Por ejemplo, las empresas de transporte aéreo son mucho más complicadas, pero se pueden particionar en subsistemas aislados para poder comprenderlos mejor. Las personas que diseñan los inodoros en una aeronave no tie- nen que preocuparse por el sistema de radar. Los dos subsistemas no tienen mucha interacción. En un sistema operativo, el sistema de archivos interactúa a menudo con el de memoria en formas inesperadas e imprevistas. En segundo lugar, los sistemas operativos tienen que lidiar con la concurrencia. Hay varios usuarios y dispositivos de E/S activos al mismo tiempo. En esencia, es mucho más difícil adminis- trar la concurrencia que una sola actividad secuencial. Las condiciones de carrera y los interblo- queos son sólo dos de los problemas que surgen. En tercer lugar, los sistemas operativos tienen que lidiar con usuarios potencialmente hostiles que desean interferir con la operación del sistema o hacer cosas prohibidas, como robar los archi- vos de otro usuario. EL sistema operativo necesita tomar las medidas necesarias para evitar que es- tos usuarios se comporten de manera inapropiada. Los programas de procesamiento de palabras y los editores de fotografías no tienen este problema. En cuarto lugar, a pesar del hecho de que no todos los usuarios desconfían de los otros, muchos de ellos desean compartir parte de su información y recursos con otros usuarios seleccionados. El sistema operativo tiene que hacer esto posible, pero de tal forma que los usuarios maliciosos no pue- dan interferir. De nuevo, los programas de aplicaciones no se enfrentan a ningún reto similar. En quinto lugar, los sistemas operativos viven por mucho tiempo. UNIX ha estado en operación durante un cuarto de siglo; Windows, durante más de dos décadas y no muestra signos de desapari- ción. En consecuencia, los diseñadores tienen que pensar sobre la forma en que pueden cambiar el hardware y las aplicaciones en un futuro distante, y cómo deben prepararse para ello. Por lo general, los sistemas que están demasiado encerrados en una visión específica del mundo desaparecen. En sexto lugar, los diseñadores de sistemas operativos en realidad no tienen una buena idea so- bre la forma en que se utilizarán sus sistemas, por lo que necesitan proveer una generalidad consi- derable. Ni UNIX ni Windows se diseñaron con el correo electrónico o los navegadores Web en mente, y aun así hay muchas computadoras que utilizan estos sistemas operativos y casi todo el tiempo utilizan estas dos aplicaciones. Nadie le dice a un diseñador de barcos cómo crear uno sin especificarle que desean un bote de pesca, un crucero o un buque de guerra. E incluso algunos cam- bian de opinión después de que les llega el producto. En séptimo lugar, por lo general los sistemas operativos modernos están diseñados para ser porta- bles, lo cual significa que deben ejecutarse en varias plataformas de hardware. También tienen que ad- mitir miles de dispositivos de E/S, y todos están diseñados de manera independiente, sin ningún tipo de relación con los demás. Un ejemplo en donde esta diversidad ocasiona problemas es la necesidad de que un sistema operativo se ejecute en máquinas que utilizan las notaciones little-endian y big-en- dian. Un segundo ejemplo se podía ver de manera constante en MS-DOS, cuando los usuarios trataban de instalar, por ejemplo, una tarjeta de sonido y un módem que utilizaban los mismos puertos de E/S SECCIÓN 13.2 DISEÑO DE INTERFACES 963 o las mismas líneas de petición de interrupción. Pocos programas además de los sistemas operativos tienen que lidiar con los problemas que ocasionan las piezas de hardware en conflicto. En octavo y último lugar, está la frecuente necesidad de tener compatibilidad inversa con cier- to sistema operativo anterior. Ese sistema puede tener restricciones en cuanto a las longitudes de las palabras, los nombres de archivos u otros aspectos que los diseñadores ya consideran obsoletos pe- ro que deben seguir utilizando. Es como convertir una fábrica para producir los autos del próximo año en vez de los de este año, pero seguir produciendo los autos de este año a toda su capacidad. 13.2 DISEÑO DE INTERFACES Para estos momentos el lector debe tener claro que no es fácil escribir un sistema operativo moder- no. Pero, ¿dónde se puede empezar? Tal vez el mejor lugar para iniciar sea pensar sobre las interfa- ces que va a proporcionar. Un sistema operativo proporciona un conjunto de abstracciones, que en su mayor parte se implementan mediante tipos de datos (por ejemplo, archivos) y las operaciones que se realizan en ellos (por ejemplo, read). En conjunto, estos dos elementos forman la interfaz para sus usuarios. Hay que considerar que en este contexto, los usuarios del sistema operativo son programadores que escriben código que utiliza llamadas al sistema, y no personas que ejecutan pro- gramas de aplicación. Además de la interfaz principal de llamadas al sistema, la mayoría de los sistemas operativos tienen interfaces adicionales. Por ejemplo, algunos programadores necesitan escribir drivers de dis- positivos para insertarlos en el sistema operativo. Estos drivers ven ciertas características y pueden realizar llamadas a ciertos procedimientos. Estas características y llamadas también definen una in- terfaz, pero es muy distinta de la que ven los programadores de aplicaciones. Todas estas interfaces se deben diseñar con cuidado, para que el sistema tenga éxito. 13.2.1 Principios de guía ¿Acaso hay principios que puedan guiar el diseño de las interfaces? Nosotros creemos que sí. En resumen son simplicidad, integridad y la habilidad de implementarse de manera eficiente. Principio 1: Simplicidad Una interfaz simple es más fácil de comprender e implementar sin que haya errores. Todos los di- señadores de sistemas deben memorizar esta famosa cita del pionero francés aviador y escritor, An- toine de St. Exupéry: No se llega a la perfección cuando ya no hay nada más que agregar, sino cuando ya no hay nada que quitar. Este principio dice que es mejor menos que más, por lo menos en el mismo sistema operativo. Otra forma de decir esto es mediante el principio KISS: Keep It Simple, Stupid (Manténganlo bre- ve y simple). 964 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 Principio 2: Integridad Desde luego que la interfaz debe permitir al usuario hacer todo lo que necesita; es decir, debe estar completa. Esto nos lleva a una famosa cita de Albert Einstein: Todo debe ser lo más simple posible, pero no más simple. En otras palabras, el sistema operativo debe hacer exactamente lo que se necesita de él, y nada más. Si los usuarios necesitan almacenar datos, debe proveer cierto mecanismo para almacenar- los; si los usuarios necesitan comunicarse unos con otros, el sistema operativo tiene que propor- cionar un mecanismo de comunicación. En su conferencia del Premio Turing de 1991, Francisco Corbató, uno de los diseñadores de CTSS y MULTICS, combinó los conceptos de simplicidad e integridad y dijo: En primer lugar, es importante enfatizar el valor de la simplicidad y la elegancia, ya que la complejidad tiende a agravar las dificultades y, como hemos visto, crear errores. Mi de- finición de elegancia es la obtención de una funcionalidad dada con un mínimo de meca- nismo y un máximo de claridad. La idea clave aquí es mínimo de mecanismo. En otras palabras, cada característica, función y lla- mada al sistema debe llevar su propio peso. Debe hacer una sola cosa y hacerla bien. Cuando un miembro del equipo de diseño propone extender una llamada al sistema o agregar una nueva carac- terística, los otros deben preguntar si ocurriría algo terrible en caso de que no se incluyera. Si la res- puesta es: “No, pero alguien podría encontrar esta característica útil algún día” hay que ponerla en una biblioteca de nivel de usuario y no en el sistema operativo, incluso si es más lenta de esa for- ma. No todas las características tienen que ser más rápidas que una bala. El objetivo es preservar lo que Corbató denominó un mínimo de mecanismo. Ahora consideremos en forma breve dos ejemplos de mi propia experiencia: MINIX (Tanen- baum y Woodhull, 2006) y Amoeba (Tanenbaum y colaboradores, 1990). Para todos los fines y pro- pósitos, MINIX tiene tres llamadas al sistema: send, receive y sendrec. El sistema está estructurado como una colección de procesos donde el administrador de memoria, el sistema de archivos y cada driver de dispositivo son un proceso que se programa por separado. En primera instancia, todo lo que el kernel hace es programar procesos y manejar el paso de mensajes entre ellos. En consecuen- cia, se necesitan sólo dos llamadas al sistema: send para enviar un mensaje y receive para recibir- lo. La tercera llamada (sendrec) es sólo una optimización por razones de eficiencia, para permitir enviar un mensaje y devolver la respuesta con sólo una trampa en el kernel. Para realizar todo lo demás se pide a algún otro proceso (por ejemplo, el proceso del sistema de archivos o el driver de disco) que realice el trabajo. Amoeba es incluso más simple. Sólo tiene una llamada al sistema: realizar llamada a procedi- miento remoto. Esta llamada envía un mensaje y espera una solicitud. En esencia es igual que sen- drec de MINIX. Todo lo demás está integrado en esta llamada. SECCIÓN 13.2 DISEÑO DE INTERFACES 965 Principo 3: Eficiencia El tercer lineamiento es la eficiencia de la implementación. Si una característica o llamada al siste- ma no se puede implementar con eficiencia, tal vez no valga la pena tenerla. También debe ser in- tuitivamente obvia para el programador, en relación con el costo de una llamada al sistema. Por ejemplo, los programadores de UNIX esperan que la llamada al sistema lseek sea menos costosa que la llamada al sistema read, debido a que la primera sólo cambia un apuntador en la memoria, mientras que la segunda realiza operaciones de E/S de disco. Si los costos intuitivos son incorrec- tos, los programadores escribirán programas ineficientes. 13.2.2 Paradigmas Una vez que se han establecido los objetivos, puede empezar el diseño. Un buen lugar para iniciar es pensar sobre la forma en que los clientes verán el sistema. Una de las cuestiones más importan- tes es cómo hacer que todas las características del sistema funcionen bien y presenten lo que se co- noce comúnmente como coherencia arquitectónica. En este aspecto, es importante distinguir dos tipos de “clientes” de los sistemas operativos. Por un lado están los usuarios, quienes interactúan con los programas de aplicaciones; por el otro están los programadores, quienes escriben los siste- mas operativos. Los primeros clientes tratan en su mayor parte con la GUI; los segundos tratan en su mayor parte con la interfaz de llamadas al sistema. Si la intención es tener una sola GUI que do- mine el sistema completo, como en la Macintosh, el diseño debe empezar ahí. Por otra parte, si la intención es proporcionar todas las GUIs que sea posible, como en UNIX, la interfaz de llamadas al sistema se debe diseñar primero. En esencia, realizar la primera GUI es un diseño de arriba ha- cia abajo. Las cuestiones son qué características tendrá, la forma en que el usuario interactuará con ella y cómo se debe diseñar el sistema para producirla. Por ejemplo, si la mayoría de los programas muestran iconos en la pantalla que esperan a que el usuario haga clic en uno de ellos, esto sugiere un modelo controlado por eventos para la GUI, y probablemente también para el sistema operati- vo. Por otra parte, si la mayor parte de la pantalla está llena de ventanas de texto, entonces tal vez sea mejor un modelo en el que los procesos lean del teclado. Realizar la interfaz de llamadas al sistema primero es un diseño de abajo hacia arriba. Aquí las cuestiones son qué tipo de características necesitan los programadores en general. En realidad no se necesitan muchas características especiales para proporcionar una GUI. Por ejemplo, el sistema de ventanas X de UNIX es sólo un programa grande en C que realiza llamadas a read y write en el te- clado, ratón y pantalla. X se desarrolló mucho después de UNIX y no se requirieron muchos cam- bios en el sistema operativo para que funcionara. Esta experiencia validó el hecho de que UNIX estaba bastante completo. Paradigmas de la interfaz de usuario Para la interfaz a nivel de GUI y la interfaz de llamadas al sistema, el aspecto más importante es te- ner un buen paradigma (a lo que algunas veces se le conoce como metáfora) para proveer una for- 966 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 ma de ver la interfaz. Muchas GUIs para los equipos de escritorio utilizan el paradigma WIMP que vimos en el capítulo 5. Este paradigma utiliza apuntar y hacer clic, apuntar y hacer doble clic, arras- trar y otros modismos más a lo largo de la interfaz para ofrecer una coherencia arquitectónica en to- do el sistema. A menudo, estos son requerimientos adicionales para los programas, como tener una barra de menús con ARCHIVO, EDICIÓN y otras entradas, cada una de las cuales tiene ciertos ele- mentos de menú reconocidos. De esta forma, los usuarios que conocen un programa pueden apren- der otro con rapidez. Sin embargo, la interfaz de usuario WIMP no es la única posible. Algunas computadoras de bolsillo utilizan una interfaz de escritura manual estilizada. Los dispositivos multimedia dedi- cados pueden utilizar una interfaz tipo VCR. Y por supuesto, la entrada de voz tiene un paradig- ma completamente distinto. Lo importante no es tanto el paradigma que se seleccione, sino el hecho de que hay un solo paradigma que invalida a los demás y unifica a toda la interfaz de usuario. Sin importar el paradigma seleccionado, es importante que todos los programas de aplica- ción lo utilicen. En consecuencia, los diseñadores de sistemas necesitan proveer bibliotecas y kits de herramientas para que los desarrolladores de aplicaciones tengan acceso a los procedimientos que producen la apariencia visual uniforme. El diseño de la interfaz de usuario es muy importan- te, pero no es el tema de este libro, por lo que ahora regresaremos al tema de la interfaz del sis- tema operativo. Paradigmas de ejecución La coherencia arquitectónica es importante a nivel de usuario, pero tiene igual importancia a ni- vel de la interfaz de llamadas al sistema. Aquí es con frecuencia útil diferenciar entre el paradig- ma de ejecución y el de datos, por lo que analizaremos ambos, empezando con el primero. Hay dos paradigmas de ejecución de uso extenso: algorítmicos y controlados por eventos. El paradigma algorítmico se basa en la idea de que se inicia un programa para realizar cierta función que conoce de antemano, o que obtiene de sus parámetros. Esa función podría ser compilar un pro- grama, realizar la nómina o volar un avión a San Francisco. La lógica básica está fija en el código, y el programa realiza llamadas al sistema de vez en cuando para obtener datos de entrada del usua- rio, servicios del sistema operativo, etc. Este método se describe en la figura 13-1(a). El otro paradigma de ejecución es el paradigma controlado por eventos de la figura 13-1(b). Aquí el programa realiza cierto tipo de inicialización; por ejemplo, al mostrar cierta pantalla y des- pués esperar a que el sistema operativo le indique sobre el primer evento. A menudo este evento es la pulsación de una tecla o un movimiento del ratón. Este diseño es útil para los programas muy in- teractivos. Cada una de estas formas de hacer las cosas engendra su propio estilo de programación. En el paradigma algorítmico, los algoritmos son centrales y el sistema operativo se considera como pro- veedor de servicios. En el paradigma controlado por eventos el sistema operativo también propor- ciona servicios, pero este papel se ve eclipsado por su papel como coordinador de las actividades de usuario y generador de los eventos consumidos por los procesos. SECCIÓN 13.2 DISEÑO DE INTERFACES 967 main( ) main( ) { { int...; mess_t msj; init( ); init( ); hacer_algo( ); while (obtener_mensaje(&msj)) { read(...); switch (msj.type) { hacer_algo_mas( ); case 1:...; write(...); case 2:...; seguir_funcionando( ); case 3:...; exit(0); } } } } (a) (b) Figura 13-1. (a) Código algorítmico. (b) Código controlado por eventos. Paradigmas de datos El paradigma de ejecución no es el único que exporta el sistema operativo. El paradigma de datos es igual de importante. La pregunta clave aquí es la forma en que se presentan las estructuras y los dispositivos del sistema al programador. En los primeros sistemas de procesamiento por lotes de FORTRAN, todo se modelaba como una cinta magnética secuencial. Las pilas de tarjetas que se introducían se consideraban como cintas de entrada, las pilas de tarjetas que se iban a perforar se consideraban como cintas de salida, y la salida para la impresora se consideraba como una cin- ta de salida. Los archivos en el disco también se consideraban como cintas. Para tener acceso alea- torio a un archivo, había que rebobinar la cinta correspondiente al mismo y leerla de nuevo. Para realizar la asignación se utilizaban tarjetas de control de trabajos de la siguiente manera: MONTAR(CINTA08, CARRETE781) EJECUTAR(ENTRADA, MISDATOS, SALIDA, PERFORADORA, CINTA08) La primera tarjeta indicaba al operador que obtuviera el carrete 781 del estante de cintas y lo mon- tara en la unidad de cinta 8; la segunda indicaba al sistema operativo que ejecutara el programa FORTRAN que se acababa de compilar, y que asignara ENTRADA (el lector de tarjetas) a la cinta lógica 1, el archivo de disco MISDATOS a la cinta lógica 2, la impresora (llamada SALIDA) a la cin- ta lógica 3, la perforadora de tarjetas (llamada PERFORADORA) a la cinta lógica 4, y la unidad de cinta física 8 a la cinta lógica 5. FORTRAN tenía una sintaxis para leer y escribir en cintas lógicas. Al leer de la cinta lógica 1, el programa obtenía la entrada de la tarjeta. Al escribir en la cinta lógica 3, la salida aparecería pos- teriormente en la impresora. Al leer de la cinta lógica 5 se podía leer el carrete 781 y así en forma sucesiva. Hay que tener en cuenta que la idea de la cinta era sólo un paradigma para integrar el lec- tor de tarjetas, la impresora, la perforadora, los archivos de disco y las cintas. En este ejemplo, só- lo la cinta lógica 5 era una cinta física; el resto eran archivos de disco ordinarios (en cola). Era un paradigma primitivo, pero fue un inicio en la dirección correcta. 968 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 Después llegó UNIX, que utiliza en forma más avanzada el modelo de “todo es un archivo”. Mediante el uso de este paradigma, todos los dispositivos de E/S se consideran como archivos, y se pueden abrir y manipular como archivos ordinarios. Las instrucciones de C fd1 = open(“archivo1”, O_RDWR); fd2 = open(“/dev/tty”, O_RDWR); abren un verdadero archivo de disco y la terminal del usuario (teclado + pantalla). Las instruccio- nes subsecuentes pueden utilizar fd1 y fd2 para leer y escribir en ellos, respectivamente. De ahí en adelante no hay diferencia entre acceder al archivo y a la terminal, excepto porque no se permite realizar búsquedas en esta última. UNIX no sólo unifica los archivos y los dispositivos de E/S, sino que también permite acceder a otros procesos como archivos mediante las tuberías. Además, cuando se admiten archivos asigna- dos, un proceso puede obtener su propia memoria virtual como si fuera un archivo. Por último, en las versiones de UNIX que aceptan el sistema de archivos /proc, la instrucción de C fd3 = open(“/proc/501”, O_RDWR); permite al proceso (tratar de) acceder a la memoria del proceso 501 en modo de lectura y escritura mediante el descriptor de archivo fd3, algo que puede ser útil para un depurador, por ejemplo. Windows Vista va más allá y trata de hacer que todo parezca un objeto. Una vez que un proce- so adquiere un manejador válido para un archivo, proceso, semáforo, bandeja de correo u otro ob- jeto del kernel, puede realizar operaciones con él. Este paradigma es más general incluso que el de UNIX y mucho más general que el de FORTRAN. Los paradigmas unificadores ocurren también en otros contextos. Aquí vale la pena mencionar uno de ellos: la Web. El paradigma detrás de la Web es que el ciberespacio está lleno de documen- tos, cada uno de los cuales tiene un URL. Al escribir un URL o hacer clic en una entrada respalda- da por un URL, el usuario recibe el documento. En realidad, muchos “documentos” no son documentos en sí, sino que los genera un programa o una secuencia de comandos del shell cuando llega una petición. Por ejemplo, cuando un usuario pide a una tienda en línea una lista de CDs de un artista específico, un programa genera el documento al instante, pues no existía antes de realizar la petición. Ahora hemos visto cuatro casos: a saber, todo es una cinta, archivo, objeto o documento. En los cuatro casos, la intención es unificar los datos, dispositivos y demás recursos para facilitar su manejo. Cada sistema operativo debe tener un paradigma de datos unificador de ese tipo. 13.2.3 La interfaz de llamadas al sistema Si uno cree en el dicho de Corbató del mecanismo mínimo, entonces el sistema operativo debe pro- porcionar la menor cantidad de llamadas al sistema con las que pueda funcionar, y cada una debe ser lo más simple posible (pero no más simple). Un paradigma de datos unificador puede desempe- ñar un papel importante para ayudar con esto. Por ejemplo, si los archivos, procesos, dispositivos de E/S y todo lo demás se ven como archivos u objetos, entonces se pueden leer mediante una SECCIÓN 13.2 DISEÑO DE INTERFACES 969 sola llamada al sistema read. En caso contrario, tal vez sea necesario tener llamadas separadas pa- ra read_file, read_proc y read_tty, entre otras. En algunos casos puede parecer que las llamadas al sistema necesitan variantes, pero a menu- do es mejor tener una sola llamada al sistema que maneje el caso general, con distintos procedi- mientos de biblioteca para ocultar este hecho de los programadores. Por ejemplo, UNIX tiene una llamada al sistema para superponer un espacio de direcciones virtuales de un proceso, exec. La lla- mada más general es exec(nombre, argp, envp); que carga el archivo ejecutable nombre y le proporciona argumentos a los que apunta argp y varia- bles de entorno a las que apunta envp. Algunas veces es conveniente listar los argumentos en forma explícita, para que la biblioteca contenga procedimientos que se llamen de la siguiente manera: exec(nombre, arg0, arg1, …, argn, 0); execle(nombre, arg0, arg1, …, argn, envp); Todo lo que hacen estos procedimientos es poner los argumentos en un arreglo y después llamar a exec para que haga el trabajo. Este arreglo conjunta lo mejor de ambos mundos: una sola llamada al sistema directa mantiene el sistema operativo simple, y a la vez el programador obtiene la con- veniencia de varias formas de llamar a exec. Desde luego que tener una llamada para manejar todos los casos posibles puede salirse de con- trol fácilmente. En UNIX, para crear un proceso se requieren dos llamadas: fork seguida de exec. La primera no tiene parámetros; la segunda tiene tres. En contraste la llamada a la API Win32 para crear un proceso (CreateProcess) tiene 10 parámetros, uno de los cuales es un apuntador a una es- tructura con 18 parámetros adicionales. Hace mucho tiempo, alguien debió haber preguntado si ocurriría algo terrible al omitir algunos de estos parámetros. La respuesta sincera hubiera sido que en algunos casos, los programadores tal vez tendrían que realizar más trabajo para lograr un efecto específico, pero el resultado neto hubie- ra sido un sistema operativo más simple, pequeño y confiable. Desde luego que la persona con la proposición de 10 + 18 parámetros podría haber respondido: “Pero a los usuarios les gustan todas estas características”. La réplica podría haber sido que les gustan los sistemas que utilizan poca me- moria y nunca tienen fallas. Los sacrificios entre obtener más funcionalidad a costa de más memo- ria son por lo menos visibles, y se les puede asignar un precio (ya que se conoce el precio de la memoria). Sin embargo, es difícil estimar las fallas adicionales por año que agregará cierta carac- terística, y si los usuarios realizarían la misma decisión si conocieran el precio oculto. Este efecto se puede resumir en la primera ley del software de Tanenbaum: Al agregar más código se agregan más errores Al agregar más características se agrega más código y consecuentemente más errores. Los progra- madores que creen que al agregar nuevas características no se agregan nuevos errores son nuevos en las computadoras, o creen que el hada de los dientes anda por ahí cuidándolos. 970 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 La simplicidad no es la única cuestión que se debe considerar al diseñar las llamadas al siste- ma. Una consideración importante es el eslogan de Lampson (1984): No ocultes el poder. Si el hardware tiene una forma en extremo eficiente de realizar algo, debe exponerse a los progra- madores de manera simple y no enterrarse dentro de alguna otra abstracción. El propósito de las abstracciones es ocultar las propiedades indeseables, no las deseables. Por ejemplo, suponga que el hardware tiene una forma especial de desplazar mapas de bits extensos por la pantalla (es decir, la RAM de video) a una alta velocidad. Se podría justificar el tener una nueva llamada al sistema pa- ra acceder a este mecanismo, en vez de sólo proporcionar formas de leer la RAM de video y colo- carla en memoria principal, y después volver a escribir todo de nuevo. La nueva llamada sólo desplazaría bits y nada más. Si una llamada al sistema es rápida, los usuarios siempre pueden crear interfaces más convenientes encima de ella. Si es lenta, nadie la utilizará. Otra cuestión de diseño es la comparación entre las llamadas orientadas a conexión y aquéllas orientadas a no conexión. Las llamadas al sistema estándar de UNIX y Win32 para leer un archivo son orientadas a conexión, como utilizar el teléfono. Primero hay que abrir un archivo, después leer- lo y por último cerrarlo. Algunos protocolos de acceso a archivos también son orientados a cone- xión. Por ejemplo, para utilizar FTP el usuario primero debe iniciar sesión en la máquina remota, luego leer los archivos y después cerrar la sesión. Por otro lado, algunos protocolos de acceso a archivos remotos son orientados a no conexión. Por ejemplo, el protocolo Web (HTTP) no tiene conexión. Para leer una página Web sólo hay que pedirla; no se requiere configurar nada de antemano (se requiere una conexión TCP, pero esto es a un nivel más bajo del protocolo; el protocolo HTTP para acceder a la Web en sí es orientada a no conexión). El sacrificio entre cualquier mecanismo orientado a conexión y uno orientado a no conexión es el trabajo adicional que se requiere para establecer el mecanismo (por ejemplo, abrir el archivo) y la ventaja de no tener que hacerlo en las llamadas subsiguientes (que tal vez sean muchas). Para la E/S de archivos en una sola máquina, en donde el costo de configuración es bajo, tal vez la forma estándar (primero abrir y después usar) sea la mejor. Para los sistemas de archivos remotos, se pue- de hacer de ambas formas. Otra cuestión relacionada con la interfaz de llamadas al sistema es su visibilidad. La lista de llamadas al sistema requeridas por POSIX es fácil de encontrar. Todos los sistemas UNIX las ad- miten, así como un pequeño número de llamadas adicionales, pero la lista completa siempre es pú- blica. Por el contrario, Microsoft nunca ha hecho pública la lista de llamadas al sistema de Windows Vista. En vez de ello se han hecho públicas la API Win32 y otras APIs, pero éstas contienen gran- des cantidades de llamadas en las bibliotecas (más de 10,000) y sólo un pequeño número de ellas son verdaderas llamadas al sistema. El argumento para hacer públicas todas las llamadas al sistema es que permite a los programadores saber qué es económico (las funciones que se ejecutan en espa- cio de usuario) y qué es costoso (las llamadas al kernel). El argumento para no hacerlas públicas es que proporcionan a los implementadores la flexibilidad de cambiar las llamadas al sistema subya- centes actuales para mejorarlas sin quebrantar los programas de usuario. SECCIÓN 13.3 IMPLEMENTACIÓN 971 13.3 IMPLEMENTACIÓN Ahora vamos a dejar de lado las interfaces de usuario y de llamadas al sistema para analizar la for- ma de implementar un sistema operativo. En las siguientes ocho secciones examinaremos algunas cuestiones conceptuales generales relacionadas con las estrategias de implementación. Después analizaremos algunas técnicas de bajo nivel que a menudo son de utilidad. 13.3.1 Estructura del sistema Tal vez la primera decisión que deben tomar los implementadores es sobre la estructura del siste- ma. En la sección 1.7 examinamos las principales posibilidades, y aquí las repasaremos. En reali- dad, un diseño monolítico sin estructura no es una buena idea, excepto tal vez para un pequeño sistema operativo en (por ejemplo) un refrigerador, pero incluso en ese caso es discutible. Sistemas en capas Un método razonable que se ha establecido muy bien a través de los años es un sistema en capas. El sistema THE de Dijkstra (figura 1-25) fue el primer sistema operativo en capas. UNIX y Win- dows Vista también tienen una estructura en capas, pero es más como una forma de describir el sis- tema que un principio real de lineamiento utilizado para construir el sistema. Para un nuevo sistema, los diseñadores que optan por esta ruta deben primero tener mucho cui- dado en elegir las capas y definir la funcionalidad de cada una. La capa inferior siempre debe tra- tar de ocultar las peores idiosincrasias del hardware, como lo hace el HAL en la figura 11-7. Tal vez la siguiente capa deba manejar las interrupciones, el cambio de contexto y la MMU, para que enci- ma de esta capa el código sea en su mayor parte independiente de la máquina. Encima de esta ca- pa, los distintos diseñadores tendrán gustos (y orientaciones) diferentes. Una posibilidad es hacer que la capa 3 administre los hilos, incluyendo la planificación y la sincronización entre ellos, como se muestra en la figura 13-2. La idea aquí es que desde la capa 4 debemos tener hilos apropiados que se planifiquen en forma normal y se sincronicen mediante un mecanismo estándar (por ejem- plo, los mutexes). En la capa 4 podríamos encontrar los drivers de dispositivos, cada uno de los cuales se ejecu- ta como un hilo separado, con su propio estado, contador del programa, registros, etc., posiblemen- te (pero no necesariamente) dentro del espacio de direcciones del kernel. Dicho diseño puede simplificar de manera considerable la estructura de E/S, debido a que cuando ocurre una interrup- ción, se puede convertir en una llamada a unlock en un mutex y una llamada al planificador para programar (potencialmente) el hilo que recién se encuentra en el estado listo y que estaba bloquea- do en el mutex. MINIX utiliza este método, pero en UNIX, Linux y Windows Vista los manejado- res de interrupciones se ejecutan en un área donde nadie tiene el control, en vez de ejecutarse como hilos apropiados que se pueden planificar, suspender, etc. Como una gran parte de la complejidad de cualquier sistema operativo está en la E/S, vale la pena considerar cualquier técnica para que se pueda tratar y encapsular mejor. 972 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 Nivel 7 Manejador de llamadas al sistema 6 Sistema de... Sistema de archivos 1 archivos m 5 Memoria virtual 4 Driver 1 Driver 2... Driver n 3 Hilos, planificación de hilos, sincronización de hilos 2 Manejo de interrupciones, cambio de contexto, MMU 1 Ocultar el hardware de bajo nivel Figura 13-2. Un posible diseño para un sistema operativo en niveles moderno. Arriba de la capa 4 podríamos esperar encontrar la memoria virtual, uno o más sistemas de ar- chivos y los manejadores de las llamadas al sistema. Si la memoria virtual está en un nivel más ba- jo que los sistemas de archivos, entonces la caché de bloques se puede paginar fuera de la memoria, con lo cual el administrador de memoria virtual puede determinar, de manera dinámica, la forma en que se debe dividir la memoria real entre las páginas de usuario y las de kernel, incluyendo la ca- ché. Windows Vista funciona de esta manera. Exokernels Aunque el sistema en niveles tiene sus partidarios entre los diseñadores de sistemas, también hay otro campo que tiene precisamente la visión opuesta (Engler y colaboradores, 1995). Su visión se basa en el argumento de punta a cabo (end-to-end) (Saltzer y colaboradores, 1984). Este concep- to dice que si el programa de usuario tiene que hacer algo por sí mismo, es un desperdicio realizar- lo en un nivel inferior también. Considere una aplicación de ese principio en el acceso a archivos remotos. Si un sistema se preocupa por que los datos se corrompan en el trayecto, debe hacer los arreglos para que se realice una suma de comprobación en cada archivo al momento de escribirlo, y esa suma de comprobación se debe almacenar junto con el archivo. Al transferir un archivo a través de una red, desde el disco de origen hasta el proceso de destino, también se transfiere la suma de comprobación y se vuelve a calcular en el extremo receptor. Si los dos valores no coinciden, se descarta el archivo y se vuelve a transferir. Esta comprobación es más precisa que utilizar un protocolo de red confiable, ya que también atrapa los errores de disco, de memoria, de software en los enrutadores y otros errores además de los de transmisión de bits. El argumento de punta a cabo dice que entonces no es necesario utilizar un protocolo de red confiable, ya que el punto final (el proceso receptor) tiene suficiente informa- ción como para verificar que el archivo sea correcto. La única razón de utilizar un protocolo de red confiable en este método es para la eficiencia; es decir, para atrapar y reparar los errores de trans- misión de manera anticipada. SECCIÓN 13.3 IMPLEMENTACIÓN 973 El argumento de punta a cabo se puede extender a casi todo el sistema operativo. Sostiene que el sistema operativo no tiene que hacer nada que el programa de usuario pueda hacer por su cuenta. Por ejemplo, ¿por qué tener un sistema de archivos? Sólo hay que dejar que el usuario lea y escriba en una porción del disco puro en una forma protegida. Desde luego que a la mayoría de los usuarios les gusta tener archivos, pero el argumento de punta a cabo dice que el sistema de archivos debe ser un procedimiento de biblioteca vinculado con cualquier programa que necesite utilizar archivos. Es- te método permite que distintos programas tengan diferentes sistemas de archivos. Esta línea de ra- zonamiento indica que todo lo que el sistema operativo debe hacer es asignar recursos en forma segura (por ejemplo, la CPU y los discos) entre los usuarios que compiten por ellos. El Exokernel es un sistema operativo construido de acuerdo con el argumento de punta a cabo (Engler y colaborado- res, 1995). Sistemas cliente-servidor basados en microkernel Un compromiso entre hacer que el sistema operativo haga todo y que no haga nada es que haga un poco. Este diseño conlleva a un microkernel, en donde gran parte del sistema operativo se ejecuta en forma de procesos de servidor a nivel de usuario, como se ilustra en la figura 13-3. Este es el diseño más modular y flexible de todos. Lo último en flexibilidad es hacer que cada driver de dispositivo tam- bién se ejecute como un proceso de usuario, completamente protegido contra el kernel y otros drivers, pero incluso hacer que los drivers de dispositivos se ejecuten en el kernel aumenta la modularidad. Proceso Proceso Proceso Servidor de Servidor Servidor Modo de usuario cliente cliente cliente procesos de archivos de memoria Modo de kernel Microkernel El cliente obtiene el servicio al enviar mensajes a los procesos de servidor Figura 13-3. Computación cliente-servidor basada en un microkernel. Cuando los drivers de dispositivos están en el kernel, pueden acceder directamente a los regis- tros de los dispositivos de hardware. Cuando no están ahí se necesita cierto mecanismo para pro- veer esta funcionalidad. Si el hardware lo permite, cada proceso de driver podría recibir acceso sólo a los dispositivos de E/S que necesita. Por ejemplo, con la E/S por asignación de memoria, cada proceso de driver podría hacer que se asignara en memoria la página para su dispositivo, pero nin- guna otra página de dispositivo. Si el espacio de puertos de E/S se puede proteger en forma parcial, se podría hacer disponible la porción correcta del mismo para cada driver. Aun si no hay disponible asistencia del hardware, la idea de todas formas puede funcionar. Lo que se necesita entonces es una nueva llamada al sistema, disponible sólo para los procesos de dri- ver de dispositivo, que suministre una lista de pares (puerto, valor). Lo que hace el kernel es prime- ro comprobar si el proceso posee todos los puertos en la lista. De ser así, después copia los valores 974 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 correspondientes a los puertos para iniciar la E/S del dispositivo. Se puede utilizar una llamada si- milar para leer los puertos de E/S de una forma protegida. Este método evita que los drivers de dispositivos examinen (y dañen) las estructuras de datos del kernel, lo cual es bueno (en su mayor parte). Se podría hacer disponible un conjunto análogo de llamadas para permitir que los procesos de driver leyeran y escribieran en las tablas del kernel, pe- ro sólo de una manera controlada y con la aprobación del kernel. El principal problema con este método, y con los microkernels en general, es la disminución en el rendimiento que provocan los cambios adicionales de contexto. Sin embargo, casi todo el tra- bajo en los microkernels se realizó hace muchos años, cuando las CPUs eran mucho más lentas. Hoy en día, las aplicaciones que utilizan todo el poder de la CPU y no pueden tolerar una pequeña pérdida de rendimiento son sólo unas cuantas. Después de todo, cuando se ejecuta un procesador de palabras o un navegador Web, la CPU tal vez esté inactiva 95% del tiempo. Si un sistema ope- rativo basado en microkernel convirtiera un sistema poco confiable de 3 GHz en un sistema confia- ble de 2.5 GHz, probablemente pocos usuarios se quejarían. Después de todo, la mayoría de ellos estaban bastante felices hace sólo unos cuantos años, cuando obtuvieron su computadora anterior a la entonces estupenda velocidad de 1 GHz. Vale la pena observar que aunque los microkernels no son populares en el escritorio, se utili- zan mucho en los teléfonos celulares, los PDAs, los sistemas industriales, los sistemas embebidos y los sistemas militares, en donde la alta confiabilidad es en absoluto esencial. Sistemas extensibles Con los sistemas cliente-servidor antes descritos, la idea era eliminar la mayor parte del kernel que fuera posible. El método opuesto es colocar más módulos en el kernel, pero de forma prote- gida. La palabra clave aquí es protegida, desde luego. En la sección 9.5.6 estudiamos algunos me- canismos de protección que en un principio eran para importar applets por Internet, pero se pueden aplicar de igual forma para insertar código externo en el kernel. Los más importantes son las cajas de arena y la firma de código, ya que la interpretación en realidad no es práctica para el código de kernel. Desde luego que un sistema extensible en sí no es una forma de estructurar un sistema opera- tivo. Sin embargo, al empezar con un sistema mínimo que consista de un poco más que un meca- nismo de protección, y después agregar módulos protegidos al kernel, uno a la vez hasta que se obtenga la funcionalidad deseada, se puede crear un sistema mínimo para la aplicación que se tie- ne a la mano. En este caso, un nuevo sistema operativo se puede optimizar para cada aplicación al incluir sólo las partes que requiere. Paramecium es un ejemplo de dicho sistema (Van Doorn, 2001). Hilos del kernel Otra cuestión relevante aquí, sin importar el modelo de estructuración seleccionado, es la de los hi- los del sistema. Algunas veces es conveniente permitir que existan hilos del kernel, separados de SECCIÓN 13.3 IMPLEMENTACIÓN 975 cualquier proceso de usuario. Estos hilos se pueden ejecutar en segundo plano para escribir páginas sucias al disco, intercambiar procesos entre la memoria principal y el disco, etc. De hecho, el mis- mo kernel se puede estructurar por completo mediante dichos hilos, de manera que cuando un usua- rio realice una llamada al sistema, en vez de que el hilo del usuario se ejecute en modo de kernel, se bloquee y pase el control a un hilo del kernel que se encargará de hacer el trabajo. Además de los hilos del kernel que se ejecutan en segundo plano, la mayoría de los sistemas operativos inician muchos procesos tipo demonios en segundo plano. Aunque éstos no forman par- te del sistema operativo, a menudo realzan actividades de tipo del “sistema”. Algunas de ellas son el obtener y enviar correo electrónico, y dar servicio a varios tipos de peticiones para los usuarios remotos, como FTP y páginas Web. 13.3.2 Comparación entre mecanismo y directiva Otro principio que ayuda a la coherencia arquitectónica, además de mantener todo pequeño y bien estructurado, es el de separar el mecanismo de la directiva. Al colocar el mecanismo en el sistema operativo y dejar la directiva a los procesos de usuario, el sistema en sí puede quedar sin modifica- ción, incluso si existe la necesidad de cambiar de directiva. Aun si el módulo de la directiva se tie- ne que mantener en el kernel, de ser posible debe estar aislado del mecanismo para que los cambios en el módulo de la directiva no afecten al módulo del mecanismo. Para que la división entre directiva y mecanismo sea más clara, vamos a considerar dos ejem- plos reales. Como primer ejemplo tenemos a una empresa grande que tiene un departamento de nóminas, el cual está a cargo de pagar los salarios de los empleados. Tiene computadoras, softwa- re, cheques bancarios, acuerdos con los bancos y más mecanismo para realizar los pagos de los sa- larios. Sin embargo, la directiva (determinar cuánto se paga a cada quién) está completamente separada y es la administración quien decide sobre ella. El departamento de nóminas sólo hace lo que se le indica. Como segundo ejemplo consideremos un restaurante. Tiene el mecanismo para servir comidas, incluyendo las mesas, los platos, los meseros, una cocina llena de equipo, acuerdos con las compa- ñías de tarjetas de crédito, etc. El chef establece la directiva; a saber, lo que hay en el menú. Si el chef decide servir grandes filetes en vez de tofú, el mecanismo existente puede manejar esta nueva directiva. Ahora consideremos algunos ejemplos del sistema operativo. Primero veremos la programa- ción de hilos. El kernel podría tener un planificador de prioridades, con k niveles de prioridad. El mecanismo es un arreglo indexado por nivel de prioridad, como es el caso en UNIX y Windows Vista. Cada entrada es la parte inicial de una lista de hilos listos en ese nivel de prioridad. El plani- ficador sólo busca el arreglo de la prioridad más alta a la prioridad más baja, y selecciona el primer hilo que encuentra. La directiva es establecer las prioridades. Por ejemplo, el sistema puede tener distintas clases de usuarios, cada uno con una prioridad distinta. También podría permitir que los procesos de usuario establezcan la prioridad relativa de sus hilos. Las prioridades se podrían incre- mentar después de completar una operación de E/S, o se podrían reducir después de utilizar un quantum. Hay muchas otras directivas que se podrían seguir, pero la idea aquí es la separación en- tre establecer la directiva y llevarla a cabo. 976 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 Un segundo ejemplo es la paginación. El mecanismo implica la administración de la MMU, llevar listas de páginas ocupadas y páginas libres, y código para transportar páginas hacia/desde el disco. La directiva es decidir lo que se debe hacer cuando ocurre un fallo de página. Podría ser lo- cal o global, basado en LRU o en PEPS o cualquier otra cosa, pero este algoritmo puede (y debe) estar separado por completo de la mecánica de administrar realmente las páginas. Un tercer ejemplo es permitir cargar módulos en el kernel. El mecanismo trata sobre la forma en que se insertan y se vinculan, qué llamadas pueden hacer y qué llamadas se pueden hacer con ellos. La directiva es determinar quién puede cargar un módulo en el kernel, y qué módulos puede cargar. Tal vez sólo el superusuario pueda cargar módulos, pero tal vez cualquier usuario pueda car- gar un módulo que la autoridad apropiada haya firmado en forma digital. 13.3.3 Ortogonalidad El buen diseño de sistemas consiste en conceptos separados que se pueden combinar de manera independiente. Por ejemplo, en C hay tipos de datos primitivos que incluyen enteros, caracteres y números de punto flotante. También hay mecanismos para combinar tipos de datos, incluyen- do arreglos, estructuras y uniones. Estas ideas se combinan de manera independiente para permi- tir arreglos de enteros, arreglos de caracteres, estructuras y miembros de uniones que son números de puntos flotantes, etc. De hecho, una vez que se ha definido un nuevo tipo de datos, como un arreglo de enteros, se puede utilizar como si fuera un tipo de datos primitivo; por ejem- plo, como miembro de una estructura o de una unión. La habilidad de combinar conceptos sepa- rados de manera independiente se conoce como ortogonalidad. Es una consecuencia directa de los principios de simplicidad e integridad. El concepto de ortogonalidad también ocurre de varias formas en los sistemas operativos. Un ejemplo es la llamada al sistema clone de Linux, la cual crea un hilo. Esta llamada tiene un mapa de bits como parámetro, el cual permite compartir o copiar de manera individual el espacio de di- recciones, el directorio de trabajo, los descriptores de archivos y las señales. Si todo se copia tene- mos un nuevo proceso, igual que fork. Si no se copia nada, se crea un hilo en el proceso actual. Sin embargo, también es posible crear formas intermedias de compartir que no son posibles en los sis- temas UNIX tradicionales. Al separar las diversas características y hacerlas ortogonales, es posible un grado de control más detallado. Otro uso de la ortogonalidad es la separación del concepto de proceso del concepto de hilo en Windows Vista. Un proceso es un contenedor de recursos, nada más y nada menos. Un hilo es una entidad planificable. Cuando un proceso recibe un manejador para otro proceso, no importa cuán- tos hilos tiene. Cuando un hilo se planifica, no importa a cuál proceso pertenece. Estos conceptos son ortogonales. Nuestro último ejemplo de ortogonalidad proviene de UNIX. En este sistema, la creación de procesos se realiza en dos pasos: fork y exec. Las acciones de crear el espacio de direcciones y car- garlo con una nueva imagen de memoria son separadas, y las cosas se pueden realizar entre ambas (como la manipulación de los descriptores de archivos). En Windows Vista, estos pasos no se pue- den separar; es decir, los conceptos de crear un espacio de direcciones y llenarlo no son ortogona- les. La secuencia en Linux de clone y exec es más ortogonal, ya que incluso hay disponibles más SECCIÓN 13.3 IMPLEMENTACIÓN 977 bloques de construcción detallados. Como regla general, al tener un pequeño número de elementos ortogonales que se pueden combinar de muchas formas se obtiene un sistema pequeño, simple y elegante. 13.3.4 Nomenclatura La mayoría de las estructuras de datos que utiliza un sistema operativo tienen cierto tipo de nom- bre o identificador mediante el cual se puede hacer referencia a ellas. Los ejemplos obvios son los nombres de inicio de sesión, nombres de archivos, nombres de dispositivos, IDs de procesos, etc. La forma en que se construyen y administran estos nombres es una cuestión importante en el dise- ño y la implementación de sistemas. Los nombres que se diseñaron para los seres humanos son cadenas de caracteres en ASCII o Unicode, y por lo general son jerárquicos. Las rutas de directorio como /usr/ast/libros/mos2/cap- 12 son sin duda jerárquicas, lo cual indica una serie de directorios en donde se va a realizar una búsqueda, empezando en el directorio raíz. Los URLs también son jerárquicos. Por ejemplo, www.cs.vu.nl/~ast/ indica una máquina específica (www) en un departamento específico (cs), en una universidad específica (vu) de un país específico (nl). La parte después de la barra diagonal indica un archivo específico en la máquina designada, en este caso por convención: www/in- dex.html en el directorio de inicio de ast. Observe que los URLs (y las direcciones DNS en gene- ral, incluyendo las direcciones de correo electrónico) son “inversos”, ya que empiezan en la parte inferior del árbol y van hacia arriba, a diferencia de los nombres de archivos, que empiezan en la parte superior del árbol y van hacia abajo. Otra forma de ver esto es si el árbol se escribe desde la parte superior, empieza del lado izquierdo y va hacia la derecha, o si empieza del lado derecho y va hacia la izquierda. A menudo, la nomenclatura se lleva a cabo en dos niveles: externo e interno. Por ejemplo, los archivos siempre tienen un nombre de cadena de caracteres para que las personas lo utilicen. Ade- más, casi siempre hay un nombre interno que el sistema utiliza. En UNIX, el verdadero nombre de un archivo es su número de nodo-i; el nombre ASCII no se utiliza en forma interna. De hecho, ni siquiera es único, ya que un archivo puede tener varios vínculos. El nombre interno análogo en Win- dows Vista es el índice del archivo en la MFT. El trabajo del directorio es proveer la asignación en- tre el nombre externo y el nombre interno, como se muestra en la figura 13-4. En muchos casos (como el ejemplo del nombre de archivo antes mostrado) el nombre interno es un entero sin signo que sirve como índice en una tabla del kernel. Otros ejemplos de nombres de índice de tabla son los descriptores de archivos en UNIX y los manejadores de objetos en Windows Vista. Observe que ninguno de ellos tiene representación externa. Son para uso exclusivo del siste- ma y los procesos en ejecución. En general, es conveniente utilizar índices de tablas para los nom- bres transitorios que se pierden cuando se reinicia el sistema. A menudo los sistemas operativos aceptan varios espacios de nombres, tanto externos como in- ternos. Por ejemplo, en el capítulo 11 analizamos tres espacios de nombres externos que Windows Vista admite: nombres de archivos, nombres de objetos y nombres del registro (y también está el espacio de nombres de Active Directory, que no analizamos). Además, hay innumerables espacios de nombres internos que utilizan enteros con signo; por ejemplo, los manejadores de objetos y las 978 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 Nombre externo: /usr/ast/libros/mos2/Cap-12 Directorio: /usr/ast/libros/mos2 Tabla de nodos-i Cap-10 114 7 Cap-11 38 6 Cap-12 2 5 4 3 Nombre interno: 2 2 1 Figura 13-4. Los directorios se utilizan para asignar nombres externos en nombres internos. entradas en la MFT. Aunque los nombres en los espacios de nombres externos son cadenas de Uni- code, no se busca un nombre de archivo en el registro, de igual forma que no se puede utilizar un índice de la MFT en la tabla de objetos. En un buen diseño se considera mucho cuántos espacios de nombres se necesitan, cuál es la sintaxis de los nombres en cada uno, cómo se pueden distinguir unos de otros, si existen nombres absolutos y relativos, etcétera. 13.3.5 Tiempo de vinculación Como hemos visto, los sistemas operativos utilizan varios tipos de nombres para referirse a los ob- jetos. Algunas veces la asignación entre un nombre y un objeto es fija, pero otras no. En este últi- mo caso, puede ser importante el momento en que se vincula el nombre al objeto. En general, la vinculación anticipada es simple pero no flexible, mientras que la vinculación postergada es más complicada pero a menudo más flexible. Para aclarar el concepto del tiempo de vinculación, veamos algunos ejemplos reales. Un ejem- plo de la vinculación anticipada es la práctica que tienen ciertos colegas de permitir que los padres inscriban a un bebé al nacer y paguen de antemano la colegiatura actual. Cuando el estudiante apa- rece 18 años después, la colegiatura está pagada por completo, sin importar qué tan alto sea su pre- cio en ese momento. En la manufactura, pedir piezas por adelantado y mantener un inventario de ellas es vincula- ción anticipada. Por el contrario, la manufactura justo a tiempo requiere que los proveedores pue- dan suministrar las piezas al momento, sin requerir un aviso por adelantado. Esto es vinculación postergada. A menudo los lenguajes de programación admiten varios tiempos de vinculación para las va- riables. Las variables globales se vinculan a una dirección virtual específica mediante el compila- dor. Esto ejemplifica la vinculación anticipada. A las variables locales para un procedimiento se les SECCIÓN 13.3 IMPLEMENTACIÓN 979 asigna una dirección virtual (en la pila) cuando se invoca el procedimiento. Esto es vinculación in- termedia. A las variables que se almacenan en el montículo (las que se asignan mediante malloc en C o new en Java) se les asignan direcciones virtuales sólo cuando realmente se utilizan. Aquí tene- mos una vinculación postergada. Con frecuencia, los sistemas operativos utilizan la vinculación anticipada para la mayoría de las estructuras de datos, pero en ocasiones utilizan la vinculación postergada por flexibilidad. La asignación de memoria es un buen ejemplo. Los primeros sistemas de multiprogramación en las máquinas que no contaban con hardware de reasignación de direcciones tenían que cargar un pro- grama en cierta dirección de memoria y reubicarlo para que se ejecutara ahí. Si alguna vez se inter- cambiaba, se tenía que traer de vuelta a la misma dirección de memoria o fallaría. Por el contrario, la memoria virtual paginada es una forma de vinculación postergada. La dirección física actual que corresponde a una dirección virtual dada no se conoce sino hasta que se hace contacto con la pági- na y se regresa a la memoria. Otro ejemplo de vinculación postergada es la colocación de ventanas en una GUI. Al contrario de los primeros sistemas gráficos, en donde el programador tenía que especificar las coordenadas absolutas en pantalla para todas las imágenes, en las GUIs modernas el software utiliza coordena- das relativas al origen de la ventana, pero eso no se determina sino hasta que la ventana se coloca en la pantalla, e incluso se puede cambiar más adelante. 13.3.6 Comparación entre estructuras estáticas y dinámicas Los diseñadores de sistemas operativos se ven forzados constantemente a elegir entre las estructu- ras de datos estáticas y las dinámicas. Las estáticas siempre son más fáciles de entender o de pro- gramar, y su uso es más rápido; las dinámicas son más flexibles. Un ejemplo obvio es la tabla de procesos. Los primeros sistemas simplemente asignaban un arreglo fijo de estructuras por proceso. Si la tabla de procesos contenía 256 entradas, entonces sólo 256 procesos podían existir en cual- quier instante. Un intento de crear el proceso 257 fracasaría por falta de espacio en la tabla. Para la tabla de archivos abiertos (tanto por usuario como para todo el sistema) y muchas otras tablas del kernel se aplicaban consideraciones similares. Una estrategia alternativa es crear la tabla de procesos como una lista vinculada de minitablas, en donde al principio sólo hay una. Si esta tabla se llena, se asigna otra de una reserva de almace- namiento global y se vincula con la primera. De esta forma, la tabla de procesos no se puede llenar sino hasta que se agote toda la memoria del kernel. Por otra parte, el código para buscar en la tabla se vuelve más complicado. Por ejemplo, el có- digo para buscar en una tabla de procesos estática un PID específico (pid) se muestra en la figura 13-5. Es simple y eficiente. Para hacer lo mismo en una lista de minitablas vinculadas se requiere más trabajo. Las tablas estáticas son mejores cuando hay mucha memoria, o cuando el uso de las tablas se puede pronosticar con suficiente precisión. Por ejemplo, en un sistema con un solo usuario es im- probable que éste inicie más de 64 procesos a la vez, y no es un desastre total si fracasa el intento de iniciar el proceso 65. Otra alternativa es utilizar una tabla de tamaño fijo, que al momento de llenarse asigne una nueva tabla de tamaño fijo, por ejemplo, del doble del tamaño. Así, las entradas actuales se copian 980 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 encontro = 0; for (p = &tabla_proc; p < &tabla_proc[TAMANIO_TABLA_PROC]; p++) { if (p->pid_proc == pid) { encontro = 1; break; } } Figura 13-5. Código para buscar en la tabla de procesos un PID específico. a la nueva tabla y la antigua se regresa a la reserva de almacenamiento libre. De esta forma, la ta- bla siempre es contigua en vez de estar vinculada. La desventaja aquí es que se requiere cierta ad- ministración del almacenamiento, y la dirección de la tabla es ahora una variable en vez de una constante. En las pilas del kernel se aplica una cuestión similar. Cuando un hilo cambia al modo de kernel, o cuando se ejecuta un hilo en modo de kernel, necesita una pila en el espacio del kernel. Para los hilos de usuario, la pila se puede inicialziar para que avance hacia abajo desde la parte superior del espacio de direcciones virtuales, por lo que no hay que especificar el tamaño por adelantado. Para los hilos de kernel, el tamaño se debe especificar por adelantado debido a que la pila ocupa cierto es- pacio de direcciones virtuales del kernel y puede haber muchas pilas. La pregunta es: ¿Cuánto espa- cio debe obtener cada una? Los sacrificios aquí son similares a los de la tabla de procesos. Otro sacrificio estático-dinámico es la programación de procesos. En algunos sistemas, en es- pecial los de tiempo real, la programación se puede realizar de manea estática por adelantado. Por ejemplo, una aerolínea sabe a qué hora saldrán sus vuelos semanas antes de su partida. De manera similar, los sistemas multimedia saben cuándo planificar los procesos de audio, video y otros por adelantado. Para el uso de propósito general, estas consideraciones no se aplican y la planificación debe ser dinámica. Otra cuestión estática-dinámica es la estructura del kernel. Es mucho más simple si el kernel se crea como un solo programa binario y se carga en memoria para ejecutarse. Sin embargo, la conse- cuencia de este diseño es que para agregar un nuevo dispositivo de E/S se requiere volver a vincu- lar el kernel con el nuevo controlador de dispositivo. Las primeras versiones de UNIX funcionaban así, y era muy satisfactorio en un entorno de minicomputadoras, cuando agregar nuevos dispositi- vos de E/S era algo que ocurría muy raras veces. En la actualidad, la mayoría de los sistemas ope- rativos permiten agregar código al kernel en forma dinámica, con toda la complejidad adicional que esto implica. 13.3.7 Comparación entre la implementación de arriba-abajo y la implementación de abajo-arriba Aunque es mejor diseñar el sistema de arriba hacia abajo, en teoría se puede implementar de arriba hacia abajo o de abajo hacia arriba. En una implementación de arriba-abajo, los implementadores empiezan con los manejadores de llamadas al sistema y averiguan qué mecanismos y estructuras de datos se necesitan para darles soporte. Estos procedimientos se escriben, y el proceso continúa has- ta que se llega al hardware. SECCIÓN 13.3 IMPLEMENTACIÓN 981 El problema con este método es que es difícil probar algo cuando sólo están disponibles los procedimientos de nivel superior. Por esta razón, muchos desarrolladores encuentran que es más práctico construir el sistema de abajo hacia arriba. Para este método, primero hay que escribir có- digo que oculte el hardware de bajo nivel, en esencia, la HAL de la figura 11-6. El manejo de inte- rrupciones y el driver del reloj también se requieren de manera anticipada. Después se puede tratar la multiprogramación, junto con un planificador simple (por ejemplo, la planificación por turno rotatorio). En este punto debe ser posible evaluar el sistema para ver si puede ejecutar varios procesos en forma correcta. Si eso funciona, ahora es tiempo de empezar la cuidadosa definición de las diversas tablas y estructuras de datos necesarias a lo largo del sistema, en especial las que se requieren para la administración de procesos e hilos y después la administra- ción de la memoria. La E/S y el sistema de archivos pueden esperar al principio, excepto una for- ma primitiva de leer el teclado y escribir en la pantalla para la evaluación y la depuración. En algunos casos, las estructuras de datos de bajo nivel se deben proteger al permitir el acceso sólo me- diante procedimientos de acceso específicos; en efecto, la programación orientada a objetos, sin im- portar cuál sea el lenguaje de programación. A medida que se completan los niveles inferiores, se pueden evaluar con detalle. De esta forma, el sistema avanza de abajo hacia arriba, algo muy simi- lar a la forma en que los contratistas construyen edificios de oficina altos. Si hay un equipo grande disponible, un método alternativo es realizar primero un diseño deta- llado de todo el sistema, y después asignar grupos distintos para escribir módulos diferentes. Cada uno prueba su propio trabajo en aislamiento. Cuando todas las piezas están listas, se integran y eva- lúan. El problema con esta línea de ataque es que si nada funciona al principio, puede ser difícil de- terminar de manera aislada si están fallando uno o más módulos, o si un grupo malentendió lo que se suponía debía hacer otro módulo. Sin embargo, con equipos grandes, este método se utiliza con frecuencia para maximizar la cantidad de paralelismo en el esfuerzo de programación. 13.3.8 Técnicas útiles Acabamos de analizar algunas ideas abstractas para el diseño y la implementación de sistemas. Ahora examinaremos varias técnicas concretas útiles para la implementación de sistemas. Desde luego que hay muchas otras, pero las limitaciones de espacio nos restringen a sólo unas cuantas. Ocultar el hardware Gran parte del hardware es desagradable. Se tiene que ocultar lo más pronto posible (a menos que exponga poder, y en la mayoría del hardware no es así). Algunos de los diversos detalles de bajo ni- vel se pueden ocupar mediante un nivel tipo HAL, del tipo que se muestra en la figura 13-2. Sin embargo, muchos detalles del hardware no se pueden ocultar de esta manera. Algo que merece atención anticipada es la forma de lidiar con las interrupciones. Hacen que la programación sea desagradable, pero los sistemas operativos tienen que lidiar con ellas. Un méto- do para ello es convertirlas en algo más de inmediato. Por ejemplo, toda interrupción se podría con- 982 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 vertir en un hilo emergente al instante. En ese momento estamos tratando con hilos en vez de inte- rrupciones. Un segundo método es convertir cada interrupción en una operación unlock en un mutex por el que espera el driver correspondiente. Así, el único efecto de una interrupción es hacer que un hi- lo cambie al estado listo. Un tercer método es convertir una interrupción en un mensaje para algún hilo. El código de bajo nivel sólo crea un mensaje que indica de dónde provino la interrupción, lo pone en cola y lla- ma al planificador para ejecutar (potencialmente) el manejador, que tal vez estaba bloqueado en espera del mensaje. Lo que hacen todas estas técnicas (y otras como ellas) es tratar de convertir las interrupciones en operaciones de sincronización de hilos. Es más fácil administrar el hecho de que cada interrupción se maneje mediante un hilo apropiado en un contexto apropiado que ejecu- tar un manejador en el contexto arbitrario en el que ocurrió. Desde luego que esto se debe realizar con eficiencia, pero en un nivel muy profundo del sistema operativo todo se debe realizar con efi- ciencia. La mayoría de los sistemas operativos están diseñados para ejecutarse en varias plataformas de hardware. Estas plataformas pueden diferir en términos del chip de la CPU, la MMU, la longitud de las palabras, el tamaño de la RAM y otras características que no se pueden enmascarar con fa- cilidad mediante el HAL o su equivalente. Sin embargo, es muy conveniente tener un solo conjun- to de archivos de código fuente que se utilicen para generar todas las versiones; en caso contrario, cada error que aparezca después se deberá corregir varias veces en varios códigos fuente, con el pe- ligro de que éstos se distancien. Para resolver algunas diferencias del hardware (como el tamaño de la RAM), el sistema ope- rativo debe determinar el valor en tiempo de inicio y mantenerlo en una variable. Por ejemplo, los asignadores de memoria pueden utilizar la variable del tamaño de la RAM para determinar el tama- ño de la caché de bloques, las tablas de páginas, etc. Incluso se puede definir el tamaño de las ta- blas estáticas (como la tabla de procesos) con base en la memoria total disponible. Sin embargo, otras diferencias (como distintos chips de CPU) no se pueden resolver al tener un solo binario que determine en tiempo de ejecución en qué CPU se está ejecutando. Una manera de tratar con el problema de un código fuente y varios destinos es utilizar la compilación condicional. En los archivos de código fuente se definen ciertas banderas en tiempo de compilación para las dis- tintas configuraciones, y se utilizan para agrupar código que es dependiente de la CPU, la longitud de las palabras, la MMU, etc. Por ejemplo, imagine un sistema operativo que se debe ejecutar en los chips Pentium o UltraSPARC, que necesitan distinto código de inicialización. El procedimien- to init se podría escribir como se ilustra en la figura 13-6(a). Dependiendo del valor de CPU, que se define en el archivo de encabezado config.h, se realiza un tipo de inicialización u otro. Como el binario actual contiene sólo el código necesario para la máquina de destino, no hay pérdida de efi- ciencia de esta forma. Como segundo ejemplo, suponga que se requiere un tipo de datos Registro, el cual debe ser de 32 bits en el Pentium y de 64 bits en el UltraSPARC. Esto se podría manejar mediante el código condicional de la figura 13-6(b) (suponiendo que el compilador produzca enteros de 32 bits y ente- ros largos de 64 bits). Una vez que se realiza esta definición (probablemente en un archivo de en- cabezado que se incluirá en todas partes), el programador sólo tiene que declarar variables de tipo Registro y éstas tendrán la longitud correcta. SECCIÓN 13.3 IMPLEMENTACIÓN 983 #include “config.h” #include “config.h” init( ) #if (LONG_PALABRA == 32) { typedef int Registro; #if (CPU == PENTIUM) #endif #endif #if (LONG_PALABRA == 64) typedef long Registro; #if (CPU == ULTRASPARC) #endif #endif Registro R0, R1, R2, R3; } (a) (b) Figura 13-6. (a) Compilación condicional dependiente de la CPU. (b) Compilación condicional dependiente de la longitud de las palabras. Desde luego que el archivo de encabezado config.h se tiene que definir de manera correcta. Pa- ra el Pentium podría ser algo así: #define CPU PENTIUM #define LONG_PALABRA 32 Para compilar el sistema para el UltraSPARC se utilizaría un archivo config.h distinto, con los va- lores correctos para este procesador, como por ejemplo: #define CPU ULTRASPARC #define LONG_PALABRA 64 Algunos lectores tal vez se pregunten por qué CPU y LONG_PALABRA se manejan mediante distin- tas macros. Podríamos haber agrupado fácilmente la definición de Registro con una prueba sobre CPU, para establecer su valor en 32 bits para el Pentium y 64 bits para el UltraSPARC. Sin embar- go, esto no es conveniente. Considere lo que ocurre si después portamos el sistema al procesador In- tel Itanium de 64 bits. Tendríamos que agregar una tercera instrucción condicional a la figura 13-6(b) para el Itanium. Si lo hacemos como hasta ahora, todo lo que tenemos que hacer es incluir la línea #define LONG_PALABRA 64 en el archivo config.h para el Itanium. Este ejemplo ilustra el principio de ortogonalidad que vimos antes. Los elementos que son de- pendientes de la CPU se deben compilar de manera condicional con base en la macro CPU, y los que son dependientes de la longitud de las palabras deben utilizar la macro LONG_PALABRA. Pa- ra muchos otros parámetros se aplican consideraciones similares. 984 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 Indirección Algunas veces se dice que no hay un problema en las ciencias computacionales que no se pueda re- solver con otro nivel de indirección. Aunque es en parte una exageración, sin duda hay algo de ver- dad aquí. Vamos a considerar algunos ejemplos. En los sistemas basados en Pentium, cuando se oprime una tecla el hardware genera una interrupción y coloca el número de tecla (en vez del códi- go de carácter ASCII) en un registro de dispositivo. Después, cuando la tecla se libera se genera una segunda interrupción, también con el número de clave. Esta indirección permite al sistema operati- vo la posibilidad de utilizar el número de tecla para indexar en una tabla y obtener el carácter AS- CII, con lo cual es fácil manejar los diversos teclados que se utilizan en todo el mundo en distintos países. Al obtener la información cuando se oprime y libera la tecla es posible utilizar cualquier te- cla como una tecla de mayúsculas, ya que el sistema operativo conoce la secuencia exacta en que se oprimieron y liberaron las teclas. La indirección también se utiliza en la salida. Los programas pueden escribir caracteres ASCII en la pantalla, pero éstos se interpretan como índices en una tabla para el tipo de letra de salida ac- tual. La entrada en la tabla contiene el mapa de bits para el carácter. Mediante esta indirección es posible separar los caracteres de los tipos de letras. Otro ejemplo de indirección es el uso de números de dispositivo mayores en UNIX. Dentro del kernel hay una tabla indexada por número de dispositivo mayor para los dispositivos de bloque y otro para los dispositivos de carácter. Cuando un proceso abre un archivo especial como /dev/hd0, el sistema extrae el tipo (bloque o carácter) y los números de dispositivo mayor y menor del nodo- i, y los indexa en la tabla de drivers apropiada para encontrar el driver. Esta indirección facilita la reconfiguración del sistema, ya que los programas tratan con nombres de dispositivos simbólicos y no con los nombres reales de los drivers. Otro ejemplo más de indirección ocurre en los sistemas de paso de mensajes que nombran una bandeja de correo en vez de un proceso como el destino del mensaje. Al realizar la indirección a través de bandejas de correo (a diferencia de nombrar un proceso como el destino), se puede obte- ner una flexibilidad considerable (por ejemplo, hacer que una secretaria se encargue de los mensa- jes de su jefe). En cierto sentido, el uso de macros tales como #define TAM_TABLA_PROC 256 es también una forma de indirección, ya que el programador puede escribir código sin tener que sa- ber qué tan grande es realmente la tabla. Es una buena práctica otorgar nombres simbólicos a todas las constantes (excepto algunas veces 1, 0 y 1) y colocarlos en encabezados con comentarios que expliquen su función. Reutilización Con frecuencia es posible reutilizar el mismo código en contextos ligeramente distintos. Esto es una buena idea, ya que reduce el tamaño del archivo binario y significa que el código sólo se tiene que depurar una vez. Por ejemplo, suponga que se utilizan mapas de bits para llevar la cuenta de los blo- SECCIÓN 13.3 IMPLEMENTACIÓN 985 ques libres en el disco. Para manejar la administración de los bloques de disco se pueden utilizar los procedimientos alloc y free, que administran los mapas de bits. Como mínimo, estos procedimientos deben funcionar para cualquier disco. Pero podemos ir más allá de eso. Los mismos procedimientos también pueden funcionar para administrar los blo- ques de memoria, los bloques en la caché de bloques del sistema de archivo y los nodos-i. De he- cho, se pueden utilizar para asignar y desasignar todos los recursos que se puedan enumerar en forma lineal. Reentrancia La reentrancia se refiere a la habilidad de ejecutar código dos o más veces al mismo tiempo. En un multiprocesador, siempre existe el peligro de que mientras una CPU ejecuta un procedimiento, otra CPU empiece a ejecutarlo también, antes de que haya terminado el primero. En este caso, dos (o más) hilos en distintas CPUs podrían ejecutar el mismo código al mismo tiempo. Hay que proteger- se de esa situación mediante el uso de mutexes o cualquier otro medio para proteger las regiones críticas. Sin embargo, el problema también existe en un uniprocesador. En especial, la mayoría de los sistemas operativos se ejecutan con las interrupciones habilitadas. De lo contrario se perderían mu- chas interrupciones y el sistema no sería confiable. Mientras el sistema operativo está ocupado eje- cutando cierto procedimiento P, es completamente posible que ocurra una interrupción y que el manejador de interrupciones también llame a P. Si las estructuras de datos de P estuvieran en un estado inconsistente al momento de la interrupción, el manejador los verá en un estado inconsisten- te y fallará. Un ejemplo obvio en donde puede ocurrir esto es si P es el planificador. Suponga que cierto proceso utilizó su quantum y el sistema operativo lo estaba desplazando al final de su cola. Luego, a mitad de la manipulación de la lista ocurre la interrupción, hace que un proceso cambie al estado listo y ejecuta el planificador. Con las colas en un estado inconsistente, es probable que el sistema falle. Como consecuencia incluso en un uniprocesador, es mejor que la mayor parte del sistema ope- rativo sea reentrante, que las estructuras de datos críticas estén protegidas por mutexes y que las in- terrupciones se deshabiliten cuando no se puedan tolerar. Fuerza bruta El uso de fuerza bruta para resolver un problema ha acumulado una mala reputación a través de los años, pero con frecuencia es el método más conveniente debido a su simplicidad. Cada sistema ope- rativo tiene muchos procedimientos que reciben muy pocas llamadas, o que operan con tan pocos datos que no vale la pena optimizarlos. Por ejemplo, con frecuencia es necesario buscar en varias tablas y arreglos dentro del sistema. El algoritmo de fuerza bruta es sólo dejar la tabla en el orden en el que se realizan las entradas y buscar en forma lineal cuando haya que realizar una búsqueda. Si el número de entradas es pequeño (por ejemplo, menor de 1000), la ganancia de ordenar la tabla o generar valores de hash es poca, pero el código es mucho más complicado y es más probable que contenga errores. 986 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 Desde luego que para las funciones que están en la ruta crítica (como el cambio de contexto), se debe hacer todo el esfuerzo posible para que sean muy rápidas, tal vez hasta escribirlas en len- guaje ensamblador (como último recurso). Pero las partes extensas del sistema no están en la ruta crítica. Por ejemplo, muchas llamadas al sistema se invocan raras veces. Si hay una llamada a fork cada segundo y requiere 1 mseg para llevarse a cabo, entonces aunque se optimice a 0 sólo gana un 0.1%. Si el código optimizado es mayor y tiene más errores, tal vez sea más conveniente no llevar a cabo la optimización. Comprobar errores primero Muchas llamadas al sistema pueden llegar a fallas por una variedad de razones: el archivo que se va a abrir pertenece a alguien más; la creación del proceso falla debido a que la tabla de procesos está llena; o una señal no se puede enviar debido a que el proceso de destino no existe. El sistema operativo debe comprobar con detalle todos los errores posibles antes de llevar a cabo la llamada. Muchas llamadas al sistema también requieren la adquisición de recursos, como ranuras en la tabla de procesos, ranuras en la tabla de nodos-i o descriptores de archivos. Un consejo general que puede ahorrar muchos problemas es comprobar primero si la llamada al sistema se puede llevar a cabo antes de adquirir recursos. Esto significa colocar todas las pruebas al principio del procedi- miento que ejecuta la llamada al sistema. Cada prueba debe ser de la siguiente forma: if (condicion_error) return (CODIGO_ERROR); Si la llamada pasa por toda la gama de pruebas, entonces es seguro que tendrá éxito. En ese punto se pueden adquirir los recursos. Intercalar las pruebas con la adquisición de recursos significa que si falla alguna prueba a lo largo del camino, todos los recursos adquiridos hasta ese punto se deben regresar. Si se comete un error aquí y no se devuelve cierto recurso, los daños no se reflejan de inmediato. Por ejemplo, una entrada en la tabla de procesos tal vez ya no esté disponible en forma permanente. Sin embargo, du- rante cierto periodo este error se puede activar varias veces. En un momento dado, la mayoría de las entradas (o todas) en la tabla de procesos tal vez ya no estén disponibles, con lo cual se produ- cirá un fallo del sistema de una forma en extremo impredecible y difícil de depurar. Muchos sistemas sufren de este problema en la forma de fugas de memoria. Por lo general, el programa llama a malloc para asignar espacio pero olvida llamar a free más adelante para liberar- lo. De esta forma, la memoria va desapareciendo en forma gradual hasta que se agota y se reinicia el sistema. Engler y sus colaboradores (2000) han propuesto una forma interesante de comprobar algunos de estos errores en tiempo de compilación. Ellos observaron que el programador conoce muchas in- variantes que el compilador no conoce; por ejemplo, cuando se bloquea un mutex, todas las rutas que inician en el bloqueo deben de contener un desbloqueo y no debe haber más bloqueos del mis- mo mutex. Han ideado una forma para que el programador indique al compilador este hecho y lo instruya para que compruebe todas las rutas en tiempo de compilación, en caso de que haya viola- ciones de la invariante. El programador también puede especificar que la memoria asignada se de- be liberar en todas las rutas y muchas otras condiciones también. SECCIÓN 13.4 RENDIMIENTO 987 13.4 RENDIMIENTO En igualdad de condiciones, un sistema operativo rápido es mejor que uno lento. Sin embargo, un sistema operativo rápido pero no confiable no es tan bueno como uno lento pero confiable. Como las optimizaciones complejas a menudo producen errores, es importante utilizarlas con moderación. A pesar de esto, hay lugares en donde el rendimiento es crítico y las optimizaciones bien valen el esfuerzo. En las siguientes secciones analizaremos algunas técnicas generales que se pueden utili- zar para mejorar el rendimiento en lugares en donde se requiere. 13.4.1 ¿Por qué son lentos los sistemas operativos? Antes de hablar sobre técnicas de optimización, vale la pena recalcar que la lentitud de muchos sis- temas operativos es en gran parte auto-infligida. Por ejemplo, los sistemas operativos antiguos co- mo MS-DOS y UNIX versión 7 se iniciaban en unos cuantos segundos. Los sistemas UNIX y Windows Vista modernos pueden requerir minutos para iniciarse, a pesar de que se ejecutan en hardware que es 1000 veces más rápido. La razón es que están haciendo muchas cosas más, se de- seen o no. Un caso en cuestión. La tecnología plug and play facilita de cierta manera la instalación de un nuevo dispositivo de hardware, pero el precio a pagar es que en cada inicio, el sistema ope- rativo tiene que inspeccionar todo el hardware para ver si hay algo nuevo. Esta exploración del bus requiere tiempo. Un método alternativo (y, según la opinión del autor, mejor) sería desechar por completo la tec- nología plug-and-play y tener en la pantalla un icono llamado “Instalar nuevo hardware”. Al insta- lar un nuevo dispositivo de hardware, el usuario haría clic en este icono para empezar la exploración del bus, en vez de hacerlo en cada inicio del sistema. Por supuesto que los diseñadores de los siste- mas actuales estaban muy al tanto de esta opción. La rechazaron, básicamente debido a que supo- nían que los usuarios eran demasiado estúpidos como para poder hacer esto en forma correcta (aunque lo definieron de una manera más amable). Éste es sólo un ejemplo, pero hay muchos más en donde el deseo de hacer al sistema “amigable para el usuario” (o “a prueba de idiotas”, depen- diendo del punto de vista del lector) reduce su velocidad todo el tiempo y para todos. Tal vez lo mejor que pueden hacer los diseñadores para mejorar el sistema es ser mucho más selectivos en cuanto a agregar nuevas características. La pregunta por hacer no es “¿A algunos usua- rios les gusta?” sino “¿Vale la pena el inevitable sacrificio en cuanto al tamaño del código, la veloci- dad, complejidad y confiabilidad?”. Sólo si las ventajas superan de manera concisa a las desventajas es cuando se debe incluir. Los programadores tienen la tendencia a suponer que el tamaño de códi- go y el conteo de errores serán de 0 y la velocidad será infinita. La experiencia muestra que esta idea es un poco optimista. Otro factor que participa en el rendimiento es el marketing de los productos. Para cuando la versión 4 o 5 llega al mercado, tal vez se hayan incluido todas las características que son actual- mente útiles, y la mayoría de las personas que necesitan el producto ya lo tienen. Sin embargo, pa- ra continuar con las ventas, muchos fabricantes continúan produciendo un flujo continuo de nuevas versiones con más características, sólo para que puedan vender actualizaciones a sus clientes exis- 988 DISEÑO DE SISTEMAS OPERATIVOS CAPÍTULO 13 tentes. Agregar nuevas características sólo por ello puede ayudar en las ventas, pero raras veces ayu- da en el rendimiento. 13.4.2 ¿Qué se debe optimizar? Como regla general, la primera versión el sistema debe ser lo más simple posible. Las únicas opti- mizaciones deben ser cosas que sin duda indiquen un problema inevitable. Tener una caché de blo- ques para el sistema de archivos es un ejemplo. Una vez que el sistema esté en funcionamiento, se deben realizar mediciones cuidadosas para ver a dónde se va realmente el tiempo. Con base en es- tas cifras, se deben realizar optimizaciones en donde sean más útiles. He aquí una verdadera historia de una optimización que hizo más daño que beneficio. Uno de los estudiantes del autor (cuyo nombre no divulgaremos) escribió el programa mkfs de MINIX. Es- te programa establece un nuevo sistema de archivos en un disco recién formateado. El estudiante invirtió aproximadamente 6 meses para optimizarlo, incluyendo la integración de u