Regras do Contexto de Interrupção em Kernel Space

kernel-space
kernel-space

No que tange desenvolvimento em Kernel Space do Linux e concorrência1, existe um volume enorme de técnicas e procedimentos necessários para evitar as malditas condições de concorrência. Neste artigo apresentamos as particularidades ao lidar com concorrência em interrupções e três maneiras de compartilhar recursos entre interrupções e threads.

 

Aprenda mais sobre Linux Embarcado e Yocto Project com os vídeos das palestras do "Seminário Linux Embarcado 2015"

 

Diferentemente do restante do kernel, o contexto de interrupção é atômico2, ou seja, seu código é executado em um modo especial no processador em alta prioridade onde não existe processos, threads e nem escalonador3, assim, interrupções jamais poderão dormir4 nem ficar em longos períodos de espera. Elas existem para atender uma requisição proveniente de um hardware e as vezes de software, com baixa latência e curta duração.

 

Ao escrever código para rodar em interrupções deve-se saber que diversos mecanismos do kernel (mutex e cia, kmalloc, schedule, copy_to(from)_user, etc.) devem ser usados considerando duas ressalvas:

 

  • a interrupção não pode chamar nenhuma função que durma sob o risco de causar comportamento indefinido, uma vez que não existe processo para escalonar;
  • interrupções não podem aguardar por travas, sob pena de causar dead locks5, já que a interrupção não pode ser removida de contexto, o processador jamais vai ter a oportunidade de executar a thread que possui a trava que ocasionou o travamento da interrupção.

 

Em resumo, não é permitido chamar qualquer função que:

 

  • lide com user space: copy_to_user, copy_from_user, etc;
  • lide com paginação de memória: kmalloc(GFP_KERNEL), alloc_page, etc;
  • durma: down, wait_for_completion, sleep, etc.

 

Quando o código de uma interrupção necessita acessar uma estrutura de dados compartilhada, é importante gerenciar o acesso usando spin_lock_irqsave e spin_lock_irqrestore. Primeiramente porque a família spin_lock não dorme, suas esperas são implementadas com loops do tipo while(1) e podem ser usadas seguramente dentro de interrupções, e, segundo, especificamente as versões _irqsave e _irqrestore desligam (e religam depois) as interrupções, garantindo que nenhuma ocorra enquanto uma thread tiver acesso à trava. Este mecanismo que desliga as interrupções é importante, porque, já que o código de interrupção não pode esperar, ela não deve ocorrer de maneira alguma enquanto uma trava compartilhada estiver habilitada. Esta técnica posterga a execução de todas as interrupções do sistema, aumentando a latência das mesmas, e por isso deve ser mínimo o tempo em que a trava é mantida.

 

Depois, se a memória compartilhada entre threads e interrupções for de apenas uma variável, é possível usar a família de funções atomic_, definida no arquivo <asm/atomic.h>. Essas funções são seguras por executarem em contexto atômico (onde não pode ser interrompido) evitando qualquer problema de concorrência no uso compartilhado dessas variáveis.

 

Por fim, a última opção para lidar com memória compartilhada apresentada neste artigo serve para compartilhar stream de dados. Trata-se do uso da kfifo (<linux/kfifo.h>), uma FIFO que não precisa de controle de acesso quando operada sob único consumidor e único produtor, algo que é característico de algoritmos de FIFOs.

 

Vale lembrar que tasklets6 ocorrem dentro de interrupções (de software no caso), e todas as regras apresentadas neste artigo também se aplicam a eles.

 

 

Notas

 

1 Quando duas, ou mais threads ou interrupções, compartilham o acesso a um mesmo recurso, ocorre em sistemas multi-thread, interrupções e processadores multi-core.
2 Código que não pode ter sua execução interrompida.
3 Principal mecanismo de sistemas operacionais, responsável por gerenciar o tempo de processador dispendido em cada thread.
4 Perder o uso do processador temporariamente por solicitação de software, trocar de contexto.
5 Quando duas ou mais threads ou interrupções ficam impossibilitadas de rodar por estarem aguardando em uma trava que erroneamente jamais será libertada.
6 Mecanismo de agendar a execução de uma função no futuro. Útil para executar processamento postergado.

 

Referências

 

 Linux Device Drivers, 3rd Edition - O'Reilly Media - Capítulo 5 e 10.