A estrutura de um módulo do kernel e seus métodos de compilação. Características de compilação de um programa com estrutura modular. Introdução e Contextualização

Por que compilar o kernel você mesmo?
Talvez a principal pergunta feita sobre a compilação de um kernel seja: “Por que devo fazer isso?”
Muitos consideram isso uma perda de tempo inútil para se mostrarem usuários inteligentes e avançados de Linux. Na verdade, compilar o kernel é um assunto muito importante. Digamos que você comprou um laptop novo e sua webcam não funciona. Suas ações? Você olha para o mecanismo de busca e procura uma solução para o problema. Muitas vezes pode acontecer que a sua webcam esteja funcionando em um nível mais nova versão do que o seu. Se você não sabe qual versão possui, digite uname -r no terminal, como resultado você obterá a versão do kernel (por exemplo, linux-2.6.31-10). A compilação do kernel também é amplamente utilizada para aumentar o desempenho: o fato é que, por padrão, as distribuições do kernel compilam “para todos”, e é por isso que inclui um grande número de drivers que você pode não precisar. Portanto, se você conhece bem o hardware que está usando, pode desabilitar drivers desnecessários na fase de configuração. Também é possível ativar o suporte para mais de 4 GB de RAM sem alterar a profundidade de bits do sistema. Então, se você ainda precisa ter seu próprio kernel, vamos começar a compilar!

Obtendo o código-fonte do kernel.
A primeira coisa que você precisa fazer é obter o código-fonte da versão necessária do kernel. Normalmente você precisa obter a versão estável mais recente. Todas as versões oficiais do kernel estão disponíveis em kernel.org. Se você já possui o servidor X instalado ( computador de casa), então você pode acessar o site em seu navegador favorito e baixar a versão desejada no arquivo tar.gz (compactado em gzip). Se você estiver trabalhando no console (por exemplo, ainda não instalou o servidor X ou está configurando o servidor), poderá usar um navegador de texto (por exemplo, elinks). Você também pode usar o gerenciador de download padrão wget:
wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.33.1.tar.gz
Mas lembre-se de que você deve saber o número exato da versão necessária.

Descompactando o arquivo do código-fonte.
Depois de receber o arquivo do código-fonte, você precisa extrair o arquivo para uma pasta. Isso pode ser feito a partir de gráficos gerenciadores de arquivos(golfinho, nautilus, etc.) ou via mc. Ou use o comando tar tradicional:
tar -zxvf caminho_para_arquivo
Agora que você tem uma pasta com o código-fonte, acesse-a usando o comando CD kernel_source_directory(para listar os diretórios em uma pasta, use o comando ls).

Configuração do kernel.
Depois de navegar até o diretório de origem do kernel, você precisa realizar uma configuração do kernel de “20 minutos”. Seu objetivo é deixar apenas os drivers e funções necessários. Todos os comandos já devem ser executados como superusuário.

make config - modo console do configurador.

make menuconfig - modo console na forma de uma lista.

make xconfig - modo gráfico.

Após fazer as alterações necessárias, salve as configurações e saia do configurador.

Compilação.
Chegou a hora da etapa final da montagem - compilação. Isso é feito com dois comandos:
fazer && fazer instalar
O primeiro comando compilará todos os arquivos em código de máquina e o segundo instalará o novo kernel em seu sistema.
Esperamos de 20 minutos a várias horas (dependendo da potência do computador). O kernel está instalado. Para fazê-lo aparecer na lista grub(2), digite (como superusuário)
atualização-grub
Agora, após reiniciar, pressione "Escape" e você verá o novo kernel na lista. Se o kernel não ligar, basta inicializar com o kernel antigo e configurá-lo com mais cuidado.

KernelCheck - compila o kernel sem acessar o console.
permite que você construa o kernel em modo totalmente gráfico para Debian e distribuições baseadas nele. Após o lançamento, o KernelCheck oferecerá as versões e patches mais recentes do kernel e, após o seu consentimento, baixará o código-fonte e iniciará o configurador gráfico. O programa irá compilar o kernel em pacotes .deb e instalá-los. Tudo que você precisa fazer é reiniciar.

Sobre: ​​"Baseado na tradução" Linux Device Driver 2ª edição. Tradução: Knyazev Alexei [e-mail protegido] Data da última modificação: 03/08/2004 Local: http://lug.kmv.ru/index.php?page=knz_ldd2

Agora vamos começar a programar! Este capítulo fornece o básico sobre módulos e programação do kernel.
Aqui construiremos e lançaremos um módulo completo, cuja estrutura corresponde a qualquer driver modular real.
Ao mesmo tempo, nos concentraremos nas posições principais, sem levar em conta as especificidades dos dispositivos reais.

Todas as partes do kernel, como funções, variáveis, arquivos de cabeçalho e macros mencionadas aqui, serão
são descritos em detalhes no final do capítulo.

Olá Mundo!

No processo de familiarização com o material original escrito por Alessndro Rubini e Jonathan Corbet, o exemplo dado como Hello world me pareceu um tanto malsucedido. Portanto, quero oferecer ao leitor, na minha opinião, uma versão mais bem-sucedida do primeiro módulo. Espero que não haja problemas com sua compilação e instalação no kernel 2.4.x. O módulo proposto e a forma como é compilado permitem que ele seja utilizado em kernels que suportam e não suportam controle de versão. Você se familiarizará com todos os detalhes e terminologia mais tarde, então agora abra o vim e comece a trabalhar!

================================================= === //arquivo hello_knz.c #include #incluir <1>Olá, mundo\n"); return 0; ); void cleanup_module(void) ( printk("<1>Adeus mundo cruel\n"); ) MODULE_LICENSE(“GPL”); ================================== =================

Para compilar tal módulo, você pode usar o seguinte Makefile. Não se esqueça de colocar um caractere de tabulação antes da linha que começa com $(CC) ... .

================================================= === FLAGS = -c -Wall -D__KERNEL__ -DMODULE PARAM = -I/lib/modules/$(shell uname -r)/build/include hello_knz.o: hello_knz.c $(CC) $(FLAGS) $( PARAM) -o $@ $^ ========================================== ================== ====

Isso usa dois recursos em comparação com o código Hello World original proposto por Rubini & Corbet. Primeiro, o módulo terá a mesma versão da versão do kernel. Isto é conseguido definindo a variável PARAM no script de compilação. Em segundo lugar, o módulo será agora licenciado sob a GPL (usando a macro MODULE_LICENSE()). Se isso não for feito, ao instalar o módulo no kernel você poderá ver algo como o seguinte aviso:

# insmod hello_knz.o Aviso: carregar hello_knz.o irá contaminar o kernel: sem licença Consulte http://www.tux.org/lkml/#export-tainted para obter informações sobre módulos contaminados Módulo hello_knz carregado, com avisos

Vamos agora explicar as opções de compilação do módulo (as definições de macro serão explicadas posteriormente):

-Com- com esta opção, o compilador gcc interromperá o processo de compilação do arquivo imediatamente após a criação do arquivo objeto, sem tentar criar um binário executável.

-Parede- nível máximo de aviso quando o gcc está em execução.

-D— definições de símbolos macro. Igual à diretiva #define no arquivo compilado. Não faz absolutamente nenhuma diferença como definir os símbolos de macro usados ​​neste módulo, usando #define no arquivo fonte ou usando a opção -D para o compilador.

-EU- caminhos de pesquisa adicionais para arquivos incluídos. Observe o uso da substituição “uname -r”, que determinará o nome exato da versão do kernel atualmente em uso.

A próxima seção fornece outro exemplo de módulo. Também explica detalhadamente como instalá-lo e descarregá-lo do kernel.

Original Olá, mundo!

Agora vamos dar uma olhada no código original do módulo simples "Hello, World" oferecido por Rubini & Corbet. Este código pode ser compilado nas versões 2.0 a 2.4 do kernel. Este exemplo, e todos os outros apresentados no livro, estão disponíveis no site FTP da O'Reilly (ver Capítulo 1).

//arquivo hello.c #define MÓDULO #include int init_module(void) ( printk("<1>Olá, mundo\n"); return 0; ) void cleanup_module(void) ( printk("<1>Adeus mundo cruel\n"); )

Função imprimir() definido no kernel do Linux e funciona como uma função de biblioteca padrão imprimirf() em linguagem C. O kernel precisa de sua própria função de inferência, de preferência pequena, contida diretamente no kernel, e não em bibliotecas de nível de usuário. Um módulo pode chamar uma função imprimir() porque depois de carregar o módulo usando o comando insmod O módulo se comunica com o kernel e tem acesso às funções e variáveis ​​​​publicadas (exportadas) do kernel.

Parâmetro de string “<1>”passado para a função printk() é a prioridade da mensagem. As fontes originais em inglês usam o termo loglevel, que significa o nível de registro de mensagens. Aqui, usaremos o termo prioridade em vez do “loglevel” original. Neste exemplo, usamos alta prioridade para a mensagem que possui um número baixo. A alta prioridade da mensagem é definida intencionalmente, pois uma mensagem com a prioridade padrão pode não ser exibida no console a partir do qual o módulo foi instalado. A direção de saída das mensagens do kernel com prioridade padrão depende da versão do kernel em execução, da versão do daemon klogd e sua configuração. Mais detalhadamente, trabalhando com a função imprimir() explicaremos no Capítulo 4, Técnicas de depuração.

Você pode testar o módulo usando o comando insmod para instalar o módulo no kernel e comandos rmod para remover um módulo do kernel. Abaixo mostraremos como isso pode ser feito. Neste caso, o ponto de entrada init_module() é executado quando um módulo é instalado no kernel, e cleanup_module() é executado quando é removido do kernel. Lembre-se que apenas um usuário privilegiado pode carregar e descarregar módulos.

O exemplo de módulo acima só pode ser usado com um kernel que foi construído com o sinalizador “suporte à versão do módulo” desativado. Infelizmente, a maioria das distribuições usa kernels controlados por versão (isso é discutido na seção "Controle de versão em módulos" do Capítulo 11, "kmod e modularização avançada"). E embora versões mais antigas do pacote módulos permitir que tais módulos sejam carregados em kernels controlados por versão, o que não é mais possível. Lembre-se de que o pacote modutils contém um conjunto de programas que inclui os programas insmod e rmmod.

Tarefa: Determine o número da versão e a composição do pacote modutils da sua distribuição.

Ao tentar inserir esse módulo em um kernel que suporta controle de versão, você poderá ver uma mensagem de erro semelhante a esta:

# insmod hello.o hello.o: incompatibilidade de versão do módulo do kernel hello.o foi compilado para a versão 2.4.20 do kernel enquanto este kernel é a versão 2.4.20-9asp.

No catálogo módulos diversos exemplos em ftp.oreilly.com você encontrará o programa de exemplo original hello.c, que contém um pouco mais de linhas e pode ser instalado em kernels controlados por versão e não versionados. Entretanto, recomendamos fortemente que você construa seu próprio kernel sem suporte de controle de versão. Ao mesmo tempo, é recomendável levar as fontes originais do kernel no site www.kernel.org

Se você é novo na montagem de kernels, tente ler o artigo que Alessandro Rubini (um dos autores do livro original) postou em http://www.linux.it/kerneldocs/kconf, que deve ajudá-lo a dominar o processo.

Execute os seguintes comandos em um console de texto para compilar e testar o módulo de exemplo original acima.

Root# gcc -c hello.c root# insmod ./hello.o Olá, mundo root# rmmod olá Adeus mundo cruel root#

Dependendo do mecanismo que seu sistema usa para passar strings de mensagens, a direção de saída das mensagens enviadas pela função imprimir(), pode ser diferente. No exemplo dado de compilação e teste de um módulo, as mensagens enviadas da função printk() foram enviadas para o mesmo console a partir do qual foram fornecidos os comandos para instalar e executar os módulos. Este exemplo foi retirado de um console de texto. Se você executar os comandos insmod E rmod do programa termo x, provavelmente você não verá nada em seu terminal. Em vez disso, a mensagem pode acabar em um dos logs do sistema, por exemplo, em /var/log/messages. O nome exato do arquivo depende da distribuição. Observe o horário das alterações nos arquivos de log. O mecanismo usado para passar mensagens da função printk() é descrito na seção "Como as mensagens são registradas" no Capítulo 4 "Técnicas"
depuração".

Para visualizar mensagens do módulo no arquivo de log do sistema /val/log/messages é conveniente usar utilitário do sistema tail, que, por padrão, exibe as últimas 10 linhas do arquivo passado para ele. Uma opção interessante deste utilitário é a opção -f, que executa o utilitário no modo de monitoramento das últimas linhas do arquivo, ou seja, Quando novas linhas aparecerem no arquivo, elas serão impressas automaticamente. Para interromper a execução do comando neste caso, você deve pressionar Ctrl+C. Assim, para visualizar as últimas dez linhas do arquivo de log do sistema, digite o seguinte na linha de comando:

Raiz# cauda /var/log/messages

Como você pode ver, escrever um módulo não é tão difícil quanto pode parecer. O mais difícil é entender como funciona o seu dispositivo e como melhorar o desempenho do módulo. À medida que continuarmos neste capítulo, aprenderemos mais sobre como escrever módulos simples, deixando as especificações do dispositivo para capítulos posteriores.

Diferenças entre módulos e aplicativos do kernel

O aplicativo possui um ponto de entrada, que começa a ser executado imediatamente após a colocação aplicativo em execução na RAM do computador. Este ponto de entrada é descrito em C como a função main(). Encerrar a função main() significa encerrar o aplicativo. O módulo possui diversos pontos de entrada que são executados ao instalar e remover o módulo do kernel, bem como ao processar solicitações do usuário. Assim, o ponto de entrada init_module() é executado quando o módulo é carregado no kernel. A função cleanup_module() é executada quando um módulo é descarregado. No futuro, conheceremos outros pontos de entrada no módulo, que são executados ao executar diversas solicitações ao módulo.

A capacidade de carregar e descarregar módulos é dois pilares do mecanismo de modularização. Eles podem ser avaliados de diferentes maneiras. Para o desenvolvedor isso significa, antes de tudo, uma redução no tempo de desenvolvimento, pois você pode testar a funcionalidade do driver sem um longo processo de reinicialização.

Como programador, você sabe que uma aplicação pode chamar uma função que não foi declarada na aplicação. Nas etapas de vinculação estática ou dinâmica, são determinados os endereços de tais funções das bibliotecas correspondentes. Função imprimirf() uma dessas funções que podem ser chamadas que está definida na biblioteca libc. Um módulo, por outro lado, está associado apenas ao kernel e só pode chamar funções exportadas pelo kernel. O código executado no kernel não pode usar bibliotecas externas. Assim, por exemplo, a função imprimir(), que foi usado no exemplo olá.c, é um análogo da função bem conhecida imprimirf(), disponível em aplicativos de nível de usuário. Função imprimir() localizado no núcleo e deve ser o menor possível. Portanto, diferentemente de printf(), ele tem suporte muito limitado para tipos de dados e, por exemplo, não oferece suporte a números de ponto flutuante.

