Indice
1.
Prólogo
3. Diseño De Windows
Nt
4.
Procesos
5. Administracion De La
Memoria
6. Sistema De
Archivos
7. Entrada Y
Salida
8. Windows Nt
5
9. 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 | Clase normal en segundo | 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 |