- O suporte do Linux à preempção em tempo real é um trabalho que espera há quase 20 anos para entrar no mainline, e Thomas Gleixner afirmou na Linux Plumbers Conference de 2023 que o último grande obstáculo é o
printk() - O objetivo é fazer com que o processo de maior prioridade execute com latência curta e previsível, e para isso muitas partes centrais do kernel foram reescritas ao longo de muito tempo
- O
printk()pode ser chamado em qualquer contexto, então é muito mais complicado do que uma simples saída de log, e a atual saída síncrona entra em conflito com as metas de latência em tempo real - Desde 2018, cerca de 300 patches entraram no upstream ou estão aguardando no linux-next, e as tarefas restantes são lidar com o handover de mensagens urgentes e com a segurança dos drivers de console
- Quando a arrumação do
printk()terminar e o restante do código de tempo real estiver pronto no linux-next, a fusão na mesma merge window também pode acontecer, mas Gleixner disse que não fará mais previsões sobre a data de conclusão
Quase 20 anos de trabalho em preempção em tempo real
- O suporte a tempo real no Linux apareceu pela primeira vez em 2004 na LWN e, por muito tempo, pareceu estar “só mais um pouco” de ficar pronto
- A LWN também usou em 2009 o título the realtime preemption endgame, mas na Linux Plumbers Conference de 2023 Gleixner avaliou que agora o fim realmente está próximo
- Para Gleixner, pessoalmente, isso é um trabalho de quase 25 anos
- Ele começou a trabalhar no suporte a tempo real do Linux em 1999
- O projeto em si também já dura quase 20 anos
- Ele disse que haverá “a big party” quando o trabalho terminar, mas o último grande obstáculo que resta é o
printk()
A latência que a preempção em tempo real tenta reduzir
- O objetivo da preempção em tempo real é permitir que o processo de maior prioridade sempre consiga executar com latência mínima e previsível
- Para isso, o kernel precisa ser preemptível no maior número possível de situações, e as exceções devem ficar limitadas a um escopo estreito e claramente definido
- A forma básica de funcionamento já se estabeleceu há muito tempo, mas resolver os problemas de detalhe levou bastante tempo
- Nesse processo, muitas partes do kernel central foram reescritas, e os benefícios se estenderam para todo o kernel, além dos casos de uso em tempo real
Por que o printk() é o último obstáculo
- Quando o código do kernel precisa enviar mensagens ao console e ao log, ele chama o
printk()ou funções construídas sobre ele - Parece uma saída simples, mas o
printk()precisa funcionar em quase qualquer contexto- Ele pode ser chamado até mesmo em handlers de non-maskable interrupt
- Pode ser chamado novamente de dentro de outra chamada de
printk() - Em situações de crash do sistema, as informações de saída podem ser importantes, então é difícil restringir os contextos de chamada
- Por causa dessas exigências, o
printk()envolve de forma complexa problemas de concorrência, locking e tratamento de drivers - O
printk()do kernel atual tem uma estrutura totalmente síncrona- A chamada não retorna até que a mensagem seja enviada para todos os destinos configurados
- Gleixner descreveu essa estrutura como “stupid”
- Especialmente durante o boot, a maior parte da saída pode ser apenas ruído, mas ainda assim é preciso esperar até que tudo seja enviado
- Esse tempo de espera entra em conflito direto com a latência que o trabalho em tempo real tenta reduzir
- Os desenvolvedores de tempo real já haviam movido há muito tempo a saída do
printk()para uma thread separada, tornando-a assíncrona, mas esse código estava mais para uma coleção de hacks do que para uma solução fundamental
A reformulação do printk() desde 2018
- O problema do
printk()passou a ser tratado de forma séria a partir de 2018, e cerca de 300 patches entraram no upstream ou estão aguardando no linux-next - No momento, estão em andamento os três últimos conjuntos de patches necessários para concluir o trabalho
- Uma das tarefas mais difíceis nos detalhes é o mecanismo de handover
- Quando o kernel precisa imprimir uma mensagem urgente, como em um crash, pode ser necessário tomar o controle de um console que está imprimindo mensagens de prioridade mais baixa
- Não é fácil fazer isso com segurança em qualquer contexto
- Outro desafio é marcar os drivers de console que não podem ser usados com segurança em determinados contextos
- Por exemplo, se for preciso imprimir uma mensagem durante um non-maskable interrupt, mas isso exigir configurar modo de vídeo, não vai funcionar
- Gleixner respondeu que, no último ano, não houve mudanças conceituais fundamentais
- Há 76 drivers de console no kernel que precisam ser corrigidos
- O código de handover foi alterado para permitir atualizar os drivers um por um, sem precisar corrigir todos de uma vez
- Mais discussões recentes sobre o trabalho no
printk()estão neste texto
Saída assíncrona e condições para a fusão no mainline
- Quando Masami Hiramatsu perguntou quais mensagens do kernel precisariam ser impressas de forma síncrona, Gleixner respondeu que quase tudo deveria se tornar assíncrono
- A saída assíncrona reduz a latência gerada pelas chamadas de
printk()e permite ter uma thread de kernel separada para cada console- Assim, um console rápido pode operar no seu próprio ritmo sem esperar pelo console mais lento
- O código foi alterado para que mensagens importantes sejam copiadas completamente para o buffer de mensagens antes de a primeira linha ser impressa
- Isso é uma medida de proteção caso um driver de console defeituoso comprometa o sistema inteiro
- Para uma ordem de saída mais segura, o kernel grava primeiro nos consoles conhecidos como seguros
- Por exemplo, se houver um armazenamento em memória persistente, a mensagem é salva primeiro ali antes de ser enviada para o dispositivo físico
- A ideia é preservar o conteúdo da saída mesmo que um driver defeituoso derrube o sistema
- Gleixner disse que o trabalho está perto do fim, mas que o
printk()é imprevisível, então não falará mais sobre a data de conclusão - Ainda assim, afirmou esperar que o restante do código de preempção em tempo real entre no mainline antes do 20º aniversário, no fim de 2024
- Quando Clark Williams perguntou se, depois que os patches do
printk()entrarem no upstream, o restante do código de tempo real poderia entrar na mesma merge window, Gleixner respondeu “yes” de forma condicional- Se todo o código estiver staged no linux-next e parecer pronto, isso pode ser tentado
1 comentários
Opiniões no Hacker News
QNX já fazia isso direito há décadas. O microkernel tem um limite superior para tudo o que faz, e o código tem apenas dezenas de milhares de linhas.
O microkernel só faz alocação de memória, despacho de CPU e passagem de mensagens entre processos. Todo o resto, incluindo drivers e loggers, fica no espaço de usuário e pode sofrer preempção por threads de prioridade mais alta.
O kernel do QNX não lida com strings. Não há parsing, formatação nem mensagens. O Linux ficou inchado demais para uso em tempo real, e sua própria estrutura não se encaixa bem em tempo real, porque seria preciso tornar preemptíveis milhões de linhas de código do kernel. Por isso levou 20 anos para consertar.
A maior contribuição ao design de kernels talvez seja o uso generalizado de capabilities, uma forma de levar o controle para o espaço de usuário de maneira segura e flexível.
O inchaço do kernel em si não me preocupa muito. Há muito tempo de desenvolvimento investido no Linux e, embora desktops não tenham a mesma prioridade que servidores, o trabalho para criar um kernel de alto desempenho em lugares como dispositivos portáteis também deve beneficiar usuários de desktop.
mainbem estruturada em C ou em uma linguagem da família C. Amainapenas coordena chamadas a outras funções; aqui há a diferença de que o kernel do QNX faz menos inicialização, mas o conceito geral é parecido.Não sou desenvolvedor de kernel, mas essa forma de mantê-lo simples parece boa.
Há um exemplo de como, mesmo em um sistema morrendo, o kernel tenta de alguma forma emitir mensagens de log, e de como isso é usado em ambientes reais de produção.
https://netflixtechblog.com/kubernetes-and-kernel-panics-ed6...
Fico imaginando se, quando esse problema for corrigido, ele poderá substituir uma parte considerável de algumas combinações de hardware/software feitas para tempo real. Hoje há muitas opções de chips ARM e x86 baratos, de baixo consumo e com clocks altos.
Como os clocks são tão altos, mesmo que alguns prazos sejam perdidos, muitas vezes pode haver ciclos sobrando o suficiente para que a temporalidade perfeita seja menos importante. Sei que isso não é elegante nem eficiente, mas às vezes componentes de prateleira vencem a precisão.
Uma única tarefa mal feita pode segurar o kernel e impedi-lo de fazer trabalho útil. O ponto central do tempo real rígido é: “nada pode impedir a execução desta tarefa importante”. Em áreas como automotiva ou aeroespacial, os sistemas de controle precisam funcionar em qualquer circunstância.
Se você realmente precisa de tempo real, então realmente precisa; “perto o bastante” não existe. Dito isso, é minha impressão como alguém de fora.
Um sistema operacional, mesmo sendo um RTOS, atrapalha demais. Não sei o que essa mudança vai alterar. Ainda assim, depende da aplicação, e há muitos casos que precisam apenas de algo “quase em tempo real”, então pode ser útil para esses usos.
A discussão aqui está focada na distinção entre aplicações de tempo real “rígidas” e “flexíveis”. Em tempo real rígido, é bem provável que você nem queira usar um sistema operacional de propósito geral como o Linux; em tempo real flexível, como videoconferência ou reprodução de áudio, não é o fim do mundo se houver engasgos ocasionais ou alguns quadros perdidos.
O argumento é que o RT Linux será uma solução forte para esses usos de tempo real flexível. Mas os usos flexíveis propostos já são possíveis hoje com Linux embarcado. Reprodução de vídeo ou áudio de baixa latência por software não era impossível; isso já era possível há 20 anos.
O problema aparece em sistemas ocupados quando I/O não preemptível interfere com frequência, mas em ambientes embarcados isso é raro. Há motivos convincentes para tornar o kernel totalmente preemptível e dar mais controle sobre o escalonamento, mas isso tem pouca relação com a ideia de que o Linux deveria substituir sistemas operacionais mínimos de tempo real ou código bare-metal.
É mais uma questão de boa higiene: resulta em um sistema operacional que se comporta melhor sob carga até mesmo em aplicações que não são de tempo real.
É uma boa notícia, mas mesmo que o kernel Linux se torne em tempo real, há uma grande chance de o hardware não ser em tempo real por causa do cache e da mágica complexa dentro da CPU
Hardware grande e complexo não combina com tempo real de verdade. Por isso a AbsInt e as ferramentas de pior tempo de execução (WCET) lidam principalmente com arquiteturas de CPU simples. O 8051 realmente vai sobreviver para sempre. A propósito, também existe o Zephyr RTOS
Basta assumir cenários como nenhum acerto de cache e carga máxima. Se der para impor um limite superior ao tempo gasto, está tudo bem
Um timer pode receber entrada de encoder em quadratura e só enviar uma interrupção ao dar wrap, ou o sistema de GPIO pode ser conectado a DMA para transmitir memória para pinos de saída sem intervenção da CPU. Também dá para fazer streaming para um DAC ou transferência via DMA de um ADC para a memória. Essas coisas muitas vezes contornam o cache para obter latência previsível
Na prática, muitos sistemas de tempo real precisam processar e agregar um volume cada vez maior de dados de sensores, então estão ficando cada vez mais potentes
No começo da carreira, passei por um número frustrante de entrevistas em que os entrevistadores não sabiam o que tempo real realmente significa. Muita gente deixava passar o conceito citado no texto, “e latência previsível”, e parecia achar que tempo real significava apenas “rápido”
Se você controla o sistema de frenagem de um carro, “latência média de 50 ms, mas máxima de 80 ms” pode ser aceitável, enquanto “latência média de 1 ms, mas que pode crescer arbitrariamente e levar alguns segundos” não é aceitável
Logging síncrono causou problema de novo. Na empresa passamos por algo parecido por causa do GLOG, a biblioteca de logging do Google; por exemplo, se stdout for um arquivo, pode bloquear em I/O de disco
Quando nosso serviço travava por mais de 100 ms, em 90% a 99% dos casos a causa era o GLOG
“Se algo precisa obrigatoriamente ser registrado em log, mas não dá para registrar, o que fazemos?” Hoje em dia eu simplesmente aponto para o teorema CAP e digo que logging é igual a qualquer outro sistema distribuído. Talvez por haver um artigo da Wikipédia com um desenho de triângulo e a palavra “teorema”, as pessoas tendem a aceitar
Depois disso mudamos para envio por UDP. É melhor perder alguns logs do que perder toda a produção
$MSFT, um problema do tipo “a biblioteca de logging quebra tudo”. Imagine 100 threads, cada uma com um buffer de logging de 300 MBNaturalmente isso destruiu a memória, e o servidor caiu até no SKU mais caro do Azure App Service
Isso me trouxe uma onda de nostalgia. Uns 17 ou 18 anos atrás, compilei um kernel Debian com RT_PREEMPT para usar em equipamento científico que precisava de temporização mais precisa
A latência e o jitter eram muito impressionantes. Quase não pensei mais nisso desde então, mas, ao criar aplicações embarcadas com Raspberry Pi quando você não quer migrar para um microcontrolador com RTOS, parece haver muitos usos possíveis
Achei particularmente interessante porque antes eu já tinha visto propostas de executar Linux como uma tarefa de um RTOS. A ideia é rodar no RTOS aquilo que precisa de deadlines hard de tempo real, sem sofrer o impacto das latências que o sistema de memória virtual pode causar. Não me lembro se isso era apenas uma ideia ou se chegou a ser implementado, e também só vi uma menção a RpiOS em cima de um RTOS, então fiquei curioso
Fico curioso sobre o que isso significa para usuários comuns. É um recurso para ativar apenas em situações muito específicas, ou também pode trazer um sistema mais responsivo para o público em geral?
Cada tarefa recebe um orçamento X e não deve ultrapassá-lo. Se o melhor caso é rápido, mas o pior caso é lento, isso significa que o sistema precisa sempre assumir o pior caso
O exemplo de evitar chamadas síncronas a
printk()é exatamente isso, e deve melhorar a latência sob carga mesmo sem ativar RT. Imagino que um kernel RT totalmente incorporado upstream não se comporte de modo diferente de um kernel comum, a menos que você de fato execute processos RT. O motivo de ter demorado tanto para chegar ao upstream foi a necessidade de compromissos para possibilitar RT e, segundo o texto, agora não restam muitos desses compromissosPorque passa a ser possível usar o kernel mainline mais recente mesmo quando for necessário escalonamento em tempo real
Fico curioso sobre o que vocês acham do Xenomai[1]. Uso há alguns anos sem problemas
No BeagleBone Black, normalmente obtenho jitter na casa de centenas de nanossegundos, e considero isso tempo real “hard”. Dá para escalonar tarefas periódicas na faixa de dezenas de microssegundos sem nunca perder uma
Ao contrário do Real-Time Linux, que tenta tornar o próprio Linux preemptível, o Xenomai é essencialmente seu próprio kernel e executa o Linux como uma tarefa sobre ele. Ele fornece uma ABI para que tarefas criadas pelo usuário rodem lado a lado com o Linux, ou com prioridade mais alta. Por exemplo, ele consegue contornar o problema do
printk(): o Xenomai não se importa e faz de bom grado uma troca de contexto a partir doprintkpara executar a tarefa do usuárioA desvantagem é que, dentro do contexto do Xenomai, você não pode fazer chamadas de sistema comuns. Até pode, mas naturalmente isso quebra o modelo de tempo real. Por exemplo, se chamar
printf()oumalloc()dentro de uma tarefa Xenomai, isso não será preemptível. A ABI do Xenomai replica, no lado das chamadas de sistema, o máximo possível do que você possa precisar, e funciona muito bem se você estiver satisfeito em lidar diretamente com a alocação de heap[1]: https://xenomai.org/