- O kernel Linux vem mantendo vários modos de preempção como compromisso entre vazão e tempo de resposta, e um novo conjunto de patches de Peter Zijlstra recolocou em pauta a discussão sobre a preempção preguiçosa (
PREEMPT_LAZY) - Os modos existentes
PREEMPT_NONE,PREEMPT_VOLUNTARY,PREEMPT_FULLePREEMPT_RTdiferem no quanto permitem preempção; quanto mais frequente ela for, melhor pode ser a responsividade, mas maior também é o custo para a vazão e a contenção de locks PREEMPT_LAZYusa a flagTIF_NEED_RESCHED_LAZYpara marcar que “é preciso reescalonar, mas não imediatamente”, adiando a maior parte das preempções até o tick do temporizador- No longo prazo, a ideia é reduzir os modos de preempção não em tempo real para
PREEMPT_LAZYePREEMPT_FULL, além de remover a maior parte das chamadas acond_resched()espalhadas pelo kernel - O conjunto de patches atual ainda precisa de estabilização, revisão dos pontos de chamada e testes de desempenho; nos testes iniciais, a vazão de
PREEMPT_LAZYficou ligeiramente abaixo dePREEMPT_VOLUNTARY
Modos de preempção existentes no kernel Linux
- O kernel atual oferece vários modos de preempção que controlam quando uma tarefa em execução pode ser preemptada por outra
PREEMPT_NONE: o modo mais simples, que só permite preempção quando a tarefa em execução consome todo o seu time slicePREEMPT_VOLUNTARY: modo que adiciona muitos pontos no kernel onde a preempção pode ocorrer quando necessárioPREEMPT_FULL: permite preempção em quase todos os pontos, exceto em trechos bloqueados pelo kernel, como quando um spinlock está sendo mantidoPREEMPT_RT: prioriza preempção acima de quase tudo e torna preemptável até a maior parte do código que mantém spinlocks
- Um nível maior de preempção pode permitir reações mais rápidas a eventos como o movimento do mouse ou um sinal iminente de anomalia em um reator nuclear
- Em compensação, quanto mais frequente for a preempção, menor pode ser a vazão total de tarefas longas e intensivas em CPU, além de aumentar a contenção de locks
- Muitas distribuições compilam o kernel com o pseudomodo
PREEMPT_DYNAMIC- Na inicialização, é possível escolher um dos três modos não RT anteriores
- O padrão é
PREEMPT_VOLUNTARY - Em sistemas com
debugfsmontado, o modo atual pode ser verificado em/sys/kernel/debug/sched/preempt
Por que cond_resched() era necessário
PREEMPT_NONEePREEMPT_VOLUNTARYnão permitem preempção arbitrária durante a execução de código do kernel- Se um trabalho longo continuar dentro do kernel, isso pode causar latência excessiva mesmo em sistemas onde latência mínima não é a prioridade máxima
- Para evitar isso, chamadas a
cond_resched()foram adicionadas em vários loops de longa duração- Cada chamada é um ponto extra de preempção voluntária
- Também funciona no modo
PREEMPT_NONE - Há centenas dessas chamadas no kernel
- Essa abordagem é uma heurística que só funciona nos pontos escolhidos pelos desenvolvedores
- Pode haver chamadas desnecessárias
- Pode faltar chamadas onde elas seriam necessárias
- A lógica de decisão de escalonamento acaba espalhada por todo o código do kernel
Funcionamento central da preempção preguiçosa
- O kernel observa várias variáveis ao decidir se a tarefa atual pode ser preemptada
- Entre elas,
TIF_NEED_RESCHEDé uma flag que indica que uma tarefa de prioridade mais alta está esperando acesso à CPU- Quando uma tarefa de maior prioridade desperta, essa flag pode ser definida na tarefa atualmente em execução
- Sem essa flag, o kernel não precisa preemptar a tarefa atual
- O kernel pode verificar
TIF_NEED_RESCHEDem vários pontos e preemptar a tarefa atual- no tick do temporizador do escalonador
- ao retornar para o espaço do usuário após uma chamada de sistema
- ao concluir um manipulador de interrupção
- em chamadas a
cond_resched()
- O patch de preempção preguiçosa adiciona a nova flag
TIF_NEED_RESCHED_LAZY- Ela significa que é necessário reescalonar, mas não necessariamente executar isso de imediato
- No modo
PREEMPT_LAZY, a maior parte dos eventos define essa nova flag em vez deTIF_NEED_RESCHED
- Nos pontos em que o kernel retorna ao espaço do usuário, basta que uma das duas flags esteja definida para acionar o escalonador
- Já nos pontos de preempção voluntária e nos caminhos de retorno de interrupção, apenas
TIF_NEED_RESCHEDé verificada
O compromisso criado por PREEMPT_LAZY
- Em
PREEMPT_LAZY, a maior parte dos eventos dentro do kernel não preempta imediatamente a tarefa atual - Em vez disso, o manipulador do tick do temporizador verifica se
TIF_NEED_RESCHED_LAZYestá definida- Se estiver, ele também define
TIF_NEED_RESCHED - Como resultado, a tarefa em execução pode então ser preemptada
- Se estiver, ele também define
- Em geral, a tarefa continua executando por um período próximo ao seu time slice, a menos que ceda a CPU voluntariamente
- Espera-se que esse comportamento resulte em boa vazão
- Com essa mudança,
PREEMPT_LAZYtambém pode rodar, comoPREEMPT_FULL, com a preempção do kernel ativada quase o tempo todo- Sempre que o contador de preempção permitir, a preempção pode ocorrer
- Se outras condições não bloquearem, até código de kernel de longa execução pode ser preemptado
- Quando a preempção imediata é realmente necessária, ela não é adiada
- Por exemplo, se o resultado do tratamento de uma interrupção tornar uma tarefa em tempo real executável,
TIF_NEED_RESCHEDserá definida - Nesse caso, a preempção ocorre quase imediatamente, sem esperar o tick do temporizador
- Por exemplo, se o resultado do tratamento de uma interrupção tornar uma tarefa em tempo real executável,
- Quando apenas
TIF_NEED_RESCHED_LAZYestá definida, não ocorre preempção- Por isso, um kernel
PREEMPT_LAZYtem muito menos probabilidade de preemptar a tarefa em execução do que um kernelPREEMPT_FULL
- Por isso, um kernel
O trabalho que ainda falta até remover cond_resched()
- O objetivo de longo prazo é reduzir os modos de preempção não em tempo real a dois
PREEMPT_LAZYPREEMPT_FULL
PREEMPT_LAZYdeve ocupar a posição entrePREEMPT_NONEePREEMPT_VOLUNTARY, substituindo ambos- Se a preempção passar a ser possível em quase toda parte, diminui a necessidade de adicionar pontos voluntários específicos em locais determinados
- No momento, as chamadas a
cond_resched()continuam presentes- Elas ainda são necessárias enquanto
PREEMPT_NONEePREEMPT_VOLUNTARYexistirem - Também ajudam a evitar problemas durante a estabilização da preempção preguiçosa
- Elas ainda são necessárias enquanto
- No conjunto atual de patches,
cond_resched()verifica apenasTIF_NEED_RESCHED- Por isso, em
PREEMPT_VOLUNTARYouPREEMPT_NONE, muitos casos que antes seriam preemptados imediatamente podem acabar adiados
- Por isso, em
- Steve Rostedt perguntou se manter o significado antigo de
cond_resched(), especialmente emPREEMPT_VOLUNTARY, poderia facilitar a transição - Thomas Gleixner considera correta a escolha de verificar apenas
TIF_NEED_RESCHED- Porque isso força a revisão de todas as chamadas a
cond_resched() - Chamadas que não precisam verificar o bit lazy poderão ser removidas com a adoção de
PREEMPT_LAZY - Chamadas que precisam verificar o bit lazy deverão permanecer
- Porque isso força a revisão de todas as chamadas a
- Gleixner espera que menos de 5% das chamadas a
cond_resched()precisem verificarTIF_NEED_RESCHED_LAZY - Até a transição ser concluída, será preciso revisar centenas de chamadas a
cond_resched()e remover a maioria delas - Um conjunto separado de patches de Ankur Arora trata de parte desses detalhes relacionados
- Também serão necessários testes de desempenho em larga escala
- Nos testes iniciais de Mike Galbraith, a vazão da preempção preguiçosa ficou ligeiramente abaixo de
PREEMPT_VOLUNTARY
- Nos testes iniciais de Mike Galbraith, a vazão da preempção preguiçosa ficou ligeiramente abaixo de
Objetivo final
- Como resultado do trabalho com a preempção preguiçosa, o kernel pode ficar um pouco mais enxuto e simples
- A meta é ter um kernel que ofereça latências previsíveis sem espalhar chamadas relacionadas ao escalonador por todo o código
- A abordagem atual parece uma solução melhor, mas ainda levará tempo até chegar a esse ponto
1 comentários
Comentários do Hacker News
Parece promissor. Como o EEVDF, segue uma direção que melhora ao mesmo tempo em que simplifica o estado atual; difícil ser melhor que isso
Fico me perguntando por que o nível de preempção não é um atributo de eventos específicos, em vez de um modo global. Alguns eventos precisam ser tratados com latência menor do que outros
Portanto, até a prioridade máxima que um evento pode ter é limitada por quão curta pode ser a fatia de tempo que um programa recebe antes de passar por uma troca de contexto. Para responder de forma confiável e com baixa latência a qualquer tipo de evento, todos os programas intensivos em CPU precisam sempre pagar esse custo de desempenho, por mais raro que esse evento seja
Os pontos potenciais de preempção são uma propriedade do escalonador, e é disso que se fala aqui como modo global. Quanto mais pontos de preempção houver, naturalmente maior a chance de um processo ser preemptado em um momento inconveniente, mas também aumentam as oportunidades de refletir corretamente as prioridades. O nível de preempção mencionado na pergunta, isto é, a prioridade dada pelo escalonador, é de fato uma propriedade do processo e pode ser configurado. O escalonador padrão do Linux também dá fatias de tempo maiores a processos com prioridade e tenta preemptar menos outros processos
SCHED_IDLE, SCHED_BATCH e SCHED_NORMAL/OTHER usam preempção adiada, enquanto FIFO, RR e DEADLINE usam o comportamento Full existente
Por isso é importante minimizar o número de aplicativos em execução, ou controlar manualmente aqueles breves momentos que a maioria dos usuários vivencia. Às vezes, trabalhos intensivos em CPU têm mais chance de ser código ruim do que uso realmente eficiente de recursos. Em jogos, o desempenho deve ter prioridade, mas é preciso um equilíbrio delicado para não paralisar o sistema em nome da multitarefa. De qualquer forma, isso é principalmente voltado a tarefas ociosas, então não parece haver grande necessidade de automatizar além de oferecer ao usuário um comando simples para alternar várias ações em scripts
O texto diz que “o kernel atual tem quatro modos que controlam quando uma tarefa pode ser preemptada em favor de outra”. Fico curioso se isso se refere a tarefas do kernel ou se inclui também tarefas de usuário
Não encontrei números na thread vinculada em que o patch foi enviado. Imagino que já devesse haver ao menos alguns benchmarks iniciais mostrando o potencial prático dessa mudança
Ele diz que serão necessários testes de desempenho abrangentes, que Mike Galbraith iniciou o trabalho preliminar e que os resultados mostraram que a vazão da preempção adiada é ligeiramente menor que a do PREEMPT_VOLUNTARY
Fico curioso sobre o quanto o escalonador é fortemente acoplado ao restante do código do kernel
Por exemplo, se eu quisesse simplificar bastante o escalonador para uma aplicação de computação científica que não se importa nada com preempção, isso seria possível de forma limpa e modular? Haveria algum benefício real?
tasksetMas aí é preciso realmente atribuir as tarefas às CPUs manualmente, e também fica fácil acabar com todas as tarefas indo parar nas CPUs erradas. A abordagem padrão é configurar máscaras de interrupção para que interrupções não sejam enviadas às CPUs de “trabalho”, e usar cpuset para que apenas determinados cgroups sejam executados no cpuset especificado
Como a fila de execução fica muito curta, o impacto do que quer que o escalonador faça deve ser bem pequeno. Se a aplicação não fizer muito I/O, também não haverá muitas interrupções. Se for possível usar um kernel tickless — não sei se hoje isso ainda é uma opção separada ou o padrão —, talvez quase não haja interrupções por longos períodos
Porém, o motivo para simplificar drasticamente seria evitar bugs, não obter muito desempenho em comparação com um escalonador padrão bem configurado. Há muitas configurações, mas também não havia muitos bugs nessa área. Se você simplificar de forma ingênua, na maioria das vezes vai perder desempenho em vez de ganhar. Se estiver rodando um sistema não interativo, a mudança mais simples é aumentar a quota de tempo dos processos