• Nenhum resultado encontrado

desenvolvimento de sistemas confiáveis baseados em componentes

No capítulo anterior conhecemos o trabalho descrito em [RAMOS, 2011] que propõe a composição sistemática e confiável de componentes. Nele, algumas condições foram expressas em termos de refinamentos que podem ser verificados automaticamente em FDR2. No entanto, muitas das condições foram descritas diretamente na semântica de CSP. A prova destas condições requer uma interação com provadores de teoremas que dificultariam a aplicação prática dessa estratégia. Nesse contexto, em [OLIVEIRA, 2012b] foi proposto um conjunto de asserções FDR2 que correspondem às condições originais de [RAMOS, 2011]. Utilizando-se deste trabalho, é possível verificar automaticamente em FDR2 todas as condições que validam os contratos e a composição entre eles.

A seguir, descrevemos as asserções apresentadas em [OLIVEIRA, 2012b], as quais são utilizadas por nossa ferramenta, BRIC-Tool-Suport (BTS), como base para a validação dos contratos e suas composições.

3.1

Análise CSP

Inicialmente, como descrito no capítulo anterior, um componente deve ter o compor- tamento de um processo de entrada e saída (I/O process). Para isso, ele deve atender às cinco condições descritas pelas asserções apresentadas na Tabela 3.1.

A primeira asserção diz respeito à condição de Canais de entrada e saída, onde para um processo P, é verificado se a interseção (inter) dos seus eventos de entrada com seus eventos de saída é vazia. Para essa verificação é necessária a definição de duas fun- ções: inputs(P) e outputs(P), as quais retornam os eventos de entrada e de saída de um componente, respectivamente. Na definição da asserção, foram criados os proces-

