Clase 04: Procesos PROCESOS Y THREADS El concepto de proceso se origina en el campo de los sistemas operativos, donde comúnmente se define como un programa en ejecución. Desde la perspectiva de un sistema operativo, la administración y programación temporal de los procesos es tal vez el aspecto más crítico. Sin embargo, en los sistemas distribuidos hay otras cuestiones que son igualmente importantes. Por ejemplo, para organizar eficientemente sistemas cliente-servidor, frecuentemente resulta conveniente utilizar técnicas multithreading (multihilo). La principal contribución de los threads (hilos) en los sistemas operativos es que permiten que clientes y servidores sean construidos de tal manera que la comunicación y el procesamiento local puedan traslaparse en el tiempo, permitiendo con ello mayores niveles de desempeño. Aunque los procesos constituyen el bloque básico en la implementación de sistemas distribuidos, la práctica muestra que el plantear la granularidad (división) de un sistema distribuido en diferentes procesos, tal como lo establecen los sistemas operativos en los que se construyen los sistemas distribuidos, no es suficiente. Resulta que plantear una granularidad más fina en la forma de threads múltiples de control por proceso, hace más fácil desarrollar aplicaciones distribuidas y obtener un mejor desempeño. 4.1 Introducción a Procesos y Threads Para entender el papel que juegan los threads en los sistemas distribuidos, es importante entender qué es un proceso, y como se relacionan los procesos y los threads. Al ejecutar programas, el sistema operativo crea un cierto número de procesos virtuales, cada uno para correr un programa diferente. Con la finalidad de mantener un control y seguimiento de todos estos procesadores virtuales, el sistema operativo usa una tabla de procesos, la cual contiene entradas para almacenar los valores de los registros del CPU, mapas de memoria, archivos abiertos, información del proceso, privilegios, etc. Un proceso es frecuentemente definido como un programa en ejecución, es decir, un programa que está siendo ejecutado en uno de los procesadores virtuales del sistema operativo. Un asunto importante es que el sistema operativo tiene mucho cuidado de asegurar que procesos independientes no puedan de forma maliciosa o inadvertida afectar el comportamiento adecuado de los demás. En pocas palabras, procesos múltiples puedan compartir concurrentemente el mismo CPU y otros recursos de hardware de forma transparente. Usualmente, el sistema operativo requiere de hardware que permita esta separación. Esta transparencia de concurrencia tiene un alto costo. Por ejemplo, cada vez que un proceso es creado, el sistema operativo debe crear un espacio de direcciones independiente y completo. La asignación de este espacio de memoria puede requerir de la inicialización de segmentos de memoria, por ejemplo, poner en ceros todo el segmento de datos, copiar el programa asociado en el segmento de texto (instrucciones), y preparar un segmento de stack para datos temporales. También resulta costoso el que el CPU realice un switch (cambio) de contexto (valores de registros, contador de programa, Clase 04: Procesos apuntador de stack, etc.) para cambiar de la ejecución de un proceso a otro. Aparte de grabar el contexto del CPU, el sistema operativo debe modificar los registros de una Unidad de Administración de Memoria (MMU) e invalidar cachés de traducción de direcciones, tales como el buffer de traducción de direcciones virtuales (TLB – Translation Lookaside Buffer). Además, si el sistema operativo brinda soporte a más procesos de los que pueden sostenerse simultáneamente en la memoria principal, éste debe hacer un swap (cambio) de procesos entre la memoria principal y el disco antes de que se realice el switch de contexto. Un proceso sencillo puede contener varios programas ejecutables conocidos como threads (hilos), que trabajan de manera conjunta como un todo coherente, cooperando entre sí (ver Figura 4.1). En un programa o proceso, por ejemplo, un thread podría manejar señales de error, otro podrí enviar un mensaje al usuario sobre ese error, mientras que un tercer thread podría ejecutar la tarea principal del programa. Figura 4.1. Diferentes esquemas de uso de threads en procesos. Un thread resulta de la división de un programa en dos o más tareas que pueden correr concurrentemente. Múltiples threads pueden existir dentro de un mismo proceso y, por lo tanto, comparten entre sí los mismos recursos, tales como el espacio de memoria del proceso al que pertenecen. Las diferencias principales entre procesos y threads son las siguientes: Los procesos son típicamente independientes, mientras los threads existen como subconjuntos de los procesos. Los procesos contienen una cantidad considerable de información de estatus o contexto, mientras los threads múltiples dentro de un proceso comparte este estatus al igual que la memoria y otros recursos (ver Figura 4.2). Clase 04: Procesos Los procesos tienen espacios de direcciones separados entre sí, son independientes. Los threads comparten el mismo espacio de direcciones (ver Figura 4.2). Los procesos interactúan entre sí solo mediante mecanismos de comunicación interproceso (IPC), lo cual se explicará con mayor detalle más adelante. El cambio de contexto (context switching) entre threads en el mismo proceso es típicamente más rápido que el cambio de contexto entre procesos. Al igual que un proceso, un thread ejecuta su propia parte de código, independientemente de los otros threads. Sin embargo, en contraste con un proceso, el thread no intenta obtener un alto grado de transparencia de concurrencia si esto implica una degradación de desempeño. Por lo tanto, un sistema de threads generalmente mantiene solo el mínimo de información para que un CPU pueda ser compartido por varios threads. En particular, un contexto de thread comúnmente consiste simplemente de un contexto de CPU y de información para la administración de threads. Figura 4.2. Información de los contextos de un proceso y un thread (hilo). Hay dos implicaciones importantes del uso de threads al momento de implementar una aplicación. Primero que todo, el desempeño de una aplicación multithread difícilmente es peor que su contraparte de thread sencillo (proceso simple). De hecho, en muchos casos, el uso de thread múltiples permite obtener mejores niveles de desempeño. Segundo, ya que los threads no son protegidos automáticamente unos de otros como en el caso de los procesos, el desarrollo de aplicaciones multithread requiere de un esfuerzo intelectual adicional. 4.2 Niveles de Threads Existen dos categorías básicas de threads: Threads de Nivel-Usuario: implementados generalmente por medio de librerías de threads. Threads de Nivel-Kernel: implementados mediante llamadas a sistema. Clase 04: Procesos De la combinación de ambas categorías se deriva una tercera conocida como Threads de Nivel-Hibrido o Combinados. La Figura 4.3 muestra el esquema de cada uno de estas tres categorías de threads. Figura 4.3. (a) Threads Nivel-Usuario, (b) Threads Nivel-Kernel, (c) Threads Hibridos o Combinados. Thread Nivel-Usuario (ULT) En este nivel, el Kernel no tiene la noción de la existencia de los threads. Toda la administración de los threads es realizada por la aplicación usando una librería de threads. El cambio de contexto de threads no requiere de privilegios para el modo kernel (no hay cambio de modo) y la calendarización o programación en tiempo depende específicamente de la aplicación. Actividad del Kernel en threads de nivel-usuario: El Kernel no sabe de la actividad de los threads pero aún administra la actividad de procesos. Cuando un thread hace una llamada a sistema (system call), el proceso por completo será bloqueado (*), pero para la librería de threads ese thread aún está corriendo (en estado de correr - running). Los estados de los threads son independientes de los estados de los procesos. NOTA (*): Recuerde que un proceso puede estar en distintos estados dentro de su vida (mientras existe). Uno de estos estados es el de “bloqueado”, lo cual significa que se encuentra en espera de que ocurra un evento específico para poder continuar su procesamiento. Clase 04: Procesos Ventajas: El cambio de contexto de los threads no involucra al kernel, no se requiere cambio de modo. La calendarización puede ser específica a la aplicación, se puede seleccionar el mejor algoritmo. Los threads nivel-usuario pueden correr en cualquier sistema operativo, solo se requiere una librería de threads. Desventajas: La mayoría de las llamadas a sistema son de bloqueo y el Kernel bloquea a los procesos, entonces todos los threads dentro del proceso también serán bloqueados. El Kernel solo puede asignar procesos a procesadores, por lo que dos threads dentro del mismo proceso no pueden correr simultáneamente en dos procesadores. Thread Nivel-Kernel (KLT) En este nivel, toda la administración de los threads se efectúa en el Kernel, no se usa librería de threads pero sí debe existir llamadas a sistema dirigidas a los servicios de threads que provee el Kernel. El Kernel mantiene la información de contexto tanto de procesos como de threads, y el cambio de contexto de threads requiere que la calendarización que establece el Kernel sea basada en threads. Ventajas: El Kernel puede calendarizar simultáneamente varios threads del mismo proceso en varios procesadores, el bloqueo se efectúa a nivel thread y no a nivel proceso. Las rutinas del Kernel pueden ser multithread. Desventajas: El cambio de contexto entre threads del mismo proceso involucra al Kernel. Si se tienen dos cambios de modo por cambio de contexto de switch, se aletarga la respuesta del sistema. Thread Hibrido o Combinado La idea es combinar lo mejor de cada categoría anterior. El sistema operativo solaris, UNIX de Sun Microsystems, es un buen ejemplo de sistema operativo que da soporte a threads hibridos. La creación de threads tiene lugar en el espacio de usuario (modo de usuario). Las tareas de calendarización y sincronización de threads se realiza en el espacio de usuario. El programador puede ajustar el número de KLTs. El Proceso incluye el espacio de direcciones de usuario, stack y bloqueo de control de proceso. Los ULTs (librerías de threads) invisibles al sistema operativo son la interfaz para el paralelismo de la aplicación. Los KLTs son la unidad que puede ser despachada (asignada) a un procesador. Clase 04: Procesos Cada proceso de peso ligero (LWP – Lightweight Process) (**) soporta uno o más ULTs y es mapeado a solo un KLT. NOTA (**): Un proceso de peso ligero (LWP) es un tipo específico de thread nivel-kernel (KLT) que comparte el mismo estado e información.Un LWP es un medio para implementar el multitasking. Un LWP corre arriba de un thread nivel-kernel sencillo y comparte su espacio de direcciones y recursos de sistema con otros LWPs que pertenecen al mismo proceso. El LWP puede generar múltiples threads de nivel-usuario (ULTs), permitiendo con ello el multitasking a nivel usuario, lo cual puede traer algunos benecifios de desempeño. La Figura 4.4 muestra lo anteriormente dicho. Figura 4.4. Implementación de threads en el sistema operativo Solaris de Sun Microsystems. 4.3 Uso de procesos y threads en sistemas no distribuidos Las aplicaciones grandes son frecuentemente desarrolladas como una colección de programas que colaboran entre sí, y cada uno de los cuales es ejecutado por un proceso separado. Este método es típico en un ambiente UNIX. La cooperación entre procesos es comúnmente implementada por medio de mecanismos de comunicación interproceso (IPC). En los sistemas UNIX, estos mecanismos típicamente incluyen pipas, filas de mensaje, segmentos de memoria compartida, sockets, RPC’s, etc. Una de las principales desventajas de todos los mecanismos IPC es que la comunicación requiere Clase 04: Procesos frecuentemente de una cantidad considerable de cambios de contexto, los cuales se dan en tres puntos diferentes, tal como se muestra en la Figura 4.5. Figura 4.5. Cambio de contexto (context switching) como resultado de un IPC. Ya que un IPC requiere de la intervención del kernel, un proceso generalmente tiene que cambiar de modo de usuario a modo de kernel, lo cual se muestra en el punto S1 de la figura anterior. Esto requiere cambiar el mapa de memoria en el MMU, y también desalojar el TLB. Dentro del kernel, se efectúa un cambio de contexto de proceso (punto S2 en la gráfica anterior, caso ilustrado también en la Figura 4.6a), después de lo cual el otro proceso puede se activado al cambiar de modo kernel al modo usuario, de nueva cuenta (punto S3). El último cambio requiere una vez más el cambiar el mapa del MMU y desalojar el TLB. En lugar de usar procesos, una aplicación puede ser construida de tal manera que partes diferentes de la misma sean ejecutadas por threads separados. La comunicación entre estas partes se implementa completamente con el uso de datos compartidos. El cambio de contexto de threads (threads switching) puede hacerse frecuentemente en el espacio de usuario, aunque el kernel esté al tanto de los threads y los controle y programe en el tiempo (ver Figura 4.6). El resultado puede ser una mejora dramática en el desempeño. Una ventaja de una granulación más fina, mediante la división de la aplicación en múltiples threads (multithreading) es que hace posible el explotar el paralelismo cuando se ejecuta un programa en un ambiente multitasking, y, aún más, en una máquina multiprocesador (existen sistemas operativos que dan soporte a ambos ambientes, multitasking y multiprocesador a la vez). En el caso multitasking, el tiempo de CPU es dividido entre threads y no entre procesos (ver Figura 4.6b). En el caso del sistema multiprocesador, cada thread es asignado a un CPU diferente mientras que los datos compartidos son almacenados en memoria principal compartida (ver Figura 4.6c). Cuando el diseño es apropiado, tal Clase 04: Procesos paralelismo puede ser transparente: el proceso correrá igualmente bien en un sistema uniprocesador, aunque un poco más lento. El uso de multithreading para la implementación de paralelismo se ha vuelto cada vez más importante y extendido, gracias a la disponibilidad de estaciones de trabajo multiprocesador cuyos precios se han reducido significativamente. Estos sistemas de cómputo son típicamente usados para correr servidores en aplicaciones cliente-servidor. Figura 4.6. (a) Multitasking basado en procesos (procesos con un solo thread), (b) Multitasking basado en threads (procesos con multithreads), (c) Procesos con multithreads en un ambiente multiprocesador. Finalmente, hay una razón de la ingeniería de software para usar threads: muchas aplicaciones son más sencillas de estructurar como una colección de threads cooperativos. Por ejemplo, en el caso de un procesador de palabras (ver Figura 4.7), se pueden usar threads separados para manejar la entrada de usuario en el teclado, revisar la ortografía y gramática, grabar el documento en disco, etc. Clase 04: Procesos Figura 4.7. Esquema de un procesador de palabras dividido en threads. 4.4 Uso de threads en sistemas distribuidos A continuar …