4. Escrever o código corretamente

Embora haja muito o que se dizer sobre um processo de design sólido e orientado à comunidade, a prova de qualquer projeto de desenvolvimento de kernel está no código resultante. É o código que será examinado por outros desenvolvedores e mesclado (ou não) na árvore principal (mainline). Portanto, é a qualidade deste código que determinará o sucesso final do projeto.

Esta seção examinará o processo de codificação. Começaremos analisando uma série de maneiras pelas quais os desenvolvedores de kernel podem errar. Em seguida, o foco mudará para como fazer as coisas do jeito certo e as ferramentas que podem ajudar nessa busca.

4.1. Armadilhas

4.1.1. Estilo de Codificação

O kernel há muito possui um estilo de codificação padrão, descrito em Documentation/process/coding-style.rst. Por grande parte desse tempo, as políticas descritas naquele arquivo eram consideradas, no máximo, como recomendações. Como resultado, há uma quantidade substancial de código no kernel que não cumpre as diretrizes de estilo de codificação. A presença desse código leva a dois riscos independentes para os desenvolvedores do kernel.

O primeiro deles é acreditar que os padrões de codificação do kernel não importam e não são exigidos. A verdade é que adicionar novo código ao kernel é muito difícil se esse código não estiver escrito de acordo com o padrão; muitos desenvolvedores solicitarão que o código seja reformatado antes mesmo de revisá-lo. Uma base de código tão grande quanto a do kernel exige certa uniformidade para tornar possível que os desenvolvedores entendam rapidamente qualquer parte dela. Portanto, não há mais espaço para códigos com formatações estranhas.

Ocasionalmente, o estilo de codificação do kernel entrará em conflito com o estilo exigido por um empregador. Nesses casos, o estilo do kernel terá que vencer para que o código possa ser mesclado. Colocar código no kernel significa abrir mão de um certo grau de controle de várias maneiras — incluindo o controle sobre como o código é formatado.

A outra armadilha é presumir que o código já presente no kernel necessita urgentemente de correções de estilo de codificação. Os desenvolvedores podem começar a gerar patches de reformatação como uma forma de ganhar familiaridade com o processo, ou como um meio de incluir seus nomes nos logs de alterações (changelogs) do kernel ou ambos. No entanto, patches puramente de estilo de codificação são vistos como ruído pela comunidade de desenvolvimento; eles tendem a receber uma recepção fria. Portanto, é melhor evitar esse tipo de patch. É natural corrigir o estilo de um trecho de código ao trabalhar nele por outros motivos, mas mudanças de estilo de codificação não devem ser feitas apenas por fazer.

O documento de estilo de codificação também não deve ser lido como uma lei absoluta que nunca pode ser transgredida. Se houver um bom motivo para ir contra o estilo (uma linha que se torna muito menos legível se for dividida para caber no limite de 80 colunas, por exemplo), simplesmente faça isso.

Note que você também pode usar a ferramenta clang-format para ajudá-lo com essas regras, para reformatar rapidamente partes do seu código de forma automática e para revisar arquivos completos a fim de identificar erros de estilo de codificação, erros de digitação e possíveis melhorias. Ela também é útil para ordenar #includes, alinhar variáveis/macros, reajustar o fluxo de textos e outras tarefas semelhantes. Veja o arquivo Documentation/dev-tools/clang-format.rst para mais detalhes.

Algumas configurações básicas do editor, como indentação e fins de linha, serão definidas automaticamente se você estiver usando um editor compatível com o EditorConfig. Consulte o site oficial do EditorConfig para obter mais informações: https://editorconfig.org/

4.1.2. Camadas de Abstração

Os professores de Ciência da Computação ensinam os alunos a fazerem uso extensivo de camadas de abstração em nome da flexibilidade e da ocultação de informações. Certamente o kernel faz uso extensivo de abstração; nenhum projeto que envolva vários milhões de linhas de código poderia fazer o contrário e sobreviver. No entanto, a experiência tem mostrado que a abstração excessiva ou prematura pode ser tão prejudicial quanto a otimização prematura. A abstração deve ser usada até o nível necessário e não além.

