sexta-feira, 22 de fevereiro de 2008

PROGRAMAÇÃO C - APONTADORES E VETORES


Considerações sobre Apontadores e Vetores em C


Talvez a maior dificulade em um curso de linguagem C seja a compreenção quanto ao uso de ponteiros e vetores (arrays). Ao contrário da maioria das linguagens, existe em C, uma forte relação entre apontadores e vetores. Tão forte que apontadores e vetores deveriam ser discutidos juntos. Em C, qualquer operação que possa ser feita com subscritos de um vetor pode ser feita com apontadores. Assim, o objetivo deste texto será mostrar essa relação.

Considere a declaração abaixo:

// Programa 1

void main()

{

int a[10];

}

Essa declaração define um vetor de escopo local de tamanho 10, isto é, um bloco com 10 variáveis do tipo int consecutivos chamados a[0],a[1],...,a[9]. A figura abaixo tenta mostrar, de forma hipotética, como seria a disposição de memória para o vetor <a>. O símbolo ? representa um valor desconhecido, pois, no momento em que uma variável de escopo local ganha vida, ou seja, é instanciada, o seu valor inicial é desconhecido.


?

?

?

?

?

?

?

?

?

?

Conteúdo

0

1

2

3

4

5

6

7

8

9

Subscrito



A notação a[i] refere-se ao elemento da i-ésima posição do vetor.


Considere o programa abaixo. Note que ele declara o vetor <a> e atribui o valor 0 (zero) para cada elemento.


// Programa 2

void main()

