Consideraciones sobre las hebras

De Departamento de Informatica
Saltar a: navegación, buscar

Hay varios pequeños, pero importantes temas que hay que considerar y aprender cuando hablamos de Hebras, sobre todo cuando trabajamos, estudiamos y diseñamos aplicaciones Multihebra. Consideraciones sobre hebras toma en cuenta los temas de como tratar, implementar, como se comportan las hebras, entre otros detalles.

Contenido

Las llamadas al sistema fork() y exec().

Ejemplo duplicación de hebras al aplicar 2 fork

Estas dos llamadas al sistemas son útiles cuando se trabaja con aplicaciones multihebra. Un programa secuencial de una sola hebra que trate millones de datos, si lo transformamos en multihebra, lo hará en un tiempo mucho menor. fork() y exec() son llamadas al sistema que nos ayudan a hacer este tipo de programas.

  • fork() : Lo que hace la llamada al sistema fork, es clonar (duplicar) hebras. ¿Se duplican todas las hebras?, Eso depende del la versión del sistema UNIX que estemos ocupando, hay algunos sistemas que tienen 2 opciones de fork, una que duplica todas las hebras. En general si hacemos un programa que contenga una llamada al sistema fork, el proceso hijo ejecutará solo una parte del total del código, pero aún así el total del codigo estará duplicado en memoria por tantos hijos se tengan. (trataremos solo con los fork() los cuales duplican las hebras del proceso quien invocó la llamada al sistema). Es importante señalar también que fork() es la única forma de crear nuevos procesos, menos el "primer proceso", que se crea por defecto.

En el diagrama de duplicacion de hebras, podemos notar a grandes rasgos como van creciendo las hebras a medida que realizamos forks'.

Abajo se muestra un sencillo ejemplo de como funcionaria un fork() en C, para que quede un poco mas claro su funcionamiento. (se sugiere compliar con gcc)

/*
* Ejemplo de uso de fork() en lenguaje C, extraído desde la fuente: http://blog.txipinet.com/
*/
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
	pid_t pid;

	if ( (pid=fork()) == 0 )
	{ /* hijo */
		printf("Soy el hijo (%d, hijo de %d)\n", getpid(),
        getppid());
	}
	else
	{ /* padre */
		printf("Soy el padre (%d, hijo de %d)\n", getpid(),
        getppid());
	}

	return 0;
}
  • exec() : Un proceso puede ir cambiando en su vida de "procesos" desde que nace hasta que muere, digamos que se puede transformar en cualquier cosa, pero lo único que no puede cambiar es su PID, un proceso puede estar relacionado una aplicación de autentificación y de pronto transformarse en una shell de comandos bash. De esta forma trabaja la llamada al sistema exec(), si una hebra lo invoca, el proceso se reemplazará por un programa completamente nuevo (este programa se pasa por parámetro a la funcion exec), además reemplazará a todas las hebras relacionadas.

Cancelación.

Definición

Diagrama cancelación

Una cancelación es la acción de finalizar un Thread antes de que haya acabado, en otras palabras, tenemos un proceso corriendo en una maquina y de pronto por cualquier circunstancia, tanto interna como externa, cancelamos el proceso.

Pareciera que es muy simple, pero no lo es. Supongamos que estamos con nuestro Navegador preferido y empezamos a descargar un archivo de varios Gigas y en un momento 'cancelamos' la descarga, O simplemente cuando tenemos una conexión a internet bastante lenta y cargamos una página que prácticamente se va cargando por cuadritos y depronto pulsamos sobre el boton 'parar'. Internamente lo que estamos haciendo es cancelar los Threads de procesos y hay que notar que en el caso del navegador estamos cancelando en cascada varios Threads.

En pthread tenemos la funcion pthread_cancel() que permite que un thread cancele a otro en la mitad de su ejecución.

El thread a cancelar se denominta Target thread.

Tipos

  • Asincrónicos: El thread es cancelado inmediatamente. (esta forma hay que evitarla, es casi de forma bruta. Podría traer problemas)
  • Diferida: Hay que verificar si es que el thread se encuentra en un punto de cancelación (cancellation point), si es que no se encuentra en este punto, el thread no se puede cancelar


Problemas

