Lamport

40 visualizações

Publicada em

0 comentários
0 gostaram
Estatísticas
Notas
  • Seja o primeiro a comentar

  • Seja a primeira pessoa a gostar disto

Sem downloads
Visualizações
Visualizações totais
40
No SlideShare
0
A partir de incorporações
0
Número de incorporações
10
Ações
Compartilhamentos
0
Downloads
0
Comentários
0
Gostaram
0
Incorporações 0
Nenhuma incorporação

Nenhuma nota no slide

Lamport

  1. 1. Implementa¸c˜ao do algoritmo de exclus˜ao m´utua de Lamport usando um framework de implementa¸c˜ao de algoritmos distribu´ıdos Alvaro Augustho de Souza Silva Orientador: Luiz Eduardo Buzato 4 de novembro de 2011 Resumo Neste documento ser´a apresentada uma implementa¸c˜ao em Java do algoritmo de ex- clus˜ao m´utua de Lamport [1], baseado em rel´ogio l´ogicos, com o uso do framework Neko. O Neko [3] ´e um framework desenvolvido para facilitar o desenvolvimento e im- plementa¸c˜ao de algoritmos distribu´ıdos. O escolha do algoritmo de Lamport foi com o objetivo de apresentar o funcionamento deste em um ambiente distribu´ıdo real (no caso um cluster de 5 computadores) e os poss´ıveis usos dele e do pr´oprio Neko. Foi im- plementado um mecanismo de gera¸c˜ao de log da troca de mensagens entre os processos em um arquivo de texto simples, e com este log pˆode-se gerar um diagrama visual da execu¸c˜ao correspondente a ele, em que um gr´afico representava a troca de mensagens entre os n´os por meio de setas coloridas e etiquetas mostrando o tipos de mensagens sendo enviadas. O programa usado para gerar este diagrama chama-se LogView e foi feito em um projeto de semestre num instituto de tecnologia na Sui¸ca, com supervis˜ao de um dos autores do Neko. O LogView [2] usado aqui foi modificado por mim, de forma que ele pudesse exibir mais detalhes da troca de mensagens que a vers˜ao original n˜ao exibia, como os tempos de envio e recebimento da mensagem, o tipo dela, algumas mudan¸cas na interface de forma a tornar a visualiza¸c˜ao um pouco mais clara, entre outras modifica¸c˜oes. 1 Introdu¸c˜ao O problema da exclus˜ao m´utua ´e um ponto crucial de preocupa¸c˜ao em sistemas que com- partilham recursos entre processos em v´arios computadores, ou mesmo threads de execu¸c˜ao de um processo sendo executado em um ´unico computador. A quest˜ao ´e evitar que dois ou mais processos ou threads tenham acesso simultaneamente a este recurso compartilhado, comumente chamado de regi˜ao cr´ıtica (r.c.). A solu¸c˜ao apresentada para este problema por Leslie Lamport em seu artigo Time, clocks, and the ordering of events in a distributed system de 1978 se baseia na id´eia de timestamps. Timestamps s˜ao contadores monotonica- mente crescentes mantidos unicamente por cada processo, e que seguem as trˆes seguintes regras: 1
  2. 2. 1. Um processo incrementa seu contador antes de cada evento neste processo. 2. Quando um processo envia uma mensagem, ele inclui seu pr´oprio contador na men- sagem enviada. 3. Ao receber uma mensagem, o processo recipiente atualiza seu pr´oprio contador para que ele seja maior que o m´aximo entre seu valor atual e o valor recebido na mensagem, antes que ele considere a mensagem como recebida. Com este contador e estas regras simples ´e poss´ıvel estabelecer uma rela¸c˜ao de an- tecedˆencia entre eventos numa execu¸c˜ao distribu´ıda. A partir da id´eia de timestamps o algoritmo de exclus˜ao m´utua de Lamport se desenvolve. Antes de mostr´a-lo ´e necess´ario mencionar um elemento crucial em seu funcionamento: cada processo mant´em um vetor de mensagens com tamanho igual ao n´umero de processos presentes no ambiente distribu´ıdo, incluindo ele pr´oprio. Cada posi¸c˜ao deste vetor guarda a ´ultima mensagem recebida pelo processo corresponte a esta posi¸c˜ao (todos os processos tem identificadores, que v˜ao de 0 a n-1, sendo n o n´umero de processos; este identificador se chama PID). Assim, cada processo consegue saber o timestamp da ´ultima mensagem recebida de cada outro processo, e isso ´e necess´ario para o funcionamente do algoritmo. Quando este texto se referir ao vetor de mensagens ou simplesmente ao vetor ele estar´a se referindo a este elemento. O algoritmo se baseia nas seguintes regras: • Envio de requisi¸c˜ao para entrada na r.c.: um processo envia uma mensagem de req- uisi¸c˜ao req para todos os outros processos com um timestamp (valor do seu rel´ogio l´ogico atual), e adiciona a requisi¸c˜ao rotulada com o timestamp ao vetor de mensagens, na posi¸c˜ao correspondente a ele mesmo. • Recep¸c˜ao de requisi¸c˜ao: a mensagem de requisi¸c˜ao, com o timestamp gravado nela, ´e colocada no vetor na posi¸c˜ao correspondente ao processo remetente, e um reconheci- mento ack ´e enviado a ele. • Libera¸c˜ao da r.c.: um processo envia uma mensagem de libera¸c˜ao rel para todos os outros processos. • Recep¸c˜ao de rel: a requisi¸c˜ao correspondente `a libera¸c˜ao ´e removida do vetor de mensagens. • Entrada na r.c. (guarda): um processo determina que pode entrar na r.c. se e somente se: 1. ele tem uma requisi¸c˜ao no vetor com timestamp t 2. t ´e o menor timestamp no vetor 3. se t for igual a outro timestamp no vetor, o acesso ser´a concedido ao processo que tiver o menor identificador de processo (PID) 2
  3. 3. A entrada na r.c. ´e determinada pela ordena¸c˜ao total gerada pelos rel´ogios l´ogicos. Quando a guarda de de Pi torna-se verdadeira (o processo ´e autorizado a entrar na r.c.) n˜ao existe outra requisi¸c˜ao no sistema (no vetor de mensagens ou em trˆansito) que tenha timestamp menor que o da requisi¸c˜ao de Pi. Para que ele entre ´e necess´ario que o estado das demais posi¸c˜oes do vetor de mensagens tenha sido atualizado com timestamps maiores que o da requisi¸c˜ao de Pi; essa atualiza¸c˜ao ´e garantida pelo ack e pelo fato dos canais de comunica¸c˜ao serem FIFO. Portanto, ´e garantida a justi¸ca e os deadlocks devido `a ordena¸c˜ao total. J´a a exclus˜ao m´utua ´e garantida porque o processo que entra na regi˜ao cr´ıtica somente remover´a a sua requisi¸c˜ao do vetor dos demais processos depois que a tiver deixado e enviado uma mensagem de release com rel´ogio l´ogico maior para todos eles. Novamente o mecanismo de rel´ogio l´ogico garante que o algoritmo est´a correto. Este texto trata da implementa¸c˜ao deste algoritmo para a execu¸c˜ao em um cluster de cinco computadores, sendo cada computador um processo na execu¸c˜ao, atrav´es do uso de um framework para a constru¸c˜ao de algoritmos distribu´ıdos chamado Neko. O Neko ´e um framework desenvolvido em Java, e dispon´ıvel na internet para download e uso (ver bibliografia). Ele foi escrito no Distributed Systems Laboratory do Swiss Federal Institute of Technology in Lausanne (EPFL), por X´avier D´efago e P´eter Urb´an, em conjunto com outros estudantes. Seu desenvolvimento continuou no Japan Advanced Institute of Technology (JAIST), grupo DDSG, e na Universid´ad Polit´ecnica de Madrid, laborat´orio LSD. Ele ´e mantido principalmente por P´eter Urb´an, e veio sendo desenvolvido desde 2000. Esta pequena introdu¸c˜ao explicou brevemente o algoritmo de exclus˜ao m´utua aqui im- plementado e deu uma id´eia de o que ´e o Neko. O restante do texto est´a estruturado da seguinte forma. A se¸c˜ao 2 explicar´a em mais profundidade o que ´e e como funciona o framework Neko, e como algoritmos distribu´ıdos s˜ao implementados e executados quando se usa ele. Em seguida, na se¸c˜ao 3 ´e apresentada a implementa¸c˜ao do algoritmo em si, explicando cada classe em detalhes, a forma como elas se ligam, como a pilha de protocolos em cada processo ´e montada antes da execu¸c˜ao, e como ela se comporta durante a execu¸c˜ao, seja ela simulada (que n˜ao ser´a mostrada aqui) ou distribu´ıda num ambiente de rede real. Em seguida, na se¸c˜ao 4 finalmente ´e apresentado como essa implementa¸c˜ao ´e colocada em funcionamento, mostrando como inicializar cada processo e a execu¸c˜ao em si, e o que ´e mostrado indicando que o algoritmo est´a realmente sendo executado. Ap´os isso, na se¸c˜ao 5 s˜ao mostradas as classes respons´aveis pela gera¸c˜ao do log da execu¸c˜ao. O log gerado numa execu¸c˜ao e o uso dele para criar o diagrama de execu¸c˜ao no LogView ´e mostrado na se¸c˜ao 6. Por fim, uma conclus˜ao ´e apresentada na se¸c˜ao 7, listando os pr´os e os contras do Neko e comentando a vantagem de se poder visualizar o funcionamento de um algoritmo executando de forma distribu´ıda de verdade num diagrama visual e intuitivo que mostra a troca de mensagens que est´a ocorrendo. 2 Neko: Arquitetura e Funcionamento O Neko ´e uma ´e uma plataforma de comunica¸c˜ao que permite tanto simular um algoritmo distribu´ıdo numa ´unica m´aquina quanto execu¸c˜oes verdadeiramente distribu´ıdas em v´arias 3
  4. 4. Figura 1: Estrutura em camadas de uma aplica¸c˜ao usando o Neko m´aquinas, numa rede de computadores, usando a mesma implementa¸c˜ao para um algoritmo. Ele foi feito em Java, com o objetivo de simplificar o desenvolvimento de algoritmos dis- tribu´ıdos, diminuindo o tempo de implementa¸c˜ao e testes e facilitando a execu¸c˜ao e estudo dos resultados. Como ´e mostrado na Figura 1, a arquitetura do Neko consiste de duas partes principais: aplica¸c˜ao (aplication) e rede (networks). No n´ıvel da aplica¸c˜ao, uma cole¸c˜ao de processos (numerados de 0 a n-1) comunicam-se atrav´es de uma interface simples de troca de men- sagens: um processo sender coloca sua mensagem na rede com a primitiva ass´ıncrona send e a rede ent˜ao entrega essa mensagem ao processo destinat´ario com a primitiva deliver. Processo s˜ao implementados como programas em v´arias camadas. A comunica¸c˜ao n˜ao ´e uma caixa-preta: a infrastrutura de troca de mensagens pode ser controlada de v´arias maneiras. Primeiro, uma rede pode ser instanciada de uma cole¸c˜ao de redes pr´e-definidas, como uma rede real usando TCP/IP ou uma Ethernet simulada. Segundo, o Neko consegue controlar v´arias redes em paralelo. Terceiro, redes que extendem e modificam as existentes s˜ao facilmente implement´aveis. O fato de o c´odigo ser aberto e a linguagem de implementa¸c˜ao ser Java facilitam muito a moldagem da plataforma ao gosto do usu´ario. Mas, em geral, n˜ao ´e necess´aria a modifica¸c˜ao do c´odigo-fonte do Neko; ele j´a oferece uma plataforma pronta, simples e completa para o teste e implementa¸c˜ao de muitos algoritmos. O algoritmo de exclus˜ao m´utua de Lamport foi implementado apenas usando as ferramentas e classes de Java disponibilizadas por ele na sua vers˜ao atual (Neko 1.0 beta 1, colocada `a disposi¸c˜ao para download e uso no site oficial do projeto Neko em 16 de junho de 2009). Para a execu¸c˜ao do algoritmo um arquivo simples de configura¸c˜ao ´e necess´ario. O que foi usado aqui ser´a apresentado e explicado para maior clareza de sua fun¸c˜ao e para mostrar melhor um detalhe importante da plataforma Neko. Os c´odigos das classes representando o algoritmo de Lamport tamb´em ser˜ao explicados detalhadamente, e assim tanto ele quanto 4
  5. 5. a plataforma devem ser bem apresentados. Vamos `a exposi¸c˜ao do c´odigo implementando o algoritmo e a exclus˜ao m´utua distribu´ıda. 3 A Implementa¸c˜ao do Algoritmo 3.1 A Interface MEAlgorithm A exposi¸c˜ao da implementa¸c˜ao da exclus˜ao m´utua ser´a come¸cada mostrando uma interface simples, criada com o objetivo de simplificar o programa: package lamport; public interface MEAlgorithm { public void enterCriticalSection(); public void exitCriticalSection(); } Os dois m´etodos da interface s˜ao enterCriticalSection e exitCriticalSection. Como os pr´oprios nomes dizem, o primeiro ´e chamado quando uma classe que use um objeto que faz uso desta interface deseja entrar numa se¸c˜ao cr´ıtica do c´odigo, que usa dados compartilhados entre processos, e para isso quer garantir acesso exclusivo e sem risco de encontrar ou criar incosistˆencias. Quando a aplica¸c˜ao terminar de usar essa regi˜ao cr´ıtica ela deve chamar exitCriticalSection, que faz o que for necess´ario para liberar o acesso `a essa regi˜ao e deixar os outros processos cientes de que ningu´em mais tem a prioridade. 3.2 A Classe Que Faz Uso da Exclus˜ao M´utua: Application.java Antes de mostrar a implementa¸c˜ao do algoritmo propriamente dita ser´a apresentada a classe que chama os m´etodos da interface acima para entrar e sair da regi˜ao cr´ıtica sem o perigo de gerar incosistˆencias de dados: package lamport; // java imports: import java.util.Random; //lse.neko imports: import lse.neko.ActiveReceiver; import lse.neko.NekoProcess; import lse.neko.SenderInterface; public class Application extends ActiveReceiver { private SenderInterface sender; public void setSender(SenderInterface sender) { this.sender = sender; } /* O algoritmo de exclus~ao m´utua deste processo. * Este m´etodo ´e usado pela classe MEInitializer no momento da cria¸c~ao dos processos. */ private MEAlgorithm algorithm; 5
  6. 6. public void setMEAlgorithm(MEAlgorithm algorithm) { this.algorithm = algorithm; } /* o PID deste processo */ private int me; /* o n´umero de processos private int n; /* objeto gerador de n´umeros aleat´orios */ private Random random; public Application(NekoProcess process) { super(process, "[Aplica¸c~ao] Thread-p" + process.getID()); me = process.getID(); n = process.getN(); } A primeira parte do c´odigo faz as importa¸c˜oes de classes necess´arias. Esta classe estende ActiveReceiver, uma classe do Neko que cria objetos capazes de receber ativamente men- sagens que forem enviadas a eles, ao inv´es de simplesmente esperar elas serem entregues. Seu funcionamento ser´a melhor entendido quando o algoritmo for apresentado; ele foi im- plementado usando o funcionamento desta classe. Neste come¸co tamb´em ´e criado o objeto Sender que tratar´a de enviar as mensagens que porventura devam ser enviadas a outros processos, e por fim o algoritmo de exclus˜ao m´utua a ser usado. Se eu tivesse implementado outro algoritmo em outra classe, bastava que ele implementasse a mesma interface. Para escolher um e n˜ao outro, o arquivo de configura¸c˜ao mencionado anteriormente seria usado. Por fim, o construtor desta classe ´e mostrado. Ele chama o construtor da classe NekoThread, a que ActiveReceiver estende, usando super. Esse construtor recebe um objeto NekoProcess, que representa o processo que ser´a criado, e um nome ´e passado para nomear a thread deste novo processo. ´E inicializada tamb´em uma vari´avel que guarda o pr´oprio PID, me, e outra que guarda o n´umero de processos no ambiente de execu¸c˜ao. Ambos ser˜ao usados `a frente. public void run() { if(me == (n-1)) System.out.println("Processo " + me + " fazendo logging da execu¸c~ao..."); else { System.out.println("Processo " + me + " tentando entrar na r.c.."); littleSleep(-1); algorithm.enterCriticalSection(); 6
  7. 7. System.out.println("nn--------------- Processo " + me + " entrou na r.c. ---------------n"); littleSleep(8000); System.out.println("Processo " + me + " saindo da r.c.."); algorithm.exitCriticalSection(); System.out.println("n----------------- Processo " + me + " saiu da r.c.-----------------nn"); littleSleep(8000); } } Este ´e o m´etodo run que ficar´a executando em loop na thread do processo at´e que o usu´ario termine a execu¸c˜ao. Basicamente, o que ele faz ´e ficar entrando e saindo da regi˜ao cr´ıtica, e dormindo v´arias vezes para que a execu¸c˜ao n˜ao fique muito r´apida e possa ser acompanhada por quem estiver monitorando. Caso o processo atual n˜ao tenha a prioridade para entrar na r.c. no momento, mensagens ser˜ao impressas na tela esperando que a prior- idade seja passada a ele. Mas isso ´e tratado na classe que implementa o algoritmo e ser´a mostrado daqui a pouco. O m´etodo littleSleep coloca o processo em sleep e ´e mostrado abaixo. O teste inicial ´e feito porque o processo n´umero n-1 ficar´a respons´avel pelo logging da execu¸c˜ao, e n˜ao executar´a normalmente um n´o do algoritmo. private void littleSleep(int k) { int sleepTime; random = new Random(); if(k != -1) sleepTime = k; else sleepTime = random.nextInt(8000); try { sleep(sleepTime); } catch (InterruptedException e) { System.out.println("Erro ao tentar dormir na aplica¸c~ao."); } } } Esse m´etodo coloca o processo em sleep por tempos aleat´orios de at´e 8 segundos, exceto quando a thread est´a dentro da r.c., quando ela dorme exatamente por 8 segundos, para depois acordar e sair da regi˜ao. Como d´a para perceber, seria poss´ıvel implementar algo ´util durante o tempo que o processo ganha a prioridade para a regi˜ao cr´ıtica. Basta retirar o sleep de 8 segundos e colocar algo nessa parte. Aqui o sleep foi usado para demonstrar que a exclus˜ao m´utua estava funcionando. 7
  8. 8. 3.3 A Classe de Inicializa¸c˜ao: MEInitializer.java Para montar a estrutura de processos necess´aria para a execu¸c˜ao de um algoritmo no Neko package lamport; // lse.neko imports: import lse.neko.NekoProcess; import lse.neko.NekoProcessInitializer; import lse.neko.ReceiverInterface; import lse.neko.SenderInterface; // other imports: import org.apache.java.util.Configurations; public class MEInitializer implements NekoProcessInitializer { public MEInitializer() { } public void init(NekoProcess process, Configurations config) throws Exception { /* Primeiro, constr´oi a pilha de protocolos e configura a rede. */ /* Camada da aplica¸c~ao */ Class applicationClass = Class.forName(config.getString("application")); Class[] appConstructorParamClasses = { NekoProcess.class }; Object[] appConstructorParams = { process }; ReceiverInterface application = (ReceiverInterface) applicationClass .getConstructor(appConstructorParamClasses) .newInstance(appConstructorParams); application.setId("app"); /* Camada do algoritmo */ Class algorithmClass = Class.forName(config.getString("algorithm")); Class[] algConstructorParamClasses = { NekoProcess.class }; Object[] algConstructorParams = { process }; ReceiverInterface algorithm = (ReceiverInterface) algorithmClass .getConstructor(algConstructorParamClasses) .newInstance(algConstructorParams); algorithm.setId("alg"); /* Configura a rede. */ SenderInterface net = process.getDefaultNetwork(); Na primeira parte s˜ao criados os objetos usados na pilha de protocolos. Neste caso s˜ao trˆes: a camada da aplica¸c˜ao, mostrada anteriormente, a camada do algoritmo, usando uma 8
  9. 9. classe que ser´a mostrada a seguir, e o objeto ’net’, que implementa a camada de rede e tratar´a da troca de mensagens, envio e entrega, entre os processos. Como pode-se perceber, foi usado uma propriedade do Java chamada Reflection. Isso ´e usado para podermos saber em tempo de execu¸c˜ao qual a classe da aplica¸c˜ao e a do algoritmo, que dever˜ao estar no arquivo de configura¸c˜ao. Fazendo assim, podemos usar uma classe de inicializa¸c˜ao apenas (esta mesma) e escrever v´arias aplica¸c˜oes e algoritmos, bastando mudar uma linha no arquivo de configura¸c˜ao para trocar entre elas. Se n˜ao quis´essemos isso, bastava criar os objetos fazendo ReceiverInterface application = new Application(process) para a camada de aplica¸c˜ao, e ReceiverInterface algorithm = new LamportME(process), mas a flexibilidade e modularidade da nossa implementa¸c˜ao ficaria prejudicada. Essa decis˜ao fica a cargo do programador. ReceiverInterface ´e uma interface do Neko que s´o tem um m´etodo: deliver. Ele tem o papel de gerenciar mensagens recebidas. ActiveReceiver implementa esta interface e sobrescreve este m´etodo de modo que ele joga numa queue as mensagens recebidas. O m´etodo receive que ele tamb´em implementa, por sua vez, retira mensagens da fila e entrega ao processo que o chama, o destinat´ario original delas. A classe da aplica¸c˜ao estende ActiveReceiver, mas n˜ao faz uso deste m´etodo; a classe que implementa o algoritmo far´a amplo uso dele, como ser´a mostrado em breve. /* Segundo, liga as camadas. */ /* Configura o objeto Sender que a aplica¸c~ao pode usar para mandar mensagens. */ applicationClass .getMethod("setSender", new Class[] { SenderInterface.class }) .invoke(application, new Object[] { net }); /* Configura a camada do algoritmo criada anteriormente para ser usada * pela camada de aplica¸c~ao quando ela quiser garantir exclus~ao m´utua * no acesso `a regi~ao cr´ıtica. */ applicationClass .getMethod("setMEAlgorithm", new Class[] { MEAlgorithm.class }) .invoke(application, new Object[] { algorithm }); /* Configura o objeto Sender que a camada do algoritmo usar´a para mandar mensagens. */ algorithmClass .getMethod("setSender", new Class[] { SenderInterface.class }) .invoke(algorithm, new Object[] { net }); Continuando a usar Java Reflection, esta segunda parte liga as camadas da pilha de protocolos do processo que est´a sendo constru´ıdo. Usa m´etodos pr´oprios das classes Appli- cation, que usa setMEAlgorithm para dizer que o objeto ’algorithm’ criado anteriormente ´e o que deve ser usado como gerenciador da exclus˜ao m´utua, e setSender em ambos Appli- cation e LamportME (a classe do algoritmo) para dizer que ’net’ ´e o objeto que gerenciar´a a troca de mensagens na rede entre os processos. // Terceira, inicia a execu¸c~ao dos protocolos. application.launch(); algorithm.launch(); 9
  10. 10. } } Por ´ultimo, as camadas s˜ao lan¸cadas, e por conseguinte o processo. O m´etodo launch come¸ca a execu¸c˜ao das threads e deve ser usado para este fim. 3.4 A Classe que Implementa o Algoritmo: LamportME.java Ser´a apresentada finalmente a classe que implementa a execu¸c˜ao do algoritmo de exclus˜ao m´utua de Lamport. O tamanho dela ´e relativamente extenso, mas cada m´etodo ´e bastante simples de entender, apenas seguindo `a risca a id´eia original do algoritmo e procurando n˜ao usar artif´ıcios que escapem do escopo dela. Come¸carei mostrando a vari´aveis usadas e o m´etodo construtor. package lamport; // lse.neko imports: import lse.neko.ActiveReceiver; import lse.neko.MessageTypes; import lse.neko.NekoMessage; import lse.neko.NekoProcess; import lse.neko.SenderInterface; /** * Lamport’s mutual exclusion algorithm. * See the paper * <blockquote>Lam78 <br> * L.~Lamport. <br> * Time, clocks, and the ordering of events in a distributed system. <br> * Commun. ACM, 21(7):558--565, July 1978.</blockquote> * for details of the algorithm. */ public class LamportME extends ActiveReceiver implements MEAlgorithm { // message types used by this algorithm, also used as states private static final int req = 9032; private static final int ack = 9033; private static final int rel = 9034; private static final int s = 9042; private static final int r = 9043; // registering the message types and associating names with the types. static { MessageTypes.instance().register(req, "req"); MessageTypes.instance().register(ack, "ack"); MessageTypes.instance().register(rel, "rel"); MessageTypes.instance().register(s, "s"); MessageTypes.instance().register(r, "r"); } private SenderInterface sender; 10
  11. 11. public void setSender(SenderInterface sender) { this.sender = sender; } /* objeto respons´avel pelo logging. */ /* Somente o processo n-1 ir´a tratar do logging, portanto somente ele usar´a este objeto */ MyLogger logger; //algorithm variables /* the id of this process */ private int me; /* the number of processes */ private int n; /* list of addresses that includes all processes but this one */ private int[] allButMe; /* internal Lamport’s scalar clock */ int osn; /* array of requests */ private NekoMessage[] q; public LamportME(NekoProcess process) { super(process, "LamportME-p" + process.getID()); n = process.getN(); me = process.getID(); allButMe = new int[n - 1]; for (int i = 0; i < n - 1; i++) { allButMe[i] = (i < me) ? i : i + 1; } q = new NekoMessage[n]; osn = 0; logger = new MyLogger(n); } Como dito anteriormente, esta classe estende ActiveReceiver e implementa a interface MEAlgorithm, mostrada primeiro. Os tipos de menagens usados na execu¸c˜ao do algoritmo s˜ao expostos no come¸co da classe: s˜ao associados n´umeros inteiros com as strings que nomeiam os tipos da mensagens, e depois esses tipos s˜ao “registrados” no Neko com o uso do m´etodo register da classe MessageTypes do Neko. Tudo que este m´etodo faz ´e associar o nome do tipo da mensagem com o inteiro que a representar´a, e para isso usa um objeto do tipo HashMap. Ap´os isso s˜ao listadas as vari´aveis. Sender, como sabemos, ser´a o objeto que simular´a a rede e gerenciar´a o envio de mensagens, e ´e inicializado usando o m´etodo setSender pela classe de inicializa¸c˜ao, como mostrado anteriormente. Os inteiro me e n guardam o PID do processo e o n´umero de processos no ambiente de execu¸c˜ao atual, respectivamente. O vetor de inteiros allButMe serve para guardar o PID de todos os processos menos o pr´oprio 11
  12. 12. PID, e ele ser´a fundamental quando quisermos enviar mensagens em broadcast. O inteiro osn guardar´a o rel´ogio l´ogico de Lamport do processo atual, e o vetor de NekoMessages q guardar´a as mensagens que chegarem de outros processos, usando uma l´ogica pr´opria do algoritmo que ser´a mostrada. O construtor chama o m´etodo super para criar a thread de execu¸c˜ao e nome´a-la. Tamb´em inicializa as vari´aveis me e n, os vetores allButMe e q, e o rel´ogio l´ogico osn ´e setado inicialmente em zero, para mostrar que a execu¸c˜ao ainda n˜ao come¸cou. Cria tamb´em o objeto respons´avel pelo logging da execu¸c˜ao. Somente o processo n-1 ´e respons´avel pelo logging, portanto s´o ele usar´a este objeto. public void run() { if(me == (n-1)) { while(true) { NekoMessage m = receive(); logger.doLogging(m); } } else { while(true) { NekoMessage m = receive(); switch(m.getType()) { case req: requestMessageHandling(m); break; case ack: ackMessageHandling(m); break; case rel: releaseMessageHandling(m); break; default: throw new RuntimeException("Unknown message received"); } } // while(true) }//method Aqui est´a o m´etodo run que deve ser executado pela thread que controla o algoritmo. Se o processo em quest˜ao n˜ao ´e o n-1, respons´avel pelo logging, o que ele faz ´e ficar num loop infinito esperando mensagens e escolhendo como tratar elas, com base no seu tipo. Como explicado antes, receive ´e um m´etodo da classe ActiveReceiver que retira mensagens da fila de mensagens e entrega-as `a classe que a chamou. As mensagens s˜ao objetos NekoMessage, e um dos campos que as definem ´e o seu tipo (type), que ´e obtido pelo m´etodo p´ublico getType. Esses tipos de mensagens s˜ao os registrados anteriormente e associados a inteiros, 12
  13. 13. portanto este m´etodo retorna um inteiro. Vemos que, para cada tipo de mensagem, um m´eodo foi escrito especialmente para tratar dela. Esses m´etodos ser˜ao mostrados abaixo. /** * M´etodo que gerencia mensagens do tipo "req" * @param m: a mensagem do tipo "req" recebida */ private void requestMessageHandling(NekoMessage m) { // obtem o valor do rel´ogio l´ogico enviado na mensagem int k = ((Integer)m.getContent()).intValue(); // atualiza o pr´oprio rel´ogio l´ogico update(k); // coloca a mensagem no vetor de mensagens int j = m.getSource(); q[j] = m; // envia um "ack" de volta ao remetente, carregando o valor do pr´oprio rel´ogio NekoMessage Ack = new NekoMessage(me, new int[] {j}, getId(), new Integer(osn), ack); sender.send(Ack); this.LogMessage(m, osn, r); this.LogMessage(Ack, osn, s); } O tratamento de uma mensagem do tipo “req”, ou request ´e feito neste m´etodo. Primeiro o rel´ogio l´ogico pr´oprio ´e atualizado com base no rel´ogio l´ogico recebido na mensagem envi- ado de outro processo. Essa atualiza¸c˜ao ´e feita no m´etodo update. Ap´os isso, a mensagem ´e colocada na fila q na posi¸c˜ao destinada a mensagens recebidas pelo processo remetente dela. Por fim, uma mensagem de resposta ack ´e enviada em unicast de volta ao processo remetente, levando consigo o valor do rel´ogio l´ogico do processo atual. O m´etodo send do objeto sender tem como ´unico argumento a mensagem a ser enviada. Essa mensagem, encapsulada num objeto do tipo NekoMessage, ´e constru´ıda passando-se ao m´etodo construtor como argumentos o PID do processo remetente, um vetor representando os v´arios processos destinat´arios dela (neste caso o vetor s´o contem o remetente da mensagem “req” recebida), um identificador do protocolo de destino (basta usar o m´etodo getId), o conte´udo a ser transmitido (neste caso o valor do pr´oprio rel´ogio l´ogico, mas pode ser qualquer objeto Java) e o tipo da mensagem, nesta ordem. send enviar´a a mensagem sem que o usu´ario precise se preocupar com mais detalhes. /** * M´etodo que trata mensagens "rel" * @param m: a mensagem do tipo "rel" recebida */ private void releaseMessageHandling(NekoMessage m) { int k = ((Integer)m.getContent()).intValue(); // obtem o valor do rel´ogio l´ogico enviado na mensagem update(k); // atualiza o pr´oprio rel´ogio l´ogico int j = m.getSource(); 13
  14. 14. q[j] = m; this.LogMessage(m, osn, r); } Este ´e o m´etodo que trata mensagens do tipo “rel”, ou release. S˜ao feitas duas coisas: o rel´ogio l´ogico pr´oprio ´e atualizado (ele sempre ´e atualiado quando uma mensagem ´e recebido, como o algoritmo pede para ser feito) e a mensagem ´e colocada no vetor de mensagens, na posi¸c˜ao destinada a mensagens do remetente dela. /** * Method that handles ack messages. * It also counts the number of acks received since a request was sent. * @param m: the message received */ private void ackMessageHandling(NekoMessage m) { int k = ((Integer)m.getContent()).intValue(); // obtem o valor do rel´ogio l´ogico enviado na mensagem update(k); // atualiza o pr´oprio rel´ogio l´ogico int j = m.getSource(); if(q[j] != null && q[j].getType() != req) q[j] = m; this.LogMessage(m, osn, r); } Por fim, mensagens do tipo “ack” s˜ao tratadas por este m´etodo. Como nas mensagens do tipo release, nenhuma resposta precisa ser enviada de volta ao remetente. Portanto, apenas a atualiza¸c˜ao do rel´ogio l´ogico pr´oprio e o coloca¸c˜ao da mensagem no vetor de mensagens ´e feito. Por´em, essa coloca¸c˜ao do “ack” no vetor deve ser feita com um cuidado extra: o “ack” n˜ao pode sobrescrever request que por acaso tenham sido enviados pelo processo remetente deste “ack” anteriormente; somente releases podem fazˆe-lo. Caso contr´ario, o processo atual poderia julgar que a prioridade para entrar na ´area cr´ıtica pudesse ser sua, quando na verdade era do processo que teve sua mensagem de request apagada pelo “ack”. Os dois processos entrariam na r.c. e a exclus˜ao m´utua n˜ao aconteceria. Portanto, esse cuidado ´e tomado no teste do if, antes de atribuir q[j] a m. public void enterCriticalSection() { // multicast de uma mensagem do tipo ’req’ requisitando a entrada na r.c. NekoMessage Req = new NekoMessage(me, allButMe, getId(), new Integer(osn), req); sender.send(Req); this.LogMessage(Req, osn, s); // adiciona pr´opria requisi¸c~ao `a queue de mensagens e atualiza rel´ogio l´ogico q[me] = Req; osn = osn + 1; while(!testPriority()) { try { 14
  15. 15. sleep(500); } catch (InterruptedException e) { System.out.println("Erro ao testar prioridade da thread."); } } } Este ´e m´etodo que coloca o algoritmo em funcionamento. ´E a implementa¸c˜ao do enterCriticalSection presente na interface MEAlgorithm e, como sabemos, ´e o que deve ser chamado quando a aplica¸c˜ao deseja entrar na regi˜ao cr´ıtica. Como a defini¸c˜ao do al- goritmo pede, o que ´e feito ´e o seguinte: ´e feito um broadcast de uma mensagem “req”, carregando o pr´oprio PID e o pr´oprio valor do rel´ogio l´ogico do processo atual antes do envio da mensagem. Essa mensagem “req” tamb´em ´e guardada no pr´oprio vetor de mensagens, na posi¸c˜ao destinada `as pr´oprias mensagens, e o rel´ogio l´ogico ´e atualizado. Entao, um loop de teste fica rodando em ciclos de 500ms, testando se o processo atual tem a prioridade para entrar na regi˜ao cr´ıtica. Quando esta prioridade ´e ganha, o loop termina, e o m´etodo retorna. A aplica¸c˜ao continua sua execu¸c˜ao, agora com o processo atual utilizando-se da regi˜ao cr´ıtica. public void exitCriticalSection() { // multicast de release NekoMessage Rel = new NekoMessage(me, allButMe, getId(), new Integer(osn), rel); sender.send(Rel); this.LogMessage(Req, osn, s); q[me] = Rel; osn = osn + 1; } Quando a aplica¸c˜ao termina de usar a regi˜ao cr´ıtica, o m´etodo exitCriticalSection, tamb´em presente na interface MEAlgorithm, ´e executado. O que ele faz ´e enviar em broad- cast uma mensagem de release, “rel”, mostrando que terminou de usar sua prioridade e que outro processo requisitante pode fazer uso da regi˜ao. Essa mensagem de release ´e colocada no vetor de mensagens, na posi¸c˜ao destinada `as pr´oprias mensagens, e o rel´ogio l´ogico ´e incrementado de uma unidade de tempo. private void update(int k) { if(osn < k) osn = k; osn = osn + 1; } Este m´etodo simples trata da atualiza¸c˜ao do rel´ogio l´ogico como deve ser feita na especi- fica¸c˜ao do algoritmo de exclus˜ao m´utua de Lamport. Um inteiro k ´e recebido; se este k for maior que o rel´ogio l´ogico atual, osn ´e atualizado com esse valor (acontece no caso que uma mensagem recebida tinha um rel´ogio l´ogico maior que o do processo atual, significando que 15
  16. 16. ele estava atrasado em rela¸c˜ao ao processo remetente). Para terminar, osn ´e aumentado em mais uma unidade. public boolean testPriority() { int myReqClock = ((Integer)q[me].getContent()).intValue(); for(int i = 0; i < n; i++) { if(q[i] == null) return false; if(q[i].getType() == req) { int otherReqClock = ((Integer)q[i].getContent()).intValue(); if((otherReqClock < myReqClock) || (otherReqClock == myReqClock && i < me)) { System.out.println(me + ": Processo " + i + " tem prioridade para entrar na r.c.!" + " O rel´ogio da minha req ´e " + myReqClock + " e o da dele ´e " + otherReqClock + "."); return false; } } } osn = osn + 1; return true; } } // class Para terminar a descri¸c˜ao desta classe, o ´ultimo m´etodo ´e o que controla o processo da exclus˜ao m´utua, e portanto ´e o mais importante. O teste para saber se o processo em quest˜ao vai ganhar ou n˜ao a prioridade de entrada na ´area cr´ıtica ´e feito da seguinte maneira, como especificado na defini¸c˜ao do algoritmo: o rel´ogio da mensagem de request do pr´oprio processo presente na fila de mensagens ´e comparado com todas as outras mensagens de request de outros processos presentes na fila. Se nenhuma delas tiver um rel´ogio de valor menor que o da “req” pr´opria, ou tiver um valor igual, por´em o PID do outro processo for maior que o PID do processo que est´a testando a prioridade, ent˜ao esse processo ganha a prioridade e pode entrar na regi˜ao cr´ıtica. Caso contr´ario, ele n˜ao ganha a prioridade, e o teste ser´a executado novamente no loop mostrado no m´etodo enterCriticalSection, at´e que ele ganhe a prioridade e possa usufruir da ´area cr´ıtica. Dessa forma, ´e assegurado que somente um processo por vez tem acesso a essa ´area, e a exclus˜ao m´utua funciona durante a execu¸c˜ao da aplica¸c˜ao. Quando o processo entra na ´area cr´ıtica, o rel´ogio dele aumenta em uma unidade, por isto ser tratado como um evento na execu¸c˜ao. Isso tamb´em facilita um pouco a visualiza¸c˜ao das mensagens de release no gr´afico gerado pelo logView, mostrado mais `a frente. 16
  17. 17. 3.5 O Arquivo de Configura¸c˜ao: distributed.config O arquivo de configura¸c˜ao ´e um arquivo de texto com extens˜ao .config, passado como argumento de linha de comando quando a execu¸c˜ao ´e iniciada, como ser´a mostrada na parte de execu¸c˜ao. Ele ´e usado para se passar informa¸c˜oes ao Neko durante a inicializa¸c˜ao da aplica¸c˜ao e que podem ser usadas durante toda ela. Portanto, pode ser uma ferramenta muito ´util na implementa¸c˜ao de um algoritmo ou aplica¸c˜ao no Neko. simulation = false process.num = 4 slave = lsd96, lsd97, lsd98 process.initializer = lamport.MEInitializer network = lse.neko.networks.comm.TCPNetwork log = home/usu´ario/Desktop/log.txt Esse come¸co define se a execu¸c˜ao ser´a uma simula¸c˜ao em m´aquina ´unica ou uma execu¸c˜ao distribu´ıda entre v´arios computadores, na palavra-chave ’simulation’. Aqui foi feita uma execu¸c˜ao distribu´ıda real. Depois, define-se o n´umero de processos em ’process.num’; neste caso s˜ao 4 processos, rodando em 4 m´aquinas diferentes, cada um em uma m´aquina. O endere¸co das m´aquinas que ser˜ao tratadas como slaves ´e passado na palavra-chave ’slave’. Esses slaves, na verdade, s˜ao apenas as m´aquinas que ficar˜ao esperando a execu¸c˜ao come¸car, aguardando que uma das m´aquinas, que det´em este arquivo de configura¸c˜ao, se conecte a elas, envie o arquivo de configura¸c˜ao e dˆe o sinal para o in´ıcio da execu¸c˜ao. Quando a execu¸c˜ao come¸ca, n˜ao h´a mais nenhuma distin¸c˜ao entre os n´os: n˜ao h´a mais ’master’ ou ’slave’, e todos rodam o algoritmo de forma igualit´aria. Em ’process.initializer’ ´e passado o nome completo da classe que trata da inicializa¸c˜ao da execu¸c˜ao; n´os sabemos que nesta aplica¸c˜ao esta classe ´e a MEInitializer, do pacote ’lamport’. Por ´ultimo ´e passado que tipo de rede ser´a simulada com base numa classe do Neko que simula esta rede. Neste caso, uma rede TCP ´e usada, por meio da classe do Neko lse.neko.networks.comm.TCPNetwork. Outras redes est˜ao dispon´ıveis para serem usadas. Basta ver na pasta do Neko quais s˜ao elas. application = lamport.Application algorithm = lamport.LamportME #algorithm = mutualexclusion.RicartAgrawalaME #algorithm = mutualexclusion.SinghalME Aqui ´e importante. Anteriormente foi mencionada a capacidade do Neko de pegar o nome das classes que implementar˜ao a pilha de protocolos de cada processo em tempo de execu¸c˜ao, atrav´es do arquivo de configura¸c˜ao aqui exposto, e para isso usar a propriedade de Reflection do Java na classe de inicializa¸c˜ao MEInitializer. Aqui est´a a parte em que isto ´e usado. Usando a palavras-chave application e algorithm, podemos dizer quais s˜ao as classes que funcionar˜ao como a camada de aplica¸c˜ao e a camada do algoritmo do programa. Para 17
  18. 18. acessar estas informa¸c˜oes, o Neko usa um objeto do tipo Configurations, que guarda todas as informa¸c˜oes expostas no arquivo de configura¸c˜ao. Estas informa¸c˜oes podem ser acessadas durante a execu¸c˜ao atrav´es deste objeto. Examine a classe MEInitializer para ver como isso ´e feito e perceber como ´e simples. O m´etodo getString da classe Configurations retorna uma string associada `a chave que ´e passada a ela como argumento. Por exemplo, em MEInitializer, para se descobrir o nome da classe que implementar´a o algoritmo, que aqui se chama lamport.LamportME, ´e passado ao getString a palavra-chave algorithm. Como pode-se ver, nestas linhas do arquivo de configura¸c˜ao a palavra-chave algorithm ´e associada com a string lamport.LamportME, que ´e exatamente o nome da classe do algoritmo de exclus˜ao m´utua que ser´a usado. Portanto, sabendo usar isso e a Reflection do Java, podemos fazer v´arias implementa¸c˜oes de aplica¸c˜oes, algoritmos, e passar parˆametros como n´umeros e etc., no arquivo de configura¸c˜ao, para que depois sejam usados em tempo real na execu¸c˜ao da nossa aplica¸c˜ao no Neko. E isso ´e bastante ´util quando o principal objetivo de quem est´a usando o Neko ´e simplicidade no uso e rapidez na implementa¸c˜ao e teste de algoritmos. 4 Execu¸c˜ao Para come¸car a execu¸c˜ao distribu´ıda do algoritmo ´e necess´ario antes iniciar os n´os slaves. Slave ´e um n´o que ficar´a escutando numa porta, passada como argumento na linha de co- mando ou na padr˜ao 8632, se nada for passado. Ele espera receber de um n´o, ´unico entre todos os processos chamado master, o arquivo de configura¸c˜ao e o sinal para a execu¸c˜ao come¸car. Assim que recebe esse arquivo, ele monta a pilha de protocolos do processo corre- spondente a ele (cada n´o roda um processo) e come¸ca a execu¸c˜ao dele. Como ´e mostrado na Figura 2, deve ser chamada a classe java.lse.comm.Slave do Neko, com o comando java lse.neko.comm.Slave no terminal para o slave ser iniciado e ficar escutando em alguma porta. Se uma porta diferente da padr˜ao quiser ser passada, basta adicionar o n´umero dela como argumento deste comando (java lse.neko.comm.Slave XXXX, com XXXX sendo o n´umero da porta). Esse procedimento deve ser feito independentemente em cada um dos n´os que forem come¸car a execu¸c˜ao como slaves; o n´o master executa um comando diferente. Na Figura 3 ´e mostrado como ´e feito o in´ıcio da execu¸c˜ao atrav´es do n´o master. Usando- se o comando neko junto com o caminho para o arquivo de configura¸c˜ao mostrado ante- riormente passado como argumento, e ap´os todos os outros n´os terem sido iniciados como slaves e estarem conectados entre si (usando ssh, de preferˆencia), a execu¸c˜ao ter´a in´ıcio. Na Figura 3 o arquivo de configura¸c˜ao estava na mesma pasta em que foi chamado o comando, portanto n˜ao foi necess´ario passar um caminho absoluto ou relativo at´e ele, apesar de isso ainda ser poss´ıvel. A Figura 4 mostra o momento na execu¸c˜ao em que todos os processos est˜ao tentando conseguir a prioridade para a regi˜ao cr´ıtica, mas somente o processo 1 conseguiu. Isso se deve a ele ter enviado o req antes do processo 0, e de ou ter enviado antes do outros processos tamb´em ou ter enviado ao mesmo tempo, sendo que neste caso ele ganha a prioridade por ter PID menor, da forma como o algoritmo de exclus˜ao m´utua de Lamport decide a garantia de prioridade `a ´area cr´ıtica. 18
  19. 19. Figura 2: Configurando um n´o slave no Neko antes de come¸car a execu¸c˜ao Figura 3: Come¸cando a execu¸c˜ao com o n´o master 19
  20. 20. Figura 4: Momento em que um processo consegue acesso `a regi˜ao cr´ıtica Por fim, a Figura 5 mostra o momento na execu¸c˜ao em que um processo termina de usar a regi˜ao cr´ıtica e manda em broadcast uma mensagem de release. Assim que os outros processos recebem essa mensagem, removem o req do processo remetente que estava no vetor de mensagens e testam novamente a prioridade. Um dos processos descobre que ele ´e o pr´oximo a receber o acesso `a regi˜ao cr´ıtica e entra, enquanto os outros processos continuam esperando. 5 As Classes Usadas Para Gerar o Log da Execu¸c˜ao Foi mencionada a cria¸c˜ao de um arquivo de log durante a execu¸c˜ao do algoritmo. Essa cria¸c˜ao do log ´e feita por um processo, que aqui foi escolhido como o processo n-1, que fica esperando mensagens espec´ıficas de gera¸c˜ao de log enviadas pelos outros processos, que est˜ao envolvidos na execu¸c˜ao do algoritmo. Essas mensagens espec´ıficas carregam as mensagens que estes processos enviam ou recebem, junto com o rel´ogio l´ogico do processo quando o evento do envio ou recebimento desta mensagem ocorreu. Isto ´e utilizado para criar o log, que consiste de linhas que registram detalhes das mensagens. Esse arquivo de log ´e usado na gera¸c˜ao de um gr´afico da execu¸c˜ao pelo logView, como ser´a mostrado na pr´oxima se¸c˜ao. 5.1 A Classe Event package lamport; import java.io.Serializable; 20
  21. 21. Figura 5: Momento em que um processo libera a r.c. e outro entra import lse.neko.NekoMessage; public class Event implements Serializable { NekoMessage m; int osn; public Event(NekoMessage m, int osn) { this.m = m; this.osn = osn; } public NekoMessage getMessage() { return m; } public int getOsn() { return osn; } } A classe Event serve para que as mensagens de log possam ser enviadas pelos processos participantes da execu¸c˜ao com as mensagens que eles trocam entre si e o tempo l´ogico que marca o momento em que eles enviam ou recebem estas mensagens. ´E como se as mensagens rebidas ou enviadas fossem “empacotadas” numa outra mensagem com alguns metadados necess´ario ao registro dessa mensagem no arquivo de log. Como estes envios ou recebimentos de mensagens s˜ao chamados de eventos em um ambiente distribu´ıdo a classe 21
  22. 22. recebeu este nome. ´E necess´ario que ela implemente a interface do Java chamada Serializable, caso contr´ario uma exce¸c˜ao ´e levantada durante a execu¸c˜ao em um ambiente distribu´ıdo. Esta interface Serializable n˜ao apresenta nenhum m´etodo a ser implementado. 5.2 A Classe MyLogger MyLogger ´e a classe que trata da cria¸c˜ao e gerenciamento do arquivo de log. package lamport; import java.io.BufferedWriter; import java.io.FileWriter; import lse.neko.MessageTypes; import lse.neko.NekoMessage; import lse.neko.NekoSystem; import org.apache.java.util.Configurations; public class MyLogger { Configurations config; int n; boolean firstEntry; public MyLogger(int n) { config = NekoSystem.instance().getConfig(); this.n = n; firstEntry = true; } O m´etodo construtor inicia as vari´aveis da classe. Um objeto Configurations ´e criado, para que o arquivo de configura¸c˜ao da execu¸c˜ao do Neko seja utilizado. Uma entrada nele guarda o diret´orio e nome do arquivo de log a ser criado. Outra vari´avel iniciada ´e a que guarda o n´umero de processos na execu¸c˜ao atual. Por fim, uma vari´avel booleana diz se estamos tratando da primeira entrada no arquivo de log, e por isso ele precisa ser criado do zero, ou se apenas anexamos a pr´oxima entrada ap´os as outras j´a gravadas, quando ele j´a foi criado e a execu¸c˜ao atual j´a est´a avan¸cada. public void doLogging(NekoMessage logMsg) { FileWriter fw; config = NekoSystem.instance().getConfig(); String logPath = config.getString("log"); Event event = (Event)logMsg.getContent(); int osn = event.getOsn(); NekoMessage msg = event.getMessage(); 22
  23. 23. int eventSource = logMsg.getSource(); int eventType = logMsg.getType(); String[] s = formatString(msg, eventSource, eventType, osn); for(int i=0; i<s.length;i++) { if(s[i] != null) { try{ if(firstEntry) { fw = new FileWriter(logPath, false); firstEntry = false; } else fw = new FileWriter(logPath, true); BufferedWriter out = new BufferedWriter(fw); out.write(s[i] + "n"); out.close(); } catch (Exception e) { System.err.println("Error: " + e.getCause()); } } } } O m´etodo doLogging trata os eventos de envio e recebimento de mensagens que devem ser registrados no arquivo de log. private String[] formatString(NekoMessage m, int source, int type, int osn) { String[] v = new String[n]; String time = String.valueOf(osn); String eventType = MessageTypes.instance().getName(type); int messageSource = m.getSource(); int[] messageDest = m.getDestinations(); String messageType = MessageTypes.instance().getName(m.getType()); String messageContent = String.valueOf(m.getContent()); if(eventType == "r") { String f = time + " p" + source + " messages e " + eventType + " p" + messageSource + " p" + source + " " + messageType + " " + messageContent; v[0] = f; } else for(int i=0; i<messageDest.length; i++) { String f = time + " p" + source + " messages e " + "s" + " p" + source + " p" + messageDest[i] + " " + messageType + " " + messageContent; v[i] = f; } return v; } 23
  24. 24. } E o m´etodo formatString cria as strings que representam cada evento e que ficar˜ao registradas em cada linha no arquivo de log. O formato das strings pode ser conferido no arquivo de log mostrado na pr´oxima se¸c˜ao. 6 Visualiza¸c˜ao de Execu¸c˜oes: o LogView Uma execu¸c˜ao do algoritmo produz um arquivo de log, com o nome e diret´orio passados no arquivo de configura¸c˜ao mostrado anteriormente, e que registra as mensagens enviadas e recebidas. Esse registros contˆem detalhes como o processo que enviou, o que recebeu, o tipo da mensagem, o conte´udo e etc.. Um arquivo de log produzido por uma execu¸c˜ao distribu´ıda ´e mostrado abaixo. Cada linha mostra, na ordem: o tempo l´ogico em que um processo enviou ou recebeu uma mensagem, qual processo foi esse, se o evento foi de envio (“s”) ou recebimento (“r”) de mensagem , qual foi o remetente e o destinat´ario, o tipo de mensagem e o conte´udo que ela carregou (no caso o conte´udo ´e o rel´ogio l´ogico do processo no momento de envio da mensagem). 1 p0 messages e r p2 p0 req 0 1 p0 messages e s p0 p2 ack 1 1 p1 messages e r p2 p1 req 0 1 p1 messages e s p1 p2 ack 1 0 p2 messages e s p2 p0 req 0 0 p2 messages e s p2 p1 req 0 2 p2 messages e r p1 p2 ack 1 3 p2 messages e r p0 p2 ack 1 1 p0 messages e s p0 p1 req 1 1 p0 messages e s p0 p2 req 1 4 p2 messages e r p0 p2 req 1 2 p1 messages e r p0 p1 req 1 4 p2 messages e s p2 p0 ack 4 2 p1 messages e s p1 p0 ack 2 3 p0 messages e r p1 p0 ack 2 5 p0 messages e r p2 p0 ack 4 2 p1 messages e s p1 p0 req 2 2 p1 messages e s p1 p2 req 2 6 p0 messages e r p1 p0 req 2 5 p2 messages e r p1 p2 req 2 6 p0 messages e s p0 p1 ack 6 5 p2 messages e s p2 p1 ack 5 6 p1 messages e r p2 p1 ack 5 7 p1 messages e r p0 p1 ack 6 6 p2 messages e s p2 p0 rel 6 6 p2 messages e s p2 p1 rel 6 7 p0 messages e r p2 p0 rel 6 8 p1 messages e r p2 p1 rel 6 8 p0 messages e s p0 p1 rel 8 8 p0 messages e s p0 p2 rel 8 9 p2 messages e r p0 p2 rel 8 9 p1 messages e r p0 p1 rel 8 9 p2 messages e s p2 p0 req 9 9 p2 messages e s p2 p1 req 9 24
  25. 25. 11 p1 messages e r p2 p1 req 9 10 p0 messages e r p2 p0 req 9 11 p1 messages e s p1 p2 ack 11 10 p0 messages e s p0 p2 ack 10 12 p2 messages e r p1 p2 ack 11 13 p2 messages e r p0 p2 ack 10 11 p1 messages e s p1 p0 rel 11 11 p1 messages e s p1 p2 rel 11 14 p2 messages e r p1 p2 rel 11 12 p0 messages e r p1 p0 rel 11 12 p0 messages e s p0 p1 req 12 12 p0 messages e s p0 p2 req 12 16 p2 messages e r p0 p2 req 12 13 p1 messages e r p0 p1 req 12 16 p2 messages e s p2 p0 ack 16 13 p1 messages e s p1 p0 ack 13 14 p0 messages e r p1 p0 ack 13 17 p0 messages e r p2 p0 ack 16 16 p2 messages e s p2 p0 rel 16 16 p2 messages e s p2 p1 rel 16 17 p1 messages e r p2 p1 rel 16 18 p0 messages e r p2 p0 rel 16 17 p1 messages e s p1 p0 req 17 17 p1 messages e s p1 p2 req 17 20 p0 messages e r p1 p0 req 17 18 p2 messages e r p1 p2 req 17 18 p2 messages e s p2 p1 ack 18 20 p0 messages e s p0 p1 ack 20 21 p1 messages e r p0 p1 ack 20 22 p1 messages e r p2 p1 ack 18 20 p0 messages e s p0 p1 rel 20 20 p0 messages e s p0 p2 rel 20 23 p1 messages e r p0 p1 rel 20 21 p2 messages e r p0 p2 rel 20 24 p1 messages e s p1 p0 rel 24 24 p1 messages e s p1 p2 rel 24 25 p0 messages e r p1 p0 rel 24 25 p2 messages e r p1 p2 rel 24 Com a ajuda de um programa chamado LogView podemos gerar um gr´afico visualizando o fluxo dessas mensagens e assim tendo uma id´eia de como ocorre o funcionamento de um algoritmo implementado no Neko. O LogView foi escrito tamb´em em Java e gera gr´aficos a partir de logs do Neko com o formato de mensagens mostrado acima, e um arquivo XML de configura¸c˜ao, em que v´arios detalhes podem ser modificados para gerar gr´aficos mais ao gosto do usu´ario. O que foi usado aqui neste documento foi ligeiramente modificado por mim em rela¸c˜ao ao original, que vem junto com o pr´oprio Neko. As modifica¸c˜oes incluem menos dependˆencia deste arquivo de configura¸c˜ao, sendo necess´ario apenas que o arquivo de log seja listado nele, e um menu de op¸c˜oes que possibilita mostrar tags sobre as setas que mostrem conte´udo da mensagem, tipo, tempos de envio e recebimento, entre outras modifica¸c˜oes. O arquivo de configura¸c˜ao usado nas visualiza¸c˜oes mostradas a seguir ´e este abaixo: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE logView SYSTEM "logView.dtd"> 25
  26. 26. <logView> <display> <timeAxis xSize="200"/> <!--timeAxis xSize="100"/> <processAxis ySize="100"/> <window xSize="1000" ySize="600"/> <messages type="REQUEST" color="red"/> <messages type="ACK" color="blue"/> <messages type="SCORE" color="yellow"/> <messages type="RELEASE" color="green"/> <label halign="left" valign="top" distance="30" percent="0.7"/--> </display> <file> <log filename="/home/alvaro/Desktop/log.txt"/> </file> </logView> Pode se ver que esse arquivo ´e bem pequeno. O cabe¸calho inicial indica a vers˜ao do XML, a codifica¸c˜ao e a segunda linha indica que este arquivo deve seguir o padr˜ao de um outro arquivo DTD (Documente Type Definition , que diz como ele deve ser entendido pelo parser e escrito pelo usu´ario. Este arquivo DTD ´e inclu´ıdo junto com o LogView. A segunda parte indica detalhes do gr´afico, como espa¸camento nos eixos do tempo e de processos, intervalos a serem representados, com que cor deve ser mostrado cada tipo de mensagem, entre outros. Pode-se ver que a parte entre <!-- e --> est´a comentada e n˜ao foi portanto considerada como configura¸c˜ao v´alida; essa parte ´e mostrada para exemplificar que tipo de op¸c˜oes est˜ao dispon´ıveis neste arquivo de configura¸c˜ao para personalizar a visualiza¸c˜ao. A ´ultima parte indica o arquivo de log a ser lido. A princ´ıpio a ´unica parte estritamente necess´aria que este arquivo deve conter ´e o cabe¸calho e a indica¸c˜ao do arquivo de log; o resto n˜ao ´e necess´ario e o logView consegue gerar diagramas sem estas outras configura¸c˜oes. A Figura 6 mostra o come¸co da execu¸c˜ao. Setas azuis representam mensagens de request, vermelhas de ack e cinzas de release. Os n´umeros sobre as setas s˜ao o conte´udo da mensagem, que neste caso ´e o rel´ogio l´ogico do processo no momento de envio da mensagem. Nesta figura visualiza-se as mensagens de request enviadas pelos processos 2, 1 e 0, nesta ordem, e as mensagens de ack enviadas por outros processos quando estas s˜ao recebidas. As setas come¸cam no tempo em que s˜ao enviadas pelo processo remetente e terminam no tempo em que foram recebidas pelo processo destinat´ario. A Figura 7 mostra o momento em que o processo 2 envia um release em broadcast. Como ele tinha enviado o request antes de todos os outros processos a prioridade de entrada na r.c. foi dada a ele primeiro. Ap´os fazer uso dela, ele faz esse broadcast aviasando todos os outros processos de que a prioridade n˜ao ´e mais dele e que outro processo pode usar a ´area cr´ıtica. Sabendo disso, o processo 0 ganha a prioridade, por ter enviado o “req” antes do processo 1. E, da mesma forma, ap´os terminar de usar a r.c. ele tamb´em faz broadcast de “rel”, e as setas cinzas saindo dele e indo para os outros processos mostram isso. ´E mostrado tamb´em o processo 2 tentando entrar na ´area cr´ıtica novamente, mandando um broadcast de request, e as setas azuis saindo dele no lado direito da imagem ilustram isso. Por ´ultimo, a Figura 8 mostra uma parte da execu¸c˜ao em que o Processo 0 termina de usar a regi˜ao cr´ıtica, faz um broadcast de release e ent˜ao o Processo 1 ganha a prioridade. 26
  27. 27. Figura 6: O come¸co da execu¸c˜ao. Todos os processos tentam entrar na ´area cr´ıtica. Figura 7: Processo 2 sai da r.c. e Processo 0 entra. Processo 2 tenta novamente entrar na r.c., fazendo um broadcast de request. 27
  28. 28. Figura 8: Processo 0 deixa a r.c., e o Processo 1 entra. Ap´os o uso da r.c. ambos fazem broadcast de release. Ap´os tamb´em terminar de fazer uso desta prioridade ele tamb´em avisa aos outros processos de que a r.c. est´a sem uso no momento. Importante deixar claro que ao entrar na regi˜ao cr´ıtica um processo aumenta seu rel´ogio l´ogico em uma unidade, portanto o Processo 1 recebeu o “rel” do Processo 0 no tempo 23 e, ao entrar na ´area cr´ıtica, aumentou seu rel´ogio l´ogico em 1 unidade, e por isso ele envia o seu broadcast de release no tempo 24. 7 Conclus˜ao Neste documento, foi apresentado o Neko, uma plataforma Java simples de comunica¸c˜ao que provˆe suporte `a simula¸c˜ao e prototipagem de algoritmos distribu´ıdos. Atrav´es dele, foi implementado o algoritmo de exclus˜ao m´utua de Lamport, e dessa forma foi mostrado como ´e simples e pr´atico o uso do Neko para este fim. Na classe que implementava o algoritmo propriamente, viu-se que n˜ao foi necess´ario muito mais c´odigo al´em do que descrevia a pr´opria execu¸c˜ao dele. A troca de mensagens ´e feita com m´etodos j´a implementados em classes do Neko, que tamb´em controla a camada de redes e a troca de mensagens entre as camadas da pilhas que representam os processos. Por fim, a execu¸c˜ao desta implementa¸c˜ao gerou um arquivo de log que tornou poss´ıvel a gera¸c˜ao de um gr´afico, representando a execu¸c˜ao do algoritmo e a troca de mensagens entre os processos. Cada mensagem foi representada por uma seta entre os processos remetente e destinat´ario, mostrando tamb´em uma etiqueta com o conte´udo dessas mensagens. Os tempos de chegada e sa´ıda destas setas eram os pr´oprios tempos l´ogicos em cada processo, 28
  29. 29. tornando poss´ıvel a visualiza¸c˜ao dos eventos na execu¸c˜ao segundo uma ordena¸c˜ao do tipo happened-before entre todas a mensagens. Sendo assim, a visualiza¸c˜ao gerada se torna um instrumento muito ´util para se enxergar como um algoritmo distribu´ıdo funciona. O Neko e o LogView podem ser encontrados nas p´aginas listadas na bibliografia. Referˆencias [1] L Lamport. “Time, Clocks, and the Ordering of Events in a Distributed System”. In: Communications of the ACM 21 (1978), pp. 558–565. [2] J. Muller, M. Galanthay e P. Urb´an. Rapport de Projet de Semestre Visualisation des Fichiers de Traces de Neko : LogView. 2002. url: http://ddsg.jaist.ac.jp/neko/ logView/rapport/rapport.pdf. [3] P. ´Urban, X. D´efago e A. Schiper. “Neko: A Single Environment to Simulate and Pro- totype Distributed Algotithms”. In: Journal of Information Science and Engineering 18 (2002), pp. 981–997. url: http://ddsg.jaist.ac.jp/pub/UDS02.pdf. 29

×