As implementações do kernel 2.0 e 2.2 não suportavam especificadores de tipo eu E Z. Eles foram introduzidos apenas na versão 2.4 do kernel.

A Figura 2-1 mostra a implementação do mecanismo para chamar funções que são pontos de entrada no módulo. Além disso, esta figura mostra o mecanismo de interação de um módulo instalado ou instalado com o kernel.

Arroz. 2-1. Comunicação entre o módulo e o kernel

Uma das características dos sistemas operacionais Unix/Linux é a falta de bibliotecas que possam ser vinculadas aos módulos do kernel. Como você já sabe, os módulos, quando carregados, são vinculados ao kernel, portanto todas as funções externas ao seu módulo devem ser declaradas nos arquivos de cabeçalho do kernel e presentes no kernel. Fontes de módulo nunca não deve incluir arquivos de cabeçalho regulares de bibliotecas de espaço do usuário. Nos módulos do kernel, você só pode usar funções que realmente fazem parte do kernel.

Toda a interface do kernel é descrita em arquivos de cabeçalho localizados nos diretórios incluir/linux E incluir/asm dentro das fontes do kernel (geralmente localizadas em /usr/src/linux-x.y.z(x.y.z é a sua versão do kernel)). Distribuições mais antigas (baseadas em libc versão 5 ou inferior) usava links simbólicos /usr/incluir/linux E /usr/incluir/asm para os diretórios correspondentes nas fontes do kernel. Esses links simbólicos possibilitam, se necessário, o uso de interfaces de kernel em aplicativos de usuário.

Embora a interface das bibliotecas do espaço do usuário agora esteja separada da interface do kernel, às vezes os processos do usuário precisam usar interfaces do kernel. Entretanto, muitas referências nos arquivos de cabeçalho do kernel referem-se apenas ao próprio kernel e não devem ser acessíveis aos aplicativos do usuário. Portanto, esses anúncios são protegidos #ifdef__KERNEL__ blocos. É por isso que seu driver, como qualquer outro código do kernel, deve ser compilado com uma macro declarada __NÚCLEO__.

A função dos arquivos de cabeçalho individuais do kernel será discutida conforme apropriado ao longo do livro.

Os desenvolvedores que trabalham em grandes projetos de software (como o kernel) devem estar cientes e evitar "poluição do namespace". Este problema ocorre quando existe um grande número de funções e variáveis ​​globais cujos nomes não são suficientemente expressivos (distinguíveis). O programador que mais tarde tiver que lidar com tais aplicações é forçado a gastar muito mais tempo lembrando-se de nomes "reservados" e criando nomes exclusivos para novos elementos. Colisões de nomes (ambiguidades) podem criar uma ampla gama de problemas, desde erros ao carregar um módulo até comportamento instável ou inexplicável do programa que pode ocorrer para usuários que usam um kernel construído em uma configuração diferente.

Os desenvolvedores não podem permitir tais erros ao escrever o código do kernel, porque mesmo o menor módulo estará vinculado ao kernel inteiro. A melhor solução para evitar colisões de nomes é primeiro declarar os objetos do seu programa como estático e, em segundo lugar, o uso de um prefixo exclusivo, dentro do sistema, para nomear objetos globais. Além disso, como desenvolvedor de módulo, você pode controlar o escopo dos objetos em seu código, conforme descrito posteriormente na seção "Tabela de links do kernel".

A maioria (mas não todas) versões do comando insmod exportar todos os objetos do módulo que não são declarados como estático, por padrão, ou seja, a menos que o módulo defina instruções especiais para este fim. Portanto, é bastante razoável declarar objetos de módulo que você não pretende exportar como estático.

Usar um prefixo exclusivo para objetos locais dentro de um módulo pode ser uma boa prática, pois facilita a depuração. Ao testar seu driver, pode ser necessário exportar objetos adicionais para o kernel. Ao usar um prefixo exclusivo para designar nomes, você não corre o risco de introduzir colisões no namespace do kernel. Os prefixos usados ​​no kernel são, por convenção, caracteres minúsculos, e seguiremos essa convenção.

Outra diferença significativa entre o kernel e os processos do usuário é o mecanismo de tratamento de erros. O kernel controla a execução do processo do usuário, portanto, um erro no processo do usuário resulta em uma mensagem inofensiva para o sistema: falha de segmentação. Ao mesmo tempo, um depurador sempre pode ser usado para rastrear erros no código-fonte do aplicativo do usuário. Erros que ocorrem no kernel são fatais - se não para todo o sistema, pelo menos para o processo atual. Na seção “Depuração de erros do sistema” do Capítulo 4, “Técnicas de depuração”, veremos maneiras de rastrear erros do kernel.

Espaço do usuário e espaço do kernel

O módulo é executado no chamado espaço do kernel, enquanto os aplicativos são executados em . Este conceito é a base da teoria dos sistemas operacionais.

Um dos principais objetivos do sistema operacional é fornecer ao usuário e aos programas do usuário recursos de computador, a maioria dos quais representados por dispositivos externos. O sistema operacional deve não apenas fornecer acesso aos recursos, mas também controlar sua alocação e utilização, evitando colisões e acessos não autorizados. Além disso, sistema operacional pode criar operações independentes para programas e proteger contra acesso não autorizado a recursos. Resolver este problema não trivial só é possível se o processador proteger os programas do sistema dos aplicativos do usuário.

Quase todo processador moderno é capaz de fornecer essa separação implementando diferentes níveis de privilégios para o código em execução (são necessários pelo menos dois níveis). Por exemplo, os processadores da arquitetura I32 possuem quatro níveis de privilégios de 0 a 3. Além disso, o nível 0 possui os privilégios mais altos. Para tais processadores, existe uma classe de instruções privilegiadas que só podem ser executadas em níveis privilegiados. Os sistemas Unix usam dois níveis de privilégios de processador. Se um processador tiver mais de dois níveis de privilégio, o mais baixo e o mais alto serão usados. O kernel Unix é executado em mais alto nível privilégios, garantindo o controle dos equipamentos e processos do usuário.

Quando falamos sobre espaço do kernel E espaço de processo do usuário Isto significa não apenas diferentes níveis de privilégios para o código executável, mas também diferentes espaços de endereço.

O Unix transfere a execução do espaço do processo do usuário para o espaço do kernel em dois casos. Em primeiro lugar, quando um aplicativo de usuário faz uma chamada ao kernel (chamada de sistema) e, em segundo lugar, durante a manutenção de interrupções de hardware. O código do kernel executado durante uma chamada do sistema é executado no contexto de um processo, ou seja trabalhando em nome do processo de chamada, ele tem acesso aos dados do espaço de endereço do processo. Por outro lado, o código executado ao atender uma interrupção de hardware é assíncrono em relação ao processo e não pertence a nenhum processo especial.

O objetivo dos módulos é expandir a funcionalidade do kernel. O código do módulo é executado no espaço do kernel. Normalmente, um módulo executa ambas as tarefas mencionadas anteriormente: algumas funções do módulo são executadas como parte das chamadas do sistema e algumas são responsáveis ​​pelo gerenciamento de interrupções.

Paralelização no kernel

Ao programar drivers de dispositivos, em oposição à programação de aplicativos, a questão da paralelização do código executável é especialmente aguda. Normalmente, um aplicativo é executado sequencialmente do início ao fim, sem se preocupar com alterações em seu ambiente. O código do kernel deve funcionar com o entendimento de que pode ser acessado várias vezes ao mesmo tempo.

Existem muitos motivos para paralelizar o código do kernel. O Linux normalmente tem muitos processos em execução e alguns deles podem tentar acessar o código do seu módulo ao mesmo tempo. Muitos dispositivos podem causar interrupções de hardware no processador. Os manipuladores de interrupção são chamados de forma assíncrona e podem ser chamados enquanto o driver está executando outra solicitação. Algumas abstrações de software (como temporizadores de kernel, explicados no Capítulo 6, “Fluxo de tempo”) também são executadas de forma assíncrona. Além disso, o Linux pode ser executado em um sistema com multiprocessadores simétricos (SMP), o que significa que o código do driver pode ser executado em paralelo em vários processadores ao mesmo tempo.

Por estas razões, o código do kernel Linux, incluindo o código do driver, deve ser reentrante, ou seja, deve ser capaz de trabalhar com mais de um contexto de dados ao mesmo tempo. As estruturas de dados devem ser projetadas para acomodar a execução paralela de múltiplos threads. Por sua vez, o código do kernel deve ser capaz de lidar com vários fluxos de dados paralelos sem danificá-los. Escrever código que possa ser executado em paralelo e evitar situações nas quais uma sequência de execução diferente levaria a um comportamento indesejável do sistema requer muito tempo e talvez muitos truques. Cada exemplo de driver neste livro foi escrito tendo em mente a execução paralela. Se necessário, explicaremos as especificidades da técnica para escrever tal código.

Maioria erro geral O problema que os programadores criam é que eles presumem que a simultaneidade não é um problema porque alguns segmentos de código não conseguem dormir. Na verdade, o kernel do Linux não é paginado, com a importante exceção dos manipuladores de interrupção, que não podem adquirir a CPU enquanto o código crítico do kernel está em execução. Ultimamente, a não paginabilidade tem sido suficiente para evitar paralelização indesejada na maioria dos casos. Em sistemas SMP, entretanto, o download do código não é necessário devido à computação paralela.

Se o seu código assumir que não será descarregado, ele não funcionará corretamente em sistemas SMP. Mesmo que você não tenha esse sistema, alguém que use seu código poderá ter um. Também é possível que no futuro o kernel utilize a paginação, de modo que mesmo os sistemas de processador único terão que lidar com a simultaneidade o tempo todo. Já existem opções para implementar tais kernels. Assim, um programador prudente escreverá o código do kernel presumindo que ele será executado em um sistema executando SMP.

Observação tradutor: Desculpe, mas os dois últimos parágrafos não estão claros para mim. Isto pode ser o resultado de um erro de tradução. Portanto, apresento o texto original.

Um erro comum cometido por programadores de drivers é assumir que a simultaneidade não é um problema, desde que um determinado segmento de código
não vá dormir (ou "bloqueie"). É verdade que o kernel do Linux não é preemptivo; com a importante exceção de
atendendo interrupções, ele não afastará o processador do código do kernel que não cede voluntariamente. No passado, esse comportamento não-preemptivo
o comportamento foi suficiente para evitar simultaneidade indesejada na maioria das vezes. Em sistemas SMP, entretanto, a preempção não é necessária para causar
execução simultânea.

Se o seu código assumir que não será preemptado, ele não será executado corretamente em sistemas SMP. Mesmo se você não tiver esse sistema,
outras pessoas que executam seu código podem ter um. No futuro, também é possível que o kernel passe para um modo de operação preventivo,
ponto em que até mesmo os sistemas uniprocessados ​​terão que lidar com a simultaneidade em todos os lugares (algumas variantes do kernel já implementam
isto).

Informações sobre o processo atual

Embora o código do módulo do kernel não seja executado sequencialmente como os aplicativos, a maioria das chamadas ao kernel são executadas em relação ao processo que o chama. O código do kernel pode identificar o processo que o chamou acessando um ponteiro global que aponta para a estrutura estrutura tarefa_struct, definido para kernels versão 2.4, no arquivo incluído em . Ponteiro atual indica o processo do usuário atualmente em execução. Ao executar chamadas do sistema como abrir() ou fechar(), deve haver um processo que os causou. O código do kernel pode, se necessário, chamar informações específicas sobre o processo de chamada por meio de um ponteiro atual. Para obter exemplos de como usar esse ponteiro, consulte a seção “Controle de acesso a arquivos do dispositivo” no Capítulo 5, “Operações aprimoradas do driver Char”.

Hoje, o índice atual não é mais uma variável global, como nas versões anteriores do kernel. Os desenvolvedores otimizaram o acesso à estrutura que descreve o processo atual, movendo-o para a página da pilha. Você pode ver os detalhes da implementação atual no arquivo . O código que você vê aí pode não parecer simples para você. Tenha em mente que o Linux é um sistema centrado em SMP e uma variável global simplesmente não funcionará quando você estiver lidando com múltiplas CPUs. Os detalhes da implementação permanecem ocultos para outros subsistemas do kernel e o driver do dispositivo pode acessar o ponteiro atual somente através da interface .

Do ponto de vista do módulo, atual parece um link externo imprimir(). O módulo pode usar atual sempre que necessário. Por exemplo, o código a seguir imprime o ID do processo (PID) e o nome do comando do processo que chamou o módulo, obtendo-os através dos campos correspondentes da estrutura estrutura tarefa_struct:

Printk("O processo é \"%s\" (pid %i)\n", atual->comm, atual->pid);

O campo current->comm é o nome do arquivo de comando que gerou o processo atual.

Compilando e carregando módulos

O restante deste capítulo é dedicado a escrever um módulo completo, embora atípico. Aqueles. O módulo não pertence a nenhuma das classes descritas na seção “Classes de dispositivos e módulos” no Capítulo 1, “Introdução aos drivers de dispositivos”. O driver de exemplo mostrado neste capítulo será chamado de Skull (Simple Kernel Utility for Load Localities). Você pode usar o módulo scull como modelo para escrever seu próprio código local.

Usamos o conceito de “código local” (local) para enfatizar suas alterações de código pessoal, na boa e velha tradição Unix (/usr/local).

No entanto, antes de preenchermos as funções init_module() e cleanup_module(), escreveremos um script Makefile que make usará para construir o código-objeto do módulo.

Antes que o pré-processador possa processar a inclusão de qualquer arquivo de cabeçalho, o símbolo da macro __KERNEL__ deve ser definido com uma diretiva #define. Conforme mencionado anteriormente, um contexto específico do kernel pode ser definido nos arquivos de interface do kernel, visível apenas se o símbolo __KERNEL__ for pré-definido no pré-processamento.

Outro símbolo importante definido pela diretiva #define é o símbolo MODULE. Deve ser definido antes de ativar a interface (excluindo os drivers que serão compilados com o kernel). Os drivers montados no kernel não serão descritos neste livro, portanto o símbolo MODULE estará presente em todos os nossos exemplos.

Se você estiver construindo um módulo para um sistema com SMP, também precisará definir o símbolo da macro __SMP__ antes de ativar as interfaces do kernel. Na versão 2.2 do kernel, um item separado na configuração do kernel introduziu uma escolha entre um sistema de processador único e um sistema multiprocessador. Portanto, incluir as linhas a seguir como as primeiras linhas do seu módulo resultará em suporte multiprocessador.

#incluir #ifdef CONFIG_SMP # define __SMP__ #endif

