viernes, 7 de febrero de 2014

Ejercicio #4 - Planificación y variables compartidas


Crea dos procesos A y B que modifican una variable compartida. El proceso A itera 40 veces, incrementando la variable compartida de 100 en 100. El proceso B repite la misma acción pero incrementa el valor de de la variable compartida de 1 en 1.

1. Asigna un esquema de planificación fifo y round robin. Analiza el comportamiento.
2. Asigna prioridades (por ejemplo,
A con prioridad 24 y B con prioridad 26) y un esquema
de planificación round robin. Analiza el comportamiento.

Nota 1: se puede añadir una espera activa de 1 sg en la iteración de cada proceso para
observar mejor el comportamiento del planificador.
Nota 2: para observar el comportamiento del planificador, es necesario que el programa se ejecute en una máquina con una única CPU. 

Planificación de procesos

El Planificador

El elemento fundamental de un SOTR es el planificador. Este, básicamente, controla el estado de las tareas y decide qué tarea pasará a ejecutarse. 

Tenemos que tener en cuenta que las tareas en el sistema no pueden bloquear otras tareas, además una tarea bloqueada nunca tomará el procesador


Para conseguir la portabilidad de las aplicaciones puede ser necesario especificar la política de planificación.


  • SCHED_FIFO: Política de planificación expulsiva basada en prioridades fijas y un comportamiento FIFO para las tareas con la misma prioridad.
  • SCHED_RR: Igual que en el caso anterior pero se emplea la política Round-Robin para procesos de igual prioridad.
  • SCHED_OTHER: Definida por la implementación.

#include <sched.h>

int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);



int sched_getscheduler(pid_t pid);

int sched_setparam(pid_t pid, const struct sched_param *param); int sched_getparam(pid_t pid, struct sched_param *param);

int sched_get_priority_max(int policy); 
int sched_get_priority_min(int policy);

int sched_yield(void);



Ejercicio #3 - Ejecutivo cíclico


Supongamos el siguiente conjunto de tareas. Implementar un ejecutivo cíclico, donde los tiempos son expresados en segundos.
El programa principal puede tener el siguiente formato, donde cada tarea mostrará un mensaje mostrando la hora de inicio de ejecución. Además, dormirá por el tiempo que sea necesario hasta completar su tiempo de cómputo especificado, en cuyo caso mostrará un mensaje indicando la finalización de su ejecución. 


En este caso, el periodo principal, o hiperperiodo, será el mcm de todos los periodos, es decir 20. El periodo mínimo es 10. Por tanto el número de ciclos de este ejecutivo cíclico será de 2 (periodo principal / periodo mínimo).

jueves, 6 de febrero de 2014

Ejercicio #2 - Señales en POSIX. Tareas periódicas

Diseñe e implemente un proceso que genere una señal SIGUSR1 de forma periódica cada 1 segundo (utilizando un temporizador). Así mismo, instale un manejador de la señal SIGUSR1 tal que cada vez que reciba dicha señal incremente el valor de un contador de tiempo (inicialmente a cero) que cuente los segundos transcurridos desde el inicio del programa.

Además, cuando reciba una señal SIGUSR2, debe terminar el proceso mostrando un mensaje de finalización con el valor final del contador del tiempo.

El programa debe mostrar al principio de la ejecución su identificador de proceso (pid), así como el número de la señal en la que ha instalado un manejador de señales y el número de la señal por la que espera por su terminación.

Nota 1: puede enviar señales al proceso desde un Terminal mediante el comando: kill -USR2 pid-del-proceso.
Nota 2: puede conocer los identificadores de los procesos desde un Terminal mediante el comando: ps -e


Autor de los ejercicios (Profesorado UMA)

Temporizadores y tareas periódicas

Un temporizador se trata de un registro contador asociado a un reloj. Una vez creado se inicializa con el tiempo a contar y cada pulso de reloj decrementa el contador del temporizador. Al llegar a 0 se envía una señal al proceso que lo creó e inicializa de nuevo su valor.

Creación de un temporizador

#includes <sys/time.h>

int timer_create (clockid_t clock_id, struct sigevent *evp, timer_t *timerid)

int timer_settime (timer_t timerid, int flags, const struct itimerspec *value, struct itimerspec *ovalue);

Devuelve 0 en caso de éxito y -1 en caso de error. Es importante definir un identificador para el temporizador y el tipo de notificación que se produce cuando expira el temporizador. NULL eleva SIGALARM.

