Вы находитесь на странице: 1из 5

Sistemas Operacionais – Cap.

5 (Sincronização de Processos)

Um processo cooperativo pode afetar outros processos em execução no sistema


ou ser por eles afetado. Eles também podem compartilhar diretamente um espaço de
endereçamento lógico ou podem ser autorizados a compartilhar dados somente por
meio de arquivos ou mensagens.
Os processos podem ser executados concorrentemente ou em paralelo. O
acesso concorrente a dados compartilhados pode resultar em inconsistência de dados.
É chamado de condição de corrida uma situação em que vários processos
acessam e manipulam os mesmos dados concorrentemente e o resultado da execução
depende da ordem específica em que o acesso ocorre. No entanto, o ideal é que apenas
um processo de cada vez possa manipular uma variável. Logo, para isso, é necessário
que os processos sejam sincronizados de alguma forma.

5.2 O Problema da Seção Crítica

É chamado de seção crítica o segmento de código que um processo possui,


onde o processo pode estar alterando variáveis comuns, atualizando uma tabela,
gravando um arquivo, entre outros. Dois processos não podem executar suas seções
críticas ao mesmo tempo. Cada processo deve solicitar permissão para entrar em sua
seção crítica. A seção de entrada é a seção de código que implementa essa solicitação.
A seção crítica pode ser seguida por uma seção de saída. O código restante é chamado
de seção remanescente.
Uma solução para o problema da seção crítica deve satisfazer aos três requisitos
a seguir:

1- Exclusão mútua: Apenas um processo pode executar a sua seção crítica de cada
vez.
2- Progresso: quando não há processos executando suas seções críicas, apenas
aqueles processos que não estão executando suas seções remanescentes podem
participar da decisão de qual entrará em sua seção crítica a seguir, essa seleção não
pode ser adiada indefinidamente.
3- Espera limitada: Há um limite, ou fronteira, para quantas vezes outros processos
podem entrar em suas seções críticas após um processo ter feito uma solicitação para
entrar em sua seção crítica e antes de essa solicitação ser atendida. Duas abordagens
gerais são usadas para manipular seções críticas em sistemas operacionais:
 kernels com preempção: permite que um processo seja interceptado enquanto
está sendo executado em modalidade de kernel.
 kernels sem preempção: não permite interceptação em um processo que esteja
sendo executado em modalidade de kernel.

Um processo em modalidade de kernel será executado até sair dessa modalidade,


ser bloqueado ou abandonar voluntariamente o controle da CPU.
O kernel com preempção pode apresentar melhor capacidade de resposta, pois há
menos risco de um processo em modalidade kernel ser executado por um período de
tempo arbitrariamente longo antes de liberar o processador para os processos em
espera.
5.4 Hardware de Sincronização

Todas as soluções apresentadas a seguir são baseadas na premissa do


trancamento (locking) - que é a proteção de regiões críticas através do uso de locks. Em
um ambiente com um único processador, o problema da seção crítica poderia ser
resolvido facilmente, se pudéssemos impedir que interrupções ocorressem enquanto
uma variável compartilhada estivesse sendo modificada. Teríamos a certeza de que a
sequência de instruções corrente seria autorizada a executar em ordem e sem
preempção. Essa solução não é viável em um ambiente multiprocessador, pois a
desabilitação de interrupções em um multiprocessador pode ser demorada, já que a
mensagem é passada a todos os processadores.
Outra forma de resolver o problema seria com o uso de instruções de hardware
especiais, fornecidas por sistemas de computação modernos, essas instruções nos
permitem testar e modificar o conteúdo de uma palavra ou permutar os conteúdos de
duas palavras atomicamente - isto é, como uma unidade impossível de interromper.
A característica importante da instrução test_and_set()
é que ela é executada atomicamente. Portanto, se duas instruções test_and_set ()
forem executadas simultaneamente (cada uma em uma CPU diferente), elas serão
executadas sequencialmente em alguma ordem arbitrária. Se a máquina der suporte à
Instrução test_and_set(), poderemos implementar a exclusão mútua declarando uma
variável boliana lock, inicializada como false.

5.5 Locks Mutex

As soluções baseadas em hardware para o problema da seção crítica, vistas na


