sistemas operacionais iii - cooperação entre tarefas

48
Sistemas Operacionais III - Cooperação entre Tarefas Prof. Carlos Alberto Maziero PPGIA CCET PUCPR http://www.ppgia.pucpr.br/maziero 9 de maio de 2008 Resumo Muitas implementações de sistemas complexos são estruturadas como várias tarefas inter-dependentes, que cooperam entre si para atingir os objetivos da apli- cação, como por exemplo em um navegador Web. Para que as várias tarefas que compõem uma aplicação possam cooperar, elas precisam comunicar informações umas às outras e coordenar suas atividades, para garantir que os resultados obti- dos sejam coerentes. Este módulo apresenta os principais conceitos, problemas e soluções referentes à comunicação e à coordenação entre tarefas. Copyright (c) 2006 Carlos Alberto Maziero. É garantida a permissão para copiar, distribuir e/ou mo- dificar este documento sob os termos da Licença de Documentação Livre GNU (GNU Free Documentation License), Versão 1.2 ou qualquer versão posterior publicada pela Free Software Foundation. A licença está disponível em http://www.gnu.org/licenses/gfdl.txt.

Upload: others

Post on 17-Oct-2021

3 views

Category:

Documents


0 download

TRANSCRIPT

Sistemas OperacionaisIII - Cooperação entre Tarefas ∗

Prof. Carlos Alberto MazieroPPGIA CCET PUCPR

http://www.ppgia.pucpr.br/∼maziero

9 de maio de 2008

Resumo

Muitas implementações de sistemas complexos são estruturadas como váriastarefas inter-dependentes, que cooperam entre si para atingir os objetivos da apli-cação, como por exemplo em um navegador Web. Para que as várias tarefas quecompõem uma aplicação possam cooperar, elas precisam comunicar informaçõesumas às outras e coordenar suas atividades, para garantir que os resultados obti-dos sejam coerentes. Este módulo apresenta os principais conceitos, problemas esoluções referentes à comunicação e à coordenação entre tarefas.

∗Copyright (c) 2006 Carlos Alberto Maziero. É garantida a permissão para copiar, distribuir e/ou mo-dificar este documento sob os termos da Licença de Documentação Livre GNU (GNU Free DocumentationLicense), Versão 1.2 ou qualquer versão posterior publicada pela Free Software Foundation. A licença estádisponível em http://www.gnu.org/licenses/gfdl.txt.

c©Prof. Carlos Maziero SUMÁRIO – 2

Sumário

1 Objetivos 3

2 Comunicação entre tarefas 32.1 Formas de comunicação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4

2.1.1 Comunicação direta ou indireta . . . . . . . . . . . . . . . . . . . . . 52.1.2 Sincronismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52.1.3 Formato de envio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62.1.4 Capacidade dos canais . . . . . . . . . . . . . . . . . . . . . . . . . . 82.1.5 Número de participantes . . . . . . . . . . . . . . . . . . . . . . . . 9

2.2 Exemplos de mecanismos de comunicação . . . . . . . . . . . . . . . . . . 102.2.1 UNIX Message queues . . . . . . . . . . . . . . . . . . . . . . . . . . 112.2.2 Pipes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122.2.3 Memória compartilhada . . . . . . . . . . . . . . . . . . . . . . . . . 13

3 Coordenação entre tarefas 163.1 Condições de disputa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163.2 Seções críticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193.3 Inibição de interrupções . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203.4 Soluções com espera ocupada . . . . . . . . . . . . . . . . . . . . . . . . . . 21

3.4.1 A solução óbvia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213.4.2 Alternância de uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213.4.3 O algoritmo de Peterson . . . . . . . . . . . . . . . . . . . . . . . . . 223.4.4 Instruções Test-and-Set . . . . . . . . . . . . . . . . . . . . . . . . . . 233.4.5 Problemas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24

3.5 Semáforos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253.6 Variáveis de condição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283.7 Monitores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

4 Problemas clássicos de coordenação 314.1 O problema dos produtores/consumidores . . . . . . . . . . . . . . . . . . 314.2 O problema dos leitores/escritores . . . . . . . . . . . . . . . . . . . . . . . 334.3 O jantar dos filósofos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

5 Impasses 365.1 Caracterização de impasses . . . . . . . . . . . . . . . . . . . . . . . . . . . 375.2 Grafos de alocação de recursos . . . . . . . . . . . . . . . . . . . . . . . . . 385.3 Técnicas de tratamento de impasses . . . . . . . . . . . . . . . . . . . . . . 40

5.3.1 Prevenção de impasses . . . . . . . . . . . . . . . . . . . . . . . . . . 405.3.2 Impedimento de impasses . . . . . . . . . . . . . . . . . . . . . . . . 415.3.3 Detecção e resolução de impasses . . . . . . . . . . . . . . . . . . . 43

c©Prof. Carlos Maziero Objetivos – 3

1 Objetivos

Nem sempre um programa seqüencial é a melhor solução para um determinadoproblema. Muitas vezes, as implementações são estruturadas na forma de várias tarefasinter-dependentes que cooperam entre si para atingir os objetivos da aplicação, comopor exemplo em um navegador Web. Existem várias razões para justificar a construçãode sistemas baseados em tarefas cooperantes, entre as quais podem ser citadas:

Atender vários usuários simultâneos : um servidor de banco de dados ou de e-mailcompletamente seqüencial atenderia um único cliente por vez, gerando atrasos in-toleráveis para os demais clientes. Por isso, servidores de rede são implementadoscom vários processos ou threads, para atender simultaneamente todos os usuáriosconectados.

Uso de computadores multi-processador : um programa seqüencial executa um únicofluxo de instruções por vez, não importando o número de processadores presentesno hardware. Para aumentar a velocidade de execução de uma aplicação, estadeve ser “quebrada” em várias tarefas cooperantes, que poderão ser escalonadassimultaneamente nos processadores disponíveis.

Modularidade : um sistema muito grande e complexo pode ser melhor organizadodividindo suas atribuições em módulos sob a responsabilidade de tarefas inter-dependentes. Cada módulo tem suas próprias responsabilidades e coopera comos demais módulos quando necessário. Sistemas de interface gráfica, como osprojetos Gnome [Gno05] e KDE [KDE05], são geralmente construídos dessa forma.

Construção de aplicações interativas : navegadores Web, editores de texto e jogos sãoexemplos de aplicações comalta interatividade; nelas, tarefas associadas à interfacereagem a comandos do usuário, enquanto outras tarefas comunicam através darede, fazem a revisão ortográfica do texto, renderizam imagens na janela, etc.Construir esse tipode aplicaçãode forma totalmente seqüencial seria simplesmenteinviável.

Para que as tarefas presentes emumsistema possam cooperar, elas precisam comuni-car, compartilhando as informações necessárias à execução de cada tarefa, e coordenarsuas atividades, para que os resultados obtidos sejam consistentes (sem erros). Estemódulo visa estudar os principais conceitos, problemas e soluções empregados parapermitir a comunicação e a coordenação entre tarefas executando em um sistema.

2 Comunicação entre tarefas

Tarefas cooperantes precisam trocar informações entre si. Por exemplo, a tarefaque gerencia os botões e menus de um navegador Web precisa informar rapidamente

c©Prof. Carlos Maziero Formas de comunicação – 4

as demais tarefas caso o usuário clique nos botões stop ou reload. Outra situação decomunicação freqüente ocorre quando o usuário seleciona um texto em uma página daInternet e o arrasta para um editor de textos. Em ambos os casos ocorre a transferênciade informação entre duas tarefas distintas.

Implementar a comunicação entre tarefas pode ser simples ou complexo, depen-dendo da situação. Se as tarefas estão no mesmo processo, elas compartilham a mesmaárea de memória e a comunicação pode então ser implementada facilmente, usandovariáveis globais comuns. Entretanto, caso as tarefas pertençam a processos distintos,não existem variáveis compartilhadas; neste caso, a comunicação tem de ser feita porintermédio do núcleo do sistema operacional, usando chamadas de sistema. Caso astarefas estejam em computadores distintos, o núcleo deve implementar mecanismos decomunicação específicos, fazendo uso do suporte de rede disponível. A figura 1 ilustraessas três situações.

Figura 1: Comunicação intra-processo (ti → t j), inter-processos (t j → tk) e inter-sistemas(tk → tl).

2.1 Formas de comunicação

A implementação da comunicação entre tarefas pode ocorrer de várias formas. Aodefinir os mecanismos de comunicação oferecidos por um sistema operacional, seusprojetistas devem considerar muitos aspectos, como o formato dos dados a transferir, osincronismo exigido nas comunicações, a necessidade de buffers e o número de emisso-res/receptores envolvidos em cada ação de comunicação. Um termo muito empregadopara designar genericamente os mecanismos de comunicação é IPC - Inter-Process Com-munication.

c©Prof. Carlos Maziero Formas de comunicação – 5

2.1.1 Comunicação direta ou indireta

De formamais abstrata, a comunicação entre tarefas pode ser implementada por duasprimitivas básicas: enviar (dados, destino), que envia os dados relacionados ao destinoindicado, e receber (dados, origem), que recebe os dados previamente enviados pela origemindicada. Essa abordagem, na qual o emissor identifica claramente o receptor e vice-versa, é denominada comunicação direta.

Poucos sistemas empregam a comunicação direta; na prática são utilizados meca-nismos de comunicação indireta, por serem mais flexíveis. Na comunicação indireta,emissor e receptor não precisam se conhecer, pois não interagem diretamente entre si.Eles se relacionam através de um canal de comunicação, que é criado pelo sistema ope-racional, geralmente a pedido de uma das partes. Assim, as primitivas de comunicaçãonão designam diretamente tarefas, mas canais de comunicação aos quais as tarefas estãoassociadas: enviar (dados, canal) e receber (dados, canal).

2.1.2 Sincronismo

Em relação aos aspectos de sincronismo do canal de comunicação, a comunicaçãoentre tarefas pode ser:

Síncrona : quando as operações de envio e recepção de dados bloqueiam (suspendem)as tarefas envolvidas até a conclusãoda comunicação: o emissor será bloqueado atéque a informação seja recebida pelo receptor, e vice-versa. A figura 2 apresenta osdiagramas de tempo de duas situações freqüentes em sistemas com comunicaçãosíncrona.

Figura 2: Comunicação síncrona.

Assíncrona : em um sistema com comunicação assíncrona, as primitivas de envio erecepção não são bloqueantes: caso a comunicação não seja possível no momento

c©Prof. Carlos Maziero Formas de comunicação – 6

em que cada operação é invocada, esta retorna imediatamente com uma indicaçãode erro. Deve-se observar que, caso o emissor e o receptor operem ambos deforma assíncrona, torna-se necessário criar um buffer para armazenar os dados dacomunicação entre eles. Sem esse buffer, a comunicação se tornará inviável, poisraramente ambos estarão prontos para comunicar ao mesmo tempo. Esta formade comunicação esta representada no diagrama de tempo da figura 3.

Figura 3: Comunicação assíncrona.

Semi-síncrona : primitivas de comunicação semi-síncronas têm um comportamentosíncrono (bloqueante) durante um prazo pré-definido. Caso o prazo se esgotesem que a comunicação tenha ocorrido, a primitiva retorna com uma indicação deerro. Para refletir esse comportamento, as primitivas de comunicação recebem umparâmetro adicional : enviar (dados, destino, prazo) e receber (dados, origem, prazo). Afigura 4 ilustra duas situações em que ocorre esse comportamento.