Os desenvolvedores de módulos também devem definir o sinalizador de otimização -O para o compilador porque muitas funções são declaradas inline nos arquivos de cabeçalho do kernel. O compilador gcc não executa expansão embutida em funções, a menos que a otimização esteja habilitada. Permitir que as substituições embutidas sejam estendidas usando as opções -g e -O permitirá que você depure posteriormente o código que usa funções embutidas no depurador. Como o kernel faz uso extensivo de funções inline, é muito importante que elas sejam estendidas corretamente.

Observe, entretanto, que usar qualquer otimização acima do nível -O2 é arriscado porque o compilador pode estender funções que não são declaradas inline. Isso pode causar problemas porque... Algum código de função espera encontrar a pilha padrão de sua chamada. Uma extensão inline é entendida como a inserção de um código de função no ponto de sua chamada, em vez da instrução de chamada de função correspondente. Conseqüentemente, neste caso, como não há chamada de função, então não há pilha de sua chamada.

Talvez seja necessário garantir que você esteja usando o mesmo compilador para compilar os módulos que foi usado para construir o kernel no qual o módulo será instalado. Para obter detalhes, consulte o documento original do arquivo Documentação/Alterações localizado no diretório de fontes do kernel. O desenvolvimento do kernel e do compilador normalmente é sincronizado entre as equipes de desenvolvimento. Pode haver casos em que a atualização de um destes elementos revele erros em outro. Alguns fabricantes de distribuição fornecem versões ultranovas do compilador que não correspondem ao kernel que estão usando. Neste caso, eles geralmente fornecem um pacote separado (geralmente chamado kgcc) com um compilador projetado especificamente para
compilação do kernel.

Finalmente, para evitar erros desagradáveis, sugerimos que você use a opção de compilação -Parede(todos os avisos - todos os avisos). Para satisfazer todos esses avisos, pode ser necessário alterar seu estilo de programação habitual. Ao escrever o código do kernel, é preferível usar o estilo de codificação proposto por Linus Torvalds. Sim, documento Documentação/Estilo de codificação, do diretório fonte do kernel, é bastante interessante e recomendado para todos aqueles interessados ​​em programação em nível de kernel.

Recomenda-se colocar um conjunto de flags de compilação de módulo, com os quais nos familiarizamos recentemente, em uma variável CFLAGS seu Makefile. Para o utilitário make, esta é uma variável especial, cujo uso ficará claro na descrição a seguir.

Além dos sinalizadores na variável CFLAGS, você pode precisar de um destino em seu Makefile que combine diferentes arquivos de objetos. Tal objetivo só é necessário quando o código do módulo é dividido em vários arquivos fonte, o que geralmente não é incomum. Arquivos de objeto são combinados com o comando ld -r, que não é uma operação de ligação no sentido geralmente aceito, apesar do uso de um linker( ld). O resultado da execução do comando ld -ré outro arquivo objeto que combina os códigos objeto dos arquivos de entrada do vinculador. Opção -r significa " relocável - relocabilidade”, ou seja Movemos o arquivo de saída do comando no espaço de endereço, porque ainda não contém endereços absolutos de chamadas de função.

O exemplo a seguir mostra o Makefile mínimo necessário para compilar um módulo que consiste em dois arquivos de origem. Se o seu módulo consiste em um único arquivo de origem, então, a partir do exemplo dado, você precisa remover o destino que contém o comando ld -r.

# O caminho para o diretório fonte do seu kernel pode ser alterado aqui, # ou você pode passá-lo como parâmetro ao chamar “make” KERNELDIR = /usr/src/linux include $(KERNELDIR)/.config CFLAGS = -D__KERNEL__ -DMODULE - I$(KERNELDIR) /include \ -O -Wall ifdef CONFIG_SMP CFLAGS += -D__SMP__ -DSMP endif all: crânio.o crânio.o: crânio_init.o crânio_clean.o $(LD) -r $^ -o $@ limpo : rm -f * .o *~ núcleo

Se você é novo no funcionamento do make, poderá se surpreender ao saber que não existem regras para compilar arquivos *.c em arquivos objeto *.o. Não é necessário definir tais regras, porque o utilitário make, se necessário, converte arquivos *.c em arquivos *.o usando o compilador padrão ou o compilador especificado por uma variável $(CC). Neste caso, o conteúdo da variável $(CFLAGS) usado para especificar sinalizadores de compilação.

A próxima etapa após construir um módulo é carregá-lo no kernel. Já dissemos que para isso utilizaremos o utilitário insmod, que associa todos os símbolos indefinidos (chamadas de função, etc.) do módulo à tabela de símbolos do kernel em execução. No entanto, ao contrário de um vinculador (por exemplo, como ld), ele não altera o arquivo do disco do módulo, mas carrega o objeto do módulo vinculado ao kernel na RAM. O utilitário insmod pode aceitar algumas opções de linha de comando. Detalhes podem ser visualizados através homem insmod. Usando essas opções, você pode, por exemplo, atribuir variáveis ​​inteiras e de string específicas em seu módulo a valores especificados antes de vincular o módulo ao kernel. Assim, se o módulo for projetado corretamente, ele poderá ser configurado na inicialização. Este método de configuração de um módulo oferece ao usuário maior flexibilidade do que a configuração em tempo de compilação. A configuração durante a inicialização é explicada na seção “Configuração manual e automática”, posteriormente neste capítulo.

Alguns leitores estarão interessados ​​nos detalhes de como funciona o utilitário insmod. A implementação do insmod é baseada em diversas chamadas de sistema definidas em kernel/module.c. A função sys_create_module() aloca a quantidade necessária de memória no espaço de endereço do kernel para carregar o módulo. Esta memória é alocada usando a função vmalloc() (veja a seção “vmalloc e amigos” no Capítulo 7, “Obtendo Memória”). A chamada do sistema get_kernel_sysms() retorna a tabela de símbolos do kernel, que será usada para determinar os endereços reais dos objetos durante a vinculação. A função sys_init_module() copia o código do objeto do módulo no espaço de endereço do kernel e chama a função de inicialização do módulo.

Se você observar as fontes do código do kernel, encontrará nomes de chamadas de sistema que começam com o prefixo sys_. Este prefixo é usado apenas para chamadas de sistema. Nenhuma outra função deve usá-lo. Tenha isso em mente ao processar fontes de código do kernel com o utilitário de pesquisa grep.

Dependências de versão

Se você não sabe nada além do que é abordado aqui, então provavelmente os módulos que você criar terão que ser recompilados para cada versão do kernel ao qual estão vinculados. Cada módulo deve definir um símbolo chamado __module_kernel_versão, cujo valor
é comparado com a versão do kernel atual usando o utilitário insmod. Este símbolo está localizado na seção .modinfo Arquivos ELF (formato executável e de link). Isso é explicado com mais detalhes no Capítulo 11 “kmod e Modularização Avançada”. Observe que este método de controle de versão só é aplicável às versões 2.2 e 2.4 do kernel. No kernel 2.0 isso é feito de uma maneira um pouco diferente.

O compilador definirá este símbolo sempre que o arquivo de cabeçalho estiver incluído . Portanto, no exemplo hello.c dado anteriormente, não descrevemos este símbolo. Isso também significa que se o seu módulo consistir em muitos arquivos de origem, você deverá incluir o arquivo em seu código apenas uma vez. Uma exceção é o caso ao usar a definição __NÃO_VERSÃO__, que conheceremos mais tarde.

Abaixo está a definição do símbolo descrito do arquivo module.h extraído do código do kernel 2.4.25.

Char const estático __module_kernel_versio/PRE__attribute__((section(".modinfo"))) = "kernel_version=" UTS_RELEASE;

Se um módulo falhar ao carregar devido a uma incompatibilidade de versão, você pode tentar carregar este módulo passando a chave insmod para a linha de parâmetro do utilitário -f(força). Este método de carregar um módulo não é seguro e nem sempre é bem-sucedido. É muito difícil explicar os motivos de possíveis falhas. É possível que o módulo não carregue porque os símbolos não podem ser resolvidos durante a vinculação. Nesse caso, você receberá uma mensagem de erro apropriada. As razões para a falha também podem estar em mudanças na operação ou na estrutura do kernel. Nesse caso, carregar o módulo pode causar sérios erros de execução, bem como pânico no sistema. Este último deverá servir como um bom incentivo à utilização de um sistema de controlo de versões. As incompatibilidades de versão podem ser tratadas de maneira mais elegante usando o controle de versão no kernel. Falaremos sobre isso em detalhes na seção “Controle de versão em módulos” no Capítulo 11 “kmod e modularização avançada”.

Se você deseja compilar seu módulo para uma versão específica do kernel, você deve incluir os arquivos de cabeçalho para essa versão específica do kernel. No exemplo Makefile descrito acima, a variável foi usada para determinar o diretório para esses arquivos KERNELDIR. Essa compilação personalizada não é incomum quando as fontes do kernel estão disponíveis. Além disso, não é incomum que existam versões diferentes do kernel na árvore de diretórios. Todos os exemplos de módulos neste livro usam a variável KERNELDIR para indicar a localização do diretório de origem da versão do kernel ao qual o módulo montado deve estar vinculado. Você pode usar uma variável de sistema para especificar esse diretório ou pode passar sua localização por meio de opções de linha de comando para criar.

Ao carregar um módulo, o utilitário insmod usa seus próprios caminhos de pesquisa para os arquivos de objeto do módulo, examinando diretórios dependentes de versão começando em /lib/módulos. E embora as versões mais antigas do utilitário incluíssem o diretório atual no caminho de pesquisa, esse comportamento agora é considerado inaceitável por razões de segurança (os mesmos problemas do uso da variável de sistema CAMINHO). Então se você quiser carregar um módulo do diretório atual você pode especificá-lo no estilo ./module.o. Esta indicação da posição do módulo funcionará para qualquer versão do utilitário insmod.

Às vezes você pode encontrar interfaces de kernel diferentes entre 2.0.xe 2.4.x. Neste caso, você precisará recorrer a uma macro que determine a versão atual do kernel. Esta macro está localizada no arquivo de cabeçalho . Indicaremos casos de diferenças nas interfaces ao utilizá-las. Isso pode ser feito imediatamente ao longo da descrição ou no final da seção, em uma seção especial dedicada às dependências de versão. Em alguns casos, colocar os detalhes em uma seção separada permitirá evitar complicar a descrição da versão 2.4.x do kernel que é relevante para este livro.

No arquivo de cabeçalho linux/versão.h As macros a seguir são definidas em relação à determinação da versão do kernel.

UTS_RELEASE Macro que se expande em uma string descrevendo a versão atual do kernel
árvore de origem. Por exemplo, uma macro pode se expandir para algo assim:
linha: "2.3.48" . LINUX_VERSION_CODE Esta macro se expande para uma representação binária da versão do kernel, por
um byte para cada parte do número. Por exemplo, binário
representação para a versão 2.3.48 será 131888 (decimal
representação para hexadecimal 0x020330). Possivelmente binário
Você achará a representação mais conveniente do que a representação de string. Observe o que é
representação permite descrever no máximo 256 opções em cada
partes do número. KERNEL_VERSION(maior, menor, lançamento) Esta definição de macro permite que você construa “kernel_version_code”
dos elementos individuais que compõem a versão do kernel. Por exemplo,
próxima macro KERNEL_VERSION(2, 3, 48)
será expandido para 131888. Esta definição de macro é muito conveniente quando
comparando a versão atual do kernel com a necessária. Estaremos repetidamente
use esta definição de macro ao longo do livro.

Aqui está o conteúdo do arquivo: linux/versão.h para o kernel 2.4.25 (o texto do arquivo de cabeçalho é fornecido na íntegra).

#define UTS_RELEASE "2.4.25" #define LINUX_VERSION_CODE 132121 #define KERNEL_VERSION(a,b,c) (((a)<< 16) + ((b) << 8) + (c))

O arquivo de cabeçalho version.h está incluído no arquivo module.h, portanto, geralmente você não precisa incluir version.h explicitamente no código do módulo. Por outro lado, você pode evitar que o arquivo de cabeçalho version.h seja incluído em module.h declarando uma macro __NÃO_VERSÃO__. Você vai usar __NÃO_VERSÃO__, por exemplo, no caso em que você precisa ativar em vários arquivos de origem, que serão posteriormente vinculados em um módulo. Anúncio __NÃO_VERSÃO__ antes de incluir o arquivo de cabeçalho module.h evita
descrição automática da string __module_kernel_versão ou seu equivalente em arquivos de origem. Você pode precisar disso para satisfazer as reclamações do vinculador quando ld -r, que não gostará de múltiplas descrições de símbolos em tabelas de links. Normalmente, se o código do módulo for dividido em vários arquivos de origem, incluindo um arquivo de cabeçalho , então o anúncio __NÃO_VERSÃO__é feito em todos esses arquivos, exceto um. No final do livro há um exemplo de módulo que utiliza __NÃO_VERSÃO__.

A maioria das dependências de versão do kernel podem ser tratadas usando lógica construída em diretivas de pré-processador usando definições de macro KERNEL_VERSION E LINUX_VERSION_CODE. No entanto, verificar as dependências da versão pode complicar bastante a legibilidade do código do módulo devido a diretivas heterogêneas #ifdef. Portanto, talvez a melhor solução seja colocar a verificação de dependência em um arquivo de cabeçalho separado. É por isso que nosso exemplo inclui um arquivo de cabeçalho sysdep.h, usado para armazenar todas as definições de macro associadas às verificações de dependência de versão.

A primeira dependência de versão que queremos representar está na declaração de destino" fazer instalar" nosso script de compilação de driver. Como seria de esperar, o diretório de instalação, que muda de acordo com a versão do kernel usada, é selecionado com base na visualização do arquivo version.h. Aqui está um trecho de código do arquivo Regras.make, que é usado por todos os Makefiles do kernel.