struct sigevent {  
   int sigev_notify /* notification type */ 
   int sigev_signo; /* signal number */  
   union sigval sigev_value; /* signal value */ 
};

sigev_notify
   SIGEV_SIGNAL : Se asocia una señal.
   SIGEV_NONE : No se notifica nada.

El valor de espera se especifica mediante una estructura de tipo itimerspec.
  • it_value: valor inicial del temporizador (0 lo desactiva).
  • it_interval: nuevo valor tras expiración (0 un solo evento).
struct itimerspec {    
   struct timespec it_interval; /* periodo */    
   struct timespec it_value; /* valor inicial*/ 
};  

struct timespec {   
   time_t tv_sec; /* seconds */    
   long tv_nsec; /* and nanoseconds */ 
}; 


Tarea periódica

Los temporizadores por sí solos no tienen mucha función, por ello añadimos las tareas periódicas.


void periodic (void *arg) {  
   int signum; /* señal recibida */  
   sigset_t set; /* señales a las que se espera */  
   struct sigevent sig; /* información de señal */  
   timer_t timer;  
   struct itimerspec required, old;  
   struct timespec first, period;  

   sig.sigev_notify = SIGEV_SIGNAL;  
   sig.sigev_signo = SIGRTMIN;  
   
   if (clock_gettime (CLOCK_MONOTONIC, &first) != 0) error();   

   first.tv_sec = first.tv_sec + 1;  
   period.tv_sec = 0;  
   period.tv_nsec = 10.0E6; /* 10 ms */  
   required.it_value = first;  
   required.it_interval = period;


   if (timer_create(CLOCK_MONOTONIC,&sig,&timer) != 0) error();  
   if (sigemptyset(&set) != 0) error ();  
   if (sigaddset(&set, SIGRTMIN) != 0) error();  
   if (timer_settime(timer,0, &required, &old) != 0)    error ();  
   
   while (1) {  
      if (sigwait(&set, &signum) != 0) error();   // Comportamiento de la tarea 
   }
}


Ejecutivo cíclico

Si todas las tareas son periódicas, se puede confeccionar un plan fijo de ejecución. No se utiliza la concurrencia, por lo que las distintas tareas se simulan en un programa secuencial, sin soporte del lenguaje o del sistema operativo.

Ejercicio #1 - Señales en POSIX

Vamos a empezar a realizar una serie de ejercicios para asimilar los conocimientos adquiridos en entradas anteriores. Las soluciones de los mismos las añadiré en un comentario de las correspondientes entradas a los ejercicios.

Diseñe e implemente un proceso que cada vez que reciba una señal SIGUSR1 incremente el valor de un contador (inicialmente a cero). Para ello, instale un manejador de la señal SIGUSR1 tal que cada vez que reciba dicha señal incremente el valor de un contador que cuente la cantidad de señales SIGUSR1 recibidas desde el inicio del programa. 

Además, cuando reciba una señal SIGUSR2, debe terminar el proceso mostrando un mensaje de finalización con el valor final del contador. 

El programa debe mostrar al principio de la ejecución su identificador de proceso (pid), así como el número de la señal en la que ha instalado un manejador de señales y el número de la señal por la que espera por su terminación. 

Nota 1: puede enviar señales al proceso desde un Terminal mediante el comando: kill -USR2 pid-del-proceso. 
Nota 2: puede conocer los identificadores de los procesos desde un Terminal mediante el comando: ps -ef

Autor de los ejercicios (Profesorado UMA)

Extensiones de Tiempo Real

El mecanismo de manejo de señales de POSIX.4 cuenta con la posibilidad de encolar las señales de tiempo real. Las señales pendientes están ordenadas por prioridad y se permite el intercambio de datos.

El rango de señales de Tiempo Real van desde SIGRTMIN a SIGRTMAX (el número de estas señales viene definido en RTSIG_MAX, constante de "rt_limits.h", se puede saber usando sysconf(_SC_RTSIG_MAX))

Señales POSIX

Configuración

Bloquear una señal significa que, en el caso de que sea generada, la señal no se pierde y se posterga su atención. El bloque se realiza mediante la máscara de señal

int sigemptyset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);

El bloqueo se genera por:

int pthread_sigmas(int how, const singset_t *restrict set, sigset_t *restrict oset);

