Escalonamento
Unix é time-sharing, ilusão de múltiplos processos concorrentes:
- Estratégia (Policy): regras usadas para decidir que
processo colocar e quando mudar;
- Implementação: estruturas de dados e algoritmos usados na
implementação do sistema
Objectivos conflituantes:
- Resposta rápida para processos interactivos;
- throughput alto para processos background;
- evitar "starvation"
Implementação exige context switch, uma operação cara.
- Guardar registos correntes no PCB;
- Ler PCB do novo registo corrente;
- Tarefas específicas da arquitectura:
- Flush de caches de dados, instruções, ou TLB;
- Prejudica o pipeline e reduz localidade.
- Tb fazer flush do pipeline.
- Custos influenciam escolha da melhor estratégia.
Ver __switch_to em arch/i386/process.c e
kernel/sched.c para Linux. Ver cpu_switch em
i386/i386/swtch.s para FreeBSD.
OS interrompido HZ ticks por segundo:
- reiniciar hw clock, se necessário.
- incrementar estatísticas.
- escalonamento, eg. prioridades e time-slice.
- enviar SIGXCPU para processo se excedeu quota.
- alterar relógio de tempo real.
- processar callouts
- acordar processos de sistemas como swapper e
pageout
- processar alarmes.
Algumas tarefas só são processadas no major tick.
Em Linux do_timer_interrupt()
(arch/i386/kernel/time.c) ->
do_timer() (kernel/timer.c) ->
mark_bh() (include/kernel/interrupt.h)
-> tasklet_action()
(kernel/softirq.c)
Funções a chamar mais tarde (timeout ou task queue):
- Retransmissão de pacotes;
- Funções do escalonador e gestor de memória;
- Monitoração de devices;
- Polling
Interrupt handler coloca uma flag que é verificada no retorno à prioridade
normal.
Callouts são ordenados por:
- "tempo até disparar" em BSD;
- ringlist em Linux: ver run_timer_list() em
kernel/timer.c.
Alarmes são activados ao fim de um certo intervalo de tempo:
- Tempo-Real, SIGALRM
- profiling, SIGPROF
- virtual-time, SIGVTALRM
- BSD usa setitimer(), microsegundos, mas funciona em
ticks.
- SVR4 fornece hrtsys().
- POSIX fornece nanosleep() com precisão de nano-segundos.
- Note que processo só responde ao sinal quando é escalonado, o
que afecta precisão.
Ver kernel/timer.c e kernel/itimer.c em Linux.
Três tipos de aplicações: interactivas, batch e real-time.
- Unix tradicional desenhado para aplicações interactivas.
- Cada processo tem uma prioridade que varia dinâmincamente.
- Processos de mais alta prioridade tiram outros processos do CPU
mesmo quando o processo não terminou o seu quantum.
- Kernel é non-preemtible: processo só returna o CPU quando
bloqueia ou quando regressa a User Mode.
- Prioridades: 0 a 49 para kernel, 50 a 127 para
user-mode.
- Em proc: p_pri, prioridade corrente;
p_usrpri, prioridade em modo utilizador, p_cpu,
uso de CPU, e p_nice.
- Depois de bloquear, pri é associada à prioridade do
recurso (eg., 28 para terminais e 20 para disco).
- quando regressa a user mode, volta a usrpri.
- nice pode ser usado para controlar prioridades.
- p_usrpri = PUSER + (p_cpu/4) +
(2×p_nice)
- p_cpu decai por um factor de 1/2 em SVR3 e (2×load_average)/(2×load_average+1) em BSD,
activado de segundo a segundo por um callout.
load_average é o número médio de processos executáveis
no último segundo.
- BSD previne "starvation": factor dependente do load evita que
prioridades aumentem quando a load aumenta.
- são mantidas 32 filas com as prioridades (VAX). whichqs
contém um bitmask com um 1 para filas ocupadas.
- swtch() examina primeira fila, muda contexto, e quando
retorna processo já está executando.
- Cada 100 ms (BSD) roundrobin() vai buscar outro processo com
a mesma prioridade. Senão, o mesmo processo continua.
- schedcpu() é chamada de segundo a segundo para recomputar a
prioridade.
- clock recomputa prioridade do processo corrente cada vez em 4.
- flag runrun é usada para indicar que processo de mais alta
prioridade está à espera de ser executada, e é verificada antes de entrar
em user-mode.
Problemas do Escalonador BSD:
- Não escala bem: muitos processos faz com que recomputar prioridades
seja pesado.
- Não se pode dar uma porção de CPU a um processo.
- Não há garantias de tempo de resposta para aplicações em tempo
real.
- Aplicações não podem controlar as suas prioridades.
- Kernel nonpreemptive significa que processos de prioridade alta podem
ter que esperar muito tempo antes de executar.
Objectivos do desenho do escalonador em SVR4:
- Suportar mais aplicações, incluindo tempo-real.
- Separar a política de escalonamento dos mecanismos de
escalonamento.
- Permitir às aplicações maior controle sobre prioridade e
escalonamento.
- Definir uma interface bem estabelecida.
- Permitir a adição de novas políticas de uma forma modular.
- Limitar a latência de despacho para aplicações dependentes do
tempo.
Ideias principais:
- Fornecidas duas classes: time-sharing e tempo-real.
- Processamento independentes de classe para:
- mudança de contexto;
- manipulação da fila de processos;
- "preemption".
- Interface para funções com herança e prioridades.
O nível independente de classe tem as seguintes características:
- Prioridades de 0 a 160, com filas separadas.
- Processo de maior prioridade corre sempre.
- Processos são colocados na fila por setfrontdq() e
setbackdq() e removidas por dispdeq().
- Para evitar latência de despacho (problema em Unix por o kernel
ser nonpreemptive) define "preemption points".
- Nesses pontos kernel testa kprunrun para ver se há
processo tempo-real e tira o processo corrente.
- Exemplos são em parsing do pathname; rotina open()
antes de criar o ficheiro; e antes de libertar página.
- runrun existe: preempt() chama
CL_PREEMPT() e depois swtch().
A componente dependente de classe é acedida como um vector de funções que
implementam as componentes dependentes de classe.
- Processos herdam classe do pai e podem ser mudados de classe com
priocntl()
- proc inclui ptrs. para id da classe,
funções da classe, e estruturas de dados privadas.
- CL_TICK é chamada do relógio: time slice, recomputa
prio, expiração do quantum.
- CL_FORK inicializa. CL_FORKRET inicializa
runrun permitindo ao filho correr primeiro.
- CL_ENTERCLASS e CL_EXITCLASS são chamadas
ao entrar e sair de classe.
- CL_SLEEP de sleep() e pode recomputar prioridade.
- CL_WAKEUP é chamada de wakeprocs() coloca
processo na fila e pode colocar runrun ou
kprunrun.
Prioridades são divididas entre:
- 0-59 para time-sharing;
- 60-99 para system;
- 100-159 para tempo-real.
Escalonamento é "round-robin" usando uma tabela de parâmetros fixa:
- Processos com menor prioridade têm maior time slice.
- Usa event-driven scheduling: prioridade é alterada na resposta
a events.
- Dados dependentes de classe:
- ts_timeleft: tempo para
terminar o quantum;
- ts_cpupri, a parte de sistema;
- ts_upri, parte de usuário (nice);
- ts_umdpri: prioridade em modo user é
max(0,min(59,ts_cpupri+ts_upri)),
- ts_dispwait: tempo de relógio desde o início do quantum.
- Em modo kernel prioriade é determinada pela condição de sleep,
depois é restaurada de ts_umdpri.
| glbpri | quant | tqexp |
slpret | mxwt | lwait |
0 | 0 | 100 | 0 | 10 | 5 | 10 |
1 | 1 | 100 | 0 | 11 | 5 | 10 |
... | ... | ... | ... | ... | ... | ... |
15 | 15 | 80 | 7 | 25 | 5 | 25 |
... | ... | ... | ... | ... | ... | ... |
40 | 40 | 20 | 30 | 50 | 5 | 50 |
... | ... | ... | ... | ... | ... | ... |
59 | 59 | 10 | 49 | 59 | 5 | 59 |
|
- ts_globpri: prioridade global;
- ts_quantum: quantum;
- ts_tqexp: ts_cpupri depois
de quantum;
- ts_tqexp: ts_cpupri depois de sleep;
- ts_maxwait: número de segundos para esperar fim de
quantum antes de usar ts_lwait.
- Exigem tempo de latência e tempo de resposta
limitadas.
- prioridade maior do que processos em modo kernel.
- Escalonamento com prioridade e quantum fixos.
Novo algoritmo de escalonamento:
- Configurável por uma tabela.
- Não é preciso recomputar prioridades de todos os processos uma
vez por segundo.
- Ajustes podem ser necessários para manter equilibrio e evitar
prejudicar processos interactivos com computação.
- Obj: definir uma nova classe sem mexer no código do
kernel.
- priocntl() é restrito ao superuser.
- real-time não é deadline-driven.
- Difícil encontrar prioridades certas
(Nieh): escrita,
batch, video e X. Necessário colocar video e X como tempo-real, mas
prejudicava batch-jobs e sistema não respondia ao rato
Solaris tem um mecanismo de escalonamento diferente:
- Kernel é "preemptive".
- Threads de interrupt permitem evitar ipl
- Suporte a multiprocessamento.
- Evitar escalonamento escondido
- Herança de prioridades
- Turnstiles.
Suporte a Multiprocessadores inclui:
- Única fila de despacho.
- Threads podem ser restritos a um processador.
- Processadores podem enviar cross-processor interrupts.
- Cada processador mantém:
- cpu_thread executando;
- cpu_dispthread, o último thread executado;
- cpu_idle thread;
- cpu_runrun, cpu_kprunun;
cpu_chosen_level, prioridade do thread que vai tomar o
processador.
- Se Pi tem um processo com maior prioridade que Pj, coloca o
seu chosen_level e envia um IPI para Pj.
T6 e T7 acordam:
Garante que T7 fica na fila, mesmo que outro CPU veja que P3
está a correr com prioridade 100.
O kernel faz trabalho assíncrono, sem considerar a prioridade das
threads que fizeram a chamada original:
- Kernel pode verificar pedidos em STREAMS, que são servidos pelo
e com a prioridade do processo actual em modo kernel. Ideia: STREAMs
é feito em modo kernel, e abaixo de tempo real.
- Problema: pedidos de STREAMs feitos por processos de tempo-real?
- Callouts têm o mesmo problema, por principio são executados
com prioridade de interrupts.
- Solaris usa uma callout thread, que não inclui os
callouts de real-time.
Thread de baixa prioridade pode ser necessário para activar thread de alta
prioridade:
Quando T3 acorda:
Solução correcta:
O problema pode ser recursivo!
- Herança de prioridade: threads têm prioridade global,
dependendo da classe, e prioridade herdada que depende da
interacção com objectos de sincronização.
- pi_willto() é usada quando thread bloqueia para
passear prioridade recursivamente para os donos de um objecto.
- Fácil para mutexes.
- Em geral impossível para semáforos e variáveis de
sincronização.
- readers-writers: Solaris usa owner-of-record, primeiro
thread a ler o objecto.
- Herança de prioridades reduz tempo de espera, mas não garante
TR, nem evita que cadeias de bloqueamento cresçam.
Muitos objectos de sincronização podem exigir muitos recursos ao sistema.
- Kernel tradicional usa "sleep channel", um endereço, e
usa esse endereço para procurar numa tabela de hash.
- Turnstiles são objectos de tamanho fixo que mantem os
dados para sinc., como um ptr para a lista de threads bloqueados e
para o dono.
- Threads bloqueados são colocados em ordem de prioridade e
acordados por signal() ou broadcast()
- Mach escalona threads independentemente de tasks:
- Ignora overhead de context switches.
- Prioridade-base por task + factor de uso por thread, decaindo a
5/8 por segundo inactivo.
- Cálculos são feitos pelo thread qdo acorda, e pelo relógio. Um
thread interno recomputa prioridades de 2 em 2 segundos.
- Um thread corre até ao fim do quantum. Cede CPU com thread de > prio.
- handoff scheduling: thread pode passar controle a
outro:
- Mach não usa IPIs: atraso prejudica RT, não time-sharing.
- Utilizadores podem criar conjuntos de processadores. Um
servidor determina a alocação.
- Threads podem ser forçados a correr num CPU: útil para
servidores sequenciais, ie de UNIX.
- É possível dedicar um conjunto de CPUs a uma task: gang
scheduling.
- útil para barreiras pq nenhum thread se atrasa;
- e aplicaçãoes fine-grained, pq podem atrasar num thread
suspenso.
- Cada CPU tem uma fila local, e existe fila para o conjunto de trabalho.
- Filas locais são vistas primeiro.
- sched_setscheduler: time-sharing, round-robin (prio.
fixa) e FIFO (prio fixa, sem time-quantum).
- Escalonador escolhe o processo com > prioridade. Se
processador preempted antes de terminar o quantum, colocado na
frente da fila, senão atrás.
- Prioridades de threads são sobrepostas, dando flexibilidade:
- Time-sharing entre 0 e 29.
- Máximo é 63.
- Para ir acima de 19 processo precisa de superuser.
- sched_setparam muda prioridades de processo FIFO e
round-robin;
- sched_yield cede o resto do quantum para outro
processo com a mesma prio.
- Objectivos:
- optimizar mudanças de contexto;
- optimizar utilização de cache;
- evitar problemas de luta por recursos quando vários processos
são acordados da mesma fila global.
- Cada CPU tem uma fila local, e existe fila global.
- Escalonador tenta manter as filas equilibradas.
- Tenta recolocar threads no mesmo processador: soft
affinity. Time-sharing threads usam filas locais a CPU. Sistema
evita load imbalance.
- Processos com prio. fixa são escalonados de fila global,
escalonador tenta reutilizar CPU.
Implementado por schedule() em kernel/sched.c:
- Classes de escalonamento semelhantes a True64 Unix.
- Usa goodness() para estimar em que ponto o processo precisa
do CPU:
- em YIELD, retorna -1;
- RT ou FIFO: retorna 1000+rt_priority;
- OTHER: se p -> counter =
0, dá 0;
- começa com p -> counter;
- dá +PROC_CHANGE_PENALTY se tiver corrido no mesmo CPU (15
no x86);
- dá +1 se tiver mesmo mm;
- dá +20 e subtrai p -> nice.
- schedule() percorre a lista dos processos activos e
escolhe aquele com maior goodness.
- p -> counter é ajustado em:
- se nenhum processo escalonável tiver quanta, usando
p -> nice.
- timer decrementa p -> counter e se 0
coloca p -> need_resched a 1
(update_process_times()).
- pai divide com filho em fork();
- pai recupera p -> counter do filho
em exit();
- processo de tempo-real força recomputação se tiver
interrompido processo time-sharing p com
p -> counter == 0.
- wake_up_process(): processos que acordam podem forçar
reescalonamento se tiverem maior goodness que processo no CPU
currente; complexo em SMP.
- Linux pode definir interfaces em source: aumenta eficiência
- Problemas:
- Suportar a multithreads, ver trabalho da IBM para
Java
e Linux Scalability
Project?
- Afinidade?
- Hints da Aplicação?
- Colocar CPUs ofline?
- Fair-share: cada "share" tem uma percentagem do CPU e outros
recursos, eg Eclipse.
- Deadline Driven com vários tipos de deadline:
- Hard são garantidas;
- Soft tem probabilidade quantificada;
- Time-Sharing e batch.
- 3-niveis com isocronico,
tempo-real e time-sharing:
- reserva de recursos: CPU, MEM, HD;
- processos de tempo-real podem ser interrompidas por tarefas
isocronicas em pontos bem-definidos (fecho de unidade de trabalho)
- tarefas isocronicas usam escalonamento "rate-monotonic".
- Tarefas time-sharing são fully preemptible.
- Evita receive livelock onde sistema só processa
ints colocando rede como TR.
vitor@cos.ufrj.br