Indice1. Prólogo3. Diseño De Windows Nt4. Procesos5. Administracion De La Memoria6. Sistema De Archivos7. Entrada Y Salida8. Windows Nt 59. Bibliografia
El presente trabajo trata sobre la Arquitectura de Windows NT. La investigación se ha llevado a cabo desde cuatro puntos de vista que son los componentes señalados por Andrew Tanenbaum para el enfoque del estudio de un Sistema Operativo, es decir: procesos, administración de la memoria, ficheros y entrada/salida. Se ha comenzado con un capítulo dedicado a conceptos básicos y otro a la arquitectura global del sistema, para seguidamente estudiar en detalle cada uno de los cuatro puntos antes citados.
Se puede decir que Windows NT representa la primera propuesta de la empresa Microsoft por construir un Sistema Operativo serio, capaz de competir con los dinosaurios clásicos como UNIX o VMS (o al menos intentarlo). Para ello se ha elegido un enfoque cliente–servidor, con la intención de alcanzar el sueño de los Sistemas Distribuidos.
Windows NT no se puede considerar todavía un sistema operativo de carácter doméstico. Pero el objetivo de sus diseñadores parece ser que lo sea dentro de muy pocos años, y de hecho, para que los programadores vayan entreteniéndose en el uso de las llamadas al sistema (el llamado Win32), han construido un Sistema Operativo de poco confiable. Hablamos, cómo no, de Windows 95.
La arquitectura de Windows 95/98 no tiene absolutamente nada que ver con la de Windows NT. No es que sea un mal trabajo pero deja bastante que desear.
Durante la elaboración de este trabajo nos hemos encontrado con varias dificultades principalmente por la falta de fuentes bibliográficas que trataran el diseño con la suficiente profundidad como para satisfacer el nivel que se buscaba. Los libros que se encontraron, la mayoría se refieren a la Administración del Sistema a alto nivel, o bien de la programación bajo Windows NT en lenguaje C++, por lo que he tenido que recurrir a diversos artículos de la Internet, principalmente al nodo de Microsoft en Internet. Por ello, pedimos disculpas por las posibles erratas que puedan existir.
Para tratar ciertos temas dentro de cada capítulo se ha creído conveniente apoyarnos en la descripción de las llamadas al sistema, ya que, a nuestro entender, de esta manera todo resulta mucho más fácil de explicar y de comprender.
2. Introduccion Y Conceptos Basicos
Eventos A Través Del Tiempo
A finales de los años 40's y a principios de los años 50's las computadoras masivas, eran controladas por tubos al vacío inestables. Todo la programación se hacía directamente en lenguaje de máquina porque la industria no había avanzado lo suficiente para necesitar Sistemas Operativos. Con la aparición del transistor a mediados de los 50's, las computadoras se fueron haciendo más y más confiables.
Lenguajes crudos como Ensamblador y Fortran aparecieron, pero un Sistema Operativo (S.O.), tal como los conocemos ahora, aún no. Para accesar a la programación de la maquinaria se manejaron tarjetas perforadas.
1960's. Cuando IBM introdujo la computadora System/360 intentó tomar el mercado científico y el comercial. Cuando en este proyecto surgieron problemas de conflictos por la arquitectura, se inició el desarrollo de un software que resolviera todos aquellos conflictos, el resultado fue un muy complejo sistema operativo. Luego AT&T trató de desarrollar a Multics, un Sistema Operativo que soportara cientos de usuarios de tiempo compartido, pero falló. Más adelante científicos de la computación desarrollaron Unics, que sería monousuario. Ello marca el nacimiento de Unix (1969), el primero de los sistemas operativos modernos.
1980's. En este tiempo la arquitectura de las computadoras, circuitos LSI (Large Scale Integration) abrieron el paso para una nueva generación de computadoras. DOS de Microsoft aparece en 1981 dominando este mercado de las PCs inmediatamente, aunque el sistema UNIX, predomina en las estaciones de trabajo.
1990's. Aumenta el uso de conexiones en redes, equipos de trabajo y aplicaciones distribuidas, los cuales surgen en la década anterior, con ello los Sistemas Operativos como Unix, Windows NT, etc., soportan muchos clientes, dando así el nacimiento de la Computación en Red.
Sistema Operativo
Introducción
Software básico que controla y administra los recursos de una computadora. El sistema operativo tiene tres grandes funciones: coordina y manipula el hardware de la computadora, como la memoria, las impresoras, las unidades de disco, el teclado o el mouse; organiza los archivos en diversos dispositivos de almacenamiento, como discos flexibles, discos duros, discos compactos o cintas magnéticas, y gestiona los errores de hardware y la pérdida de datos.
¿Cómo funciona un sistema operativo?
Los Sistemas Operativos controlan diferentes procesos de la computadora. Un proceso importante es la interpretación de los comandos que permiten al usuario comunicarse con el ordenador. Algunos intérpretes de instrucciones están basados en texto y exigen que las instrucciones sean tecleadas. Otros están basados en gráficos, y permiten al usuario comunicarse señalando y haciendo clic en un icono.
Los Sistemas Operativos pueden ser de tarea única o multitarea. Los sistemas operativos de tarea única, más primitivos, sólo pueden manejar un proceso en cada momento. Por ejemplo, cuando la computadora está imprimiendo un documento, no puede iniciar otro proceso ni responder a nuevas instrucciones hasta que se termine la impresión.
Todos los Sistemas Operativos modernos son multitarea y pueden ejecutar varios procesos simultáneamente. En la mayoría de los ordenadores sólo hay una UCP; un Sistema Operativo multitarea crea la ilusión de que varios procesos se ejecutan simultáneamente en la UCP. El mecanismo que se emplea más a menudo para lograr esta ilusión es la multitarea por segmentación de tiempos, en la que cada proceso se ejecuta individualmente durante un periodo de tiempo determinado. Si el proceso no finaliza en el tiempo asignado, se suspende y se ejecuta otro proceso. Este intercambio de procesos se denomina conmutación de contexto. El sistema operativo se encarga de controlar el estado de los procesos suspendidos. También cuenta con un mecanismo llamado planificador que determina el siguiente proceso que debe ejecutarse. El planificador ejecuta los procesos basándose en su prioridad para minimizar el retraso percibido por el usuario. Los procesos parecen efectuarse simultáneamente por la alta velocidad del cambio de contexto.
Los Sistemas Operativos pueden emplear memoria virtual para ejecutar procesos que exigen más memoria principal de la realmente disponible. Con esta técnica se emplea espacio en el disco duro para simular la memoria adicional necesaria.
Sistemas Operativos actuales
Los sistemas operativos empleados normalmente son UNIX, Macintosh OS, MS-DOS, OS/2 y Windows-NT. El UNIX y sus clones permiten múltiples tareas y múltiples usuarios. Su sistema de archivos proporciona un método sencillo de organizar archivos y permite la protección de archivos. Sin embargo, las instrucciones del UNIX no son intuitivas. Otros sistemas operativos multiusuario y multitarea son OS/2, desarrollado inicialmente por Microsoft Corporation e International Business Machines (IBM) y Windows-NT, desarrollado por Microsoft. El sistema operativo multitarea de las computadoras Apple se denomina Macintosh OS. El DOS y su sucesor, el MS-DOS, son sistemas operativos populares entre los usuarios de computadoras personales. Sólo permiten un usuario y una tarea.
Sistema Operativo de Red
A un Sistema Operativo de Red se le conoce como NOS. Es el software necesario para integrar los muchos componentes de una red en un sistema particular, al cual el usuario final puede tener acceso.
Otra definición es la siguiente; es un software que rige y administra los recursos, archivos, periféricos, usuarios, etc., en una red y lleva el control de seguridad de los mismos.
Un NOS maneja los servicios necesarios para asegurar que el usuario final tenga o esté libre de error al accesar a la red. Un NOS normalmente provee una interfaz de usuario que es para reducir la complejidad y conflictos al momento de usar la red.
Dentro del contexto del Sistema Operativo de Red, se pueden escribir aplicaciones tales como un sistema de correo electrónico pueden ser escritas para que permitan "conexiones virtuales" entre entidades de red, sin intervención humana directa.
Diferencia entre un S.O. Distribuido, un S.O. de Red y un S.O. Centralizado.
En un Sistema Operativo de Red, los usuarios saben de la existencia de varias computadoras y pueden conectarse con máquinas remotas y copiar archivos de una máquina a otra, cada máquina ejecuta su propio sistema operativo local y tiene su propio usuario o grupo de usuarios.
Por el contrario, un Sistema Operativo Distribuido es aquel que aparece ante sus usuarios como un sistema tradicional de un solo procesador, aun cuando esté compuesto por varios procesadores. En un sistema distribuido verd adero, los usuarios no deben saber del lugar donde su programa se ejecute o del lugar donde se encuentran sus archivos; eso debe ser manejado en forma automática y eficaz por el Sistema Operativo.
Además son sistemas autónomos capaces de comunicarse y cooperar entre sí para resolver tareas globales. Es indispensable el uso de redes para intercambiar datos. Además de los servicios típicos de un Sistema Operativo, un Sistema Distribuido debe gestionar la distribución de tareas entre los diferentes nodos conectados. También, debe proporcionar los mecanismos necesarios para compartir globalmente los recursos del sistema.
Sistemas Operativos Centralizados, de un solo procesador, de un solo CPU o incluso tradicionales; en todo caso, lo que esto quiere decir es que un sistema operativo controla una sola computadora.
3. Diseño De Windows Nt
Windows NT presenta una arquitectura del tipo cliente-servidor. Los programas de aplicación son contemplados por el sistema operativo como si fueran clientes a los que hay que servir, y para lo cual viene equipado con distintas entidades servidoras.
Uno de los objetivos fundamentales de diseño fue el tener un núcleo tan pequeño como fuera posible, en el que estuvieran integrados módulos que dieran respuesta a aquellas llamadas al sistema que necesariamente se tuvieran que ejecutar en modo privilegiado (también llamado modo kernel, modo núcleo y modo supervisor). El resto de las llamadas se expulsarían del núcleo hacia otras entidades que se ejecutarían en modo no privilegiado (modo usuario), y de esta manera el núcleo resultaría una base compacta, robusta y estable. Por eso se dice que Windows NT es un sistema operativo basado en micro-kernel.
Es por ello que en un primer acercamiento a la arquitectura distinguimos un núcleo que se ejecuta en modo privilegiado, y se denomina Executive, y unos módulos que se ejecutan en modo no privilegiado, llamados subsistemas protegidos.
Los programas de usuario (también llamados programas de aplicación) interaccionan con cualquier sistema operativo (SO) a través de un juego de llamadas al sistema, que es particular de cada SO. En el mundo Windows en general, las llamadas al sistema se denominan API (Application Programming Interfaces, interfaces para la programación de aplicaciones). En Windows NT y en Windows 95 se usa una versión del API llamada API Win32. Un programa escrito para Windows NT o Windows 95, y que por consiguiente hace uso del API Win32, se denomina genéricamente "programa Win32", y de hecho esta denominación es bastante frecuente en artículos y libros al respecto. Desgraciadamente, y conviene dejarlo claro cuanto antes, el término "Win32" tiene tres acepciones (al menos hasta ahora) totalmente distintas. Una es el API, otra es el nombre de uno de los subsistemas protegidos de Windows NT del que hablaremos más adelante, y por último se denomina Win32s a una plataforma desarrollada por Microsoft, similar a Windows 3.1, pero que usa el API Win32 en vez del API Win16 del Windows 3.1.
Hechas estas aclaraciones, podemos continuar adelante. Algunas de las llamadas al sistema, debido a su naturaleza, son atendidas directamente por el Executive, mientras que otras son desviadas hacia algún subsistema. Esto lo veremos con detalle en breve.
El hecho de disponer de un núcleo rodeado de subsistemas que se ejecutan en modo usuario nos permite además añadir nuevos subsistemas sin producir ningún tipo de confrontación.
En el diseño de Windows NT han confluido aportaciones de tres modelos: el modelo cliente-servidor, el modelo de objetos, y el modelo de multiprocesamiento simétrico.
Modelo cliente-servidor. En la teoría de este modelo se establece un kernel que básicamente se encarga de recibir peticiones de procesos clientes y pasárselas a otros procesos servidores, ambos clientes y servidores ejecutándose en modo usuario. Windows NT pone el modelo en práctica pero no contempla el núcleo como un mero transportador de mensajes, sino que introduce en él aquellos servicios que sólo pueden ser ejecutados en modo kernel. El resto de servicios los asciende hacia subsistemas servidores que se ejecutan en modo usuario, independientes entre sí, y que por tanto pueden repartirse entre máquinas distintas, dando así soporte a un sistema distribuido (de hecho, el soportar los sistemas distribuidos fue otra de las grandes directivas de diseño de este SO).
Modelo de objetos. Decir que no implementa puramente la teoría de este modelo, sino que más bien lo que hace es simplemente contemplar los recursos (tanto internos como externos) como objetos. Más adelante daremos una lista de los objetos de Windows NT. Brevemente, señalar que todo objeto ha de poseer identidad propia (es único y distinguible de todos los demás), y una serie de atributos (variables) y métodos (funciones) que modifican sus atributos. Los objetos interaccionan entre sí a través del envío de mensajes. No sólo existen en Windows NT objetos software (lógicos), sino que los dispositivos hardware (físicos) también son tratados como objetos (a diferencia de UNIX, que recordemos trataba a los dispositivos como ficheros).
Modelo de multiprocesamiento simétrico. Un SO multiproceso (o sea, aquel que cuenta con varias CPU y cada una puede estar ejecutando un proceso) puede ser simétrico (SMP) o asimétrico (ASMP). En los sistemas operativos SMP (entre los que se encuentran Windows NT y muchas versiones de UNIX) cualquier CPU puede ejecutar cualquier proceso, ya sea del SO o no, mientras que en los ASMP se elige una CPU para uso exclusivo del SO y el resto de CPU quedan para ejecutar programas de usuario. Los sistemas SMP son más complejos que los ASMP, contemplan un mejor balance de la carga y son más tolerantes a fallos (de manera que si un subproceso del SO falla, el SO no se caerá pues podrá ejecutarse sobre otra CPU, cosa que en los ASMP no sería posible, con lo que se bloquearía el sistema entero).
Comencemos describiendo los subsistemas protegidos, para seguidamente estudiar la estructura del Executive.
Figura 1. El núcleo se ejecuta en modo privilegiado (Executive) y en modo no privilegiado (subsistemas protegidos)
Los Subsistemas Protegidos
Son una serie de procesos servidores que se ejecutan en modo usuario como cualquier proceso de usuario, pero que tienen algunas características propias que los hacen distintos. Al decir subsistemas protegidos nos referiremos, pues, a estos procesos. Se inician al arrancar el SO. Los hay de dos tipos: integrales y de entorno.
Un Subsistema Integral: es aquel servidor que ejecuta una función crítica del SO (como por ejemplo el que gestiona la seguridad). Tenemos los siguientes:
El Subsistema Proceso de Inicio (Logon Process)
El proceso de inicio (Logon Process) recibe las peticiones de conexión por parte de los usuarios. En realidad son dos procesos, cada uno encargándose de un tipo distinto de conexión:
El proceso de inicio local: gestiona la conexión de usuarios locales directamente a una máquina Windows NT.
El proceso de inicio remoto: gestiona la conexión de usuarios remotos a procesos servidores de Windows NT.
Figura 2. Diagrama de Flujo del Proceso de Inicio de Windows NT.
El Subsistema de Seguridad
Este subsistema interacciona con el proceso de inicio y el llamado monitor de referencias de seguridad (se tratara en el Executive), y de esta forma se construye el modelo de seguridad en Windows NT.
El subsistema de seguridad interacciona con el proceso de inicio, atendiendo las peticiones de acceso al sistema. Consta de dos subcomponentes:
La autoridad de seguridad local: es el corazón del subsistema. En general gestiona la política de seguridad local; así, se encarga de generar los permisos de acceso, de comprobar que el usuario que solicita conexión tiene acceso al sistema, de verificar todos los accesos sobre los objetos (para lo cual se ayuda del monitor de referencias a seguridad) y de controlar la política de auditorías, llevando la cuenta de los mensajes de auditoría generados por el monitor de referencias. Las auditorías son una facilidad que proporciona Windows NT para monitorizar diversos acontecimientos del sistema por parte del Administrador.
El administrador de cuentas: mantiene una base de datos con las cuentas de todos los usuarios (login, claves, identificaciones, etc.). Proporciona los servicios de validación de usuarios requeridos por el subcomponente anterior.
Un Subsistema de Entorno: da soporte a aplicaciones procedentes de SO distintos, adaptándolas para su ejecución bajo Windows NT. Existen tres de este tipo:
El Subsistema Win32
Es el más importante, ya que atiende no sólo a las aplicaciones nativas de Windows NT, sino que para aquellos programas no Win32, reconoce su tipo y los lanza hacia el subsistema correspondiente. En el caso de que la aplicación sea MS-DOS o Windows de 16 bits (Windows 3.11 e inferiores), lo que hace es crear un nuevo subsistema protegido pero no servidor. Así, la aplicación DOS o Win16 se ejecutaría en el contexto de un proceso llamado VDM (Virtual DOS Machine, máquina virtual DOS), que no es más que un simulador de un ordenador funcionando bajo MS-DOS. Las llamadas al API Win16 serían correspondidas con las homónimas en API Win32. Microsoft llama a esto WOW (Windows On Win32).
El subsistema soporta una buena parte del API Win32. Así, se encarga de todo lo relacionado con la interfaz gráfica con el usuario (GUI), controlando las entradas del usuario y salidas de la aplicación. Por ejemplo, un buen número de funciones de las bibliotecas USER32 y GDI32 son atendidas por Win32, ayudándose del Executive cuando es necesario.
El funcionamiento como servidor de Win32 lo veremos un poco más adelante, en el apartado de llamadas a procedimientos locales.
El Subsistema POSIX
La norma POSIX (Portable Operating System Interface for Unix) fue elaborada por IEEE para conseguir la portabilidad de las aplicaciones entre distintos entornos UNIX. La norma se ha implementado no sólo en muchas versiones de UNIX, sino también en otros SO como Windows NT, VMS, etc. Se trata de un conjunto de 23 normas, identificadas como IEEE 1003.0 a IEEE 1003.22, o también POSIX.0 a POSIX.22, de las cuales el subsistema POSIX soporta la POSIX.1, que define un conjunto de llamadas al sistema en lenguaje C.
El subsistema sirve las llamadas interaccionando con el Executive. Se encarga también de definir aspectos específicos del SO UNIX, como pueden ser las relaciones jerárquicas entre procesos padres e hijos (las cuales no existen en el subsistema Win32, por ejemplo, y que por consiguiente no aparecen implementadas directamente en el Executive).
El Subsistema OS/2
Igual que el subsistema POSIX proporciona un entorno para aplicaciones UNIX, este subsistema da soporte a las aplicaciones OS/2. Proporciona la interfaz gráfica y las llamadas al sistema; las llamadas son servidas con ayuda del Executive.
El Executive
No se debe confundir el Executive con el núcleo de Windows NT, aunque muchas veces se usan (incorrectamente) como sinónimos. El Executive consta de una serie de componentes software, que se ejecutan en modo privilegiado, y uno de los cuales es el núcleo. Dichos componentes son totalmente independientes entre sí, y se comunican a través de interfaces bien definidas. Recordemos que en el diseño se procuró dejar el núcleo tan pequeño como fuera posible, y, como veremos, la funcionalidad del núcleo es mínima. Pasemos a comentar cada módulo.
El Administrador de Objetos (Object Manager)
Se encarga de crear, destruir y gestionar todos los objetos del Executive. Tenemos infinidad de objetos: procesos, subprocesos, ficheros, segmentos de memoria compartida, semáforos, mutex, sucesos, etc. Los subsistemas de entorno (Win32, OS/2 y POSIX) también tienen sus propios objetos. Por ejemplo, un objeto ventana es creado (con ayuda del administrador de objetos) y gestionado por el subsistema Win32. La razón de no incluir la gestión de ese objeto en el Executive es que una ventana sólo es innata de las aplicaciones Windows, y no de las aplicaciones UNIX o OS/2. Por tanto, el Executive no se encarga de administrar los objetos relacionados con el entorno de cada SO concreto, sino de los objetos comunes a los tres.
El Administrador de Procesos (Process Manager)
Se encarga (en colaboración con el administrador e objetos) de crear, destruir y gestionar los procesos y subprocesos. Una de sus funciones es la de repartir el tiempo de CPU entre los distintos subprocesos (ver el capítulo de los procesos). Suministra sólo las relaciones más básicas entre procesos y subprocesos, dejando el resto de las interrelaciones entre ellos a cada subsistema protegido concreto. Por ejemplo, en el entorno POSIX existe una relación filial entre los procesos que no existe en Win32, de manera que se constituye una jerarquía de procesos. Como esto sólo es específico de ese subsistema, el administrador de objetos no se entromete en ese trabajo y lo deja en manos del subsistema.
El Administrador de Memoria Virtual (Virtual Memory Manager)
Windows NT y UNIX implementan un direccionamiento lineal de 32 bits y memoria virtual paginada bajo demanda. El VMM se encarga de todo lo relacionado con la política de gestión de la memoria: determina los conjuntos de trabajo de cada proceso, mantiene un conjunto de páginas libres, elige páginas víctima, sube y baja páginas entre la memoria RAM y el archivo de intercambio en disco, etc. Una explicación detallada la dejaremos para el capítulo de la memoria.
Facilidad de Llamada a Procedimiento Local (LPC Facility)
Este módulo se encarga de recibir y envíar las llamadas a procedimiento local entre las aplicaciones cliente y los subsistemas servidores.
Administrador de Entrada/Salida (I/O Manager)
Consiste en una serie de subcomponentes, que son:
El administrador del sistema de ficheros
El servidor y el redirector de red
Los drivers de dispositivo del sistema
El administrador de caches
Buena parte de su trabajo es la gestión de la comunicación entre los distintos drivers de dispositivo, para lo cual implementa una interfaz bien definida que permite el tratamiento de todos los drivers de una manera homogénea, sin que intervenga el cómo funciona específicamente cada uno.
Trabaja en conjunción con otros componentes del Executive, sobre todo con el VMM. Le proporciona la E/S síncrona y asíncrona, la E/S a archivos asignados en memoria y las caches de los ficheros.
El administrador de caches no se limita a gestionar unos cuantos buffers de tamaño fijo para cada fichero abierto, sino que es capaz de estudiar las estadísticas sobre la carga del sistema y variar dinámicamente esos tamaños de acuerdo con la carga. El VMM realiza algo parecido en su trabajo, como veremos en su momento.
Este componente da soporte en modo privilegiado al subsistema de seguridad, con el que interacciona. Su misión es actuar de alguna manera como supervisor de accesos, ya que comprueba si un proceso determinado tiene permisos para acceder a un objeto determinado, y monitoriza sus acciones sobre dicho objeto.
De esta manera es capaz de generar los mensajes de auditorías. Soporta las validaciones de acceso que realiza el subsistema de seguridad local.
En UNIX, de la seguridad se encargaba un módulo llamado el Kerberos (Cancerbero), desarrollado por el MIT como parte del Proyecto Atenas. Kerberos se ha convertido en una norma de facto, y se incorporará a Windows NT en su versión 5.0.
El Núcleo (Kernel)
Situado en el corazón de Windows NT, se trata de un micro-kernel que se encarga de las funciones más básicas de todo el SO:
Ejecución de subprocesos
Sincronización multiprocesador
Manejo de las interrupciones hardware
Nivel de Abstracción de Hardware (HAL)
Es una capa de software incluida en el Executive que sirve de interfaz entre los distintos drivers de dispositivo y el resto del sistema operativo. Con HAL, los dispositivos se presentan al SO como un conjunto homogéneo, a través de un conjunto de funciones bien definidas. Estas funciones son llamadas tanto desde el SO como desde los propios drivers. Permite a los drivers de dispositivo adaptarse a distintas arquitecturas de E/S sin tener que ser modificados en gran medida. Además oculta los detalles hardware que conlleva el multiprocesamiento simétrico de los niveles superiores del SO.
Llamadas a Procedimientos Locales y Remotos
Windows NT, al tener una arquitectura cliente-servidor, implementa el mecanismo de llamada a procedimiento remoto (RPC) como medio de comunicación entre procesos clientes y servidores, situados ambos en máquinas distintas de la misma red. Para clientes y servidores dentro de la misma máquina, la RPC toma la forma de llamada a procedimiento local (LPC). Vamos a estudiar en detalle ambos mecanismos pues constituyen un aspecto fundamental del diseño de Windows NT.
RPC (Remote Procedure Call)
Se puede decir que el sueño de los diseñadores de Windows NT es que algún día se convierta en un sistema distribuido puro, es decir, que cualquiera de sus componentes pueda residir en máquinas distintas, siendo el kernel en cada máquina el coordinador general de mensajes entre los distintos componentes. En la última versión de Windows NT esto no es aún posible.
No obstante, el mecanismo de RPC permite a un proceso cliente acceder a una función situada en el espacio virtual de direcciones de otro proceso servidor situado en otra máquina de una manera totalmente transparente.
Vamos a explicar el proceso en conjunto. Supongamos que se tiene un proceso cliente ejecutándose bajo una máquina A, y un proceso servidor bajo una máquina B. El cliente llama a una función f de una biblioteca determinada. El código de f en su biblioteca es una versión especial del código real; el código real reside en el espacio de direcciones del servidor. Esa versión especial de la función f que posee el cliente se denomina proxy. El código proxy lo único que hace es recoger los parámetros de la llamada a f, construye con ellos un mensaje, y pasa dicho mensaje al Executive. El Executive analiza el mensaje, determina que va destinado a la máquina B, y se lo envía a través del interfaz de transporte. El Executive de la máquina B recibe el mensaje, determina a qué servidor va dirigido, y llama a un código especial de dicho servidor, denominado stub, al cual le pasa el mensaje. El stub desempaqueta el mensaje y llama a la función f con los parámetros adecuados, ya en el contexto del proceso servidor. Cuando f retorna, devuelve el control al código stub, que empaqueta todos los parámetros de salida (si los hay), forma así un mensaje y se lo pasa al Executive.
Ahora se repite el proceso inverso; el Executive de B envía el mensaje al Executive de A, y este reenvía el mensaje al proxy. El proxy desempaqueta el mensaje y devuelve al cliente los parámetros de retorno de f. Por tanto, para el cliente todo el mecanismo ha sido transparente. Ha hecho una llamada a f, y ha obtenido unos resultados; ni siquiera tiene que saber si el código real de f está en su biblioteca o se encuentra en una máquina situada tres plantas más abajo.
LPC (Local Procedure Call)
Las LPC se pueden considerar una versión descafeinada de las RPC. Se usan cuando un proceso necesita los servicios de algún subsistema protegido, típicamente Win32. Se intentara descubrir su funcionamiento.
El proceso cliente tiene un espacio virtual de 4 Gb. Los 2 Gb inferiores son para su uso (excepto 128 Kb). Los 2 Gb superiores son para uso del sistema.
Vamos a suponer que el cliente realiza una llamada a la función CreateWindow. Dicha función crea un objeto ventana y devuelve un descriptor al mismo. No es gestionada directamente por el Executive, sino por el subsistema Win32 (con algo de colaboración por parte del Executive, por supuesto; por ejemplo, para crear el objeto). El subsistema Win32 va guardando en su propio espacio de direcciones una lista con todos los objetos ventana que le van pidiendo los procesos. Por consiguiente, los procesos no tienen acceso a la memoria donde están los objetos; simplemente obtienen un descriptor para trabajar con ellos. Cuando el cliente llama a CreateWindow, se salta al código de esa función que reside en la biblioteca USER32.DLL asignada en el espacio de direcciones del cliente.
Por supuesto, ese no es el código real, sino el proxy. El proxy empaqueta los parámetros de la llamada, los coloca en una zona de memoria compartida entre el cliente y Win32, pone al cliente a dormir y ejecuta una LPC. La facilidad de llamada a procedimiento local del Executive captura esa llamada, y en el subsistema Win32 se crea un subproceso que va a atender a la petición del cliente. Ese subproceso es entonces despertado, y comienza a ejecutar el correspondiente código de stub. Los códigos de stub de los subsistemas se encuentran en los 2 Gb superiores (los reservados) del espacio virtual del proceso cliente. Aunque no he encontrado más documentación al respecto, es muy probable que dichos 2 Gb sean los mismos que se ven desde el espacio virtual de Win32. Sea como sea, el caso es que el stub correspondiente desempaqueta los parámetros del área de memoria compartida y se los pasa a la función CreateWindow situada en el espacio de Win32. Ése sí es el código real de la función. Cuando la función retorna, el stub continúa, coloca el descriptor a la ventana en la memoria compartida, y devuelve el control de la LPC al Executive. El subproceso del Win32 es puesto a dormir. El Executive despierta al subproceso cliente, que estaba ejecutando código proxy. El resto de ese código lo que hace es simplemente tomar el descriptor y devolverlo como resultado de la función CreateWindow.
Definición de Proceso y Sub Proceso
Debemos tener cuidado con no confundir el proceso en Windows NT con el proceso en los SO más clásicos, como UNIX. Vamos a intentar dar una definición general de lo que entiende Windows NT por proceso y subproceso, aunque después iremos perfilando poco a poco ambos conceptos.
Un proceso es una entidad no ejecutable que posee un espacio de direcciones propio y aislado, una serie de recursos y una serie de subprocesos. En el espacio de direcciones hay colocado algún código ejecutable (entre otras cosas). Bien, hemos dicho que un proceso es una entidad "no-ejecutable". En efecto, no puede ejecutar el código de su propio espacio de direcciones, sino que para esto le hace falta al menos un subproceso. Por consiguiente, un subproceso es la unidad de ejecución de código. Un subproceso está asociado con una serie de instrucciones, unos registros, una pila y una cola de entrada de mensajes (enviados por otros procesos o por el SO).
Cuando se crea un proceso, automáticamente se crea un subproceso asociado (llamado subproceso primario). Los subprocesos también se llaman "hebras de ejecución" (threads of execution). Debe quedarnos muy claro, pues, que lo que se ejecutan son subprocesos, no procesos. Los procesos son como el soporte sobre el que corren los subprocesos. Y entre los subprocesos se reparte el tiempo de CPU.
Podemos pensar en los subprocesos de Windows NT como los procesos de los SO clásicos (aunque existen matices, como sabemos). A veces, por comodidad y por costumbre, usaremos ambos términos como sinónimos, y diremos que "el proceso A llama a CreateWindow", aunque se debe entender que "un subproceso del proceso A llama a CreateWindow".
Un proceso tiene un espacio de direcciones virtuales de 4 Gb. En algún lugar de ese espacio se halla un código ejecutable (que quizás es la imagen de un programa en disco). Un proceso puede crear subprocesos, estando su número fijado por el sistema. Se dice que muere cuando todos sus subprocesos han muerto (incluso aunque el subproceso primario haya muerto, si aún existe algún subproceso propiedad del proceso, el proceso seguirá vivo).
Planificación del Tiempo de la CPU por Round Robin con Prioridades
Windows NT utiliza la planificación del anillo circular o round robin. Esta técnica consiste en que los subprocesos que pueden ser ejecutados se organizan formando un anillo, y la CPU va dedicándose a cada uno durante un tiempo. El tiempo máximo que la CPU va a estar dedicada a cada uno se denomina quantum, y es fijado por el Administrador del Sistema.
Si el subproceso está esperando por alguna entrada-salida o por algún suceso, la CPU lo pondrá a dormir, y pondrá en ejecución al siguiente del anillo. Si un subproceso que la CPU está ejecutando consume su quantum, la CPU también lo pondrá a dormir, pasando al siguiente.
En Windows NT, existe un rango de prioridades que va del 1 al 31, siendo 31 la más alta. Todo proceso y subproceso tienen un valor de prioridad asociado.
Existe un anillo o cola circular por cada uno de los niveles de prioridad. En cada anillo están los subprocesos de la misma prioridad. El Executive comienza a repartir el tiempo de CPU en el primer anillo de mayor prioridad no vacío. A cada uno de esos subprocesos se les asigna secuencialmente la CPU durante el tiempo de un quantum, como ya indicamos antes. Cuando todos los subprocesos de nivel de prioridad n están dormidos, el Executive comienza a ejecutar los del nivel (n-1), siguiendo el mismo mecanismo.
Análogamente, si un subproceso se está ejecutando, y llegara uno nuevo de prioridad superior, el Executive suspendería al primero (aunque no haya agotado su quantum), y comenzaría a ejecutar el segundo (asignándole un quantum completo).
Prioridad de proceso y subproceso
Un proceso se dice que pertenece a una clase de prioridad. Existen cuatro clases de prioridad, que son:
Desocupado. Corresponde a un valor de prioridad 4.
Normal. Corresponde a un valor de prioridad 7 ó 9.
Alta. Corresponde a un valor de prioridad 13.
Tiempo Real. Corresponde a un valor de prioridad 24.
La clase "Normal" es la que el Executive asocia a los procesos por defecto. Los procesos en esta clase se dice que tienen una prioridad dinámica: el Executive les asigna un valor de 7 si se están ejecutando en segundo plano, mientras que si pasan a primer plano, la prioridad se les aumenta a un valor de 9.
La clase "Desocupado" va bien para procesos que se ejecuten periódicamente y que por ejemplo realicen alguna función de monitorización.
La clase "Alta" la tienen procesos tales como el Administrador de Tareas (Task Manager). Dicho proceso está la mayor parte del tiempo durmiendo, y sólo se activa si el usuario pulsa Control-Esc. Entonces, el SO inmediatamente pone a dormir al subproceso en ejecución (aunque no haya agotado su quantum) y ejecuta al subproceso correspondiente del proceso Administrador de Tareas, que visualizará el cuadro de diálogo característico, mostrándonos las tareas actuales.
La clase "Tiempo Real" no es recomendable que la tenga ningún proceso normal. Es una prioridad más alta incluso que muchos procesos del sistema, como los que controlan el ratón, el teclado, el almacenamiento en disco en segundo plano, etc. Es evidente que usar esta prioridad sin un control extremo puede causar consecuencias nefastas.
Así como un proceso tiene una prioridad oscilando entre cuatro clases, un subproceso puede tener cualquier valor en el rango [1,31]. En principio, cuando el subproceso es creado, su prioridad es la correspondiente a la de la clase de su proceso padre. Pero este valor puede ser modificado si el subproceso llama a la función
BOOL SetThreadPriority (HANDLE hThread, int nPriority);
Donde:
hThread es el descriptor del subproceso
nPriority puede ser:
THREAD_PRIORITY_LOWEST : resta 2 a la prioridad del padre
THREAD_PRIORITY_BELOW_NORMAL: resta 1 a la prioridad del padre
THREAD_PRIORITY_NORMAL: mantiene la prioridad del padre
THREAD_PRIORITY_ABOVE_NORMAL: suma 1 a la prioridad del padre
THREAD_PRIORITY_HIGHEST: suma 2 a la prioridad del padre
THREAD_PRIORITY_IDLE: hace la prioridad igual a 1, independientemente de la prioridad del padre
THREAD_PRIORITY_TIME_CRITICAL: hace la prioridad igual a 15 si la clase de prioridad del padre es desocupada, normal o alta; si es tiempo real, entonces hace la prioridad igual a 31
De esta manera es como calcula el Executive la prioridad de un subproceso. Por tanto, la prioridad de un subproceso es relativa a la de su padre (excepto en IDLE y TIME_CRITICAL). Mediante suma y resta de la prioridad del padre obtenemos todo el rango de prioridades:
Clase proceso padre Prior. Subproceso | Clase desocupado | Clase normal en primer plano | Clase normal en segundo plano | Clase alta | Clase tiempo real |
Crítico en tiempo | 15,00 | 15,00 | 15,00 | 15,00 | 31,00 |
Más alta | 6,00 | 9,00 | 11,00 | 15,00 | 26,00 |
Más que normal | 5,00 | 8,00 | 10,00 | 14,00 | 25,00 |
Normal | 4,00 | 7,00 | 9,00 | 13,00 | 24,00 |
Menos que normal | 3,00 | 6,00 | 8,00 | 12,00 | 23,00 |
Más baja | 2,00 | 5,00 | 7,00 | 11,00 | 22,00 |
Desocupado | 1,00 | 1,00 | 1,00 | 1,00 | 16,00 |
La ventaja de este sistema de las prioridades relativas es que si un proceso cambia su clase de prioridad durante su vida, sus subprocesos hijos tendrían sus prioridades automáticamente actualizadas.
Creación y destrucción de procesos
La llamada al sistema que crea un proceso es una de las más complejas de todo el API Win32. Vamos a comentarla para comprender mejor la forma en la que el Executive trabaja.
Un proceso se crea cuando un subproceso de otro proceso hace una llamada a:
BOOL CreateProcess (LPCTSTR lpszImageName, LPCTSTR lpszCommandLine, LPSECURITY_ATTRIBUTES lpsaProcess, LPSECURITY_ATTRIBUTES lpsaThread, BOOL fInheritHandles, DWORD fdwCreate, LPVOID lpvEnvironment, LPTSTR lpszCurDir, LPSTARTUPINFO lpsiStartInfo, LPROCESS_INFORMATION lppiProcInfo);
El Executive crea un espacio virtual de 4 Gb para el proceso, y también crea el subproceso primario. Veamos el significado de los parámetros:
lpszImageName: es el nombre del archivo que contiene el código ejecutable que el Executive asignará en el espacio virtual del proceso. Si es NULL, entonces se entenderá que viene dado en el siguiente parámetro.
lpszCommandLine: argumentos para el nombre del archivo
lpsaProcess, lpsaThread y fInheritHandles: los dos primeros son punteros con los que podemos dar unos atributos de seguridad para el proceso y su subproceso primario. Si pasamos NULL, el sistema pondrá los valores por defecto. Los parámetros son punteros a una estructura del tipo:
El campo lpSecurityDescriptor se refiere a permisos sobre el objeto proceso, pero no he encontrado más información al respecto.
De esta estructura vamos a destacar el campo bInheritHandle, que se refiere a la herencia. En Windows NT, cualquier objeto que creemos va a tener asociada una estructura de este tipo en donde se indicará, con el parámetro bInheritHandle, si dicho objeto es heredable o no. El objeto es propiedad de un proceso. Ese proceso puede crear procesos hijos; dichos procesos, por defecto, no heredarán ninguno de los objetos de su padre. Los procesos hijos que tengan la capacidad de heredar, heredarán aquellos objetos de su padre que tengan el campo bInheritHandle a TRUE.
Ahora bien, un proceso y un subproceso son también objetos. Por tanto, ambos objetos podrán ser heredables por otros procesos. Si el campo bInheritHandle es TRUE en la estructura apuntada por lpsaProcess, entonces el proceso que estamos creando será heredable por otros procesos hijos de su mismo padre.
Si el campo bInheritHandle es TRUE en la estructura apuntada por lpsaThread, entonces e subproceso primario del proceso que estamos creando será igualmente heredable.
Resta explicar el parámetro fInheritHandles. Si vale TRUE, entonces el proceso que estamos creando podrá heredar todos los objetos heredables de su padre (es decir, aquellos objetos cuyo campo bInheritHandle sea TRUE).
No se debe confundir todo esto con herencia entre procesos y subprocesos hijos. Un subproceso siempre podrá tener acceso a los objetos creados por el proceso al que pertenece (o mejor dicho, creados por algún otro subproceso del proceso al que pertenece).
fdwCreate: es una máscara donde se pueden especificar (mediante OR lógica) muchos indicadores para el proceso a crear. Los más importantes son:la clase de prioridad del proceso: IDLE_PRIORITY_CLASS (desocupado), NORMAL_PRIORITY_CLASS (normal), HIGH_PRIORITY_CLASS (alta), REALTIME_PRIORITY_CLASS (tiempo real)si el proceso va a ser dormido al crearse, usando CREATE_SUSPENDED
lpvEnvironment: apunta a un bloque de memoria que contiene cadenas de entorno. Si vale NULL, usará las mismas cadenas que su proceso padre. Las cadenas de entorno no son más que variables con algún valor que usarán los procesos con algún fin, (por ejemplo, el directorio home, el path). Tiene un significado similar al concepto de entorno de UNIX.
lpszCurDir: cadena con directorio y unidad de trabajo para el nuevo proceso.
lpsiStartInfo: apunta a una estructura bastante grande que no vamos a escribir. Los campos de dicha estructura dan informaciones al subsistema Win32 como por ejemplo si el proceso va a estar basado en GUI o en consola (GUI es con ventanas; consola es simulando el modo texto), el tamaño de la ventana inicial del proceso, las coordenadas de dicha ventana, su tamaño, su título …
En hprocess y hThread el Executive devuelve un par de descriptores a los objetos proceso y subproceso primario recién creados, y que servirán para hacer referencias a los mismos. Cada vez que el Executive crea cualquier objeto, asocia con dicho objeto un contador de uso. Cada vez que un proceso distinto usa un mismo objeto, incrementa el contador en 1. Cada vez que un proceso libera un objeto, decrementa el contador en 1. Cuando el contador de uso de un objeto es 0, el Executive destruye el objeto. Pues bien, cuando CreateProcess devuelve el control, los objetos proceso y subproceso primario tienen sus respectivos contadores con valor 2. De este modo, para que el Executive pueda destruirlos, habrán de ser liberados dos veces: una, cuando ellos mismos terminen (el contador pasaría a valer 1), y otra, cuando el proceso padre los libere (o sea, cuando cierre los descriptores; así, sus contadores valdrían 0 y 0, y el Executive los destruiría). De aquí es infiere que es vital que el proceso padre cierre esos descriptores (si no, no se destruirían los objetos y podría desbordarse la memoria). La llamada para cerrar descriptores es
BOOL CloseHandle (HANDLE hobject);
dwProcessId y dwThreadId son unos identificadores únicos que el Executive asocia al proceso y subproceso primario, respectivamente, análogos al PID en UNIX.
Hasta aquí la llamada al sistema que nos permite crear procesos. La llamada para finalizar el proceso es, afortunadamente, mucho más simple:
VOID ExitProcess (UINT fuExitCode); que devuelve el entero fuExitCode, que es un código de salida que el proceso envía antes de finalizar (que diga si ha finalizado con éxito, si no, etc). Cuando un proceso termina, se realizan las siguientes acciones:
Todos los subprocesos del proceso finalizan
Se cierran todos los descriptores de objetos del Executive y de Win32 abiertos por el proceso
El estado del objeto proceso para a estar señalado
El estado de terminación del proceso se pone al código de salida adecuado
Hemos dicho que el objeto proceso se pone a estado señalado. Un objeto puede tener dos estados: señalado y no señalado, estados que utiliza el sistema y los propios procesos para varias funciones. El curioso lector puede consultar el apartado de "Comunicación entre procesos".
Creación y destrucción de subprocesos
Análogamente a como nos hemos auxiliado de la llamada al sistema CreateProcess para comprender el mecanismo de creación de procesos, vamos a hacer lo propio para la creación de subprocesos. Un subproceso se crea cuando otro subproceso hace una llamada a:
HANDLE CreateThread (LPSECURITY_ATTRIBUTES lpsa, DWORD cbStack, LPTHREAD_START_ROUTINE lpStartAddr, LPVOID lpvThreadParm, DWORD fdwCreate, LPDWORD lpIDThread);
lpsa: tiene el mismo significado que en CreateProcess, es decir, un puntero a una estructura SECURITY_ATTRIBUTES, donde se especifican permisos del subproceso y si es heredable o no.
cbStack: vamos a aprovechar este parámetro para explicar la pila de un subproceso. Todo subproceso tiene una pila asociada situada en el espacio virtual del proceso al que pertenece. Virtualmente, la pila tiene un tamaño por defecto de 1 Mb (y además es el máximo; tamaños inferiores pueden ser indicados al enlazar la aplicación). De esa pila virtual el subproceso puede tener asignada en memoria un trocito. El tamaño de ese trocito viene dado por el parámetro cbStack, y por defecto es 1 página (4 Kb). Técnicamente, se dice que la pila tiene reservado un espacio de 1 Mb, y asignado un espacio de cbStack bytes. (el significado de ambos términos lo veremos detenidamente en el capítulo de administración de la memoria). Si el subproceso desborda su trocito de pila asignado en memoria se eleva una excepción; el Executive captura la excepción y asigna otros cbStack bytes en memoria física para la pila del subproceso. Por tanto, la pila crece dinámicamente en trozos de cbStack bytes. Lo más eficiente es que cbStack sea múltiplo del tamaño de la página.
lpStartAddr, lpThreadParm: ya dijimos que todo subproceso ejecuta una porción de código del proceso al que pertenece. Este parámetro apunta a la función que contiene el código a ejecutar por nuestro subproceso. Es posible hacer que varios subprocesos tengan la misma función asociada. El perfil de la función es fijo:
El parámetro lpvThreadParm de la función es justamente el mismo que se le pasa a CreateThread; de hecho, el sistema se limita a pasar dicho parámetro a la función asociada al subproceso cuándo éste comienza su ejecución.
El parámetro puede ser un valor de 32 bits o un puntero de 32 bits. Puede servir para dar algún tipo de inicialización al subproceso.
El parámetro dwresult de la anterior función es un código de salida que el subproceso devuelve cuando acaba de ejecutar código. Es similar al código que devuelven los procesos al acabar.
fdwCreate: si vale 0, entonces el subproceso comienza a ejecutarse en cuanto esté creado. Si vale CREATE_SUSPENDED, entonces se creará, pero automáticamente será suspendido.
lpIDThread: debe ser un puntero a una estructura DWORD, donde el Executive pondrá el identificador que le ha asignado al subproceso. Es obligatorio pasar una dirección válida.
El subproceso recién creado iniciará su ejecución inmediatamente antes del retorno de CreateThread (a menos que hayamos especificado el indicador CREATE_SUSPENDED).
Comunicación y Sincronización de Procesos Mediante Objetos
En Windows NT, los mecanismos clásicos de sincronización entre procesos (como los semáforos, las regiones críticas, los sucesos, etc.) son tratados como objetos. Es más, existen objetos no específicos de sincronización pero que también pueden ser usados con estos fines. Por tanto, vamos en primer lugar a enumerar todos los objetos de sincronización y a dar algunas características globales, para posteriormente adentrarnos a estudiar los más importantes.
Podemos sincronizar subprocesos mediante alguno de los siguientes objetos:
- Semáforos
- Mutexes
- Sucesos
- Archivos
- Procesos
- Subprocesos
- Entrada del terminal
- Notificación de cambio de archivo
Antes hemos mencionado las regiones críticas. Aunque Windows NT las incluye como mecanismo de sincronización, no las trata explícitamente como objeto. No obstante también las estudiaremos en este apartado por mantener la homogeneidad.
Como ya esbozamos en el capítulo de los procesos, en cualquier instante un objeto está en uno de dos estados: señalado (1) y no señalado (0). Cada estado tiene un significado dependiendo de la clase del objeto.
Por ejemplo, en el apartado anterior vimos que durante la vida de un proceso o un subproceso su estado era no señalado, pero que al morir pasaban al estado señalado. De aquí que ambos tipos de objetos sirvan para la sincronización. Por ejemplo, un subproceso A puede querer dormir hasta que otro proceso/subproceso B acabe; por tanto, A dormirá mientras el objeto asociado al B esté no señalado. En cuanto B pase a señalado, A despertará.
Igualmente, un subproceso se puede sincronizar con el fin de una lectura/escritura en un archivo. En general, cuando alguna de estas operaciones finalizan, el objeto archivo en cuestión pasa a estado señalado.
El objeto asociado a la entrada de teclado se pone señalado cuando existe algo en el buffer de entrada. La aplicación de este hecho para sincronización es evidente. Un subproceso puede así estar durmiendo mientras el buffer esté vacío.
El resto de objetos los veremos con más detalle a lo largo de este capítulo. Pero antes vamos a dar las llamadas al sistema que se utilizan para la sincronización con objetos.
Se trata de:
DWORD WaitForSingleObject (HANDLE hObject, DWORD dwTimeout);
Esta llamada simplemente mantiene al subproceso que la realiza dormido hasta que el objeto identificado por hObject pase al estado señalado. El parámetro dwTimeout indica el tiempo (en ms) que el subproceso quiere esperar por el objeto. Si vale 0, entonces la llamada sólo hace una comprobación del estado y retorna inmediatamente; si devuelve WAIT_OBJECT_0, el objeto estaba señalado; si devuelve WAIT_TIMEOUT, el objeto estaba no señalado. Si como tiempo metemos INFINITE, el subproceso dormirá hasta que el objeto pase a señalado. Hay un par de códigos de salida más que comentaremos en su momento (cuando expliquemos los mutex).
DWORD WaitForMultipleObjects (DWORD cObjects, LPHANDLE lpHandles, BOOL bWaitAll, DWORD dwTimeout);
Es parecida a la anterior pero da la posibilidad de esperar por varios objetos o bien por alguno de una lista de objetos. cObjects indica el número de objetos a comprobar. lpHandles apunta a una matriz que contiene descriptores a los objetos. El booleano bWaitAll indica si queremos esperar a que todos los objetos pasen a estado señalado o tan sólo uno, y dwTimeout es el tiempo a esperar. Si hay varios objetos que han pasado al estado señalado, la llamada coge sus descriptores, toma el menor y devuelve su posición dentro de la matriz (sumada a un código de retorno análogo a los de WaitForSingleObject).
El tema de sincronización está íntimamente relacionado con el de interbloqueos. Supongamos que tenemos dos subprocesos A y B compitiendo por dos objetos 1 y 2, esperando a que ambos estén señalados, para lo cual han hecho sendas llamadas a WaitForMultipleObjects. Supongamos que 1 pasa a estado señalado. Y supongamos que el Executive decidiera otorgar ese hecho al proceso A, colocando a 1 de nuevo a no señalado. Mientras tanto, 2 pasa también a señalado, y el Executive otorga este hecho a B, y también pone a 2 a no señalado. En esta situación, ninguno de los dos subprocesos podría terminar nunca, pues cada uno estaría esperando a que el objeto del otro pasara a señalado. Entonces A y B están interbloqueados. Para evitar esta situación, el Executive no entrega los objetos hasta que ambos estén señalados; en ese momento despertaría a uno de los subprocesos. El otro seguiría dormido hasta que el primer subproceso terminara de trabajar con los objetos.
A continuación se estudia cada uno de los objetos específicos de sincronización de Windows NT.
Secciones Críticas
Las secciones críticas son un mecanismo para sincronizar subprocesos pertenecientes a un mismo proceso, pero, como ya hemos indicado, no son objetos.
Una sección o región crítica (SC) es un trozo de código ejecutable tal que para que un subproceso pueda ejecutarlo se requiere que tenga acceso a una estructura de datos especial y compartida.
Dicha estructura es del tipo CRITICAL_SECTION, cuyos campos no son interesantes, y además no son accesibles directamente, sino a través de una llamada al subsistema Win32:
VOID InitializeCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
Veamos algunas llamadas para manejar las SC:
VOID EnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
VOID LeaveCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
La primera sirve para que un subproceso solicite entrar en una SC. La segunda permite salir de la SC a un subproceso que esté dentro de la misma.
La función de entrar opera así: la primera vez que es llamada, se registra en la estructura CRITICAL_SECTION un identificador del subproceso que la posee.
Si antes de que el subproceso la abandone, el SO entrega la CPU a otro (el caso más frecuente), y entonces ese otro subproceso hace la llamada para entrar en la SC, entonces la función ve que la estructura ya está en uso, con lo que el segundo subproceso sería puesto a dormir.
Cuando la CPU vuelva al primero y éste haga una llamada para salir, la estructura será asignada al segundo subproceso.
Si un subproceso vuelve a llamar a la función de entrar estando ya dentro de la SC, simplemente se incrementará un contador de uso asociado con el objeto SC. Más tarde, deberá hacer tantas otras llamadas a la función de salir para poner el contador a 0; en caso contrario, ningún otro subproceso podría ocupar la SC.
La estructura CRITICAL_SECTION y todos los recursos que el SO le hubiera asignado se pueden eliminar haciendo una llamada a
VOID DeleteCriticalSection (LPCRITICAL_SECTION lpCriticalSection);
Sería catastrófico usar esta función estando un subproceso dentro.
Exclusión Mutua (Mutex)
Los objetos exclusión mutua (abreviadamente mutex, de mutual exclusión) son muy parecidos a las SC, pero permiten sincronizar subprocesos pertenecientes a procesos distintos.
Se crean con la llamada:
HANDLE CreateMutex (LPSECURITY_ATTRIBUTES lpsa, BOOL fInitialOwner, LPTSRT lpszMutexName);
fInitialOwner: dice si el subproceso que crea el mutex ha de ser su propietario inicial o no. Si vale TRUE, entonces el estado inicial del objeto mutex sería no señalado, por lo que todo subproceso que espere por él sería inmediatamente puesto a dormir. Si vale FALSE, el mutex se crea con estado señalado, por lo que al primer proceso que estuviera esperando le sería asignado y podría continuar ejecutándose.
lpszMutexName: apunta a una cadena con el nombre que le hemos querido dar al objeto (o NULL si no se pone un nombre).
La llamada devuelve un descriptor al objeto creado.
Si otro subproceso llamara a la función pasándole el mismo nombre de mutex, el SO comprobaría que ya está creado y devolvería otro descriptor al mismo objeto.
HANDLE OpenMutex (DWORD fwdAccess, BOOL fInherit, LPTSTR lpszMutexName);
Esta función comprobaría si existe algún objeto mutex con nombre lpszMutexName. Si es así, devolvería un descriptor al mismo. Si no, devolvería NULL. El booleano fInherit permite que el mutex sea heredado por los subprocesos hijos.
Si el mutex no tuviera nombre, un subproceso podría obtener un descriptor al mismo llamando a DuplicateHandle.
Otra diferencia de los mutex con las SC (y en general con cualquier objeto de sincronización en Windows NT) es que un subproceso mantiene la propiedad de un mutex hasta que quiera liberarlo, pero hasta el punto de que, si el subproceso muere y no ha liberado al mutex, éste seguiría siendo de su propiedad.
Así, si un mutex está libre (señalado) y es tomado por un subproceso (pasa a no señalado), y dicho subproceso finaliza antes de liberarlo, el estado del mutex pasaría a señalado; los subprocesos que estuvieran esperando por el mutex serían despertados pero no se les asignaría el objeto a ninguno, sino que con el valor WAIT_ABANDONED de retorno de las llamadas WaitFor…Object(s) se les informaría de lo que ha sucedido, de que el mutex no ha sido liberado sino abandonado. Esto se considera como una situación de fallo en un programa.
Para liberar un mutex usaremos la llamada
BOOL ReleaseMutex (HANDLE hMutex);
Donde:
hMutex: es un descriptor al objeto. La función decrementa el contador de uso que el subproceso tiene sobre el mutex. Cuando sea 0, el objeto podrá ser asignado al primer subproceso que por él esté esperando, igual que con las SC.
Semáforos
Un semáforo es un objeto distinto de las SC y los mutex. A diferencia de ambos, el objeto semáforo puede ser poseído a la vez por varios subprocesos, y no posee dentro ningún identificador del subproceso/s que lo está usando. Lo que tiene es un contador de recursos, que indica el número de subprocesos que pueden acceder al semáforo. Cuando un subproceso toma el objeto semáforo, el SO mira si el contador de recursos del mismo es 0. De ser así, pone al subproceso a dormir. Si no es 0, asigna el semáforo al subproceso y decrementa el contador. Cada vez que un subproceso libera el semáforo, se incrementa el contador.
Un semáforo está señalado cuando su contador es mayor que 0, y no señalado cuando su contador vale 0.
Un semáforo se crea con la llamada:
HANDLE CreateSemaphore (LPSECURITY_ATTIBUTES lpsa, LONG cSemInitial, LONG cSemMax, LPTSTR lpszSemName);
cSemInitial es el valor inicial no negativo del contador de recursos.
cSemMax es el valor máximo que puede alcanzar dicho contador (por tanto 0 <= cSemInitial <= cSemMax)
lpszSemName es el nombre que le damos al objeto.
HANDLE OpenSemaphore (DWORD fdwAccess, BOOL fInherit, LPTSTR lpszName);
La semántica de esta llamada es análoga a la de OpenMutex.
Para incrementar el contador de referencia del semáforo se usa:
HANDLE ReleaseSemaphore (HANDLE hSemaphore, LONG cRelease, LPLONG lplPrevious);
Donde:
cRelease indica el número de veces que queremos incrementar el contador (el número de veces que liberamos el semáforo). A la vuelta de la función, tendremos en lplPrevious un puntero al valor anterior del contador. Por tanto, si queremos averiguar el valor del contador tenemos que modificarlo. Ni siquiera llamando a la función con cRelease igual a 0 podríamos saber el valor anterior sin modificar el semáforo, pues entonces la función devuelve 0 como dato apuntado por lplPrevious.
Sucesos.
Los sucesos son objetos utilizados para indicar a los subprocesos que ha ocurrido algo en el entorno. Se puede distinguir dos tipos de objetos suceso:
Sucesos con inicialización manual.
Sucesos con autoinicialización.
Cualquier objeto suceso podrá estar en estado señalado o no señalado. No señalado significa que la situación asociada con el objeto aún no ha ocurrido. Señalado indica que sí se ha producido.
Ambos tipos de objeto se crean con la misma llamada:
HANDLE CreateEvent (LPSECURITY_ATTIBUTES lpsa, VOOL fManualReset, BOOL fInitialState, LPTSTR lpszEventName);
fManualReset a TRUE le indicará al SO que queremos crear un suceso de inicialización manual, y a FALSE, un suceso de autoinicialización.
fInitialState indica el estado inicial del objeto; un valor TRUE significa que el suceso se creará como obejto señalado, y a FALSE como no señalado.
Como vimos con los otros tipos de objetos de sincronización, otros procesos pueden acceder al mismo objeto usando CreateEvent y el mismo nombre, o usando la herencia, o con DuplicateHandle, o con:
HANDLE OpenEvent (DWORD fdwAccess, BOOL fInherit, LPTSTR lpszName);
Sucesos con inicialización manual (manual reset)
Este tipo de objetos se usan para que, cuando el suceso ocurra, es decir, se ponga a señalado, todos los subprocesos que esperaban por ese suceso se despierten a la vez y todos puedan seguir ejecutándose. Por tanto, ahora las funciones WaitFor…Objext(s) no van a tocar el estado del objeto, sino que debe hacerlo el propio subproceso que creó el objeto. Dicho subproceso puede usar las llamadas:
BOOL SetEvent (HANDLE hEvent);
BOOL ResetEvent (HANDLE hEvent);
que ponen, respectivamente, el suceso identificado por hEvent en estado señalado y no señalado. O sea, con SetEvent indicaremos que la situación asociada al obejto se ha producido, y con ResetEvent indicaremos lo contrario.
Existe una llamada muy útil con este tipo de objetos:
BOOL PulseEvent (HANDLE hEvent);
que le da un "pulso" al suceso hEvent: lo pone en estado señalado, con lo que se libera a todos los subprocesos en espera (que se seguirán ejecutando), y entonces lo pone a no señalado, con lo que los subprocesos que a partir de ese momento esperen por el suceso serán dormidos. Por tanto, equivale a SetEvent + liberación + ResetEvent.
Sucesos con autoinicialización (auto-reset)
Con estos objetos las funciones WaitFor…Object(s) van a funcionar como con cualquier otro tipo de objeto. Por tanto, cuando la situación asociada con el suceso ocurra y éste se ponga a señalado (con SetEvent), sólo uno de los subprocesos que estaban esperando podrá continuar, y su función WaitFor…Object(s) correspondiente pondrá el estado del objeto a no señalado. La función ResetEvent no tendría efecto aquí. La función PulseEvent pondría el suceso a señalado, liberaría un sólo subproceso de los que están esperando, y pondría el suceso a no señalado.
Página siguiente |