Capítulo 2
Processos e Threads
Processo –
entidade dinâmica que consiste num programa em
execução, os seus valores correntes, informação de estado e
recursos utilizados pelo sistema operativo na gestão da
execução do processo.
Um processo constitui uma actividade. Ele possui 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 um outro.
2.1.2 Criação de processos
Há quatro eventos principais que fazem com que processos sejam criados: 1. Inicio do sistema
2. Execução de uma chamada ao sistema de criação de processo por um processo em execução 3. Uma requisição do usuário para criar um novo processo
4. Inicio de um Job em lote.
Tecnicamente, em todos estes casos, um novo processo e criado por um processo existente executando uma chamada ao sistema de criação de processo.
2.1.2 Fim de processos
Depois de criado, um processo começa a executar e faz o seu trabalho. Mais cedo ou mais tarde o processo terminara, normalmente por uma das seguintes razoes:
Saída normal (voluntária) – Na maioria das vezes os processos terminam porque fizeram o seu trabalho. Outra saída normal e por exemplo quando o utilizador sai do programa como deve ser. (A cruz da janela por exemplo)
Saída por erro (voluntária) – Quando o processo descobre um erro fatal. O processo emite uma chamada de saída ao sistema.
Erro fatal (involuntário) – Quando o erro e causado pelo processo, muitas vezes um erro de programa. Exemplos: quando encontra uma divisão por zero, instrução ilegal, etc.
Cancelamento por um outro processo (involuntário) – Quando um processo executa uma chamada ao sistema dizendo para cancelar algum outro processo. Em linux: kill
2.1.2 Estado dos processos
1. Em execução (realmente utilizando o CPU nesse instante):
2. Pronto (executável; temporariamente parado para dar lugar a outro processo) 3. Bloqueado (incapaz de executar enquanto um evento externo não ocorrer) As transições possíveis entre os vários estados são:
Execução Bloqueado: quando um processo descobre que não pode prosseguir passa para o estado de bloqueado: Ex: cat teste | grep tree o processo do grep tem de esperar pela saída do processo cat. Se o
processo grep tem tempo de CPU e o processo cat ainda não terminou então o grep passa para o estado de bloqueado.
Execução Pronto e Pronto Execução: transições causadas pelo escalonador de processos, grerenciamento de tempo de CPU, Execução Pronto ocorre quando o escalonador de processos decide que o processo em execução já teve o seu tempo de CPU, e Pronto Execução ocorre quando já todos os processos estiveram em execução volta novamente para o mesmo.
Bloqueado Pronto: ocorre quando o processo que estava no estado bloqueado tem disponível aquilo que estava à espera. Se o CPU estiver livre esse processo passa logo para o estado de Execução, senão fica a aguardar até chegar a sua vez.
2.1.2 Threads
E o fluxo de controle de um processo. Permitem que múltiplas execuções ocorram no mesmo ambiente do processo com um grande grau de independência uma da outra.
Itens compartilhados por todos os threads de um processo: Espaço de endereçamento
Variáveis globais Ficheiros abertos Processos filhos Alarmes pendentes
Sinais e tratadores de sinais Informação de contabilidade Itens por thread:
Contador de programa Registradores
Pilha Estado
Cada thread tem a sua própria pilha. Isso e fácil e perceber porque normalmente cada thread chama procedimentosdiferentes… ou se chamam os mesmos o que se vai passar, e diferente para cada thread, então e necessário que cada thread tenha a sua pilha. A pilha tem uma estrutura aonde são guardadas as vaiáveis locais de um procedimento e o endereço de retorno para usa-lo quando o procedimento terminar
2.2.2 O uso de threads
A principal razão para existirem threads é que em muitas aplicações ocorrem múltiplas actividades ao mesmo tempo. Algumas dessas actividades podem bloquear de tempos em tempos. O modelo de programação torna-se mais simples se decompormos uma aplicação em varias threads sequenciais que executam quase em paralelo. O paralelismo das threads é o mesmo que existe para os processos mas neste caso as threads de um mesmo processo utilizam todas o mesmo espaço de endereçamento.
Uma segunda razão para o uso de threads tem a ver com o facto de serem mais fáceis de criar e de destruir do que os processos, pois não tem quaisquer recursos associados a eles. Em muitos sistemas criar threads e muito mais rápido que criar processos. Esta propriedade e útil quando o numero de threads necessários se altera dinâmica e rapidamente.
O desempenho também e uma razão para o uso e threads pois quando existe uma grande quantidade de computação e de E\S, os threads permitem que essas actividades se sobreponham e desse modo aceleram a aplicação.
2.2.3 Implementação de threads de usuário
Sucintamente: O usuário cria um processo e depois é o próprio processo que gerência as suas threads. O processo tem uma tabela de threads, gerida por um sistema supervisor, para manter o controle sobre elas, tabela essa que é em tudo parecida a tabela de processos do núcleo.
Vantagens: quando uma thread decide parar de executar, a informação da thread e guardada na tabela e threads e o escalonador de threads pode ser chamado pela paragem daquela thread e seleccionar outra thread para executar. A vantagem disto é que é mais eficiente, o escalonador de threads e o guardar a informação da thread na tabela assim do que fazer uma chamada ao núcleo.
Outra vantagem e cada processo pode ter o seu algoritmo de escalonamento personalizado.
2.2.4 Implementação de threads de núcleo
Contrariamente a implementação de threads de usuário, a implementação de threads de núcleo funciona com o núcleo a gerir as duas tabelas, quer a de processos bem como a das threads. Esta tabela das threads acompanha todas as threads do sistema, enquanto que na implementação de threads de usuário cada tabela de threads apenas gerência as threads correspondentes a um processo.
2.2.7 Threads pop-up
Thread criada por uma thread existente para tratar o trabalho.
2.3.1 Condições de disputa
Dois processos querem ter acesso simultaneamente à memória partilhada.
2.3.2 Regiões criticas
As regiões criticas são a resposta para evitar o problema das condições de disputa. Dando-lhe outro nome: exclusão-mutua é um modo de assegurar que outros processos sejam impedidos de usar uma variável ou arquivo que já esteja a ser utilizado por um outro processo. Região critica e aquela parte do programa em que existe acesso à memoria partilhada. Para chegar a uma boa solução de evitar as condições de disputa precisamos satisfazer quatro soluções.
1. Nunca dois processos podem estar simultaneamente nas suas regiões criticas 2. Nada pode ser afirmado sobreavelocidadeousobreonúmerodeCPU’s
3. Nenhum processo executando fora da sua região critica pode bloquear outros processos 4. Nenhum processo deve esperar eternamente para entrar na sua região critica
2.3.3 Exclusão Mútua Com Espera Ociosa
Varias soluções para o problema das regiões critica. Mas todas elas com o problema da espera ociosa. Quer isto dizer que quando um processo entra na sua região critica nenhum outro pode entrar nessa mesma região invadindo-a e causando danos.
A espera ociosa faz com que os processos que querem entrar na região estejam constantemente a perguntar se podem (averiguar uma variável por exemplo) e isto não e muito bom pois assim consome o tempo de CPU desnecessário
Esta solução para o problema das regiões criticas em vez dos processos que querem aceder a região critica ficarem em espera ociosa ficam bloqueados, fazendo com que estes não desperdicem tempo e CPU. Isto e feio com o par de chamadas ao sistema wakeup e sleep.
O problema produtor-consumidor: dois processos partilham um buffer comum e de tamanho fixo. O produtor põe informação dentro do buffer e o consumidor retira. O problema existe quando o produtor que colocar um item no buffer mas este já esta cheio. A solução e por o produtor a dormir e desperta-lo quando o consumidor consumir um ou mais itens. Ou então ao contrário, se o consumidor quiser consumir e o buffer estiver vazio, vai dormir ate o produtor produzir um ou mis itens.
Resumidamente o problema disto tudo e quando ocorre a situação em que o buffer esta vazio e o consumidor acabou de ler a variável count (que diz o numero de itens do buffer) para verificar se o seu valor e zero e nesse instante o escalonador de processos decide parar de executar o consumidor e começa a executar o produtor. O produtor insere um item no buffer e incrementa count, depois verifica que e 1 e então envia o sinal para despertar o consumidor. Infelizmente o consumidor não esta a dormir e o sinal perde-se. Na próxima vez que o consumidor executar, testa o valor de count anteriormente lido e verifica que e zero e dormira. O produtor mais cedo ou mais tarde preenchera todo o buffer e também dormira. E então ambos dormiram para sempre.
Uma solução para isto seria utilizar um bit de espera pelos sinais de acordar e dormir. Quando um sinal acordar é enviado a um processo que ainda esta acordado então esse bit e activado. Quando o processo tentar dormir, se o bit estiver activado então o bit e desactivado e o processo continuara acordado.
2.3.5 Semáforos
Uma variável inteira é utilizada para guardar o número de sinais de acordar salvos para uso futuro. Um semáforo pode conter o valor 0 – indicando que nenhum sinal de acordar foi salvo – ou algum valor positivo se um ou mais sinais de acordar estivessem pendentes.
As operações para trabalhar com os semáforos são o down e o up. A operação verifica se um semáforo é maior que zero. Se for então decrementa um valor ao semáforo (gasta um sinal de acordar armazenado). Se o valor for zero o processo será posto para dormir. Verificar o valor do semáforo, altera-lo e possivelmente i dormir são tarefas executadas todas como uma só. Uma vez iniciada uma operação ao semáforo mais nenhum outro processo pode ter acesso ao semáforo, para evitar as condições de disputa.
A operação up incrementa o valor de um dado semáforo.
2.3.6 Mutexes
O mutex serve para gerenciar a exclusão mútua de algum recurso ou parte de código partilhada. É tipo um semáforo, mas apenas utiliza, ou o valor 0 para dizer que esta livre e outro qualquer para dizer que esta ocupado.
Quando uma thread precisa de ter acesso a uma região critica chama mutex_lock. Se o mutex estiver livre a chamada continua e o processo (ou thread) que chamou mutex_lock ficara livre para entrar na região critica. Por outro lado se o mutex estiver ocupado, significa que um outro processo esta a utilizar aquela região critica e então o processo que chamou mutex_lock fica bloqueado ate que o outro processo acabe de utilizar a região critica e chame mutex_unlock. Se existirem múltiplas threads bloqueados á espera de entrar na região critica um deles será escolhido aleatoriamente.
A diferença entre os mutexes e a espera ociosa é que quando um processo chama mutex_lock e verifica que não pode entrar na região critica, fica bloqueado e chama thread_yield para que liberte o CPU para outra thread
2.3.7 Monitores
Ah pois é!
2.3.8 Troca de mensagens
Do inglês message passing. É um método e comunicação entre processos e utiliza duas primitivas, a send e a receive. A send envia uma mensagem para um dado destino e a receive recebe uma
mensagem de uma dada origem. Se nenhuma mensagem estiver disponível o receptor poderá ficar bloqueado até que alguma mensagem chegue. Ou então retornar imediatamente um código e erro.
2.3.9 Barreiras
Mecanismo dirigido a grupos de processos. Algumas aplicações são divididas em fases e tem como regra que nenhum processo pode avançar para a próxima fase ate que todos os processos estejam pontos a faze-lo.
2.5 Scheduler
Quando um computador é multiprogramado, ele muitas vezes tem variados processos que competem pela CPU o mesmo tempo. Esta situação ocorre sempre que dois processos estão no estado de pronto. Se apenas um CPU se encontra disponível, devera ser feita uma escolha de qual processo executara primeiro. A parte do sistema operacional que faz essa escolha é chamada de escalonador e o algoritmo que ele usa é o algoritmo de escalonamento.
2.5.1 Introdução ao Scheduler
Objectivos do escalonador: Todos os sistemas:
Justiça – dar a cada processo uma porção justa d CPU
Aplicação da política – verificar se a politica estabelecida é cumprida Equilíbrio – manter ocupadas todas as partes do sistema
Sistemas em lote
Vazão – maximizar o número de jobs por hora
Tempo de retorno – minimizar o tempo entre a submissão e o término Utilização de CPU – manter a CPU ocupada o tempo todo
Sistemas interactivos
Tempo de resposta – responder rapidamente as requisições Proporcionalidade – satisfazer as expectativas dos utilizadores Sistemas de tempo real
Cumprimentos dos prazos – evitar a perda de dados
Previsibilidade – evitar a degradação da qualidade em sistemas multimédia
2.5.2 Scheduler em sistemas bash
Primeiro a chegar primeiro a ser servido – FCFS – A CPU é atribuída aos processos na ordem que eles a requisitam
Job mais curto primeiro – o nome diz tudo. O escalonador escolhe o Job com o tempo de execução mais curto primeiro.
Próximo de menor tempo restante – o escalonador escolhe o processo com o menor tempo restante de execução
Escalonamentos em três níveis – A medida que chegam ao sistema, os jobs são inicialmente colocados numa fila de entrada armazenada no disco. O escalonador de admissão decide qual o Job que será admitido no sistema. Os outros são mantidos na fila e entrada ate que sejam seleccionados. Assim que um Job é admitido no sistema, um processo é criado para ele que passa então a competir pela CPU. Por vezes o número de processos é tão grande que pode não existir espaço suficiente na memória. Nesse caso alguns processos devem ser levados para o disco. O segundo nível de escalonamento, o escalonador de memoria é que decide quais os processos que ficam na memoria e os que vão para o disco.
Resumindo: escalonador de admissão, escalonador de CPU e escalonador de memoria
Escalonamento por alternância circular (round robin) – a cada processo e atribuído o seu intervalo de tempo, o seu quantum, no qual ele é permitido executar. Se ao final do quatum o processo ainda estiver a executar, o escalonado dará o CPU ao processo seguinte e o processo que estava a executar vai para o fim da lista circular
Escalonamento por prioridades – o escalonamento circular pressupõe que todos os processos são igualmente importantes. A cada processo é atribuída uma prioridade e os processos são então escalonados conforme essas prioridades. Para evitar que os processos com prioridade mais alta sejam executados indefinidamente o CPU, o escalonado pode reduzir a prioridade do processo a cada tick de relógio.
Filas múltiplas – são definidas classes por prioridades. Os processos na classe mais alta são executados por um quatum, os processos na classe a seguir são executados por dois quantum e assim sucessivamente.
Próximo processo mais curto (shortest process next) - Escalonamento garantido
Escalonamento por loteria
Escalonamento por fraccao just (fair-share)
2.5.4 Scheduler em sistemas de tempo real
Um sistema de tempo real é aquele no qual o tempo tem uma fucao essencial. E é tudo… maisalguma coisa liga para o 118.
2.5.5 Politica versus mecanismo
O paijávai….
2.5.6 Scheduler de threads
Quando um de vários processos tem múltiplas threads, ocorrem dois níveis de paralelismo, processos e threads.