Implementación de Threads – ULT y KLT

Los hilos o Threads descriptos en los post anteriores, tienen más de una implementación en la realidad. En forma general, existen dos tipos o dos clases de Threads, aquellos que se manejan a nivel Kernel y los que se manejan a nivel de Usuario.

¿Qué es esto de los niveles del Kernel y de Usuario?

En el mundo de los sistemas operativos existen básicamente dos niveles de operación, el nivel del Kernel o nivel de Sistema Operativo, y el nivel de Usuario, en donde se ejecutan la mayoría de las aplicaciones, entre ellas las que usamos diariamente o mismo las que nosotros fabricamos.

A nivel Kernel, se ejecutan todas las aplicaciones que tengan un cierto grado de compromiso con la integridad del sistema, y aquellos servicios de acceso a todos los dispositivos con los que cuenta el sistema en principio (este tema da para extenderse mucho más, y de seguro será tema de un futuro post :-)).

En el nivel de usuario, es en donde se ejecutan casi todas las aplicaciones de un sistema operativo actual. La manera de acceder a los dispositivos o servicios que ofrece el sistema operativo, generalmente es mediante lo que se conoce como System Call o llamada al sistema, para los amigos: syscall ;-). De esta forma se logra un cierto control sobre los recursos que posee el sistema operativo, ya que las aplicaciones de usuario sólo podrán acceder a los dispositivos o servicios del sistema mediante estos procedimientos. El tema con estas llamadas al sistema es, que muchas de ellas son bloqueantes, ¿y que quiere decir el término bloqueante?: una vez que un proceso pide un recurso de entrada salida por ejemplo, en el caso  de llamar a fread() de C, todo el proceso pasa a estar bloqueado en espera de la respuesta desde la controladora de la unidad de disco, mientras el planificador de tareas (task scheduller) sede el procesador a otro proceso en la lista de espera.

Entonces, volviendo a la implementación de los Threads, los KLT van a tener soporte del sistema operativo, el mismo va a conocer y a disponer de los servicios necesarios para trabajar con Threads. Esto tiene como ventaja que cuando un hilo solicite un recurso de entrada salida, no se bloqueará todo el proceso, sino que sólo el Thread o hilo que invoca este servicio del sistema operativo permanece a la espera de una respuesta por parte de la controladora del dispositivo.

Los ULT, están implementados desde una biblioteca creada a nivel de usuario que se encarga de todos los aspectos de la gestión de los hilos. En este caso, el sistema operativo no tiene por que conocer acerca de la existencia de los hilos en un determinado proceso, ya que este mismo se encargará de gestionar el funcionamiento de los hilos. La ventaja aquí es la no intervención del sistema operativo en esta tarea, lo cual implica la no existencia del cambio de contexto, o context switch (lo cual si existe para los KLT), esto reduce el overhead, pero a cambio, si un ULT pide un recurso de entrada/salida mediante una syscall bloqueante, hará que todo el proceso en donde este reside se bloquee también, ya que el sistema operativo no sabe que existen los hilos dentro de este proceso, y lo único que ve es que dicho proceso pide entrada salida, entonces lo bloquea por completo hasta obtener una respuesta por parte del dispositivo. Existen técnicas que permiten manejar el bloqueo a nivel proceso para poder gestionarlo internamente a nivel hilo de usuario, una de estas técnicas se conoce como Jacketing, básicamente consiste en no pedir de forma directa un recurso que provocará el bloqueo del proceso. Los ULT utilizan en lo posible syscalls no bloqueantes, las cuales se ejecutaran de forma asincronica, estas se manejan mediante una pequeña rutina de consulta que verificará el estado del dispositivo solicitado, y si este se encuentra ocupado, pasará el control a otro hilo en espera, y cuando le llegue nuevamente el turno del hilo que solicitó acceso al dispositivo, este repetirá nuevamente la consulta. Este procedimiento se llevará a cabo hasta que el dispositivo haya terminado con lo que se le pidió.

Un aspecto importante es que los ULT no permiten aprovechar el multiprocesamiento, es decir, no permiten la ejecución de dos o más ULT en procesadores separados (en paralelo), esto se debe a que el Kernel del sistema operativo que no conoce el manejo de Threads que realiza el proceso, asignará un proceso distinto a cada procesador, y como los hilos viven dentro de los procesos, no será posible llevar a cabo la paralelización.

Esto no sucede con el manejo de los KLT, ya que el Kernel conoce como manejar los hilos, y le es posible repartirlos entre los distintos procesadores de estar disponibles.

¿Pero entonces cuáles son más rápidos? ¿Qué es mejor, KLT o UTL?