{

int a[10];

int I;

for ( I = 0; I <>

a[I] = 0;

}


Observe o que aconteceu com os valores do vetor <a> após a sua execução:


0

0

0

0

0

0

0

0

0

0

Conteúdo

0

1

2

3

4

5

6

7

8

9

Subscrito





Considere agora a possibilidade de fazer a mesma operação utilizando ponteiros.

Para tanto, vamos ver uma breve introdução a ponteiros em C.

Em C, um ponteiro é uma variável que contém um endereço de memória, ou seja, no lugar de armazenar um valor do tipo “Quantidade de Alunos Matriculados”, ou algo que o valha, ele é usado para apontar para um endereço de memória previamente alocado.


Vamos analisar o programa a seguir:


// Programa 3

void main()

{

int *pa;

int b = 10;

pa = &b;

*pa = 6;

printf(“\nO valor de b eh: %d\n”,b);

}


  1. A intenção da declaração int *pa foi de declarar uma variável que irá apontar para uma posição de memória que será representada como inteiro (int), ou seja, <pa> é um ponteiro para um tipo int.

  2. A variável <b>, declarada como inteira, poderá ser a posição de memória desejada para ser manipulada pelo ponteiro <pa>. Note que o valor inicial de b é 10;

  3. Para atribuir o endereço da variável b ao ponteiro pa, usamos o operador &, ou seja, pa = &b. Isso significa: Atribua a <pa> o endereço de memória de <b>.

  4. Posso agora manipular o endereço de memória que <pa> aponta da seguinte forma: *pa = 6. Isso pode ser interpretado como: Coloque o valor 6 na célula de memória apontada por <pa>, ou seja, se <pa> aponta para <b>, logo b será igual a 6 após a execução dessa instrução.


O uso de ponteiros em C deve ser bastante racional. O menor erro na manipulação com ponteiros poderá causar resultados inesperados. Usando o exemplo anterior, se no lugar de *pa = 6 fosse usado pa = 6, o programa não atingiria o seu propósito.


Voltando com a idéia inicial com relação a vetores. Se <pa> for uma apontador para um inteiro, declarado por


int *pa;


E <a> for um um vetor de inteiros, declarado por


int a[10];

então a atribuição

pa = &a[0];

ou

pa = a;

define <pa> de modo que aponte para o elemento zero do vetor <a> ; isto é, <pa> contém o endereço de a[0].


Continuando com o raciocínio e como já foi mostrado no início, a figura abaixo representa a disposição de memória para o vetor <a>. Note a relação entre subscritos do vetor a e o ponteiro <pa>.












Conteúdo

0

1

2

3

4

5

6

7

8

9

Subscrito

pa+0

pa+1

pa+2

pa+3

pa+4

pa+5

Pa+6

pa+7

pa+8

pa+9

Aritimética de Ponteiro



O índice 0 poderá ser representado por (pa+0) ou simplesmente pa;

O índice 1 poderá ser representado por (pa+1);

O elemento I-ésimo do vetor <a> poderpa ser representado por (pa+I).


Considere o trecho de código abaixo:


// Programa 4

void main()

{

int *pa;

int a[10];

int x;


pa = a; // ou pa = &pa[0]


x = *pa;

}

x = *pa

copiará o conteúdo de a[0] em x.




Se <pa> aponta para o primeiro elemento vetor <a>, então podemos concluir que pa + 1 apontará para o próximo elemento do vetor <a>.


Considere o programa a seguir:


// Programa 5

#include

#include


void mostre( int *, int );


void main()

{

int a[10];

int *pa;

int i;


for ( i = 0; i <>

mostre(a,10);


pa = &a[0]; // pa aponta para o primeiro elemento


for ( i = 0; i <>


mostre(a,10);

}


void mostre(int *vetor, int tamanho )

{

int i;

for ( i = 0; i <>

printf("\n%d",vetor[i]);

}

No programa 5, exemplo acima, a primeira estrutura for utiliza a operação de subscrito para fazer atribuição ao vetor <a> (exe.: a[i] = 10 ). Isso, dependendo da capacidade que o compilador tem para otimizar código, poderá resultar em custo adicional de processamento. Para ilustrar, considere que o tamanho do tipo inteiro utilizado seja 4 Bytes, ou seja, o nosso compilador C representa o tipo int com 4 bytes.


4 Bytes

4 Bytes

4 Bytes

4 Bytes

4 Bytes

4 Bytes …


0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

i=0

i=1

i=2

i=3

i=4

i=5 …

pa + 0

pa + 1

pa + 2

pa + 3

pa + 4

pa + 5 …



Sabendo-se que a unidade de endereçamento da maioria dos computadores modernos é o Byte, para endereçar o quarto valor inteiro, índice 3, do vetor <a> será feita a seguinte aritmética:


(Base do vetor) + (tamanho em bytes do tipo de dados) * (o Índice)

Ou seja,

Para endereçar a[3] temos:

a + 4 * 3.


Para o exemplo acima, isso significa que terei que deslocar 12 Bytes (4 * 3) a partir do endereço base do vetor <a> para chegar ao Índice 3 ( 4° elemento do vetor <a>).

Como você pôde notar no exemplo dado, foi necessária uma operação de multiplicação para endereçar a posição do vetor desejada. Sabendo-se que uma operação de multiplicação custa muito mais caro que uma operação de adição para uma CPU, operações com subscritos poderá resultar em um tempo adicional, considerando um vetor de grande dimensão.


Ao contrário, se no lugar de usar operações com subscrito, for utilizado ponteiros, o resultado final será o mesmo, porém, o tempo poderá ser sensivelmente menor para vetores de grandes dimensões. Isso em função da aritmética utilizada para ponteiros que, no nosso caso é diferente da utilizada pelos subscritos, ou seja, se <pa> for um ponteiro para inteiros de 4 bytes e apontar para a base do vetor <a>, então pa = pa + 1 ou pa++, fará o ponteiro <pa> deslocar 4 Bytes, fazendo assim, <pa> apontar para o próximo elemento do vetor. Dessa forma, para acesso seqüencial a um vetor, poderá ser mais barato utilizar um ponteiro.


Voltando ao programa 5, a segunda estrutura for, faz uso do ponteiro <pa> para manipular o vetor <a>. Nesse caso, o compilador irá gerar um código sem operações de multiplicação, tornando assim, o código final mais eficiente para esse tipo de problema.


Você notará essa forte relação entre apontadores e vetores, quando usar as funções de manipulação de strings (strlen, strcpy, etc). A manipulação de uma string, vetor de char em C, será por meio de ponteiro ou vetor de acordo com a conveniência.


Concluindo, devido a relação muito próxima entre ponteiros e vetores, o programador C deverá ser capaz de discernir quando usar um ou outro. Muitas vezes será mais conveniente utilizar subscritos para operar com vetores, sobretudo quando o tempo de execução e o tamanho do código gerado não são críticos. O uso de ponteiros tende para um código mais otimizado, porém, poderá deixar seu código em C menos legível além de poder comprometer a robustez do seu programa caso o ponteiro seja utilizado de forma errada.


Referências Bibliográficas:


Kernighan Brian W., Dennis Ritchie M, C A Linguagem de Programação Padrão ANSI, Editora CAMPUS

Aaron M., Yedidyah Langsan e Moshe J. Augenstein, Data Structures Using C, Editora Prentice-Hall


domingo, 17 de fevereiro de 2008

Métodos Nativos em Java

UTILIZAÇÃO DE MÉTODOS NATIVOS EM JAVA

Entenda como escrever métodos nativos em C e utiliza-los em Java




Por Ricardo Lima Caratti

Introdução


Durante o desenvolvimento de uma aplicação em Java, é possível que se tenha a necessidade de executar um código escrito em outra linguagem. Isso pode ocorrer por vários motivos, dentre eles destacam-se: aproveitamento de código previamente escrito, utilização de funcionalidades do sistema operacional, acesso ao hardware e até mesmo melhoria de performance. Java possui várias formas de fazer isso. Por exemplo, execução dos métodos exec() da classe Java.lang.Runtime, utilização de CORBA ou RMI. O objetivo deste artigo é mostrar uma outra forma, além das descritas acima, para fazer comunicação de uma aplicação Java com código escrito em linguagem C.


A linguagem Java possui a palavra chave native, que é um modificador utilizado somente para métodos. O modificador native indica que o corpo do método encontra-se fora do Java Virtual Machine (JVM). Um código nativo é escrito, geralmente, em C ou C++ e compilado para um tipo de hardware específico. O exemplo a seguir mostra como utilizar código nativo em Java.


class ChamadaCodigoNativo {

static {

System.loadLibrary("CodigoNativo");

}

public native int Soma(int a, int b);

}

Arquivo 1: ChamadaCodigoNativo.java



class Exemplo1 {

public static void main(String[] args) {

int a, b, c;

ChamadaCodigoNativo nativo =

new ChamadaCodigoNativo();

a = 10;

b = 11;

c = nativo.Soma(a,b);

System.out.println("Resultado: "+c);

}

}

Arquivo 2: Exemplo1.java


Note na implementação da classe ChamadaCodigoNativo, o trecho do código estático. Ele será executado uma única vez no momento em que a classe for instanciada.

A chamada System.loadLibrary(“CodigoNativo”) tenta carregar a biblioteca de ligação dinâmica CodigoNativo.dll no caso do ambiente Windows ou CodigoNativo.so no caso do ambiente UNIX. Para tanto, é claro, ele deve estar implementada.


Note também que a chamada do método Soma não difere de um método normal em Java.



Vantagens e desvantagens da utilização de código nativo.


Vantagens


Aproveitamento de código legado: Talvez seja a principal razão para utilização de código nativo. Em muitas situações é melhor utilizar um código já testado e funcionando que recodifica-lo para Java.


Eficiência: Embora a tecnologia utilizada nos compiladores Java esteja, a cada dia, melhorando em favor da eficiência, códigos nativos, em geral, executam mais rápidos que códigos Java.


Desvantagens:


Programa dependente de plataforma: Essa é a principal desvantagem em usar código nativo, pois, sabe-se que uma das principais características do Java é a portabilidade. Nesse caso, essa é a maior razão para evitar código nativo.


Problema de robustez: O JVM não protege a aplicação de código nativo mal escrito, isto é, uso inadequado de ponteiros, operações de I/O etc.


Coletor de lixo (garbage collection): O uso de alocação dinâmica de memória no método nativo, não trabalha em conjunto com o garbage collection do JVM. Exemplo: utilizar a função malloc() do C no método nativo.

O Java Native Interface (JNI)


Considerando uma real necessidade de utilização de código nativo, o Java disponibiliza o JNI como parte do JDK. O JNI possibilita uma interface segura entre o Java e outra linguagem. A figura 1 mostra como isso é feito.


Figura 1 - Utilização do JNI como interface do Java com código nativo


Por meio do JNI, é possível utilizar métodos nativos para:


*

Ter acesso a variáveis membros de classes em Java;
*

Criar e manipular objetos em Java. Isso inclui arrays e strings;
*

Manipular exceções;
*

Executar cheque de tipo em tempo de execução;
*

Utilizar threads.



Tratamento de tipos de dados primitivos entre Java e métodos nativos


Como foi mostrado na figura 1, o JNI é o elo entre o programa Java e os métodos nativos. Para tanto, ele precisa mapear os tipos de dados Java para os tipos de dados nativos. A tabela 1 mostra o mapeamento dos tipos primitivos e os tipos nativos equivalentes.


Tipo em Java


Tipo em C


Tamanho em bits

Void


void


-

Byte


jbyte


8

Char


jchar


16

short


jshort


16

Int


jint


32

Long


jlong


64

Float


jfloat


32

double


jdouble


64

Tabela 1 – Integração de tipos primitivos Java para tipos nativos equivalentes.


Os tipos: jbyte, jchar, jshort, jint, jlong, jfloat, jdouble e outros, estão definidos em jni.h.


Para manipulação de array o JNI usa jarray para representar uma referência a arrays em Java. Seguindo a mesma idéia, existe o jstring para manipulação de string. A manipulação de array e strings em métodos nativos requer procedimentos extras e não serão tratados aqui. Para mais informações consulte as referências indicadas neste artigo.



Os seis passos necessários


Passo 1: Implementar o código em Java;


Considere as classes ChamadaCodigoNativo.java e Exemplo1.java implementadas no início deste artigo.



Passo 2: Compilar código Java


Compilado as classes


javac ChamadaCodigoNativo.java

javac Exemplo1.java


Note que até o momento não foi feito nada além do de costume.


Passo 3: Utilizar o utilitário javah


javah –jni ChamadaCodigoNativo


O comando acima criará um arquivo chamado ChamadaCodigoNativo.h. Esse arquivo será utilizado mais adiante na implementação do código em C. A listagem a seguir mostra o conteúdo desse arquivo.


/* DO NOT EDIT THIS FILE - it is machine generated */

#include

/* Header for class ChamadaCodigoNativo */


#ifndef _Included_ChamadaCodigoNativo

#define _Included_ChamadaCodigoNativo

#ifdef __cplusplus

extern "C" {

#endif

/*

* Class: ChamadaCodigoNativo

* Method: Soma

* Signature: (II)I

*/

JNIEXPORT jint JNICALL Java_ChamadaCodigoNativo_Soma

(JNIEnv *, jobject, jint, jint);


#ifdef __cplusplus

}

#endif

#endif


Arquivo 3: ChamadaCodigoNativo.h



Note o cometário da primeira linha indicado que o arquivo ChamadaCodigoNativo.h não deve ser alterado.


Note também a denominação data para o método Soma no código nativo. Isto é, a composição do nome da função é dada pelo literal Java, o nome do pacote (se for o caso), o nome da classe e o nome do método. Portanto, a função Soma em C será implementada com o nome Java_ChamadaCodigoNativo_Soma. Os dois primeiros argumento, JNIEnv * e jobject, devem sempre ser declarados, mesmo que não sejam utilizados. Veja o passo a seguir para ver como isso funciona.



Passo 4: Implementar o método nativo


Para implementar o método Soma em C, deve-se usar a mesma nomenclatura prototipada no arquivo ChamadaCodigoNativo.h. A listagem a seguir mostra o código em C (arquivo CodigoNativo.c).


#include

#include "ChamadaCodigoNativo.h"


JNIEXPORT jint JNICALL Java_ChamadaCodigoNativo_Soma(JNIEnv *env, jobject this, jint a, jint b)

{

return a + b;

}

Arquivo 4: CodigoNativo.c


Passo 5: Criando uma biblioteca de ligação dinâmica


Na codificação da classe ChamaCodigoNativo, foi utilizado bloco estático como mostrado abaixo.


static {

System.loadLibrary("CodigoNativo");

}


Como dito anteriormente, este código será executado no momento em que a classe for instanciada. A chamada a System.loadLibrary() é responsável pela carga da biblioteca de ligação dinâmica ou biblioteca compartilhada (shared library).


Para construir uma biblioteca de ligação dinâmica você deverá instruir o compilador C.


O exemplo a seguir mostra a criação de uma biblioteca de ligação dinâmica (DLL) no ambiente Microsoft Windows utilizando o código em C implementado no Passo 4. O compilador utilizado é Microsoft Visual C++ 6.0.


cl -Ic:\java\include -Ic:\java\include\win32

-LD CodigoNativo.c -FeCodigoNativo.dll


Observações sobre as chaves de compilação:


-Ic indica o caminho onde serão encontrados os arquivos de cabeçalho do JNI;

-LD indica que o código gerado será uma (DLL);

-Fe indica o nome do arquivo a ser gerado.


Exemplo utilizando o Sistema Operacional UNIX Solaris.


cc -G -I/usr/local/java/include -I/usr/local/java/include/solaris \

CodigoNativo.c -o CodigoNativo.so


Exemplo utilizando o compilador C da GNU no ambiente Linux.


gcc –fPIC –c CodigoNativo.c -I/usr/local/java/include -I/usr/local/java/include/solaris

gcc –shared –Wl, -soname, libCodigoNativo.so.1 –o libCodigoNativo.so.1.0 CodigoNativo.o


Para os dois últimos exemplos, é assumido que o java está instalado no caminho /usr/local/java.


Para mais detalhes sobre a forma de compilar e criar bibliotecas compartilhadas consulte o manual do compilador disponível em cada plataforma.


Passo 6: Execução do Programa


A execução do programa é feita normalmente, ou seja, como um programa Java comum.


java Exemplo1


A saída do programa será:


Resultado: 21



Resumo


É possível fazer aplicações Java se comunicar com código escrito em outra linguagem. As principais técnicas são:


*

Interprocess communication (IPC);
*

Objetos distribuídos (CORBA);
*

Java Native Interface (JNI).



Você deve usar métodos nativos em Java quando:


*

Você já possui uma biblioteca escrita em uma outra linguagem (código legado) e deseja utiliza-la com suas aplicações em Java;
*

Você deseja utilizar uma funcionalidade dependente de plataforma que a biblioteca padrão do Java não possui;
*

Você precisa implementar uma funcionalidade onde o tempo é um fator crítico.


As principais implicações do uso de métodos nativos são:


*

Problemas quanto a portabilidade;
*

Problema em toda aplicação no caso de código nativo mal escrito.



Referências


Wall, Watson, and Whitis. Linux Programminig, Sams, 1999.


Avenue, Garcia, Java Native Interface Specification Release 1.1, Java Software, MountainView, CA, USA, 1997

Caratti, Ricardo Lima, Revista do Linux, Ano I, Nº 5, Maio 2000, Artigo: Bibliotecas Compartilhadas.


Darwin, Ian, Java Cookbook, O’Reilly, 2001


http://java.sun.com/docs/books/tutorial/native1.1/

http://java.sun.com/docs/books/jni/html/intro.html#1811


MSDN Library Visual Studio6.0, Compiler Reference.

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;