- O que são Processos?
Uma abstração de um programa em execução.
Mantêm a capacidade de operações (pseudo)concorrentes, mesmo quando há apenas uma CPU disponível.
Transformam uma única CPU em várias CPUs virtuais.
Possuem programa, entrada, saída e um estado.
Um único processador pode ser compartilhado entre os vários processos, com algum algoritmo de escalonamento usado para determinar quando parar o trabalho sobre um processo e servir outro.
- Criação de Processos
Em sistemas muito simples, ou em sistemas projetados para executar apenas uma única aplicação, pode ser possível que todos os processos que serão necessários seja criados quando o sistema é ligado.
Em sistemas de propósito geral, é necessário algum mecanismo para criar e terminar processos durante a operação, quando for preciso.
Há quatro eventos principais que fazem com que processos sejam criados: Iníco do sistema
Execução de uma chamada de sistema de criação de processo por um processo em execução
Uma requisição do usuário para criar um novo processo
Início de uma tarefa em lote (batch lote)
- Estados de Processo
Embora cada processo seja uma entidade independente, com seu próprio contador de programa e estado interno, muitas vezes os processos precisam interagir com outros.
Um processo pode gerar uma saída que outro processo usa como entrada.
Ex.:
cat chapter1 chapter2 chapter3 | grep tree
O primeiro processo, que executa cat, gera como saída a concatenação dos três arquivos.
O segundo processo, que executa grep, seleciona todas as linhas contendo a palavra “tree”.
- Estados de Processo
Em execução
ProntoBloqueado
1 23
41. O processo bloqueia aguardando uma entrada2. O escalonador seleciona outro processo3. O escalonador esse processo4. A entrada torna-se disponível
Estados:
Em execução → realmente usando a CPU naquele momento.
Pronto → executável; temporariamente parado para dar lugar a outro processo.
Bloqueado → incapaz de executar enquanto não ocorrer um evento externo.
- Comunicação entre Processos
Frequentemente processos precisam se comunicar com outros (IPC – interprocess communication).
Há três tópicos em relação a comunicação de processos:
Como um processo passa informação para outro;
Como garantir que dois ou mais processos não entrem em conflito (concorrência pelo mesmo recurso);
Sequência adequada quando existirem dependências (produtor/consumidor).
- Comunicação entre Processos
Condições de Corrida
Dois ou mais processos estão lendo ou escrevendo algum dado compartilhado e cujo resultado depende de quem executa precisamente e quando a executa.
Ex.: spool de impressão
abc
prog.c
prog.h
Diretório de spool...
.
.
.
out = 4
in = 7
4567
Processo A
Processo B
Dois processos querem acessar a memória compartilhada ao mesmo tempo
- Comunicação entre Processos
Regiões Críticas
O que fazer para evitar condições de disputa?
Exclusão Mútua → modo de assegurar que outros processos sejam impedidos de usar uma variável ou um arquivo compartilhado que já estiver em uso por um processo.
Parte do programa que acessa um recurso compartilhado é chamada de região crítica.
- Comunicação entre Processos
Regiões Críticas
É preciso satisfazer quatro condições para chegar a uma boa solução que impeça condições de disputa:
Dois processos nunca podem estar simultaneamente em suas regiões críticas;
Nada pode ser afirmado sobre a velocidade ou sobre o número de CPUs;
Nenhum processo executando fora de sua região crítica pode bloquear outros processos;
Nenhum processo deve esperar eternamente para entrar em sua região crítica.
- Comunicação entre Processos
Regiões Críticas
Processo A
Processo B
T1 T2 T3 T4
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
B deixa a região crítica
B bloqueado
TempoExclusão mútua usando regiões críticas
- Comunicação entre Processos
Exclusão Mútua Desabilitando interrupções
Em sistemas de processador único, a solução mais simples é aquela em que cada processo desabilita todas as interrupções logo depois de entrar em sua região crítica e as reabilita imediatamente antes de sair dela.
Com as interrupções desligadas, a CPU não será mais chaveada para outro processo. Desta forma um processo pode verificar e atualizar a memória compartilhada sem temer a intervenção de um outro processo.
De modo geral, esta abordagem não é interessante, porque não é prudente dar aos processos dos usuários o poder de desligar interrupções. Pois o usuário pode desabilitar as interrupções e não mais habilitar. Além disso, se o sistema for multicore, desabilitar as interrupções afetará somente a CPU que executou a instrução disable. As outras continuarão executando e tendo acesso à memória compartilhada.
- Comunicação entre Processos
Exclusão Mútua Variáveis do tipo trava (lock)
Outra alternativa com solução através de software.
Existência de uma única variável compartilhada para controle da região crítica. Inicialmente o valor da variável é zero.
Para entrar em sua região crítica, o processo testa antes se o valor da variável é zero. Em caso verdadeiro, o processo altera essa variável para um e entra na região crítica. Se a variável já estiver com o valor um, o processo simplesmente aguardará até que ela se torne zero.
Desvantagem: quando um processo lê o conteúdo da variável com valor zero e antes que ele modifique para um, outro processo é escalonado e altera o valor para um. Ao voltar a executar, o primeiro processo também coloca o valor um na variável e, assim, os dois processos estarão em suas regiões críticas ao mesmo tempo.
- Comunicação entre Processos
Exclusão Mútua Chaveamento obrigatório
while (TRUE) {
while (turn != 0);
turn = 1;
critical_region();
turn = 0;
noncritical_region();
}
(a)
while (TRUE) {
while (turn != 0);
turn = 1;
critical_region();
turn = 0;
noncritical_region();
}
(b)
Solução proposta para o problema da região crítica. (a) Processo 0. (b) Processo 1
- Comunicação entre Processos
Exclusão Mútua Solução de Peterson
Consiste em duas rotinas (enter_region e leave_region).
Antes de usar as variáveis compartilhadas (ou seja, antes de entrar em sua região crítica), cada processo chama enter_region com seu próprio número de processo, 0 ou 1, como parâmetro.
Essa chamada fará com que ele fique esperando, se necessário, até que seja seguro entrar.
Depois que terminou de usar as variáveis compartilhadas, o processo chama leave_region para indicar seu término e permitir que outro processo entre, se assim desejar.
- Comunicação entre Processos
Exclusão Mútua Solução de Peterson
#define FALSE 0
#define TRUE 1
#define N 2 /*número de processos*/
int turn; /*de quem é a vez?*/
int interested[N]; /*todos os valores 0 ou 1*/
void enter_region(int process); { /*processo é 0 ou 1*/
int other; /*número do outro processo*/
other = 1 – process; /*o oposto do processo*/
interested[process] = TRUE; /*configura qual processo está interessado*/
turn = process; /*altera o valor de turn*/
while (turn == process && interested[other] == TRUE) /*comando nulo*/;
}
void leave_region(int process) { /*processo: quem está saindo*/
interested[process] = FALSE; /*indica a saída da região crítica*/
}
- Comunicação entre Processos
Exclusão Mútua Instrução TSL
Requer um pequeno auxílio do hardware.
Muitos computadores – especialmente aqueles projetados com múltiplos processadores – têm uma instrução TSL RX, LOCK (test and set lock – teste e atualize a variável de trava).
As operações de leitura e armazenamento da palavra são seguramente indivisíveis – nenhum outro processador pode ter acesso à palavra na memória enquanto a instrução não terminar.
A CPU que está executando a instrução TSL impede o acesso ao barramento de memória para proibir que outras CPUs tenham acesso à memória enquanto ela não terminar.
- Comunicação entre Processos
Dormir e Acordar A solução de Peterson e a solução com base em TSL são corretas,
mas ambas apresentam o defeito de precisar da espera ociosa.
Em essência, o que essas soluções fazem é: quando quer entrar em uma região crítica, um processo verifica se sua entrada é permitida. Se não for, ele ficará em um laço esperando até que seja permitida a entrada.
Problema da inversão de prioridade: Dois processos → H, com alta prioridade, e L, com baixa prioridade.
As regras de escalonamento são tais que H é executado sempre que estiver no estado de pronto.
Em certo momento, com L em sua região crítica, H torna-se pronto pra executar.
Agora H inicia uma espera ocupada, mas, como L nunca é escalonado enquanto H está executando, L nunca tem a oportunidade de deixar sua região crítica e, assim, H fica em um laço infinito.
- Comunicação entre Processos
Dormir e Acordar Problema do produtor-consumidor:
Dois processos compartilham um buffer comum e de tamanho fixo.
Um deles, o produtor, coloca informação dentro do buffer e o outro, o consumidor, a retira.
O problema se origina quando o produtor quer colocar um novo item no buffer, mas ele está cheio. A solução é colocar o produtor para dormir e só despertá-lo quando o consumidor remover um ou mais itens.
Da mesma maneira, se o consumidor quiser remover um item do buffer e perceber que está vazio, ele dormirá até que o produtor coloque algo no buffer e o desperte.
O código do consumidor é similar.
- Comunicação entre Processos
Dormir e Acordar Problema do produtor-consumidor:
#define N 100 //número de lugares no bufferint count = 0; //número de itens no buffervoid producer(void) { int item; while(TRUE) { //repita para sempre item = produce_item(); //gera o próximo item if (count==N) sleep(); //se o buffer estiver cheio, vá dormir inset_item(item); //ponha um item no buffer count++; //incremente o contador de itens no buffer if (count==1) wakeup(consumer); //o buffer está vazio? }} void consumer(void) {
int item; while(TRUE) { //repita para sempre if (count==0) sleep(); //se o buffer estiver vazio, vá dormir item = remove_item(); //retire o item do buffer count--; //decresça de um o contador de itens no buffer if (count==N-1) wakeup(producer); //o buffer estava cheio? consume_item(item); //imprima o item }}
- Comunicação entre Processos
Dormir e Acordar Problema do produtor-consumidor:
Qual o problema que pode acontecer?
- Comunicação entre Processos
Dormir e Acordar Problema do produtor-consumidor:
Caso aconteça condição de disputa seria possível acontecer a seguinte situação: o buffer está vazio e o consumidor acabou de ler a variável count para verificar se seu valor é 0. Nesse instante, o escalonador decide parar de executar o consumidor temporariamente e começa a executar o produtor. O produtor insere um item no buffer, incrementa a variável count e percebe que seu valor agora é 1. Inferindo que o valor de count era 0 e que o consumidor deveria ir dormir, o produtor chama wakeup para acordar o consumidor.
Infelizmente, o consumidor ainda não está logicamente adormecido; então, o sinal de acordar é perdido. Na próxima vez em que o consumidor executar, testará o valor de count anteriormente lido por ele, verificará que o valor é 0 e dormirá. Mais cedo ou mais tarde o produtor preencherá todo o buffer e também dormirá. Ambos dormirão para sempre.
- Comunicação entre Processos
Semáforos Criado pelo matemático holandês E. W. Dijkstra em1965
É um tipo abstrato de dado composto por um valor inteiro e uma fila de processos.
Permite somente duas operações: P (down) e V (up).
Quando um processo executa a operação P sobre um semáforo, o seu valor é decrementado. Caso o novo valor seja negativo, o processo é bloqueado e inserido no fim da fila desse semáforo.
Quando um processo executa a operação V sobre um semáforo, o seu valor é incrementado. Caso exista processo bloqueado na fila desse semáforo, o primeiro processo da fila é liberado.
- Comunicação entre Processos
SemáforosP(S):
S.valor = S.valor – 1;
se S.valor < 0
Então bloqueia o processo, insere em S.fila;
V(S):
S.valor = S.valor + 1;
se S.valor <= 0
Então retira processo P de S.fila, acorda P
- Comunicação entre Processos
Semáforos Para que semáforos funcionem corretamente, é essencial que as
operações P e V seja atômicas.
Semáforos tornam a proteção da seção crítica muito simples.
Para cada estrutura de dados compartilhada, deve ser criado um semáforo S inicializado com valor 1.
Todo processo, antes de acessar a estrutura, deve executar um P(S), ou seja, a operação P sobre o semáforo S associado com a estrutura de dados em questão.
Ao sair da seção crítica, o processo executa V(S).
- Comunicação entre Processos
Troca de Mensagens Processos executando em diferentes máquinas não possuem variáveis
compartilhadas.
Eles trocam informações através de mensagens via uma rede de comunicação.
Tipicamente, o mecanismo que permite a troca de mensagens entre processos é implementado pelo sistema operacional.
Ele é acessado através de duas chamadas de sistema básica: Send e Receive.
send(destino,&mensagem);
receive(origem,&mensagem);
- Comunicação entre Processos
Troca de Mensagens Produtor-consumidor com troca de mensagem (caixa postal)
Local para armazenar temporariamente um certo número de mensagens.
Quando as caixas postais são usadas, os parâmetros de endereço nas chamadas send e receive são as caixas postais, não os processos.
Ao tentar enviar para uma caixa postal que esteja cheia, um processo é suspenso até que uma mensagem seja removida daquela caixa postal e dê lugar a uma nova.
- O que são Threads?
Também chamados de processos leves, permitem múltiplas atividades dentro de um único processo;
Fluxo de controle sequencial isolado dentro de um programa;
Como um programa sequencial qualquer, uma thread tem um começo, uma sequencia de comandos e um fim;
Threads permitem que um programa simples possa executar várias tarefas diferentes ao mesmo tempo, independentemente umas das outras.
- Criando Threads em Java
Em Java, as threads se constituem de instâncias da classe Thread que fornecem suporte a comunicação concorrente;
A classe Thread provê métodos necessários para criar e controlar threads (independentemente da plataforma usada) e executá-los concorrentemente;
O corpo de uma thread é o seu método run(), e é nele que são executadas as tarefas às quais thread se destina.
- Duas formas de criar Threads
Criando uma subclasse da classe Thread e definindo o seu método run() de maneira adequada à realização da tarefa do thread;
Criando uma instância de Thread que recebe como parâmetro um objeto que implemente a interface Runnable - esse objeto providenciará o método run() para a thread.
- Usando Herança
Usando herança, a classe deve sobrescrever o método public void run():
class Tarefa1 extends Thread {
public void run() {
for(int i=0; i<1000; i++)
System.out.println(“Usando herança”);
}
}
- Usando a interface Runnable
A interface Runnable obriga a implementar o método public void run():
class Tarefa2 implements Runnable {
public void run() {
for(int i=0; i<1000; i++)
System.out.println(“Usando Runnable”);
}
}
- Exemplo de uso
Para usar as classes Tarefa1 e Tarefa2 devemos fazer:
Thread threadComHeranca = new Tarefa1();
Thread threadComRunnable = new Thread(new Tarefa2());
threadComHeranca.start();
threadComRunnable.start();
- Saída do exemplo
A saída do exemplo seria mais ou menos essa:
Usando Runnable
Usando Herança
Usando Herança
Usando Runnable
Usando Herança
Usando Runnable
(...)