U.T.1. Creación, control, sincronización e comunicación de procesos.
De ASIRodeira
Índice |
Introducción
Os sistemas multitarefa empregan dúas aproximacións distintas á hora de conseguir que o sistema execute varias cousas ó mesmo tempo: Os threads (fíos) e os procesos.
Un proceso é un programa que está sendo executado pola CPU. Nun sistema multitarefa, nun instante determiñado a CPU pode ter varios procesos en estado de execución.
Un thread é unha parte dun proceso que está executándose de xeito concurrente (á vez) co resto do proceso (fío principal de execución).
Un proceso é unha copia exacta do proceso que o lanza, contendo unha copia das variables, dos descritores de ficheiros e dos manexadores (handlers) de sinais. En cambio os threads (fíos) comparten co proceso cos lanza o espacio de memoria, a tabla de descritores e os manexadores de sinais.
Threads (Fíos)
Os threads (fíos) permiten que dous ou máis partes do código se executen simultáneamente compartindo os mesmos recursos (ficheiros, variables globais ...).
Un programa 'normal' ten un único thread (fío) de execución que chama á función main e a executa ata que remata. Un novo thread executará unha función en paralelo coa función principal (main) e cos outros threads creados con anteriormente. O sistema operativo encargarase de xestionar a execución dos distintos threads, sendo incluso posible en sistemas multiprocesador que varios fíos se leven a cabo simultáneamente dun xeito real.
Os threads resultan útiles cando se precisa facer máis dunha cousa o mesmo tempo. Por exemplo, un servidor Web necesita servir páxinas a varios clientes ó mesmo tempo e ademáis continuar aceptando peticións de novos clientes.
Programación con Threads Baixo Linux
En Linux existen moitas librerías que implementan os threads de execución xa que, a diferencia dos procesos (fork), non forman parte do kernel do sistema. Probablemente a librería de threads máis utilizada sexa a librería pthread que é a que imos a estudiar.
Para utilizar a librería pthread é necesario incluir o ficheiro de cabeceira pthread.h (#include <pthread.h>) e cando se compile o programa enlazalo coa librería (-lpthread).
As funcións de manexo de threads son as seguintes:
Creación do Thread
int pthread_create (pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void * arg);
Esta función crea un novo thread de execución que levará a cabo a función start_routine ó mesmo tempo que o thread principal. A función start_routine debe ter o seguinte prototipo:
void * start_routine (void *param);
O parámetro arg é a información que se lle pasará como parámetro á función start_routine. Si a rutina non precisa de ningún argumento, se poñerá NULL.
O parámetro thread é a dirección de memoria dunha variable de tipo pthread_t onde se almacenará a información do novo thread creado.
O parámetro attr contén os atributos que vai ter o novo thread (ver as funcións de manexo de atributos). Se queremos os atributos por defecto, poñeremos NULL
Facer que o thread principal espere a que rematen os threads en execución:
int pthread_join (pthread_t th, void **pthread_return);
A función pthread_join fai que a función que o chama (o thread principal) espere pola finalización dos threads secundarios antes de rematar, de xeito que poda liberar os seus recursos.
O parámetro th é o thread polo que se vai esperar a que finalice.
O parámetro pthread_return almacenará o valor devolto polo thread finalizado.
Finalizar a execución do thread actual
void pthread_exit (void *retval);
Esta función provoca a finalización do thread en execución devolvendo o valor retval. É o equivalente a escribir return retval.
'Independizar' a execución do fío do que o creou (garantizar que o fío libere os seus recursos o finalizar)
int pthread_detach (pthread_t th);
Esta función fai que o thread th se execute de xeito independiente do thread principal, e decir, que unha vez finalizado liberará os seus recursos por si mesmo. A un thread detached con esta función non se lle poderá facer un pthread_join pra esperar pola súa finalización.
Manexo dos atributos do thread que se vai a crear
int pthread_attr_init (pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr); int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate); int pthread_attr_setschedpolicy(pthread_attr_t *attr, int schedpolicy); int pthread_attr_getschedpolicy(pthread_attr_t *attr, int *schedpolicy); int pthread_attr_setschedparam(pthread_attr_t *attr, int schedparam); int pthread_attr_getschedparam(pthread_attr_t *attr, int *schedparam); int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched); int pthread_attr_getinheritsched(pthread_attr_t *attr, int *inheritsched); int pthread_attr_setscope(pthread_attr_t *attr, int scope); int pthread_attr_getscope(pthread_attr_t *attr, int *scope);
Para poñer os atributos a un thread, se debe encher primeiro unha estructura de tipo pthread_attr_t e logo pasarllo como argumento á función pthread_create, si a esta función lle pasamos NULL como atributo o thread collerá os atributos por defecto. A función pthread_attr_init inicializa o parámetro attr cos atributos por defecto dos threads, mentras que pthread_attr_destroy elimina os atributos que se crearon (en Linux non ten efecto). Pra manipular os distintos atributos que teñen os threads utilízanse o resto das funcións, sendo as funcións que comenzan por pthread_attr_set pra poñer atributos e as que comenzan por pthread_attr_get pra saber que atributos ten. Os atributos que se poden manipular son:
- detachstate : controla si o thread se crea nun estado no que se pode facer un pthread_join (PTHREAD_CREATE_JOINABLE) ou en estado desenganchado (como si lle fixeramos un pthread_detach) (PTHREAD_CREATE_DETACHED). O valor por defecto é PTHREAD_CREATE_JOINABLE.
- schedpolicy: controla a política de planificación do proceso (sheduling) vinculados ó thread creado. Pode ser SCHED_OTHER (planificación normal), SCHED_RR (round robin ou roubo de ciclo) ou SCHED_FIFO (planificación FIFO, tamén chamada FCFS (First Comed First Served)). O valor por defecto é SCHED_OTHER
- schedparam: conten os parámetros da política de planificación de procesos empregada no thread, normalmente a súa prioridade de execución. Por defecto a prioridade é 0 é unicamente ten sentido coas políticas SCHED_FIFO e SCHED_RR.
- inheritsched: Indica si as políticas de planificación e os seus parámetros se collen do proceso que lanza o thread (PTHREAD_INHERIT_SCHED) ou dos atributos indicados (schedpolicy e schedparam) no momento de crear o thread (PTHREAD_EXPLICIT_SCHED). Por defecto é PTHREAD_EXPLICIT_SCHED.
- scope: O único valor soportado en Linux é PTHREAD_SCOPE_SYSTEM que indica que os threads competirán co resto dos procesos do sistema pra utilizar a CPU. O outro valor especificado polo estándar pero non soportado en Linux é PTHREAD_SCOPE_PROCESS que indica que os threads únicamente compiten co resto de threads do proceso en funcionamento.
Mutex, Semáforos e Condicións de Sincronización
Supoñamos que temos un programa que lanza dous fíos de execución para atender conexións e vender entradas pra un espectáculo e que inicialmente temos 100 entradas. Deste xeito poderemos atender ós clientes de dous en dous.
Para vender as entradas cada fío comproba si quedan entradas (inicialmente temos 100). Si quedan, realiza a venta restándolle 1 ó número de entradas que quedan por vender.
A primeira vista este algoritmo é correcto, pero supoñamos que queda 1 entrada por vender e solicitan unha compra dous clientes ó mesmo tempo. Cada thread atenderá a un cliente; en primeiro lugar comprobarán si quedan entradas, vendo ambos threads que queda unha entrada e procedendo a efectuar a venta.
¡ Como resultado venderase unha entrada de máis !.
Este tipo de situacións reciben o nome de condicións de competencia ou condicións de carreira (racing condition), xa que varios procesos compiten por un recurso. Para solucionalas os threads proporcionan Mutex, Semáforos e as Condicións de Sincronización.
Mutex
Os mutex permiten bloquear unha parte do código, de xeito que ningún outro fío de execución poda acceder a él mentras non se libere o bloqueo. Deste xeito, si os threads bloquean o código que comproba o número de entradas que quedan e lo liberan despois de realizada a venta, obligamos a que primeiro execute a parte crítica un proceso e logo o outro.
As funcións de manexo de mutex son as seguintes:
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP; pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP; int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_destroy(pthread_mutex_t *mutex);
Un mutex é un dispositivo de exclusión mutua útil pra protexer estructuras de datos compartidas de modificacións concurrentes, e para implementar rexións críticas e monitores.
Os mutex teñen dous estados posibles: unlocked (libre, non ocupado por ningún thread) ou locked (ocupado por un thread). Un mutex nunca pode estar ocupado por máis de un thread.
Si un thread intenta ocupar un mutex xa ocupado, quedará a espera de que o thread que ten o mutex o libere.
A función pthread_mutex_init inicializa o obxecto mutex indicado polo parámetro mutex de acordo cos atributos indicados no parametro mutexattr. Si o parámetro mutexattr é NULL, utilizaránse atributos por defecto.
A implementación de Linux dos Threads (pthreads) únicamente soporta un atributo para os mutex: O tipo (Mutex Kind). Os mutex poden ser "fast", "recursive" ou "error checking" , indicando cando o mutex pode ser volto a ocupar por un thread que o ten xa ocupado (recursive). O tipo error checking verifica que o thread que quita o bloqueo sexa o mesmo que o puxo; nos outros tipos un thread pode quitar o bloqueo posto por un thread distinto. Por defecto o tipo é "fast".
As variables de tipo pthread_mutex_t poden ser inicializadas tamén de forma estática mediante as constantes PTHREAD_MUTEX_INITIALIZER (fast mutex), PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP (recursive mutex) e PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP (error checking mutex).
A función pthread_mutex_lock ocupa o mutex indicado como parámetro. Si o mutex non está ocupado por outro thread, o ocupa e a continúa a execución, si o mutex xa o ten ocupado outro thread, suspende a execución do thread ata que estea dispoñible.
Si o mutex xa está ocupado polo thread que intenta facer un novo pthread_mutex_lock, o comportamento depende do tipo de mutex. Si é fast, o thread queda a espera de que o mutex sexa liberado, o que producirá un dead lock (bloqueo indefinido); si é error checking a función devolverá o código de erro EDEADLK e si é recursive, o thread reocupa o mutex devolvendo o número de veces que o ten ocupado; para liberar o mutex será necesario facer o mesmo número de chamadas a pthread_mutex_unlock que veces se teña ocupado.
pthread_mutex_trylock é igual que pthread_mutex_lock, pero non bloquea o thread si o mutex está ocupado por outro, se non que devolve o código de erro EBUSY.
pthread_mutex_unlock "libera" o mutex ocupado polo thread. Si o mutex é fast , sempre se liberará pero si é recursive decrementará a conta de ocupación do mutex, liberándoo si é 0. No tipo error_check se verifica que o mutex pertence realmente o thread que intenta liberalo, e si non é así devolve un código de erro, os outros tipos permiten liberar mutex que non lles pertencen, sendo esta unha característica non portable e que non se debe utilizar (en realidade é un bug).
pthread_mutex_destroy destrúe un obxecto mutex liberando todos os recursos que teña ocupados. Únicamente se poderán destruir mutex que non estean ocupados por ningún thread. (Como en Linux os mutex non ocupan recursos, esta función en realidade non fai nada).
int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); int pthread_mutexattr_settype(pthread_mutexattr_t *attr,int kind); int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *kind);
- pthread_mutexattr_init inicializa o obxecto mutex attr e o inicializa cos valores por defecto.
- pthread_mutexattr_destroy destrúe un obxecto que ten atributos de un mutex. En Linux esta función non ten ningún efecto.
- pthread_mutexattr_settype e pthread_mutexattr_gettype sirven pra poñer e pra obter o tipo de mutex.
Os atributos poden ser PTHREAD_MUTEX_FAST_NP, PTHREAD_MUTEX_RECURSIVE_NP ou PTHREAD_MUTEX_ERRORCHECK_NP.
Semáforos
Outro mecanismo que poden utilizar os threads pra protexer seccións de código son os semáforos.
Un semáforo é similar a un mutex cun contador. As operacións principais son:
- esperar a que o contador sexa distinto de 0 e restar 1 o contador antes de continuar
- sumar 1 o contador
Si o contador é 0 indicará que o thread non pode acceder a sección crítica, e si é >0, poderán acceder tantos threads como valor teña o contador. As funcións de manexo de semáforos son:
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_wait(sem_t *sem); int sem_trywait(sem_t *sem); int sem_post(sem_t *sem); int sem_getvalue(sem_t *sem,int *sval); int sem_destroy(sem_t *sem);
sem_init inicializa o semáforo sem, poñendo a súa conta inicial a value. O argumento pshared indica cando o semáforo é local ó proceso actual (0) ou compartido entre varios procesos (distionto de 0). Linux únicamente soporta semáforos locais ó proceso actual (pshared debe ser igual a 0).
sem_wait suspende a execución do thread mentras a conta do semáforo sexa 0. Cando a conta deixe de ser 0, restarálle 1 e continuará a execución.
sem_trywait é similar a sem_wait pero en lugar de suspender a execución devolverá o erro EAGAIN
sem_post incrementará a conta do semáforo que se lle indique.
sem_getvalue devolverá a conta do semáforo sem en sval.
sem_destroy elimina o semáforo que se lle indique.
Condicións de Sincronización
Unha Condición de Sincronización permite suspender a execución de un thread menstras algúns datos compartidos entre varios threads non cumpran un requisito.
As operacións básicas nas condicíóns de sincronización son: Sinalizar unha Condición (indicar que o requisito se cumpre), e Esperar pola Condición (esperar a que outro thread sinalice a condición ).
Unha condición de sincronización sempre está asociada cun mutex pra evitar a condición de competencia que se produce si un thread vai a esperar por unha condición xusto ó mesmo tempo que outro thread a sinaliza, sin darlle a tempo de esperar por ela.
As funcións de manexo de condicións de sincronización son as seguintes:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr); int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime); int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_init inicializa a variable de condición cond usando os atributos especificados no parámetro cond_attr, ou os atributos por defecto si o parámetro é NULL. A implementación en Linux dos threads non soporta atributos para as condicións, de xeito que cond_attr é sempre ignorado. As variables de tipo pthread_cond_t tamén poden ser inicializadas estáticamente mediante a constante PTHREAD_COND_INITIALIZER.
pthread_cond_signal fai que continúe ún dos threads que están a espera da variable que se lle pasa como parámetro, pero non se sabe cal (pode ser calquera dos que están en espera pra esa condición).
pthread_cond_broadcast fai que continúen todos os threads que estan a esperar pola condición especificada como parámetro.
pthread_cond_wait desbloquea o mutex e suspende o thread á espera de que se cumpla a condición indicada (cond). Antes de chamar a esta función é necesario bloquea-lo mutex chamando á función pthread_mutex_lock. Antes de voltar, esta función volve a bloquea-lo mutex de xeito automático. Para evitar a condición de competencia xa comentada, o thread que sinalice a condición con pthread_cond_signal debe bloquea-lo mutex antes.
pthread_cond_timedwait traballa do mesmo xeito que pthread_cond_wait, pero su a condición non se cumple no tempo indicado no parámetro abstime, a función devolve o erro ETIMEDOUT. O tempo ven expresado no formato da función time (número de segundos dende o 1 de xaneiro de 1970).
pthread_cond_destroy libera unha variable de condición e todos os recursos que poida ter asociados. Na implementación de Linux non ten ningún efecto.
Tanto a función pthread_cond_wait como pthread_cond_timedwait son 'puntos de cancelación'.
Vexamos un caso práctico:
Consideremos dúas variables compartidas x e y protexidas polo mutex mut e a condición cond que será sinalizada cando x sexa maior que y:
int x,y;
pthread_mutex_t mut=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
Os threads que esperan, incluirán o seguinte código:
// Bloqueo a sección crítica pthread_mutex_lock (&mut); // En realidade bastaría un if, pero si hai varios threads... while(x<=y) { // Esperamos o sinal.. pthread_cond_wait(&cond,&mut); } /* Aquí traballaríamos con '''x''' e con '''y''' */ // Os outros threads poden continuar... pthread_mutex_unlock(&mut);
O thread que sinaliza a condición tería o código seguinte:
// Bloqueo a sección crítica pthread_mutex_lock(&mut); /* Aquí traballaríamos con '''x''' e con '''y''' */ // Sinalo a condición. Se únicamente tivera 1 thread en espera bastaría pthread_cond_signal if (x>y) pthread_cond_broadcast(&cond); // Libero o mutex pthread_mutex_unlock(&mut);
Se quixeramos que a espera tivera un timeout de 5 segundos:
struct timeval now; struct timespec timeout; int retcode; pthread_mutex_lock(&mut); gettimeofday(&now); timeout.tv_sec=now.tv_sec+5; timeout.tv_nsec=now.tv_usec*1000; retcode=0; while ((x<=y) && (retcode != ETIMEDOUT)) { retcode=pthread_cond_timedwait(&cond,&mut,&timeout); } if (retcode == ETIMEDOUT) { /* Error, acabouse o tempo (pasaron os 5 segundos) */ } else { /* Traballar coas variables '''x''' e '''y''' */ } pthread_mutex_unlock(&mut);
Cancelación de Threads
A cancelación é un mecanismo polo que un thread pode provocar a finalización de outro enviándolle unha petición de finalización. Dependendo do xeito de traballar do outro thread, pode ignora-la petición, finalizar inmediatamente ou esperar a alcanzar un punto de cancelación. Si o thread atende inmediatamente a petición remata devolvendo o código PTHREAD_CANCELED.
Os puntos de cancelación son puntos na execución donde se comproba si hai peticións de cancelación pendentes. As seguintes funcións teñen puntos de cancelación: pthread_join pthread_cond_wait pthread_cond_timedwait pthread_testcancel sem_wait sigwait
As funcions de cancelación de thread son as seguintes:
int pthread_cancel(pthread_t thread); int pthread_setcancelstate(int state, int *oldstate); int pthread_setcanceltype(int type,int *oldtype); int pthread_testcancel(void);
pthread_cancel envía unha petición de cancelación ó thread indicado como argumento.
pthread_setcancelstate cambia o estado de cancelación do thread, é decir indica cando se ignorarán as sinais de cancelación e cando non. O parámetro state pode ser PTHREAD_CANCEL_ENABLE pra permitir a cancelación ou PTHREAD_CANCEL_DISABLE pra non permitila. Si o parámetro oldstate non é NULL, devolverá o estado de cancelación previo.
pthread_setcanceltype cambia o tipo de resposta ás peticións de cancelación, e pode ser PTHREAD_CANCEL_ASYNCHRONOUS (pra cancelación inmediata) ou PTHREAD_CANCEL_DEFERRED (esperar ó seguinte punto de cancelación). Si o parámetro oldtype non é NULL almacenará o tipo de cancelación anterior.
pthread_testcancel únicamente sitúa un punto de cancelación no lugar en que se chame.
Os threads créanse por defecto coa cancelación activa co tipo PTHREAD_CANCEL_DEFERRED
Envío e Xestión de Sinais.
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask); int pthread_kill(pthread_t thread, int signo); int sigwait(const sigset_t *set, int *sig);
pthread_sigmask cambia o conxunto de sinais o que vai a respostar o thread. Si o parámetro oldmask non é NULL, almacenará o conxunto vello de sinais. Si how é SIG_SETMASK o conxunto de sinais se inicializa a newmask, si é SIG_BLOCK engádense as sinais especificadas en newmask e si é SIG_UNBLOCK quítanse as sinais especificadas en newmask do conxunto de sinais ós que vai respostar o thread. Os conxuntos de sinais manipúlanse coas funcións:
int sigemptyset(sigset_t *conxunto); // (vacía o conxunto de sinais indicado). int sigfillset(sigset_t *conxunto); // (inclúe todos os sinais existentes no conxunto indicado). int sigaddset(sigset_t *conxunto, int numsinal); // (engade o sinal indicado ó conxunto). int sigdelset(sigset_t *conxunto, int numsinal); // (elimina o sinal indicado do conxunto). int sigismember(const sigset_t *conxunto, int numsinal); // (indica si o sinal indicado pertence o conxunto).
pthread_kill envía o sinal signo o thread especificado.
sigwait suspende o thread hasta que recibe unha sinal do conxunto indicado en set, almacenando entonces o sinal recibido en sig. Se ignorará calqueira función asociada o sinal.
Exemplos
Creación dun Thread
Este programa crea dous fíos de execución que visualizan o mesmo tempo os números dende 0 a 1000000000. Pra distinguir o primeiro thread visualizará THREAD PPAL: <nº> e o thread que lanzamos visualizará THREAD 1: <nº>:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> void *funcion(void *arg); // Función que vai a ser executada como un thread secundario. // Visualiza números entre 0 e 1000000000 // void *funcion(void *arg) { unsigned int x; x=0; while(x<=1000000000) { printf("THREAD 1: %d\n",x); x++; } return NULL; } // Programa Principal. Visualizará números entre 0 e 1000000000 // despois de lanzar outro thread de execución. void main(void) { // Variable para almacenar a información do thread creado. // O thread principal é a función main(). pthread_t thr; // Variable pra almacenar o valor devolto polo thread executado. void *retval; unsigned int x; // Creación do novo thread. pthread_create(&thr,NULL,funcion,NULL); // A partir deste momento, 'funcion' executarase ó mesmo tempo. // que o seguinte código.... x=0; while(x<=1000000000) { printf("THREAD PPAL: %d\n",x); x++; } // Esperamos a que finalice o thread secundario. // (lanzado dende esta función). pthread_join(thr,&retval); }
Sincronización con Mutex
Este programa crea dous fíos de execución que visualizan o mesmo tempo os números dende 0 a 1000000000. Para distinguir o primeiro thread visualizará THREAD PPAL: <nº> e o thread que lanzamos visualizará THREAD 1: <nº>. Utilizaremos un mutex pra facer que primeiro faga toda a conta un thread e logo o outra:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> void *funcion(void *arg); // Variable mutex pra facer que un thread espere a que finalice o outro. // utilizaremos un mutex 'fast'. pthread_mutex_t semaforo=PTHREAD_MUTEX_INITIALIZER; void *funcion(void *arg) { unsigned int x; // Ocupamos o mutex (si non está ocupado). pthread_mutex_lock(&semaforo); x=0; while(x<=1000000000) { printf("THREAD 1: %d\n",x); x++; } // Liberamos o mutex. pthread_mutex_unlock(&semaforo); } void main(void) { // Variable pra almacenar a información do thread creado. . // O thread principal será a función main(). pthread_t thr; // Variable pra almacenar o valor devolto polo thread executado. void *retval; unsigned int x; // Creación do novo thread. pthread_create(&thr,NULL,funcion,NULL); // A partir deste momento, 'funcion' executarase simultáneamente. // Ocupamos o mutex (si non está ocupado). pthread_mutex_lock(&semaforo); x=0; while(x<=1000000000) { printf("THREAD PPAL: %d\n",x); x++; } // Liberamos o mutex. pthread_mutex_unlock(&semaforo); // Esperamos a que finalice o thread secundario. // (lanzado dende esta función). pthread_join(thr,&retval); }
Sincronización con Semáforos
Este programa crea dous fíos de execución que mercan entradas mentras quedan. Para distinguir o primeiro thread visualizará THREAD 1: e o segundo thread THREAD 2: .
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> int __numEntradas=100000; sem_t semaforo; void *funcion(void *arg); // Función que vai a ser executada como un thread secundario. // Visualiza números entre 0 e 1000000000 // void *funcion(void *arg) { while(__numEntradas>0) { sem_wait(&semaforo); if (__numEntradas>0) { printf("%s MERCA ENTRADA %d\n",arg,__numEntradas); __numEntradas--; } sem_post(&semaforo); } } // Programa Principal. Visualizará números entre 0 e 1000000000 // despois de lanzar outro thread de execución. void main(void) { // Variable para almacenar a información do thread creado. // O thread principal é a función main(). pthread_t thr,thr1; // Variable pra almacenar o valor devolto polo thread executado. void *retval; sem_init(&semaforo,0,1); // Creación do novo thread. pthread_create(&thr,NULL,funcion,"THREAD 1"); pthread_create(&thr1,NULL,funcion,"THREAD 2"); // Esperamos a que finalice o thread secundario. // (lanzado dende esta función). pthread_join(thr,&retval); pthread_join(thr1,&retval); printf("Quedan %d\n",__numEntradas); }
Procesos (fork)
Conceptos Xerais.
O Linux é un sistema operativo multitarefa, e como tal proporciona ás linguaxes de programación unha interface que permite a creación e o control de múltiples procesos. Un proceso é unha instancia dun programa en execución.
Nun instante determiñado hai moitos procesos executándose á vez. Un programa en execución pode crear un novo proceso, nese caso, o proceso resultante recibe o nome de fillo mentras que o proceso creador chámase pai.
Para optimizar o rendimento do sistema, os procesos créanse seguindo un esquema de creación en escritura, que consiste en crear a copia da páxina á que se accede únicamente cando se modifica o seu contido. Deste xeito o sistema traballa cunha única copia das páxinas de memoria que non cambian e aforra todo o proceso de duplicación no momento da creación do novo proceso.
O sistema Linux ten unha estructura de procesos xerárquica na que todos os procesos descenden do proceso init que é o primeiro que lanza o sistema. Todos os procesos descenden de init. Cando lanzamos un proceso dende a liña de comandos, ese proceso será fillo do intérprete que utilicemos, normalmente o Bash.
O sistema identifica os procesos cun número que recibe o nome de PID (Process IDentification), tendo cada proceso en execución no sistema un PID único. As funcións proporcionadas polo sistema pra crear e controlar os procesos tamén traballarán cos números de proceso (PID), non co seu nome.
Os procesos, identificados polo seu PID, teñen a capacidade de comunicarse con outros procesos e co exterior mediante ordes de entrada/saída que permiten acceder a ficheiros e dispositivos ou mediante operacións de comunicación de procesos (IPC).
En todos os casos, o elemento clave da comunicación é o descritor do obxecto de comunicación como poden ser ficheiros, pipes, sockets ... Cando un proceso precisa comunicarse utiliza unha chamada o sistema para crear un obxecto de comunicación, ou si xa existe para abrilo. Estas chamadas proporcionarán un descritor que servirá para referenciar a estructura de datos necesaria pra acceder ó dispositivo.
Cada proceso ten unha táboa cos descritores dos obxectos que pode manipular, sendo os descritores o índice que sinala a posición dos datos do obxecto na táboa de descritores (e dicir, un número enteiro). A manipulación dos obxectos de comunicación mediante chamadas o sistema consistirá en:
- Apertura ou creación do obxecto de comunicación pra obter o descritor.
- Realización de operacións de lectura ou escritura no obxecto mediante o descritor.
- Obtención de información do obxecto ou modificación das súas características.
- Peche do obxecto indicado polo descritor, liberando así os recursos ocupados.
Todos os procesos nos sistamas Unix teñen como mínimo tres descritores abertos: 0, que é o descritor do dispositivo de entrada estándar (stdin, normalmente o teclado), 1 ou saída estándar (stdout, normalmente a pantalla) e 2 que identifica o dispositivo de saída para informar dos erros (stderr, tamén a pantalla). E posible reasignar os descritores de xeito que referencien dispositivos ou ficheiros distintos da pantalla ou teclado, para facer esto pódese empregar a función:
int dup(int oldfd);
Esta función duplica o descritor indicado por oldfd na primeira entrada da táboa de descritores libre, ou sexa que si pechamos stdout:
// STDOUT_FILENO é unha constante // definida en unistd.h que indica o descritor da saída estándar close (STDOUT_FILENO); //logo duplicamos calqueira descritor.... dup(descritor); // Agora todo o que sería enviado á pantalla a través de stdout iría // ó ficheiro representado por '''descritor'''.
Como xa se comentou, cando un proceso crea un fillo, o fillo hereda unha copia da táboa de descritores do pai poidendo manipular os mesmos obxectos.
Creación e Xestión de Procesos.
As principais funcións de creación e xestion de procesos son as seguintes:
#include <stdlib.h> int system(const char *mandato);
Executa a orde especificada chamando a /bin/sh (ó interprete de comandos). Unha vez executado o comando a execución do proceso continúa. Un exemplo é un borrado de pantalla, que pode facerse como: system("clear")
#include <unistd.h> int execl (const char *path, const char *arg, ...); int execlp (const char *file, const char *arg, ...); int execle (const char *path, const char *arg, ..., char *const envp[]); int execv (const char *path, char *const argv[]); int execvp (const char *file, char *const argv[]); int execve (const char *filename, char *const argv[], char *const envp[]);
As funcións da familia exec sirven para executar un programa dende un proceso. O programa executado reemplazará ó proceso actual e si funciona correctamente non retornarán ningún valor.
#include <sys/types.h> #include <sys/wait.h> #include <unistd.h> pid_t fork (void); pid_t wait (int *status); pid_t getpid (void); pid_t getppid (void);
fork crea unha copia exacta do proceso en execución, herdando os descritores abertos. No proceso actual chamado proceso pai devolve o PID do novo proceso creado. E no novo proceso creado chamado fillo devolve un 0. En caso de erro se devolve -1.
wait fai que o proceso espere a recibir unha sinal de finalización dun dos seus fillos, recibe como parámetro a dirección dun enteiro (status) donde se almacenará o valor devolto pola función main do proceso fillo. Os procesos que creen outros (pais) deben esperar a que rematen os fillos antes de finalizar a súa execución.
getpid devolve o PID do proceso actual e getppid devolve o PID do proceso pai do actual.
Envío e Xestión de Sinais
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); int sigpending(sigset_t *set); int sigsuspend(const sigset_t *mask);
O Timer do Sistema
#include <sys/time.h> struct itimerval { struct timeval it_interval; /* valor próximo */ struct timeval it_value; /* valor actual */ }; struct timeval { long tv_sec; /* segundos */ long tv_usec; /* microsegundos */ }; int getitimer(int which, struct itimerval *value); int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
Comunicación de Procesos con Tuberías (Pipes)
Named Pipes (FIFOS)
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *path, mode_t mode)
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int mknod(const char *pathname, mode_t mode, dev_t dev);
Unnamed Pipes
#include <unistd.h> int pipe(int descf[2]);
Comunicación de Procesos IPC System V
Os mecanismos para comunicar procesos que se están a executar no mesmo ordenador son dous: A memoria compartida e as colas de mensaxes. Para utilizar calquera dos dous mecanismos é necesario obter en primeiro lugar un identificador único (chave IPC) que se utilizará para nomear a comunicación que se está a crear mediante a función:
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(char *camino,char proy);
Esta función recibe como parámetros o camiño a un ficheiro sobre o que os procesos a comunicar deben ter permiso (camino) e un byte (proy) para que se poda utilizar o mesmo camiño para crear identificadores distintos. O sistema crea o identificador combinando o ficheiro do camiño indicado co byte indicado en proy e devolve unha chave de comunicación System V. Todos os procesos que se queiran comunicar deben chamar a esta función cos mesmos argumentos. En caso de producirse un erro, devolve -1 colocando o código de erro na variable errno.
Memoria Compartida
A memoria compartida consiste en crear un espacio na memoria que pode ser utilizado de xeito simultáneo por varios procesos. Para que un proceso poda utilizar memoria compartida son necesarios os seguintes pasos:
1. Creación da chave de comunicación IPC
- Chamando á función ftok como se explicou no apartado anterior.
2. Creación da área de memoria compartida
- Chamando á función:
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, int size, int shmflg);
- Esta función recibe como parámetros a chave de comunicación (key), o tamaño desexado en bytes (size) e os atributos da memoria que se vai a crear (shmflg) e devolve un identificador da área de memoria compartida creada. Si se produce un erro devolve -1 e almacena o código de error en errno.
- O tamaño reservado realmente será sempre un múltiplo do tamaño de páxina do sistema (nos compatibles i386, 4K), e os atributos poden ser unha combinación dos seguintes:
- IPC_CREAT : Para crear o segmento de memoria.
- IPC_EXCL : Si cando intentamos crear o segmento de memoria este xa existe, saír con un erro.
- Permisos : permisos para o usuario, grupo e resto de usuarios sobre a memoria creada.
- A combinación dos atributos faise mediante un 'or', por exemplo: IPC_CREAT|0640
- Despois de chamar a:
- fork() - Herédanse os segmentos de memoria compartida.
- exec() - 'desenganchase' a memoria compartida (non se hereda).
- exit() - 'desenganchase' a memoria.
3. 'Enganchar' a memoria creada ó espacio de direccións de memoria do proceso
- E necesario mapear a memoria compartida creada no espacio de direccións de memoria do proceso, para poder acceder a ela. Unha vez mapeada é posible acceder a ela para leer ou escribir mediante un punteiro. A función necesaria para facer esto é :
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> char *shmat(int shmid,char *shmaddr,int shmflg);
- shmat 'pega' o segmento de memoria compartida identificado por shmid nunha dirección de memoria pertencente ó proceso actual. Si shmaddr é 0, a dirección en que será situada a memoria compartida calcúlase automáticamente entre o rango que vai dende 1 a 1.5GB empezando por la dirección máis alta. Si o parámetro shmflg é SHM_RDONLY, únicamente se poderá leer do segmento de memoria compartida engadido. Devolve a dirección donde estará situado o segmento de memoria compartida, ou -1 si se produce un erro, poñendo o seu código en errno.
4. Uso da memoria compartida.
- Unha vez que temos a dirección de memoria donde está "enganchada" a memoria compartida é posible almacenar información nela ou leer a información que xa ten. Cando xa non necesitemos a memoria compartida, teremos que liberala para o que seguiremos os pasos 5 e 6.
5. 'Desenganchar' a memoria compartida
- Deste xeito a memoria compartida deixa de formar parte do espacio de direccións de memoria do proceso. Para facer esto utilízase a función:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmdt(char *shmaddr);
- shmdt 'despega' un segmento de memoria compartida situado na dirección shmaddr. Se non se consigue devólvese -1 poñendo o código de erro en errno, no caso contrario devolve 0.
6. Liberar a memoria compartida
- En realidade o único que se fai é marcar a memoria como liberada, liberándose únicamente cando xa non exista ningún proceso que teña a memoria en uso.
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid,int cmd,struct shmid_ds *buf);
- Permite realizar operacións sobre a memoria compartida identificada por shmid. As operacións son:
- IPC_STAT : Devolve información sobre a memoria compartida no buffer buf.
- IPC_SET : Aplica os atributos situados en buf á memoria compartida. Únicamente pódense cambiar os atributos de uid, gid e os permisos de acceso.
- IPC_RMID :Marca o segmento de memoria compartida para ser eliminado. O segmento será eliminado cando ningún proceso o teña 'pegado'.
- En caso de éxito devolve 0, e se non -1 poñendo en errno o código de erro.
- O formato de buf es el siguiente:
struct shmid_ds { struct ipc_perm shm_perm; // permisos sobre la memoria compartida int shm_segsz; // tamaño del segmento en bytes time_t shm_atime; // hora de la última union time_t shm_dtime; // hora de la última separación time_t shm_ctime; // hora del último cambio unsigned short shm_cpid; // pid del proceso que creó el segmento unsigned short shm_lpid; // pid del último proceso que usó el segmento short shm_nattch; // número de procesos utilizando el segmento unsigned short shm_npages; // tamaño del segmento en páginas unsigned long *shm_pages; struct shm_desc *attaches; }; struct ipc_perm { key_t key; ushort uid; // UID y GID del dueño ushort gid; ushort cuid; // UID y GID del creador ushort cgid; ushort mode; // Permisos de acceso ushort seq; };
Colas de Mensaxes
As colas de mensaxes son un mecanismo de comunicación no que dous procesos poden comunicarse introducindo mensaxes nunha cola (FIFO, First Input First Output). Nesta cola poderán poñerse e quitarse mensaxes de distintos tipos.
Para crear unha cola de menxaxes, do mesmo xeito que na memoria compartida, é necesario obter en primeiro lugar un identificador IPC coa función ftok vista no apartado anterior. O resto de funcións de interese cítanse a continuación:
# include <sys/types.h> # include <sys/ipc.h> # include <sys/msg.h> int msgget(key_t key,int msgflg);
- Crea unha cola de mensaxes e devolve o identificador da cola de mensaxes asociada a key. O parámetro msgflg é similar á función shmget. Devolve -1 no caso de erro e almacena o código de erro en errno.
# include <sys/types.h> # include <sys/ipc.h> # include <sys/msg.h> int msgctl(int msgid,int cmd,struct msqid_ds *buf);
- Esta función é similar a shmctl. Os únicos campos que poden ser modificados con IPC_SET son msg_perm.uid, msg_perm.gid, msg_perm.mode e msg_qbytes. O formato de msqid_ds é o seguinte.
/* one msqid structure for each queue on the system */ struct msqid_ds { struct ipc_perm msg_perm; struct msg *msg_first; /* first message on queue */ struct msg *msg_last; /* last message in queue */ time_t msg_stime; /* last msgsnd time */ time_t msg_rtime; /* last msgrcv time */ time_t msg_ctime; /* last change time */ struct wait_queue *wwait; struct wait_queue *rwait; ushort msg_cbytes; ushort msg_qnum; ushort msg_qbytes; /* max number of bytes on queue */ ushort msg_lspid; /* pid of last msgsnd */ ushort msg_lrpid; /* last receive pid */ };
As funcións para ler mensaxes da cola e para poñer mensaxes na cola son as seguintes:
# include <sys/types.h> # include <sys/ipc.h> # include <sys/msg.h> int msgsnd(int msgid, struct msgbuf *msgp,int msgsz, int msgflg); int msgrcv(int msgid, struct msgbuf *msgp, int msgsz,int msgtyp, int msgflg);
- msgsnd pon unha mensaxe contida en msgp cun tamano msgsz na cola de mensaxes identificada por msgid, e msgrcv lé e quita da cola de mensaxes a mensaxe de tipo msgtype copiándoo en msgp.
struct msgbuf { long mtype; // Tipo de mensaxe, debe ser maior que 0 char mtext[1] // Información (dirección de la información) }
- O campo mtype da estructura identifica o tipo de mensaxe que se está a enviar. Esto é importante, xa que cando se reciba a mensaxe coa función msgrcv se especificará no parámetro msgtyp o tipo de mensaxe que se quere ler do seguinte xeito:
- En msgtyp 0: Se lé a primeira mensaxe da cola.
- En msgtyp un valor > 0: Se lé a primeira mensaxe de tipo msgtyp.
- En msgtyp un valor < 0: Se lé a primeira mensaxe cun tipo <= ó valor absoluto de msgtyp.
- msgflg pode ter os seguintes valores:
- IPC_NOWAIT O recibir unha mensaxe, se non hai ningún dispoñible devolve o erro ENOMSG. O enviar unha mensaxe, si non cabe na cola devolve EAGAIN. Se non especificamos IPC_NOWAIT, as funcións de envío e recepción son bloqueantes: Esperan a recibir unha mensaxe, ou esperan a poder enviala.
- MSG_EXCEPT Lé a primeira mensaxe de tipo distinto a msgtype.
- MSG_NOERROR Si a mensaxe é maior que msgsz, o trunca. Se non se pon este indicador, nese caso a función falla co erro E2BIG
- Ambas devolven -1 poñendo o código de erro en errno no caso de fallo. msgsnd devolve 0 no caso de execución correcta e msgrcv devuelve el número de bytes leídos.
Semáforos
Os semáforos sirven para o control do acceso a recursos compartidos entre moitos procesos. Os semáforos proporcionados polos mecanismos IPC System V se manexan coas seguintes funcións:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg)
Esta función crea un conxunto de nsems semáforos. Os flags (semflg) son similares ós flags dos outros mecanismos IPC System V (memoria compartida e colas de mensaxes). Devolve -1 en caso de erro. Os semáforos creados estarán sen inicializar, e será preciso facelo mediante a función semctl e as operacións SETALL ou SETVAL si é oportuno.
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *sops, unsigned nsops); int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout); struct sembuf { unsigned short int sem_num; /* semaphore number */ short int sem_op; /* semaphore operation */ short int sem_flg; /* operation flag */ };
A función semop permite operar cun conxunto de semáforos. As operacións indícanse nunha táboa de estructuras de tipo struct sembuf, que conteñen o número de semáforo do conxunto co que se vai a operar (sem_num), a operación a realizar sobre o semáforo (sem_op) e unha serie de indicadores (sem_flg).
As operacións poden ser:
- sem_op > 0. O valor indicado en sem_op se suma ó valor do semáforo e o proceso continúa a súa execución. É necesario permiso de escritura sobre o semáforo.
- sem_op == 0. O proceso queda bloqueado mentras o valor do semáforo sexa maior que 0, a non ser que se indique IPC_NOWAIT en sem_flg que voltaría un código de erro. Si se recibe unha sinal ou outro proceso elimina o semáforo, tamén continúa devolvendo un código de erro. É necesario permiso de lectura.
- sem_op < 0. Si o valor do semáforo é >= que abs(sem_op) se lle resta abs(sem_op) ó valor do semáforo e a execución do proceso continúa. Noutro caso, o proceso queda á espera de que o valor do semáforo sexa >= abs(sem_op) ou que se de unha das condicións indicadas para sem_op==0.
Si algunha operación falla, non se realizará ningunha.
En sem_flg ademáis de IPC_NOWAIT pode especificarse SEM_UNDO que causaría que ó finalizar o proceso se desfixera automáticamente a operación realizada sobre o semáforo.
semtimedop é igual que semop, pero permite especificar un tempo máximo de espera, tras o que a función voltaría un código de erro.
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> union semun { int val; /* valor para SETVAL */ struct semid_ds *buf; /* buffer para IPC_STAT, IPC_SET */ unsigned short int *array; /* array para GETALL, SETALL */ struct seminfo *__buf; /* buffer para IPC_INFO */ }; int semctl(int semid, int semnum, int cmd, union semun arg);
A función semctl sirve para controlar o semáforo realizando operacións como a eliminación do semáforo (IPC_RMID) ou a inicialización do semáforo (SETALLL, SETVAL).
- Con SETALL se poñen todos os semáforos creados semid ó valor indicado en arg.
- con SETVAL é posible poñer un semáforo do grupo semid ó valor indicado pasado na unión arg:
Comandos de Xestión de IPC
Existen dous comandos da shell para examinar os recursos IPC que están sendo utilizados nun momento dado e para eliminar os mesmos:
- ipcs
- ipcrm