Le pasamos como primer parámetro el bloqueo/desbloqueo "SIG_BLOQ" y "SIG_UNBLOQ". Acto seguido le pasamos el conjunto de señales.

int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

Esta función afecta a todas las hebras del proceso. Si el "oset" no es NULL, almacena el conjunto de señales antiguo.

La asignación de un manejador para la señal se realiza mediante la función sigaction. Nos permitirá, entre otras cosas, ignorar una señal, asignar un manejador por defecto y asignar un manejador propio.

int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
   void (*sa_handler) (int);
   sigset_t_t sa_mask;
   int sa_flagas;
};

SIG_DFL: Manejador por defecto.
SIG_IGN: Ignora la señal

Manejador

Los manejadores de señales deben ser muy cortos y simples. Si una hebra necesita esperar hasta que se produzca una señal:

int sigwait (cont sigset_t *set, int *sig);
int sigwaitinfo(const sigset_t *set, siginfo_t *info);

Esta función realiza tres operaciones de forma atómica. Desbloquea el conjunto de señales, queda a la espera de que se produzca alguna señal no bloqueada y cuando se produce, se restablecen los bloqueos y se devuelve la señal producida.

Regiones críticas condicionales

pthread_cond_destroy(pthread_cond_t *cond);   
int pthread_cond_init(pthread_cond_t *restrict cond, 
                      const pthread_condattr_t *restrict attr);   
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int pthread_cond_timedwait(pthread_cond_t *restrict cond,                                    pthread_mutex_t *restrict mutex, 
                           const struct timespec *restrict                                  abstime);   
int pthread_cond_wait(pthread_cond_t *restrict cond,                                   pthread_mutex_t *restrict mutex);   
int pthread_cond_broadcast(pthread_cond_t *cond);  
int pthread_cond_signal(pthread_cond_t *cond)


El funcionamiento es sencillo, una hebra obtiene un mutex, comprueba la condición bajo la protección del mutex y si la condición es cierta la hebra continua su tarea y libera el mutex cuando sea apropiado. Si la condición no es cierta el mutex se libera y la hebra se duerme sobre la variable condición. Cuando se produce un cambio en la condición se deberá producir una llamada que despierte la hebra dormida. por último la hebra despertada vuelve a tomar el mutex y repite el procedimiento mencionado anteriormente.

Ejemplo #3 - [Threads] Regiones críticas: El problema de los jardines

#include <pthread.h>
#include <stdio.h>

long Visitantes = 0;
pthread_mutex_t mutex;

void IncVisitantes() {
   pthread_mutex_lock(&mutex);
   Visitantes = Visitantes + 1;
   pthread_mutex_unlock(&mutex);
}

void *HebraVisitas (void *argg) {
   int i=0;

   long *NumVisitas;

   NumVisitas = (long *) argg;

   for (i=0; i<*NumVisitas;i++) {
      IncVisitantes();
   }
}


int main () {
   pthread_t th1, th2, th3, th4;
   pthread_attr_t attr;
   long visi1 = 1000000;
   long visi2 = 2000000;

   pthread_attr_init(&attr);
   pthread_mutex_init(&mutex, NULL);

   pthread_create(&th1, &attr, HebraVisitas, &visi1);
   pthread_create(&th2, &attr, HebraVisitas, &visi2);
   pthread_create(&th3, &attr, HebraVisitas, &visi1);
   pthread_create(&th4, &attr, HebraVisitas, &visi2);
   pthread_join(th1, NULL);
   pthread_join(th2, NULL);
   pthread_join(th3, NULL);
   pthread_join(th4, NULL);

   printf("El numero de visitas totales =%ld\n", Visitantes);
   printf("Fin\n");
   return 0;
}

Probad a realizar el ejercicio sin usar mutex y ampliando el número de visitantes. Comentar los resultados :D

[Threads] Regiones críticas

POSIX ofrece diversos mecanismos para interactuar con las regiones críticas entre hebras, nosotros nos centraremos en uno de los más sencillos e intuitivos, el mutex.

int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
                       const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

Si los attr son NULL, será un mutex con atributos por defecto. Se inicializa a desbloqueado y además debemos destruirlo a desbloqueado.

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

La función "pthread_mutex_trylock" intenta tomar un mutex. Si ya está bloqueado se devuelve un código de error EBUSY (puede resultar útil para evitar condiciones de deadlock).