Bueno la respuesta es: depende, ¿de qué depende?, y… de la naturaleza de la aplicación que se desea implementar.  Los KLT pierden tiempo en el cambio de contexto, lo cual no pasa con los ULT, pero los KLT permiten el uso de múltiples procesadores, lo cual no es posible con los ULT. Como se ve, todo depende de la aplicación en particular de la cual estemos hablando.

Existe también una combinación de ambas implementaciones, por ejemplo Solaris utiliza ambas, KTL y ULT, lo cual permite sacar provecho de las ventajas de una y de la otra, y así lograr un equilibrio que permita obtener un buen rendimiento. Windows 2000/XP, Linux en sus versiones actuales hacen uso de KLT.

Bueno, eso es todo por hoy, a quien le interese seguir leyendo:

  • Sistemas Operativos, de William Stallings
  • Sistemas Operativos, de Silberschatz Galvin
  • Sistemas Operativos, diseño e implentación, de Andrew S. Tanenbaum, Albert S. Woodhull
  • UNIX network programming – Volumen 1 (en especial el capítulo 6 en donde habla del uso de Multiplexacion de Entrada/Salida y Entrada Salida asincrónica), de W. Richard Stevens, Bill Fenner, Andrew M. Rudoff

Todos son buenas opciones para seguir leyendo sobre el tema.

Será hasta el próximo post.

Javier.

Categorías:Sistemas Operativos Etiquetas: ,

Threads y Procesos

En el post anterior veíamos la diferencia entre ejecutar tareas o procesos de forma concurrente y paralela. En este post vamos a ver: ¿Qué son los procesos? ¿Qué son los Threads? ¿Qué diferencias hay entre usar Threads o Procesos para agilizar las cosas? ¿En qué casos es más conveniente hacer uso de uno, o el otro o ambos?.

¿Qué son los procesos?

En la jerga de los sistemas operativos, básicamente se le llama proceso a aquel programa que ha pasado por todas las etapas de compilación y link-edición y se encuentra almacenado en memoria listo para poder ejecutarse. Los procesos entonces son unidades de ejecución en un sistema operativo, y como tal tienen una serie de estructuras de datos asociadas que guardan información ya sea de control, estado, acerca de sus ancestros, registros del procesador, área de memoria, stack, privilegios, etc. Dichas estructuras de datos suelen conocerse como PCB (Process Control Block) o Bloque de Control de Procesos, suelen variar en algunos campos según el sistema operativo en el cual se esté ejecutando un determinado proceso, pero en sí la estructura es básicamente la misma.

Como vimos en el post anterior, a veces es necesario ejecutar tareas/procesos de forma concurrente o paralela, para lo cual supongamos que contamos con un proceso, que en un determinado momento crea otro proceso (un proceso hijo) que se encargará de llevar a cabo una tarea de forma concurrente o paralela a la tarea padre. Generalmente lo que hace el sistema operativo en estos casos es crear un nuevo proceso en base al actual, al padre, para esto el sistema operativo debe mantener ahora dos copias (totalmente separadas, cada una con su propia área de memoria) de un mismo proceso, que claramente desde el momento en que el nuevo proceso es creado, variará en algunos aspectos en función del tiempo a medida que ambos se vayan ejecutando.

Supongamos que el proceso padre se encuentra ejecutando, y de repente necesita leer varias líneas de un archivo alojado en uno de los discos locales. Dependiendo de la política que utilice el panificador de procesos del sistema operativo, el proceso que acaba de pedir un recurso de entrada salida, se bloqueará a la espera de noticias acerca de lo que acaba de pedir, cediendo su lugar en el procesador a un nuevo proceso en espera. Supongamos que este proceso en espera es el hijo que el anterior había creado en su momento, entonces el sistema operativo lo quita de la cola de espera y le sede el procesador para que pueda trabajar.

Todo este conjunto de pasos para la administración de los procesos por parte del sistema operativo lleva un tiempo, que no es para nada despreciable, si bien se trata de que sí lo sea, esto consume tiempo que generalmente se suele conocer como Overhead del sistema operativo. El overhead habla del tiempo de ejecución consumido por el sistema operativo, y resulta que el proceso de transferir un proceso en ejecución a un estado bloqueado, y viceversa, implica un movimiento en las estructuras de datos mantenidas por el sistema opertivo correspondiente a los procesos involucrados (PCB, TSS o task state segment, etc.), este pasaje de datos es lo que en mayor medida genera este overhead del que venimos hablando.

¿Qué son los Threads o Procesos Livianos? ¿Qué diferencias hay entre usar Threads o Procesos para agilizar las cosas?