2.1.3 Formato de envio

A informação enviada pelo emissor ao receptor pode ser vista basicamente de duasformas: como uma seqüência demensagens independentes, cada uma com seu próprioconteúdo, ou como um fluxo seqüencial e contínuo de dados, imitando o comporta-mento de um arquivo com acesso seqüencial.

Na abordagem baseada em mensagens, cada mensagem consiste de um pacote dedados quepode ser tipadoounão. Esse pacote é recebido oudescartadopelo receptor emsua íntegra; não existe a possibilidade de receber “meiamensagem” (figura 5). Exemplosde sistema de comunicação orientados a mensagens incluem asmessage queues do UNIXe os protocolos de rede IP e UDP, apresentados na seção 2.2.

Caso a comunicação seja definida como um fluxo contínuo de dados, o canal decomunicação é visto como o equivalente a um arquivo: o emissor “escreve” dados nesse

c©Prof. Carlos Maziero Formas de comunicação – 7

Figura 4: Comunicação semi-síncrona.

Figura 5: Comunicação baseada em mensagens.

canal, que serão “lidos” pelo receptor respeitando a ordem de envio dos dados. Não háseparação lógica entre os dados enviados em operações separadas: eles podem ser lidosbyte a byte ou em grandes blocos a cada operação de recepção, a critério do receptor. Afigura 6 apresenta o comportamento dessa forma de comunicação.

Exemplos de sistemas de comunicação orientados a fluxo de dados incluem os pipesdo UNIX e o protocolo de rede TCP/IP (este último é normalmente classificado comoorientado a conexão, com o mesmo significado). Nestes dois exemplos a analogia como conceito de arquivos é tão forte que os canais de comunicação são identificados pordescritoresde arquivos e as chamadasde sistemaread ewrite (normalmenteusadas comarquivos) são usadas para enviar e receber os dados. Esses exemplos são apresentadosem detalhe na seção 2.2.

c©Prof. Carlos Maziero Formas de comunicação – 8

Figura 6: Comunicação baseada em fluxo de dados.

2.1.4 Capacidade dos canais

O comportamento síncrono ou assíncrono de um canal de comunicação pode serafetado pela presença de buffers que permitam armazenar temporariamente os dadosem trânsito, ou seja, as informações enviadas pelo emissor e que ainda não foramrecebidas pelo receptor. Em relação à capacidade de buffering do canal de comunicação,três situações devem ser analisadas:

Capacidade nula (n = 0) : neste caso, o canal não pode armazenar dados; a comuni-cação é feita por transferência direta dos dados do emissor para o receptor, semcópias intermediárias. Caso a comunicação seja síncrona, o emissor permanecebloqueado até que o destinatário receba os dados, e vice-versa. Essa situação es-pecífica (comunicação síncrona com canais de capacidade nula) implica em umaforte sincronização entre as partes, sendo por isso denominadaRendez-Vous (termofrancês para “encontro”). A figura 2 ilustra dois casos de Rendez-Vous. Por outrolado, a comunicação assíncrona torna-se inviável usando canais de capacidadenula (conforme discutido na seção 2.1.2).

Capacidade infinita (n = ∞) : o emissor sempre pode enviar dados, que serão armaze-nados no buffer do canal enquanto o receptor não os consumir. Obviamente essasituação não existe na prática, pois todos os sistemas de computação têm capaci-dade de memória e de armazenamento finitas. No entanto, essa simplificação éútil no estudo dos algoritmos de comunicação e sincronização, pois torna menoscomplexas a modelagem e análise dos mesmos.

Capacidade finita (0 < n < ∞) : neste caso, uma quantidade finita (n) de dados podeser enviada pelo emissor sem que o receptor os consuma. Todavia, ao tentarenviar dados em um canal já saturado, o emissor poderá ficar bloqueado até surgir

c©Prof. Carlos Maziero Formas de comunicação – 9

espaço no buffer do canal e conseguir enviar (comportamento síncrono) ou receberum retorno indicando o erro (comportamento assíncrono). Amaioria dos sistemasreais opera com canais de capacidade finita.

Para exemplificar esse conceito, a figura 7 apresenta o comportamento de duastarefas trocando dados através de um canal de comunicação com capacidade para duasmensagens e comportamento síncrono (bloqueante).

Figura 7: Comunicação síncrona usando um canal com capacidade 2.

2.1.5 Número de participantes

Nas situações de comunicação apresentadas até agora, cada canal de comunicaçãoenvolve apenas um emissor e um receptor. No entanto, existem situações em queuma tarefa necessita comunicar com várias outras, como por exemplo em sistemas dechat ou mensagens instantâneas (IM – Instant Messaging). Dessa forma, os mecanismosde comunicação também podem ser classificados de acordo com o número de tarefasparticipantes:

1:1 : quando exatamente um emissor e um receptor interagem através do canal decomunicação; é a situação mais freqüente, implementada por exemplo nos pipes eno protocolo TCP.

M:N : quando um ou mais emissores enviam mensagens para um ou mais receptores.Duas situações distintas podem se apresentar neste caso:

• Cada mensagem é recebida por apenas um receptor (em geral aquele quepedir primeiro); neste caso a comunicação continua sendo ponto-a-ponto,

c©Prof. Carlos Maziero Exemplos de mecanismos de comunicação – 10

através de um canal compartilhado. Essa abordagem é conhecida como mail-box (figura 8), sendo implementada nas message queues UNIX e nos sockets doprotocolo UDP. Na prática, o mailbox funciona como um buffer de dados, noqual os emissores depositam mensagens e os receptores as consomem.

• Cadamensagem é recebida por todos os receptores (cada receptor recebe umacópia da mensagem). Essa abordagem é conhecida pelos nomes de difusão(multicast) ou canal de eventos (figura 9), sendo implementada por exemplo noprotocolo UDP.

Figura 8: Comunicação M:N através de um mailbox.

Figura 9: Difusão através de um canal de eventos.

2.2 Exemplos de mecanismos de comunicação

Nesta seção serão apresentados alguns mecanismos de comunicação usados comfreqüência em sistemas UNIX. Mais detalhes sobre estes e outros mecanismos podemser obtidos em [Ste98, RR03]. Mecanismos de comunicação implementados nos sistemasWindows são apresentados em [Pet98, Har04].

c©Prof. Carlos Maziero Exemplos de mecanismos de comunicação – 11

2.2.1 UNIX Message queues

As filas de mensagens foram definidas inicialmente na implementação UNIX SystemV, sendo atualmente suportadas pela maioria dos sistemas. Além do padrão System V, opadrãoPosix tambémdefineuma interfaceparamanipulaçãodefilasdemensagens. Essemecanismo é um bom exemplo de implementação do conceito de mailbox, permitindo oenvio e recepção ordenada de mensagens tipadas entre processos locais. As operaçõesde envio e recepção podem ser síncronas ou assíncronas, a critério do programador.

A listagem a seguir implementa um “consumidor de mensagens”, ou seja, um pro-grama que cria uma fila para receber mensagens. O código apresentado segue o padrãoPOSIX (exemplos de uso de filas de mensagens no padrão System V estão disponíveisem [RR03]). Para compilá-lo em Linux é necessário efetuar a ligação com o biblioteca detempo-real POSIX, usando a opção -lrt.

1 #include <stdio.h>

#include <stdlib.h>

#include <mqueue.h>

#include <sys/stat.h>

int main (int argc, char *argv[])

{

mqd_t queue; // descritor da fila de mensagens

struct mq_attr attr; // atributos da fila de mensagens

int msg ; // mensagens contendo um inteiro

11

// define os atributos da fila de mensagens

attr.mq_maxmsg = 10 ; // capacidade para 10 mensagens

attr.mq_msgsize = sizeof(msg) ; // tamanho de cada mensagem

attr.mq_flags = 0 ;

umask (0) ; // mascara de permissoes (umask)

// abre ou cria a fila com permissoes 0666

if ( (queue = mq_open ("/fila", O_RDWR|O_CREAT, 0666, &attr)) == -1)

21 {

perror ("mq_open");

exit (1);

}

// recebe cada mensagem e imprime seu conteudo

while (1)

{

if ( (mq_receive (queue, (void*) &msg, sizeof(msg), 0)) == -1)

{

31 perror("mq_receive:") ;

exit (1) ;

}

printf ("Received msg value %d\n", msg);

}

}

c©Prof. Carlos Maziero Exemplos de mecanismos de comunicação – 12

A listagem a seguir implementa o programa produtor das mensagens consumidaspelo programa anterior:

#include <stdio.h>

#include <stdlib.h>

#include <mqueue.h>

4 #include <unistd.h>

int main (int argc, char *argv[])

{

mqd_t queue; // descritor da fila

int msg; // mensagem a enviar

// abre a fila de mensagens

if( (queue = mq_open ("/fila", O_RDWR)) == -1)

{

14 perror ("mq_open");

exit (1);

}

while (1)

{

msg = random() % 1000 ; // valor entre 0 e 999

// envia a mensagem

if ( mq_send (queue, (void*) &msg, sizeof(msg), 0) == -1)

24 {

perror ("mq_send");

exit (1);

}

printf ("Sent message with value %d\n", msg);

sleep (1) ;

}

}

Deve-se observar que o arquivo /fila referenciado em ambas as listagens serveunicamente como identificador único para a fila de mensagens; nenhum arquivo comesse nome será criado pelo sistema. As mensagens não transitam por arquivos, apenaspela memória do núcleo. Referências de recursos na forma de nomes de arquivos sãousadas para identificar vários mecanismos de comunicação e coordenação em UNIX,como filas de mensagens, semáforos e áreas de memória compartilhadas (vide seção2.2.3).

2.2.2 Pipes

Um dos mecanismos de comunicação entre processos mais simples de usar no ambi-ente UNIX é o pipe, ou tubo. Na interface de linha de comandos, o pipe é freqüentementeusado para conectar a saída padrão (stdout) de um comando à entrada padrão (stdin)

c©Prof. Carlos Maziero Exemplos de mecanismos de comunicação – 13

de outro comando, permitindo assim a comunicação entre eles. A linha de comando aseguir traz um exemplo do uso de pipes:

# who | grep marcos | sort > login-marcos.txt

A saída do comando who é uma listagem de usuários conectados ao computador.Essa saída é encaminhada através de um pipe (indicado pelo caractere “|”) ao comandogrep, que a filtra e gera como saída somente as linhas contendo a string “marcos”. Essasaída é encaminhada através de outro pipe ao comando sort, que ordena a listagemrecebida e a deposita no arquivo login-marcos.txt. Deve-se observar que todos osprocessos envolvidos são lançados simultaneamente; suas ações são coordenadas pelocomportamento síncrono dos pipes. A figura 10 detalha essa seqüência de ações.

Figura 10: Comunicação através de pipes.

O pipe é um canal de comunicação unidirecional entre dois processos (1:1), comcapacidade finita (os pipes do Linux armazenam 4 KBytes por default), visto pelosprocessos como um arquivo, ou seja, a comunicação que ele oferece é baseada em fluxo.O envio e recepção de dados são feitos pelas chamadas de sistema write e read, quepodem operar em modo síncrono (bloqueante, por default) ou assíncrono.