Los problemas más frecuentes en la cancelación de threads se producen cuando realizamos cancelación Asíncrona, por ejemplo cuando cancelamos inmediatamente una hebra a la cual le hemos asignado recursos o cuando tenemos thread que necesitan comunicarse entre ellas enviandose datos y cancelamos una de ellas bruscamente. Entonces por lo general hay veces que el sistema operativo no se dará cuenta de los recursos que tiene que liberar y quedarán asignados.

Tratamiento de señales.

Cuando iniciamos nuestro laptop, una tablet, etc, un centenar de procesos se crean y también a medida que el sistema operativo lo requiera. Todos estos procesos tienen un contexto distinto, es decir, no tienen por qué estar relacionados entre si, pero si pueden tener una comunicación entre ellos. Hay varios métodos de comunicación entre procesos (interprocess comunication), dentro de ellos están los semáforos, paso por mensaje, memoria compartida, etc. En los sitemas UNIX , además de los IPC, existen las señales, que son interrupciones mediante Software y sirven para notificar que se ha producido un determinado suceso/evento (se puede hacer una analogía con las interrupciones por Hardware). En resumen las señales permiten controlar eventos internos y externos a un programa. Todas las señales tienen ciertas propiedades como el nombre y un numero que las identifica, para ello podemos ejecutar el comando kill -l en sistemas UNIX y veremos las señales con el número al cual se asocian.

Ejemplo kill -l

Handling

Para manejar las señales se utiliza los Signal Handlers o tratadores de señales, que son procedimientos que capturan las señales y actúan de determinada forma (Equivalente al vector de interrupción por hardware). En C se pueden programar fácilmente ocupando la biblioteca <signal.h>, en Java o Python por ejemplo podemos capturar algunas de las señales con los famosos Try/catch. Siempre se siguen los mismos pasos en el proceso de signal handling.

Pasos a seguir tratamiento señales

  • 1.-Una señal se crea luego haber ocurrido un evento/suceso en particular.
  • 2.-Esta señal es entregada a un proceso, para que este la trate.
  • 3.-Finalmente la señal es manejada.

En general tanto para señales sincrónicas como asincrónicas, un proceso puede reaccionar de de tres formas distintas:

Diagrama tratamiento de señales
  • El proceso simplemente ignora la señal recibida y no hace nada.
  • El proceso invoca a una función por defecto que sabe como tratar esta señal, siempre y cuando el programador no haya programado

el como tratarla.

  • El proceso invoca a una función realizada por el programador, el cual seguramente le habrá dado un tratamiento especial a aquella señal.

En el diagrama de tratamiento de señales, se pueden apreciar los 3 casos antes mencionados. En la ejecución, cuando recibe la Señal 1 podemos apreciar de que no hace nada, no cambia el curso normal de ejecución del proceso e ignora la señal. Luego cuando se detecta la Señal 2, podemos apreciar que es necesario tratar la señal y hacer algún procedimiento de Handling programado por usuarios, luego podemos volver a un punto donde se produjo la interrupción, finalizar el proceso o volver a un estado anterior y volver a tomar el curso normal del proceso. Finalmente visualizamos que se produce la Señal 3 en el cual se procede con el tratamiento por defecto de la señal.

¿A qué Thread enviamos señales?

Los procesos por lo general están asociados a mas de un Thread. Entonces podemos preguntarnos ¿A qué Tread le entregamos la señal?, Tenemos cuatro opciones para aquello:

  • Enviar las señales al hilo (Thread) el cual las ha causado.
  • Enviar las señales a todos los hilos asociados.
  • Enviar las señales solo a algunos hilos en el proceso.
  • O en su defecto puede haber un Thread específico asignado para que reciba todas las señales.

Estas cuatro opciones presentadas anteriormente, dependen del sincronismo del señales, por ejemplo, una Señal sincrónica se mandan siempre al hilo que las ha causado. Por otra parte las señales asincrónicas pueden enviarse a cualquiera de las hebras asociadas, sin embargo existen hebras que están bloqueadas para recibir señales (no está demás mencionar que las señales deben ser tratadas una sola vez). Esto es un punto a tener en cuenta cuando se estamos trabajando con software multihebra.

funcion KILL y pthread_kill

kill es una función estandar de UNIX, que la podemos usar para suministrar una señal a un proceso. La firma de la función kill luce como sigue:

