Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
OBJETIVOS DO CAPÍTULO
• Introduzir o scheduling da CPU que é a base dos sistemas operacionais multiprogramados.
• Descrever vários algoritmos de scheduling da CPU.
• Discutir critérios de avaliação para a seleção de um algoritmo de scheduling da CPU para um
sistema específico.
• Examinar os algoritmos de scheduling de vários sistemas operacionais.
6.1 Conceitos Básicos
Em um sistema com um único processador, só um processo pode ser executado de cada vez. Os outros
devem esperar até que a CPU esteja livre e possa ser realocada. O objetivo da multiprogramação é haver
sempre algum processo em execução para maximizar a utilização da CPU. A ideia é relativamente simples.
Um processo é executado até ter que esperar, em geral, pela conclusão de alguma solicitação de I/O. Em um
sistema de computação simples, a CPU permanece ociosa. Todo esse tempo de espera é desperdiçado;
nenhum trabalho útil é realizado. Com a multiprogramação, tentamos usar esse tempo produtivamente.
Vários processos são mantidos na memória ao mesmo tempo. Quando um processo precisa esperar, o
sistema operacional desvincula a CPU desse processo e a designa a outro processo. Esse padrão continua.
Sempre que um processo tem de esperar, outro processo pode assumir o uso da CPU.
Um scheduling desse tipo é uma função básica do sistema operacional. Quase todos os recursos do
computador são alocados antes de serem usados. É claro que a CPU é um dos principais recursos do
computador. Portanto, seu scheduling é essencial no projeto do sistema operacional.
6.3 Algoritmos de Scheduling
O scheduling da CPU lida com o problema de decidir para qual dos processos da fila de prontos a CPU deve
ser alocada. Há muitos algoritmos de scheduling da CPU diferentes. Nesta seção, descrevemos vários deles.
6.3.1 Scheduling “PrimeiroaChegar, PrimeiroaSerAtendido”
Sem dúvida, o algoritmo mais simples de scheduling da CPU é o algoritmo “primeiroachegar, primeiro
aseratendido” (FCFS –firstcome, firstserved). Nesse esquema, o processo que solicita a CPU primeiro é
o primeiro a usála. A implementação da política FCFS é facilmente gerenciada com uma fila FIFO. Quando
um processo entra na fila de prontos, seu PCB é conectado na cauda da fila. Quando a CPU está livre, ela é
alocada ao processo na cabeça da fila. O processo em execução é então removido da fila. O código do
scheduling FCFS é simples de escrever e entender.
O lado negativo é que o tempo médio de espera na política FCFS geralmente é bem longo. Considere o
conjunto de processos a seguir que chegam no tempo 0, com o intervalo do pico de CPU dado em
milissegundos:
Processo Duração do Pico
P1 24
P2 3
P3 3
Se os processos chegam na ordem P1, P2, P3 e são atendidos na ordem FCFS, obtemos o resultado
mostrado no gráfico de Gantt, a seguir, que é um gráfico de barras que ilustra um schedule específico,
incluindo os momentos de início e fim de cada um dos processos participantes:
O tempo de espera é de 0 milissegundo para o processo P1, 24 milissegundos para o processo P2, e 27
milissegundos para o processo P3. Logo, o tempo médio de espera é de (0 + 24 + 27)/3 = 17 milissegundos.
Se os processos chegarem na ordem P2, P3, P1, no entanto, os resultados serão os mostrados no gráfico de
Gantt a seguir:
O tempo médio de espera agora é de (6 + 0 + 3)/3 = 3 milissegundos. Essa redução é substancial. Portanto,
geralmente o tempo médio de espera na política FCFS não é mínimo e pode variar significativamente se os
intervalos de pico de CPU dos processos variarem muito.
Além disso, considere o desempenho do scheduling FCFS em uma situação dinâmica. Suponha que
tenhamos um processo limitado por CPU e muitos processos limitados por I/O. Enquanto os processos fluem
pelo sistema, podemos ter como resultado o cenário a seguir. O processo limitado por CPU ocupará e
manterá a CPU. Durante esse período, todos os outros processos terminarão seus I/O e entrarão na fila de
prontos, esperando pela CPU. Enquanto os processos esperam na fila de prontos, os dispositivos de I/O
ficam ociosos. Eventualmente, o processo limitado por CPU termina seu pico de CPU e passa para um
dispositivo de I/O. Todos os processos limitados por I/O que têm picos de CPU curtos são executados
rapidamente e voltam para as filas de I/O. Nesse momento, a CPU permanece ociosa. O processo limitado
por CPU volta então para a fila de prontos e a CPU é alocada para ele. Novamente, todos os processos
limitados por I/O têm de esperar na fila de prontos até que o processo limitado por CPU termine. Há um
efeito comboio, já que todos os outros processos esperam que o grande processo saia da CPU. Esse efeito
resulta em uma utilização da CPU e dos dispositivos menor do que seria possível se os processos mais curtos
pudessem ser atendidos antes.
Observe também que o algoritmo de scheduling FCFS não tem preempção. Uma vez que a CPU tenha
sido alocada para um processo, esse processo a ocupa até liberála, seja ao encerrar sua execução ou ao
solicitar I/O. O algoritmo FCFS é, portanto, particularmente problemático para sistemas de tempo
compartilhado, em que é importante que cada usuário tenha sua vez na CPU a intervalos regulares. Seria
desastroso permitir que um processo se apropriasse da CPU por um período extenso.
6.3.2 Scheduling MenorJobPrimeiro
Uma abordagem diferente para o scheduling da CPU é o algoritmo de scheduling menorjobprimeiro (SJF
– shortestjobfirst). Esse algoritmo associa a cada processo a duração do próximo pico de CPU do processo.
Quando a CPU está disponível, ela é atribuída ao processo que tem o próximo pico de CPU mais curto. Se os
próximos picos de CPU de dois processos forem iguais, o scheduling FCFS será usado para resolver o
impasse. Observe que um termo mais apropriado para esse método de scheduling seria o algoritmo do
próximo pico de CPU mais curto porque o scheduling depende da duração do próximo pico de CPU de um
processo, e não de sua duração total. Usamos o termo SJF porque a maioria das pessoas e livros usa esse
termo para se referir a esse tipo de scheduling.
Como exemplo de scheduling SJF, considere o conjunto de processos a seguir, com a duração de pico de
CPU dada em milissegundos:
Processo Duração do Pico
P1 6
P2 8
P3 7
P4 3
Usando o sheduling SJF, esses processos seriam organizados para execução de acordo com o seguinte
gráfico de Gantt:
O tempo de espera é de 3 milissegundos para o processo P1, 16 milissegundos para o processo P2, 9
milissegundos para o processo P3, e 0 milissegundo para o processo P4. Portanto, o tempo médio de espera é
de (3 + 16 + 9 + 0)/4 = 7 milissegundos. Por comparação, se estivéssemos usando o esquema de scheduling
FCFS, o tempo médio de espera seria de 10,25 milissegundos.
O algoritmo de scheduling SJF é comprovadamente ótimo, pelo fato de fornecer o tempo médio de espera
mínimo para determinado conjunto de processos. Executar um processo curto antes de um longo reduz mais
o tempo de espera do processo curto do que aumenta o tempo de espera do processo longo.
Consequentemente, o tempo médio de espera diminui.
A grande dificuldade do algoritmo SJF é como saber a duração da próxima solicitação de CPU. No
scheduling de longo prazo (scheduling de jobs) em um sistema batch, podemos usar o limite de tempo do
processo, que é especificado pelo usuário quando submete o job. Nessa situação, os usuários são motivados a
estimar o limite de tempo do processo de maneira precisa, já que um valor mais baixo pode significar uma
resposta mais rápida, mas um valor baixo demais causará um erro de limite de tempo excedido e demandará
uma nova submissão do job. O scheduling SJF costuma ser usado no scheduling de longo prazo.
Embora o algoritmo SJF seja ótimo, ele não pode ser implementado no nível do scheduling da CPU de
curto prazo. No scheduling de curto prazo, não há maneira de saber a duração do próximo pico de CPU. Uma
abordagem para esse problema é tentar encontrar um valor aproximado ao do scheduling SJF. Podemos não
saber a duração do próximo pico de CPU, mas talvez possamos prever seu valor. Esperamos que o próximo
pico de CPU tenha duração semelhante à dos picos de CPU anteriores. Calculando um valor aproximado
para a duração do próximo pico de CPU, podemos selecionar o processo com o menor pico de CPU previsto.
O próximo pico de CPU é, em geral, previsto como uma média exponencial dos intervalos medidos dos
picos de CPU anteriores. Podemos definir a média exponencial com a fórmula a seguir. Seja tn a duração do
enésimo pico de CPU e seja τn+1 o valor previsto para o próximo pico de CPU. Então, para α, 0 ≤ a ≤ 1,
temos
τn+1 = α tn + (1 − α)τn.
O valor de tn contém nossa informação mais recente, enquanto τn armazena a história passada. O parâmetro α
controla o peso relativo da história recente e da passada em nossa previsão. Se α = 0, então τn+1 = τn, e a
história recente não tem efeito (as condições correntes são consideradas transientes). Se α = 1, então τn+1 = τn,
e somente o pico de CPU mais recente importa (a história é considerada passada e irrelevante). O mais
comum é α = 1/2; assim, as histórias recente e passada têm peso igual. O τ0inicial pode ser definido como
uma constante ou como uma média geral do sistema. A Figura 6.3 mostra uma média exponencial com α =
1/2 e τ0 = 10.
Para entender o comportamento da média exponencial, podemos expandir a fórmula para τn+1,
substituindo τn, para encontrar
τn+1 = αtn + (1 − α)αtn−1 +… + (1 − α)j αtn−j +… + (1 − α)n+1τ0.
Normalmente, a é menor do que 1. Como resultado, (1 – a) também é menor do que 1, e cada termo
sucessivo tem menos peso do que seu predecessor.
O algoritmo SJF pode ter ou não ter preempção. A escolha é feita quando um novo processo chega à fila
de prontos enquanto um processo anterior ainda está sendo executado. O próximo pico de CPU do processo
recémchegado pode ser mais curto do que o tempo remanescente deixado pelo processo em execução
corrente. Um algoritmo SJF com preempção interromperá o processo em execução corrente, enquanto um
algoritmo SJF sem preempção permitirá que o processo em execução corrente termine seu pico de CPU. O
algoritmo SJF com preempção também é chamado de scheduling do temporemanescentemaiscurto
primeiro.
Como exemplo, considere os quatro processos a seguir, com a duração do pico de CPU dada em
milissegundos:
P1 0 8
P2 1 4
P3 2 9
P4 3 5
Se os processos chegarem à fila de prontos nos momentos mostrados e necessitarem dos tempos de pico
indicados, então o scheduling SJF com preempção resultante será como o mostrado no gráfico de Gantt
abaixo:
Figura 6.3 Previsão da duração do próximo pico de CPU.
O processo P1 é iniciado no tempo 0, já que é o único processo na fila. O processo P2 chega no tempo 1. O
tempo restante do processo P1 (7 milissegundos) é maior do que o tempo requerido pelo processo P2 (4
milissegundos); portanto, o processo P1 é interceptado, e o processo P2 é incluído no schedule. O tempo
médio de espera nesse exemplo é de [(10 – 1) + (1 – 1) + (17 – 2) + (5 – 3)]/4 = 26/4 = 6,5 milissegundos. O
scheduling SJF sem preempção resultaria em um tempo médio de espera de 7,75 milissegundos.
6.3.3 Scheduling por Prioridades
O algoritmo SJF é um caso especial do algoritmo geral de scheduling por prioridades. Uma prioridade é
associada a cada processo, e a CPU é alocada ao processo com a prioridade mais alta. Processos com
prioridades iguais são organizados no schedule em ordem FCFS. O algoritmo SJF é simplesmente um
algoritmo por prioridades em que a prioridade (p) é o inverso do próximo pico de CPU (previsto). Quanto
maior o pico de CPU, menor a prioridade, e viceversa.
Observe que discutimos o scheduling em termos de alta e baixa prioridade. As prioridades são,
geralmente, indicadas por algum intervalo de números fixo, como 0 a 7 ou 0 a 4.095. No entanto, não há um
consenso geral sobre se 0 é a prioridade mais alta ou mais baixa. Alguns sistemas usam números baixos para
representar baixa prioridade; outros usam números baixos para prioridades altas. Essa diferença pode levar à
confusão. Neste texto, assumimos que números baixos representam alta prioridade.
Como exemplo, considere o conjunto de processos a seguir, que assumimos tenham chegado no tempo 0,
na ordem P1,P2, …, P5, com a duração do pico de CPU dada em milissegundos:
P1 10 3
P2 1 1
P3 2 4
P4 1 5
P5 5 2
Usando o scheduling por prioridades, esses processos seriam organizados no schedule de acordo com o
gráfico de Gantt a seguir:
O tempo médio de espera é de 8,2 milissegundos.
As prioridades podem ser definidas interna ou externamente. Prioridades definidas internamente usam
uma ou mais quantidades mensuráveis para calcular a prioridade de um processo. Por exemplo, limites de
tempo, requisitos de memória, número de arquivos abertos e a razão entre o pico médio de I/O e o pico
médio de CPU têm sido usados no cômputo das prioridades. Prioridades externas são definidas por critérios
externos ao sistema operacional, como a importância do processo, o tipo e o montante dos fundos pagos pelo
uso do computador, o departamento que patrocina o trabalho, e outros fatores, geralmente políticos.
O scheduling por prioridades pode ou não ter preempção. Quando um processo chega à fila de prontos,
sua prioridade é comparada com a prioridade do processo em execução corrente. Um algoritmo de
scheduling por prioridades com preempção se apropriará da CPU se a prioridade do processo recémchegado
for mais alta do que a prioridade do processo em execução corrente. Um algoritmo de scheduling de
prioridades sem preempção simplesmente inserirá o novo processo na cabeça da fila de prontos.
Um grande problema dos algoritmos de scheduling por prioridades é o bloqueio indefinido ou inanição.
Um processo que esteja pronto para ser executado, mas em espera pela CPU, pode ser considerado
bloqueado. Um algoritmo de scheduling por prioridades pode deixar alguns processos de baixa prioridade
esperando indefinidamente. Em um sistema de computação muito carregado, um fluxo constante de
processos de prioridade mais alta pode impedir que um processo de prioridade baixa consiga usar a CPU.
Geralmente, acontece uma entre duas coisas. O processo acaba sendo executado (às 2 da madrugada de
domingo, quando finalmente o sistema está pouco carregado) ou o sistema de computação cai e perde todos
os processos de baixa prioridade não concluídos. (Dizem que quando o IBM 7094 foi desligado no MIT em
1973, foi achado um processo de baixa prioridade que tinha sido submetido em 1967 e ainda não tinha sido
executado.)
Uma solução para o problema do bloqueio indefinido de processos de baixa prioridade é o
envelhecimento. O envelhecimento envolve o aumento gradual da prioridade dos processos que esperam no
sistema por muito tempo. Por exemplo, se as prioridades variam de 127 (baixa) a 0 (alta), podemos aumentar
a prioridade de um processo em espera de uma unidade a cada 15 minutos. Eventualmente, até mesmo um
processo com prioridade inicial igual a 127 teria a prioridade mais alta no sistema e seria executado. Na
verdade, não demoraria mais do que 32 horas para que um processo de prioridade 127 envelhecesse até se
tornar um processo de prioridade 0.
6.3.4 Scheduling RoundRobin
O algoritmo de scheduling roundrobin (RR) foi projetado especialmente para sistemas de tempo
compartilhado. Ele é semelhante ao scheduling FCFS, mas a preempção é adicionada para habilitar o sistema
a se alternar entre os processos. Uma pequena unidade de tempo, chamada quantum de tempo ou porção
de tempo, é definida. Geralmente um quantum de tempo tem duração de 10 a 100 milissegundos. A fila de
prontos é tratada como uma fila circular. O scheduler da CPU percorre a fila de prontos, alocando a CPU a
cada processo por um intervalo de até um quantum de tempo.
Para implementar o scheduling RR, devemos tratar novamente a fila de prontos como uma fila FIFO de
processos. Novos processos são adicionados à cauda da fila de prontos. O scheduler da CPU seleciona o
primeiro processo da fila de prontos, define um timer com interrupção após 1 quantum de tempo e despacha
o processo.
Portanto, uma entre duas coisas ocorrerá. O processo pode ter um pico de CPU menor do que 1 quantum
de tempo. Nesse caso, o próprio processo liberará a CPU voluntariamente. O scheduler passará então para o
próximo processo na fila de prontos. Se o pico de CPU do processo em execução corrente for maior do que 1
quantum de tempo, o timer será desligado e causará uma interrupção para o sistema operacional. Uma
mudança de contexto será executada e o processo será inserido na cauda da fila de prontos. O scheduler da
CPU selecionará então o próximo processo na fila de prontos.
O tempo médio de espera na política RR é frequentemente longo. Considere o conjunto de processos, a
seguir, que chegam no tempo 0, com a duração do pico de CPU dada em milissegundos:
Processo Duração do Pico
P1 24
P2 3
P3 3
Se usarmos um quantum de tempo de 4 milissegundos, o processo P1 ficará com os 4 primeiros
milissegundos. Já que ele requer outros 20 milissegundos, é interceptado após o primeiro quantum de tempo,
e a CPU é alocada ao próximo processo na fila, o processo P2. O processo P2 não precisa de 4 milissegundos
e, portanto, é encerrado antes que seu quantum de tempo expire. A CPU é então liberada para o próximo
processo, o processo P3. Uma vez que cada processo tenha recebido 1 quantum de tempo, a CPU é retornada
para o processo P1 por um quantum de tempo adicional. O schedule RR resultante é o seguinte:
Vamos calcular o tempo médio de espera desse schedule. P1 espera por 6 milissegundos (10 – 4), P2 espera
por 4 milissegundos e P3 espera por 7 milissegundos. Assim, o tempo médio de espera é de 17/3 = 5,66
milissegundos.
No algoritmo de scheduling RR, nenhum processo é alocado à CPU por mais de 1 quantum de tempo
sucessivamente (a menos que seja o único processo executável). Se o pico de CPU de um processo exceder 1
quantum de tempo, esse processo é interceptado e devolvido à fila de prontos. Portanto, o algoritmo de
scheduling RR tem preempção.
Se existem n processos na fila de prontos e o quantum de tempo é q, cada processo recebe 1/n do tempo
da CPU em porções de no máximo q unidades de tempo. Cada processo não deve esperar por mais de (n – 1)
× q unidades de tempo até seu próximo quantum de tempo. Por exemplo, no caso de cinco processos e um
quantum de tempo de 20 milissegundos, cada processo receberá até 20 milissegundos a cada 100
milissegundos.
O desempenho do algoritmo RR depende substancialmente do tamanho do quantum de tempo. Por um
lado, se o quantum de tempo é extremamente longo, a política RR é igual à política FCFS. Por outro lado,
quando o quantum de tempo é extremamente curto (digamos, 1 milissegundo), a abordagem RR pode
resultar em um grande número de mudanças de contexto. Suponha, por exemplo, que tenhamos apenas um
processo de 10 unidades de tempo. Se o quantum é de 12 unidades de tempo, o processo termina em menos
de 1 quantum de tempo, sem overhead. Se o quantum é de 6 unidades de tempo, no entanto, o processo
precisa de 2 quanta, resultando em uma mudança de contexto. Se o quantum de tempo é de 1 unidade de
tempo, nove mudanças de contexto ocorrem, tornando proporcionalmente mais lenta a execução do processo
(Figura 6.4).
Assim, queremos que o quantum de tempo seja longo em relação ao tempo de mudança de contexto. Se o
tempo de mudança de contexto for de aproximadamente 10% do quantum de tempo, então cerca de 10% do
tempo da CPU serão gastos com mudança de contexto. Na prática, a maioria dos sistemas modernos tem
quanta de tempo que variam de 10 a 100 milissegundos. O tempo requerido por uma mudança de contexto é,
tipicamente, menor do que 10 microssegundos; portanto, o tempo de mudança de contexto é uma pequena
fração do quantum de tempo.
O tempo de turnaround também depende do tamanho do quantum de tempo. Como podemos ver na
Figura 6.5, o tempo médio de turnaround de um conjunto de processos não melhora necessariamente na
medida em que o tamanho do quantum de tempo aumenta. Geralmente, o tempo médio de turnaround pode
ser melhorado quando a maioria dos processos termina seu próximo pico de CPU em um único quantum de
tempo. Por exemplo, dados três processos de 10 unidades de tempo cada e um quantum de 1 unidade de
tempo, o tempo médio de turnaround é de 29. Se o quantum de tempo é igual a 10, no entanto, o tempo
médio de turnaround cai para 20. Se o tempo de mudança de contexto for incluído, o tempo médio de
turnaround aumenta ainda mais para um quantum de tempo menor, já que mais mudanças de contexto são
necessárias.
Embora o quantum de tempo deva ser longo, comparado ao tempo de mudança de contexto, ele não deve
ser longo demais. Como apontamos anteriormente, se o quantum de tempo for longo demais, o scheduling
RR degenerará para uma política FCFS. Uma regra prática é a de que 80% dos picos de CPU devem ser
menores do que o quantum de tempo.
6.3.5 Scheduling de Filas Multiníveis
Outra classe de algoritmos de scheduling foi criada para situações em que os processos são facilmente
classificados em diferentes grupos. Por exemplo, uma divisão comum é feita entre processos de foreground
(interativos) e processos de background (batch). Esses dois tipos de processos têm requisitos de tempo de
resposta diferentes e, portanto, podem ter diferentes necessidades de scheduling. Além disso, os processos de
foreground podem ter prioridade (definida externamente) sobre os processos de background.
Figura 6.4 Como um quantum de tempo menor aumenta as mudanças de contexto.
Figura 6.5 Como o tempo de turnaround varia com o quantum de tempo.
Um algoritmo de scheduling de filas multiníveis particiona a fila de prontos em várias filas separadas
(Figura 6.6). Os processos são atribuídos permanentemente a uma fila, geralmente com base em alguma
propriedade do processo, como o tamanho da memória, a prioridade do processo ou o tipo do processo. Cada
fila tem seu próprio algoritmo de scheduling. Por exemplo, filas separadas podem ser usadas para processos
de foreground e de background. A fila de foreground pode ser organizada no schedule por um algoritmo RR,
enquanto a fila de background, por um algoritmo FCFS.
Além disso, deve haver um scheduling entre as filas, que é normalmente implementado como um
scheduling de prioridade fixa com preempção. Por exemplo, a fila de foreground pode ter prioridade absoluta
sobre a fila de background.
Examinemos um exemplo de um algoritmo de scheduling de filas multiníveis com cinco filas, listadas
abaixo em ordem de prioridade:
1. Processos do sistema
2. Processos interativos
3. Processos de edição interativa
4. Processos batch
5. Processos de estudantes
Cada fila tem prioridade absoluta sobre as filas de menor prioridade. Nenhum processo na fila batch, por
exemplo, pode ser executado, a não ser que as filas de processos do sistema, processos interativos e
processos de edição interativa estejam todas vazias. Se um processo de edição interativa entrar na fila de
prontos enquanto um processo batch estiver em execução, o processo batch sofrerá preempção.
Figura 6.6 Scheduling de filas multiníveis.
Outra possibilidade é a divisão do tempo entre as filas. Aqui, cada fila recebe determinada parcela do
tempo de CPU que ela pode então distribuir entre seus diversos processos. Por exemplo, no caso das filas de
foreground e de background, a fila de foreground pode receber 80% do tempo da CPU para o scheduling RR
entre seus processos, enquanto a fila de background recebe 20% da CPU para distribuir entre seus processos
usando o scheduling FCFS.
6.3.6 Scheduling de Filas Multiníveis com Retroalimentação
Normalmente, quando o algoritmo de scheduling de filas multiníveis é usado, os processos são atribuídos
permanentemente a uma fila quando entram no sistema. Se houver filas separadas para processos de
foreground e de background, por exemplo, os processos não passam de uma fila para a outra, já que eles não
mudam sua natureza de foreground ou background. Essa definição tem a vantagem de gerar baixo overhead
de scheduling, mas é inflexível.
Por outro lado, um algoritmo de scheduling de filas multiníveis com retroalimentação permite a
alternância de um processo entre as filas. A ideia é separar os processos de acordo com as características de
seus picos de CPU. Se um processo usar muito tempo da CPU, ele será passado para uma fila de prioridade
mais baixa. Esse esquema deixa os processos interativos e limitados por I/O nas filas de prioridade mais alta.
Além disso, um processo que esperar demais em uma fila de prioridade mais baixa pode ser movido para
uma fila de maior prioridade. Esse tipo de envelhecimento evita a inanição.
Por exemplo, considere um scheduler de filas multiníveis com retroalimentação manipulando três filas,
numeradas de 0 a 2 (Figura 6.7). Primeiro, o scheduler executa todos os processos na fila 0. Somente quando
a fila 0 estiver vazia é que ele executará os processos na fila 1. Da mesma forma, os processos na fila 2 serão
executados apenas se as filas 0 e 1 estiverem vazias. Um processo que chegue à fila 1 interceptará um
processo na fila 2. Por sua vez, um processo na fila 1 será interceptado por um processo que chegue à fila 0.
Um processo que entre na fila de prontos é inserido na fila 0. Um processo na fila 0 recebe um quantum
de tempo de 8 milissegundos. Se ele não for concluído dentro desse período, será passado para a cauda da
fila 1. Se a fila 0 estiver vazia, o processo na cabeça da fila 1 receberá um quantum de tempo de 16
milissegundos. Se ele não for concluído, sofrerá preempção e será inserido na fila 2. Os processos da fila 2
serão executados segundo o scheduling FCFS, mas só entrarão em execução quando as filas 0 e 1 estiverem
vazias.
Figura 6.7 Filas multiníveis com retroalimentação.
Esse algoritmo de scheduling dá prioridade mais alta a qualquer processo com pico de CPU de 8
milissegundos ou menos. Tal processo obterá rapidamente a CPU, terminará seu pico de CPU e passará para
seu próximo pico de I/O. Processos que precisam de mais de 8 e menos de 24 milissegundos também são
atendidos rapidamente, embora com prioridade mais baixa do que processos mais curtos. Processos longos
são automaticamente relegados à fila 2, sendo atendidos em ordem FCFS com quaisquer ciclos de CPU
deixados pelas filas 0 e 1.
Em geral, um scheduler de filas multiníveis com retroalimentação é definido pelos parâmetros a seguir:
• O número de filas
• O algoritmo de scheduling de cada fila
• O método usado para determinar quando um processo deve ser elevado a uma fila de prioridade mais alta
• O método usado para determinar quando um processo deve ser rebaixado a uma fila de prioridade mais
baixa
• O método usado para determinar a fila em que um processo entrará quando precisar de serviço
A definição de um scheduler de filas multiníveis com retroalimentação o torna o algoritmo de scheduling de
CPU mais geral. Ele pode ser configurado para se ajustar a um sistema específico que esteja sendo projetado.
Infelizmente, também é o algoritmo mais complexo, já que a definição do melhor scheduler requer alguma
forma de seleção de valores para todos os parâmetros.
6.4 Scheduling de Threads
No Capítulo 4, introduzimos os threads ao modelo de processo, fazendo a distinção entre threads de nível de
usuário e de nível de kernel. Em sistemas operacionais que os suportam, são os threads de nível de kernel —
e não os processos — que são incluídos no schedule pelo sistema operacional. Os threads de nível de usuário
são gerenciados por uma biblioteca de threads, e o kernel não tem conhecimento deles. Para serem
executados em uma CPU, os threads de nível de usuário devem ser mapeados para um thread de nível de
kernel associado, embora esse mapeamento possa ser indireto e usar um processo peso leve (LWP). Nesta
seção, exploramos questões de scheduling envolvendo threads de nível de usuário e de nível de kernel e
outros exemplos específicos de scheduling para o Pthreads.
6.4.1 Escopo de Disputa
Uma diferença entre os threads de nível de usuário e de nível de kernel diz respeito a como eles são
organizados no schedule. Em sistemas que implementam os modelos muitosparaum (Seção 4.3.1) e muitos
paramuitos (Seção 4.3.3), a biblioteca de threads organiza os threads de nível de usuário para execução em
um LWP disponível. Esse esquema é conhecido como escopo de disputa de processos (PCS — process
contention scope), já que a disputa pela CPU ocorre entre threads pertencentes ao mesmo processo. (Quando
dizemos que a biblioteca de threads organiza threads de usuário para execução em LWPs disponíveis, não
queremos dizer que os threads estão sendo realmente executados em uma CPU. Isso demandaria que o
sistema operacional designasse o thread de kernel a uma CPU física.) Para decidir que thread de nível de
kernel deve ser designado a uma CPU, o kernel usa o escopo de disputa do sistema (SCS — system
contention scope). A disputa pela CPU com o scheduling SCS ocorre entre todos os threads no sistema.
Sistemas que usam o modelo umparaum (Seção 4.3.2), como o Windows, o Linux e o Solaris, organizam
threads para execução usando somente o SCS.
Normalmente, o PCS é estabelecido de acordo com prioridades — o scheduler seleciona para execução o
thread executável com a prioridade mais alta. As prioridades dos threads de nível de usuário são definidas
pelo programador e não são ajustadas pela biblioteca de threads, embora algumas bibliotecas de threads
possam permitir que o programador altere a prioridade de um thread. É importante observar que o PCS,
normalmente, intercepta o thread em execução corrente em favor de um thread de prioridade mais alta; no
entanto, não há garantia de divisão do tempo (Seção 6.3.4) entre threads de prioridade igual.
6.4.2 Scheduling no Pthreads
Fornecemos um exemplo de programa Pthreads do POSIX na Seção 4.4.1, junto com uma introdução à
criação de threads com o Pthreads. Agora, destacamos a API POSIX Pthreads que permite a especificação do
PCS ou do SCS durante a criação de threads. O Pthreads identifica os valores de escopo de disputa a seguir:
• PTHREAD_SCOPE_PROCESS organiza threads para execução usando o scheduling PCS.
• PTHREAD_SCOPE_SYSTEM organiza threads para execução usando o scheduling SCS.
Em sistemas que implementam o modelo muitosparamuitos, a política PTHREAD_SCOPE_PROCESS
designa threads de nível de usuário para execução em LWPs disponíveis. O número de LWPs é mantido pela
biblioteca de threads, casualmente usando ativações do scheduler (Seção 4.6.5). A política de scheduling
PTHREAD_SCOPE_SYSTEM cria e vincula um LWP a cada thread de nível de usuário em sistemas
muitosparamuitos, mapeando efetivamente os threads com o uso da política umparaum.
A IPC do Pthreads fornece duas funções para a obtenção — e o estabelecimento — da política de escopo
de disputa:
O primeiro parâmetro das duas funções contém um ponteiro para o conjunto de atributos do thread. O
segundo parâmetro da função pthread_attr_setscope ( ) recebe o valor
PTHREAD_SCOPE_SYSTEM ou PTHREAD_SCOPE_PROCESS, indicando como o escopo de disputa
deve ser estabelecido. No caso de pthread_attr_getscope ( ), esse segundo parâmetro contém um
ponteiro para um valor int, que é posicionado com o valor corrente do escopo de disputa. Se um erro
ocorre, cada uma dessas duas funções retorna um valor diferente de zero.
Na Figura 6.8, ilustramos uma API de scheduling do Pthreads. Primeiro o programa determina o escopo
de disputa existente e o define como PTHREADS_SCOPE_SYSTEM. Em seguida, cria cinco threads
separados que serão executados com o uso da política de scheduling SCS. Observe que, em alguns sistemas,
apenas certos valores de escopo de disputa são permitidos. Por exemplo, os sistemas Linux e Mac OS X
permitem somente PTHREAD_SCOPE_SYSTEM.
#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS 5
/* cria os threads */
for (i = 0; i < NUM_THREADS; i++)
pthread_create(&tid[i],&attr,runner,NULL);
pthread_exit(0);
}
Figura 6.8 API de scheduling do Pthreads.
6.5 Scheduling para Múltiplos Processadores
Até agora nossa discussão enfocou os problemas de scheduling da CPU em um sistema com um único
processador. Se múltiplas CPUs estão disponíveis, o compartilhamento de carga tornase possível; mas os
problemas do scheduling passam a ser igualmente mais complexos. Muitas possibilidades têm sido tentadas,
e como vimos no scheduling da CPU com um único processador, não há uma solução melhor.
Aqui, discutimos várias questões referentes ao scheduling com múltiplos processadores. Enfocamos
sistemas em que os processadores são idênticos — homogêneos — quanto à sua funcionalidade. Podemos
assim usar qualquer processador disponível para executar qualquer processo na fila. Observe, no entanto,
que, mesmo com multiprocessadores homogêneos, podemos ter limitações no scheduling. Considere um
sistema com um dispositivo de I/O conectado a um bus privado de um processador. Processos que queiram
usar esse dispositivo devem ser designados para execução nesse processador.
6.5.1 Abordagens para o Scheduling com Múltiplos Processadores
Em uma das abordagens para o scheduling da CPU em um sistema multiprocessador, todas as decisões de
scheduling, o processamento de I/O e outras atividades do sistema são manipulados por um único
processador — o servidor mestre. Os outros processadores executam apenas código de usuário. Esse
multiprocessamento assimétrico é simples porque somente um processador acessa as estruturas de dados
do sistema, reduzindo a necessidade de compartilhamento de dados.
Uma segunda abordagem usa o multiprocessamento simétrico (SMP — symmetric multiprocessing),
em que cada processador faz o seu próprio scheduling. Todos os processos podem estar numa fila de prontos
comum ou cada processador pode ter sua própria fila privada de processos prontos. De uma forma ou de
outra, o scheduling é executado tendo o scheduler de cada processador que examinar a fila de prontos e
selecionar um processo para execução. Como vimos no Capítulo 5, se existem múltiplos processadores
tentando acessar e atualizar a mesma estrutura de dados, o scheduler deve ser programado cuidadosamente.
Devemos assegurar que dois processadores não selecionem o mesmo processo para execução e que
processos não sejam perdidos da fila. Praticamente todos os sistemas operacionais modernos dão suporte ao
SMP, incluindo o Windows, o Linux e o Mac OS X. No resto desta seção, discutimos questões relacionadas
com os sistemas SMP.
6.5.2 Afinidade com o Processador
Considere o que ocorre na memória cache quando um processo é executado em um processador específico.
Os dados acessados mais recentemente pelo processo preenchem o cache do processador. Como resultado,
acessos sucessivos à memória executados pelo processo são atendidos com frequência na memória cache.
Agora considere o que acontece quando o processo migra para outro processador. O conteúdo da memória
cache deve ser invalidado para o primeiro processador, e o cache do segundo processador deve ser
preenchido novamente. Por causa do alto custo da invalidação e repovoamento dos caches, a maioria dos
sistemas SMP tenta evitar a migração de processos de um processador para outro e, em vez disso, tenta
manter o processo em execução no mesmo processador. Isso é conhecido como afinidade com o
processador — isto é, um processo tem afinidade com o processador em que está em execução corrente.
A afinidade com o processador assume diversas formas. Quando um sistema operacional tem uma
política de tentar manter um processo em execução no mesmo processador — mas não garantindo que ele
fará isso — temos uma situação conhecida como afinidade leve. Nesse caso, o sistema operacional tenta
manter o processo em um único processador, mas é possível que um processo migre entre processadores. Por
outro lado, alguns sistemas fornecem chamadas de sistema que dão suporte à afinidade rígida, permitindo
que um processo especifique um subconjunto de processadores em que ele pode ser executado. Muitos
sistemas fornecem tanto a afinidade leve quanto a rígida. Por exemplo, o Linux implementa a afinidade leve,
mas também fornece a chamada de sistema sched_setaffinity ( ), que suporta a afinidade rígida.
A arquitetura da memória principal de um sistema pode afetar questões relacionadas com a afinidade com
o processador. A Figura 6.9 ilustra uma arquitetura representando o acesso não uniforme à memória
(NUMA), em que uma CPU tem acesso mais rápido a algumas partes da memória principal do que a outras
partes. Normalmente, isso ocorre em sistemas que contêm placas de CPU e memória combinadas. As CPUs
em uma placa podem acessar a memória nessa placa com mais rapidez do que conseguem acessar a memória
em outras placas do sistema. Se os algoritmos de scheduler da CPU e de alocação da memória do sistema
operacional funcionam em conjunto, um processo ao qual é atribuída afinidade com uma CPU específica
pode ter memória alocada na placa em que essa CPU reside. Esse exemplo também mostra que os sistemas
operacionais frequentemente não são definidos e implementados de maneira tão clara como descrito nos
livros de sistemas operacionais. Em vez disso, as “linhas sólidas” entre as seções de um sistema operacional
são com frequência apenas “linhas pontilhadas” com algoritmos criando conexões de maneiras destinadas a
otimizar o desempenho e a confiabilidade.
Figura 6.9 O NUMA e o scheduling da CPU.
6.5.3 Balanceamento de Carga
Em sistemas SMP, é importante manter a carga de trabalho balanceada entre todos os processadores para que
os benefícios do uso de mais de um processador sejam auferidos plenamente.
Caso contrário, um ou mais processadores podem ficar ociosos enquanto outros terão cargas de trabalho
altas, juntamente com listas de processos esperando pela CPU. O balanceamento de carga tenta manter a
carga de trabalho uniformemente distribuída entre todos os processadores em um sistema SMP. É importante
observar que normalmente o balanceamento de carga é necessário somente em sistemas em que cada
processador tem sua própria fila privada de processos elegíveis para execução. Em sistemas com uma fila de
execução comum, o balanceamento de carga não costuma ser necessário, porque, uma vez que um
processador se torne ocioso, ele extrai imediatamente um processo executável da fila de execução comum.
Também é importante observar, no entanto, que, na maioria dos sistemas operacionais contemporâneos que
suportam o SMP, cada processador tem uma fila privada de processos elegíveis.
Existem duas abordagens gerais para o balanceamento de carga: migração por impulsão e migração
por extração. Na migração por impulsão, uma tarefa específica verifica periodicamente a carga em cada
processador e — quando encontra um desequilíbrio — distribui uniformemente a carga, movendo (ou
impulsionando) processos de processadores sobrecarregados para processadores ociosos ou menos ocupados.
A migração por extração ocorre quando um processador ocioso extrai uma tarefa que está esperando em um
processador ocupado. As migrações por impulsão e extração não precisam ser mutuamente exclusivas; na
verdade, são frequentemente implementadas em paralelo em sistemas de balanceamento de carga. Por
exemplo, o scheduler do Linux (descrito na Seção 6.7.1) e o scheduler ULE disponível para sistemas
FreeBSD implementam as duas técnicas.
O interessante é que geralmente o balanceamento de carga neutraliza os benefícios da afinidade com o
processador, discutida na Seção 6.5.2. Isto é, a vantagem de mantermos um processo em execução no mesmo
processador é que o processo pode se beneficiar de seus dados estarem na memória cache desse processador.
A extração ou a impulsão de um processo de um processador para outro invalida esse benefício. Como
costuma ocorrer na engenharia de sistemas, não há uma regra absoluta com relação a que política é melhor.
Portanto, em alguns sistemas, um processador ocioso sempre extrai um processo de um processador não
ocioso. Em outros sistemas, os processos são movidos apenas quando o desequilíbrio excede determinado
limite.
6.5.4 Processadores Multicore
Tradicionalmente, os sistemas SMP têm permitido que vários threads sejam executados concorrentemente,
fornecendo múltiplos processadores físicos. No entanto, uma prática recente no hardware dos computadores
tem sido a inserção de múltiplos núcleos processadores no mesmo chip físico, resultando em um
processador multicore. Cada núcleo mantém o estado de sua arquitetura e, portanto, parece ser um
processador físico separado para o sistema operacional. Sistemas SMP que usam processadores multicore
são mais rápidos e consomem menos energia do que sistemas em que cada processador tem seu próprio chip
físico.
Os processadores multicore podem complicar questões relacionadas com o scheduling. Vejamos como
isso pode ocorrer. Pesquisadores descobriram que, quando um processador acessa a memória, ele gasta um
montante de tempo significativo esperando que os dados fiquem disponíveis. Essa situação, conhecida como
obstrução da memória, pode ocorrer por várias razões, como um erro de cache (acesso a dados que não
estão na memória cache). A Figura 6.10 ilustra uma obstrução da memória. Nesse cenário, o processador
pode gastar até 50% de seu tempo esperando que os dados da memória se tornem disponíveis. Para remediar
essa situação, muitos projetos de hardware recentes têm implementado núcleos processadores multithreaded
em que dois (ou mais) threads de hardware são atribuídos a cada núcleo. Dessa forma, se um thread ficar
obstruído enquanto espera pela memória, o núcleo pode permutar para outro thread. A Figura 6.11 ilustra um
núcleo processador com thread dual em que a execução do thread 0 e a execução do thread 1 são
intercaladas. Para o sistema operacional, cada thread de hardware aparece como um processador lógico que
está disponível para executar um thread de software. Portanto, em um sistema dualcore e dualthreaded,
quatro processadores lógicos são apresentados ao sistema operacional. A CPU UltraSPARC T3 tem
dezesseis núcleos por chip e oito threads de hardware por núcleo. Para o sistema operacional, parece haver
128 processadores lógicos.
Em geral, há duas maneiras de tornar um núcleo processador multithreaded: criação de ambiente
multithreads de baixa granularidade e de alta granularidade. No ambiente multithread de baixa
granularidade, um thread é executado em um processador até que ocorra um evento de latência longa como
uma obstrução da memória. Em razão do atraso causado pelo evento de latência longa, o processador deve
permutar para outro thread e começar sua execução. No entanto, o custo da alternância entre threads é alto, já
que o pipeline de instruções deve ser esvaziado antes que o outro thread possa começar a ser executado no
núcleo processador. Uma vez que esse novo thread comece a ser executado, ele inicia o preenchimento do
pipeline com suas instruções. O ambiente multithread de alta granularidade (ou intercalado) alternase entre
os threads com um nível de granularidade muito mais fina — normalmente no limite de um ciclo de
instrução. No entanto, o projeto da arquitetura de sistemas de alta granularidade inclui a lógica para a
alternância entre threads. Como resultado, o custo da alternância entre threads é baixo.
Figura 6.10 Obstrução da memória.
Figura 6.11 Sistema multithreaded e multicore.
Observe que um processador multicore e multithreaded requer na verdade dois níveis diferentes de
scheduling. Em um nível estão as decisões de scheduling que devem ser tomadas pelo sistema operacional ao
selecionar qual thread de software executar em cada thread de hardware (processador lógico). Para esse nível
de scheduling, o sistema operacional pode selecionar qualquer algoritmo de scheduling, como os descritos na
Seção 6.3. Um segundo nível de scheduling especifica como cada núcleo decide qual thread de hardware
executar. Há várias estratégias que podem ser adotadas nessa situação. O UltraSPARC T3, mencionado
anteriormente, usa um algoritmo roundrobin simples para organizar a execução dos oito threads de
hardware para cada núcleo. Outro exemplo, o Intel Itanium, é um processador dualcore com dois threads
gerenciados pelo hardware por núcleo. A cada thread de hardware é atribuído um valor de urgência
dinâmico que varia de 0 a 7, com 0 representando a urgência mais baixa, e 7, a mais alta. O Itanium
identifica cinco eventos diferentes que podem disparar uma permuta de threads. Quando um desses eventos
ocorre, a lógica de alternância de threads compara a urgência dos dois threads e seleciona o thread com valor
de urgência mais alto para executar no núcleo processador.
6.6 Scheduling da CPU de Tempo Real
O scheduling da CPU para sistemas de tempo real envolve questões especiais. Em geral, podemos fazer a
distinção entre sistemas de tempo real não crítico e sistemas de tempo real crítico. Os sistemas de tempo
real não crítico não fornecem garantia de quando um processo de tempo real crítico será alocado no
schedule. Eles garantem apenas que o processo terá preferência sobre processos não críticos. Sistemas de
tempo real crítico têm requisitos mais rigorosos. Uma tarefa deve ser atendida de acordo com seu limite de
tempo; o atendimento após o limite de tempo ter expirado é o mesmo que não haver atendimento. Nesta
seção, exploramos várias questões relacionadas com o scheduling de processos em sistemas operacionais de
tempo real tanto crítico quanto não crítico.
6.6.1 Minimizando a Latência
Considere a natureza dirigida por eventos de um sistema de tempo real. O sistema espera normalmente pela
ocorrência de um evento de tempo real. Eventos podem ocorrer em software — como quando um timer
expira — ou em hardware — como quando um veículo de controle remoto detecta que está se aproximando
de um obstáculo. Quando um evento ocorre, o sistema deve responder a ele e atendêlo o mais rápido
possível. Denominamos latência do evento o período de tempo decorrido desde a ocorrência de um evento
até o seu atendimento (Figura 6.12).
Usualmente, eventos diferentes têm diferentes requisitos de latência. Por exemplo, o requisito de latência
para um sistema de freios antitravamento pode ser de três a cinco milissegundos. Isto é, a partir do momento
em que uma roda detecta que está derrapando, o sistema que controla os freios antitravamento terá de três a
cinco milissegundos para responder à situação e controlála. Qualquer resposta mais demorada pode resultar
na perda do controle da direção do automóvel. Por outro lado, um sistema embutido de controle de radar em
uma aeronave pode tolerar um período de latência de vários segundos.
Dois tipos de latências afetam o desempenho de sistemas de tempo real:
1. Latência de interrupção
2. Latência de despacho
A latência de interrupção é o período de tempo que vai da chegada de uma interrupção na CPU até o
início da rotina que atende à interrupção. Quando ocorre uma interrupção, o sistema operacional deve
primeiro concluir a instrução que está executando e determinar o tipo de interrupção que ocorreu. Então, ele
deve salvar o estado do processo corrente antes de atender à interrupção usando a rotina de serviço de
interrupção (ISR — interrupt service routine) específica. O tempo total requerido para a execução dessas
tarefas é a latência de interrupção (Figura 6.13). Obviamente, é crucial que os sistemas operacionais de
tempo real minimizem a latência de interrupção para assegurar que as tarefas de tempo real recebam atenção
imediata. Na verdade, para sistemas de tempo real crítico, a latência de interrupção não deve apenas ser