O uso de pipes na linha de comando é simples, mas seu uso na construção deprogramas pode ser complexo. Vários exemplos do uso de pipes UNIX na construçãode programas são apresentados em [RR03].

2.2.3 Memória compartilhada

A comunicação entre tarefas situadas em processos distintos deve ser feita atravésdo núcleo, usando chamadas de sistema, porque não existe a possibilidade de acessoa variáveis comuns a ambos. No entanto, essa abordagem pode ser ineficiente caso acomunicação seja muito volumosa e freqüente, ou envolva muitos processos. Para essassituações, seria conveniente ter uma área de memória comum que possa ser acessadadireta e rapidamente pelos processos interessados, sem o custo da intermediação pelonúcleo.

A maioria dos sistemas operacionais atuais oferece mecanismos para o compartilha-mento de áreas de memória entre processos (shared memory areas). As áreas de memória

c©Prof. Carlos Maziero Exemplos de mecanismos de comunicação – 14

compartilhadas e os processos que as utilizam são gerenciados pelo núcleo, mas o acessoao conteúdo de cada área é feito diretamente pelos processos, sem intermediação ou co-ordenação do núcleo. Por essa razão, mecanismos de coordenação (apresentados naseção 3) podem ser necessários para garantir a consistência dos dados armazenadosnessas áreas.

A criação e uso de uma área de memória compartilhada entre dois processos pa epb em um sistema UNIX pode ser resumida na seguinte seqüência de passos, ilustradafigura 11:

1. O processo pa solicita ao núcleo a criação de uma área de memória compartilhada,informando o tamanho e as permissões de acesso; o retorno dessa operação é umidentificador (id) da área criada.

2. O processo pa solicita ao núcleo que a área recém-criada seja anexada ao seuespaço de endereçamento; esta operação retorna um ponteiro para a nova área dememória, que pode então ser acessada pelo processo.

3. O processo pb obtém o identificador id da área de memória criada por pa.

4. O processo pb solicita ao núcleo que a área de memória seja anexada ao seu espaçode endereçamento e recebe um ponteiro para o acesso à mesma.

5. Os processos pa e pb acessam a área de memória compartilhada através dos pon-teiros informados pelo núcleo.

Deve-se observar que, ao solicitar a criação da área de memória compartilhada, padefine as permissões de acesso à mesma; por isso, o pedido de anexação da área dememória feito por pb pode ser recusado pelo núcleo, se violar as permissões definidaspor pa. A listagem a seguir exemplifica a criação e uso de uma área de memóriacompartilhada, usando o padrão POSIX (exemplos de implementação no padrão SystemV podem ser encontrados em [RR03]). Para compilá-lo em Linux é necessário efetuara ligação com o biblioteca de tempo-real POSIX, usando a opção -lrt. Para melhorobservar seu funcionamento, devem ser lançados dois ou mais processos executandoesse código simultaneamente.

#include <stdio.h>

#include <stdlib.h>

#include <fcntl.h>

#include <sys/stat.h>

#include <sys/mman.h>

int main (int argc, char *argv[])

