Sistemas Operacionais
Aula Passada
• Continuação do Estudo de Escalonamento • Escalonamento Multilevel Queue
• Escalonamento Multilevel Feedback Queue • Escalonamento em Multiprocessadores
• Escalonamento em Tempo Real
Processos Cooperativos
• São processos que compartilham um espaço de
endereço lógico (código e dados) diretamente ou têm permissão para compartilhar dados por meio de arquivos ou mensagens.
Produtor / Consumidor
Produtor Consumidor
Tanto o produtor quanto o consumidor compartilham uma variável em comum: count
Produtor / Consumidor
• Considere a seguinte situação:++count registrador1 = count; registrador1 = registrador1 + 1; count = registrador1; --count registrador2 = count; registrador2 = registrador2 + 1; count = registrador2;
S0: produtor executa registrador1 = count
S1: produtor executa registrador1 = registrador1 + 1 S2: consumidor executa registrador2 = count
S3: consumidor executa registrador2 = registrador2 - 1 S4: produtor executa count = registrador1
S5: consumidor executa count = registrador2
Qual o valor de count ao final da execução? 4
Pool de Impressão
0 1 2 3 4 5 6 A B abc prog.c prog.n out = 0 in = 3 O processo A lê in e armazena 3 Ocorre uma interrupçãoA CPU chaveia para o processo B O processo B lê in e armazena 3 O processo B continua a execução
Armazena o nome do arquivo na posição 3 O processo B atualiza in para 4
O processo A volta a ser executado Verifica a variável local e grava em 3
O processo A atualiza in para 4
3 3
arquivoB.doc
in = 4
Problemas
• Observe que em tais exemplos que são executados de
forma assíncrona não é possível garantir o valor correto dos dados compartilhados.
• Nós temos várias threads acessando e manipulando
dados ao mesmo tempo.
• O resultado depende da ordem de execução das
threads.
• Quando isso acontece chamamos de condição de
Sincronismo
• As condições de corrida são muito frequentes em
um sistema operacional e devem ser evitadas.
• Devemos garantir que apenas uma thread acesse
a variável count, por exemplo, por vez.
Seção Crítica
• Primeira técnica para tratar do problema de
sincronismo
• Consistem em identificar um trecho de código
como uma seção crítica
• Cada thread possui seu própria seção crítica • Uma vez que uma thread está executando sua
seção crítica nenhuma outra thread tem permissão de executar sua própria seção crítica
Seção Crítica
• Os algoritmos devem ter os seguintes requisitos: • Exclusão Mútua: se um processo está na seção
crítica, nenhum outro processo pode entrar nela
• Progresso: se nenhum processo está na seção
crítica, nenhum processo que queira entrar em sua seção crítica não pode esperar indefinitivamente.
• Espera limitada: se um processo deseja entrar na
seção crítica, existe limite em termos de números de processos que pode fazer isto antes dele.
Seção Crítica
entering critical sectionleaving critical section
critical section
non critical section
Essa parte do código só pode ser executada apenas por uma thread
Seção Crítica
Processo A
Processo B
A entra na região crítica A deixa a região crítica
B tenta entrar na região crítica
B entra na
região crítica região críticaB deixa a
Seção Crítica - Algoritmo 1
• Considerando 2 threads:• As threads compartilham uma variável turn,
inicializadas com 0 ou 1.
• Se turn == i, então a Thread i tem permissão
Seção Crítica - Algoritmo 1
Implementação do Algoritmo 1
Implementação da Thread
Seção Crítica - Algoritmo 1
• O problema dessa solução é que ela garante a
exclusão mútua mas não garante o progresso
• Nessa solução a thread não guarda informação
suficiente sobre o estado de cada thread. Ela sabe apenas qual thread deve entrar na seção crítica.
Seção Crítica - Algoritmo 2
• Para solucionar o problema anterior, vamosconsiderar duas variáveis booleanas: flag0 e flag1
• Cada variável é inicializara com o valor false. Se
uma das variáveis for true, isso significa que a
thread em questão está pronta para entrar em sua seção crítica.
Seção Crítica - Algoritmo 2
Seção Crítica - Algoritmo 2
• Essa abordagem apesar de resolver o problema
citado no Algoritmo 1 ainda não garante a condição de progresso.
• Imagine que T0 define flag0 como true
• Antes de executar o loop while, a thread T1 define a
flag1 como true!
• O que acontece?
Seção Crítica - Algoritmo 3
• Para evitar tais problemas e garantir os requisitosapresentados anteriormente, a solução é combinar o Algoritmo 1 e 2 em uma só solução
• A threads irão compartilhar as 3 variáveis: • boolean flag0;
• boolean flag1 • int turn;
Seção Crítica - Algoritmo 3
Seção Crítica - Algoritmo 3
• Essa solução atende os três requisitos ditosanteriormente: exclusão mútua, progresso e espera limitada.
• Qual o problema das abordagens apresentadas
até aqui?
Semáforo
• Mecanismo de sincronização que não requer espera ocupada • Proposto por Dijkstra
• Semáforo é uma variável inteira (S) que é acessada apenas por
duas operações:
• acquire( ), P( ), wait( ), up( );
• release( ), V ( ), signal( ), down( );
• Essas operações devem ser atômicas: é garantido que, uma
vez iniciada a operação, nenhum outro processo tem acesso ao semáforo.
Semáforo
Semáforo
• Podemos implementar uma semáforo de duas formas: • Semáforo binário: só pode variar entre 0 e 1
• conhecido como mutex
• Semáforo contador: pode variar em N valores
inteiros positivos
• A implementação de semáforo aqui apresentada
ainda apresenta o problema de espera ocupada. Como resolver?
Semáforo
• Para resolver tal problema, devemos modificar nossa
implementação de acquire() e release()
• Devemos implementar tais funções utilizando o bloqueio de
processos e o wakeup ao invés de um loop.
• De forma geral, temos:
acquire(S) { value--;
if(value < 0) {
acrescenta esse processo a lista block; } } release(S) { value++; if(value <= 0) {
remove o processo P da lista wakeup(P)
} }
Semáforo
• Um aspecto crítico do semáforo é que as funções de
acquire() e release() devem ser executadas atomicamente.
• Podemos fazer isso de duas formas:
• Em um ambiente monoprocessado, podemos
desabilitar interrupções
• Podemos aplicar uma solução de seção crítica, onde
Semáforos
Deadlocks e Starvation
• Deadlocks: dois ou mais processos ficam
esperando um recurso que só pode ser fornecido por um processo que está bloqueado.
• Starvation: bloqueio indefinido. Um processo pode
nunca ser removido da fila de semáforos na qual está bloqueado.
Semáforo
Deadlocks e Starvation
P0 P1 acquire(S) acquire(Q) acquire(Q) acquire(S) . . . . . . release(S) release(Q) release(Q) release(S)Problemas Clássicos de
Sincronismo
• O problema do bounded buffer
• O problema dos leitores-escritores • O problema do jantar dos filósofos • O problema do barbeiro dorminhoco
Bounded Buffer
produtor
consumidor
Como resolver esse problema com semáforos? Buffer
Bounded Buffer
• Solução com três semáforos• mutex: semáforo binário
• empty e full: semáforos contador • mutex: permite a exclusão mútua
• empty e full: contam o número de buffers vazios e
Leitores e Escritores
Banco de Dados
Leitores Escritores Qual o problema envolvendo
Leitores e Escritores
• Qual o problema de sincronismo nesse caso?• Se dois leitores acessarem concorrentemente na
há problema de sincronismo
• No entanto, se um leitor e um escritor acessar o
banco ao mesmo tempo, devemos tratar o sincronismo
• Os escritores devem ter acesso exclusivo ao
Leitores e Escritores
• Podemos tratar esse problema de duas maneiras:
• Nenhum leitor seja mantido esperando, a menos que um
escritor já tenha obtido permissão para usar o banco de dados compartilhado
• Quando um escritor estiver pronto, ele realiza sua escrita o
mais breve possível.
• Ambos tem problema de starvation: • primeiro caso: com os escritores • segundo caso: com os leitores
Jantar dos filósofos
• Problema:
• O filósofo ou pensa ou come
• Enquanto ele está pensando, ele não interage com os demais
colegas
• Quando um filósofo ele sente fome, ele pega o hashi da
direita, pega o hashi da esquerda e come sem largar os hashi
• Ao terminar coloca os hashi de volta na mesa e recomeça a
pensar
Jantar dos filósofos
• Cada hashi é um semáforo• Tenta pegar um hashi: acquire() • Solta o hashi: release()
• Qual o problema dessa abordagem? • Possibilidade de deadlock
Atividade
• Implemente o problema dos filósofos propondo uma
solução para o deadlock.
• Entrega: 20/12/12 às 23:59:59 via SIGAA ou via
Monitores
• Tanto a seção crítica quando os semáforos são
técnicas que estão sujeitas a erro de programação
• Esses erros podem afetar o funcionamento da
sincronização, impedindo que a exclusão mútua seja garantida
• Os monitores foram propostos com a finalidade de
evitar tais problemas
Monitores
• Criado por Dijkstra (1971) e desenvolvidos por Hoare e Hansen
(1974/75).
• É um tipo de dado abstrato que encapsula variáveis e métodos
que são compartilhado entre vários processos
• Um tipo monitor apresenta um conjunto de operações definidas
pelo programador, que recebem exclusão mútua dentro do monitor.
• Um procedimento dentro do monitor só pode acessar variáveis
dentro do monitor e os parâmetros passados.
• Da mesma forma, variáveis locais do monitor só pode ser
Monitores
• A própria implementação do monitor proíbe o
acesso simultâneo a procedimento definidos dentro do monitor.
• Somente um processo pode estar ativo dentro do
monitor
• O programador não precisa explicitar esse
sincronismo, ele já está embutido no próprio monitor
• O código da seção crítica não precisa ser mais
Monitores
• Quando um processo chama um procedimento em um
monitor, o primeiro passo é verificar se existe algum outro processo ativo no monitor:
• Sim: o processo atual é suspenso
• Não: o processo atual pode executar sua seção crítica • A sincronização é feita por meio de operações de WAIT e
SIGNAL
• Um processo (ou thread) que chama x.wait é suspenso até que outro processo chame x.signal
Implementando
yield()
• Quando uma thread chama o método yield ela continua no modo executável,
mas permite que a JVM selecione outra thread executável para ser executada.
• É mais eficiente que a espera ocupada, mas pode acontecer o que
synchronized()
• Cada objeto java possui o que chamamos de lock • Normalmente, esse lock é ignorado a não ser que
especifique o método como sincronizado
• Exemplo:
public synchronized void insert(Object item) { …
synchronized()
• Podemos definir os métodos insert e
remove do produtor-consumidor com o synchronized.
• Essa solução evita que duas threads
modifiquem o valor da variável count
• Se uma thread estiver executando um
método synchronized, ela obtém a posse do lock.
• Apenas a thread que tem posse do
lock pode executar sua seção crítica
• Outra thread não pode ter acesso ao
lock, logo fica aguardando até
executar o seu método synchronized
synchronized()
• Apesar desse método garantir que apenas uma thread executará a seção
crítica, o synchronized não soluciona o problema de deadlock
• Um deadlock pode acontecer quando o buffer está cheio, o consumidor esteja
dormindo e o produtor deseja inserir no buffer:
• Produtor entra no método insert (o lock está disponível) , verifica que o
buffer está cheio e chama o método yield
• O produtor fica em um estado de espera, mas ainda possui o lock do objeto • Quando o consumidor acordar será impossibilitado de continuar, já que:
• O produtor está bloqueado esperando que o consumidor libere espaço
no buffer
• O consumidor está bloqueado esperando que o produtor libere o lock Qual a solução?
wait() e notify()
• Cada objeto java possui também um conjunto de
espera (set wait) que juntamente com o lock permite o sincronismo
• Quando não é possível continuar a execução da
thread por conta de alguma condição, essa thread libera o lock e é colocada neste conjunto de
espera, evitando o deadlock.
• Esse controle é feito utilizando os métodos wait() e
wait() e notify()
• Wait:!
• A thread libera o lock do objeto
• O estado da thread é definido como bloqueado
• A thread é colocada no conjunto de espera do objeto
• Notify:!
• Apanha uma thread T qualquer na lista de threads do conjunto de
espera
• Move T do conjunto de espera para o conjunto de entrada • Define o estado de T de bloquado para executável
notifyAll()
• Qual o problema com a chamada do método
notify?
• Ela funciona bem quando você tem apenas um
thread no seu conjunto de espera.
• Imagine o seguinte cenário:
• Várias threads no conjunto de espera com
diferentes condições para retomarem a execução
notifyAll()
• Considere 5 threads (T1, T2, T3, T4, T5) e uma
variável compartilhada turn
• turn indica qual tread deve ser executada
• Quando uma thread deseja realizar um trabalho ela
notifyAll()
• Essa abordagem pode causar
deadlock
• Em um determinado estado
todas as threads podem está no wait()
• O problema é que a chamada
notify seleciona
arbitrariamente alguma thread do conjunto de espera.
• O programado não tem
controle sobre qual thread será escolhida
notifyAll()
• O uso da chamada notifyall() no lugar da notify()
soluciona tal problema
• A chamada notifyall() permite notificar todas as
threads do conjunto de espera
• Ou seja, o método notifyall permite despertar todas
as threads que estão em espera e permite que elas decidam quem vai executar.
synchronized()
(em bloco)
• Java permite utilizar o synchronized em um bloco de
código
• Útil para métodos que possuem apenas uma pequena