kill(aid_t aid, int signal)

  • En donde aid corresponde al proceso al cual le enviaremos la señal y signal el numero que representa una señal específica.
Ejemplo: Terminaremos el proceso de firefox, pasandole como parametros el 'pid' y el valor '-9' de la señal:
$ ps -ef | grep firefox
1986 ?        Sl     7:22 /usr/lib/firefox-3.5.3/firefox

$ kill -9 1986

También cabe destacar que Pthread (POSIX Threads Programming) tiene la función pthread_kill que básicamente permite que una señal sea enviada a una hebra en especifico. La firma de la función luce como sigue:

pthread_kill(pthread_t tid, int signal)

  • Análogo a kill pero en este caso tid corresponde al id de la hebra.


En la siguiente tabla se indican las señales que incluye <signal.h>, se muestra la constante y su significado.

Constante Significado Sistemas
SIGHUP Hangup POSIX
SIGINT Interrupt ANSI
SIGQUIT Quit POSIX
SIGILL Illegal instruction ANSI
SIGABRT Abort ANSI
SIGTRAP Trace trap POSIX
SIGIOT IOT trap 4.2 BSD
SIGEMT EMT trap 4.2 BSD
SIGINFO Information 4.2 BSD
SIGFPE Floating-point exception ANSI
SIGKILL Kill, unblock-able POSIX
SIGBUS Bus error 4.2 BSD
SIGSEGV Segmentation violation ANSI
SIGSYS Bad argument to system call 4.2 BSD
SIGPIPE Broken pipe POSIX
SIGALRM Alarm clock POSIX
SIGTERM Termination ANSI
SIGUSR1 User-defined signal 1 POSIX
SIGUSR2 User-defined signal 2 POSIX
SIGCHLD Child status has changed POSIX
SIGCLD Same as SIGCHLD System V
SIGPWR Power failure restart System V
SIGXCPU Exceeded CPU time POSIX
SIGSTOP Pause execution POSIX
SIGCONT Resume execution POSIX

Conjunto compartido de hebras.

Concepto Pool de Threads

Conjunto compartido de hebras o Pool de Threads nace como solución al problema de los servidores web que utilizan multihebras para sus procesos, el hecho de crear las hebras y desecharlas cada vez que se hacen solicitudes es muy costoso. Entonces el conjunto compartido de hebras se refiere a una serie de Threads compartidas disponibles para usar y ahorrar tiempos de respuestas.

Problema / solución

El problema se puede puede modelar como el conocido Problema Productor-Consumidor, en donde los actores son 3; el Consumidor, el productor y un intermediario o buffer. Esto funciona de la manera siguiente; tanto los procesos productores como consumidores tienen acceso a un recurso en común que sería nuestro intermediario o buffer (para el caso del Pool el recurso son las hebras), entonces a medida que hayan recursos en el consumidor puede acceder a ellos y si en algun momento se acaba el recurso simplemente se bloquea y el consumidor tiene que esperar hasta que el productor ponga a disposicion mas recursos o hebras en nuestro caso.

Problema servidores web

  • Problema: Los Servidores web trabajan con modelos multihebras. Entonces a medida que los servidores reciben solicitudes el servidor crea una nueva hebra, aunque crear una hebra es menos costoso que crear un proceso, los servidores tienen que responder a una infinidades de consultas en pequeños lapsos de tiempo (cabe notar que una hebra se desechará luego que termine su objetivo de responder a la consulta). Ahora si permitimos que por cada solicitud se cree una hebra sin poner un limite a estas, se pueden agotar los recursos como la memoria, por ejemplo. Entonces tenemos un problema de tiempos y un problema de recursos.
Diagrama Solicitud/Conjunto de hebras.
  • Solución: Para solucionar lo antes planteado se crea el Conjunto compartido de hebras, entonces como se mencionó en un principio, se crea un número limitado de hebras que estarán en este pool como recurso a la espera de que se realizen solicitudes. Cuando una hebra es terminada de usar vuelve a nuestra 'canasta' de recursos y si es que se acaban el servidor espera hasta que hayan más hebras disponibles.