Canais de entrada e saída assert not Test(inter(inputs(P),outputs(P)) == {}) [T= ERROR

Traces infinitos assert not HideAll(P): [divergence free [FD]] Ausência de divergência assert P: [divergence free [FD]]

Entrada determinística assert LHS_InputDet_A(P) [F= RHS_InputDet(P) Saída fortemente decisiva assert LHS_OutputDec_A(P) [F= RHS_OutputDec_A(P)

assert LHS_OutputDec_B(P, c1) [F= RHS_OutputDec_B(P, c1) assert LHS_OutputDec_B(P, c2) [F= RHS_OutputDec_B(P, c2)

Tabela 3.1: Teste de Caracterização da Composição em Interleave

sos ERROR e Test, com os respectivos comportamentos: ERROR = error -> SKIP e Test(c) = not c & ERROR. Nessa verificação, inicialmente Test recebe uma con- dição booleana, cujo valor é representado pela verificação da interseção entre as inputs e outputs ser vazia ou não. Caso o valor de c seja verdadeiro, o processo Test nega a condição e bloqueia o processo, fazendo com que Test não refine ERROR. Porém, essa condição é negada, o que torna a verificação verdadeira.

A verificação que determina se um processo é livre de divergência, está incluído no próprio FDR2. Já para a verificação de traces infinitos, é usada uma função chamada Hideall que recebe um processo e esconde todos os seus eventos de entrada (inputs) e saída (outputs). Como a ocultação dos eventos de um processo que possui traces infinitos gera divergência, a afirmação da asserção é dada através de sua negação.

As verificações de entrada determinística e saída decisiva exigem um nível de comple- xidade bem maior em sua implementação, como descrito a seguir.

A entrada determinística é teoricamente definida como o seguinte:

Entrada Determinística. Um processo P possui entrada determinística se

∀s ˆ hc.ai : traces(P ) | c.a : inputs(c,P ) • (s,{c.a}) 6∈ failures(P )

Informalmente, isso significa que se um conjunto de eventos de entrada em P é ofere- cido ao ambiente, nenhum desses deve ser recusado.

A ideia geral dessa propriedade é verificar se o comportamento dos eventos de entrada de um processo é determinístico. Um processo é dito determinístico se, ao executarmos duas cópias do mesmo processo, ele comporta-se exatamente da mesma forma.

A solução para essa definição foi verificar o refinamento de falha entre dois processos: LHS_InputDet_A e RHS_InputDet, os quais descreveremos a seguir. Inicialmente, são executadas duas cópias de um processo, que sincronizam em um evento chamado clunk. Este evento sincroniza as cópias do processo depois de cada evento. Inicialmente,

isto é obtido pela execução do processo em paralelo com o outro, que força a produção do clunk depois de executar um evento.

Channel clunk

AllButClunk = diff(Events, clunk)

onde AllButClunk é o conjunto de todos os eventos de P com exceção do clunk.

Clunking(P) = P [| AllButClunk |] Clunker

Clunker = [] x: AllButClunk @ x -> clunk -> Clunker

Podemos observar que Clunking comporta-se exatamente como P, exceto que ele co- munica clunk entre cada sincronização de eventos. Depois são executadas cópias do processo em paralelo, sincronizando unicamente no clunk.

(Clunking(P) [| clunk |] Clunking(P)) \ clunk

Nesse caso, ambas as cópias dos processos executam independentemente, mas suas sequên- cias de traces nunca terão tamanho cuja diferença seja maior do que um, pois sempre que um dos processos executar um evento, esse aguardará a execução do outro para que ambos possam sincronizar no evento clunk.

Se P é um processo determinístico e uma cópia de P executa um evento, a outra não pode recusar. Nesse contexto, ambas as cópias devem ter o mesmo trace.

RHS_InputDet(P) =

(Clunking(P) [|cluck|] Clunking(P)) \ clunck [|AllButClunck|]

Repeat

Repeat = [] x: AllButClunck @ x -> x -> Repeat

Dessa forma, o processo RHS_InputDet força as duas cópias do processo a executa- rem o mesmo trace, por sincronizá-los com o processo Repeat e esse faz com que o evento sincronizado seja executado duas vezes. Com isso, nunca teremos o comportamento de

deadlocks, já que esse só ocorre se, depois da execução de traces da formaha,a...d,di, onde

um processo P executaha,...,di, uma cópia de P aceita um evento e a outra recusa. Voltando à Tabela 3.1, podemos comprovar o determinismo no modelo de falhas ve- rificando se RHS_InputDet(P) refina o seguinte processo sobre F (falhas).

Deterministic(S) =

STOP |˜|

([] x:AllButClunk @ x ->

(if member(x,S) then

x -> Deterministic(S)

else

(STOP |˜| x -> Deterministic(S))))

LHS_InputDet(P) = Deterministic(inputs(P))

assert LHS_InputDet(P) [F= RHS_InputDet(P)

O processo LHS_inputDet(P) especifica um comportamento determinístico do con- junto de eventos de entradas de um dado processo (inputs(P)), tal que a função Deterministicexecuta um evento pertencente a AllButClunk, caso esse faça parte das entradas de P, é executada em seguida outra cópia desse evento. Como estamos fo- cando apenas no comportamento determinístico para os eventos de entrada, em qualquer outro momento o processo pode travar.

Finalizando o conjunto de asserções da Tabela 3.1, apresentaremos a seguir a verifi- cação da saída fortemente decisiva, a qual é definida da seguinte maneira:

Saída fortemente decisiva. Um processo P possui saída fortemente decisiva se:

∀s ˆ hc.bi : traces(P ) | c.b : outputs(c,P )•

(s, outputs(c, P ))6∈ failures(P ) ∧ (s,outputs(c,P ) \ {c.b}) ∈ failures(P )

Isso significa que todas as escolhas entre eventos de saídas de um determinado canal em P são internas. Esse não determinismo na saída fornece ao processo a capacidade de fazer decisões internas baseadas nas entradas. Em um processo com saída fortemente decisiva, dado o trace s ˆ hc.bi tal que c.b pertence a saída de P , toda saída em c será recusada, exceto c.b - representado por (s, outputs(c)\ {|c.b|}) ∈ failures(P ). Com isso, é nota-se que o processo sempre irá oferecer uma saída nesse canal.

A solução para Saída fortemente decisiva é dividida em duas partes: a primeira con- dição verifica se depois da execução de uma trace s ˆhc.xi, o processo não pode recusar todos os eventos em {|c|}, o qual representa o conjuntos de todos os eventos através do canal c). A primeira verificação é feita para todo o processo de uma só vez.

Considerando o processo Clunker(P) descrito anteriormente, temos duas cópias que sincronizam no evento clunk e em todos os outros, exceto os valores de outputs(P) que representam o conjunto de eventos de saída de todos os canais de um processo.

(Clunking(P) [| diff(Events, outputs(P))|] Clunking(P)) \ { clunk }

O processo One2Many executa os eventos de um processo e que não fazem parte dos canais de saída. Caso ocorra uma saída, é forçada outra comunicação neste canal.

One2Many(S) =

([] x:diff(Events,union(S,{ clunk })) @ x -> One2Many(S)) [] ([] c:S @ [] x: {|c|} @ x -> One2Many’(S,c,x))

One2Many’(S,c,x) =

[] y:{|c|} @ y -> if x==y then One2Many(S) else STOP

Colocando esse processo em paralelo com os descritos acima, temos o primeiro lado da asserção apresentado a seguir:

RHS_OutputDec_A(P) =

(Clunking(P) [|diff(Events, outputs(P))|] Clunking(P)) \ {clunck} [| AllButClunk |]

One2Many(outputs(P))

O processo RHS_OutputDec_A(P) sincroniza as cópias do processo em todos os eventos, menos os de saída. Quando ocorrer um evento de saída, uma cópia irá sincronizar com o processo One2Many que em seguida aguardará o evento de saída da outra cópia. O teste só continuará sendo executado se ambas as cópias realizarem o mesmo evento. Assim, todas as vezes que o teste carregar, ambas as cópias de P devem realizar o mesmo trace. O resultado dessa implementação é testada contra a especificação a seguir:

LHS_OutputDec_A(P) = STOP |˜| ([] x:diff(Events,union(outputs(P),{ clunck })) @ x -> LHS_OutputDec_A(P)) [] ([] x:outputs(P) @ x -> (|˜| y:chan(x,P) @ y -> LHS_OutputDec_A(P))) onde

chan(ev,P) =

inter(outputs(P),

{| c | c <- GET_CHANNELS(P), member(ev,{|c|})|})

e a função GET_CHANNELS(P) retorna todos os canais do processo.

No processo LHS_OutputDec_A os eventos de P são executados normalmente até ocorrer um evento pertencente a saída, que força a execução de outro evento no mesmo canal, através da função chan. Além disso, é considerado que o processo pode a qualquer momento travar, por isso existe uma escolha interna entre executar o comportamento descrito por LHS_OutputDec_A ou STOP.

Assim, o refinamento LHS_OutputDec_A [F= RHS_OutputDec_A(P) checa que depois de todos os traces t do processo P, a ocorrência de uma saída em um canal é sempre acompanhada por um evento desse mesmo canal na cópia. Ou seja, essa asserção verifica que caso um determinado trace t possa ser seguido de uma saída no canal c, o processo não pode recusar todas as saídas em c. No entanto, esse refinamento não verifica se um processo é não determinístico para um dado canal de saída. Sendo assim, é necessária uma verificação adicional para garantir uma escolha não determinística nesse canal.

A segunda condição para se ter uma saída fortemente decisiva é verificar que depois do trace sˆhc.xi, o processo pode recusar todos os eventos em {|c|} \ {c.x}. Assim, o processo é não-determinístico para as saídas no canal.

A asserção a seguir verifica individualmente cada canal no processo. Por exemplo, supondo o alfabeto de P igual ahc1,c2i, são necessárias as seguintes asserções:

assert LHS_OutputDec_B(P,c1) [F= RHS_OutputDec_B(P,c1) assert LHS_OutputDec_B(P,c2) [F= RHS_OutputDec_B(P,c2)

Nessa parte da verificação, o refinamento garante que em cada saída de P que for bloqueada, o deadlock deve ocorrer sempre no mesmo trace.

Para mais detalhes sobre a verificação formal de entrada determinística e saída forte-

mente decisiva, consulte [OLIVEIRA, 2012a].

Além da análise descrita na Tabela 3.1, que caracteriza toda as condições que um processo deve atender para poder ser composto de forma segura, foram definidos outros conjuntos de asserções voltados para verificação dos processos, auxiliando nas regras de composição.

Análise de Composição

A composição em interleave é a mais simples de todas. Nessa regra, para dois processos P e Q, não deve existir eventos em comum entre eles. Para isso, é utili- zado o processo RUN que executa todos os eventos da interseção entre dois processos (inter(events(P),events(Q)), tal que events é uma função que recebe um pro- cesso e realiza a união (union) entre os seus eventos de entrada e seus eventos de saída). Para que essa condição seja válida, a interseção entre os processos deve ser vazia e conse- quentemente, seus traces serão vazios. Nesse contexto, seu resultado deve refinar os traces de STOP. Como apresentado na asserção a seguir.

assert STOP [T= RUN(inter(events(P),events(Q)))

As próximas regras apresentam um nível de complexidade maior, já que lidam com a interação entre os componentes. Como discutido no capítulo anterior, a composição em comunicação requer que o protocolo tenha entrada e saída confluente, forte compatibi- lidade e satisfaça a propriedade de saída finita. A seguir apresentaremos o conjunto de asserções que verificam essas propriedades.

As asserções descritas em [OLIVEIRA, 2012b] foram definidas para um estudo de caso manual, que gerou e analisou toda especificação em um único módulo, ou seja, de forma genérica, onde, através da função “apply”, do FDR2, pega vários processos de uma só vez e aplica a verificação. Logo, algumas alterações foram necessárias, como a retirada dessa função. Em nossa ferramenta a descrição e análise são realizadas de forma específica, ou seja, para processos (contratos) individuais descritos na BTS, que será apresentado no próximo Capítulo. Tomando como base esse exemplo, acrescentamos que todas as modificações realizadas foram em nível de código CSP, tentando buscar a adaptação do conjunto de verificações a o que propõe a ferramenta. Essas verificações representam a implementação das propriedades descritas na Seção 2.3 que validam a composição entre componentes.

Inicialmente, apresentaremos o grupo de asserções que validam a propriedade de en- tradas e saídas confluentes.

Todas as verificações descritas a partir desse ponto são voltadas para os protocolos dos processos.

Iniciamos verificando se o protocolo do processo P é livre de divergência, tal que PROT_IMP_Prepresenta a implementação do processo sobre um canal.

assert PROT_IMP_P :[divergence free [FD]]

Logo em seguida é verificado se este de fato é um protocolo válido para um deter- minado canal no processo P, ou seja, ele deve ser refinado pela projeção de P sobre um canal. Além disso, também é verificado se o protocolo é um refinamento dessa projeção. Essas duas verificações garantem que protocolo do processo P possui os mesmos traces de sua projeção sobre um canal.

assert PROT_IMP_P [F= PROT_IMP_def(P, ip)

assert PROT_IMP_def(P,ip) [FD= PROT_IMP_P

Foram definidas asserções para verificar se todos os eventos da entrada ou da saída de um protocolo fazem parte das produções de um único canal. Para isso é realizada uma interseção entre os eventos de entrada do processo P com os eventos do canal ip (inputs_PROT_IMP(P, ip)). Logo em seguida, através da função subseteq é ve- rificado se todos esses eventos de entrada do processo fazem parte das produções de um único canal. O resultado é analisado através dos dois processos utilizados na verificação de Canais de entrada e saída (Tabela 3.1) Test e ERROR.

assert not Test(subseteq(inputs_PROT_IMP(P, ip), {|ip|})) [T= ERROR assert not Test(subseteq(outputs_PROT_IMP(P, ip), {|ip|})) [T= ERROR

A última asserção que valida a condição de entrada e saída confluente verifica se uma renomeação do protocolo é determinística.

assert InBufferProt(PROT_IMP_R(P,ip ,R_IO(P,ip,oq))):[deterministic[F]]

tal que R_IO(P,ip,oq) é uma função que recebe dois canais (ip, oq) e renomeia os dados de oq pelos dados de saída de ip. Por exemplo, para o evento ip.x outputs(ip) será descrito o evento oq.x. Isso significa que o canal de entrada de um processo será renomeado com os dados do canal de saída do outro. Em seguida, a função PROT_IMP_R substitui o canal renomeado oq no protocolo de ip, verificando se esse tem o comportamento determinístico. Como os canais são representados pela saída de P e a entrada do outro processo, respectivamente, o resultado dessa substituição sendo determinístico, implica dizer que no envio de informação para o outro processo não terá problema no recebimento.

O segundo conjunto de verificações para a composição em comunicação analisa a com- patibilidade de protocolo. Essa análise é referente a versão renomeada desses protocolos (PROT_IMP_R), descrita na verificação de entrada e saída confluente.

A primeira asserção verifica se o protocolo PROT_IMP_R é livre de deadlocks. Em seguida, é verificado se ele é um protocolo de comunicação, tanto na entrada como na saída, igualmente as asserções anteriores, porém essas verificam o comportamento de PROT_IMP_R. Essas verificações são realizadas para validar a descrição dos protocolos renomeados.

assert PROT_IMP_R(P, ip,R_IO(P,ip,oq)):[deadlock free [FD]]

assert not Test(subseteq(inputs(PROT_IMP_R(P, ip,R_IO(P,ip,oq)), {|ip|})) [T= ERROR

assert not Test(subseteq(outputs(PROT_IMP_R(P, ip,R_IO(P,ip,oq)),{|ip|})) [T= ERROR

Nessa parte, analisamos também o comportamento do Dual-protocol. Inicialmente, verificamos se o conjunto de eventos de entrada de um protocolo é igual ao conjunto de eventos de saída do seu dual-protocol.

assert not Test(inputs(PROT_IMP_R(P, ip,R_IO(P,ip,oq))) ==

outputs(DUAL_PROT_IMP_R(P, ip,R_IO(P,ip,oq)))) [T= ERROR

Para uma análise mais segura, também é verificado se o protocolo e seu dual possuem traces equivalentes.

assert DUAL_PROT_IMP_R(P, ip,R_IO(P,ip,oq)) [T= PROT_IMP_R(P,

ip,R_IO(P,ip,oq))

Finalizando, a analise de protocolo fortemente compatível pode ser definida por veri- ficar se o dual-protocol de um protocolo P é refinado pelo protocolo Q.

assert DUAL_PROT_IMP_R(P, ip,R_IO(P,ip,oq)) [F= PROT_IMP_R(Q,

iq,R_IO(P,ip,oq))

Verificada todas as condições acima, a propriedade de saída finita é caracterizada pela asserção que exclui todos os eventos de saída de um protocolo, mas não introduz divergência, como apresentado a seguir:

assert PROT_IMP_R(P, ip,R_IO(P,ip,oq)) \ allOutputs:[divergence free [FD]]

A composição por feedback impõe as mesmas restrições da composição por comunica- ção descritas anteriormente, exceto que adicionalmente analisa a condição de que canais sejam desacoplados, como descrito a seguir.

assert PROJECTION(P, ip,oq) [FD= INTER_PROT_IMP(P, ip, oq)

A análise para a propriedade de canais desacoplados recebe dois canais de um pro- cesso e realiza uma verificação através de um refinamento bidirecional entre a proje- ção de P sobre os canais e a intercalação da implementação dos protocolos, tal que (INTER_PROT_IMP) gera um interleave entre os protocolos dos canais descritos na en- trada e PROJECTION retorna a projeção de dois canais sobre o processo P.

Concluindo, a composição reflexiva possui, adicionalmente, a verificação de Compa- tibilidade de Auto-Injeção com Buffer, que estabelece comunicação entre canais de um mesmo processo via buffer sem introduzir deadlocks, como descrito a seguir:

assert not PROJECTION(P,ip, oq) [| {| ip, oq |} |] BUFF_IO_1(P_LR1, P_LR2) :[deadlock free [F]]

onde a PROJECTION do processo P sobre dois canais é colocado em paralelo com o processo BUFF_IO_1 que recebe dois conjuntos de eventos de comunicação P_LR1 e P_LR2, renomeia seus canais e os coloca em interleave. O interleave desses sincronizando em paralelo com a projeção de um processo sobre esses dois canais deve ser livre de

deadlock.

Utilizando esse conjunto de verificações foi possível aplicar a abordagem para o desen- volvimento sistemático e confiável em uma verificação automática através da ferramenta apresentada no próximo capítulo.

Documentos relacionados