{

9 int fd, value, *ptr;

// Passos 1/3: abre/cria uma area de memoria compartilhada

fd = shm_open("/sharedmem", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);

c©Prof. Carlos Maziero Exemplos de mecanismos de comunicação – 15

Figura 11: Criação e uso de uma área de memória compartilhada.

if(fd == -1)

{

perror ("shm_open");

exit (1) ;

}

19 // Passos 1/3: ajusta o tamanho da area compartilhada

if (ftruncate(fd, sizeof(value)) == -1)

{

perror ("ftruncate");

exit (1) ;

}

// Passos 2/4: mapeia a area no espaco de enderecamento deste processo

ptr = mmap(NULL, sizeof(value), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

if(ptr == MAP_FAILED)

29 {

perror ("mmap");

exit (1);

}

c©Prof. Carlos Maziero Coordenação entre tarefas – 16

while (1)

{

// Passo 5: escreve um valor aleatorio na area compartilhada

value = random () % 1000 ;

(*ptr) = value ;

39 printf ("Wrote value %i\n", value) ;

sleep (1);

// Passo 5: le e imprime o conteudo da area compartilhada

value = (*ptr) ;

printf("Read value %i\n", value);

sleep (1) ;

}

}

3 Coordenação entre tarefas

Em um sistema multi-tarefas, várias tarefas podem executar simultaneamente, aces-sando recursos compartilhados como áreas dememória, arquivos, conexões de rede, etc.Nesta seção serão estudados os problemas que podem ocorrer quando duas ou mais ta-refas acessam os mesmos recursos de forma concorrente; também serão apresentadas asprincipais técnicas usadas para coordenar de forma eficiente os acessos das tarefas aosrecursos compartilhados.

3.1 Condições de disputa

Quando duas ou mais tarefas acessam simultaneamente um recurso compartilhado,podem ocorrer problemas de consistência dos dados ou do estado do recurso acessado.Esta seção descreve detalhadamente a origem dessas inconsistências, através de umexemplo simples, mas que permite ilustrar claramente o problema.

O código apresentado a seguir implementa de forma simplificada a operação dedepósito (função depositar) de um valor em uma conta bancária informada como pa-râmetro. Para facilitar a compreensão do código de máquina apresentado na seqüência,todos os valores manipulados são inteiros.

typedef struct conta_t

{

3 int saldo ; // saldo atual da conta

... // outras informações da conta

} conta_t ;

void depositar (conta_t* conta, int valor)

{

conta->saldo += valor ;

}

c©Prof. Carlos Maziero Condições de disputa – 17

Após a compilação em uma plataforma Intel i386, a função depositar assume aseguinte forma em código de máquina (nos comentários ao lado do código, regi é umregistrador e mem(x) é a posição de memória onde está armazenada a variável x):

00000000 <depositar>:

push %ebp # guarda o valor do "stack frame"

mov %esp,%ebp # ajusta o stack frame para executar a função

mov 0x8(%ebp),%ecx # mem(saldo) → reg1mov 0x8(%ebp),%edx # mem(saldo) → reg2mov 0xc(%ebp),%eax # mem(valor) → reg3add (%edx),%eax # [reg1, reg2] + reg3 → [reg1, reg2]mov %eax,(%ecx) # [reg1, reg2] → mem(saldo)leave # restaura o stack frame anterior

ret # retorna à função anterior

Considere que a função depositar faz parte de um sistema mais amplo de controlede contas bancárias, que pode ser acessado simultaneamente por centenas ou milharesde usuários em terminais distintos. Caso dois clientes em terminais diferentes tentemdepositar valores na mesma conta ao mesmo tempo, existirão duas tarefas acessando osdados (variáveis) da conta de forma concorrente. A figura 12 ilustra esse cenário.

Figura 12: Acessos concorrentes a variáveis compartilhadas.

O comportamento dinâmico da aplicação pode ser modelado através de diagramasde tempo. Caso o depósito da tarefa t1 execute integralmente antes ou depois dodepósito efetuado por t2, teremos os diagramas de tempo da figura 13. Em ambas asexecuções o saldo inicial da conta passou de R$ 0,00 para R$ 1050,00, conforme esperado.

No entanto, caso as operações de depósito de t1 e de t2 se entrelacem, podem ocor-rer interferências entre ambas, levando a resultados incorretos. Em sistemas mono-processados, a sobreposição pode acontecer caso ocorram trocas de contexto durante aexecução do depósito. Em sistemasmulti-processados a situação é aindamais complexa,pois cada tarefa poderá estar executando em um processador distinto.

Os diagramas de tempo apresentados na figura 14 mostram execuções onde houveentrelaçamento das operações de depósito de t1 e de t2. Em ambas as execuções o saldofinal não corresponde ao resultado esperado, pois um dos depósitos é perdido. No caso,

c©Prof. Carlos Maziero Condições de disputa – 18

Figura 13: Operações de depósitos não-concorrentes.

apenas é concretizado o depósito da tarefa que realizou a operação mem(saldo)← reg1por último1.

Figura 14: Operações de depósito concorrentes.

Os erros e inconsistências gerados por acessos concorrentes a dados compartilhados,como os ilustrados na figura 14, são denominados condições de disputa, ou condiçõesde corrida (do inglês race conditions). Condições de disputa podem ocorrer em qualquersistema onde várias tarefas (processos ou threads) acessam de forma concorrente recur-sos compartilhados (variáveis, áreas de memória, arquivos abertos, etc). Finalmente,

1Não há problema em ambas as tarefas usarem os mesmos registradores reg1 e reg2, pois os valores detodos os registradores são salvos/restaurados a cada troca de contexto entre tarefas.

c©Prof. Carlos Maziero Seções críticas – 19

condições de disputa somente existem caso ao menos uma das operações envolvidasseja de escrita; acessos de leitura concorrentes entre si são geram condições de disputa.

É importante observar que condições de disputa são erros dinâmicos, ou seja, que nãoaparecemno código fonte e que só semanifestamdurante a execução, sendo dificilmentedetectáveis através da análise do código fonte. Além disso, erros dessa natureza nãose manifestam a cada execução, mas apenas quando certos entrelaçamentos ocorrerem.Assim, uma condição de disputa poderá permanecer latente no código durante anos, oumesmo nunca se manifestar. A depuração de programas contendo condições de disputapode ser muito complexa, pois o problema só se manifesta com acessos simultâneosaos mesmos dados, o que pode ocorrer raramente e ser difícil de reproduzir durantea depuração. Por isso, é importante conhecer técnicas que previnam a ocorrência decondições de disputa.

3.2 Seções críticas

Na seção anterior vimos que tarefas acessando dados compartilhados de forma con-corrente podem ocasionar condições de disputa. Os trechos de código de cada tarefaque acessam dados compartilhados são denominados seções críticas (ou regiões críticas).No caso da figura 12, as seções críticas das tarefas t1 e t2 são idênticas e resumidas àseguinte linha de código:

conta.saldo += valor ;

De modo geral, seções críticas são todos os trechos de código que manipulam dadoscompartilhados onde podem ocorrer condições de disputa. Um programa pode ter vá-rias seções críticas, relacionadas entre si ou não (caso manipulem dados compartilhadosdistintos). Para assegurar a correção de uma implementação, deve-se impedir o entrela-çamento de seções críticas: apenas uma tarefa pode estar na seção crítica a cada instante.Essa propriedade é conhecida como exclusão mútua.

Diversos mecanismos podem ser definidos para impedir o entrelaçamento de seçõescríticas e assim prover a exclusão mútua. Todos eles exigem que o programador definaos limites (início e o final) de cada seção crítica. Genericamente, cada seção crítica ipode ser associada a um identificador csi e são definidas as primitivas enter(ta, csi), paraque a tarefa ta indique que deseja entrar na seção crítica csi, e leave(ta, csi), para que tainforme que está saindo da seção crítica csi. A primitiva enter(csi) é bloqueante: casouma tarefa já esteja ocupando a seção crítica csi, as demais tarefas que tentarem entrardeverão aguardar até que a primeira libere a seção crítica, através da primitiva leave(csi).

Usando as primitivas enter e leave, o código da operação de depósito visto na seção3.1 pode ser reescrito como segue:

typedef struct conta_t

{

int saldo ; // saldo atual da conta

int numero ; // identificação da conta (seção crítica)

... // outras informações da conta

c©Prof. Carlos Maziero Inibição de interrupções – 20

} conta_t ;

void depositar (conta_t* conta, int valor)

9 {

enter (conta->numero) ; // tenta entrar na seção crítica

conta->saldo += valor ; // está na seção crítica

leave (conta->numero) ; // sai da seção crítica

}

Nas próximas seções serão estudadas várias soluções para a implementação dasprimitivas enter e leave, bem como abordagens alternativas. As soluções propostasdevem atender a alguns critérios básicos que são enumerados a seguir:

Exclusão mútua : somente uma tarefa pode estar dentro da seção crítica em cada ins-tante.

Espera limitada : uma tarefa que aguarda acesso a uma seção crítica deve ter esse acessogarantido em um tempo finito.

Independência de outras tarefas : a decisão sobre o uso de uma seção crítica devedepender somente das tarefas que estão tentando entrar na mesma. Outras tarefasdo sistema, que no momento não estejam interessadas em entrar na região crítica,não podem ter influência sobre essa decisão.

Independência de fatores físicos : a solução deve ser puramente lógica e não dependerda velocidade de execução das tarefas, de temporizações, do número de processa-dores no sistema ou de outros fatores físicos.

3.3 Inibição de interrupções

Uma solução simples para a implementação das primitivas enter e leave consiste emimpedir as trocas de contexto dentro da seção crítica. Ao entrar em uma seção crítica, atarefa desativa (mascara) as interrupções que possam provocar trocas de contexto, e asreativa ao sair da seção crítica. Apesar de simples, essa solução raramente é usada paraa construção de aplicações devido a vários problemas:

• Ao desligar as interrupções, a preempção por tempo ou por recursos deixa defuncionar; caso a tarefa entre em um laço infinito dentro da seção crítica, o sistemainteiro será bloqueado. Uma tarefa mal-intencionada pode forçar essa situação etravar o sistema.

• Enquanto as interrupções estão desativadas, os dispositivos de entrada/saída dei-xam de ser atendidos pelo núcleo, o que pode causar perdas de dados ou outrosproblemas. Por exemplo, uma placa de rede pode perder novos pacotes se seusbuffers estiverem cheios e não forem tratados pelo núcleo em tempo hábil.

c©Prof. Carlos Maziero Soluções com espera ocupada – 21

• A tarefa que está na seção crítica não pode realizar operações de entrada/saída,pois os dispositivos não irão responder.

Devido a esses problemas, a inibição de interrupções é uma operação privilegiada esomente utilizada em algumas seções críticas dentro do núcleo do sistema operacionale nunca pelas aplicações.

3.4 Soluções com espera ocupada

Uma primeira classe de soluções para o problema da exclusão mútua no acesso aseções críticas consiste em testar continuamente uma condição que indica se a seçãodesejada está livre ou ocupada. Esta seção apresenta algumas soluções clássicas usandoessa abordagem.

3.4.1 A solução óbvia

Uma solução aparentemente trivial para o problema da seção crítica consiste em usaruma variável busy para indicar se a seção crítica desejada está livre ou ocupada. Usandoessa abordagem, a implementação das primitivas enter e leave poderia ser escrita assim:int busy = 0 ; // a seção está inicialmente livre

void enter (int task)

{

while (busy) ; // espera enquanto a seção estiver ocupada

busy = 1 ; // marca a seção como ocupada

7 }

void leave (int task)

{

busy = 0 ; // libera a seção (marca como livre)

}

Infelizmente, essa solução óbvia e simples não funciona! Seu grande defeito é que oteste da variável busy (na linha 5) e sua atribuição (na linha 6) são feitos em momentosdistintos; casoocorrauma trocade contexto entre as linhas 5 e 6do código, poderá ocorreruma condição de disputa envolvendo a variável busy, que terá como conseqüência aviolação da exclusão mútua: duas ou mais tarefas poderão entrar simultaneamente naseção crítica (vide o diagrama de tempo da figura 15). Em outras palavras, as linhas 5 e6 da implementação também formam uma seção crítica, que deve ser protegida.

3.4.2 Alternância de uso

Outra solução simples para a implementação das primitivas enter e leave consiste emdefinir uma variável turno, que indica de quem é a vez de entrar na seção crítica. Essavariável deve ser ajustada cada vez que uma tarefa sai da seção crítica, para indicar apróxima tarefa a usá-la. A implementação das duas primitivas fica assim:

c©Prof. Carlos Maziero Soluções com espera ocupada – 22

Figura 15: Condição de disputa no acesso à variável busy.

int turn = 0 ;

int num_tasks ;

void enter (int task)

{

while (turn <> task) ; // a tarefa espera seu turno

}

8

void leave (int task)

{

if (turn < num_tasks) // o turno é da próxima tarefa

turn ++ ;

else

turn = 1 ; // volta à primeira tarefa

}

Nessa solução, cada tarefa aguarda seu turno de usar a seção crítica, em uma seqüên-cia circular: t1 → t2 → t3 → · · · → tn → t1. Essa abordagem garante a exclusão mútuaentre as tarefas e independe de fatores externos, mas não atende os demais critérios:caso uma tarefa ti não deseje usar a seção crítica, todas as tarefas t j com j > i ficarãoimpedidas de fazê-lo, pois a variável turno não irá evoluir.

3.4.3 O algoritmo de Peterson

Uma solução correta para a exclusão mútua no acesso a uma seção crítica por duastarefas foi proposta inicialmente por Dekker em 1965. Em 1981, Gary Peterson propôsuma solução mais simples e elegante para o mesmo problema [Ray86]. O algoritmo dePeterson pode ser resumido no código a seguir:

int turn = 0 ; // indica de quem é a vez

int wants[2] = {0, 0} ; // indica se a tarefa i quer acessar a seção crítica

c©Prof. Carlos Maziero Soluções com espera ocupada – 23

void enter (int task) // task pode valer 0 ou 1

5 {

int other = 1 - task ; // indica a outra tarefa

wants[task] = 1 ; // task quer acessar a seção crítica

turn = task ;

while ((turn == task) && wants[other]) ; // espera ocupada

}

void leave (int task)

{

wants[task] = 0 ; // task libera a seção crítica

15 }

Os algoritmos deDekker e de Peterson foramdesenvolvidos para garantir a exclusãomútua entre duas tarefas, garantindo também o critério de espera limitada2. Diversasgeneralizações para n tarefas podem ser encontradas na literatura [Ray86], sendo a maisconhecida delas o algoritmo da padaria [Lam74], proposto por Leslie Lamport em 1974.

3.4.4 Instruções Test-and-Set

O uso de uma variável busy para controlar a entrada em uma seção crítica é umaidéia interessante, que infelizmente não funciona porque o teste da variável busy e seuajuste são feitos em momentos distintos do código, permitindo condições de corrida.

Para resolver esse problema, projetistas de hardware criaram instruções em códigode máquina que permitem testar e atribuir um valor a uma variável de forma atômicaou indivisível, sem possibilidade de troca de contexto entre essas duas operações. Aexecução atômica das operações de teste e atribuição (Test & Set instructions) impedea ocorrência de condições de disputa. Uma implementação básica dessa idéia estána instrução de máquina Test-and-Set Lock (TSL), cujo comportamento é descrito peloseguinte pseudo-código (que é executado atomicamente pelo processador):

TSL(x) : old← x

x← 1return(old)

A implementação das primitivas enter e leave usando a instrução TSL assume aseguinte forma:

int lock = 0 ; // variável de trava

2Este algoritmo pode falhar quando usado em algumas máquinas multi-processadas, pois algumasarquiteturas permitem acesso fora de ordem à memória, ou seja, permitem que operações de leitura namemória se antecipem a operações de escrita executadas posteriormente, para obter mais desempenho.Este é o caso dos processadores Pentium e AMD.

c©Prof. Carlos Maziero Soluções com espera ocupada – 24

void enter (int *lock) // passa o endereço da trava

{

5 while ( TSL (*lock) ) ; // espera ocupada

}

void leave (int *lock)

{

(*lock) = 0 ; // libera a seção crítica

}

Outrosprocessadores oferecemuma instruçãoque efetua a troca atômicade conteúdo(swapping) entre dois registradores, ou entre um registrador e uma posição de memória.No caso da família de processadores Intel i386 (incluindo o Pentium e seus sucessores),a instrução de troca se chama XCHG (do inglês exchange) e sua funcionalidade pode serresumida assim:

XCHG op1, op2 : op1 ⇄ op2

A implementação das primitivas enter e leave usando a instrução XCHG é um poucomais complexa:

int lock ; // variável de trava

enter (int *lock)

{

int key = 1 ; // variável auxiliar (local)

while (key) // espera ocupada

XCHG (lock, &key) ; // alterna valores de lock e key

}

9

leave (int *lock)

{

(*lock) = 0 ; // libera a seção crítica

}

Os mecanismos de exclusão mútua usando instruções atômicas no estilo TSL sãoamplamente usados no interior do sistema operacional, para controlar o acesso a se-ções críticas internas do núcleo, como descritores de tarefas, buffers de arquivos ou deconexões de rede, etc. Nesse contexto, eles são muitas vezes denominados spinlocks. To-davia, mecanismos de espera ocupada são inadequados para a construção de aplicaçõesde usuário, como será visto a seguir.

3.4.5 Problemas

Apesar das soluções para o problemade acesso à seção crítica usando espera ocupadagarantirem a exclusão mútua, elas sofrem de alguns problemas que impedem seu usoem larga escala nas aplicações de usuário:

c©Prof. Carlos Maziero Semáforos – 25

Ineficiência : as tarefas que aguardam o acesso a uma seção crítica ficam testando con-tinuamente uma condição, consumindo tempo de processador sem necessidade.O procedimento adequado seria suspender essas tarefas até que a seção críticasolicitada seja liberada.

Injustiça : não há garantia de ordem no acesso à seção crítica; dependendo da duraçãode quantum e da política de escalonamento, uma tarefa pode entrar e sair da seçãocrítica várias vezes, antes que outras tarefas consigam acessá-la.

Por estas razões, as soluções com espera ocupada são pouco usadas na construçãode aplicações. Seu maior uso se encontra na programação de estruturas de controle deconcorrência dentro do núcleo do sistema operacional (onde se chamam spinlocks), ena construção de sistemas de computação dedicados, como controladores embarcadosmais simples.

3.5 Semáforos

Em 1965, o matemático holandês E. Dijkstra propôs um mecanismo de coordena-ção eficiente e flexível para o controle da exclusão mútua entre n tarefas: o semáforo[Ray86]. Apesar de antigo, o semáforo continua sendo o mecanismo de sincronizaçãomais utilizado na construção de aplicações concorrentes, sendo usados de forma explí-cita ou implícita (na construção de mecanismos de coordenação mais abstratos, como osmonitores).

Um semáforo pode ser visto como uma variável s, que representa uma seção críticae cujo conteúdo não é acessível ao programador. Internamente, cada semáforo contémum contador inteiro s.counter e uma fila de tarefas s.queue, inicialmente vazia. Sobre essavariável podem ser aplicadas duas operações primitivas, cuja execução deve ser sempreatômica:

Down(s) : usado para solicitar acesso à seção crítica associada a s. Caso a seção estejalivre, a operação retorna imediatamente e a tarefa pode continuar sua execução;caso contrário, a tarefa solicitante é suspensa e adicionada ao final da fila do semá-foro; o contador associado ao semáforo é decrementado3. Dijkstra denominou essaoperação P(s) (do holandês probeer, que significa tentar), sendo também conhecidacomoWait(s).

s.counter← s.counter − 1if s.counter < 0 thenpõe a tarefa corrente no final de s.queuesuspende a tarefa corrente

3Alguns sistemas implementam também a chamada TryDown(s), cuja semântica é não-bloqueante:caso o semáforo solicitado esteja ocupado, a chamada retorna imediatamente, com um código de erro.

c©Prof. Carlos Maziero Semáforos – 26

end if

Up(s) : invocado para liberar a seção crítica associada a s; o contador associado aosemáforo é incrementado. Caso a fila do semáforo não esteja vazia, a primeiratarefa da fila é acordada, sai da fila e pode continuar sua execução. Essa operaçãofoi inicialmente denominada V(s) (do holandês verhoog, que significa incrementar),sendo também conhecida como Signal(s). Deve-se observar que esta chamada nãoé bloqueante: a tarefa não é suspensa ao executá-la.

Up(s):s.counter← s.counter + 1if s.counter ≤ 0 thenretira a primeira tarefa t de s.queuedevolve t à fila de tarefas prontas (acorda t)

end if

Usando essas primitivas, o código de depósito em conta bancária apresentado naseção 3.1 poderia ser reescrito da seguinte forma:

typedef struct conta_t

{

int saldo ; // saldo atual da conta

sem_t s = 1; // semáforo associado à conta, valor inicial 1

... // outras informações da conta

} conta_t ;

7

void depositar (conta_t * conta, int valor)

{

down (conta->s) ; // solicita acesso à conta

conta->saldo += valor ; // seção crítica

up (conta->s) ; // libera o acesso à conta

}

A suspensão das tarefas que aguardam o acesso à seção crítica elimina a espera ocu-pada, o que torna esse mecanismo mais eficiente que os anteriores. A fila de tarefasassociada ao semáforo contém todas as tarefas que foram suspensas ao solicitar acessoà seção crítica usando Down(s). Como a fila obedece uma política FIFO, garante-se atambém a justiça no acesso à seção crítica. Por sua vez, o valor inteiro associado aosemáforo funciona como um contador de recursos: caso seja positivo, indica quantasinstâncias daquele recurso estão disponíveis. Caso seja negativo, indica quantas ta-refas estão aguardando para usar aquele recurso. Seu valor inicial permite expressardiferentes situações de sincronização, como será visto na seção 4.

A listagem a seguir ilustra uma implementação possível de semáforos e das primiti-vasDown(s) eUp(s); deve-se no entanto ressaltar que as execuções de ambas as primitivasdevem ser atômicas (indivisíveis), para evitar condições de disputa envolvendo os pró-prios semáforos. A execução atômica pode ser obtida através dosmecanismos estudados

c©Prof. Carlos Maziero Semáforos – 27

na seção 3.4 (neste caso a espera ocupada não constitui um problema, pois a execuçãodessas primitivas é muito rápida).

task_t *current_task ; // aponta para a tarefa em execução no momento

typedef struct sem_t // estrutura que representa um semáforo

{

int counter ;

task_t *queue ;

7 } sem_t ;

void sem_init (sem_t *sem, int init_value)

{

sem->counter = init_value ; // inicializa o semáforo com o valor indicado

sem->queue = NULL ;

}

void sem_down (sem_t *sem) // esta implementação deve ser atômica

{

17 sem->counter-- ;

if (sem->counter < 0) {

append (current_task, sem->queue) ;

sched_yield () ; // suspende a tarefa corrente e retorna ao dispatcher

}

}

void sem_up (sem_t *sem) // esta implementação deve ser atômica

{

sem->counter++ ;

27 if (sem->counter <= 0)

{

awake first task from sem->queue ;

}

}

A API POSIX define várias chamadas para a criação e manipulação de semáforos.As mais frequentemente utilizadas estão indicadas a seguir:

#include <semaphore.h>

// inicializa um semáforo apontado por "sem", com valor inicial "value"

int sem_init(sem_t *sem, int pshared, unsigned int value);

// Operação Up(s)

int sem_post(sem_t *sem);

9 // Operação Down(s)

int sem_wait(sem_t *sem);

// Operação TryDown(s), retorna erro se o semáforo estiver ocupado

int sem_trywait(sem_t *sem);

c©Prof. Carlos Maziero Variáveis de condição – 28

Os semáforos nos quais o contador inteiro pode assumir qualquer valor são de-nominados semáforos genéricos e constituem um mecanismo de coordenação muitopoderoso. No entanto, Muitos ambientes de programação, bibliotecas de threads e atémesmo núcleos de sistema provêem uma versão simplificada de semáforos, na qual ocontador só assume dois valores possíveis: livre (1) ou ocupado (0). Esses semáforos sim-plificados são chamados demutexes (uma abreviação demutual exclusion) ou semáforosbinários. Por exemplo, algumas das funções definidas pelo padrão de threads Posix[Gal94, Bar05] para criar e usar mutexes são:

• pthread_mutex_init (mutex,attr): inicializa uma variável do tipo mutex.

• pthread_mutex_lock (mutex): solicita acesso à seção crítica representada pormutex; a tarefa ficará bloqueada se a seção já estiver ocupada.

• pthread_mutex_unlock (mutex): libera o acesso à seção crítica representada pormutex; se houverem tarefas aguardando, a primeira da fila obtém acesso à seçãocrítica.

3.6 Variáveis de condição

Além dos semáforos, outro mecanismo de sincronização de uso freqüente são asvariáveis de condição, ou variáveis condicionais. Uma variável de condição não representaum valor, mas uma condição, que pode ser testada por uma tarefa. Assim, uma tarefanão precisa testar continuamente uma determinada condição, evitando uma situaçãoindesejável de espera ocupada.

Uma tarefa aguardando uma condição representada pela variável de condição cpode ficar suspensa através do operador wait(c), para ser notificada mais tarde, quandoa condição se tornar verdadeira. A notificação ocorre quando outra tarefa chamar ooperador notify(c). Por definição, uma variável de condição c está sempre associadaa um semáforo binário c.mutex, que protege a condição representada pela variável decondição, e a uma fila c.queue, para armazenar as tarefas que aguardam aquela condição.As operações wait e notify podem ser representadas desta forma, para uma tarefa t:

wait (c):c.queue← t // coloca a tarefa t no fim de s.queue

unlock (c.mutex) // libera o mutex

suspend (t) // põe a tarefa atual para dormir

lock (c.mutex) // quando acordar, obtém o mutex

7 notify (c):awake (first task in c.queue)

No exemplo a seguir, a tarefa A espera por uma condição que será sinalizada pelatarefa B. A condição de espera pode ser qualquer: um novo dado em um buffer deentrada, a conclusão de um procedimento externo, etc.

c©Prof. Carlos Maziero Monitores – 29

Task A:

2 ...

lock (c.mutex)while (not condition)wait (c) ;

unlock (c.mutex)...

Task B:

...

lock (c.mutex)12 condition = true

notify (c)unlock (c.mutex)...

É importante observar que na definição inicial de variáveis de condição (conhecidacomo semântica de Hoare), a operação notify(c) faz com que a tarefa notificadora percaimediatamente o semáforo e o controle do processador, que são devolvidos à primeiratarefa que estiver esperando emwait(c). Como essa semântica é complexa de implemen-tar e interfere diretamente com o escalonador de processos, as implementações reaisde variáveis de condição normalmente adotam a semântica Mesa [LR80], da linguagemde mesmo nome. Nessa semântica, a operação notify(c) apenas “acorda” as tarefas queesperam pela condição, sem parar a execução da tarefa corrente.

3.7 Monitores

Ao usar semáforos, um programador define explicitamente os pontos de sincroniza-ção necessários em seu programa. Essa abordagem é eficaz para programas pequenose problemas de sincronização simples, mas se torna inviável e suscetível a erros emsistemas maiores. Por exemplo, se o programador esquecer de liberar um semáforopreviamente alocado, o programa pode entrar em um impasse (vide seção 5). Por outrolado, se ele esquecer de requisitar o semáforo, a exclusão mútua pode ser violada.

Em 1972, os cientistas Per Brinch Hansen e Charles Hoare definiram o conceito demonitor [LR80]. Um monitor é uma estrutura de sincronização que requisita e liberaa seção crítica associada a um recurso de forma transparente, sem que o programadortenha de se preocupar com isso. Um monitor consiste de:

• um recurso compartilhado, visto como um conjunto de variáveis internas ao mo-nitor.

• um conjunto de procedimentos que permitem o acesso a essas variáveis;

• um mutex ou semáforo para controle de exclusão mútua; cada procedimento deveobter o semáforo antes de iniciar e liberar o semáforo ao concluir;

c©Prof. Carlos Maziero Monitores – 30

• um invariante sobre o estado interno do recurso.

O pseudo-código a seguir define um monitor para operações sobre uma conta ban-cária:

monitor conta

{

float saldo = 0.0 ;

5 void depositar (float valor)

{

if (valor >= 0)

conta->saldo += valor ;

else

error ("erro: valor negativo\n") ;

}

void retirar (float saldo)

{

15 if (valor >= 0)

conta->saldo -= valor ;

else

error ("erro: valor negativo\n") ;

}

}

A definição formal de monitor prevê e existência de um invariante, ou seja, umacondição sobre asvariáveis internasdomonitor quedeve ser sempreverdadeira. No casoda conta bancária, esse invariante poderia ser o seguinte: “O saldo atual deve ser a somade todos os depósitos efetuados e todas as retiradas efetuadas (com sinal negativo)”. Entretanto,a maioria das implementações de monitor não suporta a definição de invariantes, comexceção da linguagem Eiffel.

De certa forma, um monitor pode ser visto como um objeto que encapsula o recursocompartilhado, com procedimentos (métodos) para acessá-lo. No entanto, a execuçãodos procedimentos é feita com exclusão mútua entre os procedimentos. As operaçõesde obtenção e liberação do semáforo são inseridas automaticamente pelo compilador doprograma em todos os pontos de entrada e saída do monitor, liberando o programadordessa tarefa e evitando erros. Monitores estão presentes em várias linguagens, comoAda, C#, Eiffel, Java e Modula-3. O código a seguir mostra um exemplo simplificado deuso de monitor em Java:

class Conta

{

private float saldo = 0;

public synchronized void retirar (float valor)

{

if (valor >= 0)

saldo += valor ;

c©Prof. Carlos Maziero Problemas clássicos de coordenação – 31

else

10 System.err.println("valor negativo");

}

public synchronized void retirar (float valor)

{

if (valor >= 0)

saldo -= valor ;

else

System.err.println("valor negativo");

}

20 }

A cláusula synchronized faz com que um semáforo seja associado aos métodosindicados, para cada objeto. Assim, apenas um depósito ou retirada de cada vez poderáser feito sobre cada objeto da classe Conta.

Variáveis de condição podem ser usadas no interior de monitores (na verdade, osdois conceitos nasceram juntos). Todavia, devido às restrições da semântica Mesa, umprocedimento que executa a operação notify em uma variável de condição deve concluire sair imediatamente do monitor, para garantir que o invariante associado ao estadointerno do monitor seja respeitado [Bir04].

4 Problemas clássicos de coordenação

Algumas situações de coordenação entre atividades ocorrem com muita freqüênciana programação de sistemas complexos. Os problemas clássicos de coordenação retratammuitas dessas situações e permitem compreender como podem ser implementadas suassoluções. Nesta seção serão estudados três problemas clássicos: o problema dos pro-dutores/consumidores, o problema dos leitores/escritores e o jantar dos filósofos. Diversosoutros problemas clássicos são freqüentemente descritos na literatura, como o problemados fumantes e o do barbeiro dorminhoco, entre outros [Ray86, BA90].

4.1 O problema dos produtores/consumidores

Este problema também é conhecido como o problema do buffer limitado, e consisteem coordenar o acesso de tarefas (processos ou threads) a um buffer compartilhadocom capacidade de armazenamento limitada a N itens (que podem ser inteiros, regis-tros, mensagens, etc). São considerados dois tipos de processos com comportamentossimétricos:

Produtor : periodicamente produz e deposita um item no buffer, caso o mesmo tenhauma vaga livre. Caso contrário, deve esperar até que surja uma vaga no buffer. Aodepositar um item, o produtor “consome” uma vaga livre.

c©Prof. Carlos Maziero O problema dos produtores/consumidores – 32

Consumidor : continuamente retira um item do buffer e o consome; caso o buffer estejavazio, aguarda que novos itens sejam depositados pelos produtores. Ao consumirum item, o consumidor “produz” uma vaga livre.

Deve-se observar que o acesso ao buffer é bloqueante, ou seja, cada processo ficabloqueado até conseguir fazer seu acesso, seja para produzir ou para consumir umitem. A figura 16 ilustra esse problema, envolvendo vários produtores e consumidoresacessando um buffer com capacidade para 12 entradas. É interessante observar a fortesimilaridade dessa figura com a figura 8; na prática, a implementação de mailboxes e depipes é geralmente feita usando um esquema de sincronização produtor/consumidor.

Figura 16: O problema dos produtores/consumidores.

A solução do problema dos produtores/consumidores envolve três aspectos de coor-denação distintos:

• A exclusão mútua no acesso ao buffer, para evitar condições de disputa entreprodutores e/ou consumidores que poderiam corromper seu conteúdo.

• O bloqueio dos produtores no caso do buffer estar cheio: os produtores devemaguardar até surjam vagas livres no buffer.

• O bloqueio dos consumidores no caso do buffer estar vazio: os consumidoresdevem aguardar até surjam novos itens a consumir no buffer.

A solução para esse problema exige três semáforos, um para atender cada aspectode coordenação acima descrito. O código a seguir ilustra de forma simplificada umasolução para esse problema, considerando um buffer com capacidade para N itens,inicialmente vazio:

sem_t mutex ; // controla o acesso ao buffer (inicia em 1)

sem_t item ; // número de itens no buffer (inicia em 0)

sem_t vaga ; // número de vagas livres no buffer (inicia em N)

c©Prof. Carlos Maziero O problema dos leitores/escritores – 33

produtor () {

while (1) {

... // produz um item

sem_down(&vaga) ; // aguarda uma vaga no buffer

sem_down(&mutex) ; // aguarda acesso exclusivo ao buffer

10 ... // deposita o item no buffer

sem_up(&mutex) ; // libera o acesso ao buffer

sem_up(&item) ; // indica a presença de um novo item no buffer

}

}

consumidor () {

while (1) {

sem_down(&item) ; // aguarda um novo item no buffer

sem_down(&mutex) ; // aguarda acesso exclusivo ao buffer

20 ... // retira o item do buffer

sem_up(&mutex) ; // libera o acesso ao buffer

sem_up(&vaga) ; // indica a liberação de uma vaga no buffer

... // consome o item retirado do buffer

}

}

É importante observar que essa solução é genérica, pois não depende do tamanhodo buffer, do número de produtores ou do número de consumidores.

4.2 O problema dos leitores/escritores

Outra situação que ocorre com freqüência em sistemas concorrentes é o problemados leitores/escritores. Neste caso, um conjunto de processos ou threads acessam deforma concorrente uma área de memória comum (compartilhada), na qual podem fazerleituras ou escritas de valores. As leituras podem ser feitas simultaneamente, pois nãointerferem umas com as outras, mas as escritas têm de ser feitas com acesso exclusivoà área compartilhada, para evitar condições de disputa. No exemplo da figura 17, osleitores e escritores acessam de forma concorrente uma matriz de inteirosM.

Uma solução trivial para esse problema consistiria em proteger o acesso à áreacompartilhada com um semáforo inicializado em 1; assim, somente um processo porvez poderia acessar a área, garantindo a integridade de todas as operações. O código aseguir ilustra essa abordagem simplista:

sem_t mutex_area ; // controla o acesso à área (inicia em 1)

leitor () {

while (1) {

5 sem_down (&mutex_area) ; // requer acesso exclusivo à área

... // lê dados da área compartilhada

sem_up (&mutex_area) ; // libera o acesso à área

...

}

}

c©Prof. Carlos Maziero O problema dos leitores/escritores – 34

Figura 17: O problema dos leitores/escritores.

escritor () {

while (1) {

sem_down (&mutex_area) ; // requer acesso exclusivo à área

15 ... // escreve dados na área compartilhada

sem_up (&mutex_area) ; // libera o acesso à área

...

}

}

Todavia, essa solução deixa a desejar em termos de desempenho, porque restringedesnecessariamente o acesso dos leitores à área compartilhada: como a operação deleitura não altera os valores armazenados, não haveria problema em permitir o acessosimultâneo de vários leitores à área compartilhada, desde que as escritas continuemsendo feitas de forma exclusiva. Uma nova solução para o problema, considerando apossibilidade de acesso simultâneo pelos leitores, seria a seguinte:

1 sem_t mutex_area ; // controla o acesso à área (inicia em 1)

int conta_leitores = 0 ; // número de leitores acessando a área

sem_t mutex_conta ; // controla o acesso ao contador (inicia em 1)

leitor () {

while (1) {

sem_down (&mutex_conta) ; // requer acesso exclusivo ao contador

conta_leitores++ ; // incrementa contador de leitores

if (conta_leitores == 1) // sou o primeiro leitor a entrar?

sem_down (&mutex_area) ; // requer acesso à área

11 sem_up (&mutex_conta) ; // libera o contador

... // lê dados da área compartilhada

sem_down (&mutex_conta) ; // requer acesso exclusivo ao contador

conta_leitores-- ; // decrementa contador de leitores

c©Prof. Carlos Maziero O jantar dos filósofos – 35

if (conta_leitores == 0) // sou o último leitor a sair?

sem_up (&mutex_area) ; // libera o acesso à área

sem_up (&mutex_conta) ; // libera o contador

...

21 }

}

escritor () {

while (1) {

sem_down(&mutex_area) ; // requer acesso exclusivo à área

... // escreve dados na área compartilhada

sem_up(&mutex_area) ; // libera o acesso à área

...

}

31 }

Essa soluçãomelhora o desempenho das operações de leitura, mas introduz umnovoproblema: a priorização dos leitores. De fato, sempre que algum leitor estiver acessandoa área compartilhada, outros leitores também podem acessá-la, enquanto eventuaisescritores têm de esperar até a área ficar livre (sem leitores). Caso exista muita atividadede leitura, os escritores podem ficar impedidos de acessar a área, pois ela nunca ficarávazia. Soluções com priorização para os escritores e soluções eqüitativas entre ambospodem ser facilmente encontradas na literatura [Ray86, BA90].

O relacionamento de sincronização leitor/escritor é encontrado com muita freqüênciaemaplicações commúltiplas threads. Opadrão POSIXdefinemecanismos para a criaçãoe uso de travas com essa funcionalidade (com priorização de escritores), acessíveisatravés de chamadas como pthread_rwlock_init, entre outras.

4.3 O jantar dos filósofos

Um dos problemas clássicos de coordenação mais conhecidos é o jantar dos filósofos,que foi inicialmente proposto por Dijkstra [Ray86, BA90]. Neste problema, um grupode cinco filósofos chineses alterna suas vidas entre meditar e comer. Na mesa há umlugar fixo para cada filósofo, com um prato, cinco palitos (chopsticks ou hashi) comparti-lhados e um grande prato de comida ao meio (na versão inicial de Dijkstra, os filósofoscompartilhavam garfos e comiam spaguetti). A figura 18 ilustra essa situação.

Para comer, um filósofo fi precisa pegar os dois palitos ao seu lado (pi e pi+1). Porisso, filósofos vizinhos nunca podem comer juntos. As soluções mais simples para esseproblema podem incorrer em impasses, que serão estudados na seção 5. O problema dojantar dos filósofos é representativo de uma grande classe de problemas de sincronizaçãoentre vários processos e vários recursos sem usar um coordenador central.

c©Prof. Carlos Maziero Impasses – 36

Figura 18: O jantar dos filósofos chineses.

5 Impasses

O controle de concorrência entre tarefas acessando recursos compartilhados implicaem suspender algumas tarefas enquanto outras acessam os recursos, de forma a garantira consistência dos mesmos. Para isso, a cada recurso é associado um semáforo ou outromecanismo equivalente. Assim, as tarefas solicitam e aguardam a liberação de cadasemáforo para poder acessar o recurso correspondente.

Em alguns casos, o uso de semáforos pode levar a situações de impasse (ou deadlock),nas quais todas as tarefas envolvidas ficam bloqueadas aguardando aguardando a libe-ração de semáforos, e nada mais acontece. Para ilustrar uma situação de impasse, seráutilizado o exemplo de acesso a uma conta bancária apresentado na seção 3.1. O códigoa seguir implementa uma operação de transferência de fundos entre duas contas bancá-rias. A cada conta está associado um semáforo, usado para prover acesso exclusivo aosdados da conta e assim evitar condições de disputa:

typedef struct conta_t

{

int saldo ; // saldo atual da conta

sem_t lock ; // semáforo associado à conta

... // outras informações da conta

} conta_t ;

void transferir (conta_t* contaDeb, conta_t* contaCred, int valor)

9 {

sem_down (contaDeb->lock) ; // obtém acesso a contaDeb

sem_down (contaCred->lock) ; // obtém acesso a contCred

if (contaDeb->saldo >= valor)

{

contaDeb->saldo -= valor ; // debita valor de contaDeb

contaCred->saldo += valor ; // credita valor em contaCred

c©Prof. Carlos Maziero Caracterização de impasses – 37

}

sem_up (contaDeb->lock) ; // libera acesso a contaDeb

19 sem_up (contaCred->lock) ; // libera acesso a contaCred

}

Caso dois clientes do banco (representados por duas tarefas t1 e t2) resolvam fazersimultaneamente operações de transferência entre suas contas (t1 transfere um valor v1de c1 para c2 e t2 transfere um valor v2 de c2 para c1), poderá ocorrer uma situação deimpasse, como mostra o diagrama de tempo da figura 19.

Figura 19: Impasse entre duas transferências.

Nessa situação, a tarefa t1 detémo semáforo de c1 e solicita o semáforo de c2, enquantot2 detém o semáforo de c2 e solicita o semáforo de c1. Como nenhuma das duas tarefaspoderá prosseguir sem obter o semáforo desejado, nem poderá liberar o semáforo desua conta antes de obter o outro semáforo e realizar a transferência, se estabelece umimpasse (ou deadlock).

Impasses são situações muito freqüentes em programas concorrentes, mas tambémpodem ocorrer em sistemas distribuídos. Antes de conhecer as técnicas de tratamentode impasses, é importante compreender suas principais causas e saber caracterizá-losadequadamente, o que será estudado nas próximas seções.

5.1 Caracterização de impasses

Emum impasse, duas oumais tarefas se encontrambloqueadas, aguardando eventosque dependem somente delas, como a liberação de semáforos. Em outras palavras, nãoexiste influência de entidades externas em uma situação de impasse. Além disso, comoas tarefas envolvidas detêm alguns recursos compartilhados (representados por semáfo-ros), outras tarefas que vierem a requisitá-los também ficarão bloqueadas, aumentandogradativamente o impasse, o que pode levar o sistema inteiro a parar de funcionar.

c©Prof. Carlos Maziero Grafos de alocação de recursos – 38

Formalmente, um conjunto de N tarefas se encontra em um impasse se cada umadas tarefas aguarda um evento que somente outra tarefa do conjunto poderá produzir.Quatro condições fundamentais são necessárias para que os impasses possam ocorrer[CES71, BA90]:

C1 – Exclusão mútua : o acesso aos recursos deve ser feito de forma mutuamente ex-clusiva, controlada por semáforos ou mecanismos equivalentes. No exemplo daconta corrente, apenas uma tarefa por vez pode acessar cada conta.

C2 – Posse e espera : uma tarefa pode solicitar o acesso a outros recursos sem ter deliberar os recursos que já detém. No exemplo da conta corrente, cada tarefa detémosemáforo de uma conta e solicita o semáforo da outra conta para poder prosseguir.

C3 – Não-preempção : uma tarefa somente libera os recursos que detém quando assimo decidir, e não pode perdê-los contra a sua vontade (ou seja, o sistema operacionalnão retira os recursos já alocados às tarefas). No exemplo da conta corrente, cadatarefa detém indefinidamente os semáforos que já obteve.

C4 – Espera circular : existe um ciclo de esperas pela liberação de recursos entre as ta-refas envolvidas: a tarefa t1 aguarda um recurso retido pela tarefa t2 (formalmente,t1 → t2), que aguarda um recurso retido pela tarefa t3, e assim por diante, sendoque a tarefa tn aguarda um recurso retido por t1. Essa dependência circular podeser expressa formalmente da seguinte forma: t1 → t2 → t3 → · · · → tn → t1. Noexemplo da conta corrente, pode-se observar claramente que t1 → t2 → t1.

Deve-se observar que essas quatro condições são necessárias para a formação deimpasses; se uma delas não for verificada, não existirão impasses no sistema. Poroutro lado, não são condições suficientes para a existência de impasses, ou seja, averificação dessas quatro condições não garante a presença de um impasse no sistema.Essas condições somente são suficientes se existir apenas uma instância de cada tipo derecurso, como será discutido na próxima seção.

5.2 Grafos de alocação de recursos

É possível representar graficamente a alocação de recursos entre as tarefas de umsistema concorrente. A representação gráfica provê uma visão mais clara da distribui-ção dos recursos e permite detectar visualmente a presença de esperas circulares quepodem caracterizar impasses. Em um grafo de alocação de recursos [Hol72], as tarefas sãorepresentadas por círculos (©) e os recursos por retângulos (�). A posse de um recursopor uma tarefa é representada como � → ©, enquanto a requisição de um recurso poruma tarefa é indicada por©→ �.

A figura 20 apresenta o grafo de alocação de recursos da situação de impasse ocorridana transferência de valores entre contas bancárias da figura 19. Nessa figura percebe-seclaramente a dependência cíclica entre tarefas e recursos t1 → c2 → t2 → c1 → c1, que

c©Prof. Carlos Maziero Grafos de alocação de recursos – 39

neste caso evidencia um impasse. Como há um só recurso de cada tipo (apenas umaconta c1 e uma conta c2), as quatro condições necessárias se mostram também suficientespara caracterizar um impasse.

Figura 20: Grafo de alocação de recursos com impasse.

Alguns recursos lógicos ou físicos de um sistema computacional podem termúltiplasinstâncias: por exemplo, um sistema pode ter duas impressoras idênticas instaladas, oque constituiria um recurso (impressora) com duas instâncias equivalentes, que podemser alocadas de forma independente. No grafo de alocação de recursos, a existênciade múltiplas instâncias de um recurso é representada através de “fichas” dentro dosretângulos. Por exemplo, as duas instâncias de impressora seriam indicadas no grafocomo • • . A figura 21 indica apresenta um grafo de alocação de recursos considerandoalguns recursos com múltiplas instâncias.

Figura 21: Grafo de alocação com múltiplas instâncias de recursos.

É importante observar que a ocorrência de ciclos em um grafo de alocação, envol-vendo recursos com múltiplas instâncias, pode indicar a presença de um impasse, masnão garante sua existência. Por exemplo, o ciclo t1 → r1 → t2 → r2 → t3 → r3 → t1 pre-sente no diagrama da figura 21 não representa um impasse, porque a qualquermomento

c©Prof. Carlos Maziero Técnicas de tratamento de impasses – 40

a tarefa t4 pode liberar uma instância do recurso r2, solicitado por t2, desfazendo assimo ciclo. Um algoritmo de detecção de impasses envolvendo recursos com múltiplasinstâncias é apresentado em [Tan03].

5.3 Técnicas de tratamento de impasses

Como os impasses paralisam tarefas que detêm recursos, sua ocorrência pode in-correr em conseqüências graves, como a paralisação gradativa de todas as tarefas quedependam dos recursos envolvidos, o que pode levar à paralisação de todo o sistema.Devido a esse risco, diversas técnicas de tratamento de impasses foram propostas. Essastécnicas podemdefinir regras estruturais que previnam impasses, podematuar de formapró-ativa, se antecipando aos impasses e impedindo sua ocorrência, ou podem agir deforma reativa, detectando os impasses que se formam no sistema e tomando medidaspara resolvê-los.

Embora o risco de impasses seja umaquestão importante, os sistemas operacionais demercado (Windows, Linux, Solaris, etc) adotam a solução mais simples: ignorar o risco,na maioria das situações. Devido ao custo computacional necessário ao tratamento deimpasses e à sua forte dependência da lógica das aplicações envolvidas, os projetistasde sistemas operacionais normalmente preferem deixar a gestão de impasses por contados desenvolvedores de aplicações.

As principais técnicas usadas para tratar impasses em um sistema concorrente são:prevenir impasses através, de regras rígidas para a programação dos sistemas, impedirimpasses, por meio do acompanhamento contínuo da alocação dos recursos às tarefas,e detectar e resolver impasses. Essas técnicas serão detalhadas nas próximas seções.

5.3.1 Prevenção de impasses

As técnicas de prevenção de impasses buscam garantir que impasses nunca possamocorrer no sistema. Para alcançar esse objetivo, a estrutura do sistema e a lógica dasaplicações devem ser construídas de forma que as quatro condições fundamentais para aocorrência de impasses, apresentadas na seção 5.1, nunca sejam satisfeitas. Se ao menosuma das quatro condições for quebrada por essas regras estruturais, os impasses nãopoderão ocorrer. A seguir, cada uma das condições necessárias é analisada de acordocom essa premissa:

Exclusão mútua : se não houver exclusão mútua no acesso a recursos, não poderãoocorrer impasses. Mas, como garantir a integridade de recursos compartilhadossem usar mecanismos de exclusão mútua? Uma solução interessante é usada nagerência de impressoras: umprocesso servidor de impressão (printer spooler) gerenciaa impressora e atende as solicitações dos demais processos. Com isso, os processosque desejam usar a impressora não precisam obter acesso exclusivo a esse recurso.A técnica de spooling previne impasses envolvendo as impressoras, mas não é

c©Prof. Carlos Maziero Técnicas de tratamento de impasses – 41

facilmente aplicável a outros tipos de recurso, como arquivos em disco e áreas dememória compartilhada.

Posse e espera : caso as tarefas usem apenas um recurso de cada vez, solicitando-o e liberando-o logo após o uso, impasses não poderão ocorrer. No exemploda transferência de fundos da figura 19, seria possível separar a operação detransferência em duas operações isoladas: débito em c1 e crédito em c2 (ou vice-versa), sem a necessidade de acesso exclusivo simultâneo às duas contas. Comisso, a condição de posse e espera seria quebrada e o impasse evitado.

Outra possibilidade seria somente permitir a execução de tarefas que detenhamtodos os recursos necessários antes de iniciar. Todavia, essa abordagem poderialevar as tarefas a reter os recursos pormuitomais tempo que o necessário para suasoperações, degradando o desempenho do sistema. Uma terceira possibilidadeseria associar um prazo (time-out) às solicitações de recursos: ao solicitar umrecurso, a tarefa define um tempomáximo de espera por ele; caso o prazo expire, atarefa pode tentar novamente ou desistir, liberando os demais recursos que detém.

Não-preempção : normalmente uma tarefa obtém e libera os recursos de que necessita,de acordo com sua lógica interna. Se for possível “arrancar” um recurso da tarefa,semque esta o libere explicitamente, e devolvê-lomais tarde, impasses envolvendoaquele recurso não poderão ocorrer. Essa técnica é freqüentemente usada emrecursos cujo estado internopode ser salvo e restauradode forma transparente paraa tarefa, como páginas de memória e o contexto do processador. No entanto, é dedifícil aplicação sobre recursos como arquivos ou áreas dememória compartilhada,porqueapreempçãoviola a exclusãomútua epodedeixar inconsistênciasnoestadointerno do recurso.

Espera circular : um impasse é uma cadeia de dependências entre tarefas e recursosque forma um ciclo. Ao prevenir a formação de tais ciclos, impasses não poderãoocorrer. A estratégia mais simples para prevenir a formação de ciclos é ordenartodos os recursos do sistema de acordo com uma ordem global única, e forçar astarefas a solicitar os recursos obedecendo a essa ordem. No exemplo da transfe-rência de fundos da figura 19, o número de conta bancária poderia definir umaordem global. Assim, todas as tarefas deveriam solicitar primeiro o acesso à contamais antiga e depois à mais recente (ou vice-versa, mas sempre na mesma ordempara todas as tarefas). Com isso, elimina-se a possibilidade de impasses.

5.3.2 Impedimento de impasses

Uma outra forma de tratar os impasses preventivamente consiste em acompanhara alocação dos recursos às tarefas e, de acordo com algum algoritmo, negar acessosde recursos que possam levar a impasses. As técnicas de impedimento de impassesnecessitam de algum conhecimento prévio sobre o comportamento das tarefas para

c©Prof. Carlos Maziero Técnicas de tratamento de impasses – 42

poderem operar. Normalmente é necessário conhecer com antecedência que recursosserão acessados por cada tarefa, quantas instâncias de cada um serão necessárias e quala ordem de acesso aos recursos. Por essa razão, são pouco utilizadas na prática.

Uma noção essencial nas técnicas de impedimento de impasses é o conceito de estadoseguro. Cada estado do sistema é definido pela distribuição dos recursos entre as tarefas.O conjunto de todos os estados possíveis do sistema forma um grafo de estados, no qualas aresta indicam as alocações e liberações de recursos. Um determinado estado éconsiderado seguro se, a partir dele, é possível concluir as tarefas pendentes. Caso oestado em questão somente leve a impasses, ele é considerado inseguro.

A figura 22 ilustra o conceito de estados seguros e inseguros, através do grafo deestados das tarefas t1 e t2 de transferência de valores4 da situação apresentada na figura19. Nela, pode-se observar claramente que o estado [t1 ← c1; t2 ← c2] é inseguro,pois a partir dele o sistema somente pode evoluir para um impasse. Um algoritmo deimpedimento de impasses deveria negar as alocações em tracejado (ei //___ e j ), pois elaslevam o sistema a esse estado inseguro. Os demais estados são considerados seguros,pois permitem a evolução do sistema.

c1 livrec2 livre

t1→c1t1←c1

zzuuuuuuuuut2→c2t2←c2

%%JJJJJJJJJJ

t1 ← c1t1→c2t1←c2

yyrrrrrrrrrrr t2→c2t2←c2

$$II

II

It2 ← c2t1→c1

t1←c1

yytt

tt

t t2→c2t2←c2

&&MMMMMMMMMMM

t1 ← c1t1 ← c2

t1 transfere

��

t1 ← c1t2 ← c2

t1→c2t2→c1

��

estadoinseguro

oot2 ← c1t2 ← c2

t2 transfere

��

transferênciaconcluída

libera c1 e c2

**

t1 ← c1t1 → c2t2 ← c2t2 → c1

impasseoo transferênciaconcluída

libera c1 e c2uu

Figura 22: Grafo de estados com um estado inseguro.

A técnica de impedimento de impasses mais conhecida é o algoritmo do banqueiro,criado por Dijkstra em 1965 [Tan03]. Esse algoritmo faz uma analogia entre as tarefas deum sistema e os clientes de um banco, tratando os recursos como créditos emprestados

4Este grafo de estados é simplificado; o grafo completo, detalhando cada solicitação, alocação eliberação de recursos, tem dezenas de estados possíveis.

c©Prof. Carlos Maziero Técnicas de tratamento de impasses – 43

às tarefas para a realização de suas atividades. O banqueiro decide que solicitações deempréstimo deve atender para conservar suas finanças em um estado seguro.

5.3.3 Detecção e resolução de impasses

Nesta abordagem, nenhuma medida preventiva é adotada para prevenir ou evitarimpasses. As tarefas executam normalmente suas atividades, alocando e liberandorecursos conforme suas necessidades. Quando ocorrer um impasse, o sistema o detecta,determina quais as tarefas e recursos envolvidos e toma medidas para desfazê-lo. Paraaplicar essa técnica, duas questões importantes devem ser respondidas: como detectaros impasses? E como resolvê-los?

A detecção de impasses pode ser feita através da inspeção do grafo de alocação derecursos (seção 5.2), que deve ser mantido pelo sistema e atualizado a cada alocação ouliberação de recurso. Um algoritmo de detecção de ciclos no grafo deve ser executadoperiodicamente, para verificar a presença das dependências cíclicas que podem indicarimpasses.

Alguns problemas decorrentes dessa estratégia são o custo de manutenção contínuado grafo de alocação e, sobretudo, o custo de sua análise: algoritmos de busca de ciclosemgrafos têmcusto computacional elevado, portanto sua ativação commuita freqüênciapoderá prejudicar o desempenho do sistema. Por outro lado, se a detecção for ativadaapenas esporadicamente, impasses podem demorar muito para ser detectados, o quetambém é ruim para o desempenho.

Uma vez detectado um impasse e identificadas as tarefas e recursos envolvidos, osistema deve proceder à sua resolução, que pode ser feita de duas formas:

Eliminar tarefas : umaoumais tarefas envolvidas no impasse são eliminadas, liberandoseus recursos para que as demais tarefas possam prosseguir. A escolha das tarefasa eliminar deve levar em conta vários fatores, como o tempo de vida de cada uma,a quantidade de recursos que cada tarefa detém, o prejuízo para os usuários, etc.

Retroceder tarefas : uma ou mais tarefas envolvidas no impasse têm sua execuçãoparcialmente desfeita, de forma a fazer o sistema retornar a um estado seguroanterior ao impasse. Para retroceder a execução de uma tarefa, é necessário salvarperiodicamente seu estado, de forma a poder recuperar umestado anterior quandonecessário5. Além disso, operações envolvendo a rede ou interações com o usuáriopodem ser muito difíceis ou mesmo impossíveis de retroceder: como desfazer oenvio de um pacote de rede, ou a reprodução de um arquivo de áudio?

A detecção e resolução de impasses é uma abordagem interessante, mas relativa-mente pouco usada fora de situações muito específicas, porque o custo de detecçãopode ser elevado e as alternativas de resolução sempre implicam perder tarefas ou partedas execuções já realizadas.

5Essa técnica é conhecida como checkpointing e os estados anteriores salvos são denominados check-points.

c©Prof. Carlos Maziero Técnicas de tratamento de impasses – 44

Questões

1. Quais são as vantagens e desvantagens das abordagens a seguir, sob as óticas dosistema operacional e do programador de aplicativos?

(a) comunicação bloqueante ou não-bloqueante

(b) canais com buffering ou sem buffering

(c) comunicação por mensagens ou por fluxo

(d) mensagens de tamanho fixo ou variável

(e) comunicação 1:1 ou M:N

2. Explique como processos que comunicam por troca de mensagens se comportamem relação à capacidade do canal de comunicação, considerando as semânticas dechamada síncrona e assíncrona.

3. Explique o que é espera ocupada e por que os mecanismos que empregam essatécnica são considerados ineficientes.

4. Em que circunstâncias o uso de espera ocupada é inevitável?

5. Explique o que são condições de disputa, mostrando um exemplo real.

6. Mostre como pode ocorrer violação da condição de exclusão mútua em um semá-foro se as operações down e up não forem implementadas de forma atômica.

7. Em que situações um semáforo deve ser inicializado em 0, 1 ou n > 1?

8. Por que não existem operações read(s) e write(s) para ler ou ajustar o valor correntede um semáforo?

9. Explique cadaumadasquatro condições necessárias para a ocorrênciade impasses.

10. Na prevenção de impasses, como pode ser feita a quebra da condição de posse eespera?

11. Na prevenção de impasses, como pode ser feita a quebra da condição de exclusãomútua?

12. Na prevenção de impasses, como pode ser feita a quebra da condição de esperacircular?

13. Na prevenção de impasses, como pode ser feita a quebra da condição de não-preempção?

14. Uma vez detectado um impasse, quais as abordagens possíveis para resolvê-lo?Explique-as e comente sua viabilidade.

c©Prof. Carlos Maziero Técnicas de tratamento de impasses – 45

15. Como pode ser detectada a ocorrência de impasses, considerando disponível ape-nas um recurso de cada tipo?

Exercícios

1. Mostre como pode ocorrer violação da condição de exclusãomútua se as operaçõesdown(s) e up(s) não forem implementadas de forma atômica.

2. Escreva, em pseudo-código, as operações down(s) e up(s) sobre semáforos, usandoinstruções TSL para controlar o acesso à estrutura de dados que define cada semá-foro, que é indicada a seguir. Não é necessário detalhar as operações de ponteirosenvolvendo a fila queue.

struct semaphore

{

int lock = false ;

int count ;

task_t *queue ;

}

3. Escreva, em pseudo-código, um sistema produtor/consumidor com dois bufferslimitados, ou seja, X→ b1 → Y e Y→ b2 → Z, ondeX, Y e Z são tipos de processose b1 e b2 são buffers com capacidade n1 e n2, respectivamente, inicialmente vazios.

4. Nos grafos de alocação de recursos da figura 23, indique o(s) ciclo(s) onde existeum impasse:

Figura 23: Grafos de alocação de recursos.

5. A figura 24 representa uma situação de impasse em um cruzamento de trânsitourbano. Mostre que as quatro condições necessárias para a ocorrência de impasses

c©Prof. Carlos Maziero Técnicas de tratamento de impasses – 46

estão presentes, e aponte uma regra simples para evitar esta situação, explicandosuas razões.

Figura 24: Impasse em um cruzamento de trânsito.

6. O trecho de código a seguir apresenta uma solução para o problema do jantar dosfilósofos. Explique o código e indique se ele funciona, justificando sua resposta.Que mudança simples pode ser feita no código para melhorá-lo ou corrigi-lo?

#define N 5

sem_t garfo[5] ; // 5 semáforos iniciados em 1

4

int esq (int k)

{

return k ;

}

int dir (int k)

{

return = (k + 1) % N // (k+1) módulo N

}

14

void filosofo (int i)

{

while (1)

{

medita (i);

sem_down (garfo [esq(i)]) ;

sem_down (garfo [dir(i)]) ;

come (i);

c©Prof. Carlos Maziero REFERÊNCIAS – 47

sem_up (garfo [dir(i)]) ;

24 sem_up (garfo [esq(i)]) ;

}

}

Projetos

1. Implemente uma solução em C para o problema do produtor/consumidor, usandothreads e semáforos no padrão POSIX.

2. Implemente uma solução em C para o problema do produtor/consumidor, usandothreads e variáveis de condição no padrão POSIX.

3. Implemente uma solução em C para o problema dos leitores/escritores com prio-rização para escritores, usando threads e semáforos POSIX.

4. Implemente uma solução em C para o problema dos leitores/escritores com prio-rização para escritores, usando threads e rwlocks POSIX.

5. ...

Referências

[BA90] Mordechai Ben-Ari. Principles of Concurrent and Distributed Programming.Prentice-Hall, 1990.

[Bar05] Blaise Barney. POSIX threads programming.http://www.llnl.gov/computing/tutorials/pthreads, 2005.

[Bir04] A. Birrell. Implementing condition variables with semaphores. ComputerSystems Theory, Technology, and Applications, pages 29–37, December 2004.

[CES71] E. Coffman, M. Elphick, and A. Shoshani. System deadlocks. ACM ComputingSurveys, 3(2):67–78, 1971.

[Gal94] Bill Gallmeister. POSIX.4: Programming for the Real World. O’Reilly, 1994.

[Gno05] Gnome. Gnome: the free software desktop project. http://www.gnome.org,2005.

[Har04] Johnson Hart. Windows System Programming, 3rd edition. Addison-Wesley Pro-fessional, 2004.

[Hol72] R. Holt. Some deadlock properties of computer systems. ACM ComputingSurveys, 4(3):179–196, september 1972.

c©Prof. Carlos Maziero REFERÊNCIAS – 48

[KDE05] KDE. KDE desktop project. http://www.kde.org, 2005.

[Lam74] Leslie Lamport. A new solution of Dijkstra’s concurrent programming pro-blem. Communications of the ACM, 17(8):453–455, August 1974.

[LR80] B. Lampson and D. Redell. Experience with processes and monitors in Mesa.Communications of the ACM, February 1980.

[Pet98] Charles Petzold. Programming Windows, 5th edition. Microsoft Press, 1998.

[Ray86] Michel Raynal. Algorithms for Mutual Exclusion. The MIT Press, 1986.

[RR03] Kay Robbins and Steven Robbins. UNIX Systems Programming. Prentice-Hall,2003.

[Ste98] Richard Stevens. UNIX Network Programming. Prentice-Hall, 1998.

[Tan03] Andrew Tanenbaum. Sistemas Operacionais Modernos, 2a edição. Pearson –Prentice-Hall, 2003.