VERSIONFILE = $(INCLUDEDIR)/linux/version.h VREION = $(shell awk -F\" "/REL/ (print $$2)" $(VERSIONFILE)) INSTALLDIR = /lib/modules/$(VERSION)/misc

Observe que usamos o diretório misc para instalar todos os nossos drivers (a declaração INSTALLDIR no Makefile de exemplo acima). A partir da versão 2.4 do kernel, este diretório é o diretório recomendado para colocar drivers personalizados. Além disso, as versões antigas e novas do pacote modutils contêm um diretório misc em seus caminhos de pesquisa.

Usando a definição INSTALLDIR acima, o destino de instalação no Makefile pode ser assim:

Instalar: instalar -d $(INSTALLDIR) instalar -c $(OBJS) $(INSTALLDIR)

Dependência de plataforma

Cada plataforma de computador possui características próprias que devem ser levadas em consideração pelos desenvolvedores do kernel para obter o mais alto desempenho.

Os desenvolvedores de kernel têm muito mais liberdade de escolha e tomada de decisão do que os desenvolvedores de aplicativos. É essa liberdade que permite otimizar seu código, aproveitando ao máximo cada plataforma específica.

O código do módulo deve ser compilado usando as mesmas opções do compilador que foram usadas para compilar o kernel. Isso se aplica ao uso dos mesmos padrões de uso de registro do processador e à execução do mesmo nível de otimização. Arquivo Regras.make, localizado na raiz da árvore fonte do kernel, inclui definições específicas da plataforma que devem ser incluídas em todos os Makefiles de compilação. Todos os scripts de compilação específicos da plataforma são chamados de Makefiles. plataforma e contém os valores das variáveis ​​​​do utilitário make de acordo com a configuração atual do kernel.

Outra característica interessante do Makefile é o suporte para plataforma cruzada ou simplesmente compilação cruzada. Este termo é usado quando você precisa compilar código para outra plataforma. Por exemplo, usando a plataforma i86 você criará código para a plataforma M68000. Se você for fazer compilação cruzada, precisará substituir suas ferramentas de compilação ( gcc, ld, etc.) com outro conjunto de ferramentas correspondentes
(Por exemplo, m68k-linux-gcc, m68k-linux-ld). O prefixo usado pode ser especificado pela variável $(CROSS_COMPILE) Makefile, por uma opção de linha de comando para o utilitário make ou por uma variável de ambiente do sistema.

A arquitetura SPARC é um caso especial que deve ser tratado adequadamente no Makefile. Os programas de usuário executados na plataforma SPARC64 (SPARC V9) são binários, geralmente projetados para a plataforma SPARC32 (SPARC V8). Portanto, o compilador padrão na plataforma SPARC64 (gcc) gera código objeto para SPARC32. Por outro lado, um kernel projetado para rodar em SPARC V9 deve conter código objeto para SPARC V9, portanto, mesmo assim, é necessário um compilador cruzado. Todas as distribuições GNU/Linux projetadas para SPARC64 incluem um compilador cruzado apropriado, que deve ser selecionado no Makefile para o script de compilação do kernel.

E embora a lista completa de dependências de versão e plataforma seja um pouco mais complexa do que a descrita aqui, é suficiente para realizar a compilação cruzada. Para obter mais informações, você pode consultar os scripts de compilação Makefile e os arquivos fonte do kernel.

Recursos do kernel 2.6

O tempo não pára. E agora estamos testemunhando o surgimento de uma nova geração do kernel 2.6. Infelizmente, o original deste livro não cobre o novo núcleo, portanto o tradutor tomará a liberdade de complementar a tradução com novos conhecimentos.

Você pode usar ambientes de desenvolvimento integrados, como o TimeStorm da TimeSys, que gerará corretamente o esqueleto e o script de compilação para o seu módulo, dependendo da versão necessária do kernel. Se você for escrever tudo isso sozinho, precisará de algumas informações adicionais sobre as principais diferenças introduzidas pelo novo kernel.

Uma das características do kernel 2.6 é a necessidade de usar as macros module_init() e module_exit() para registrar explicitamente os nomes das funções de inicialização e encerramento.

A macro MODULE_LISENCE(), introduzida no kernel 2.4, ainda é necessária se você não quiser ver os avisos correspondentes ao carregar um módulo. Você pode selecionar as seguintes sequências de licença a serem transferidas para a macro: “GPL”, “GPL v2”, “GPL e direitos adicionais”, “Dual BSD/GPL” (escolha entre licenças BSD ou GPL), “Dual MPL/GPL " (escolha entre licenças Mozilla ou GPL) e
"Proprietário".

Mais significativo para o novo kernel é um novo esquema de compilação de módulo, que implica não apenas mudanças no código do módulo em si, mas também no script Makefile para sua compilação.

Assim, a definição do símbolo da macro MODULE não é mais necessária nem no código do módulo nem no Makefile. Se necessário, o próprio novo esquema de compilação determinará este macrosímbolo. Além disso, você não precisará definir explicitamente os macrosímbolos __KERNEL__ ou os mais recentes como KBUILD_BASENAME e KBUILD_MODNAME.

Além disso, você não deve especificar o nível de otimização na compilação (-O2 ou outros), porque seu módulo será compilado com todo o conjunto de flags, incluindo flags de otimização, com os quais todos os outros módulos do seu kernel são compilados - o utilitário make usa automaticamente todo o conjunto de flags necessário.

Por estas razões, o Makefile para compilar um módulo para o kernel 2.6 é muito mais simples. Portanto, para o módulo hello.c, o Makefile ficará assim:

Obj-m:= olá.o

Entretanto, para compilar o módulo, você precisará de acesso de gravação à árvore de origem do kernel, onde serão criados arquivos e diretórios temporários. Portanto, o comando para compilar um módulo para o kernel 2.6, especificado no diretório atual que contém o código-fonte do módulo, deve ser semelhante a este:

# make -C /usr/src/linux-2.6.1 SUBDIRS=`pwd` módulos

Então, temos a fonte do módulo olá-2.6.c, para compilação no kernel 2.6:

//hello-2.6.c #include #incluir #incluir MODULE_LICENSE("GPL"); static int __init my_init(void) ( printk("Olá mundo\n"); return 0; ); static void __exit my_cleanup(void) ( printk("Adeus\n"); ); módulo_init(meu_init); module_exit(minha_limpeza);

Assim, temos um Makefile:

Obj-m:= olá-2.6.o

Chamamos o utilitário make para processar nosso Makefile com os seguintes parâmetros:

# make -C/usr/src/linux-2.6.3 SUBDIRS=`pwd` módulos

O processo normal de compilação produzirá a seguinte saída padrão:

Make: Entre no diretório `/usr/src/linux-2.6.3" *** Aviso: Substituir SUBDIRS na linha de comando pode causar *** inconsistências make: `arch/i386/kernel/asm-offsets.s" não requer atualização. CHK include/asm-i386/asm_offsets.h CC [M] /home/knz/j.kernel/3/hello-2.6.o Construindo módulos, estágio 2. /usr/src/linux-2.6.3/scripts/Makefile .modpost:17: *** Uh-oh, você tem entradas de módulo obsoletas. Você mexeu com SUBDIRS, /usr/src/linux-2.6.3/scripts/Makefile.modpost:18: não reclame se algo der errado. MODPOST CC /home/knz/j.kernel/3/hello-2.6.mod.o LD [M] /home/knz/j.kernel/3/hello-2.6.ko make: Sair do diretório `/usr/src / linux-2.6.3"

O resultado final da compilação será um arquivo de módulo hello-2.6.ko que pode ser instalado no kernel.

Observe que no kernel 2.6, os arquivos de módulo têm o sufixo .ko em vez de .o como no kernel 2.4.

Tabela de símbolos do kernel

Já falamos sobre como o utilitário insmod usa a tabela de símbolos públicos do kernel ao vincular um módulo ao kernel. Esta tabela contém os endereços dos objetos globais do kernel - funções e variáveis ​​- que são necessários para implementar opções de driver modular. A tabela de símbolos públicos do kernel pode ser lida em formato de texto no arquivo /proc/ksyms, desde que seu kernel suporte o sistema de arquivos /proc.

No kernel 2.6, /proc/ksyms foi renomeado para /proc/modules.

Quando um módulo é carregado, os símbolos exportados pelo módulo tornam-se parte da tabela de símbolos do kernel e você pode visualizá-los em /proc/ksyms.

Novos módulos podem usar símbolos exportados pelo seu módulo. Por exemplo, o módulo msdos depende de caracteres exportados pelo módulo fat, e cada dispositivo USB usado no modo de leitura usa caracteres dos módulos usbcore e de entrada. Esse relacionamento, realizado pelo carregamento sequencial de módulos, é chamado de pilha de módulos.

A pilha de módulos é conveniente para usar ao criar projetos de módulos complexos. Essa abstração é útil para separar o código do driver de dispositivo em partes dependentes e independentes de hardware. Por exemplo, o conjunto de drivers de vídeo para Linux consiste em um módulo principal que exporta símbolos para um driver de baixo nível que leva em consideração as especificidades do hardware que está sendo usado. De acordo com a sua configuração, você carrega o módulo de vídeo principal e um módulo específico para o seu hardware. Da mesma forma, é implementado suporte para portas paralelas e uma ampla classe de dispositivos conectados, como dispositivos USB. A pilha do sistema de portas paralelas é mostrada na Fig. 2-2. As setas mostram a interação entre os módulos e a interface de programação do kernel. A interação pode ser realizada tanto no nível das funções quanto no nível das estruturas de dados gerenciadas pelas funções.

Figura 2-2. Pilha de módulos de porta paralela

Ao usar módulos de pilha, é conveniente usar o utilitário modprobe. A funcionalidade do utilitário modprobe é em muitos aspectos semelhante ao utilitário insmod, mas ao carregar um módulo, ele verifica suas dependências subjacentes e, se necessário, carrega os módulos necessários até que a pilha de módulos necessária seja preenchida. Assim, um comando modprobe pode resultar em múltiplas chamadas para o comando insmod. Você poderia dizer que o comando modprobe é um wrapper inteligente em torno do insmod. Você pode usar modprobe em vez de insmod em qualquer lugar, exceto ao carregar seus próprios módulos do diretório atual, porque modprobe analisa apenas diretórios de módulos específicos e não será capaz de satisfazer possíveis dependências.

Dividir os módulos em partes ajuda a reduzir o tempo de desenvolvimento, simplificando a definição do problema. Isso é semelhante à separação entre mecanismo de implementação e política de controle, discutida no Capítulo 1, “Introdução aos drivers de dispositivos”.

Normalmente, um módulo implementa sua funcionalidade sem a necessidade de exportar símbolos. Você precisará exportar símbolos se outros módulos puderem se beneficiar disso. Talvez seja necessário incluir uma diretiva especial para impedir a exportação de caracteres não estáticos, porque A maioria das implementações de modutils exporta todos eles por padrão.

Os arquivos de cabeçalho do kernel Linux oferecem uma maneira conveniente de controlar a visibilidade de seus símbolos, evitando assim que o namespace da tabela de símbolos do kernel seja poluído. O mecanismo descrito neste capítulo funciona em kernels a partir da versão 2.1.18. Kernel 2.0 tinha um mecanismo de controle completamente diferente
visibilidade do símbolo, que será descrita no final do capítulo.

Se o seu módulo não precisar exportar símbolos, você poderá colocar explicitamente a seguinte chamada de macro no arquivo de origem do módulo:

EXPORT_NO_SYMBOLS;

Esta chamada de macro, definida no arquivo linux/module.h, se expande em uma diretiva assembler e pode ser especificada em qualquer lugar do módulo. Porém, ao criar código que seja portável para diferentes kernels, é necessário colocar esta chamada de macro na função de inicialização do módulo (init_module), pois a versão desta macro que definimos em nosso arquivo sysdep.h para versões mais antigas do kernel só funcionará aqui.

Por outro lado, se você precisar exportar alguns símbolos do seu módulo, você precisará usar um símbolo macro
EXPORT_SYMTAB. Este símbolo macro deve ser definido antes incluindo o arquivo de cabeçalho module.h. É prática comum
definindo este caractere macro através de um sinalizador -D no Makefile.

Se o símbolo macro EXPORT_SYMTAB definido, então símbolos individuais podem ser exportados usando um par de macros:

EXPORT_SYMBOL(nome); EXPORT_SYMBOL_NOVERS(nome);

Qualquer uma dessas duas macros disponibilizará o símbolo fornecido fora do módulo. A diferença é que a macro EXPORT_SYMBOL_NOVERS exporta o símbolo sem informações de versão (consulte o Capítulo 11 “kmod e Modularização Avançada”). Para mais detalhes
confira o arquivo de cabeçalho , embora o que é afirmado seja suficiente para uso prático
macros.

Inicializando e Concluindo Módulos

Conforme mencionado, a função init_module() registra os componentes funcionais de um módulo com o kernel. Após esse cadastro, a aplicação que utiliza o módulo terá acesso aos pontos de entrada do módulo através da interface fornecida pelo kernel.

Os módulos podem registrar muitos componentes diferentes, que, quando registrados, são os nomes das funções do módulo. Um ponteiro para uma estrutura de dados contendo ponteiros para funções que implementam a funcionalidade proposta é passado para a função de registro do kernel.

No Capítulo 1, “Introdução aos Drivers de Dispositivos”, foi mencionada a classificação dos principais tipos de dispositivos. Você pode registrar não apenas os tipos de dispositivos mencionados lá, mas também quaisquer outros, até mesmo abstrações de software, como, por exemplo, arquivos /proc, etc. Tudo o que pode funcionar no kernel através da interface de programação do driver pode ser registrado como driver .

Se você quiser aprender mais sobre os tipos de drivers registrados usando seu kernel como exemplo, você pode implementar uma busca pela substring EXPORT_SYMBOL nas fontes do kernel e encontrar os pontos de entrada oferecidos pelos vários drivers. Via de regra, as funções de registro usam um prefixo em seu nome registro_,
então outra maneira possível de encontrá-los é procurar por uma substring registro_ no arquivo /proc/ksyms usando o utilitário grep. Como já mencionado, no kernel 2.6.x o arquivo /proc/ksyms foi substituído por /proc/modules.

Tratamento de erros em init_module

Se ocorrer algum tipo de erro durante a inicialização de um módulo, você deve desfazer a inicialização que já foi concluída antes de interromper o carregamento do módulo. O erro pode ocorrer, por exemplo, devido à memória insuficiente do sistema ao alocar estruturas de dados. Infelizmente, isso pode acontecer, e um bom código deve ser capaz de lidar com tais situações.

Tudo o que foi registrado ou alocado antes de ocorrer o erro na função de inicialização init_module() deve ser cancelado ou liberado, pois o kernel Linux não rastreia erros de inicialização e não desfaz empréstimos e concessões de recursos por código de módulo. Se você não reverteu ou não conseguiu reverter o registro concluído, o kernel permanecerá em um estado instável e quando o módulo for carregado novamente
não poderá repetir o registo de elementos já registados, e não poderá cancelar um registo anteriormente efetuado, pois na nova instância da função init_module() você não terá o valor correto dos endereços das funções cadastradas. Restaurar o sistema ao seu estado anterior exigirá o uso de vários truques complexos, e isso geralmente é feito simplesmente reiniciando o sistema.

A implementação da restauração do estado anterior do sistema quando ocorrem erros de inicialização do módulo é melhor implementada usando o operador goto. Normalmente este operador é tratado de forma extremamente negativa, e até com ódio, mas é nesta situação que ele acaba por ser muito útil. Portanto, no kernel, a instrução goto é frequentemente usada para lidar com erros de inicialização de módulo.

O código simples a seguir, usando funções fictícias de registro e cancelamento de registro como exemplo, demonstra essa maneira de lidar com erros.

Int init_module(void) ( int err; /* registro recebe um ponteiro e um nome */ err = registre_this(ptr1, "caveira"); if (err) vá para fail_this; err = registre_that(ptr2, "caveira"); if (err) vá para fail_that; err = registre_those(ptr3, "skull"); if (err) vá para fail_those; return 0; /* sucesso */ fail_those: unregister_that(ptr2, "skull"); fail_that: unregister_this(ptr1, " crânio"); fail_this: return err; /* propaga o erro */ )

Este exemplo tenta registrar três componentes do módulo. A instrução goto é usada quando ocorre um erro de registro e faz com que os componentes registrados sejam cancelados antes de interromper o carregamento do módulo.

Outro exemplo de uso de uma instrução goto para facilitar a leitura do código é o truque de “lembrar” registros de módulo bem-sucedidos e chamar cleanup_module() para passar essas informações quando ocorrer um erro. A função cleanup_module() foi projetada para reverter operações de inicialização concluídas e é chamada automaticamente quando o módulo é descarregado. O valor que a função init_module() retorna deve ser
representam o código de erro de inicialização do módulo. No kernel Linux, o código de erro é um número negativo de um conjunto de definições feitas no arquivo de cabeçalho . Inclua este arquivo de cabeçalho em seu módulo para usar mnemônicos simbólicos para códigos de erro reservados, como -ENODEV, -ENOMEM, etc. Usar esses mnemônicos é considerado um bom estilo de programação. No entanto, deve-se observar que algumas versões dos utilitários do pacote modutils não processam corretamente os códigos de erro retornados e exibem a mensagem “Dispositivo ocupado”
em resposta a todo um grupo de erros de natureza completamente diferente retornados pela função init_modules(). Nas versões mais recentes do pacote, isso
O bug irritante foi corrigido.

O código da função cleanup_module() para o caso acima poderia, por exemplo, ser assim:

Void cleanup_module(void) ( unregister_those(ptr3, "crânio"); unregister_that(ptr2, "crânio"); unregister_this(ptr1, "crânio"); return; )

Se o seu código de inicialização e encerramento for mais complexo do que o descrito aqui, o uso de uma instrução goto pode resultar em texto de programa difícil de ler porque o código de encerramento deve ser repetido na função init_module() usando vários rótulos para transições goto. Por esse motivo, um truque mais inteligente é usar uma chamada para a função cleanup_module() na função init_module(), passando informações sobre a extensão da inicialização bem-sucedida quando ocorre um erro de carregamento do módulo.

Abaixo está um exemplo de como escrever as funções init_module() e cleanup_module(). Este exemplo usa ponteiros definidos globalmente que transportam informações sobre o escopo da inicialização bem-sucedida.

Estruture algo *item1; struct algo mais *item2; int coisas_ok; void cleanup_module(void) ( if (item1) release_thing(item1); if (item2) release_thing2(item2); if (stuff_ok) unregister_stuff(); return; ) int init_module(void) ( int err = -ENOMEM; item1 = allocate_thing (argumentos); item2 = alocar_coisa2(argumentos2); if (!item2 || !item2) vai falhar; err = registrar_coisa(item1, item2); if (!err) coisas_ok = 1; senão vai falhar; return 0; /* sucesso */ falha: cleanup_module(); return err; )

Dependendo da complexidade das operações de inicialização do seu módulo, você pode querer usar um dos métodos listados aqui para controlar erros de inicialização do módulo.

Contador de uso do módulo

O sistema contém um contador de utilização para cada módulo para determinar se o módulo pode ser descarregado com segurança. O sistema precisa dessas informações porque um módulo não pode ser descarregado se estiver ocupado por alguém ou algo - você não pode remover um driver de sistema de arquivos se esse sistema de arquivos estiver montado ou não pode descarregar um módulo de dispositivo de caractere se algum processo usar este dispositivo. De outra forma,
isso pode levar a uma falha do sistema - falha de segmentação ou kernel panic.

Nos kernels modernos, o sistema pode fornecer um contador automático de uso de módulos usando um mecanismo que veremos no próximo capítulo. Independentemente da versão do kernel, você pode usar o controle manual deste contador. Assim, o código que deveria ser usado em versões mais antigas do kernel deve usar um modelo de contabilidade de uso de módulo construído nas três macros a seguir:

MOD_INC_USE_COUNT Incrementa o contador de uso do módulo atual MOD_DEC_USE_COUNT Diminui o contador de uso do módulo atual MOD_IN_USE Retorna verdadeiro se o contador de uso deste módulo for zero

Essas macros são definidas em e manipulam uma estrutura de dados interna especial à qual o acesso direto não é desejável. O fato é que a estrutura interna e a forma de gerenciamento desses dados podem mudar de versão para versão, enquanto a interface externa para utilização dessas macros permanece inalterada.

Observe que você não precisa verificar MOD_IN_USE no código da função cleanup_module(), porque essa verificação é executada automaticamente antes que cleanup_module() seja chamado na chamada do sistema sys_delete_module(), que é definida em kernel/module.c.

O gerenciamento correto do contador de uso do módulo é fundamental para a estabilidade do sistema. Lembre-se que o kernel pode decidir descarregar um módulo não utilizado automaticamente a qualquer momento. Um erro comum na programação do módulo é o controle incorreto deste contador. Por exemplo, em resposta a uma determinada solicitação, o código do módulo realiza algumas ações e, quando o processamento é concluído, aumenta o contador de utilização do módulo. Aqueles. tal programador assume que este contador se destina a coletar estatísticas de uso do módulo, embora, na verdade, seja um contador para a ocupação atual do módulo, ou seja, acompanha o número de processos usando o código do módulo no momento. Assim, ao processar uma solicitação para um módulo, você deve chamar MOD_INC_USE_COUNT antes de realizar qualquer ação, e MOD_DEC_USE_COUNT depois de concluídos.

Pode haver situações em que, por motivos óbvios, você não conseguirá descarregar um módulo se perder o controle do seu contador de utilização. Esta situação ocorre frequentemente na fase de desenvolvimento do módulo. Por exemplo, um processo pode ser abortado ao tentar desreferenciar um ponteiro NULL e você não poderá descarregar esse módulo até retornar seu contador de uso a zero. Uma das soluções possíveis para este problema na fase de depuração do módulo é abandonar completamente o controle do contador de utilização do módulo redefinindo MOD_INC_USE_COUNT E MOD_DEC_USE_COUNT em código vazio. Outra solução é criar uma chamada ioctl() que force o contador de uso do módulo a zero. Abordaremos isso na seção “Usando o argumento ioctl” no Capítulo 5, “Operações aprimoradas do driver Char”. É claro que em um driver pronto para uso, tais manipulações fraudulentas com o contador devem ser excluídas, porém, na fase de depuração, elas economizam tempo do desenvolvedor e são bastante aceitáveis.

Você encontrará o contador de uso atual do sistema para cada módulo no terceiro campo de cada entrada no arquivo /proc/modules. Este arquivo contém informações sobre os módulos carregados atualmente - uma linha por módulo. O primeiro campo da linha contém o nome do módulo, o segundo campo é o tamanho ocupado pelo módulo na memória e o terceiro campo é o valor atual do contador de uso. Essas informações, em formato formatado,
pode ser obtido chamando o utilitário lsmod. Abaixo está um exemplo de arquivo /proc/modules:

Parport_pc 7604 1 (autoclean) lp 4800 0 (não utilizado) parport 8084 1 bloqueado 33256 1 (autoclean) sunrpc 56612 1 (autoclean) ds 6252 1 i82365 22304 1 pcmcia_core 41280 0

Aqui vemos vários módulos carregados no sistema. No campo flags (o último campo da linha), a pilha de dependências do módulo é exibida entre colchetes. Entre outras coisas, você pode notar que os módulos de porta paralela se comunicam através de uma pilha de módulos, conforme mostrado na Fig. 2-2. O sinalizador (autoclean) marca os módulos controlados por kmod ou kerneld. Isso será abordado no Capítulo 11 “kmod e Modularização Avançada”). O sinalizador (não utilizado) significa que o módulo não está em uso no momento. No kernel 2.0, o campo de tamanho exibia informações não em bytes, mas em páginas, que para a maioria das plataformas tem tamanho de 4kB.

