quarta-feira, 13 de fevereiro de 2008

Linux - Programação C - Comunicação entre Processos

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, Lin, Col, idx, Jogador;

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) || ( Col <>Col > 3 ) )

printf("\nEntrada Invalida!");

else {

Lin--;

Col--;

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;