Em um nível simples, considere uma função que possui um argumento que é sempre passado como zero por todos os chamadores. Alguém poderia manter esse argumento caso alguém eventualmente precise usar a flexibilidade extra que ele oferece. A essa altura, no entanto, as chances são grandes de que o código que implementa esse argumento extra tenha sido quebrado de alguma forma sutil que nunca foi percebida — porque ele nunca foi usado. Ou, quando surge a necessidade de flexibilidade extra, ela não ocorre de uma forma que corresponda à expectativa inicial do programador. Os desenvolvedores do kernel enviam patches rotineiramente para remover argumentos não utilizados; eles não devem, em geral, ser adicionados em primeiro lugar.

Camadas de abstração que ocultam o acesso ao hardware — frequentemente para permitir que a maior parte de um driver seja usada com múltiplos sistemas operacionais — são especialmente malvistas. Essas camadas obscurecem o código e podem impor uma penalidade de desempenho; elas não pertencem ao kernel Linux.

Por outro lado, se você se pegar copiando quantidades significativas de código de outro subsistema do kernel, é hora de perguntar se faria sentido, de fato, extrair parte desse código em uma biblioteca separada ou implementar essa funcionalidade em um nível superior. Não há valor em duplicar o mesmo código por todo o kernel.

4.1.3. Uso de #ifdef e do pré-processador em geral

O pré-processador C parece apresentar uma forte tentação para alguns programadores C, que o veem como uma forma de codificar eficientemente uma grande quantidade de flexibilidade em um arquivo-fonte. No entanto, o pré-processador não é C, e o uso pesado dele resulta em um código muito mais difícil de ser lido por outros e mais difícil para o compilador verificar a correção. O uso pesado do pré-processador é quase sempre um sinal de código que precisa de algum trabalho de limpeza.

A compilação condicional com #ifdef é, de fato, um recurso poderoso, e é utilizada dentro do kernel. Mas há pouco desejo de ver um código que seja salpicado liberalmente com blocos #ifdef. Como regra geral, o uso de #ifdef deve ser confinado a arquivos de cabeçalho (headers) sempre que possível. O código compilado condicionalmente pode ser confinado a funções que, se o código não estiver presente, simplesmente se tornam vazias. O compilador irá então, silenciosamente, otimizar e remover a chamada para a função vazia. O resultado é um código muito mais limpo e fácil de acompanhar.

As macros do pré-processador C apresentam uma série de riscos, incluindo a possível avaliação múltipla de expressões com efeitos colaterais e a falta de segurança de tipos. Se você se sentir tentado a definir uma macro, considere a criação de uma função inline em seu lugar. O código resultante será o mesmo, mas as funções inline são mais fáceis de ler, não avaliam seus argumentos múltiplas vezes e permitem que o compilador realize a checagem de tipos nos argumentos e no valor de retorno.

4.1.4. Funções Inline

No entanto, as funções inline apresentam um perigo próprio. Os programadores podem ficar encantados com a eficiência percebida inerente a evitar uma chamada de função e encher um arquivo de código-fonte com funções inline. Essas funções, contudo, podem na verdade reduzir o desempenho. Como seu código é replicado em cada local de chamada, elas acabam inflando o tamanho do kernel compilado. Isso, por sua vez, cria pressão nos caches de memória do processador, o que pode desacelerar a execução drasticamente. As funções inline, como regra, devem ser bastante pequenas e relativamente raras. O custo de uma chamada de função, afinal de contas, não é tão alto; a criação de um grande número de funções inline é um exemplo clássico de otimização prematura.

Em geral, os programadores de kernel ignoram os efeitos de cache por sua própria conta e risco. O clássico compromisso entre tempo e espaço (tradeoff) ensinado nas aulas introdutórias de estruturas de dados frequentemente não se aplica ao hardware contemporâneo. Espaço é tempo, no sentido de que um programa maior será executado mais lentamente do que um que seja mais compacto.