seção anterior, são complicadas, como também geralmente inacessíveis aos
programadores de aplicações.
Lock mutex é uma ferramenta de software simples, construída para resolver o
problema da seção crítica. Usamos locks mutex para proteger regiões críticas e, assim,
evitar condições de corrida.
Um lock mutex tem uma variável booliana available cujo valor indica se o lock
está ou não disponível. Se o lock está disponível, uma chamada a acquire() é bem uma
chamada acquire ( ) é bem-sucedida, e o lock é então considerado indisponível. Um
processo que tente adquirir um lock indisponível é bloqueado até que o lock seja
liberado.
A principal desvantagem da implementação do locks mutex é que ela requer a
espera em ação. Enquanto um processo está em sua seção crítica, qualquer outro
processo que tente entrar em sua seção crítica deve entrar em loop contínuo na
chamada a acquire(). Esse tipo de lock mutex também é denominado um spinlock
porque o processo "gira" (spin) enquanto espera que o lock se torne disponível. Esse
looping contínuo é claramente um problema em um sistema de multiprogramação real,
em que uma única CPU é compartilhada entre muitos processos. A espera em ação
desperdiça ciclos da CPU que algum outro processo poderia usar produtivamente.
A vantagem que os spinlocks apresentam é que nenhuma mudança de contexto
é requerida quando um processo tem de esperar em um lock, e uma mudança de
contexto pode levar um tempo considerável. Assim, os spinlocks são úteis quando o uso
de locks é esperado por períodos curtos. Eles costumam ser empregados em sistemas
multiprocessadores em que um thread pode "desenvolver um spin" em um processador
enquanto outro thread executa sua seção crítica em outro processador.

5.6 Semáforos

Um semáforo é uma ferramenta mais robusta que pode se comportar de modo


semelhante a um lock mutex, mas também pode fornecer maneiras mais sofisticadas
para que os processos sincronizem suas atividades. Um semáforo S é uma variável
inteira que, exceto na inicialização, é acessada apenas por meio de duas operações
atômicas padrão:

 wait() - era originalmente denominada P (testar);


 signal() - era originalmente denominada V (incrementar).

Todas as modificações do valor inteiro do semáforo nas operações wait() e signal()


devem ser executadas indivisivelmente.

5.6.1 Uso dos Semáforos

Semáforo de contagem - seu valor pode variar dentro de um domínio irrestrito.


Podem ser usados para controlar o acesso a determinado recurso comporto por um
número finito de instâncias. Também pode ser usado para resolver vários problemas de
sincronização.

5.6.2 Implementação do Semáforo

As definições das operações de semáforo wait() e signal() também sofre da


espera em ação. Para eliminar esse problema podemos modificar a definição das
operações wait() e signal(). Quando um processo executa a operação wait(), mas o valor
do semáforo é negativo, ele deve esperar. O processo pode bloquear a si mesmo ao
em vez de entrar na espera em ação. A operação de bloqueio insere o processo em
uma fila de espera associada ao semáforo
e o estado do processo é comutado para o estado de espera. Em
seguida, o controle é transferido ao scheduler da CPU, que seleciona outro processo p
ara execução.
Semáforo binário - seu valor pode variar somente entre 0 e 1.

5.6.3 Deadlocks e Inanição

A implementação de um semáforo com fila de espera pode resultar em uma


situação em que dois ou mais processos fiquem esperando indefinidamente por um
evento que pode ser causado somente por um dos processos em espera. O evento em
questão é a execução de uma operação signal(). Quando tal estado é alcançado,
dizemos que esses processos estão em deadlock. Quando cada processo de um
conjunto está esperando por um evento que só pode ser causado por outro processo do
conjunto, dizemos que um conjunto de processos está em estado de deadlock. O
bloqueio indefinido ou inanição é um outro problema relacionado com os deadlocks, é
uma situação em que os processos esperam indefinidamente dentro do semáforo. O
bloquei indefinido pode ocorrer se removermos processos da lista associada a um
semáforo, em ordem LIFO.

5.6.4 Inversão de Prioridades

Ocorre apenas em sistemas com mais de duas prioridades. Logo, para


solucioná-lo é necessário o uso de apenas duas prioridades. No entanto, para a maioria
dos sistemas operacionais de uso geral isso é insuficiente. Normalmente esses sistemas
resolvem o problema implementando um protocolo de herança de prioridades. De
acordo com esse protocolo, todos os processos que estão acessando recursos
requeridos por um processo de prioridade mais alta herdam a prioridade mais alta até
terem terminado de usar os recursos em questão. Quando terminam de usá-los, suas
prioridades retornam a seus valores originais.

