Comunicação entre processos
(IPC - Interprocess Communication)
Por Ricardo Lima Caratti
Implementando o Jogo da Velha utilizando Semáforo e Memória Compartilhada
É muito comum no mundo do desenvolvimento de aplicações, deparar-se com a necessidade de dois ou mais processos se comunicam entre si. Isso ocorre quando se deseja principalmente: Compartilhar informações pertinentes a dois ou mais processos, dividir sistemas complexos em módulos menores e aumentar a velocidade de processamento utilizando-se arquiteturas de hardware paralelas.
O Linux, bem como em outros sistemas operacionais, fornecem mecanismos que facilitam a comunicação entre processos. São eles: Pipes, Filas, Semáforos e Memória Compartilhada. Dado a extensão desse assunto, será visto aqui somente a utilização de Semáforos e de Memória compartilhada. Para ilustrar o uso de comunicação entre processos, será implementado o jogo da velha, onde duas cópias do mesmo processo compartilharão uma mesma região de memória. Mais informações sobre o jogo serão descritas a seguir.
Comunicando Processos por meio de Semáforo e Memória Compartilhada
A figura 1 mostra o diagrama de comunicação entre dois processos usando memória compartilhada e semáforo. Note que a função do semáforo é controlar o acesso a memória de tal forma que somente um processo de cada vez tenha acesso.
Figura 1 – Os processos A e B compartilham informações contidas na memória. O semáforo controla acesso a memória.
Memória Compartilhada
Esse método consiste em fazer uso de uma área reservada de memória RAM onde os processos poderão trocar informações. Basicamente utiliza-se referências a uma região de memória em cada processo. Portanto, é a forma mais rápida de Comunicação entre Processos. A configuração para uso de memória compartilhada no Linux será vista mais adiante.
Semáforos
Na vida cotidiana, é possível observar a aplicação de semáforos nos cruzamentos de vias com alto fluxo de veículos. Nesse caso, o semáforo tem a função de controlar o fluxo de tal forma que todas a vias tenham condições de atender suas demandas. De forma similar em computação, semáforos são usados para gerenciar ações entres processos, reservando para cada um, o direito de ter acesso em um determinado momento a uma área de troca de informações.
Configurando o Linux para usar memória compartilhada para o programa Jogo da Velha.
Utilize um editor de texto e altere o arquivo lilo.conf existente no diretório /etc segundo mostrado a seguir em negrito:
image = /vmlinuz
append=”mem=XXm”
root=/dev/hda
label = linux
XX é a posição de memória que será reservada para compartilhamento, ou seja, se você tiver 32 MB de RAM XX deve ser igual a 31, indicado que o último MB de memória será reservado. Da mesma forma, se seu computador tiver 64 MB de RAM, utilize XX igual a 63.
Somente a linha em negrito deve ser incluída no arquivo. As demais dependerão da sua instalação, distribuição, versão do kernel e outras configurações. Portanto, não necessariamente serão iguais as apresentadas acima. Após a alteração do arquivo lilo.conf, execute o comando lilo a partir do prompt do shell. Em seguida o sistema deve ser reiniciado.
O programa Jogo da Velha
Para fazer demonstração do uso de Memória Compartilhada, foi desenvolvido o programa Jogo da Velha. Esse programa faz uso do segmento de memória compartilhada onde conterá as informações importantes do jogo. Você poderá jogar usando duas consoles ou terminais no KDE e executar o mesmo jogo em cada um. Idealmente, seria melhor executar o programa em máquinas distintas, ou seja, por meio de um telnet você poderia se conectar na máquina que contem o jogo e executar. O seu adversário poderia fazer o mesmo em um outro computador e também executar o mesmo jogo. Com isso, o programa automaticamente identificará que o jogo deu inicio e apresentará a seguinte tela:
Jogador1 x Jogador2
1 2 3
| |
1 | |
--------------
| |
2 | |
--------------
| |
3 | |
Entre com a Linha e a Coluna:
Para fazer uma jogada válida, você deve entrar com os valores de linha e coluna da seguinte forma:
11 se quiser marcar o quadro superior esquerdo, 22 se quiser marcar o quadro central. A figura abaixo mostra uma jogada onde o jogador marcou a linha 2 coluna 1.
1 2 3
| |
1 | |
--------------
X | |
2 X | |
--------------
| |
3 | |
Quando um jogador marcar uma linha ou uma coluna completa ou ainda preencher uma das diagonais, o jogo terminará dando a ele a vitória.
Fontes
O programa jogo da velha consiste em três arquivos fontes. semaforo.c, semaforo.h e JogoDaVelha.c. Semaforo.c é uma pequena biblioteca de funções para manipulação de semáforo. Os trechos considerados relevantes para compreensão do programa estão em negrito e serão comentados mais adiante. Em semaforo.h estão declaradas essas funções que serão usadas em JogoDaVelha.c.
Arquivo semaforo.h
int ObtemValor(int sid, int member);
int AbreSemaforo(int *sid, key_t key);
int CriaSemaforo(int *sid, key_t key, int members);
int LockSemaforo( int sid, int member );
int UnLockSemaforo( int sid, int member);
void RemoveSemaforo(int sid) ;
void RemoveSemaforo(int sid) ;
Arquivo semaforo.c
#include
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
// Obtém o valor do semáforo
int ObtemValor(int sid, int member)
{
return ( semctl(sid,member, GETVAL,0) );
}
// Atribui um valor ao semáforo
int AtribuiValor(int sid, int member, int valor )
{
union semun semopts;
semopts.val = valor;
return (semctl(sid,member,SETVAL, semopts));
}
// Abre um semáforo; retorna -1 se não tiver sucesso.
int AbreSemaforo(int *sid, key_t key)
{
return ( (*sid = semget(key,0,0666)) == -1)? -1: *sid;
}
// Cria um semáforo; retorna -1 se não tiver sucesso.
int CriaSemaforo(int *sid, key_t key, int members)
{
int i;
union semun semopts;
if ((*sid=semget(key,members,IPC_CREAT|IPC_EXCL|0666))==-1)
return -1;
else {
semopts.val = 1;
for ( i= 0; i <>
semctl(*sid,i,SETVAL,semopts);
return *sid;
}
}
// Prende o semáforo ate a utilização de UnLockSemaforo, caso
// esteja em uso, espera ate ser liberado; retorna -1 se erro.
int LockSemaforo( int sid, int member )
{
struct sembuf sem_lock = {0,-1,0};
sem_lock.sem_num = member;
return ( (semop(sid,&sem_lock,1) ) == -1)? -1:(sid);
}
// Libera o semáforo permitido sua utilização; retorna -1 se erro
int UnLockSemaforo( int sid, int member)
{
struct sembuf sem_unlock = {member,1,0};
int semval;
sem_unlock.sem_num = member;
return ((semop(sid,&sem_unlock,1) ) == -1)? -1:(sid);
}
// Remove o semáforo
void RemoveSemaforo(int sid)
{
semctl(sid,0,IPC_RMID,0);
}
Arquivo JogoDaVelha.c
#include
#include
#include
#include "semáforo.h"
#define ENDERECO (63*0x100000)
struct jv {
int Jogador;
char Nome[2][20];
unsigned char Matr[3][3];
};
const char marca[] = { 'X','O'};
// Limpa a matriz do Jogo
void Limpa( unsigned char Matr[3][3] )
{
int i,j;
for ( i = 0; i <>
for (j = 0; j <>
Matr[i][j] = ' ';
}
// Verifica se existe alguma linha preenchida
int Linha(unsigned char Matr[3][3], int Jogador)
{
int i,j,k;
for ( i = 0; i <>
k = 0;
for ( j = 0; j <>
if (Matr[i][j]==marca[Jogador]) k++;
if ( k == 3 ) return 1; // Verdade
}
return 0; // Falso
}
// Verifica se existe alguma coluna preenchida
int Coluna( unsigned char Matr[3][3], int Jogador )
{
int i,j,k;
for ( i = 0; i <>
k = 0;
for ( j = 0; j <>
if (Matr[j][i]==marca[Jogador]) k++;
if ( k == 3 ) return 1; // Verdade
}
return 0; // Falso
}
// Verifica se Existe alguma diagonal preenchida
int Diagonal(unsigned char Matr[3][3],int Jogador)
{
return (Matr[0][0] == marca[Jogador] &&
Matr[1][1] == marca[Jogador] &&
Matr[2][2] == marca[Jogador]) ||
(Matr[0][2] == marca[Jogador] &&
Matr[1][1] == marca[Jogador] &&
Matr[2][0] == marca[Jogador]);
}
// Mostra a matriz do Jogo da Velha
void Mostra ( struct jv *jdv )
{
printf("\n%20s x %20s\n",jdv->Nome[0], jdv->Nome[1]);
printf("\n\n 1 2 3 ");
printf("\n1 %c | %c | %c ",jdv->Matr[0][0], jdv->Matr[0][1], jdv->Matr[0][2]);
printf("\n %c | %c | %c ",jdv->Matr[0][0], jdv->Matr[0][1], jdv->Matr[0][2]);
printf("\n -------------");
printf("\n2 %c | %c | %c ",jdv->Matr[1][0], jdv->Matr[1][1], jdv->Matr[1][2]);
printf("\n %c | %c | %c ",jdv->Matr[1][0], jdv->Matr[1][1], jdv->Matr[1][2]);
printf("\n -------------");
printf("\n3 %c | %c | %c ",jdv->Matr[2][0], jdv->Matr[2][1], jdv->Matr[2][2]);
printf("\n %c | %c | %c ",jdv->Matr[2][0], jdv->Matr[2][1], jdv->Matr[2][2]);
printf("\n");
}
// Programa Principal
int main()
{
int fd, id,
char sCmd[4];
key_t chave_unica;
struct jv *JogoDaVelha;
// Cria ou abre um semáforo
chave_unica = ftok("/",'c');
if((id=AbreSemaforo(&id,chave_unica))==-1){
if((id=CriaSemaforo(&id,chave_unica,2)) ==-1){
AtribuiValor(id,0,1);
printf("\nNao foi possivel iniciar o jogo\n");
exit(2);
}
Jogador = 0;
printf("Você e o Jogador 1");
printf("\nAguardando outro jogador\n");
}
else {
Jogador = 1;
printf("\nVocê e o Jogador 2");
printf("\nJogo Iniciado\n");
}
// Abre device para manipular memória compartilhada
if ((fd = open("/dev/mem",O_RDWR)) <>
printf("\nNao foi possivel abrir mem. compartilhada!");
exit(1);
}
// Faz ponteiro da estrutura do jogo da velha apontar para memória comp.
JogoDaVelha = (struct jv *) mmap(0,sizeof(struct jv),PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, ENDERECO);
if ( Jogador == 0 )
JogoDaVelha->Jogador = 0;
printf("\nEntre com o seu nome: ");
fgets(JogoDaVelha->Nome[Jogador],20,stdin);
Limpa(JogoDaVelha->Matr);
while ( 1 )
{
LockSemaforo(id,1); // Segura a sua vez de jogar
if (ObtemValor(id,1) == -1) {
Mostra(JogoDaVelha);
printf("\nFim do Jogo.\nVoce Perdeu!\n");
return 0;
}
Mostra(JogoDaVelha);
// Jogada. Não permite uma jogada invalida
do {
printf("\nEntre com a Linha e a Coluna (exemplo: 11): ");
fgets(sCmd,3, stdin);
Lin = sCmd[0] - 48; // Converte valor da Linha para inteiro
Col = sCmd[1] - 48; // Converte valor da coluna para inteiro
if ( (Lin <> 3) || (
printf("\nEntrada Invalida!");
else {
Lin--;
if ( JogoDaVelha->Matr[Lin][Col] != ' ' )
printf("\nJogada ha Efetuada. Tente Outra!");
else
break;
}
} while (1);
idx = JogoDaVelha->Jogador = Jogador; // Jogador da vez
JogoDaVelha->Matr[Lin][Col] = marca[idx]; // X ou O
// Verifica se houve vencedor
if(Linha(JogoDaVelha->Matr, JogoDaVelha->Jogador ) ||
Coluna(JogoDaVelha->Matr, JogoDaVelha->Jogador ) ||
Diagonal(JogoDaVelha->Matr, JogoDaVelha->Jogador) ) {
Mostra(JogoDaVelha);
printf("\nFim do Jogo\nVoce Venceu\n");
RemoveSemaforo(id);
return 0;
}
Mostra(JogoDaVelha);
UnLockSemaforo(id,1); // Libera para o outro jogar
sleep(2);
}
}
Comentários sobre os programas
As chamadas as funções do kernel bem como as funções padrões do C são ricamente comentadas nas documentações existentes no próprio Linux. Para saber sobre a função mmap por exemplo, basta executar o comando man mmap, da mesma forma, para saber mais sobre a função printf utilize o comando man printf. Portanto, essas funções não serão comentadas aqui.
Arquivo semaforo.c
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
Essa estrutura é a utilizada pelas funções de chamadas ao kernel para manipular semáforos.
semctl
Essa função, dependendo dos seus argumentos, poderá ter várias utilidades. Aqui ela é usada praticamente para Ler um valor do semáforo ou para gravar um valor. Veja mais executando o comando man semctl.
semget
Essa função é usada para Criar ou abrir um semáforo dependendo dos seus argumentos. Veja mais executando o comando man semget.
semop
Essa função é utilizada para fazer operações com o semáforo. É utilizada para segurar ou liberar um recurso. Veja mais com man semop.
Arquivo JogoDaVelha.c
#define ENDERECO (63*0x100000)
define a posição de memória que será mapeada pela função mmap.
struct jv {
int Jogador;
char Nome[2][20];
unsigned char Matr[3][3];
};
Essa e a estrutura necessária para conter as informações do jogo. Jogador conterá 0 ou 1 dependendo de quem for a vez de jogar, Nome[2][20] conterá os nomes dos jogadores e Matr[3][3] conterá a matriz do jogo da velha.
const char marca[] = { 'X','O'};
Essa constante contem a marca de cada jogador, ou seja se for o jogador 0, será X, se for o jogador 1 será O.
chave_unica = ftok("/",'c');
A variável chave_unica conterá um identificador único obtido pela função ftok. Isso é necessário para não haver conflito com outros programas que usam memória compartilhada. Veja mais sobre ftok usando man ftok.
if((id=AbreSemaforo(&id,chave_unica))==-1){
if((id=CriaSemaforo(&id,chave_unica,2)) ==-1){
AtribuiValor(id,0,1);
printf("\nNao foi possivel iniciar o jogo\n");
exit(2);
}
Jogador = 0;
printf("Voce e o Jogador 1");
printf("\nAguardando outro jogador\n");
}
else
{
Jogador = 1;
printf("\nVoce e o Jogador 2");
printf("\nJogo Iniciado\n");
}
No trecho acima, a função AbreSemaforo tenta abrir o semáforo. Caso não consiga, ou seja retorne –1, então é assumido que o semáforo ainda não foi criado. Nesse caso a função CriaSemaforo entra em ação e cria um semáforo. Caso a função CriaSemaforo falhe, ou seja, retorne -1, o programa sair imediatamente informado que houve uma falha. Nesse caso o jogo não prossegue.
if ((fd = open("/dev/mem",O_RDWR)) <>
printf("\nNao foi possivel abrir mem. compartilhada!");
exit(1);
}
Nesse trecho, é usada a chamada ao kernel open para abrir um dispositivo de manipulação memória.
JogoDaVelha = (struct jv *) mmap(0,sizeof(struct jv), PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, ENDERECO);
O trecho anterior, usa a função mmap para manipular o segmento de memória compartilhada reservada pelo sistema. Note que ela usa a variável fp que recebeu um descritor por meio da função open. Veja mais sobre mmap usando man mmap.
LockSemaforo(id,1); // Segura a sua vez de jogar
O código acima, chama a função LockSemaforo implementada em semáforo.c e que na realidade é uma abstração para a função semop. Sua função é prender o recurso de tal forma que o outro jogador não possa jogar.
if(Linha(JogoDaVelha->Matr, JogoDaVelha->Jogador ) ||
Coluna(JogoDaVelha->Matr, JogoDaVelha->Jogador ) ||
Diagonal(JogoDaVelha->Matr, JogoDaVelha->Jogador) ) {
Mostra(JogoDaVelha);
printf("\nFim do Jogo\nVoce Venceu\n");
RemoveSemaforo(id);
return 0;
}
O trecho acima chamas as funções auxiliares Linha, Coluna e Diagonal, implementadas no arquivo principal, JogoDaVelha.c, para testar se o jogador conseguio preencher um linha, uma coluna ou uma diagonal. Caso afirmativo, o jogo e encerrado dando a vitória para o jogador da vez.
UnLockSemaforo(id,1); // Libera para o outro jogar
Ao contrário de LockSemaforo, a função UnLockSemaforo dá a oportunidade ao outro jogador. Ela também é uma abstração para a função semop.
Compilação do programa
Utilize seqüência de comandos a seguir para compilar e gerar o programa executável:
gcc –c semaforo.c
gcc –o jogodavelha jogodavelha.c semaforo.o
Conclusão
O Jogo da Velha foi só um exemplo de como usar memória compartilhada e semáforos no Linux. Essa técnica poderá trazer grandes benefícios em desenvolvimento de aplicações profissionais. Principalmente quando há a necessidade de criar módulos ou programas independentes que tenham a necessidade de interação. Dentre as formas de comunicação entre processo mencionadas no início deste artigo, Memória Compartilhada se destaca pela sua facilidade em usar e pela sua notável performance.
Referências Bibliográficas
· GOLDT, SEVEN.The Linux Programmer´s Guide,Version 0.4,1997(Existente no formato PDF e distribuído no Cd da RevistaDoLinux Nº 4).
· WALL, WATSON, AND WHITIS. Linux Programming. SAMS, 1999;
· TANENBAUM, ANDREW S. Operating Systems Design and Implementation. Prentice-Hall, 1987;
· STEVENS, W. RICHARD. Unix Network Programming. Volume 2, 2ª ed. , Prentice Hall, 1999;
· BACH, MAURICE J. The Design of the Unix Operating System. Prentice Hall, 1986;