Control, Sincronización e Comunicación entre Procesos

De Wiki do Ciclo ASIR do IES de Rodeira

Í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.

Modelo:Programación con Threads Baixo Linux

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