Descarregando um módulo

Para descarregar um módulo, use o utilitário rmmod. Descarregar um módulo é uma tarefa mais simples do que carregá-lo, o que envolve vinculá-lo dinamicamente ao kernel. Quando um módulo é descarregado, a chamada de sistema delete_module() é executada, que chama a função cleanup_module() do módulo descarregado se sua contagem de uso for zero ou termina com um erro.

Como já mencionado, a função cleanup_module() reverte as operações de inicialização realizadas ao carregar o módulo com a função cleanup_module(). Além disso, os símbolos dos módulos exportados são automaticamente excluídos.

Definir explicitamente funções de encerramento e inicialização

Como já mencionado, ao carregar um módulo o kernel chama a função init_module(), e ao descarregar ele chama cleanup_module(). No entanto, nas versões modernas do kernel, essas funções geralmente têm nomes diferentes. A partir do kernel 2.3.23, tornou-se possível definir explicitamente um nome para a função de carregar e descarregar um módulo. Hoje em dia, esta nomenclatura explícita destas funções é o estilo de programação recomendado.

Vamos dar um exemplo. Se você deseja declarar a função my_init() como a função de inicialização do seu módulo, e a função my_cleanup() como a função final, em vez de init_module() e cleanup_module(), respectivamente, então você precisará adicionar os dois seguintes macros para o texto do módulo (geralmente elas são inseridas no final
arquivo fonte do código do módulo):

Módulo_init(meu_init); module_exit(minha_limpeza);

Observe que para usar essas macros você precisará incluir um arquivo de cabeçalho em seu módulo .

A conveniência de usar esse estilo é que cada função de inicialização e encerramento de módulo no kernel pode ter seu próprio nome exclusivo, o que ajuda muito na depuração. Além disso, o uso dessas funções simplifica a depuração, independentemente de você implementar o código do driver como um módulo ou incorporá-lo diretamente no kernel. Obviamente, o uso das macros module_init e module_exit não é necessário se suas funções de inicialização e encerramento tiverem nomes reservados, ou seja, init_module() e cleanup_module() respectivamente.

Se você observar as fontes do kernel 2.2 ou posterior, poderá ver uma forma de descrição ligeiramente diferente para as funções de inicialização e encerramento. Por exemplo:

Static int __init my_init(void) ( .... ) static void __exit my_cleanup(void) ( .... )

Uso de atributos __iniciar fará com que a função de inicialização seja descarregada da memória após a conclusão da inicialização. No entanto, isso funciona apenas para drivers integrados ao kernel e será ignorado para módulos. Além disso, para drivers integrados ao kernel, o atributo __saída fará com que toda a função marcada com este atributo seja ignorada. Para módulos, este sinalizador também será ignorado.

Usando atributos __iniciar(E __initdata para descrever dados) pode reduzir a quantidade de memória usada pelo kernel. Bandeira __iniciar A função de inicialização do módulo não trará benefícios nem danos. O controle deste tipo de inicialização ainda não foi implementado para módulos, embora possa ser possível no futuro.

Resumindo

Assim, como resultado do material apresentado, podemos apresentar a seguinte versão do módulo “Olá mundo”:

Código do arquivo fonte do módulo ============================================== = #incluir #incluir #incluir static int __init meu_init_module (void) ( EXPORT_NO_SYMBOLS; printk("<1>Olá mundo\n"); return 0; ); static void __exit my_cleanup_module (void) ( printk("<1>Adeus\n"); ); module_init(my_init_module); module_exit(my_cleanup_module); MODULE_LICENSE("GPL"); ========================= ===================== Makefile para compilar o módulo ======================== ============== ==================== CFLAGS = -Wall -D__KERNEL__ -DMODULE -I/lib/modules/ $(shell uname -r)/build/include hello.o: =================================== ==========================

Observe que ao escrever o Makefile, usamos a convenção de que o utilitário GNU make pode determinar independentemente como gerar um arquivo objeto com base na variável CFLAGS e no compilador disponível no sistema.

Uso de recursos

Um módulo não pode completar sua tarefa sem usar recursos do sistema, como memória, portas de E/S, memória de E/S, linhas de interrupção e canais DMA.

Como programador, você já deve estar familiarizado com o gerenciamento dinâmico de memória. O gerenciamento dinâmico de memória no kernel não é fundamentalmente diferente. Seu programa pode obter memória usando a função kmalloc() e libertá-la com a ajuda kfree(). Essas funções são muito semelhantes às funções malloc() e free() com as quais você está familiarizado, exceto que a função kmalloc() recebe um argumento adicional - prioridade. Normalmente a prioridade é GFP_KERNEL ou GFP_USER. GFP é um acrônimo para “obter página gratuita”. O gerenciamento da memória dinâmica no kernel é abordado detalhadamente no Capítulo 7, “Obtendo Memória”.

Um desenvolvedor de driver iniciante pode se surpreender com a necessidade de alocar explicitamente portas de E/S, memória de E/S e linhas de interrupção. Só então o módulo do kernel poderá acessar facilmente esses recursos. Embora a memória do sistema possa ser alocada em qualquer lugar, a memória de E/S, as portas e as linhas de interrupção desempenham um papel especial e são alocadas de forma diferente. Por exemplo, o driver precisa alocar determinadas portas, não
tudo, menos aqueles que ele precisa para controlar o aparelho. Mas o motorista não pode utilizar esses recursos até ter certeza de que eles não estão sendo utilizados por outra pessoa.

A área de memória pertencente a um dispositivo periférico é geralmente chamada de memória de E/S, para distingui-la da RAM do sistema (RAM), que é simplesmente chamada de memória.

Portas e memória de E/S

O trabalho de um driver típico consiste principalmente em ler e escrever portas e memória de E/S. As portas e a memória de E/S são unidas por um nome comum - região (ou área) de E/S.

Infelizmente, nem toda arquitetura de barramento pode definir claramente a região de E/S que pertence a cada dispositivo, e é possível que o driver tenha que adivinhar a localização da região à qual pertence, ou até mesmo tentar operações de leitura/gravação em possíveis endereços. espaços. Este problema é especialmente
refere-se ao barramento ISA, que ainda é usado para instalar dispositivos simples em um computador pessoal e é muito popular no mundo industrial na implementação do PC/104 (veja a seção “PC/104 e PC/104+” no Capítulo 15 “Visão Geral dos Barramentos Periféricos”).

Qualquer que seja o barramento usado para conectar um dispositivo de hardware, o driver do dispositivo deve ter acesso exclusivo garantido à sua região de E/S para evitar colisões entre drivers. Se um módulo, acessando seu próprio dispositivo, gravar em um dispositivo que não lhe pertence, isso pode levar a consequências fatais.