5.7 Problemas Clássicos de Sincronização

Vários problemas de sincronização serão apresentados a seguir como exemplos


de uma classe ampla de problemas de controle de concorrência. Para as soluções dos
problemas usaremos semáforos para sincronização. No entanto, implementações reais
dessas soluções poderiam usar locks mutex em vez de semáforos binários.

5.7.1 O Problema do Buffer Limitado

Este problema é usado para ilustrar o poder dos primitivos de sincronização. A


inversão de prioridades pode ser mais do que uma inconveniência do scheduling. Em
sistemas com rígidas restrições de tempo a inversão de prioridades pode fazer com que
um processo demore mais do que deveria para executar uma tarefa. Quando isso
acontece outras falhas podem ser geradas em cascata, resultando em falha do sistema.

5.7.2 O Problema dos Leitores-Gravadores

Quando vários processos concorrentes compartilham um mesmo banco de


dados, alguns desses processos podem querer apenas ler o banco de dados (leitores),
enquanto outros podem querer atualizá-lo (gravadores). Se um gravador e algum outro
processo, seja leitor ou um gravador, acessarem o banco de dados simultaneamente,
isso pode resultar em caos. Já no caso de dois leitores acessarem os dados
compartilhados simultaneamente, não haverá efeitos adversos.
Os gravadores, ao gravarem no banco de dados compartilhados, devem ter
acesso exclusivo a ele, evitando esse problema de sincronização, que é chamado de
problema dos leitores-gravadores.
O problema dos leitores-gravadores tem diversas variações, todas envolvendo
prioridades. Vejamos algumas:
 Primeiro problema dos leitores-gravadores: nenhum leitor deve esperar que
outros leitores terminem, simplesmente porque um gravador está esperando.
Essa é a variação mais simples de todas.

 Segundo problema dos leitores-gravadores: se um gravador está esperando


para acessar o objeto, nenhum novo leitor pode começar a ler.
Uma solução para qualquer um dos dois problemas pode resultar em inanição.
No primeiro caso, os gravadores podem entrar em inanição; no segundo caso, os
leitores é que pode entrar em inanição.
Na solução para o primeiro problema dos leitores-gravadores, os processos
leitores compartilham as estruturas de dados a seguir:

semaphore rw_mutex = 1; //O semáforo rw_mutex funciona como um


semáforo de exclusão mútua para os gravadores. Ele também é usado pelo primeiro o
u último leitor que entra na seção crítica ou sai dela.

semaphore mutex = 1; //O semáforo mutex é usado para


assegurar a exclusão mútua quando a variável read_count é atualizada.
int read_count = 0; // A variável read_count
registra quantos processos estão lendo correntemente o objeto.

O problema dos leitores-gravadores e suas soluções têm sido generalizados pa


ra fornecer locks de leitores-gravadores em alguns sistemas. A aquisição de m lock de
leitor-gravador requer a especificação
da modalidade do lock: de acesso à leitura ou à gravação. Quando um processo desej
a somente ler dados
compartilhados, ele solicita o lock de leitor-gravador na modalidade de leitura. Um proc
esso que queira modificar os dados compartilhados deve solicitar o lock em modalidade
de gravação. Vários processos são autorizados a adquirir concorrentemente um lock de
leitor-gravador em modalidade de leitura, mas só um processo pode adquirir o lock para
gravação, já que o acesso exclusivo é requerido para gravadores.

Os locks de leitor-gravador são mais úteis nas situações a seguir:


Em aplicações em que é fácil identificar processos que apenas leem dados compartilh
ados e processos que apenas gravam dados compartilhados.
Em aplicações que têm mais leitores do que gravadores. Isso ocorre porque os locks
de leitor-gravador geralmente demandam mais overhead para serem estabelecidos do
que semáforos ou locks de exclusão mútua. O aumento da concorrência, por serem
permitidos vários leitores, compensa o overhead envolvido na especificação do lock de
leitor-gravador.

5.7.3 O Problema dos Filósofos Comensais

É considerado um problema clássico de sincronização, porque


é um exemplo de uma classe ampla de problemas de controle de concorrência. É uma
representação simples da necessidade de alocar vários recursos entre diversos
processos de uma forma livre de deadlocks e de inanição.

Вам также может понравиться