Al intentar desbloquear se pueden devolver dos tipos de errores:
  • Si el mutex ya está desbloqueado.
  • Si el mutex está tomado por otra hebra.



Ejemplo #2 - [Threads] Paso de parámetros

#include <pthread.h>
#include <stdio.h>

struc arg {
   int x;
   int v[10];
   int li;
   int lf;
};

void *BusquedaLineal(void *argg) {
   int i=0, pos = -1, enc=0;

   struct argm *arg;

   arg = (struct argm *) argg;
   i = arg->li;

   while (i<=arg->lf && !enc) {
      if ((arg->v[i]) == (arg->x)) enc =1;
      else i++;
   }
   if (enc) printf("Encontrado en pos = %d\n", i);
}

void Rellenar(int *x) {
   int i=0;
   for (i = 0; i<10; i++) {
      x[i]=i;
   }
}

int main () {
   struct argm arg, arg2;
   pthread_t th1, th2;
   pthread_att_t attr;
   int val = 1;

   Rellenar(arg.v);
   Rellenar(arg2.v);

   arg.li = 0;
   arg.lf = 4;
   arg.x = 1;

   arg2.li = 5;
   arg2.lf = 9;
   arg2.x = 1;

   pthread_attr_init(&attr);
   pthread_create(&th1, &attr, BusquedaLineal, &arg);
   pthread_create(&th2, &attr, BusquedaLineal, &arg2);

   pthread_join (th1, NULL);
   pthread_join (th2, NULL);

   printf("Fin\n");

   return 0;  

}


Destacamos que una hebra puede esperar en estado bloqueado hasta que otra termine, para ello usamos la llamada a "join" sobre esta otra hebra. No es más que un mecanismo de sincronización entre hebras, en este caso, la función pthread_join() bloquea la hebra hasta que la hebra que se indica en su parámetro no termine.

Ejemplo #1 - [Threads] Creación/Terminación

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define NUM_THREADS 5

void *PrintHello(void *threadid){
   long tid;
   tid = *(long*)threadid;
   printf("Hello World! It's me, thread #%ld!\n", tid);
   pthread_exit(NULL);
}

int main (int argc, char *argv[]){
   pthread_t threads[NUM_THREADS];
   int rc;
   long t;
   
   for (t=0; t<NUM_THREADS; t++){
      printf("In main: creating thread %ld\n", t);
      rc = pthread_create(&threads[t], NULL, PrintHello, &t);
      if (rc) {
         printf("ERROR; return code from pthread_create() is %d\n", rc);
         exit(-1);
      }
   }
   pthread_exit(NULL);
}

Para la implementación de este sencillo ejemplo no tenemos que tener muchas cosas en cuenta, únicamente tener cuidado con el paso de parámetros que referencian a bloques de memoria. En nuestro caso, en primer lugar le pasamos como primer parámetro el thread que vamos a construir, acto seguido la estructura del thread, como tercer parámetro el manejador o la función que vayamos a utilizar y por último el argumento para nuestra función.

miércoles, 5 de febrero de 2014

Threads

Antes de empezar me gustaría mencionar que es importante traer de casa una base sólida en C, ya que todos los servicios que utilizaremos de POSIX son llamadas a librerías escritas en C estándar.


Creación de hebras

#include <pthread.h> 
int pthread_create(
   pthread_t *thread, const pthread_attr_t *attr,
   void *(*start_routine) (void*), void *arg
);
 
int pthread_attr_destory(pthread_attr_t *attr); 
int pthread_attr_init(pthread_attr_t *attr);

El número máximo de hebras que se pueden crear dependerá de la implementación. Además no existe una jerarquía o dependencia entre las hebras.

Tenemos que tener en cuenta que en un sistema monoprocesador, o en el que hay más hebras que procesadores, sólo una de entre las hebras ejecutables puede estar ejecutándose, el resto deberá esperar (según la política que implantemos).

Estado ejecutable

Una hebra pasa de "lista" a "ejecución" cuando el procesador queda disponible. Del mismo modo una hebra pasa de "ejecución" a "lista" dependiendo del tipo de planificación que hayamos empleado. El cambio de una hebra por otra en el procesador se denomina "cambio de contexto" y es un proceso con una carga computacional importante.

Estado bloqueado