Compiladores mais recentes desempenham um papel cada vez mais ativo em decidir se uma determinada função deve ou não ser realmente inline. Portanto, a inserção liberal da palavra-chave “inline” pode não apenas ser excessiva; ela também pode ser irrelevante.

4.1.5. Mecanismo de Trava

Em maio de 2006, a pilha de rede “Devicescape” foi, com grande alarde, lançada sob a GPL e disponibilizada para inclusão no kernel mainline. Essa doação foi uma notícia bem-vinda; o suporte para redes sem fio no Linux era considerado abaixo do padrão, na melhor das hipóteses, e a pilha da Devicescape oferecia a promessa de corrigir essa situação. No entanto, esse código só entrou de fato no mainline em junho de 2007 (2.6.22). O que aconteceu?

Esse código mostrava vários sinais de ter sido desenvolvido a portas fechadas em ambiente corporativo. Mas um grande problema em particular era que ele não havia sido projetado para funcionar em sistemas multiprocessados. Antes que essa pilha de rede (agora chamada de mac80211) pudesse ser integrada, um esquema de locking (bloqueio) precisou ser adaptado a ela.

Era uma vez uma época em que o código do kernel Linux podia ser desenvolvido sem pensar nos problemas de concorrência apresentados por sistemas multiprocessados. Hoje, no entanto, este documento está sendo escrito em um laptop dual-core. Mesmo em sistemas com um único processador, o trabalho feito para melhorar a capacidade de resposta aumentará o nível de concorrência dentro do kernel. Os dias em que o código do kernel podia ser escrito sem pensar em locking ficaram há muito tempo no passado.

Qualquer recurso (estruturas de dados, registradores de hardware, etc.) que possa ser acessado concorrentemente por mais de uma linha de execução deve ser protegido por uma trava (lock). O novo código deve ser escrito com esse requisito em mente; adaptar o locking após o fato é uma tarefa consideravelmente mais difícil. Os desenvolvedores do kernel devem dedicar um tempo para compreender as primitivas de locking disponíveis bem o suficiente para escolher a ferramenta certa para o trabalho. Códigos que mostrem falta de atenção à concorrência terão um caminho difícil para entrar no mainline.

4.1.6. Regressions

Um perigo final que vale a pena mencionar é este: pode ser tentador fazer uma alteração (que pode trazer grandes melhorias) que faça algo quebrar para os usuários existentes. Esse tipo de alteração é chamado de “regressão”, e as regressões tornaram-se totalmente indesejadas no kernel mainline. Com poucas exceções, as alterações que causarem regressões serão revertidas se a regressão não puder ser corrigida em tempo hábil. É muito melhor evitar a regressão em primeiro lugar.

Muitas vezes argumenta-se que uma regressão pode ser justificada se ela fizer as coisas funcionarem para mais pessoas do que os problemas que ela cria. Por que não fazer uma alteração se ela trouxer uma nova funcionalidade para dez sistemas para cada um que ela quebrar? A melhor resposta para essa pergunta foi expressa por Linus em julho de 2007:

Portanto, nós não corrigimos bugs introduzindo novos problemas. Esse caminho
leva à loucura, e ninguém nunca sabe se você está realmente fazendo algum
progresso real. São dois passos para frente, um passo para trás, ou um passo
para frente e dois passos para trás?