Con el tiempo, se vio que este tipo de estructuras, en algunas ocasiones podrían reducirse, de forma tal de disminuir el overhead generado por la creación de procesos y el cambio de contexto mencionado anteriormente, añadiendo al paquete alguna que otra útil prestación. Debido a esta y algunas otras necesidades, nacieron los Threads (Hilos) o Procesos Livianos, Livianos precisamente por que su estructura (TCB o Thread Control Block, Bloque de Control de Hilos) ahora es más liviana, contiene menos datos, pero no por que se hayan obviado, sino por que ahora gran cantidad de la porción de memoria de cada proceso se comparte con los Threads, esto quiere decir que los Threads van a vivir dentro de los procesos. Normalmente uno puede ver a un proceso como contenedor de hilos, en C/C++ un proceso trivial tiene un único hilo, main(), y luego los demás que el proceso, o mismo otros Threads, vayan creando a su gusto/necesidad. De esta manera, ahora los context switch pasan a ser context switch livianos, que van a ser más rápidos debido al tamaño de la estructura TCB más compacta que la de los procesos (PCB). Así, en algunos casos, es posible reducir el overhead del sistema operativo reemplazando la creación de procesos hijo, por Threads.

Pero esto no significa que los Threads reemplazaron a los procesos, muchas veces y por ciertas razones, es necesario crear procesos que ejecuten de forma concurrente/paralela en lugar de Threads. Un aspecto que puede dar una pista acerca de esto es el hecho de que cuando un proceso finaliza, sea cual sea la razón, los Threads que lleva dentro también finalizan, en cambio esto no sucede así con los procesos hijo.

¿En qué casos es más conveniente hacer uso de Threads, o Procesos emparentados o ambos?

Apache, el servidor HTTP más utilizado en la red por los proveedores de hosting y las demás empresas, posee dos versiones, una de estas maneja a sus clientes haciendo uso de hilos separados de ejecución (MPM Worker), y la otra hace uso de procesos emparentados (MPM Prefork). En este caso es visible la diferencia, la velocidad de respuesta se incrementa notablemente en el caso de la versión Worker, es mucho más ventajoso utilizar esta última en lugar de Prefork. A continuación hay un enlace hacia un benchmark de Apache Worker vs. Prefork:

http://www.camelrichard.org/apache-prefork-vs-worker

(en inglés)

En un pequeño resumen se puede ver para Prefork:
[...]
Transactions: 6045 hits
Availability: 100.00 %
Elapsed time: 300.38 secs
Data transferred: 0.25 MB
Response time: 0.50 secs
Transaction rate: 20.12 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 9.97
Successful transactions: 6045
Failed transactions: 0
Longest transaction: 10.13
Shortest transaction: 0.00
[...]

Y para Worker:
[...]
Transactions: 11024 hits
Availability: 100.00 %
Elapsed time: 300.24 secs
Data transferred: 0.46 MB
Response time: 0.27 secs
Transaction rate: 36.72 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 9.91
Successful transactions: 11024
Failed transactions: 0
Longest transaction: 11.92
Shortest transaction: 0.00
[...]

La diferencia en velocidad es de aproximadamente el 55% para Worker por sobre Prefork, lo cual es lógico según lo planteado anteriormente.

Así como este hay varios ejemplos, pero también está el hecho en el cual es necesario utilizar procesos  emparentados, por ejemplo, un proceso en un determinado momento recibe la orden del usuario por medio del teclado de ejecutar un comando, por ejemplo el shell BASH de Linux, en donde uno escribe un determinado comando y este se ejecuta entregando un determinado resultado, esto suele hacerse creando un nuevo proceso hijo, al cual inmediatamente “se lo rellena” con el comando solicitado por el usuario, y este corre de forma concurrente/paralela a la consola o shell. Siguiendo con el ejemplo anterior de Apache, hay veces en las que debido a la naturaleza de las aplicaciones que este va a ejecutar, por ejemplo para utilizar Apache 2 con PHP5 como módulo, es necesario utilizar la versión Prefork, ya que PHP5 cómo módulo de apache no admite la versión Worker, para que esto sea así, sólo a modo informativo, se debe instalar la versión CGI/Fast CGI de PHP5.

Cómo estos hay muchos otros casos en donde dependiendo de las necesidades es más productivo utilizar Threads que Procesos emparentados y viceversa.

Bueno, espero que les haya sido interesante el tema, en el próximo post veré de plantear ejemplos de uso tanto de Threads como de Procesos emparentados en diferentes lenguajes, e iremos viendo que problemas se nos presentan en el uso e interacción de cada uno de estos.

Hasta el próximo Post =)

Javier.

Categorías:Sistemas Operativos Etiquetas: ,

Concurrencia vs Paralelísmo

En la vida de todo programador siempre llega el momento en que es necesario hacer uso de la programación concurrente y/o parelela, en donde es necesario, dada una cierta clase de problemas, ejecutar dos o mas tareas o procesos de forma casi, o en algunos casos, totalmente independiente.

