EL SISTEMA OPERAETIVO UNIX Un Reporte de SUN MICROSYSTEMS 1. Perspectiva El sistema operativo UNIX es un sistema con funcionalidades multiproceso (o multitarea) similar a otros sistemas operativos de minicomputadoras y mainframes. Pero el sistema UNIX es también un fenómeno. Diseñado por un pequeño grupo de personas para su propio uso, se ha convertido, casi sin esfuerzo de marketing convencional, en quizás el sistema operativo más utilizado para computadoras de gama media. El sistema UNIX alcanzó su popularidad no gracias a una campaña corporativa, sino al entusiasmo de sus usuarios. Estos usuarios tienen diversas impresiones y opiniones sobre el sistema, por lo que la imagen pública del sistema UNIX es algo difusa. Este capítulo intenta aclarar esta situación analizando algunos aspectos clave: por qué el sistema UNIX es tan popular; por qué recibe críticas justificadas; cómo lo ha modificado Sun; y por qué existen múltiples versiones del sistema UNIX. 1.1 Fuentes de Fortaleza La extraordinaria popularidad del sistema UNIX se debe a tres de sus propiedades fundamentales: 1. El sistema UNIX es un sistema operativo portátil. Los fabricantes de hardware se sienten atraídos por él porque pueden implementarlo en un nuevo ordenador con relativamente poco esfuerzo; al hacerlo, heredan una gran comunidad de usuarios. Con implementaciones disponibles en docenas de máquinas, tanto los proveedores de software como los usuarios se vuelven independientes del ordenador al adoptar el sistema UNIX. Un programa en lenguaje de alto nivel puede utilizar directamente las funcionalidades del sistema operativo y, sin embargo, transportarse a otra máquina simplemente recompilándolo. No solo los programas se mueven fácilmente de una máquina UNIX a otra, sino también las herramientas y la experiencia del personal. El personal capacitado en una implementación de UNIX es productivo casi de inmediato en otra. 2. El sistema UNIX proporciona un entorno informático excepcionalmente productivo, especialmente para los programadores. El sistema UNIX incluye un amplio conjunto de herramientas de desarrollo de software, muchas de las cuales también se aplican a la manipulación general de texto (por ejemplo, documentación). Además, estas herramientas pueden utilizarse en combinación para resolver una gran cantidad de problemas sin necesidad de programación convencional. En resumen, el sistema UNIX probablemente soporta el ideal del software reutilizable con mayor eficacia que cualquier otro sistema operativo comercial. 3. Finalmente, el sistema UNIX presenta un diseño elegante, intelectualmente atractivo y satisfactorio de usar; muchos usuarios experimentados dirían que incluso es divertido. Es difícil captar la esencia de su elegancia, pero reside en su potencia derivada de la simplicidad, más que de la complejidad. En lugar de la larga lista de características semiindependientes (y a veces contradictorias) que caracterizan a muchos sistemas operativos, el sistema UNIX proporciona un número relativamente pequeño de funcionalidades fundamentales, pero universales. Logra la generalidad eliminando restricciones en lugar de añadir opciones. En palabras de uno de sus diseñadores, se trata de un «sistema simple y coherente que lleva al límite unas pocas buenas ideas y modelos». Al mismo tiempo, los usuarios pueden conectar estos sencillos mecanismos para crear nuevas y potentes herramientas que satisfagan sus propias necesidades o reflejen sus propias preferencias. Es fácil comprender y apreciar el valor de un sistema operativo portátil. Las cualidades de elegancia y mayor productividad del sistema UNIX son menos tangibles. Este informe intenta demostrarlas, pero solo se pueden comprender en profundidad tras utilizar el sistema durante unos meses. Sin embargo, son estas cualidades las que alimentan el entusiasmo de los usuarios del sistema UNIX; es muy difícil encontrar un usuario que comprenda profundamente el sistema UNIX y que, a la vez, elija voluntariamente otro sistema operativo comercial. Las fortalezas del sistema superan con creces las deficiencias descritas en la siguiente sección. 1.2 Puntos débiles Los críticos suelen centrar sus quejas en la interfaz que el sistema UNIX presenta a los usuarios novatos. La interfaz de comandos es escueta e inconsistente, los informes de errores son imprecisos y el sistema carece de una función de ayuda. Algunos comandos tienen nombres extraños, como grep y awk. El sistema presupone que los usuarios saben lo que hacen; por ejemplo, su acción predeterminada es sobrescribir un archivo existente cuando se crea un nuevo archivo con el mismo nombre. ¿Por qué se presentan estas deficiencias, ampliamente percibidas, en un sistema tan popular? Son consecuencia del entorno en el que se diseñó y se utilizó por primera vez el sistema UNIX. Este entorno era un grupo de investigación en ciencias de la computación de los Laboratorios Bell a principios de la década de 1970. Los miembros de esta comunidad eran expertos en informática y estaban creando un sistema para su propio uso; no tenían pretensiones de crear un sistema operativo comercial. Su hardware consistía en una pequeña minicomputadora equipada con lentas terminales de teletipo. Diseñaron el sistema de manera que las utilidades del sistema operativo, que constituyen gran parte de la interfaz de usuario, pudieran ser añadidas por los usuarios, y muchos lo hicieron. No es de extrañar que este entorno diera origen a un sistema de tiempo compartido simple, con una interfaz de usuario concisa e inconsistente (ref: (Adiós a esos terminales lentos y a sus usuarios expertos). El sistema UNIX se extendió inicialmente a entornos similares, principalmente laboratorios de investigación y departamentos de informática universitarios. La interfaz de usuario no presentó dificultades particulares para estas comunidades, especialmente si se comparaba con las ventajas del sistema. Más tarde, cuando el sistema fue adoptado por programadores profesionales en la industria, la diferencia en la interfaz de usuario tampoco supuso un gran problema. Ahora, sin embargo, los usuarios del sistema UNIX incluyen tanto a personas sin conocimientos informáticos como a informáticos. Como resultado, la dificultad de la interfaz de usuario utilizada en nuevos entornos se ha hecho aparente. Los usuarios están recibiendo, como corresponde, mayor atención. Puede haber otra razón por la que la interfaz de usuario ha tardado en cambiar: los componentes de hardware y conceptuales para mejorar radicalmente la interfaz de usuario, como procesadores dedicados, pantallas de mapa de bits, dispositivos señaladores y los conceptos de ventanas y menús, solo recientemente se han vuelto ampliamente accesibles y asequibles. También se han señalado otras debilidades del sistema UNIX: espacios de direcciones de proceso reducidos, un sistema de archivos lento y una comunicación entre procesos excesivamente limitada. Este tipo de defectos han dificultado en cierta medida la aceptación del sistema como sistema operativo de propósito general. Como se mostrará en este informe, algunas versiones de UNIX, incluida la de Sun, han eliminado la mayoría de estos problemas, ampliando así el alcance de los problemas a los que el sistema puede aplicarse eficazmente. 1.3 Guía de la evolución Los usuarios del sistema UNIX han cambiado desde su diseño, al igual que los problemas a los que se ha aplicado. Pero el hardware en el que se ejecuta ha cambiado aún más drásticamente. Los microordenadores con espacios de direcciones de varios megabytes son ahora comunes, y los terminales de visualización rápidos han sustituido por completo a los teletipos. Las pantallas gráficas de mapa de bits y los dispositivos señaladores, como los ratones, se están convirtiendo en equipos estándar. Cada vez más, el tiempo compartido está dando paso a redes de estaciones de trabajo dedicadas. Para adaptarse a los patrones de uso cambiantes y al desarrollo del hardware, el sistema UNIX ha evolucionado a lo largo de los años, y debe seguir haciéndolo. Al mismo tiempo, para mantener intacta una de sus principales ventajas —la portabilidad—, también se ha mantenido igual y debe seguir siéndolo. Estos requisitos, aparentemente paradójicos, pueden conciliarse distinguiendo cuidadosamente entre interfaces e implementaciones. Dos tipos de interfaz son de interés: las interfaces de programa y las interfaces de usuario. El enfoque general de Sun consiste en mantener las interfaces existentes para preservar la portabilidad y añadir nuevas interfaces para facilitar el acceso a nuevas funcionalidades. Como ejemplo de una nueva interfaz, considérese el sistema SunWindows, descrito más adelante en este informe. SunWindows permite crear aplicaciones que presentan interfaces basadas en las tecnologías más recientes de superposición de ventanas, menús emergentes y el ratón. Esta aplicación se denomina herramienta. Una herramienta puede diseñarse para asumir que su usuario no conoce el sistema UNIX, quizás ni siquiera cómo teclear. Por lo tanto, SunWindows permite el desarrollo de interfaces de sistema alternativas. Aunque la mayoría de las herramientas son desarrolladas por clientes de Sun, varias de propósito general se incluyen con el sistema SunWindows. Una de ellas es un emulador de terminal que hace que una ventana se comporte como una terminal de visualización convencional, conservando así la interfaz tradicional. El sistema UNIX SunWindows incorpora nuevas tecnologías de interfaz de sistema natural, lo que permite interfaces para diversos usuarios y, al mismo tiempo, mantiene la compatibilidad con el sistema UNIX. Además de añadir nuevas interfaces, Sun ha mejorado la implementación de varias funcionalidades y seguirá haciéndolo. Mientras que una interfaz define qué hace algo, una implementación define cómo se hace. Las implementaciones son funcionalmente invisibles y, por lo tanto, pueden modificarse sin afectar negativamente a los programas ni a los usuarios. Por ejemplo, el sistema de archivos de Sun es considerablemente más rápido que muchas implementaciones de sistemas de archivos UNIX, y la gestión de memoria virtual de Sun admite programas que son demasiado grandes para muchas versiones de UNIX. Los programas escritos para otras versiones de UNIX funcionan correctamente en Sun porque Sun no ha modificado las interfaces del sistema de archivos ni de la gestión de memoria, solo sus implementaciones. 1.4 Múltiples versiones Hablando de versiones, como muestra la Figura 1.1, ha habido y sigue habiendo varias versiones del sistema UNIX. Estas son en gran medida compatibles, pero no completamente. En general, dos factores han impulsado la evolución del sistema UNIX: las nuevas capacidades de hardware (por ejemplo, microprocesadores de memoria virtual y redes de área local) y la búsqueda de una mayor aplicabilidad (por ejemplo, desde su ejecución en minicomputadoras de tiempo compartido hasta estaciones de trabajo dedicadas). A medida que el sistema madure y obtenga el compromiso de más fabricantes de computadoras, su estructura debería estabilizarse. Por el momento, la compatibilidad es una Un problema mayor de lo que debería ser. (Por otro lado, es difícil pensar en algún sistema operativo comercial disponible para procesadores de arquitectura diferente. El sistema UNIX está disponible comercialmente para al menos una docena de ellos). La siguiente reseña histórica traza los orígenes de las versiones actuales y ofrece algunas razones para su desarrollo. Fig. 1.1 Genealogía del sistema UNIX El nombre «UNIX» no es un acrónimo, sino un juego de palabras con la expresión «MULTICS castrado», siendo MULTICS un sistema operativo que influyó (tanto positiva como negativamente) en los diseñadores del sistema UNIX. En 1969 no existía un sistema operativo sencillo y potente para minicomputadoras, por lo que Ken Thompson, de los Laboratorios Bell, se propuso crear uno para su propio uso y el de su grupo de investigación en programación. Pronto se le unió Dennis Ritchie. Tras la implementación inicial en lenguaje ensamblador para el PDP-7* y dos versiones en lenguaje ensamblador para el PDP/11, se alcanzó un hito importante en 1973. El sistema se tradujo del lenguaje ensamblador del PDP/11 al lenguaje de nivel medio C. (C fue desarrollado por Ritchie y se basaba en el lenguaje B de Thompson, descendiente a su vez del BCPL de Martin Richards). La recodificación del sistema en C redujo sus componentes dependientes de la máquina a un pequeño porcentaje del total de líneas de código y simplificó la tarea de transportar el sistema UNIX a una nueva máquina a tan solo unos meses. (También aumentó el tamaño del sistema en aproximadamente un tercio). Pronto, una versión se recompiló en los Laboratorios Bell en un Interdata 8/32, una máquina cuya arquitectura era muy similar a la de un IBM/370 y muy poco a la de un PDP/11. La implementación en C se describió en el número de julio de 1974 de Communications of the ACM. Esta fue la primera exposición del sistema UNIX a un público amplio fuera de Bell Labs. La esencia del sistema no ha cambiado desde entonces. En 1974, Bell Labs comenzó a licenciar la versión 5, y poco después la versión 6, a universidades por una tarifa simbólica.⁴ El sistema no contaba con soporte, pero el código fuente se incluía en la distribución. Como resultado, durante los años siguientes, el sistema UNIX fue utilizado y estudiado por muchos estudiantes. Tras su graduación, estos estudiantes entusiastas comercializaron el sistema. La versión 7 es la antecesora común de prácticamente todas las versiones de UNIX existentes en la actualidad. En comparación con su predecesora, la versión 7 era más fácil de portar e incorporaba un mejor sistema de archivos y mejoras al lenguaje C. El siguiente acontecimiento histórico importante tuvo lugar en 1980, cuando el Departamento de Defensa de los Estados Unidos encargó a la Universidad de California en Berkeley el rediseño del sistema UNIX. Hasta ese momento, el sistema UNIX había sido un sistema operativo de tiempo compartido puro. El grupo de Berkeley debía transformarlo en un vehículo adecuado para la investigación en el cómputo distribuido. El resultado se conoce comúnmente como 4BSD, por Fourth Berkeley Software Distribution (los esfuerzos anteriores de Berkeley para el PDP/11 explotar la memoria virtual se conocen como 2BSD). El primer desarrollo de este proyecto se denominó 4.1BSD; Sus orígenes se remontan a System 32V, una versión de UNIX que se ejecutaba en el VAX, pero no utilizaba las funcionalidades de esa máquina. 4.1BSD, que supuso un rediseño importante de System 32V, incluía: • Un espacio de direcciones de proceso mucho más amplio; • Memoria virtual paginada bajo demanda; • Un sistema de archivos más rápido y robusto; • Comunicación entre procesos generalizada, incluyendo soporte básico para redes locales. Berkeley también añadió utilitarios, como un editor de pantalla completa, una interfaz genérica para terminales inteligentes y un intérprete de comandos alternativo. Este segundo grupo de funcionalidades, que muchas versiones de UNIX han adoptado, se suele describir como las «mejoras de Berkeley», pero es el primer grupo el que constituye el verdadero logro sobresaliente de Berkeley. En 1982, AT&T ofreció su primera versión comercial del sistema UNIX, llamada System III. System II añadió una serie de nuevas capacidades, incluyendo la entrada remota de trabajos y un sistema de control de código fuente para gestionar grandes proyectos en constante evolución, productos de software y rutinas contables, pero mantuvo las limitaciones de gestión de memoria de sus predecesores. El sucesor del System III, el System V, era en gran medida compatible con el System III e incluía un sistema de archivos más rápido, un controlador de terminal mejorado, comunicación interprocesos generalizada (muy diferente a la de Berkeley), memoria compartida y semáforos. Sun adoptó el diseño 4.2BSD como punto de partida para su sistema operativo. Si bien en muchos aspectos era adecuado para una red de estaciones de trabajo técnicas, el 4.2BSD sigue siendo, en esencia, un sistema de tiempo compartido para minicomputadoras. Por consiguiente, tras portar el sistema a su propio hardware, Sun inició un programa continuo de adaptación y extensión. Por ejemplo, se añadió compatibilidad con gráficos y una interfaz de ventana/ratón, y las estaciones de trabajo sin discos locales obtuvieron la capacidad de usar la red de forma transparente para paginación, almacenamiento y compartición de archivos. Aún se conservan vestigios de la herencia del tiempo compartido (por ejemplo, el planificador de procesos y el gestor de memoria virtual), pero también debió reimplementarse para adaptarse mejor al entorno de las estaciones de trabajo Sun. 1.5 Conclusión El sistema UNIX es, sin duda, uno de los grandes hitos en la historia de la informática. Sus fortalezas son sustanciales e innegables. No tiene defectos fatales. Sus deficiencias, que son en gran medida vestigios históricos, están desapareciendo. De hecho, el sistema ha demostrado ser extraordinariamente adaptable; ha incorporado los cambios con mayor fluidez que, por ejemplo, FORTRAN, el mismo sistema con el que a veces se le compara. De hecho, la comparación con el FORTRAN de las décadas de 1950 y 1960 es bastante acertada. FORTRAN fue el primer entorno de programación que trascendió las limitaciones de la máquina y del lenguaje de programación del fabricante. Los logros del lenguaje, y la idoneidad de FORTRAN para una amplia gama de aplicaciones, aunque supuso un gran avance con respecto al lenguaje ensamblador, de mayor importancia, sustituyó a docenas de lenguajes ensamblador; en otras palabras, su disponibilidad casi universal. FORTRAN demostró el poder de un lenguaje estándar (o en gran medida estándar) para programas y programadores. El sistema UNIX, por supuesto, no es un lenguaje, sino un sistema operativo. Si bien es considerablemente más potente, flexible y fácil de usar que FORTRAN, las similitudes entre ambos son evidentes. Aunque no está exento de fallos, el sistema UNIX es un muy buen sistema operativo. Es aplicable a un amplio espectro de situaciones informáticas. Proporciona un entorno en gran medida estándar tanto para programas como para programadores, que abarca una amplia gama de máquinas. Finalmente, el conjunto compartido de conocimientos y software construidos sobre este estándar ofrece una enorme ventaja para la aplicación de la informática a problemas empresariales y técnicos. 2. El Kernel Estrictamente hablando, el kernel es el núcleo del sistema operativo UNIX; el shell y las utilidades son programas comunes sin privilegios especiales. El núcleo es una máquina virtual que oculta las características del hardware subyacente, proporcionando un conjunto de servicios independientes de cualquier ordenador en particular. Estos servicios se dividen en cuatro grandes grupos funcionales: • procesos, • gestión de memoria, • E/S y • temporizadores. Antes de describirlos, es necesario ver cómo se invocan. 2.1 Llamadas al sistema El núcleo realiza algunas funciones de forma autónoma (por ejemplo, la asignación de ciclos de CPU y memoria real) y otras en respuesta a las solicitudes de servicio de los procesos. Estas solicitudes se denominan llamadas al sistema. Para el programador, una llamada al sistema es indistinguible de una llamada a una función o procedimiento común. Las llamadas al sistema se realizan con mayor frecuencia desde programas en C (el lenguaje C se describe más adelante en este informe), pero también pueden realizarse desde programas escritos en otros lenguajes. Para facilitar la comprensión a quienes no estén familiarizados con C, los ejemplos de este informe están escritos en pseudocódigo similar al de los lenguajes de la familia Pascal. Todas las llamadas al sistema adoptan la forma de funciones. Una función siempre devuelve un valor y, en el caso de las llamadas al sistema, el valor -1 indica una ejecución fallida. Cuando una llamada devuelve -1, una variable externa llamada `errno` describe la naturaleza del problema. 2.2 Procesos En este informe, consideramos un programa como un texto de instrucciones y datos interpretable por máquina; es análogo a una partitura musical. Definimos un proceso como una ejecución de un programa, análoga a una interpretación. (Algunos sistemas operativos utilizan el término «tarea» para referirse a esta unidad de ejecución). La ejecución simultánea de múltiples procesos es habitual en un sistema UNIX, y varios procesos con frecuencia ejecutan el mismo programa (por ejemplo, un compilador o un shell) simultáneamente. Sin embargo, cada proceso se ejecuta en su propio espacio de direcciones, protegido de todos los demás procesos. 2.2.1 Privilegios El sistema UNIX proporciona una serie de llamadas al sistema que, si bien son necesarias para la administración del sistema, pueden ser perjudiciales o dañinas si se utilizan incorrectamente. Solo los procesos con privilegios pueden ejecutar dichas llamadas. El control fundamental sobre qué procesos tienen privilegios y cuáles no reside en el sistema de inicio de sesión. En el sistema UNIX, todos los procesos son creados en última instancia por usuarios que inician sesión en el sistema predefinido introduciendo nombre de usuario en un campo llamado root y una contraseña. El usuario root denota al superusuario del sistema. El superusuario es la persona responsable de administrar el sistema. En general, los procesos creados por el superusuario son privilegiados y todos los demás no lo son; por lo tanto, la capacidad de crear un proceso privilegiado está restringida a los usuarios que conocen la contraseña de root. Sin embargo, el sistema UNIX también cuenta con un mecanismo de concesión de privilegios más granular llamado set-uid, que permite a los usuarios sin privilegios ejecutar programas de confianza como procesos privilegiados. Esta funcionalidad, que de hecho está patentada, se describe más adelante en este capítulo. 2.2.2 Creación y Terminación En comparación con muchos sistemas operativos, un sistema UNIX típico presenta una tasa bastante alta de creación y terminación de procesos. Parte de esta actividad se debe al diseño del intérprete de comandos, que habitualmente crea un proceso para ejecutar una instrucción `comm`. En el sistema UNIX, la mayoría de los comandos son simplemente programas de utilidad comunes; generalmente no están integrados en el sistema operativo. Los programas escritos por el usuario suelen emplear la misma técnica. Por ejemplo, un proceso podría querer contar los caracteres de un archivo. Una utilidad llamada `we` hace precisamente eso. En lugar de ejecutar su propio código, el proceso puede crear un proceso hijo para ejecutar `we`. Así, en el sistema UNIX, los procesos utilizan con frecuencia otros procesos como los programas utilizan subrutinas, y por la misma razón: para evitar reinventar código que ya existe. Por supuesto, los procesos también crean otros procesos para ejecutar partes de algoritmos en paralelo, la razón convencional para crear un proceso en la mayoría de los sistemas operativos, pero el uso de un proceso como algo similar a una subrutina es en gran medida exclusivo del sistema UNIX y del estilo de programación que se asocia con él. Cuando un proceso crea otro para que realice alguna tarea, el nuevo proceso suele tener algo en común con el anterior. Esto puede abarcar desde la completa interdependencia (el proceso antiguo crea otra instancia de sí mismo), hasta compartir archivos pero ejecutar programas diferentes (como en el ejemplo de conteo de caracteres anterior), o la completa independencia. Es difícil que una sola llamada a "crear proceso" (como las que proporcionan los sistemas operativos multiproceso típicos) gestione adecuadamente todas las situaciones posibles. El sistema UNIX proporciona varias llamadas sencillas que se pueden combinar para lograr el efecto deseado. Estas llamadas están diseñadas para aprovechar las situaciones en las que los procesos nuevo y antiguo comparten algunos recursos. En el sistema UNIX, un proceso se representa mediante tres segmentos de memoria, denominados segmentos de texto (o código), datos y pila, y mediante un conjunto de estructuras de datos conocidas colectivamente como el entorno del proceso. Un segmento de texto contiene código y datos constantes, un segmento de datos contiene variables y un segmento de pila almacena la pila del proceso. El entorno del proceso registra la información que el núcleo necesita para gestionar el proceso, como el contenido de los registros, la prioridad, los archivos abiertos, etc. Para su protección, un proceso no puede acceder a su entorno, pero puede modificarlo mediante llamadas al sistema. Se crea un nuevo proceso con la llamada al sistema `fork`. Esta llamada no requiere parámetros, ya que el proceso recién creado (llamado hijo) es, en efecto, una copia del proceso que llama a `fork` (conocido como padre). * `fork` copia los datos del padre (transposición). Pasa segmentos de pila al hijo y copia el entorno del padre también al hijo. El proceso hijo comparte el segmento de texto de su proceso padre para conservar espacio en la memoria (los programas UNIX, por defecto, reutilizan el entorno a través de las generaciones). Esto facilita el uso compartido y minimiza el trabajo que los procesos hijos deben realizar antes de poder ejecutarse; en lugar de tener que crear un nuevo entorno, solo tienen que modificar la parte de su herencia que resulta inapropiada. La función `fork` es inusual porque devuelve un valor tanto al proceso padre como al hijo —el hijo ejecuta el mismo programa que el padre— pero devuelve un valor diferente a cada uno. El padre recibe el ID del proceso del hijo (que nunca es 0) y el hijo recibe 0. De esta manera, las dos ejecuciones del programa padre pueden distinguir cuál es cuál y tomar diferentes ramas en el código. Esto es más sencillo de lo que parece; la Figura 2.1 muestra un ejemplo. Fig. 2.1 Creación de un proceso hijo (* padre llama a `fork` ID del proceso:= `fork()`; (* padre y hijo si process_id entonces... si no. (* *) prueba process_id *) = 0 childtakesthispath*) (* parenttakesthispath*) fin Si; tras comprobar el valor devuelto por fork, el proceso padre e hijo ejecutan diferentes ramas del mismo programa en paralelo. En otras palabras, el padre acaba de crear una instancia de sí mismo. A menudo, el hijo, si bien conserva gran parte de su entorno heredado, realiza algunos cambios antes de continuar con su trabajo principal. Si el hijo necesita ejecutar un programa diferente, como suele ocurrir, emite una llamada a exec. exec reemplaza los segmentos de texto y datos de quien lo llama con los de un nuevo programa leído de un archivo especificado en la llamada. exec no altera el entorno de quien lo llama; un proceso hijo puede estar ejecutando un programa diferente, pero aún tiene acceso a los archivos de su padre (posiblemente modificados por el hijo entre fork y exec). Después Al emitir una llamada `exec`, la siguiente instrucción que ejecuta el proceso hijo es la primera instrucción del nuevo programa. Por lo tanto, una bifurcación seguida de una llamada `exec` equivale a una llamada tradicional de "crear proceso" monolítica. Cabe destacar que un programa secuencial multifase cuyas fases se comunican mediante archivos, como un compilador, puede aprovechar `exec` sin necesidad de bifurcación; cada fase puede finalizar con una llamada `exec` que invoca la siguiente fase. Cuando un proceso hijo está listo para terminar, emite una llamada al sistema `exit`; esta llamada recibe un parámetro cuyo valor se devuelve al proceso padre. Lo anterior describe la terminación normal; un proceso hijo puede terminar de forma anormal por cualquiera de las señales (que se describen a continuación más adelante en este capítulo) iniciado por el núcleo, por un usuario o por otro proceso. Cuando esto ocurre, el proceso padre recibe una notificación, también mediante una señal. Volviendo al proceso padre. El entorno del padre no se ve afectado por ninguna acción del hijo, ya que este trabaja con una copia de la descripción del entorno del padre. El padre puede ejecutarse libremente en paralelo con el hijo. Si el padre desea esperar a que el hijo finalice (los padres suelen usar a los hijos para ejecutar utilidades a modo de subrutinas, como se mencionó al principio de esta sección), realiza la llamada al sistema `wait`. `wait` devuelve el identificador del proceso hijo que ha finalizado (un padre puede estar esperando a que finalicen varios hijos) y el código de estado que el hijo devolvió al finalizar. La figura 2.2 muestra cómo un padre puede crear un hijo y esperar a que finalice. Si el padre decide ejecutarse en paralelo, puede que desee recibir una notificación cuando un hijo finalice, ya sea de forma normal o anormal. Puede hacerlo capturando la señal SIGCHLD; al recibir esta señal, el proceso padre puede emitir una espera además de averiguar qué le sucedió al proceso hijo. Además de las llamadas básicas fork y exec descritas anteriormente, existen variantes para situaciones específicas. Por ejemplo, vfork es un proceso fork optimizado (y una mejora de Berkeley) que se ejecuta más rápido cuando el proceso padre sabe que no desea ejecutarse en paralelo con el proceso hijo. En un proceso fork normal, la pila, los segmentos de datos y el entorno del proceso padre se copian para el proceso hijo, y el segmento de texto se comparte. vfork copia la pila, los datos y el entorno del proceso padre, pero no copia nada. El proceso hijo utiliza la función `vfork`. Eliminar la copia de segmentos hace que vfork sea rápido, pero también hace que el proceso hijo sea responsable de terminar sin haber realizado ninguna modificación que pueda confundir al proceso padre al reanudarse. 2.2.3 Planificación El sistema Sun UNIX planifica los procesos listos según sus prioridades base, ajustadas dinámicamente mediante ajustes de prioridad. Los valores de prioridad base oscilan entre -20 (alta) y +20 (baja); un nuevo proceso hereda la prioridad base de su proceso padre. En ausencia de cualquier acción explícita (como se explicará más adelante), la prioridad base será 0 (es decir, un valor medio). Durante su ejecución, un proceso paga por cada 200 ms de tiempo de CPU que consume (un reloj de hardware interrumpe el procesador cada 200 ms). Una vez por segundo, el núcleo recalcula los ajustes de prioridad de todos los procesos listos. Suma el ajuste de prioridad de un proceso a su prioridad base, obteniendo así su prioridad actual. Tras calcular todas las prioridades actuales, el núcleo ejecuta el proceso con la prioridad actual más alta. El ajuste de prioridad da preferencia a aquellos procesos que han consumido menos tiempo de CPU recientemente. El consumo de CPU en el pasado reciente presenta una disminución exponencial del uso de la CPU (90%). El uso anterior de la CPU se olvida en 5*# segundos, donde n es el promedio del último minuto del número de procesos ejecutables. Por lo tanto, el sistema penaliza más severamente a los procesos que consumen mucha CPU durante períodos de alta carga y los tolera mejor cuando la carga es baja. Esta técnica de planificación tiene tres resultados: 1. Los procesos limitados por E/S, que incluyen la mayoría de los procesos interactivos, se ejecutan muy rápidamente después de finalizar una operación de E/S (por ejemplo, después de recibir un comando de una terminal). Esto se debe a que, en el pasado reciente, dichos procesos estaban esperando E/S y, por lo tanto, no consumían tiempo de CPU. 2. Los procesos limitados por CPU no se bloquean, sino que se ejecutan en ráfagas de hasta un segundo, ya que, con el paso del tiempo, el planificador olvida el consumo de CPU de un proceso. Cuanto mayor sea la carga del sistema, menos a menudo un proceso que consume muchos recursos de la CPU recibirá acceso a ella, minimizando así su impacto en la respuesta interactiva. 3. Los cambios en el comportamiento de los procesos (por ejemplo, un proceso que inicialmente consume muchos recursos de la CPU al leer datos de un archivo y luego los procesa) se compensan automáticamente. Un proceso puede reducir su prioridad base con la llamada `setpriority`; sin embargo, solo un proceso privilegiado puede aumentar su prioridad base o cambiar la prioridad de un proceso no relacionado. 2.2.4 Señales Una señal UNIX es un mecanismo de software muy similar a una interrupción de hardware; permite notificar asíncronamente a un proceso que ha ocurrido un evento. A diferencia de las interrupciones, todas las señales tienen la misma prioridad; las señales que ocurren simultáneamente se entregan a un proceso de forma secuencial, pero sin un orden definido. Las señales no deben usarse como un mecanismo primitivo de comunicación entre procesos. Para este propósito se proporcionan tuberías y sockets (que se describen más adelante en este capítulo). Una señal puede originarse en: El hardware; el núcleo transforma las condiciones del hardware, como las violaciones de direccionamiento y las excepciones aritméticas, en señales. El núcleo (por ejemplo, un proceso solicita ser notificado cuando un dispositivo está listo para E/S). Otro proceso (por ejemplo, un proceso padre finaliza un proceso hijo que ha estado en ejecución durante demasiado tiempo). Un usuario (por ejemplo, un proceso en ejecución). Al presionar Control-C para interrumpir un proceso, un proceso El sistema puede enviar una señal a otro proceso con la llamada al sistema `kil1`. También puede enviar una señal a un grupo de procesos (normalmente, aquellos iniciados directa o indirectamente por un usuario) con la llamada al sistema `killpg`. Solo un proceso con privilegios puede enviar una señal a un proceso fuera de su grupo. Un proceso debe esperar recibir señales; puede gestionarlas de tres maneras: 1. Puede aceptar la acción predeterminada del sistema; a menudo, la acción predeterminada es terminar el proceso y notificar a su proceso padre. Para algunas señales (por ejemplo, violaciones de direccionamiento), la acción predeterminada incluye generar un archivo con la imagen de memoria del proceso para la depuración posterior. 2. Puede ignorar la señal; el kernel simplemente descarta una señal que un proceso está ignorando. Algunas señales "fuertes", como SIGKILL, que termina un proceso de forma anómala, no se pueden ignorar. 3. Puede capturar la señal. En este caso, el proceso designa una función (llamada manejador de señales, análoga a un manejador de interrupciones), que se invoca automáticamente al recibir la señal. Cuando el manejador finaliza, la ejecución se reanuda en la instrucción que estaba a punto de ejecutarse al recibir la señal. A menudo, un manejador de señales simplemente realiza algunas operaciones de limpieza (por ejemplo, eliminar un archivo temporal) y luego finaliza el proceso de forma controlada. Sin embargo, también es posible un manejo de señales más sofisticado. La disposición de señales de un proceso (es decir, cómo desea el proceso que el kernel trate cada tipo de señal que pueda recibir) se registra en su entorno. Dado que un proceso hijo hereda su entorno, en ausencia de cualquier acción por parte del hijo, el kernel le enviará señales de la misma manera que a su proceso padre. Si el hijo desea realizar alguna acción diferente con las señales, llama al sistema `sigvec` al inicio de su ejecución. Esta llamada recibe parámetros que especifican cómo tratar cada señal y las direcciones de los manejadores de las señales que se deben capturar. Si un proceso está capturando señales, puede haber secciones críticas de su código que deban ejecutarse sin la posibilidad de que se entregue una señal. Consideremos un editor que ejecuta la siguiente secuencia de operaciones: 1. Cambiar el nombre del archivo actual a archivo de copia de seguridad. 2. Cambiar el nombre del archivo de trabajo a archivo actual. 3. Eliminar el archivo de copia de seguridad. Si el proceso recibiera SIGINT (una señal de "interrupción de programa" que un usuario puede iniciar pulsando Control-C) durante la ejecución de esta secuencia, los archivos del usuario quedarían en un estado inconsistente. Las llamadas sigblock y sigsetmask de Berkeley se pueden usar para retrasar temporalmente las señales entrantes y proteger así una región tan crítica. 2.2.5 Tuberías Una tubería es un conducto que permite que un flujo de bytes unidireccional fluya entre dos procesos. Un proceso crea una tubería con la llamada al sistema `pipe`. Esta llamada devuelve dos descriptores: uno representa el extremo de escritura de la tubería (es decir, el extremo donde se escriben los bytes) y el otro, el extremo de lectura. (Los descriptores son fundamentales en el modelo de E/S de UNIX y se analizan más adelante en este capítulo). Un proceso puede escribir una cadena de bytes en una tubería con la llamada al sistema `write`. `write` recibe tres parámetros: 1. El descriptor que representa el extremo de escritura de la tubería; 2. El área de datos que contiene los bytes; 3. El número de bytes a escribir. `write` devuelve el número de bytes escritos (que, en el caso de una tubería, siempre es el número solicitado) o -1 en caso de error. El proceso que escribe finaliza la secuencia de bytes mediante la llamada al sistema `close`. Un proceso que desea leer de una tubería realiza una llamada al sistema `read`. Esta llamada es esencialmente la imagen especular de write, ya que toma los mismos tres parámetros. read devuelve 0 (bytes transferidos) cuando no quedan más datos en la tubería (es decir, el proceso que escribe ha cerrado el otro extremo de la tubería). El kernel serializa las operaciones simultáneas de los dos procesos en la tubería y almacena en búfer los bytes escritos pero aún no leídos. read y write se bloquean si el estado de la tubería impide la finalización inmediata de la llamada (es decir, escribir en una tubería llena o leer de una vacía). Una llamada write bloqueada se completa tan pronto como se eliminan suficientes bytes de la tubería; una lectura bloqueada se completa cuando se añaden bytes a la tubería. Se puede transferir un número ilimitado de bytes a través de una tubería. A diferencia de los archivos, las tuberías no tienen nombres a nivel de sistema; para leer o escribir en una tubería, un proceso debe tener un descriptor para ella. Los descriptores solo se pueden pasar dentro de un proceso o a los procesos hijos de un proceso (mediante herencia de entorno). Por lo tanto, para usar una tubería, un proceso debe haberla creado o heredarla de un ancestro que la creó. En la práctica, un proceso padre generalmente crea una tubería y luego bifurca los procesos hijos que la utilizan. Por consiguiente, los procesos no relacionados no pueden comunicarse a través de tuberías. Si bien las tuberías son muy útiles, esta es una limitación importante. Consideremos, por ejemplo, un proceso de servidor general como un administrador de cola de impresión. Un proceso de carga diferida debería poder enviar un archivo al administrador de cola para su impresión. Para permitirlo, se requiere una configuración específica. A fin de abarcar la comunicación entre procesos no relacionados, System III introdujo las tuberías con nombre, System V añadió las colas de mensajes y Berkeley añadió los sockets. 2.2.6 Sockets Para permitir que los procesos que se ejecutan en diferentes máquinas se comuniquen a través de una red de área local, 4.2BSD introdujo el concepto de sockets. Los procesos en la misma máquina también pueden comunicarse mediante sockets, sin necesidad de estar relacionados de ninguna manera. Por lo tanto, la comunicación intramáquina mediante sockets, que es simplemente un caso degenerado de comunicación intermáquina, es el tema del Informe Técnico de Servicios de Red. Cabe destacar, sin embargo, que las mismas llamadas al sistema de lectura y escritura funcionan en sockets que en tuberías, así como en archivos y dispositivos, como se mostrará más adelante en este capítulo. 2.3 Gestión de Memoria Debido a las limitaciones de la arquitectura PDP-11, las primeras implementaciones de UNIX proporcionaban espacios de direcciones de proceso relativamente pequeños. Dependiendo del modelo PDP-11, un proceso estaba limitado a 64 KB en total, o a 64 KB para código y 64 KB para datos y pila. La multiprogramación se lograba intercambiando procesos completos al disco. La innovación fundamental del VAX Berkeley rediseñó la arquitectura de gestión de memoria, que consistía en espacios de direcciones de proceso muy amplios, soportados por memoria virtual; una parte del núcleo UNIX para aprovechar las nuevas capacidades del VAX. La implementación de memoria virtual de Sun es, actualmente, principalmente una adaptación del diseño del VAX de Berkeley a la arquitectura de Sun. Por lo tanto, presenta las características de un diseño orientado a una máquina que funciona en un entorno de tiempo compartido y no es ideal para una estación de trabajo de un solo usuario. Sin embargo, la gestión de memoria virtual de Sun funciona razonablemente bien en la práctica y es muy superior a las implementaciones de UNIX sin memoria virtual. Esta sección introduce inevitablemente algunos temas de hardware, ya que la memoria es gestionada conjuntamente por el hardware de Sun y el kernel. No se trata, en absoluto, de una descripción completa de la Unidad de Gestión de Memoria del Sun-2. 2.3.1 Segmentos, Páginas y Marcos de Página El sistema UNIX define el segmento como la unidad básica de gestión de memoria. Como se mencionó anteriormente en este capítulo, cada proceso tiene un segmento de código (o texto), un segmento de datos y un segmento de pila. Todos los procesos que ejecutan el mismo programa comparten un único segmento de código, pero los segmentos de datos y de pila de cada proceso son privados. Las unidades de gestión de memoria de varios modelos PDP-11 admiten bien el concepto de segmentos de UNIX, permitiendo que los segmentos se protejan e intercambien. Sin embargo, no admiten memoria virtual. Si bien es posible la memoria virtual basada en segmentos, el tamaño variable de estos dificulta encontrarles espacio cuando se cargan desde el disco a la memoria. En consecuencia, la mayoría de los ordenadores actuales con memoria virtual, incluidos los Sun, definen la unidad básica de gestión de memoria como una página de tamaño fijo. Una página de Sun-2 tiene 2 kilobytes de longitud. Cada proceso UNIX de Sun-2 se ejecuta en un espacio de direcciones virtuales privado de hasta 8192 páginas o 16 megabytes, cuyos segmentos están formados por estas páginas. La figura 2.3 muestra cómo se organizan los segmentos de un proceso en su espacio de direcciones virtuales y cómo estos segmentos están compuestos por páginas. (Las referencias a "salto" y "límite de pila" en la figura se explican al final de esta sección). A diferencia de los grandes espacios de direcciones virtuales de los procesos, la arquitectura de Sun-2 define un único espacio de direcciones físicas de 8 megabytes; una estación de trabajo determinada puede tener tan solo un megabyte de memoria física. El espacio de direcciones físicas se divide conceptualmente en unidades del tamaño de una página, denominadas marcos de página. Dado que puede que no haya suficientes marcos de página para almacenar todas las páginas de un proceso grande (o varios procesos pequeños), en cualquier instante es probable que muchas páginas se almacenen temporalmente en un disco, o parte de él, conocido como dispositivo de intercambio. Mediante las estructuras de datos descritas en las siguientes secciones, el núcleo reorganiza las páginas entre el dispositivo de intercambio y los marcos de página de forma transparente para los procesos y, por lo general, para los usuarios. 2.3.2 Estructuras de datos El núcleo mantiene una entrada para cada proceso en su tabla de procesos. Esta tabla y otras estructuras de datos clave para la gestión de memoria se ilustran en la Figura 2.4. La tabla de procesos contiene información mínima para cada proceso, ya que siempre se mantiene en memoria. La información más detallada se almacena en la página de usuario de cada proceso. La página de usuario de un proceso contiene los datos de contabilidad que el núcleo necesita para suspender y reanudar la ejecución del proceso; por ejemplo, el contenido de los registros del proceso y los descriptores de archivo. La página de usuario también contiene un puntero a la tabla de páginas del proceso, la estructura de datos que describe la correspondencia actual entre las páginas de un proceso y los marcos de página de la estación de trabajo. Las llamadas al sistema `fork` y `exec` están estrechamente relacionadas con las estructuras de datos de gestión de memoria. Cuando un proceso se bifurca, el núcleo: O crea una nueva entrada en su tabla de procesos para el proceso hijo, copiando la mayor parte de su contenido de la entrada del proceso padre. O asigna una página de usuario y una tabla de páginas para el proceso hijo, y las rellena copiando datos de la tabla de páginas. Estructuras correspondientes del proceso hijo. Asigna marcos de página para las entradas de la tabla de páginas del proceso hijo y copia el contenido de los segmentos de datos y pila del proceso padre a los segmentos correspondientes del proceso hijo. El código del proceso padre no se copia, ya que el proceso hijo ejecuta inicialmente el mismo programa que su padre. Asigna espacio en el dispositivo de intercambio para los segmentos, la tabla de páginas y la página de usuario del proceso hijo. La llamada al sistema `vfork` elimina parte de la copia al llenar la tabla de páginas del proceso hijo con entradas que apuntan a los marcos de página de su padre. Sin embargo, compartir segmentos escribibles de esta manera limita el comportamiento tanto del padre como del hijo; tales restricciones no siempre son aceptables. Otra solución sería introducir otra variante de `fork` que marcara las entradas de la tabla de páginas del proceso hijo como de “copia en escritura”. Luego, las páginas de datos y de segmento de pila se copiarían una a una del proceso padre al hijo si, y cuando, el hijo intentara escribir en ellas. Todas las demás páginas se compartirían. Cuando un proceso emite una llamada exec, el núcleo libera el espacio de intercambio, los marcos de página y la tabla de páginas del proceso. A continuación, asigna nuevos según la información contenida en el archivo objeto que el proceso está ejecutando. Los nuevos segmentos del proceso se pueden cargar de dos maneras. Por defecto, el núcleo no realiza ninguna acción explícita y simplemente carga las páginas bajo demanda, como se describe más adelante en esta sección. (Las páginas de datos provienen del archivo objeto; las páginas de código provienen del archivo objeto, a menos que ya estén en memoria o en el dispositivo de intercambio). Alternativamente, un programa puede enlazarse de forma que el núcleo cargue sus segmentos cuando se ejecute; este método resulta ventajoso para programas pequeños (de menos de 32 KB aproximadamente). 2.3.3 Estados de memoria Desde el punto de vista de la gestión de memoria, un proceso puede estar en uno de tres estados: Intercambiado Un proceso intercambiado reside completamente en el dispositivo de intercambio; el kernel guarda la dirección de disco de la página de usuario del proceso en la tabla de procesos; a partir de la información registrada en la página de usuario, puede recuperar la tabla de páginas y los segmentos. Residente Un proceso residente tiene su página de usuario y tabla de páginas en memoria, y generalmente, también algunas de sus páginas de segmento. Mapeado Un proceso mapeado reside y, además, su tabla de páginas se carga en el mapa de páginas del sistema. Solo los procesos mapeados pueden ejecutarse. El mapa de páginas, que se describe con más detalle a continuación, es en realidad una caché de 8 tablas de páginas que probablemente serán necesarias en un futuro próximo. Por lo tanto, el "estado" mapeado es, más propiamente, una optimización del estado residente. El kernel cambia los estados de memoria de los procesos según sea necesario. Básicamente, cuando hay pocos procesos y poca demanda de memoria física, todos los procesos se asignan y la sobrecarga de gestión de memoria es mínima. A medida que el número de procesos supera la cantidad que se puede asignar simultáneamente, el kernel desasigna los procesos menos activos y cambia el estado de los más activos de residente a asignado. En condiciones de alta contención de memoria, el kernel debe mover algunos procesos entre los estados residente e intercambiado para mantener un buen rendimiento. Las siguientes secciones describen estos cambios de estado con más detalle. 2.3.4 Asignación de direcciones Un proceso en ejecución accede a la memoria mediante direcciones virtuales, que consisten esencialmente en un número de página virtual y un desplazamiento de bytes dentro de la página. La unidad de gestión de memoria (MMU) del hardware traduce el número de página virtual a un número de marco de página, suma el desplazamiento y envía la dirección física resultante a la memoria. Cabe destacar que las transferencias de Acceso Directo a Memoria Virtual (DVMA) se realizan generando direcciones virtuales que la MMU traduce de la misma manera. Por lo tanto, las siguientes explicaciones sobre los servicios de la MMU, como la protección de memoria, se aplican tanto a las referencias realizadas por dispositivos DMA como a las realizadas por la CPU. La clave para realizar la traducción de direcciones virtuales a físicas, o mapeo, reside en el mapa de páginas que el kernel mantiene en la memoria RAM de alta velocidad de la MMU. Como se mencionó anteriormente, cada proceso tiene su propia tabla de páginas, que contiene una entrada para cada una de sus páginas; cuando el proceso se está ejecutando, su tabla de páginas se carga en el mapa de páginas, que tiene un formato similar. La figura 2.5 muestra una entrada del mapa de páginas. Los campos de una entrada del mapa de páginas se describen a continuación. Modificado La MMU establece el bit de modificación de una página cada vez que el hardware escribe en ella; el kernel borra el bit cuando actualiza la copia de la página en el dispositivo de intercambio. Por lo tanto, el bit indica si existe una copia actual de la página en el dispositivo de intercambio. Acceso Cada vez que el hardware interactúa con una página (es decir, la lee, escribe en ella o recupera una instrucción), la MMU activa el bit de acceso de la página. Al borrar y comprobar periódicamente los bits de acceso, el kernel puede identificar las páginas que se usan con poca frecuencia; estas son buenas candidatas para enviarlas al dispositivo de intercambio. Válido El kernel borra el bit de validez cuando libera el marco de página de una página para su reutilización. Un intento de acceder a una página no válida es detectado por la MMU, que ordena al kernel que asigne la página a un marco de página y reinicie el proceso. La instrucción defectuosa. Protección La MMU verifica cada acceso a página para comprobar que cumple con los bits de protección de la página. Por ejemplo, un puntero o índice de matriz incorrecto no puede provocar una escritura en un segmento de código porque las páginas de los segmentos de código están marcadas como no modificables. Existen bits de protección distintos tanto para el proceso (usuario) como para el núcleo (supervisor). Estos permiten al núcleo almacenar la tabla de páginas y la página de usuario de un proceso en el espacio de direcciones del proceso, pero haciéndolos inaccesibles para este. El espacio de almacenamiento que ocupan la tabla de páginas y la página de usuario reduce ligeramente el tamaño efectivo del espacio de direcciones de un proceso. Los bits de protección impiden que un proceso interfiera consigo mismo (y con algunos de los datos del núcleo por proceso). El hecho de que cada proceso tenga su propia tabla de páginas no superpuesta (excepto en los segmentos de código compartidos) impide que los procesos interfieran entre sí y con el núcleo. Tipo La arquitectura Sun define cuatro espacios de direcciones físicas: • memoria integrada, • memoria externa, • V/O integrada y • E/S externa. El campo de tipo identifica el espacio de direcciones asociado a la página. Dado que los registros de E/S, por ejemplo, se asignan a páginas, los controladores de dispositivos pueden acceder a estos registros mediante el mecanismo de traducción de direcciones habitual. Sin embargo, el campo de tipo no es relevante para la gran mayoría de los programas. Para obtener un buen rendimiento, no existe un único mapa de páginas, sino ocho: uno para cada uno de los siete procesos o contextos, y uno dedicado al núcleo. Un registro de la MMU, denominado registro de contexto y gestionado por el núcleo, apunta al mapa correspondiente al proceso en ejecución. Al haber ocho mapas, el procesador puede alternar entre los ocho procesos asignados simplemente modificando el registro de contexto; solo cuando el proceso a ejecutar no está asignado, se requiere la sobrecarga de cargar su tabla de páginas en un mapa de páginas. Cuando el núcleo debe mapear un nuevo proceso, sobrescribe el mapa de páginas del proceso ejecutado más recientemente, actualizando primero los bits de acceso y modificación en las entradas de la tabla de páginas del proceso. Para agilizar la respuesta a las interrupciones y llamadas al sistema, existen dos registros de contexto: uno selecciona el mapa de páginas del proceso en ejecución y el otro, el del núcleo. El registro de contexto que se utiliza como puntero al mapa de páginas para una instrucción específica depende del estado del procesador, es decir, si se ejecuta en estado de usuario o de supervisor (MC68010). Normalmente, el procesador se ejecuta en estado de usuario; las interrupciones y las llamadas al sistema lo cambian a estado de supervisor; las instrucciones de retorno correspondientes lo vuelven a cambiar a estado de usuario. Por lo tanto, el núcleo nunca necesita modificar el registro de contexto en respuesta a una interrupción o una llamada al sistema; las referencias de memoria de todas las instrucciones ejecutadas en estado de supervisor se mapean automáticamente a través del mapa de páginas del núcleo. Cuando la ejecución pasa al código del kernel, se utiliza el mapa de páginas del kernel, por lo que este no tiene acceso a las páginas de los procesos de usuario. Cuando necesita acceder a una página de usuario, como por ejemplo para ejecutar una llamada al sistema de lectura o escritura, el kernel copia los datos con la instrucción MC68010 Move Space, que transfiere bytes de un espacio de direcciones a otro. Sin embargo, cuando se deben mover más de 512 bytes, el kernel añade temporalmente la(s) página(s) correspondiente(s) a su propio mapa de páginas y, a continuación, ejecuta una instrucción Move normal. 2.3.5 Paginación El proceso de reemplazar el contenido de los marcos de página con páginas diferentes se denomina paginación. Dos estructuras, además de las tablas de páginas, son fundamentales para la paginación: la lista de páginas libres y el bucle. La lista de páginas libres contiene los marcos de página que pueden reutilizarse. Los marcos de página se añaden al principio de la lista de páginas libres cuando se sabe que ya no son necesarios. Por el contrario, los marcos de página se añaden a la lista de páginas libres (taz/) cuando puedan volver a ser necesarios. Consideremos, por ejemplo, la finalización del último proceso que ejecutaba un programa (recordemos que los segmentos de código se comparten entre procesos). El núcleo añade los marcos de pila, datos, página de usuario y tabla de páginas del proceso al inicio de la lista de páginas libres, ya que pueden no ser útiles para otro proceso. Añade los marcos que contienen páginas de código al final de la lista de páginas libres, porque es posible que otro proceso ejecute el mismo programa. Si esto ocurriera, el núcleo recuperará las páginas de código de la lista de páginas libres (si aún están allí) en lugar de obtenerlas del archivo objeto o del dispositivo de intercambio. Los marcos de página se asignan únicamente desde el inicio de la lista de páginas libres para que las páginas que puedan volver a ser necesarias permanezcan asociadas a marcos de página el mayor tiempo posible. Los marcos de página que no están en la lista de páginas libres se encuentran en el bucle, una lista que contiene todos los marcos de página asignados, ordenados por dirección física. Sin embargo, los marcos que contienen código y datos del kernel no se encuentran ni en el bucle ni en la lista de marcos libres, ya que el kernel no está sujeto a paginación. 2.3.5.1 Reemplazo de páginas El paginador es un proceso del sistema cuya función es mantener la lista de marcos libres lo suficientemente grande como para garantizar un buen rendimiento. Se ejecuta cuando el marco está vacío. La lista de páginas libres cae por debajo de un umbral inferior y continúa hasta que vuelve a alcanzar un umbral superior. La política de reemplazo del paginador consiste en liberar, a nivel de sistema, los marcos de página que contienen páginas que no se han accedido recientemente. Para implementar esta política, el paginador mantiene un puntero de marco que recorre el bucle como la manecilla de un reloj que recorre los números de su esfera. Dado que esta "manecilla" apunta a un marco de página, el paginador examina el bit de acceso a la página. Si el bit está activado, el paginador simplemente lo desactiva y pasa al siguiente marco del bucle. Si el bit está desactivado (la página no se ha accedido recientemente), el paginador marca la página como inválida, añade el marco de página al final de la lista de páginas libres y pasa al siguiente. Si la página a la que no se ha accedido recientemente también está marcada como modificada, el paginador se encarga de actualizar la copia intercambiada de la página antes de añadir el marco a la lista de páginas libres. En resumen, el paginador recorre los marcos de página asignados, liberando aquellos a los que no se ha accedido desde su ciclo anterior. 2.3.5.2 Fallos de página La MMU detecta un intento de acceso a una página que el paginador ha marcado como inválida. Este intento se denomina fallo de página e invoca al gestor de fallos de página del kernel. Una página inválida puede estar en dos lugares; el gestor de fallos de página realiza una acción diferente según su ubicación. Si la página está en la lista de páginas libres, el gestor de fallos de página desvincula el marco de página asociado de la lista, lo añade al bucle, lo marca como válido y reanuda el proceso. Recuperar un marco de página de la lista de páginas libres de esta manera no bloquea el proceso que generó el fallo y es mucho más rápido que una lectura de disco. Si la página se ha paginado (el marco que ocupaba anteriormente se ha asignado a otra página), el gestor de fallos de página bloquea el proceso que causó el fallo y programa una lectura de disco para recuperar la página del dispositivo de intercambio. Posteriormente, una vez leída la página, el kernel asigna un marco del inicio de la lista de páginas libres, lo añade al bucle, actualiza la dirección del marco en la tabla de páginas del proceso, marca la página como válida y desbloquea el proceso. Las acciones combinadas del paginador y el gestor de fallos de página tienden a mantener las páginas de acceso frecuente asociadas a marcos de página, mientras que las páginas poco utilizadas tienden a migrar al dispositivo de intercambio. El paginador simplemente mueve los marcos de página que no se han accedido recientemente a la lista de páginas libres para que puedan reasignarse. Al mismo tiempo, los fallos de página que se producen en estas páginas anulan el trabajo del paginador. Si una página se accede con mucha frecuencia, el paginador nunca la verá marcada como no accesible y nunca la añadirá a la lista de páginas libres. Si una página se accede con una frecuencia moderada, puede añadirse a la lista de páginas libres, pero el gestor de fallos de página la recuperará rápidamente antes de que llegue al inicio de la lista. Solo las páginas menos utilizadas llegan al inicio de la lista de páginas libres y, por lo tanto, deben leerse del disco antes de usarse. La lista de páginas libres sirve, por lo tanto, como fuente de marcos de página disponibles y como caché de páginas descartadas recientemente que pueden recuperarse rápidamente. 2.3.6 Intercambio El núcleo no puede predecir los patrones de referencia de memoria de un grupo arbitrario de procesos. En consecuencia, la memoria física puede resultar sobreasignada; dicha sobreasignación se indica cuando el paginador no puede mantener la lista de páginas libres por encima de su mínimo, el umbral. Para evitar la posibilidad de sobrecarga de memoria (actividad de paginación excesiva), el núcleo inicia una medida más drástica que la paginación: intercambia procesos completos al disco. El intercambio de procesos libera marcos de página; además de sus segmentos, la tabla de páginas y la página de usuario de un proceso también pueden intercambiarse. Más importante aún, el intercambio reduce la contención a corto plazo por los marcos de página (y los ciclos de CPU). Con menos contención, algunos procesos residentes deberían completarse, lo que permite que la demanda de memoria física vuelva a un nivel que puede gestionarse eficazmente con la paginación. El intercambio es tarea de un proceso del núcleo llamado intercambiador. Este intenta seleccionar para el intercambio el proceso de usuario cuyo progreso se vea menos afectado por la pérdida de residencia en memoria. Un proceso que ha estado bloqueado durante mucho tiempo probablemente seguirá estándolo (a menudo está esperando la entrada del teclado); por lo tanto, el intercambiador selecciona el proceso que lleva más tiempo bloqueado. Si ningún proceso residente está bloqueado, se selecciona el proceso que lleva más tiempo residiendo en la memoria. Esto intenta proporcionar cierta equidad entre los procesos y es un ejemplo de un vestigio del tiempo compartido que no es ideal para una estación de trabajo, donde un usuario podría querer influir en la selección del intercambiador. Un proceso se intercambia cuando está listo y hay suficiente memoria disponible. Después de intercambiar un proceso, el kernel se asegura de que el proceso avance mínimamente antes de considerarlo para su intercambio nuevamente. 2.3.7 Asignación dinámica El kernel de UNIX deja la implementación de la gestión dinámica de memoria (por ejemplo, `mal loc` y `free` en la biblioteca C, y `new` y `dispose` en Pascal) a cargo del sistema. bibliotecas de em y sistemas de ejecución de lenguajes. El núcleo proporciona, sin embargo, los mecanismos para admitir dichas implementaciones; estos son las llamadas al sistema sbrk y brk. sbrk extiende el segmento de datos de un proceso (véase la Figura 2.3). El final del segmento de datos se denomina punto de ruptura, y la llamada al sistema sbrk establece el punto de ruptura en una nueva ubicación. Lo hace añadiendo el número de bytes solicitado, redondeado al límite de página, al segmento de datos y devolviendo un puntero a la base de la extensión. brk puede utilizarse para reducir el tamaño de las páginas que se devuelven al núcleo. 2.3.8 Extensión de pila En el sistema Berkeley UNIX, cada proceso tiene una profundidad máxima de pila, denominada límite de pila. Un proceso hereda su límite de pila de su proceso padre; el límite puede modificarse mediante un comando de C-Shell o una llamada al sistema. El núcleo crea inicialmente una pila de una página para un nuevo programa. Cuando, durante la ejecución, una referencia de datos cae fuera de los segmentos de un proceso (véase la Figura 2.3), pero dentro de los límites de su pila, el núcleo añade la página referenciada y las páginas intermedias al segmento de pila y reinicia la instrucción que falló. Si la referencia cae en otro lugar, el núcleo envía una señal SIGSEGV (violación de segmento) al proceso. A diferencia del segmento de datos, el segmento de pila no se puede reducir durante la ejecución. 2.4 Entrada/Salida En comparación con muchos sistemas operativos, la E/S del sistema UNIX es excepcionalmente simple y uniforme. Es simple porque solo hay unas pocas llamadas al sistema de E/S. Es uniforme porque las operaciones en dispositivos de E/S de distintos tipos son intercambiables (limitadas por las capacidades del dispositivo subyacente). Esto, a su vez, hace que un solo programa sea útil en diversas situaciones. 2.4.1 Flujos de bytes Las llamadas al sistema de E/S de UNIX operan con flujos de bytes. Un flujo de bytes es simplemente una secuencia de bytes. No posee ninguna otra estructura definida por el sistema: no contiene registros, bloques ni datos adicionales (por ejemplo, claves, campos de longitud o marcadores de fin de archivo). La representación de E/S mediante flujos de bytes minimiza los requisitos de almacenamiento y simplifica el sistema operativo (no existen métodos de acceso), pero su ventaja más significativa es la uniformidad. Por ejemplo, un programa que traduce un flujo a notación hexadecimal funciona con cualquier flujo, ya sea que provenga de un archivo, una terminal o una línea de comunicación. Como se mostrará más adelante en este informe, las utilidades de UNIX aprovechan la naturaleza basada en flujos de la E/S de UNIX para proporcionar un conjunto de herramientas muy flexible. Si bien el sistema adopta una visión no estructurada de los flujos, los programas pueden interpretarlos según una disciplina autoimpuesta si es necesario. Por ejemplo, los compiladores producen flujos binarios (módulos objeto) que son estructurados por el enlazador. Además, muchos programas, para mayor comodidad, utilizan o producen flujos de texto, que son programas comunes flujos de caracteres ASCII divididos en líneas por caracteres de salto de línea (OA hexadecimal). Pero esto establece estas convenciones cuando y si son necesarias; el núcleo no impone nada. Solo existen dos llamadas básicas al sistema de E/S: lectura y escritura (nótese que son las mismas llamadas introducidas anteriormente para las tuberías); transfieren un segmento de un flujo desde o hacia un área de datos (por ejemplo, una matriz o estructura) definida por un proceso. El origen del flujo (en el caso de una lectura) o su destino (en el caso de una escritura) es irrelevante para el proceso que realiza la llamada. El origen o destino del flujo de datos puede ser un archivo, un proceso en la misma máquina, un proceso en una máquina diferente, una terminal, una impresora, una línea de comunicación... cualquier cosa que pueda generar o recibir un flujo de bytes. 2.4.2 Descriptores Cada flujo de bytes accesible a un proceso se identifica mediante un descriptor; es decir, un descriptor es un identificador de un flujo de bytes. Los descriptores de un proceso se almacenan en su tabla de descriptores, un elemento de su entorno. Las entradas en la tabla de descriptores se indexan desde 0...” donde » es un parámetro de configuración, normalmente 20. Este valor de índice se utiliza para especificar el descriptor de destino en una llamada de lectura o escritura; es decir, leer el descriptor 4 obtiene bytes del flujo actualmente asociado a dicho descriptor. Dado que la tabla de descriptores forma parte del entorno de un proceso, un proceso hijo hereda el acceso a todos los flujos de bytes de su proceso padre. Sin embargo, como el entorno del hijo es una copia del del padre, el hijo puede modificar su tabla de descriptores sin afectar a su padre. Los nuevos descriptores se crean de forma diferente según el tipo de objeto asociado al flujo de bytes. Como se mencionó anteriormente en este capítulo, la llamada al sistema `pipe` crea descriptores para los extremos de lectura y escritura de una tubería. La llamada `open` crea un descriptor para un archivo o un dispositivo, y la llamada `socket` crea un descriptor para un socket; `open` se describe más adelante en este capítulo. En todos los casos, `open` devuelve el valor de índice del descriptor recién creado, que el proceso utiliza en las operaciones de lectura o escritura posteriores. llamadas. Una tabla de descriptores tiene una capacidad fija, lo que limita el número de flujos con los que un proceso puede trabajar simultáneamente. La llamada al sistema `close` notifica al sistema que no se realizarán más operaciones de entrada/salida en una secuencia y libera su espacio en la tabla de descriptores para su reutilización. Un proceso también puede manipular las entradas de la tabla de descriptores con la llamada al sistema `dup`, que crea una copia de un descriptor en el primer espacio disponible (es decir, guarda una copia de un descriptor). El descriptor original se puede cerrar y se crea uno nuevo en su posición original. Para restaurar el descriptor inicial, se puede duplicar desde su ubicación guardada, después de cerrar el nuevo descriptor. 2.4.3 Archivos En el sistema UNIX, un archivo ordinario es una secuencia de bytes que tiene un nombre y se almacena en el disco. El núcleo realiza un seguimiento del tamaño de cada archivo y asigna espacio para los archivos de forma automática e incremental a medida que crecen. El tamaño máximo de un archivo es de un volumen. La llamada `open` devuelve un descriptor para un archivo existente o para un archivo nuevo. `open` acepta tres parámetros: `name` (los nombres de archivo se explicarán más adelante), `flags` y `mode`. El parámetro `flags` indica al sistema cómo el proceso pretende usar el archivo. Los indicadores básicos son: `read-only`, `write-only` y `read-and-write` (actualización). Otros indicadores, que pueden especificarse además de los descritos, son: `create` (crear un archivo nuevo), `append` (comenzar a escribir al final de un archivo existente), `truncate` (establecer la longitud de un archivo existente a cero, sobrescribiendo su contenido) y `exclusive` (devolver un error si el archivo ya existe). El parámetro `mode` indica al sistema qué permisos asociar a un archivo recién creado. Los permisos son el mecanismo de protección de archivos del sistema UNIX y se explicarán más adelante. Además de lectura y escritura, los archivos responden a la llamada al sistema `seek`. Lseek realiza acceso aleatorio a cualquier archivo; en el sistema UNIX no existe el concepto de archivo “aleatorio” frente a archivo “secuencial”; cualquier archivo puede manipularse de ambas maneras. lseek funciona de la siguiente forma: Cada archivo tiene asociado un puntero de E/S implícito, mantenido por el núcleo, que indica en qué posición del flujo se leerán o escribirán los bytes a continuación. A medida que se ejecutan las llamadas de lectura o escritura, el sistema desplaza el puntero de E/S según el número de bytes transferidos. Para saltar a un byte arbitrario del archivo, basta con actualizar el puntero VO, y eso es precisamente lo que hace lseek. Además de un descriptor, la llamada recibe argumentos que especifican el desplazamiento del puntero y el origen desde el que debe moverse (el origen puede ser el principio o el final del archivo, o la ubicación actual). 2.4.4 Directorios Para que pueda encontrar un archivo cuando un proceso le proporciona un nombre de archivo, el sistema UNIX utiliza un puntero de E/S. Los directorios registran los nombres de los archivos. Un directorio es simplemente un archivo que contiene un conjunto de nombres de archivo y sus direcciones de disco asociadas. Para mantener la integridad del sistema de archivos, el núcleo impide que los usuarios escriban en los directorios; de lo contrario, un directorio sería indistinguible de un archivo ordinario. Esto significa que los programas pueden extraer información de los directorios (por ejemplo, los nombres de todos los archivos) simplemente leyéndolos, sin necesidad de configuraciones especiales. Cada usuario tiene un directorio personal llamado directorio de inicio de sesión o home; se pueden crear directorios subordinados al directorio de inicio de sesión de forma ilimitada. Dado que cada usuario tiene un directorio de inicio de sesión independiente, los nombres de archivo que un usuario crea (subordinados a su directorio de inicio de sesión) no entrarán en conflicto con archivos del mismo nombre pertenecientes a otros usuarios. Por convención, los usuarios suelen construir los nombres de archivo con letras minúsculas y números, aunque se permiten letras mayúsculas y la mayoría de los caracteres especiales. También por convención, un nombre de archivo suele terminar con una extensión (sufijo) que indica el contenido del archivo. Por ejemplo, .c (como en binsearch.c), .f y .p son las extensiones convencionales para los archivos fuente de C, FORTRAN 77 y Pascal, respectivamente. Estas convenciones las establecen los usuarios y los programas que utilizan el sistema de archivos, no el propio sistema. En muchos sistemas UNIX, la longitud de un nombre de archivo está limitada a 14 caracteres; Berkeley amplió este límite a 256. Los directorios se organizan jerárquicamente (véase la Figura 2.6 para un ejemplo). El directorio superior se denomina «raíz», en referencia a su posición en la estructura de árbol invertida. Un archivo en la jerarquía se identifica mediante una ruta absoluta o una ruta relativa. Una ruta absoluta va desde la raíz, a través de los directorios intermedios, hasta el archivo. Por ejemplo, /usr/terry/notes/apr22.txt es la ruta absoluta de un archivo en la Figura 2.6. La barra diagonal inicial del ejemplo representa la raíz. y le indica al sistema que se trata de una ruta absoluta; las barras diagonales subsiguientes separan los nombres de los directorios. En la práctica, los usuarios agrupan archivos relacionados en directorios y suelen trabajar durante largos periodos con los archivos de un mismo directorio (otro ejemplo del principio de «localidad de referencia»). En cualquier momento de su ejecución, un proceso tiene un directorio actual o de trabajo con respecto al cual se pueden nombrar los archivos. El sistema interpreta cualquier ruta de acceso que no comience con una barra diagonal se considera relativa al directorio actual. Si, por ejemplo, el directorio de trabajo actual de un proceso es /usr/terry, entonces notes/apr22.txt se refiere al archivo utilizado en el ejemplo anterior; de manera similar, si el directorio actual es /usr/terry/notes, entonces solo se requiere apr22.txt para especificar el mismo archivo. Existen varias llamadas al sistema para cambiar el directorio de trabajo y, en general, para "navegar" por el árbol del sistema de archivos. Aunque para el usuario parezca una sola entidad, un sistema de archivos UNIX generalmente consta de varios sistemas de archivos. En otras palabras, el árbol de archivos en realidad consta de varios subárboles. Cada sistema de archivos tiene una estructura jerárquica similar, vinculada entre sí para formar el sistema de archivos individual. Sistemas de archivos Un sistema de archivos, ubicado en el dispositivo raíz (un dispositivo predefinido para el sistema al inicializarse), se designa para anclar el sistema de archivos general. Los sistemas de archivos subordinados pueden residir en el mismo disco o en discos diferentes; sin embargo, cada sistema de archivos debe residir en un disco único. La llamada al sistema de montaje privilegiado adjunta un sistema de archivos a un directorio de otro. (Normalmente, el directorio receptor está vacío. Si contiene archivos o directorios, estos serán temporalmente inaccesibles mientras el sistema de archivos esté montado en el directorio). La llamada de desmontaje (umount) realiza la operación inversa, desvinculando lógicamente un sistema de archivos de la jerarquía. De esta manera, varios sistemas de archivos independientes, ubicados en una o más unidades de disco, pueden integrarse en una única jerarquía de archivos. Esta técnica admite de forma natural paquetes de discos extraíbles, unidades que deben desconectarse, unidades nuevas, y demás. Además, Sun ha extendido este enfoque para permitir que los sistemas de archivos se compartan a través de la red; es decir, una estación de trabajo puede compartir un sistema de archivos montado físicamente en otra estación de trabajo, montándolo en su sistema de archivos local. El Sistema de Archivos de Red de Sun se describe brevemente al final de esta sección. 2.4.5 Permisos Cada archivo UNIX tiene asociados tres conjuntos de tres bits de permisos (véase la Figura 2.7). Estos tres bits definen si el archivo puede leerse, escribirse o ejecutarse (cualquier combinación es válida). Cada conjunto de permisos otorga derechos de acceso a una clase de usuario diferente. Las tres clases son el propietario del archivo (inicialmente su creador), los usuarios del grupo del propietario y el público general. Una extensión de Berkeley permite que un usuario pertenezca a varios grupos. Los grupos de un usuario se asignan al crear su cuenta en el sistema. Un usuario puede configurar los permisos de un archivo que contiene un programa para que pueda leerlo, escribirlo y ejecutarlo; los miembros de su grupo pueden tener permiso para ejecutarlo, mientras que otros usuarios pueden no tener acceso alguno. Los directorios tienen los mismos nueve permisos, interpretados de forma diferente: bits, pero son: El permiso de "Lectura" permite leer el directorio como un archivo, por ejemplo, para listar los nombres de los archivos que contiene. El permiso de "Escritura" permite añadir y eliminar entradas de directorio, es decir, permite añadir o eliminar archivos de un directorio. El permiso de "Ejecución" significa "recorrer": un programa puede usar el nombre del directorio en una ruta, pero no puede leer su contenido directamente. Para evitar su contaminación o eliminación, los archivos del sistema pertenecen al superusuario. Uno podría preguntarse por qué el concepto de superusuario es útil para una estación de trabajo dedicada a un solo usuario que puede iniciar sesión como root en cualquier momento. De hecho, proporciona cierta protección contra los errores por descuido del usuario, así como contra los errores cometidos por compañeros que comparten la estación. Todos cometemos errores, pero un error cometido al iniciar sesión como root puede ser catastrófico. Por ejemplo, el comando rm -r * elimina recursivamente todos los archivos del directorio actual y de todos los subdirectorios. Es, por lo tanto, una forma rápida de podar grandes ramas del árbol de directorios. Sin embargo, ejecutarlo en el directorio incorrecto puede borrar archivos equivocados, posiblemente en gran cantidad. Dos medidas de seguridad limitan en cierta medida el daño cuando el comando se ejecuta sin privilegios. En primer lugar, el sistema solicitará confirmación cuando encuentre un archivo con solo permiso de lectura (por esta razón, los usuarios suelen marcar sus archivos importantes como de solo lectura y editar copias de los mismos). En segundo lugar, el sistema no eliminará un archivo que pertenezca a otro usuario, por ejemplo, los archivos del sistema que pertenecen al usuario root. Cuando root ejecuta el comando rm -r * no existen medidas de seguridad; todos los archivos subordinados simplemente desaparecen sin previo aviso. Por lo tanto, un usuario que haya iniciado sesión sin privilegios puede perjudicarse a sí mismo, pero solo parcialmente; para causar un daño mayor, se requieren privilegios de root. Cada archivo tiene un décimo bit llamado set-uid que añade un importante elemento de flexibilidad al sistema de protección de archivos. A menudo hay archivos que grupos amplios de usuarios deberían poder actualizar de forma controlada. Consideremos el siguiente ejemplo: del sistema /etc/passwd (archivo de contraseñas), por ejemplo. Cualquier usuario debería poder cambiar su propia contraseña, pero permitir el acceso de escritura sin restricciones al archivo constituiría una grave violación de seguridad. Un programa en ejecución cuyo bit set-uid esté activado puede cambiar su ID de usuario del usuario que ejecuta el programa al usuario propietario del programa. Entonces adquiere los permisos del propietario con respecto a sus archivos. Solo el propietario de un archivo puede cambiar el bit set-uid del archivo. Así es como el bit set-uid permite que el archivo de contraseñas se cambie de forma protegida. El archivo de contraseñas pertenece a root (el superusuario); sus permisos permiten que cualquiera lea el archivo (las contraseñas se almacenan encriptadas), pero solo root tiene permiso para escribir en él. Existe un programa llamado passwd, también propiedad de root, que cualquier usuario puede ejecutar; El programa passwd modifica una contraseña en /ete/passwd. El programa passwd se almacena como un archivo cuyo bit set-uid está activado. Cuando un usuario ejecuta passwd, el programa realiza una llamada al sistema set-uid solicitando (como argumento) que su ID de usuario cambie a root. Al ejecutar la llamada, el kernel verifica que el programa pertenece a root y que su bit set-uid está activado; en respuesta, cambia el ID de usuario efectivo del proceso que ejecuta passwd al del propietario de passwd, es decir, a root. Esto permite que el programa passwd escriba en el archivo de contraseñas. Cabe destacar que passwd se ejecuta en su propio proceso y solo el ID de usuario efectivo de ese proceso se ve afectado por el bit set-uid. Asimismo, es importante tener en cuenta que un programa como passwd, que cambia su ID de usuario efectivo a root, debe ser un programa de confianza, ya que se ejecuta con privilegios y, por lo tanto, su comportamiento está sujeto a una verificación. La función `set-uid` no está restringida al usuario root; cualquier usuario puede restringir el acceso a sus archivos a sus propios programas, pero permitir que cualquiera (aunque no necesariamente todos) ejecute dichos programas. Es importante tener en cuenta que un programa de este tipo debe ser de confianza, ya que puede acceder a los archivos del propietario. Un programa que puede establecer su ID de usuario efectivo como root puede realizar otras acciones además de actualizar archivos propiedad de root; puede ejecutar llamadas al sistema con privilegios, como `setpriority`. Estrechamente relacionada con `set-uid` se encuentra `set-gid`, que cambia el ID de usuario efectivo no al propietario del programa, sino a su grupo. Por lo tanto, `set-gid` flexibiliza el control sobre el acceso a archivos para programas que pertenecen a cualquier miembro del grupo del propietario. 2.4.6 Compartición de archivos En el sistema UNIX, es perfectamente posible que dos procesos abran el mismo archivo; de hecho, como se mencionó anteriormente, un proceso hijo hereda los archivos abiertos de su proceso padre. Además, cualquier proceso que conozca el nombre de un archivo y tenga los permisos adecuados puede abrirlo. Nota: Los procesos que comparten archivos por herencia también comparten punteros de E/S; por lo tanto, una lectura realizada por cualquiera de ellos modificará la posición del otro en el archivo. Normalmente, esto no causa problemas, ya que el proceso padre desea que el proceso hijo se haga cargo de los archivos mientras se ejecuta. Cuando el proceso hijo finaliza, el proceso padre reanuda el procesamiento de los archivos. Los procesos que comparten archivos mediante aperturas independientes tienen punteros de E/S independientes. En ambos casos, los búferes se comparten en todo el sistema, por lo que un cambio realizado por un proceso será visible para los demás que comparten el archivo. Si varios procesos desean actualizar un archivo de forma consistente, deben sincronizar sus accesos para que las actualizaciones se apliquen una a una. La forma clásica de hacerlo, y la única en muchas implementaciones de UNIX, es que los procesos que realizan la actualización intenten crear un archivo de bloqueo antes de modificar el archivo compartido. Para ello, cada proceso podría ejecutar el código ilustrado en el pseudocódigo de la Figura 2.8. Fig. 2.8 Uso de un archivo de bloqueo (* Intentar crear el archivo de bloqueo *) exclusive := open("lockfile", write_only, create_exclusive); (* ¿Ya existía? es decir, si exclusive < 0 entonces (* ...retrasar e intentar de nuevo... alguien más está actualizando *) fin si; abrir ¿falló?*) un archivo*) (* Hemos creado el archivo de bloqueo, dándonos acceso exclusivo al archivo compartido *) (* (k ...actualizar eliminar para que el alguien compartido bloquee el archivo... *) el archivo pueda cerrar (exclusivo) ; desvincular ("archivo de bloqueo") ; crear (* desvincular *) eliminar Además de ser engorroso, el enfoque del archivo de bloqueo presenta algunas dificultades: • Es lento. • Un fallo del sistema deja archivos de bloqueo residuales; estos deben eliminarse manualmente. • No funciona para procesos privilegiados, que siempre pueden crear archivos. El sistema Sun UNIX ofrece una mejor solución, utilizando un proceso de servidor. Dos Existen enfoques generales. * En el primer enfoque, se designa un proceso de servidor para actualizar el archivo, mientras que otros procesos cliente le envían solicitudes de actualización a través de un socket. La alternativa es un servidor que serializa el acceso al archivo, pero permite que un proceso al que se le ha otorgado acceso realice sus propias actualizaciones. 2.4.7 Implementación del sistema de archivos El sistema de archivos UNIX oculta completamente las características físicas del sistema subyacente. El sistema de archivos puede almacenar o recuperar desde un solo byte o 10 823 bytes. Con tal generalidad, cabe preguntarse sobre su rendimiento. De hecho, la velocidad del sistema de archivos es probablemente el factor crucial que afecta al rendimiento general de una implementación UNIX. Todos los sistemas UNIX utilizan un esquema de almacenamiento en caché para minimizar los accesos al disco y superponer las operaciones de entrada/salida con el procesamiento; tanto los archivos como los directorios se almacenan en caché. Un conjunto de búferes se comparte globalmente entre todos los sistemas de archivos y todos los procesos que los utilizan. Cada búfer tiene el tamaño de un bloque del sistema de archivos. En respuesta a una solicitud de lectura, el sistema busca un búfer que ya contenga el bloque; si existe, el núcleo simplemente mueve los datos al área de datos del proceso y regresa, sin haber realizado ninguna operación de entrada/salida física. Si ningún búfer contiene el bloque buscado, el sistema inicia una lectura física en el búfer menos usado. Posteriormente, cuando el controlador de disco haya llenado el búfer, el sistema copia los datos del mismo. El núcleo reconoce el acceso secuencial a un archivo y prelee los bloques para mantener los búferes por delante de las solicitudes de los procesos. Cuando el sistema necesita un búfer para almacenar un nuevo bloque y este es el menos usado del grupo, si un bloque en el que se van a escribir datos no está presente en un búfer, se lee como se describe para una llamada de lectura y luego se actualiza como se describe aquí. La principal ventaja de esta técnica de almacenamiento en caché es que los bloques a los que se accede con frecuencia (por ejemplo, los directorios actuales) tienden a permanecer en memoria, lo que permite que las llamadas de lectura y escritura se completen rápidamente sin E/S física. Por otro lado, la práctica del sistema UNIX de retrasar las escrituras físicas hasta que se necesite un búfer implica que, en caso de un fallo del sistema, los datos que un proceso creía escritos podrían haber quedado en un búfer. (Para reducir la magnitud potencial de este problema, el núcleo ejecuta la llamada al sistema syne cada 30 segundos. Esta llamada escribe todos los búferes modificados, sincronizando el estado del disco con el del grupo de búferes). Los procesos que deben saber que un escritor ha actualizado realmente el disco pueden ejecutar la llamada fsyne, que funciona como syne para un solo archivo. Como se mencionó, todos los sistemas UNIX © utilizan esta técnica de almacenamiento en caché. Sin embargo, gran parte de la implementación habitual del sistema de archivos es demasiado simple para lograr un alto rendimiento. Por ejemplo, después de que el sistema lleva un tiempo funcionando, los nuevos bloques de disco se asignan en ubicaciones prácticamente aleatorias, lo que requiere búsquedas prolongadas incluso para archivos a los que se accede secuencialmente. Una de las principales contribuciones de 4.2BSD es el rediseño de la arquitectura interna del sistema de archivos, manteniendo la interfaz familiar. El resultado es un rendimiento varias veces superior. A continuación, se presentan brevemente y de forma muy simplificada las modificaciones de 4.2BSD: Los bloques del sistema de archivos son de 4 KB y un bloque se puede dividir en fragmentos asignados por separado cuyo tamaño es un múltiplo de Un archivo ocupa cero o más bloques de 4 KB más un fragmento, si es necesario, de 1 KB, 2 KB o 3 KB. (Por ejemplo, un archivo de 11 000 bytes ocupa dos bloques completos más un tercer fragmento de bloque de 3 KB; el fragmento de 1 KB restante en el tercer bloque está disponible para su asignación). El mayor tamaño de bloque transfiere más datos por transacción de disco y reduce la cantidad de archivos sujetos a penalizaciones por acceso indirecto. Al mismo tiempo, el fragmento más pequeño mantiene la utilización del disco aproximadamente a la par con la implementación tradicional de archivos (los archivos pequeños tienden a predominar en un sistema UNIX). El sistema conoce y aprovecha las características del hardware: velocidad del procesador, velocidad de rotación del disco, etc. En la medida de lo posible, los bloques de archivos se asignan en el mismo cilindro en una posición óptima para el acceso secuencial, teniendo en cuenta las características de rotación de la unidad. En la medida de lo posible, los bloques de archivos se asignan en el mismo cilindro en una posición óptima para el acceso secuencial, considerando las características de rotación de la unidad. Los archivos en el mismo directorio se ubican cerca unos de otros para acelerar las operaciones que afectan a todos los archivos de un directorio. El sistema de archivos 4.2BSD también es más robusto que su predecesor. La información crítica en cualquier sistema de archivos UNIX se denomina superbloque. Si la información de este bloque se vuelve ilegible, el sistema de archivos no se puede reparar después de un fallo del sistema o un apagado incorrecto del sistema. 4.2BSD replica el superbloque de tal manera que, aunque se pierda cualquier pista, cilindro o plato, el superbloque seguirá estando disponible. Además, el disco se actualiza en etapas bien definidas para que un programa de recuperación posterior a un fallo (llamado fsck, por comprobación del sistema de archivos) pueda completar las actualizaciones incompletas o revertirlas correctamente, manteniendo así el sistema de archivos internamente consistente. 2.4.8 Dispositivos Los dispositivos se integran de forma natural en el sencillo modelo UNIX de E/S de flujo; Después de todo, casi cualquier dispositivo puede suministrar o recibir un flujo de bytes (o ambos). Por ejemplo: • Una impresora es un flujo de solo escritura. • Un teclado de terminal es un flujo de solo lectura y su pantalla es un flujo de solo escritura. • Una unidad de cinta es de solo lectura, con Flujo de solo escritura o de solo lectura (o un flujo de lectura/escritura, si se está extendiendo un archivo de cinta). Para facilitar su manejo, los dispositivos están integrados en el sistema de archivos, donde se conocen como archivos especiales. Cada sistema UNIX tiene un directorio llamado /dev que contiene una entrada para cada dispositivo del sistema. El nombre de un dispositivo es idéntico al de un archivo y, por lo tanto, puede pasarse como parámetro en cualquier lugar donde se permita un nombre de archivo. Un pseudodispositivo particularmente útil es /dev/null, que corresponde al "cubo de bits" presente en muchos sistemas operativos. Leer desde /dev/null produce una indicación inmediata de fin de flujo, mientras que los bytes escritos en este dispositivo simplemente desaparecen. Para controlar el acceso, los bits de permisos de archivo también se aplican a los dispositivos. En general, los dispositivos pertenecen al usuario root, y el acceso a los dispositivos sensibles (por ejemplo, y el permiso para escribir en discos) está limitado a root. Cuando un usuario inicia sesión en una estación de trabajo o terminal, el sistema le otorga la propiedad de la terminal o estación de trabajo hasta que cierre la sesión. Por defecto, todas las terminales del sistema son públicas, lo que permite enviar mensajes entre usuarios mediante el comando de escritura. Sin embargo, estos mensajes pueden resultar intrusivos, especialmente al editar un archivo. Para evitar estas intrusiones, el usuario solo tiene que modificar los permisos del terminal para que no se pueda escribir en él para los miembros del grupo ni para el público en general. Los dispositivos no son completamente idénticos a los archivos, por supuesto, ya que muchos de ellos no pueden imitar completamente la unidad de disco que subyace a un archivo. Por ejemplo, la operación `seek` está diseñada para archivos y dispositivos de disco, no para terminales. En un sentido ligeramente distinto, la lectura en un teclado normalmente devuelve el resultado al final de una línea, por lo que un programa a menudo recibirá menos caracteres de los que solicitó (claro está, un programa bien escrito debería esperar la misma "lectura corta" al final de un archivo, por lo que la compatibilidad entre la entrada de archivos y la de teclado realmente no es un problema). A pesar de estas pequeñas e inevitables diferencias, la identidad fundamental de archivos y dispositivos (y tuberías y sockets) es extremadamente útil en la práctica. Por ejemplo, el La utilidad `sort` de UNIX ordena las líneas de un flujo de texto y escribe la salida en otro flujo. No importa si la entrada proviene de una línea de comunicación, el teclado o un archivo, y la salida puede ser un archivo, un dispositivo o un proceso (a través de una tubería). Sin embargo, esta versatilidad se logra sin escribir una sola línea de código en el programa dedicado a las particularidades de los dispositivos (actuales o futuros). Algunos dispositivos son capaces de realizar más operaciones que leer y escribir; los terminales inteligentes y las unidades de cinta son los ejemplos más comunes. Por lo tanto, cada dispositivo puede admitir varias operaciones específicas mediante la llamada al sistema `ioctl`; un parámetro identifica el comando de control que se ejecutará. También se puede pasar un bloque con datos relacionados con el dispositivo al controlador en una llamada `ioctl`. Los programas que utilizan llamadas `ioctl`, por supuesto, dependen del dispositivo, ya que los comandos `ioctl` disponibles para un dispositivo no necesariamente están disponibles para otro. Otro. 2.4.9 Base de datos de terminales La mayoría de los usuarios interactúan con el sistema Sun UNIX a través de una estación de trabajo Sun. Sin embargo, se pueden conectar terminales de caracteres a los puertos serie de la estación de trabajo para que varios usuarios la compartan. Con la adición de tarjetas multiplexoras, se puede conectar un número moderado de terminales a un sistema Sun. Estos terminales suelen tener funciones como subrayado, parpadeo, actualización de parte de la pantalla, etc. Desafortunadamente, no existen secuencias de escape universalmente aceptadas para invocar estas operaciones, a pesar de que en muchos casos son idénticas. El archivo termcap de Berkeley aporta cierto orden a este caos al definir un conjunto de operaciones genéricas para terminales y las secuencias de código que las implementan en numerosos terminales. Dado que termcap es un archivo de texto común, se pueden añadir nuevas descripciones simplemente editándolo. termcap ha sido adoptado por muchas otras implementaciones de UNIX. 2.4.10 E/S estándar y redirección El sistema UNIX define los flujos asociados. con los descriptores 0, 1 y 2 como entrada estándar, salida estándar y error estándar de un proceso, respectivamente. Salvo que haya una buena razón para no hacerlo, los programas leen su entrada de la entrada estándar, escriben su salida en la salida estándar y escriben los diagnósticos en el error estándar. Dado que un proceso hereda los descriptores de su proceso padre, sus canales de V/O básicos ya están configurados; solo necesita establecer descriptores para los archivos y dispositivos auxiliares que necesite. Además de ahorrar un poco de esfuerzo, el uso de los descriptores estándar proporciona a un programa una gran medida de universalidad automáticamente. Dado que los archivos, dispositivos, tuberías y sockets funcionan esencialmente igual, un programa funcionará con sus descriptores estándar conectados a cualquiera de ellos. Por lo tanto, por poner un ejemplo sencillo, un programa que cuenta palabras en un flujo de texto, escribiendo el total en la salida estándar, funciona cuando la entrada se toma de una terminal, de un archivo, de otro proceso, y lo mismo ocurre con la salida. Los programas que siguen esta sencilla convención suelen funcionar juntos sin ningún esfuerzo de diseño adicional por parte de sus programadores. Por defecto, la entrada estándar se asocia al teclado del usuario que finalmente invocó el proceso; la salida estándar y el error estándar se asocian a la pantalla del usuario. Por regla general, el error estándar mantiene su asociación con la pantalla, ya que los mensajes de error que desaparecen por una tubería, por ejemplo, no son muy útiles. Sin embargo, a menudo, un nuevo proceso hijo redirige su entrada estándar y/o salida estándar antes de ejecutar un programa. (De esta forma, el programa ejecutado no necesita preocuparse por de dónde proviene su entrada ni adónde va su salida). El ejemplo más común de esto es el intérprete de comandos, que proporciona a los usuarios una notación muy práctica para solicitar que la entrada estándar, la salida y el error de un programa se redirijan antes de su ejecución; cómo se hace esto se describe en el siguiente capítulo. 2.4.11 E/S no bloqueante La interfaz de E/S estándar de UNIX es síncrona con respecto al proceso que la llama; un proceso que realiza la llamada se bloquea hasta que su solicitud pueda ser satisfecha. A menudo, en el caso de los archivos, la caché de búfer hace que esto sea casi instantáneo. Sin embargo, existen situaciones en las que un proceso no desea bloquearse debido a una operación de E/S que no puede completarse. Si tuviera que leer una línea en la que no hay datos disponibles, se bloquearía inmediatamente. Supongamos, por ejemplo, que un proceso está monitorizando varias líneas de comunicación y desea leer datos de cualquiera de ellas. El proceso se bloquearía hasta que llegaran datos a esa línea, incluso si pudiera haber datos en una o más de las otras líneas. Para permitir la implementación sencilla de este tipo de procesos, Berkeley introdujo el concepto de E/S no bloqueante. Para utilizar esta función, un proceso emite la instrucción fent1 VO sobre el descriptor. (La llamada al sistema fent1, antes de que realice cualquier operación, significa "control de archivos"; una descripción más precisa sería "opciones de control del descriptor"). Uno de los parámetros que se le pasan a fentl le indica que marque el descriptor como no bloqueante. Leer de un descriptor no bloqueante produce un código de error identificativo si la lectura no se puede completar de inmediato porque no hay datos disponibles. Escribir en un descriptor no bloqueante transfiere tantos bytes como sea posible sin bloquearse; como es habitual en UNIX, la llamada devuelve el número de bytes escritos. Si no se pueden escribir bytes, una escritura en un descriptor no bloqueante también devuelve un código de error identificativo. Por lo tanto, mientras que la E/S se considera incondicional, la E/S no bloqueante es condicional: una solicitud en un descriptor no bloqueante significa que es posible; de ​​lo contrario, devuelve un error. “Hágalo ahora si es posible Un proceso que utiliza E/S no bloqueante puede consultar repetidamente (es decir, intentar leer o escribir) el o los descriptores de interés. Sin embargo, dicha consulta puede consumir una cantidad considerable de tiempo de CPU en algunos casos. Alternativamente, el proceso puede solicitar que se le notifique de forma asíncrona cuando un descriptor no bloqueante esté listo para E/S. Esto también se realiza con la llamada al sistema fent1. Al invocarla, el manejador de señales del proceso puede emitir la llamada select, descrita a continuación, para determinar qué descriptor está listo. La llamada select de Berkeley proporciona un método alternativo para determinar cuándo es posible la E/S en un descriptor; se puede utilizar tanto con descriptores bloqueantes como no bloqueantes. Además, select cuenta con una función de multiplexación que permite determinar cuándo es posible la E/S en cualquiera de varios descriptores. select acepta argumentos de cadena de bits que especifican los descriptores de interés; Devuelve el número total de descriptores listos para E/S. La función `select` también devuelve cadenas de bits que especifican qué descriptores están listos para leer y cuáles para escribir. Se puede configurar `select` para que devuelva un resultado inmediatamente o para que bloquee el proceso hasta que al menos un descriptor esté listo. Se puede especificar un argumento de tiempo de espera para evitar que el proceso se bloquee indefinidamente o para permitirle realizar otras tareas periódicamente. 2.4.12 Descripción general del sistema de archivos de red La mayoría de las funciones de E/S descritas en las secciones anteriores están disponibles en cualquier implementación de sistema UNIX, y todas ellas se proporcionan en las implementaciones de 4.2BSD. 4.2BSD también proporciona funciones de E/S de red de bajo nivel, como sockets, que están fuera del alcance de este informe. A un nivel superior, existen funciones en 4.2BSD para iniciar sesión en una máquina remota, copiar archivos entre máquinas y ejecutar comandos individuales en máquinas remotas. A estas capacidades básicas de red de 4.2BSD, Sun ha añadido un protocolo de disco de red (nd) que permite a una estación de trabajo sin disco utilizar (parte de) el disco de otra máquina en la red. De esta forma, los nodos sin disco pueden arrancar, paginar e intercambiar memoria a través de la red, eliminando el coste, el ruido y los requisitos de espacio de un disco local. El protocolo nd y la red son lo suficientemente rápidos como para que varias estaciones de trabajo sin disco que comparten un disco de red de alto rendimiento ofrezcan un rendimiento similar al de las máquinas con discos locales. Donde nd realiza una operación de intercambio de memoria, Aunque un disco de red parezca estar montado localmente, el Sistema de Archivos de Red (NFS) de Sun hace que cualquier número de sistemas de archivos remotos parezcan estar montados localmente. En otras palabras, NFS permite acceder a los archivos con las llamadas de E/S estándar de UNIX, independientemente de su ubicación. Los usuarios y los programas ven la jerarquía habitual de directorios y archivos; los manipulan de la forma habitual, sujetos a las comprobaciones de permisos normales. El hecho de que partes de su sistema de archivos puedan residir en varias máquinas de la red es invisible y no supone ningún problema. Tan importante como sus capacidades funcionales es la conformidad de NFS con la Arquitectura de Servicios de Red de Sun. Esta arquitectura está diseñada para admitir el funcionamiento cooperativo de máquinas y sistemas operativos heterogéneos en una red común. Niveles Por ejemplo, otros proveedores pueden implementar NFS, lo que permite que los archivos se compartan de forma transparente entre ordenadores Sun, mainframes, superordenadores, servidores de bases de datos, ordenadores personales, y demás. Estas máquinas no necesitan basarse en ningún procesador en particular, ni ejecutar el sistema operativo UNIX. Además del uso compartido de archivos, Sun y otros fabricantes pueden proporcionar otros servicios de red; estos servicios pueden ser suministrados y utilizados por cualquier máquina de la red, independientemente de su arquitectura y su sistema operativo. Para fomentar el desarrollo y la difusión de los servicios de red, incluido NFS, a una amplia gama de máquinas, Sun ha puesto los componentes básicos de los servicios de red en el dominio público. En concreto, para el servicio NFS, Sun está publicando una especificación de su protocolo. 2.4.12.1 Servidores y clientes En el modelo de servicios de red, los ordenadores de la red se denominan nodos; cada nodo tiene un nombre diferente. Un nodo determinado puede actuar como servidor, como cliente o como ambos. Un servidor proporciona un servicio a procesos o usuarios en uno o más clientes. Uno de estos servicios es el acceso remoto al sistema de archivos, el servicio que proporciona NFS. Un servidor de archivos NFS típico es una máquina dedicada con discos de alto rendimiento, pero, como se mencionó, cualquier nodo puede ser un servidor, y un nodo puede ser tanto cliente como servidor; es decir, puede proporcionar y utilizar servicios de red. Un nodo se convierte en cliente o servidor simplemente mediante la emisión de las llamadas o comandos al sistema apropiados; no se requiere registro formal, ni existe una relación fija entre clientes y servidores. Agregar un nuevo nodo cliente a la red es sencillo; para usar los servicios de red existentes, solo necesita conocer los nombres de los servidores y la interfaz que han definido para sus servicios. NFS proporciona un conjunto de operaciones que permiten a los servidores exportar sistemas de archivos a la red y a los clientes importarlos. Al igual que otras operaciones que manipulan sistemas de archivos, estas son operaciones privilegiadas. Generalmente se ejecutan como comandos desde /ete/re.local (este archivo contiene comandos que se ejecutan automáticamente al iniciar el sistema). Sin embargo, un usuario o programa con privilegios de root puede ejecutarlas en cualquier momento. 2.4.12.2 Funciones del servidor Un servidor NFS inicia el servicio mediante el comando nfsd. nfsd inicia un número específico de demonios (por defecto, 4) en el servidor. Estos demonios gestionan las solicitudes de servicio entrantes. Para que un sistema de archivos esté disponible para los clientes, el servidor coloca el nombre del sistema de archivos en un archivo llamado /ete/exports. Los datos opcionales en /ete/exports permiten al servidor limitar los nodos que pueden importar un sistema de archivos. Para retirar un sistema de archivos de la red, el servidor simplemente elimina la entrada del sistema de archivos de /etc/exports. Cualquier referencia posterior del cliente a un sistema de archivos «embargado» se devuelve al cliente como si el servidor nunca hubiera exportado el archivo. 2.4.12.3 Funciones del cliente Para acceder a un sistema de archivos exportado, un cliente simplemente monta el sistema de archivos como si estuviera grabado en un disco local. Sun ha añadido parámetros a la función `mount` que indican a NFS dónde encontrar el sistema de archivos en la red. Un cliente puede eliminar un sistema de archivos de red de su árbol de sistemas de archivos mediante una llamada `umount` de la forma habitual. La figura 2.9 muestra algunas interacciones típicas, aunque simplificadas, de tres nodos de red: un servidor, un cliente y un tercero que actúa como servidor y cliente. Tras haber creado un árbol de sistemas de archivos a partir de una combinación de sistemas de archivos de red y locales, un cliente puede realizar llamadas de E/S UNIX normales sobre los archivos. La ubicación física de un archivo en el árbol es esencialmente transparente; no solo funcionan con normalidad las operaciones de archivo habituales, como leer, escribir y buscar, sino también la comprobación de permisos, etc. Como resultado, prácticamente todas las aplicaciones UNIX existentes funcionan correctamente con archivos de red. Sin embargo, NFS no emula completamente el sistema de archivos UNIX. Por ejemplo, los dispositivos remotos (como módems y unidades de cinta) no se pueden acceder a través de NFS como archivos especiales UNIX.* Los archivos especiales remotos, y algunos otros detalles del sistema de archivos UNIX, se han sacrificado para dotar a NFS de un protocolo sin estado, como se explica a continuación. (Los dispositivos son inherentes) (con estado). El resultado es un sistema que proporciona las operaciones más importantes del sistema de archivos con un nivel de fiabilidad superior al que sería posible con una emulación completa. 2.4.12.4 Implementación de NFS El Sistema de Archivos de Red (NFS) se ha diseñado cuidadosamente para proporcionar un buen rendimiento, minimizar el impacto de los fallos de los nodos y cumplir con el principio de sistemas abiertos. Para eliminar la sobrecarga del cambio de tareas, Sun ha integrado NFS en su núcleo de sistema de procesamiento múltiple UNIX. Solicitudes Los servidores son multihilo y procesan concurrentemente. En los casos comunes. Accesos directos Tanto en el código como en el desvío, los servidores y los clientes realizan lecturas asíncronas anticipadas y escriben en segundo plano. NFS permite a los usuarios realizar concesiones entre rendimiento y espacio para obtener el rendimiento que necesitan en sus entornos. Por ejemplo, los sistemas de archivos compartidos pueden distribuirse entre varios servidores para obtener el efecto del procesamiento múltiple. Los archivos de solo lectura a los que se accede con frecuencia pueden duplicarse en varios servidores, de nuevo para distribuir la carga de procesamiento de archivos entre varias máquinas. Finalmente, los clientes con discos locales pueden elegir a qué archivos acceder a través de la red y cuáles almacenar localmente. Los fallos ocasionales en los nodos de red son inevitables, y NFS ha sido diseñado para funcionar de forma robusta ante tales fallos. La clave de su robustez reside en un protocolo sin estado entre el cliente y el servidor de NFS. Básicamente, esto significa que un servidor puede tratar cada solicitud de un cliente como si fuera nueva; no necesita recordar nada de las transacciones anteriores del cliente. Como resultado, el fallo de un cliente no afecta al servidor de ninguna manera; no es necesario notificar al servidor del fallo, ya que no ha conservado información de estado que deba limpiarse. Si un servidor falla, los clientes solo tienen que volver a intentar sus solicitudes; cuando el servidor vuelva a estar operativo, se procesarán con normalidad. Como se mencionó, NFS es un ejemplo de servicio de red de Sun. La idea de apertura es inherente al concepto de servicios de red; es decir, la red y los servicios disponibles en ella están abiertos a nodos que ejecutan máquinas y sistemas operativos que no son de Sun. Dichos nodos pueden conectarse a la red y funcionar como clientes y servidores, al igual que las estaciones de trabajo de Sun. Para cada servicio que deseen utilizar o proporcionar, el nodo debe implementar la parte correspondiente del protocolo cliente/servidor. Para ayudar a otros fabricantes a que sus máquinas funcionen como clientes o servidores NFS, Sun está poniendo el protocolo NFS a disposición del público. A medida que los servicios de red estándar, como NFS, se generalicen, las redes se volverán cada vez más heterogéneas; los usuarios podrán configurar redes con nodos específicos según sus propias necesidades y preferencias. NFS y todos los servicios de red se basan en la Llamada a Procedimiento Remoto (RPC) y la Representación de Datos Externos (XDR), desarrolladas por Sun. RPC proporciona una forma estándar para que los programas que se ejecutan en diferentes sistemas operativos se llamen entre sí, pasando argumentos y devolviendo resultados. Los programas que utilizan RPC están protegidos de las convenciones de llamada de los diferentes sistemas operativos; un proceso cliente llama a RPC como lo haría con cualquier procedimiento local; se bloquea hasta que la llamada finaliza. Mientras tanto, RPC en el cliente traduce la llamada a un formato estándar y la envía a través de la red al servidor correspondiente. RPC que se ejecuta en el servidor recibe los datos de la llamada, los traduce del formato estándar a un formato que tenga sentido en el entorno del servidor e invoca el procedimiento llamado, pasándole parámetros. Los resultados se devuelven repitiendo los mismos pasos en sentido inverso. XDR es una forma estándar de representar datos que se pasan entre diferentes máquinas. Los programas utilizan XDR para protegerse de el orden de bytes y el empaquetado de estructuras de datos que varían de una máquina y un compilador a otro. Dado que XDR y RPC proporcionan la base para la construcción de todos los servicios de red, Sun ha puesto su código a disposición del público. 2.5 Temporizadores Un proceso puede obtener la hora local y la hora media de Greenwich con la llamada al sistema gettimeofday de Berkeley. El valor devuelto se expresa en segundos y microsegundos desde la medianoche del 1 de enero de 1970. Un proceso con privilegios puede configurar la hora con la llamada al sistema settimeofday. Cada proceso dispone de tres temporizadores de intervalo que puede configurar y consultar con las llamadas al sistema setitimer y getitimer. Estos temporizadores cuentan hacia atrás desde su valor configurado hasta cero y emiten señales cuando expiran. Uno de los temporizadores se ejecuta continuamente, otro se ejecuta solo cuando el proceso ejecuta su propio código, y el tercero se ejecuta cuando el proceso ejecuta su propio código o código del kernel. Las llamadas time y profil que se encuentran en otras implementaciones de sistemas UNIX también están disponibles. 3. Shell El núcleo del sistema UNIX proporciona esencialmente un sistema de archivos y un entorno en el que se ejecutan los procesos. Estos procesos son iniciados por los usuarios con la ayuda de un programa llamado shell. Este capítulo describe un shell, de hecho, dos. La explicación se divide en tres partes. La primera describe el shell de forma interactiva. La última parte describe cómo funciona un shell, destacando articularmente sus relaciones con el núcleo. La segunda parte muestra cómo se puede usar un intérprete de comandos para ejecutar comandos, una propiedad importante de los intérpretes de comandos. Como se mencionó, la programación de intérpretes de comandos, la característica que distingue a un intérprete de comandos típico, es que un intérprete de comandos es funcionalmente equivalente al intérprete de comandos que se encuentra en otros sistemas operativos; los usuarios dan comandos a un intérprete de comandos y este realiza las llamadas al sistema correspondientes. A diferencia de la mayoría de los intérpretes de comandos, un intérprete de comandos de un sistema UNIX es completamente distinto del núcleo; no solo es un programa separado, sino que es un programa ordinario. ¿Por qué esta separación? La respuesta es la flexibilidad. No es fácil diseñar una interfaz de usuario que funcione bien para una amplia gama de usuarios, e incluso en un mismo sitio, los usuarios pueden ser desde programadores de sistemas hasta oficinistas y gerentes. Sin embargo, la mayoría de los sistemas operativos integran la interfaz de usuario en el sistema operativo, lo que determina el estilo de interacción entre el usuario y el sistema, y ​​también dificulta enormemente su modificación. Dado que un intérprete de comandos es un programa común, un sistema UNIX puede reemplazarlo por usuario. Además, los servicios que proporciona están disponibles tanto para otros programas como para los usuarios; un proceso puede simplemente crear un proceso hijo que ejecute el intérprete de comandos como si fuera cualquier otro programa. Existen dos intérpretes de comandos estándar, al menos uno de los cuales está disponible en todos los sistemas UNIX; ambos se incluyen con el sistema Sun UNIX. Los dos intérpretes se llaman sh y csh. sh significa Bourne Shell (escrito por S. R. Bourne en los Laboratorios Bell) y csh significa C-Shell (llamado así porque su sintaxis se asemeja a la de C); csh fue escrito en Berkeley. Ambos intérpretes se pueden usar como intérpretes de comandos interactivos y como lenguajes de programación. Sin embargo, C-Shell es un poco mejor para el trabajo interactivo, mientras que muchos prefieren Bourne Shell para programar. (La parte interactiva de C-Shell es principalmente un pequeño superconjunto de la shell Bourne, lo que facilita a los usuarios alternar entre ellas). Por consiguiente, muchas personas utilizan ambas shells, y la estructura de este capítulo sigue esta convención ampliamente aceptada: C-Shell se describe como «la» shell interactiva y la shell Bourne como «la» shell programable. Sin embargo, antes de continuar, veamos cómo funciona cada shell.