Os desenvolvedores do Linux implementaram um mecanismo para solicitar/liberar regiões de E/S principalmente para evitar colisões entre diferentes dispositivos. Este mecanismo tem sido usado há muito tempo para portas de E/S e recentemente foi generalizado para gerenciamento de recursos em geral. Observe que esse mecanismo representa uma abstração de software e não se estende aos recursos de hardware. Por exemplo, o acesso não autorizado às portas de E/S no nível do hardware não causa nenhum erro semelhante a uma “falha de segmentação”, uma vez que o hardware não aloca e autoriza seus recursos.

As informações sobre os recursos cadastrados estão disponíveis em formato de texto nos arquivos /proc/ioports e /proc/iomem. Esta informação foi introduzida no Linux desde o kernel 2.3. Como lembrete, este livro foca principalmente no kernel 2.4, e notas de compatibilidade serão apresentadas no final do capítulo.

Portas

A seguir está o conteúdo típico do arquivo /proc/ioports:

0000-001f: dma1 0020-003f: pic1 0040-005f: temporizador 0060-006f: teclado 0080-008f: registro de página dma 00a0-00bf: pic2 00c0-00df: dma2 00f0-00ff: fpu 0170-0177: ide1 01 f 0- 01f7: ide0 02f8-02ff: serial (conjunto) 0300-031f: NE2000 0376-0376: ide1 03c0-03df: vga + 03f6-03f6: ide0 03f8-03ff: serial (conjunto) 1000-103f: Intel Corporation 82371AB PIIX4 ACPI 10 00- 1 003: acpi 1004-1005: acpi 1008-100b: acpi 100c-100f: acpi 1100-110f: Intel Corporation 82371AB PIIX4 IDE 1300-131f: pcnet_cs 1400-141f: Intel Corporation 82371AB PIIX4 ACPI 1800-18ff : Placa PCI #02 1c00- 1cff: PCI CardBus #04 5800-581f: Intel Corporation 82371AB PIIX4 USB d000-dfff: PCI Bus #01 d000-d0ff: ATI Technologies Inc 3D Rage LT Pro AGP-133

Cada linha deste arquivo exibe em hexadecimal o intervalo de portas associadas ao driver ou proprietário do dispositivo. Nas versões anteriores do kernel, o arquivo tinha o mesmo formato, exceto que a hierarquia de portas não era exibida.

O arquivo pode ser usado para evitar colisões de portas ao adicionar um novo dispositivo ao sistema. Isto é especialmente conveniente ao configurar manualmente o equipamento instalado trocando jumpers. Neste caso, o usuário pode visualizar facilmente a lista de portas utilizadas e selecionar uma faixa livre para o dispositivo ser instalado. E embora a maioria dos dispositivos modernos não use jumpers manuais, eles ainda são usados ​​na fabricação de componentes de pequena escala.

O que é mais importante é que o arquivo /proc/ioports tenha uma estrutura de dados acessível programaticamente associada a ele. Portanto, quando o driver de dispositivo é inicializado, ele pode saber o intervalo ocupado de portas de E/S. Isso significa que caso seja necessário escanear portas em busca de um novo dispositivo, o driver consegue evitar a situação de escrita em portas ocupadas por outros dispositivos.

A varredura do barramento ISA é conhecida por ser uma tarefa arriscada. Portanto, alguns drivers distribuídos com o kernel oficial do Linux evitam tal verificação ao carregar o módulo. Ao fazer isso, eles evitam o risco de danificar um sistema em execução ao gravar em portas usadas por outros equipamentos. Felizmente, as arquiteturas de barramento modernas são imunes a esses problemas.

A interface do software usada para acessar os registradores de E/S consiste nas três funções a seguir:

Int check_region(início longo não assinado, comprimento longo não assinado); recurso struct *request_region(início longo não assinado, len longo não assinado, char *nome); void release_region(início longo não assinado, comprimento longo não assinado);

Função check_region() pode ser chamado para verificar se um intervalo especificado de portas está ocupado. Retorna um código de erro negativo (como -EBUSY ou -EINVAL) se a resposta for negativa.

Função request_region() realiza a alocação de um determinado intervalo de endereços, retornando, se bem sucedido, um ponteiro não nulo. O driver não precisa armazenar ou usar o ponteiro retornado. Tudo que você precisa fazer é verificar NULL. O código que deve funcionar apenas com um kernel 2.4 (ou superior) não precisa chamar a função check_region(). Não há dúvida sobre a vantagem deste método de distribuição, porque
não se sabe o que pode acontecer entre chamadas para check_region() e request_region(). Se você deseja manter a compatibilidade com versões mais antigas do kernel, é necessário chamar check_region() antes de request_region().

Função release_region() deve ser chamado quando o driver liberar portas usadas anteriormente.

O valor real do ponteiro retornado por request_region() é usado apenas pelo subsistema de alocação de recursos em execução no kernel.

Essas três funções são na verdade macros definidas em .

Abaixo está um exemplo da sequência de chamadas usada para registrar portas. O exemplo foi retirado do código do driver de treinamento do crânio. (O código da função crânio_probe_hw() não é mostrado aqui porque contém código dependente de hardware.)

#incluir #incluir static int crânio_detect(porta int não assinada, intervalo int não assinado) ( int err; if ((err = check_region(porta,intervalo))< 0) return err; /* busy */ if (skull_probe_hw(port,range) != 0) return -ENODEV; /* not found */ request_region(port,range,"skull"); /* "Can"t fail" */ return 0; }

Este exemplo primeiro verifica a disponibilidade do intervalo de portas necessário. Se as portas não estiverem acessíveis, o acesso ao equipamento não será possível.
A localização real das portas do dispositivo pode ser esclarecida por meio de digitalização. A função request_region() não deveria, neste exemplo,
terminará em fracasso. O kernel não pode carregar mais de um módulo por vez, portanto não ocorrerão colisões de uso de porta
deve.

Quaisquer portas de E/S alocadas pelo driver devem ser liberadas posteriormente. Nosso driver de caveira faz isso na função cleanup_module():

Static void crânio_release(porta int não assinada, intervalo int não assinado) ( release_region(porta,intervalo); )

O mecanismo de solicitação/liberação de recursos é semelhante ao mecanismo de registro/cancelamento de registro do módulo e é perfeitamente implementado com base no esquema de uso do operador goto descrito acima.

Memória

Informações sobre a memória de E/S estão disponíveis no arquivo /proc/iomem. Abaixo está um exemplo típico de tal arquivo para um computador pessoal:

00000000-0009fbff: RAM do sistema 0009fc00-0009ffff: reservada 000a0000-000bffff: área de RAM de vídeo 000c0000-000c7fff: ROM de vídeo 000f0000-000fffff: ROM do sistema 00100000-03fffff: RAM do sistema 001 0000 0-0022c557: Código do kernel 0022c558-0024455f: Dados do kernel 20000000 - 2fffffff: Intel Corporation 440BX/ZX - 82443BX/ZX Host bridge 68000000-68000fff: Texas Instruments PCI1225 68001000-68001fff: Texas Instruments PCI1225 (#2) e0000000-e3ffffff: Barramento PCI #01 e4000000-e7ffff ff : Barramento PCI nº 01 e4000000 -e4ffffff: ATI Technologies Inc 3D Rage LT Pro AGP-133 e6000000-e6000fff: ATI Technologies Inc 3D Rage LT Pro AGP-133 fffc0000-ffffffff: reservado

Os valores do intervalo de endereços são mostrados em notação hexadecimal. Para cada faixa de ares, seu proprietário é mostrado.

O registro de acessos à memória de E/S é semelhante ao registro de portas de E/S e é baseado no mesmo mecanismo no kernel.

Para obter e liberar o intervalo necessário de endereços de memória de E/S, o driver deve usar as seguintes chamadas:

Int check_mem_region(início longo não assinado, comprimento longo não assinado); int request_mem_region(início longo não assinado, comprimento longo não assinado, char *nome); int release_mem_region(início longo não assinado, comprimento longo não assinado);

Normalmente, o driver conhece o intervalo de endereços de memória de E/S, portanto, o código para alocar esse recurso pode ser reduzido em comparação com o exemplo para alocar um intervalo de portas:

If (check_mem_region(mem_addr, mem_size)) ( printk("drivername: memória já em uso\n"); return -EBUSY; ) request_mem_region(mem_addr, mem_size, "drivername");

Alocação de recursos no Linux 2.4

O atual mecanismo de alocação de recursos foi introduzido no kernel Linux 2.3.11 e fornece acesso flexível ao gerenciamento de recursos do sistema. Esta seção descreve brevemente esse mecanismo. No entanto, funções básicas de alocação de recursos (como request_region(), etc.) ainda são implementadas como macros e usadas para compatibilidade retroativa com versões anteriores do kernel. Na maioria dos casos você não precisa saber nada sobre o mecanismo de distribuição real, mas pode ser interessante ao criar drivers mais complexos.

O sistema de gerenciamento de recursos implementado no Linux pode gerenciar recursos arbitrários de maneira hierárquica unificada. Os recursos globais do sistema (por exemplo, portas de E/S) podem ser divididos em subconjuntos - por exemplo, aqueles relacionados a um slot de barramento de hardware específico. Certos drivers também podem subdividir opcionalmente os recursos capturados com base em sua estrutura lógica.

O intervalo de recursos alocados é descrito por meio da estrutura de recursos struct, que é declarada no arquivo de cabeçalho :

Recurso estrutural (const char *nome; início e fim longos não assinados; sinalizadores longos não assinados; recurso struct *pai, *irmão, *filho; );

Um intervalo global (raiz) de recursos é criado no momento da inicialização. Por exemplo, uma estrutura de recursos que descreve portas de E/S é criada da seguinte forma:

Recurso estrutural ioport_resource = ("PCI IO", 0x0000, IO_SPACE_LIMIT, IORESOURCE_IO);

Aqui é descrito um recurso chamado PCI IO, que cobre a faixa de endereços de zero a IO_SPACE_LIMIT. O valor desta variável depende da plataforma utilizada e pode ser igual a 0xFFFF (espaço de endereço de 16 bits, para arquiteturas x86, IA-64, Alpha, M68k e MIPS), 0xFFFFFFFF (espaço de endereço de 32 bits, para SPARC, PPC , SH) ou 0xFFFFFFFFFFFFFFFF (64 bits, SPARC64).

Subintervalos deste recurso podem ser criados usando uma chamada para allocate_resource(). Por exemplo, durante a inicialização do barramento PCI, um novo recurso é criado para a região de endereço deste barramento e atribuído a um dispositivo físico. Quando o código do kernel PCI processa atribuições de porta e memória, ele cria um novo recurso apenas para essas regiões e os aloca usando chamadas para ioport_resource() ou iomem_resource().

O driver pode então solicitar um subconjunto de um recurso (geralmente parte de um recurso global) e marcá-lo como ocupado. A aquisição de recursos é realizada chamando request_region(), que retorna um ponteiro para uma nova estrutura de recursos struct que descreve o recurso solicitado ou NULL em caso de erro. Esta estrutura faz parte da árvore de recursos global. Como já mencionado, após obter o recurso, o driver não necessitará do valor deste ponteiro.

O leitor interessado pode gostar de ver os detalhes deste esquema de gerenciamento de recursos no arquivo kernel/resource.c localizado no diretório de fontes do kernel. Porém, para a maioria dos desenvolvedores o conhecimento já apresentado será suficiente.

O mecanismo de alocação de recursos em camadas traz benefícios duplos. Por um lado, fornece uma representação visual das estruturas de dados do kernel. Vamos dar uma olhada no arquivo de exemplo /proc/ioports novamente:

E800-e8ff: Adaptec AHA-2940U2/W / 7890 e800-e8be: aic7xxx

A faixa e800-e8ff é alocada para o adaptador Adaptec, que se autodenomina um driver no barramento PCI. A maior parte desse intervalo foi solicitada pelo driver aic7xxx.

Outra vantagem deste gerenciamento de recursos é a divisão do espaço de endereçamento em subfaixas que refletem a real interligação dos equipamentos. O gerenciador de recursos não pode alocar subintervalos de endereços sobrepostos, o que pode impedir a instalação de um driver com defeito.

Configuração automática e manual

Alguns parâmetros exigidos pelo driver podem variar de sistema para sistema. Por exemplo, o driver deve estar ciente dos endereços de E/S e intervalos de memória válidos. Para interfaces de barramento bem organizadas isso não é um problema. No entanto, às vezes você precisará passar parâmetros ao driver para ajudá-lo a encontrar seu próprio dispositivo ou ativar/desativar algumas de suas funções.

Essas configurações que afetam a operação do driver variam de acordo com o dispositivo. Por exemplo, este pode ser o número da versão do dispositivo instalado. Obviamente, essas informações são necessárias para que o driver funcione corretamente com o dispositivo. Definir tais parâmetros (configuração do driver) é uma tarefa bastante
uma tarefa complicada executada quando o driver é inicializado.

Normalmente existem duas maneiras de obter os valores corretos deste parâmetro - ou o usuário os define explicitamente, ou o driver os determina de forma independente, com base na pesquisa do equipamento. Embora a detecção automática seja sem dúvida a melhor solução para configuração de driver,
a configuração personalizada é muito mais fácil de implementar. O desenvolvedor do driver deve implementar a configuração automática do driver sempre que possível, mas, ao mesmo tempo, deve fornecer ao usuário um mecanismo de configuração manual. É claro que a configuração manual deve ter maior prioridade do que a configuração automática. Nos estágios iniciais de desenvolvimento, normalmente apenas a transmissão manual de parâmetros ao driver é implementada. A configuração automática, se possível, é adicionada posteriormente.

Muitos drivers, entre seus parâmetros de configuração, possuem parâmetros que controlam as operações do driver. Por exemplo, os drivers de interface Integrated Device Electronics (IDE) permitem ao usuário controlar as operações DMA. Portanto, se o seu driver fizer um bom trabalho de detecção automática de hardware, você pode querer dar ao usuário controle sobre a funcionalidade do driver.

Os valores dos parâmetros podem ser passados ​​​​durante o carregamento do módulo usando os comandos insmod ou modprobe. Recentemente tornou-se possível ler o valor dos parâmetros de um arquivo de configuração (geralmente /etc/modules.conf). Valores inteiros e string podem ser passados ​​como parâmetros. Assim, se você precisar passar um valor inteiro para o parâmetro Skull_ival e um valor de string para o parâmetro Skull_sval, você pode passá-los durante o carregamento do módulo com parâmetros adicionais para o comando insmod:

Insmod crânio crânio_ival = 666 crânio_sval = "a fera"

Porém, antes que o comando insmod possa alterar os valores dos parâmetros de um módulo, o módulo deve disponibilizar esses parâmetros. Os parâmetros são declarados usando a definição de macro MODULE_PARM, que é definida no arquivo de cabeçalho module.h. A macro MODULE_PARM recebe dois parâmetros: o nome da variável e uma string que define seu tipo. Esta definição de macro deve ser colocada fora de qualquer função e geralmente está localizada no início do arquivo após a definição das variáveis. Assim, os dois parâmetros mencionados acima podem ser declarados da seguinte forma:

Int crânio_ival=0; char *crânio_sval; MODULE_PARM(crânio_ival, "i"); MODULE_PARM(crânio_sval, "s");

Atualmente existem cinco tipos de parâmetros de módulo suportados:

  • b - valor de um byte;
  • h - valor (curto) de dois bytes;
  • eu - inteiro;
  • l - inteiro longo;
  • s - string (char*);

No caso de parâmetros string, um ponteiro (char *) deve ser declarado no módulo. O comando insmod aloca memória para a string passada e a inicializa com o valor necessário. Usando a macro MODULE_PARM, você pode inicializar matrizes de parâmetros. Nesse caso, o número inteiro que precede o caractere de tipo determina o comprimento do array. Quando dois inteiros são especificados, separados por um travessão, eles determinam o número mínimo e máximo de valores a serem transmitidos. Para uma compreensão mais detalhada de como esta macro funciona, consulte o arquivo de cabeçalho .

Por exemplo, suponha que uma matriz de parâmetros deva ser inicializada com pelo menos dois e pelo menos quatro valores inteiros. Então pode ser descrito da seguinte forma:

Int crânio_array; MODULE_PARM(skull_array, "2-4i");

Além disso, o kit de ferramentas do programador possui a definição de macro MODULE_PARM_DESC, que permite colocar comentários nos parâmetros do módulo que estão sendo passados. Esses comentários são armazenados no arquivo objeto do módulo e podem ser visualizados usando, por exemplo, o utilitário objdump ou ferramentas automatizadas de administração do sistema. Aqui está um exemplo de uso desta definição de macro:

Int porta_base = 0x300; MODULE_PARM(porta_base, "i"); MODULE_PARM_DESC (base_port, "A porta base de E/S (padrão 0x300)");

É desejável que todos os parâmetros do módulo tenham valores padrão. A alteração desses valores usando insmod só deve ser necessária se necessário. O módulo pode verificar a configuração explícita dos parâmetros, verificando seus valores atuais com os valores padrão. Posteriormente, você pode implementar um mecanismo de configuração automática com base no diagrama a seguir. Se os valores dos parâmetros tiverem valores padrão, a configuração automática será executada. Caso contrário, serão utilizados os valores atuais. Para que este esquema funcione, é necessário que os valores padrão não correspondam a nenhuma das configurações possíveis do sistema no mundo real. Seria então assumido que tais valores não podem ser definidos pelo usuário na configuração manual.

O exemplo a seguir mostra como o driver crânio detecta automaticamente o espaço de endereço das portas do dispositivo. No exemplo acima, a detecção automática analisa vários dispositivos, enquanto a configuração manual limita o driver a um único dispositivo. Você já conheceu a função Skull_detect anteriormente na seção que descreve as portas de E/S. A implementação de Skull_init_board() não é mostrada porque
Executa inicialização dependente de hardware.

/* * intervalos de portas: o dispositivo pode residir entre * 0x280 e 0x300, em passos de 0x10. Ele usa portas 0x10. */ #define SKULL_PORT_FLOOR 0x280 #define SKULL_PORT_CEIL 0x300 #define SKULL_PORT_RANGE 0x010 /* * a função a seguir executa autodetecção, a menos que um * valor específico tenha sido atribuído por insmod a "skull_port_base" */ static int crânio_port_base=0; /* 0 força autodetecção */ MODULE_PARM (skull_port_base, "i"); MODULE_PARM_DESC(skull_port_base, "Porta base de E/S para crânio"); static int crânio_find_hw(void) /* retorna o número de dispositivos */ ( /* base é o valor do tempo de carregamento ou a primeira tentativa */ int base = crânio_port_base ? crânio_port_base: SKULL_PORT_FLOOR; int resultado = 0; /* loop um tempo se o valor for atribuído; experimente todos se estiver detectando automaticamente */ do ( if (skull_detect(base, SKULL_PORT_RANGE) == 0) ( crânio_init_board(base); result++; ) base += SKULL_PORT_RANGE; /* prepare-se para a próxima tentativa */ ) while (skull_port_base == 0 && base< SKULL_PORT_CEIL); return result; }

Se as variáveis ​​de configuração forem usadas apenas dentro do driver (ou seja, não publicadas na tabela de símbolos do kernel), então o programador pode tornar a vida do usuário um pouco mais fácil ao não usar prefixos nos nomes das variáveis ​​(no nosso caso, o prefixo crânio_) . Esses prefixos significam pouco para o usuário e sua ausência simplifica a digitação do comando no teclado.

Para completar, forneceremos uma descrição de mais três definições de macro que permitem colocar alguns comentários no arquivo objeto.

MODULE_AUTHOR (nome) Coloca uma linha com o nome do autor no arquivo objeto. MODULE_DESCRIPTION(desc) Coloca uma linha com uma descrição geral do módulo no arquivo objeto. MODULE_SUPPORTED_DEVICE(desenvolvedor) Coloca uma linha descrevendo o dispositivo suportado pelo módulo. O Linux fornece uma API poderosa e extensa para aplicativos, mas às vezes não é suficiente. Para interagir com o hardware ou realizar operações com acesso a informações privilegiadas do sistema, é necessário um driver de kernel.

Um módulo do kernel Linux é um código binário compilado que é inserido diretamente no kernel Linux, rodando no anel 0, o anel de execução de instruções interno e menos seguro no processador x86-64. Aqui o código é executado completamente sem nenhuma verificação, mas com uma velocidade incrível e com acesso a quaisquer recursos do sistema.

Não para meros mortais

Escrever um módulo do kernel Linux não é para os fracos de coração. Ao alterar o kernel, você corre o risco de perder dados. Não há segurança padrão no código do kernel como existe em aplicativos Linux regulares. Se você cometer um erro, desligue todo o sistema.

O que piora a situação é que o problema não aparece necessariamente de imediato. Se o módulo travar o sistema imediatamente após o carregamento, este é o melhor cenário de falha. Quanto mais código houver, maior será o risco de loops infinitos e vazamentos de memória. Se você não tomar cuidado, os problemas aumentarão gradualmente à medida que a máquina operar. Eventualmente, estruturas de dados importantes e até mesmo buffers podem ser sobrescritos.

Os paradigmas tradicionais de desenvolvimento de aplicativos podem ser amplamente esquecidos. Além de carregar e descarregar um módulo, você escreverá código que reage a eventos do sistema em vez de seguir um padrão sequencial. Ao trabalhar com o kernel, você escreve a API, não os aplicativos em si.

Você também não tem acesso à biblioteca padrão. Embora o kernel forneça algumas funções como printk (que substitui printf) e kmalloc (que funciona de maneira semelhante ao malloc), você fica basicamente por conta própria. Além disso, você deve limpar completamente após descarregar o módulo. Não há coleta de lixo aqui.

Componentes necessários

Antes de começar, você deve certificar-se de ter todas as ferramentas necessárias para o trabalho. Mais importante ainda, você precisa de uma máquina Linux. Eu sei que isso é inesperado! Embora qualquer distribuição Linux sirva, estou usando o Ubuntu 16.04 LTS neste exemplo, então você pode precisar modificar um pouco os comandos de instalação se estiver usando outras distribuições.

Em segundo lugar, você precisa de uma máquina física separada ou de uma máquina virtual. Pessoalmente, prefiro trabalhar em uma máquina virtual, mas faça a sua escolha. Não recomendo usar sua máquina principal devido à perda de dados caso você cometa um erro. Digo “quando” e não “se” porque com certeza você pendurará o carro pelo menos algumas vezes durante o processo. Suas últimas alterações de código ainda podem estar no buffer de gravação quando o kernel entra em pânico, então suas fontes também podem estar corrompidas. Testar em uma máquina virtual elimina esses riscos.

Finalmente, você precisa saber pelo menos um pouco de C. O tempo de execução do C++ é muito grande para o kernel, então você precisa escrever em C puro e simples. Algum conhecimento da linguagem assembly também é útil para interagir com o hardware.

Instalando o ambiente de desenvolvimento

No Ubuntu você precisa executar:

Apt-get install build-essential linux-headers-`uname -r`
Instalamos as ferramentas de desenvolvimento e cabeçalhos de kernel mais importantes necessários para este exemplo.

Os exemplos abaixo assumem que você está executando como um usuário normal em vez de root, mas que possui privilégios sudo. Sudo é necessário para carregar módulos do kernel, mas queremos trabalhar fora do root sempre que possível.

Começar

Vamos começar a escrever código. Vamos preparar nosso ambiente:

Mkdir ~/src/lkm_example cd ~/src/lkm_example
Inicie seu editor favorito (vim no meu caso) e crie um arquivo lkm_example.c com o seguinte conteúdo:

#incluir #incluir #incluir MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Robert W. Oliver II”); MODULE_DESCRIPTION(“Um exemplo simples de módulo Linux.”); MODULE_VERSION(“0,01”); static int __init lkm_example_init(void) ( printk(KERN_INFO “Olá, mundo!\n”); return 0; ) static void __exit lkm_example_exit(void) ( printk(KERN_INFO “Adeus, mundo!\n”); ) module_init(lkm_example_init ); module_exit(lkm_example_exit);
Projetamos o módulo mais simples possível, vamos dar uma olhada em suas partes mais importantes:

  • include lista os arquivos de cabeçalho necessários para desenvolver o kernel Linux.
  • MODULE_LICENSE pode ser definido com valores diferentes dependendo da licença do módulo. Para ver a lista completa, execute:

    Grep “MODULE_LICENSE” -B 27 /usr/src/linux-headers-`uname -r`/include/linux/module.h

  • Configuramos init (carregamento) e exit (descarregamento) como funções estáticas que retornam números inteiros.
  • Observe o uso de printk em vez de printf . Além disso, as opções para printk são diferentes de printf . Por exemplo, o sinalizador KERN_INFO para declarar a prioridade de registro para uma linha específica é especificado sem vírgula. O kernel lida com essas coisas dentro da função printk para economizar memória de pilha.
  • No final do arquivo, você pode chamar module_init e module_exit e especificar as funções de carregamento e descarregamento. Isso torna possível nomear funções arbitrariamente.
No entanto, ainda não podemos compilar este arquivo. Makefile necessário. Este exemplo básico será suficiente por enquanto. Observe que make é muito exigente com espaços e tabulações, então certifique-se de usar tabulações em vez de espaços quando apropriado.

Obj-m += lkm_example.o todos: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) módulos limpos: make -C /lib/modules/$(shell uname -r )/construir M=$(PWD) limpar
Se executarmos make ele deverá compilar nosso módulo com sucesso. O resultado será o arquivo lkm_example.ko. Se ocorrer algum erro, verifique se as aspas no código-fonte estão definidas corretamente e não acidentalmente na codificação UTF-8.

Agora você pode implementar o módulo e testá-lo. Para fazer isso, executamos:

Sudo insmod lkm_example.ko
Se tudo estiver bem, você não verá nada. A função printk fornece saída não para o console, mas para o log do kernel. Para visualizar você precisa executar:

Sudo dmesg
Você deverá ver a linha “Hello, World!” com um carimbo de data/hora no início. Isso significa que nosso módulo do kernel foi carregado e gravado com sucesso no log do kernel. Também podemos verificar se o módulo ainda está na memória:

lsmod | grep “lkm_exemplo”
Para remover um módulo, execute:

Sudo rmmod lkm_example
Se você executar o dmesg novamente, verá a entrada “Adeus, mundo!” no log. Você pode executar lsmod novamente e certificar-se de que o módulo esteja descarregado.

Como você pode ver, este procedimento de teste é um pouco tedioso, mas pode ser automatizado adicionando:

Teste: sudo dmesg -C sudo insmod lkm_example.ko sudo rmmod lkm_example.ko dmesg
no final do Makefile e depois executando:

Faça teste
para testar o módulo e verificar a saída no log do kernel sem precisar executar comandos separados.

Agora temos um módulo de kernel totalmente funcional, embora completamente trivial!

Vamos cavar um pouco mais fundo. Embora os módulos do kernel sejam capazes de executar todos os tipos de tarefas, a interface com aplicativos é um dos casos de uso mais comuns.

Como os aplicativos não têm permissão para visualizar a memória no espaço do kernel, eles devem usar a API para se comunicarem com eles. Embora tecnicamente existam várias maneiras de fazer isso, a mais comum é criar um arquivo de dispositivo.

Você provavelmente já lidou com arquivos de dispositivos antes. Comandos que mencionam /dev/zero , /dev/null e similares interagem com os dispositivos “zero” e “nulo”, que retornam os valores esperados.

Em nosso exemplo retornamos “Hello, World”. Embora este não seja um recurso particularmente útil para aplicativos, ele ainda demonstra o processo de interação com um aplicativo por meio de um arquivo de dispositivo.

Aqui está a lista completa:

#incluir #incluir #incluir #incluir #incluir MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“Robert W. Oliver II”); MODULE_DESCRIPTION(“Um exemplo simples de módulo Linux.”); MODULE_VERSION(“0,01”); #define DEVICE_NAME “lkm_example” #define EXAMPLE_MSG “Olá, mundo!\n” #define MSG_BUFFER_LEN 15 /* Protótipos para funções de dispositivos */ static int device_open(struct inode *, struct file *); static int device_release(struct inode *, arquivo struct *); static ssize_t device_read(arquivo struct *, char *, size_t, loff_t *); static ssize_t device_write(arquivo struct *, const char *, size_t, loff_t *); static int major_num; static int device_open_count = 0; caractere estático msg_buffer; caractere estático *msg_ptr; /* Esta estrutura aponta para todas as funções do dispositivo */ static struct file_operations file_ops = ( .read = device_read, .write = device_write, .open = device_open, .release = device_release ); /* Quando um processo lê nosso dispositivo, this é chamado. */ static ssize_t device_read(struct file *flip, char *buffer, size_t len, loff_t *offset) ( int bytes_read = 0; /* Se estivermos no final, volta ao início */ if (*msg_ptr = = 0) ( msg_ptr = msg_buffer; ) /* Colocar dados no buffer */ while (len && *msg_ptr) ( /* O buffer está nos dados do usuário, não no kernel, então você não pode apenas referenciar * com um ponteiro. O function put_user cuida disso para nós */ put_user(*(msg_ptr++), buffer++); len--; bytes_read++; ) return bytes_read; ) /* Chamado quando um processo tenta gravar em nosso dispositivo */ static ssize_t device_write(struct file * flip, const char *buffer, size_t len, loff_t *offset) ( /* Este é um dispositivo somente leitura */ printk(KERN_ALERT “Esta operação não é suportada.\n”); return -EINVAL; ) /* Chamado quando um processo abre nosso dispositivo */ static int device_open(struct inode *inode, struct file *file) ( /* Se o dispositivo estiver aberto, retorne ocupado */ if (device_open_count) ( return -EBUSY; ) device_open_count++; try_module_get(THIS_MODULE); retornar 0; ) /* Chamado quando um processo fecha nosso dispositivo */ static int device_release(struct inode *inode, struct file *file) ( /* Diminui o contador aberto e a contagem de uso. Sem isso, o módulo não seria descarregado. */ device_open_count- -; module_put(THIS_MODULE); return 0; ) static int __init lkm_example_init(void) ( /* Preencha o buffer com nossa mensagem */ strncpy(msg_buffer, EXAMPLE_MSG, MSG_BUFFER_LEN); /* Defina o msg_ptr para o buffer */ msg_ptr = msg_buffer ; /* Tenta registrar o dispositivo de caractere */ major_num = register_chrdev(0, “lkm_example”, &file_ops); if (major_num< 0) { printk(KERN_ALERT “Could not register device: %d\n”, major_num); return major_num; } else { printk(KERN_INFO “lkm_example module loaded with device major number %d\n”, major_num); return 0; } } static void __exit lkm_example_exit(void) { /* Remember - we have to clean up after ourselves. Unregister the character device. */ unregister_chrdev(major_num, DEVICE_NAME); printk(KERN_INFO “Goodbye, World!\n”); } /* Register module functions */ module_init(lkm_example_init); module_exit(lkm_example_exit);