Supongamos que, no sé si la mayoría sabe de qué se trata un WhoIs, pero bueno, en pocas palabras un WhoIs es una simple aplicación que verifica la disponibilidad de un nombre de dominio cualquiera. La mayoría de las empresas de Hosting ofrecen este servicio, de manera tal que cuando un cliente decide comprar un determinado plan de hosting, puede a su vez verificar si su nombre de dominio preferido está disponible o no, y hacer todo esto en el mismo paso de compra.

En los sistemas UNIX, por ejemplo en Linux, cualquiera sea la distribución, supongamos Ubuntu, existe un comando precisamente llamado whois, que permite hacer este tipo de consulta de nombre de dominio y alguna que otra cosa más, en fin, el tema es que si necesitamos hacer un WhoIs y contamos con un servidor, o una cuenta en un servidor con sistema operativo Linux, en principio podemos hacer uso de este comando (para ver más, pueden buscar en las páginas del manual de Linux, el “man”, escribiendo en la línea de comandos del shell que estén utilizando, el comando man whois).

Entonces, a qué va todo esto?. Pensemos en una simple aplicación Web, con un formulario HTML que tenga un campo de texto y un botón de tipo submit, y detrás un script (ya sea en PHP, Python, Perl, etc.) del lado del servidor, que mediante la validación correspondiente sobre los datos enviados realice esta consulta, y que luego muestre los resultados. Hasta aquí la tarea es simple, pero resulta que quiero darle más opciones al cliente, quiero que cuando busque un determinado dominio, se le den ciertas alternativas, para que en caso de no haber disponibilidad, este tenga diferentes opciones de disponibilidad de ese mismo dominio en otra extensión o TLD.

Ya en este caso, nuestro supuesto script va a necesitar ser modificado para que pueda realizar N consultas, una para el TLD que ingresó el cliente, y otras N-1 más para las restantes que se le mostrarán como opción. Entonces, suponiendo que la consulta para un único dominio, se demora unos 3 o 4 segundos, dependiendo del estado de la red, y teniendo en cuenta que se pudo establecer conexión con el servidor de WhoIs, y que además no hubo ningún error en el medio, esto mismo, en un esquema de ejecución secuencial, andaria por los 3 o 4 segundos para cada consulta, que multiplicado por la cantidad de TLD que deseo buscar, suponiendo 10 TLD, ya andaríamos por los 30 o 40 segundos.

Y entonces?, que pasó?, esto es mucho o poco?. Y… hoy en día hacer esperar a un cliente más de 10 segundos por la respuesta de un formulario, es sinónimo de que cerró la ventana o se fue a otra página por que se aburrió esperando :-(

Pero… entonces, que pasaría si pudiese ejecutar de forma concurrente esas consultas o requests?, y… automáticamente aprovecharía más los recursos, y en el tiempo en que el script espera a la respuesta de la consulta, puede ir haciendo las demás, y así solapar consultas y ahorrar mucho tiempo, esto no quiere decir que vaya a tardar lo que una única consulta, pero lo que sí significa es que se va a ahorrar muchisimo tiempo, de seguro mucho más que en el caso secuencial. De esta forma no aburriría a los usuarios mientras esperan la respuesta del WhoIs :-D

Casos como este, hay muchísimos, la mayoría de las tareas que exigen una espera ya sea sincrónica (por un tiempo definido) o asincrónica (por un tiempo indefinido), son posibles de separar de forma de ahorrar tiempo y aprovechar mejor los recursos de los que disponemos, y este es un poco el principio de la programación concurrente, arma poderosísima que hoy en día utilizan los programadores para hacer sus aplicaciones más robustas, y con un mejor aprovechamiento de los recursos de los que disponen, más aún con el Boom de los procesadores de varios núcleos, en donde en cierto modo es posible paralelizar literalmente tareas/procesos.

Como verán, hice diferencia acerca de tareas/procesos concurrentes y tareas/procesos paralelos. Cúal es la diferencia entre ambos?, es muy simple, hacer algo de forma concurrente, implica realizar un poco de cada tarea de forma tal que parezca que se realizan a la vez, este principio básicamente es el que usan los sistemas operativos que sólo cuentan con un único procesador, tratan de administrar las tareas de forma tal que parezca que se ejecutan a la vez. En cambio, cuando hay más de un procesador presente, los sistemas operativos modernos, se dan cuenta de esto, y en algunos casos, en donde es posible, ejecutan tareas en paralelo, es decir, realmente de forma simultánea. Esta en principio es la diferencia fundamental entre concurrencia y paralelismo.

Bueno, espero que les haya resultado interesante el tema. Esto sigue con la presentación de los Threads o Hilos, y su comparación con los Procesos. ¿Qué son?. ¿Qué diferencias hay entre usar Threads o Procesos para agilizar las cosas?. ¿En qué casos es más conveniente hacer uso de uno, o el otro o ambos?.

Hasta el próximo Post =)

Javier.

Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.