Cabe destacar que el conjunto de hebras compartidas se crean cuando se inicia el sistema, y el número de estas varían dependiendo de la arquitectura de la de la maquina, como por ejemplo la cantidad de procesadores, la cantidad de memoria y un análisis estimativo de cuántas solicitudes esperan tener. Existen otras arquitecturas mas complejas que además de definir un número mínimo de hebras, pueden quitar o añadir hebras dependiendo si el sistema lo requiera, estos sistemas son muy buenos porque ahorran espacio en memoria en los momentos que la cantidad de solicitudes es baja.

Ventajas

Existen básicamente dos ventajas:

  • Velocidad. Puesto que es mas rápido ir y usar una hebra disponible que crear una.
  • Protección. Puesto que evitamos que se acaben los recursos, especialmente en sistemas de servidores limitados

Datos específicos de una hebra.

Las hebras que pertenecen a un mismo proceso comparten determinada información, que le permite una rápida comunicación entre ellas, pues comparten memoria principal (por tanto las variables del proceso), sin embargo cada hebra necesita información propia, esto debido a que en si, una hebra es independiente. Dentro de los datos que son específicos de cada hebra se encuentran:

  • Identificador o (TID) que permite de manera única e inequívoca referirse a una hebra en particular.
  • Estado, que permite saber que sucede con la hebra, ya que esta puede estar ejecutándose, lista para ser asignada a un procesador, o en espera de algún evento (ej: evento de I/O).
  • Puntero de instrucción, que nos permite ir avanzando secuencialmente por las instrucciones de la hebra y no perder el hilo de ejecución (instrucción en la que me encuentro) cuando la hebra pasa a un estado de bloqueo.
  • Prioridad, ya que indiferente de la prioridad que tenga el proceso, cada hebra tiene su propia prioridad.
  • Afinidad, que si bien no siempre se encuentra en las hebras, resulta sumamente útil para mejorar rendimiento, ya que liga a una hebra con el procesador donde se estaba ejecutando.

Caso de sistemas Transaccionales

Si bien los mencionados anteriormente son datos específicos de una hebra, quedan algunos por mencionar, en particular el caso de los sistemas dedicados a hacer transacciones, pues una excelente manera para mejorar tiempos de respuesta para transacciones de alta demanda, es usando hebras, pero mas allá de eso, es muy importante que las hebras puedan portar datos con los que van a trabajar, haciendo mas eficiente todo el proceso, lo cual esta soportado por algunas interfaces de sistemas operativos como POSIX y WIN32.

Activaciones del planificador.

Hebras en el PSC y el SCS.

La mayoría de los sistemas que maneja hebras, utiliza una estructura intermedia entre hebra de usuario y hebra de kernel. Esta estructura intermedia se denomina proceso ligero o LWP (lightweight process). El LWP es un procesador virtual visto desde el punto de vista de las hebras de usuario que se ejecutan en el, de esta forma el LWP luego se asocia a una hebra de kernel, que es la que se ejecuta físicamente en el procesador, por tanto los LWP son intermediarios (a través de bibliotecas como Pthreads de POSIX), marcando la diferencia entre dos entornos, uno es el process control scope (PCS) en el lado del usuario y el otro es system control scope (SCS) en el kernel. Ambos entornos se encargan de diferente manera a planificar el uso de la cpu, o sea en el fondo los LWP permiten tener un grado de flexibilidad permitiéndonos darles a las diferentes hebras usos de cpu, todo desde el ámbito PCS, modificando en el fondo como funciona el planificador del Sistema Operativo.

Proogram and System Control Scope (PCS y SCS)

Como se menciono antes, el PCS y el SCS son entornos de control del uso de procesador, siendo el PCS el que se encuentra por el lado de las hebras de usuario, tal como podemos ver en la imagen.

Lo importante de esto, es que en el entorno de programa (PCS) podemos por ejemplo asignarle 50% de cpu a la hebra 1 y 25% para las hebras 2 y 3 cada una, controlando y flexibilizando el uso de la cpu, incluso cuando en el fondo en el SCS, el sistema operativo decida planificar de diferente forma el uso de procesador.


.

Referencias

  • Silberschatz, Abraham (2009) Operating Systems Concepts (Eight Edition). John Wiley & Sons, Inc. ISBN 978-0-470-12872-5

Enlaces externos

Herramientas personales
Espacios de nombres
Variantes
Acciones
Navegación
Herramientas