Testando um exemplo aprimorado

Agora, nosso exemplo faz mais do que apenas imprimir uma mensagem ao carregar e descarregar, portanto será necessário um procedimento de teste menos rigoroso. Vamos alterar o Makefile para carregar apenas o módulo, sem descarregá-lo.

Obj-m += lkm_example.o todos: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) módulos limpos: make -C /lib/modules/$(shell uname -r )/build M=$(PWD) clean test: # Colocamos um - na frente do comando rmmod para dizer ao make para ignorar # um erro caso o módulo não esteja carregado. -sudo rmmod lkm_example # Limpa o log do kernel sem echo sudo dmesg -C # Insira o módulo sudo insmod lkm_example.ko # Exibe o log do kernel dmesg
Agora, depois de executar o make test, você verá o número principal do dispositivo sendo gerado. No nosso exemplo, ele é atribuído automaticamente pelo kernel. No entanto, este número é necessário para criar um novo dispositivo.

Pegue o número gerado por make test e use-o para criar um arquivo de dispositivo para que possamos nos comunicar com nosso módulo do kernel a partir do espaço do usuário.

Sudo mknod /dev/lkm_example com MAJOR 0
(neste exemplo, substitua MAJOR pelo valor obtido em make test ou dmesg)

A opção c no comando mknod informa ao mknod que precisamos criar um arquivo de dispositivo de caracteres.

Agora podemos obter conteúdo do dispositivo:

Gato /dev/lkm_example
ou mesmo através do comando dd:

Dd if=/dev/lkm_example of=test bs=14 contagem=100
Você também pode acessar esse arquivo em aplicativos. Não precisam ser aplicativos compilados - até mesmo scripts Python, Ruby e PHP têm acesso a esses dados.

Quando terminamos o dispositivo, removemos-o e descarregamos o módulo:

Sudo rm /dev/lkm_example sudo rmmod lkm_example

Conclusão

Espero que tenham gostado das nossas brincadeiras no espaço central. Embora os exemplos mostrados sejam primitivos, essas estruturas podem ser usadas para criar seus próprios módulos que executam tarefas muito complexas.

Apenas lembre-se que no espaço do kernel tudo é de sua responsabilidade. Não há suporte ou segunda chance para o seu código. Se você estiver fazendo um projeto para um cliente, planeje com antecedência o dobro, se não o triplo, do tempo de depuração. O código do kernel deve ser o mais perfeito possível para garantir a integridade e confiabilidade dos sistemas nos quais é executado.

Algumas características da programação modular e recomendações gerais para a construção de subprogramas de estrutura modular.

Os módulos são conectados ao programa principal na ordem em que são declarados USES, e na mesma ordem estão os blocos de inicialização dos módulos conectados ao programa principal antes do programa iniciar a execução.

A ordem na qual os módulos são executados pode afetar o acesso aos dados da biblioteca e o acesso à rotina.

Por exemplo, se os módulos com os nomes M1, M2 contêm o mesmo tipo A, variável B e sub-rotina C, então após conectar esses modelos USES, as chamadas para A, B, C nesta PU serão equivalentes às chamadas de objetos para o módulo M2 .

Mas para caracterizar a correção das chamadas aos objetos necessários de mesmo nome de diferentes módulos conectados, é aconselhável ao acessar um módulo indicar primeiro o nome do módulo, seguido de um ponto o nome do objeto: M1. A M1.B M1.C M2.B.

Obviamente, é muito fácil dividir um programa grande em duas partes (PU), ou seja, programa principal + módulos.

Colocar cada PU em seu próprio segmento de memória e em seu próprio arquivo de disco.

Todas as declarações de tipos, bem como aquelas variáveis ​​que devem estar disponíveis para PUs individuais (o programa principal e módulos futuros) devem ser colocadas em um módulo separado com uma parte executável vazia. No entanto, você não deve prestar atenção ao fato de que alguns PE (por exemplo, um módulo) não utilizam todas essas declarações. A parte inicial de tal módulo pode incluir instruções que associam variáveis ​​de arquivo a arquivos de texto não padrão (ASSIGN) e iniciam esses arquivos, ou seja, indicando chamadas de transferência de dados para eles (RESET e REWRITE).

O primeiro grupo de outras sub-rotinas, por exemplo, várias funções compactas devem ser colocadas no módulo 3 (por sua vez), outras, por exemplo, procedimentos de uso geral - no módulo 4, etc.

Ao distribuir sub-rotinas em módulos em um projeto complexo, atenção especial deve ser dada à ordem e ao local de sua escrita.

O ambiente TP contém ferramentas que controlam várias formas de compilar módulos.

Compilar Alt+F9 EXECUTAR Cntr+F9

Memória de destino

Esses modos diferem apenas no método de comunicação e no programa principal.

Modo de compilação

Compila o programa ou módulo principal que está atualmente na janela ativa do editor. Se esta PU contiver acesso a módulos de usuário não padrão, então este modo requer a presença de arquivos de disco com o mesmo nome com a extensão ___.tpu para cada módulo plug-in.



Se o Destino estiver armazenado na memória, esses arquivos permanecerão apenas na memória e um arquivo em disco não será criado.

No entanto, é muito mais fácil criar arquivos tpu junto com o compilador de todo o programa usando outros modos que não requerem configuração de Disco para a opção de destino.

Modo fazer

Ao compilar neste modo, o seguinte é verificado primeiro (antes de compilar o programa principal) para cada módulo:

1) A existência de um arquivo tpu em disco; se não existir, então é criado automaticamente compilando o código-fonte do módulo, ou seja, seu arquivo pas

2) Correspondência do arquivo tpu encontrado com o texto fonte do módulo, onde alterações poderiam ter sido feitas; caso contrário, o arquivo tpu será criado automaticamente novamente

3) Imutabilidade da seção de interface do módulo: se ela tiver sido alterada, todos os módulos (seus arquivos pas de origem) nos quais este módulo é especificado na cláusula USES também serão recompilados.

Se não houve alteração nos códigos-fonte dos módulos, o compilador interage com esses arquivos tpu e utiliza o tempo de compilação.

Modo de construção

Ao contrário do modo Make, requer necessariamente a presença de arquivos pas de origem; compila (recompila) cada módulo e, assim, garante que todas as alterações nos textos dos arquivos pas sejam levadas em consideração. Isso aumenta o tempo de compilação do programa como um todo.

Ao contrário do modo de compilação, os modos Make e Build permitem que você comece a compilar um programa com uma estrutura modular a partir de qualquer arquivo pas (é chamado de arquivo primário), independentemente de qual arquivo (ou parte do programa) está no ativo. janela do editor. Para fazer isso, no item de compilação, selecione a opção Arquivo primário, pressione Enter e anote o nome do arquivo pas primário, e então a compilação será iniciada a partir deste arquivo.

Se o arquivo primário não for especificado desta forma, a compilação nos modos Make, Build e RUN só será possível se o programa principal estiver na janela ativa do editor.

Nota: No futuro, pretendo usar o sistema T2 para compilar o kernel e os módulos do Puppy. O T2 está atualmente instalado para compilar o kernel e vários módulos adicionais, mas não a versão atualmente usada no Puppy. Pretendo sincronizar em versões futuras do Puppy, para que o kernel compilado no T2 seja utilizado no Puppy. Consulte http://www.puppyos.net/pfs/ para obter mais informações sobre Puppy e T2.

O Puppy tem uma maneira muito simples de usar compiladores C/C++ adicionando um único arquivo, devx_xxx.sfs, onde xxx é o número da versão. Por exemplo, o Puppy 2.12 teria um arquivo de desenvolvimento de conformidade denominado devx_212.sfs. Ao executar no modo LiveCD, coloque o arquivo devx_xxx.sfs no mesmo local que seu arquivo de configurações pessoais pup_save.3fs, que geralmente está localizado no diretório /mnt/home/. Isso também se aplica a outros modos de instalação que possuem um arquivo pup_save.3fs. Se o Puppy estiver instalado em um disco rígido com instalação completa da "Opção 2", então não há arquivo pessoal, procure nas páginas do Puppy para ser compilado com diferentes opções de configuração, para que os módulos não sejam compatíveis. Essas versões requerem apenas um patch para squashfs. O Puppy 2.12 possui kernel 2.6.18.1 e três correções; squashfs, nível de log do console padrão e correção de desligamento.

Esses comandos para corrigir o kernel são fornecidos exclusivamente para sua auto-educação, uma vez que um kernel corrigido já está disponível...

A primeira coisa que você deve fazer é baixar o próprio kernel. Ele está localizado para encontrar um link para um site de download adequado. Esta deve ser uma fonte "antiga" disponível em kernel.org ou em seus espelhos.

Conecte-se à Internet, baixe o kernel para a pasta /usr/src. Em seguida, descompacte-o:

cd /usr/src tar -jxf linux-2.6.16.7.tar.bz2 tar -zxf linux-2.6.16.7.tar.gz

Você deverá ver esta pasta: /usr/src/linux-2.6.16.7. Você então precisa ter certeza de que este link aponta para o kernel:

ln -sf linux-2.6.16.7 linux ln -sf linux-2.6.16.7 linux-2.6.9

Você deve aplicar as seguintes correções para ter exatamente a mesma fonte usada ao compilar o kernel para o Puppy. Caso contrário, você receberá mensagens de erro de "símbolos não resolvidos" ao compilar o driver e tentar usá-lo com o kernel do Puppy. Aplicando a correção squashfs

Segundo, aplique o patch Squashfs. O patch Squashfs adiciona suporte para Squashfs tornando o sistema de arquivos somente leitura.

Baixe o patch, squashfs2.1-patch-k2.6.9.gz, para a pasta /usr/src. Observe que esta correção foi feita para a versão 2.6.9 do kernel, mas continua funcionando nas versões 2.6.11.x enquanto existir uma referência ao linux-2.6.9. Em seguida, aplique a correção:

Cd/ usr/ src gunzip squashfs2.1-patch-k2.6.9.gz cd/ usr/ src/ linux-2.6.11.11

Observe que p1 tem o número 1, não o símbolo l... (Grande piada - Aproximadamente. tradução)

patch --execução a seco -p1< ../ squashfs2.1-patch patch -p1 < ../ squashfs2.1-patch

Preparar! O kernel está pronto para compilar!

Compilando o kernel

Você precisa obter um arquivo de configuração para o kernel. Uma cópia dele está localizada na pasta /lib/modules.

Em seguida, siga estas etapas:

CD/ usr/ src/ linux-2.6.18.1

Se houver um arquivo .config, copie-o temporariamente em algum lugar ou renomeie-o.

fazer limpo fazer mrproper

Copie o arquivo .config do Puppy para /usr/src/linux-2.6.18.1... Ele terá nomes diferentes em /lib/modules, então renomeie para .config... As etapas a seguir leem o arquivo .config e geram um novo.

fazer menuconfig

...faça as alterações desejadas e salve-as. Agora você terá um novo arquivo .config e deverá copiá-lo em algum lugar por segurança. Ver nota abaixo

fazer bzImage

Agora você compilou o kernel.

Ótimo, você encontrará o kernel Linux em /usr/src/linux-2.6.18.1/arch/i386/boot/bzImage

Compilando módulos

Agora vá para /lib/modules e se já existir uma pasta chamada 2.6.18.1 , renomeie a pasta 2.6.18.1 para 2.6.18.1-old .

Agora instale novos módulos:

Cd/ usr/ src/ linux-2.6.18.1 make module make module_install

...depois disso você deverá encontrar os novos módulos instalados na pasta /lib/modules/2.6.18.1.

Observe que a última etapa acima executa o programa "depmod" e isso pode gerar mensagens de erro sobre símbolos ausentes em alguns dos módulos. Não se preocupe com isso - um dos desenvolvedores estragou tudo e isso significa que não podemos usar aquele módulo.

Como usar o novo kernel e módulos

É melhor se você tiver o Puppy Unleashed instalado. Então o tarball é expandido e existem 2 diretórios: "boot" e "kernels".

"Boot" contém a estrutura do arquivo e o script para criar o disco virtual inicial. Você terá que colocar alguns módulos do kernel lá.

O diretório "kernels" possui um diretório kernels/2.6.18.1/ , e você precisará substituir os módulos pelos atualizados. Você não precisa substituí-lo se recompilou a mesma versão do kernel (2.6.18.1).

Observe que em kernels/2.6.18.1 existe um arquivo chamado "System.map". Você deve renomeá-lo e substituí-lo pelo novo em /usr/src/linux-2.6.18.1. Navegue na pasta kernels/2.6.18.1/ e você saberá o que precisa ser atualizado.

Se você compilou o kernel em uma instalação completa do Puppy, poderá reinicializar usando o novo kernel. make module_install é o passo acima para instalar novos módulos em /lib/modules/2.6.18.1 , mas você também deve instalar um novo kernel. Eu inicializo com o Grub e apenas copio o novo kernel para o diretório /boot (e renomeio o arquivo de bzImage para vmlinuz).

Nota sobre menuconfig. Eu o uso há muito tempo, então considero algumas coisas certas, mas um novato pode ficar confuso ao querer sair do programa. Existe um menu no menu de nível superior para salvar a configuração - ignore-o. Basta pressionar a tecla TAB (ou tecla de seta para a direita) para destacar o “botão” Sair e pressionar a tecla ENTER. Você será perguntado se deseja salvar a nova configuração e sua resposta deverá ser Sim.


Principal