Una hebra se encuentra en estado "bloqueado" cuando no puede continuar su ejecución hasta que ocurra "algún evento". Este estado es imprescindible para la sincronización de las hebras. Las hebras en POSIX no se pueden bloquear ellas mismas.

Terminación de hebras

void pthread_exit(void *value_ptr);
int pthread_cancel(pthread_t thread);

Una hebra termina cuando lo hace la rutina asociada a ella. Se invoca a la función pthread_exit. Para cancelarla habría que invocar a pthread_cancel.

El proceso termina llamando a exec() o exit(). Mientras que el main() termina sin llamar explícitamente a pthread_exit.


Comandos básicos del shell de Unix

$ mkdir dir_trabajo # crea directorio llamado "dir_trabajo"
$ cd dir_trabajo # cambia el directorio actual a "dir_trabajo"
$ gedit programa_1.cpp & # edita el fichero "programa_1.cpp"
$ mgcc -Wall -Werror -o programa_1 programa_1.c # compila (c) el fichero "programa_1.c en MarteOS" $ gcc -ansi -Wall -Werror -o programa_1 programa_1.c # compila (c) el fichero "programa_1.c"
$ g++ -ansi -Wall -Werror -o programa_1 programa_1.cpp # compila (c++) el fichero "programa_1.cpp"
$ g++ -ansi -Wall -Werror -o programa_2 programa_2.cpp 2>&1|less # compila y pagina errores
$ ls # lista el contenido del directorio actual
$ ./programa_1 # ejecuta el programa "programa_1"
$ ls -l # listado detallado del contenido del directorio actual
$ pwd # imprime el directorio actual
$ cd .. # cambia el directorio actual al directorio padre
$ rmdir dir_trabajo # elimina el directorio llamado "dir_trabajo" (debe estar vacio)
$ rm nombre_1.ext # elimina el fichero "nombre_1.ext"
$ cp nombre_1.ext nombre_2.ext # copia (duplica) el fichero "nombre_1.ext" a "nombre_2.ext"
$ mv nombre_1.ext .. # mueve el fichero "nombre_1.ext" del directorio actual al directorio padre
$ mv ../nombre_1.ext . # mueve el fichero "nombre_1.ext" del directorio padre al directorio actual
$ mv nombre_1.ext nombre_2.ext # cambia el nombre del fichero "nombre_1.ext" a "nombre_2.ext"
$ mv ruta1/nombre_1.ext ruta2/nombre_2.ext # mueve el fichero "ruta1/nombre_1.ext" a "ruta2/nombre_2.ext"
$ cat fich.txt # muestra el contenido del fichero de texto "fich.txt"
$ less fich.txt # muestra el contenido del fichero de texto "fich.txt"
$ clear # borra la pantalla [Ctrl+L]
$ zip fich.zip fich1 fich2 ... # empaqueta y comprime un conjunto de ficheros
$ zip -r fich.zip directorio ... # empaqueta y comprime el contenido de varios directorios
$ unzip fich.zip # desempaqueta y descomprime el archivo "fich.zip"
$ man comando # imprime el manual para un determinado comando
$ # teclas del [CURSOR]: moverse y editar la historia de comandos anteriores
$ # tecla [TABULADOR]: completa el nombre del fichero
$ # tecla [ENTER]: ejecuta el comando introducido
$ [Ctrl+C] # tecla [Ctrl+C]: aborta la ejecucion del comando o programa actual

# ruta absoluta: /home/alumno/directorio1/directorio2/nombre.ext
# ruta desde home: ~/directorio1/directorio2/nombre.ext
# ruta desde actual: directorio1/directorio2/nombre.ext
# ruta desde padre: ../directorio1/directorio2/nombre.ext

# Nota: NO es conveniente poner espacios ni acentos ni ~n en los nombres de directorios ni de fichero

Introducción a POSIX

¡Bienvenidos al blog!

En las próximas entradas veremos los diferentes mecanismos que ofrece POSIX
  • Creación de hebras
  • Mecanismos de sincronización
  • Manejo de eventos
  • Mecanismos para STR
Las herramientas que vamos a utilizar serán básicas. 
  • Entorno de programación en GNU/Linux.
  • Aplicación del Terminal para interactuar con el sistema operativo por medio de su intérprete de comandos.
  • Un editor de texto básico, en nuestro caso nos servirá "gedit".

Espero que os resulte didáctico e interesante :)