ÍNDICE DE CONTEÚDO
- Comunicação entre processos
- Biblioteca de Hardware
- fork, exec, e daemon
- Pipes
- FIFO
- Shared File
- Signal – O que é e como usar?
- MMAP
- Socket TCP
- TCP SSL
- Socket UDP
- UDP Broadcast
- UDP Multicast
- Socketpair
- Queue System V
- System V – Semaphore
- Shared Memory System V
- POSIX Queue
- POSIX Semaphore
- POSIX Shared Memory
- D-Bus
Em programação Multithread ou Multiprocessos existem pontos onde é necessário compartilhar a mesma informação, normalmente conhecidos como variáveis globais, ou de forma mais charmosa: “variáveis de contexto”. Onde essas variáveis guardam algum tipo de informação ou estado interno, e seu acesso de forma não sincronizada pode acarretar em um comportamento indesejável. Neste artigo será visto um IPC conhecido como Semaphore, que garante que isso tipo de coisa não aconteça.
Semaphore System V
Semaphore System V diferente dos outros IPC’s não é utilizado para transferir dados, mas sim para coordená-los. Para exemplificar tome uma variável global em um contexto onde se tem duas Threads, sendo a Thread A responsável por incrementar o conteúdo, e a Thread B responsável por decrementar esse conteúdo e estão concorrendo o acesso dessa posição de memória. Em um dado instante de tempo o conteúdo esteja com o valor 10, neste ponto a Thread A incrementa o conteúdo em 1 totalizando 11, porém no mesmo instante a Thread B decrementa totalizando 9. Por uma análise sequencial o valor esperado seria o próprio 10, mas ao fim totalizou 9 o que aconteceu aqui foi que as Threads concorreram resultando nesse resultado inesperado. Para garantir que o acesso seja feito de forma sincronizada é necessário estabelecer uma regra de acesso onde quando uma Thread estiver usando a outra precisa esperar para assim acessar. Os Semaphores podem ter dois tipos: contador e exclusão mútua.
Systemcalls
Para usar Semaphores é necessário algumas funções sendo a primeira semget que é responsável por criar o identificador do Semaphore.
1 2 3 4 5 |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg); |
semop é usado para alterar os valores do Semaphore, essa função possui uma alternativa para ser usado com o timeout.
1 2 3 4 5 6 7 |
#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); |
semctl controla diretamente as informações do semaphore
1 2 3 4 5 |
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ...); |
ipcs
A ferramenta ipcs é um utilitário para poder verificar o estado dos IPC’s sendo eles: Queues, Semaphores e Shared Memory, o seu funcionamento será demonstrado mais a seguir. Para mais informações execute:
1 |
$ man ipcs |
Implementação
Para facilitar a implementação a API do Semaphore foi abstraída para facilitar o uso.
semaphore.h
Nesse header é criado dois enums um para identificar o estado e o outro seu tipo. Foi criado uma estrutura que contém todos os argumentos necessários para manipular o semaphore
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
typedef enum { unlocked, locked } Semaphore_State; typedef enum { slave, master } Semaphore_Type; typedef struct { int id; int sema_count; int key; Semaphore_State state; Semaphore_Type type; } Semaphore_t; |
Aqui é criada uma abstração que permite uma leitura mais simplificada de como manipular o semaphore.
1 2 3 4 |
bool Semaphore_Init(Semaphore_t *semaphore); bool Semaphore_Lock(Semaphore_t *semaphore); bool Semaphore_Unlock(Semaphore_t *semaphore); bool Semaphore_Destroy(Semaphore_t *semaphore); |
semaphore.c
Para o controle do semaphore é necessário uma estrutura equivalente a que foi usada na queue que é usada para inicializar e destruir o semaphore
1 2 3 4 5 |
union semun{ int val; struct semid_ds *buf; unsigned short *array; }; |
Aqui p semaphore é inicializado utilizando uma chave, a quantidade, as permissões e flag de criação, logo em seguida é verificado qual o tipo e caso for o principal, seu valor recebe 1 isso permite que seja bloqueado em caso de uso.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
bool Semaphore_Init(Semaphore_t *semaphore) { bool status = false; do { if(!semaphore) break; semaphore->id = semget((key_t) semaphore->key, semaphore->sema_count, 0666 | IPC_CREAT); if(semaphore->id < 0) break; if (semaphore->type == master) { union semun u; u.val = 1; if (semctl(semaphore->id, 0, SETVAL, u) < 0) break; } status = true; } while(false); return status; } |
Para realizar o bloqueio utilizando a estrutura sembuf com os valores {0, -1, SEM_UNDO} utilizando a função semop após a operação setando seu estado para locked.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
bool Semaphore_Lock(Semaphore_t *semaphore) { bool status = false; struct sembuf p = {0, -1, SEM_UNDO}; do { if(!semaphore) break; if(semop(semaphore->id, &p, 1) < 0) break; semaphore->state = locked; status = true; } while(false); return status; } |
Para realizar a liberação utilizando a estrutura sembuf com os valores {0, 1, SEM_UNDO} utilizando a função semop após a operação setando seu estado para unlocked.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
bool Semaphore_Unlock(Semaphore_t *semaphore) { bool status = false; struct sembuf v = {0, 1, SEM_UNDO}; do { if(!semaphore) break; if(semop(semaphore->id, &v, 1) < 0) break; semaphore->state = unlocked; status = true; } while(false); return status; } |
Por fim, para remover o semaphore é utilizada a função semctl com a flag IPC_RMID com o identificador do sempahore.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
bool Semaphore_Destroy(Semaphore_t *semaphore) { union semun sem_union; bool status = false; do { if(!semaphore) break; if(semctl(semaphore->id, 0, IPC_RMID, sem_union) < 0) break; semaphore->state = unlocked; status = true; } while(false); return status; } |
Para demonstrar o uso desse IPC, será utilizado o modelo Produtor/Consumidor, onde o processo Produtor(button_process) vai escrever seu estado em uma mensagem, e inserir na queue, e o Consumidor(led_process) vai ler a mensagem da queue e aplicar ao seu estado interno. A aplicação é composta por três executáveis sendo eles:
- launch_processes – é responsável por lançar os processos button_process e led_process através da combinação fork e exec
- button_interface – é responsável por ler o GPIO em modo de leitura da Raspberry Pi e escrever o estado em uma mensagem e inserir na queue
- led_interface – é responsável por ler a mensagem da queue e aplicar em um GPIO configurado como saída
launch_processes.c
No main criamos duas variáveis para armazenar o PID do button_process e do led_process, e mais duas variáveis para armazenar o resultado caso o exec venha a falhar.
1 2 |
int pid_button, pid_led; int button_status, led_status; |
Em seguida criamos um processo clone, se processo clone for igual a 0, criamos um array de strings com o nome do programa que será usado pelo exec, em caso o exec retorne, o estado do retorno é capturado e será impresso no stdout e aborta a aplicação. Se o exec for executado com sucesso o programa button_process será carregado.
1 2 3 4 5 6 7 8 9 10 |
pid_button = fork(); if(pid_button == 0) { //start button process char *args[] = {"./button_process", NULL}; button_status = execvp(args[0], args); printf("Error to start button process, status = %d\n", button_status); abort(); } |
O mesmo procedimento é repetido novamente, porém com a intenção de carregar o led_process.
1 2 3 4 5 6 7 8 9 10 |
pid_led = fork(); if(pid_led == 0) { //Start led process char *args[] = {"./led_process", NULL}; led_status = execvp(args[0], args); printf("Error to start led process, status = %d\n", led_status); abort(); } |
button_interface.h
Para realizar o uso da interface de botão é necessário preencher os callbacks que serão utilizados pela implementação da interface, sendo a inicialização e a leitura do botão.
1 2 3 4 5 6 |
typedef struct { bool (*Init)(void *object); bool (*Read)(void *object); } Button_Interface; |
A assinatura do uso da interface corresponde ao contexto do botão, que depende do modo selecionado, o semaphore, e a interface do botão devidamente preenchida.
1 |
bool Button_Run(void *object, Semaphore_t *semaphore, Button_Interface *button); |
button_interface.c
A implementação da interface baseia-se em inicializar o botão, inicializar o semaphore, e no loop realiza o lock do semaphore e aguarda o pressionamento do botão que libera o semaphore para outro processo(nesse caso o processo de LED) utilizar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
bool Button_Run(void *object, Semaphore_t *semaphore, Button_Interface *button) { if (button->Init(object) == false) return false; if(Semaphore_Init(semaphore) == false) return false; while(true) { if(Semaphore_Lock(semaphore) == true) { wait_press(object, button); Semaphore_Unlock(semaphore); } } Semaphore_Destroy(semaphore); return false; } |
led_interface.h
Para realizar o uso da interface de LED é necessário preencher os callbacks que serão utilizados pela implementação da interface, sendo a inicialização e a função que altera o estado do LED.
1 2 3 4 5 |
typedef struct { bool (*Init)(void *object); bool (*Set)(void *object, uint8_t state); } LED_Interface; |
A assinatura do uso da interface corresponde ao contexto do LED, que depende do modo selecionado, o semaphore, e a interface do LED devidamente preenchida.
1 |
bool LED_Run(void *object, Semaphore_t *semaphore, LED_Interface *led); |
led_interface.c
A implementação da interface baseia-se em inicializar o LED, inicializar o semaphore, e no loop realiza o lock do semaphore e altera o seu estado e libera o semaphore para outro processo(nesse caso o processo de Button) utilizar.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
bool LED_Run(void *object, Semaphore_t *semaphore, LED_Interface *led) { int state = 0; if(led->Init(object) == false) return false; if(Semaphore_Init(semaphore) == false) return false; while(true) { if(Semaphore_Lock(semaphore) == true) { led->Set(object, state); state ^= 0x01; Semaphore_Unlock(semaphore); } else usleep(_1ms); } } |
Compilando, Executando e Matando os processos
Para compilar e testar o projeto é necessário instalar a biblioteca de hardware necessária para resolver as dependências de configuração de GPIO da Raspberry Pi.
Compilando
Para facilitar a execução do exemplo, o exemplo proposto foi criado baseado em uma interface, onde é possível selecionar se usará o hardware da Raspberry Pi 3, ou se a interação com o exemplo vai ser através de input feito por FIFO e o output visualizado através de LOG.
Clonando o projeto
Pra obter uma cópia do projeto execute os comandos a seguir:
1 2 3 4 |
$ git clone https://github.com/NakedSolidSnake/Raspberry_IPC_Semaphore_SystemV $ cd Raspberry_IPC_Semaphore_SystemV $ mkdir build && cd build |
Selecionando o modo
Para selecionar o modo devemos passar para o cmake uma variável de ambiente chamada de ARCH, e pode-se passar os seguintes valores, PC ou RASPBERRY, para o caso de PC o exemplo terá sua interface preenchida com os sources presentes na pasta src/platform/pc, que permite a interação com o exemplo através de FIFO e LOG, caso seja RASPBERRY usará os GPIO’s descritos no artigo.
Modo PC
1 2 3 |
$ cmake -DARCH=PC .. $ make |
Modo RASPBERRY
1 2 3 |
$ cmake -DARCH=RASPBERRY .. $ make |
Executando
Para executar a aplicação execute o processo launch_processes para lançar os processos button_process e led_process que foram determinados de acordo com o modo selecionado.
1 2 |
$ cd bin $ ./launch_processes |
Uma vez executado podemos verificar se os processos estão rodando atráves do comando
1 |
$ ps -ef | grep _process |
O output
1 2 |
cssouza 21140 2321 0 08:04 pts/6 00:00:00 ./button_process cssouza 21141 2321 0 08:04 pts/6 00:00:00 ./led_process |
Interagindo com o exemplo
Dependendo do modo de compilação selecionado a interação com o exemplo acontece de forma diferente
MODO PC
Para o modo PC, precisamos abrir um terminal e monitorar os LOG’s
1 |
$ sudo tail -f /var/log/syslog | grep LED |
Dessa forma o terminal irá apresentar somente os LOG’s referente ao exemplo.
Para simular o botão, o processo em modo PC cria uma FIFO para permitir enviar comandos para a aplicação, dessa forma todas as vezes que for enviado o número 0 irá logar no terminal onde foi configurado para o monitoramento, segue o exemplo
1 |
$ echo '0' > /tmp/semaphore_file |
Output dos LOG’s quando enviado o comando algumas vezez
1 2 3 4 5 6 |
Jun 30 08:12:29 dell-cssouza LED SEMAPHORE[21141]: LED Status: On Jun 30 08:12:31 dell-cssouza LED SEMAPHORE[21141]: LED Status: Off Jun 30 08:12:32 dell-cssouza LED SEMAPHORE[21141]: LED Status: On Jun 30 08:12:32 dell-cssouza LED SEMAPHORE[21141]: LED Status: Off Jun 30 08:12:33 dell-cssouza LED SEMAPHORE[21141]: LED Status: On Jun 30 08:12:33 dell-cssouza LED SEMAPHORE[21141]: LED Status: Off |
MODO RASPBERRY
Para o modo RASPBERRY a cada vez que o botão for pressionado irá alternar o estado do LED.
ipcs funcionamento
Para inspecionar os semaphores presentes é necessário passar o argumento -s que representa queue, o comando fica dessa forma:
1 |
$ ipcs -s |
O Output gerado na máquina onde o exemplo foi executado
1 2 3 |
------ Semaphore Arrays -------- key semid owner perms nsems 0x000004d2 0 cssouza 666 2 |
Matando os processos
Para matar os processos criados execute o script kill_process.sh
1 2 |
$ cd bin $ ./kill_process.sh |
Conclusão
O semaphore é um recurso muito útil que serve para sincronizar o acesso de regiões de memória compartilhada, conhecida também como sessão crítica, porém as vezes o seu uso pode acarretar problemas de deadlocks, o que torna difícil de depurar e encontrar o problema em um programa multithreaded ou multiprocesso. Normalmente é utilizado em conjunto com a Shared Memory outro IPC que será visto no próximo artigo. Mas para evitar esse tipo de preocupação uma solução mais adequada seria usar sockets ou queues para enviar as mensagens para os seus respectivos interessados.