(https://lwn.net/Articles/243460/).

Um tipo de regressão especialmente indesejado é qualquer tipo de alteração na ABI do espaço do usuário (user-space ABI). Uma vez que uma interface tenha sido exportada para o espaço do usuário, ela deve receber suporte indefinidamente. Esse fato torna a criação de interfaces de espaço do usuário particularmente desafiadora: já que elas não podem ser alteradas de maneiras incompatíveis, elas devem ser feitas corretamente na primeira vez. Por essa razão, exige-se sempre muita reflexão, documentação clara e uma ampla revisão para as interfaces do espaço do usuário.

4.2. Ferramentas de verificação de código

Por enquanto, pelo menos, a escrita de código livre de erros continua sendo um ideal que poucos de nós conseguem alcançar. O que podemos esperar fazer, no entanto, é capturar e corrigir o máximo possível desses erros antes que nosso código entre no kernel mainline. Para esse fim, os desenvolvedores do kernel reuniram um conjunto impressionante de ferramentas que podem capturar uma ampla variedade de problemas obscuros de forma automatizada. Qualquer problema capturado pelo computador é um problema que não afligirá um usuário mais tarde, portanto, é lógico que as ferramentas automatizadas devem ser usadas sempre que possível.

O primeiro passo é simplesmente prestar atenção aos avisos (warnings) produzidos com o compilador. As versões contemporâneas do gcc podem detectar (e alertar sobre) um grande número de erros potenciais. Com bastante frequência, esses avisos apontam para problemas reais. O código enviado para revisão deve, como regra, não produzir nenhum aviso do compilador. Ao silenciar os avisos, tome o cuidado de entender a real causa e tente evitar “correções” que façam o aviso desaparecer sem resolver a sua origem.

Note que nem todos os avisos do compilador ficam ativados por padrão. Compile o kernel com “make KCFLAGS=-W” para obter o conjunto completo.

O kernel fornece várias opções de configuração que ativam recursos de depuração; a maioria delas é encontrada no submanu “kernel hacking”. Várias dessas opções devem ser ativadas para qualquer kernel usado para fins de desenvolvimento ou teste. Em particular, você deve ativar:

  • FRAME_WARN para obter avisos sobre quadros de pilha (stack frames) maiores que um determinado valor. A saída gerada pode ser volumosa, mas não é necessário se preocupar com os avisos de outras partes do kernel.

  • DEBUG_OBJECTS adicionará código para rastrear o tempo de vida de vários objetos criados pelo kernel e alertará quando as ações forem feitas fora de ordem. Se você estiver adicionando um subsistema que cria (e exporta) seus próprios objetos complexos, considere adicionar suporte à infraestrutura de depuração de objetos.

  • DEBUG_SLAB pode encontrar uma variedade de erros de alocação e uso de memória; ele deve ser usado na maioria dos kernels de desenvolvimento.

  • DEBUG_SPINLOCK, DEBUG_ATOMIC_SLEEP e DEBUG_MUTEXES encontrarão uma série de erros comuns de locking (bloqueio).

Existem várias outras opções de depuração, algumas das quais serão discutidas abaixo. Algumas delas têm um impacto significativo no desempenho e não devem ser usadas o tempo todo. Mas um tempo gasto aprendendo as opções disponíveis provavelmente se pagará muitas vezes em pouco tempo.

Uma das ferramentas de depuração mais pesadas é o verificador de locking, ou “lockdep”. Esta ferramenta rastreará a aquisição e a liberação de cada trava (spinlock ou mutex) no sistema, a ordem em que as travas são adquiridas umas em relação às outras, o ambiente de interrupção atual e muito mais. Ela pode, então, garantir que as travas sejam sempre adquiridas na mesma ordem, que as mesmas suposições de interrupção se apliquem em todas as situações e assim por diante. Em outras palavras, o lockdep pode encontrar uma série de cenários nos quais o sistema poderia, em raras ocasiões, entrar em deadlock. Esse tipo de problema pode ser doloroso (tanto para desenvolvedores quanto para usuários) em um sistema implantado; o lockdep permite que eles sejam encontrados de maneira automatizada e antecipada. Códigos com qualquer tipo de locking não trivial devem ser executados com o lockdep ativado antes de serem enviados para inclusão.

Como um programador de kernel diligente, você irá, sem dúvida, verificar o status de retorno de qualquer operação (como uma alocação de memória) que possa falhar. O fato, porém, é que os caminhos de recuperação de falha resultantes estão, provavelmente, completamente não testados. Código não testado tende a ser código quebrado; você poderia estar muito mais confiante em seu código se todos esses caminhos de tratamento de erros tivessem sido exercitados algumas vezes.

O kernel fornece um framework de injeção de falhas (fault injection) que pode fazer exatamente isso, especialmente onde alocações de memória estão envolvidas. Com a injeção de falhas ativada, uma porcentagem configurável das alocações de memória será forçada a falhar; essas falhas podem ser restritas a um intervalo específico de código. Executar o código com a injeção de falhas ativada permite ao programador ver como o código responde quando as coisas vão mal. Veja Fault injection capabilities infrastructure para mais informações sobre como usar esse recurso.

Outros tipos de erros podem ser encontrados com a ferramenta de análise estática “sparse”. Com o sparse, o programador pode ser alertado sobre confusões entre endereços do espaço do usuário e do espaço do kernel, mistura de quantidades big-endian e small-endian, a passagem de valores inteiros onde um conjunto de sinalizadores de bits (bit flags) é esperado, e assim por diante. O sparse deve ser instalado separadamente (ele pode ser encontrado em https://sparse.wiki.kernel.org/index.php/Main_Page se a sua distribuição não o incluir como pacote); ele pode então ser executado no código adicionando “C=1” ao seu comando make.

A ferramenta “Coccinelle” (http://coccinelle.lip6.fr/) é capaz de encontrar uma ampla variedade de potenciais problemas de codificação; ela também pode propor correções para esses problemas. Uma quantidade considerável de “patches semânticos” para o kernel foi empacotada sob o diretório scripts/coccinelle; executar “make coccicheck” passará por esses patches semânticos e relatará quaisquer problemas encontrados. Veja Documentation/dev-tools/coccinelle.rst para mais informações.

Outros tipos de erros de portabilidade são encontrados mais facilmente ao compilar seu código para outras arquiteturas. Se você por acaso não tiver um sistema S/390 ou uma placa de desenvolvimento Blackfin à mão, ainda assim poderá realizar a etapa de compilação. Um grande conjunto de compiladores cruzados (cross-compilers) para sistemas x86 pode ser encontrado em:

Um tempo gasto instalando e usando esses compiladores ajudará a evitar constrangimentos mais tarde.

4.3. Documentação

A documentação frequentemente tem sido mais a exceção do que a regra no desenvolvimento do kernel. Mesmo assim, uma documentação adequada ajudará a facilitar a integração de novos códigos ao kernel, tornará a vida mais fácil para outros desenvolvedores e será útil para os seus usuários. Em muitos casos, a adição de documentação tornou-se essencialmente obrigatória.

A primeira parte da documentação de qualquer patch é o seu log de alterações (changelog) associado. As entradas do log devem descrever o problema que está sendo resolvido, a forma da solução, as pessoas que trabalharam no patch, quaisquer efeitos relevantes no desempenho e qualquer outra coisa que possa ser necessária para entender o patch. Certifique-se de que o changelog diga o porquê de o patch valer a pena ser aplicado; um número surpreendente de desenvolvedores falha em fornecer essa informação.

Qualquer código que adicione uma nova interface de espaço do usuário — incluindo novos arquivos sysfs ou /proc — deve incluir a documentação dessa interface, de modo a permitir que os desenvolvedores do espaço do usuário saibam com o que estão trabalhando. Veja Documentation/ABI/README para uma descrição de como essa documentação deve ser formatada e quais informações precisam ser fornecidas.

O arquivo Documentation/admin-guide/kernel-parameters.rst descreve todos os parâmetros de boot do kernel. Qualquer patch que adicione novos parâmetros deve adicionar as entradas apropriadas a este arquivo.

Quaisquer novas opções de configuração devem ser acompanhadas por um texto de ajuda que explique claramente as opções e quando o usuário pode querer selecioná-las.

As informações de API interna de muitos subsistemas são documentadas por meio de comentários com formatação especial; esses comentários podem ser extraídos e formatados de várias maneiras pelo script “kernel-doc”. Se você estiver trabalhando em um subsistema que possui comentários kerneldoc, você deve mantê-los e adicioná-los, conforme apropriado, para funções disponíveis externamente. Mesmo em áreas que não tenham sido documentadas dessa forma, não há mal nenhum em adicionar comentários kerneldoc para o futuro; de fato, esta pode ser uma atividade útil para desenvolvedores iniciantes de kernel. O formato desses comentários, junto com algumas informações sobre como criar modelos de kerneldoc, pode ser encontrado em Documentation/doc-guide/.

Qualquer pessoa que leia uma quantidade significativa de código existente do kernel notará que, frequentemente, os comentários chamam a atenção por sua ausência. Mais uma vez, as expectativas para códigos novos são mais altas do que eram no passado; integrar código sem comentários será mais difícil. Dito isso, há pouco interesse em códigos comentados de forma prolixa. O código deve, por si só, ser legível, com os comentários explicando os aspectos mais sutis.

Certas coisas devem sempre ser comentadas. O uso de barreiras de memória (memory barriers) deve ser acompanhado por uma linha explicando por que a barreira é necessária. As regras de locking (bloqueio) para estruturas de dados geralmente precisam ser explicadas em algum lugar. Grandes estruturas de dados precisam de uma documentação abrangente em geral. Dependências não óbvias entre trechos distintos de código devem ser apontadas. Qualquer coisa que possa tentar um “faxineiro de código” (code janitor) a fazer uma “limpeza” incorreta precisa de um comentário dizendo por que foi feita daquela maneira. E assim por diante.

4.4. Alterações de API interna

A interface binária fornecida pelo kernel para o espaço do usuário não pode ser quebrada, exceto sob as circunstâncias mais graves. Por outro lado, as interfaces de programação internas do kernel são altamente fluidas e podem ser alteradas quando surgir a necessidade. Se você se encontrar tendo que criar uma gambiarra para contornar uma API do kernel, ou simplesmente deixando de usar uma funcionalidade específica porque ela não atende às suas necessidades, isso pode ser um sinal de que a API precisa mudar. Como desenvolvedor de kernel, você tem o poder de fazer tais alterações.

Existem, é claro, algumas pegadinhas. Alterações de API podem ser feitas, mas precisam ser bem justificadas. Portanto, qualquer patch que faça uma alteração de API interna deve ser acompanhado por uma descrição do que é a mudança e do porquê ela é necessária. Esse tipo de alteração também deve ser separado em um patch independente, em vez de ser enterrado dentro de um patch maior.

A outra pegadinha é que o desenvolvedor que altera uma API interna é geralmente encarregado da tarefa de corrigir qualquer código dentro da árvore do kernel que tenha sido quebrado pela mudança. Para uma função amplamente utilizada, esse dever pode levar a literalmente centenas ou milhares de alterações — muitas das quais provavelmente entrarão em conflito com o trabalho que está sendo feito por outros desenvolvedores. Desnecessário dizer que isso pode ser um grande trabalho, então é melhor ter certeza de que a justificativa é sólida. Note que a ferramenta Coccinelle pode ajudar com alterações de API de amplo alcance.

Ao fazer uma alteração incompatível de API, deve-se, sempre que possível, garantir que o código que não foi atualizado seja capturado pelo compilador. Isso ajudará você a ter certeza de que encontrou todos os usos dessa interface dentro da árvore (in-tree). Isso também alertará os desenvolvedores de códigos fora da árvore (out-of-tree) de que há uma mudança à qual eles precisam responder. Dar suporte a código fora da árvore não é algo com que os desenvolvedores do kernel precisem se preocupar, mas também não temos que tornar a vida dos desenvolvedores fora da árvore mais difícil do que precisa ser.