Um Device Driver pode ser visto como o Software que permite que o Kernel do Linux se comunique com algum dispositivo (Hardware). Em se tratando de Linux, o interessante é que o código do Kernel é aberto, isto é, qualquer pessoa que se proponha a desenvolver um Device Driver poderá fazê-lo.
Uma característica importante de Device Drivers no Linux é a sua modularização, em outras palavras, um Device Driver pode ser anexado ao Kernel sem a necessidade de recompilar todo o Kernel ou mesmo reiniciar a máquina. Outro ponto interessante no desenvolvimento de Device Drivers é que não necessariamente um Device Driver é implementado para se comunicar com algum dispositivo de Hardware; existem módulos construídos somente para coletar informações do próprio Kernel. Um exemplo de módulo desse tipo é o que implementa o sistema de arquivos virtual /proc do Linux, que permite obter informações sobre a memória virtual, processador, escalonador, processos e outros mais, como se fossem arquivos.
A linguagem predominante no código fonte do Linux é a linguagem C. Apenas algumas partes bem específicas (por exemplo, a parte do código que realiza o bootstrap) são escritas em Assembly. É importante salientar que o código, apesar de possuir comentários em quase todos os lugares e principalmente nas bibliotecas, não é trivial. Lembre-se que qualquer processamento feito pelo Sistema Operacional é considerado sobrecarga, por isso o código é otimizado sempre que possível pelos desenvolvedores do Kernel. Neste artigo, serão apresentados alguns conceitos básicos para a implementação de Device Drivers e um passo-a-passo de como começar a desenvolver os módulos para o Kernel do Linux.
Antes de começar a implementar o módulo, é importante ter em mente alguns conceitos básicos sobre Sistemas Operacionais. Em geral, um sistema operacional é dividido em duas regiões, o Kernel Space (Espaço do Kernel) e o User Space (Espaço do Usuário). Basicamente, essa diferença serve para identificar o que é código do Kernel (por exemplo, códigos do escalonador de processo, código de gerência de memória, etc.) e o que é código do Usuário (por exemplo, códigos de aplicações, etc.).
Esta separação é necessária, principalmente por questões de segurança. Caso esta separação não fosse muito bem definida, o sistema se tornaria extremamente vulnerável a ataques de códigos maliciosos, como vírus. Abaixo, são listadas algumas das principais diferenças entre códigos que são executados em Kernel Space e User Space:
- Prioridade:. Códigos executados em Kernel Space possuem prioridade bem maior que códigos executados em User Space. Isto significa que se há uma rotina do Kernel e uma aplicação mais crítica do usuário para serem executadas, a rotina do Kernel será executada primeiro (por mais insignificante que ela seja). Existem exceções como sistemas de Tempo Real, que priorizam as aplicações críticas, mas apenas em ambientes especializados;
- Instruções: Para que o código em execução em User Space seja totalmente confiável, quando o processador está executando código do usuário, algumas instruções consideradas criticas são limitadas. Por exemplo, a instrução POPF (que copia um valor de 16-bits no topo de uma pilha para o registrador EFLAGS) é considerada uma instrução critica e um programa em execução em User Space, caso tentasse utilizar esta instrução, geraria um erro ou alguma interrupção para tratamento do Kernel;
- Endereçamento de Memória: Assim que o Linux é iniciado, uma parte da memória física disponível é alocada para uso somente do Kernel. Após esta região da memória (um determinado conjunto de endereços de memória), ser alocada para o Kernel, nenhum outro programa, a não ser o próprio Kernel, poderá acessar qualquer endereço que pertença a ela.
Apesar destas restrições de acesso impostas a programas que estão executando em User Space, é necessário criar alguma forma de permitir que estes programas consigam acessar um dispositivo de Hardware (como disco ou interface de rede) com segurança. Para resolver este problema, os Sistemas Operacionais disponibilizam aos programadores as chamadas de sistemas (System Calls). Estas chamadas podem ser entendidas como uma interface dos programas em User Space com o Kernel. Quando um programa faz uma chamada de sistema, o Kernel irá parar a execução do programa que fez a chamada. Em seguida, as rotinas de tratamento necessárias para a chamada realizada (que estão em Kernel Space) são processadas. O ultimo passo é retornar a solicitação feita pelo programa e continuar com o seu processamento.
Ao implementar um Device Driver ou algum módulo para o Kernel, é importante lembrar que o código será executado em Kernel Space. Isto significa que qualquer erro de programação ou mesmo de lógica pode causar danos incalculáveis ao ambiente (desde simples congelamentos à perda completa do sistema). Realizar depurações em Device Drivers é um trabalho complexo e necessita que o programador tenha bons conhecimentos de Sistemas Operacionais e do Kernel (a depuração de Device Drivers está fora do escopo deste artigo).
Para este artigo, usaremos como exemplo um ambiente Fedora Core 14 utilizando o Kernel 2.6.35. O uso de distribuições com algum aplicativo de gerenciamento de pacotes de software (como o apt-get de distribuições baseadas no Debian ou o yum no caso de distribuições baseadas no Red-Hat) facilita o trabalho de instalação dos pacotes.
Os primeiros pacotes a serem instalados são o kernel-headers e o kernel-devel. Estes pacotes possuem as bibliotecas e o código fonte do kernel, respectivamente, necessários para a compilação. Para a instalação dos pacotes no Fedora, execute os seguintes comandos como usuário root:
# yum install kernel-devel # yum install kernel-headers |
Em seguida, deve-se instalar os pacotes das ferramentas utilizadas para compilar o Kernel, os pacotes make e o gcc.
# yum install make # yum install gcc |
Uma vez que os pacotes estejam instalados, o próximo passo é a criação do arquivo Makefile e o código fonte do próprio Device Driver.
Para implementar o Device Driver, deve-se iniciar o código fonte referenciando o cabeçalho module.h. Neste artigo, será usado um módulo bem simples como exemplo, que chamaremos de helloworld.c. Na primeira linha do módulo, é adicionado o cabeçalho module.h:
#include <linux/module.h> #include <linux/kernel.h> |
Após a adição dos cabeçalhos, deve-se colocar a macro MODULE_LICENSE. Esta macro é utilizada para informar ao Kernel se o módulo está sob a licença GPL ou se é um código proprietário. Esta macro reconhece, entre outras, as licenças: "GPL", "GPL v2" e "Dual MIT/GPL".
Os módulos devem conter uma função init_module, que deve retornar um int e não recebe nenhum parâmetro. Esta função contém o código que será executado quando o módulo for inserido no Kernel. Além desta função, o módulo deve conter outra função, chamada cleanup_module, que é executada todas as vezes que o módulo é removido do Kernel. O módulo helloworld ficaria assim:
MODULE_LICENSE("GPL");
int init_module(void) {
printk (KERN_INFO “Hello World\n”);
return 0;
}
void cleanup_module (void) {
printk (KERN_INFO “Goodbye World\n”);
}
|
O código do módulo helloworld.c está pronto. O próximo passo consiste em criar o arquivo Makefile usado pela ferramenta make para compilar o módulo. O arquivo deverá ter o conteúdo a seguir (observe que a indentação deve ser feita usando tabulação – tab – e não espaços):
ifneq ($(KERNELRELEASE),) obj-m := helloworld.o else KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) clean: rm -rf *.o *~ *# *.symvers core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order module: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules endif |
Após criar o arquivo Makefile, o diretório deverá conter os dois arquivos criados até o momento, o Makefile e o helloworld.c
# ls Makefile helloworld.c |
Para compilar o módulo, execute o comando make passando como parâmetro a diretiva module. Observe que no arquivo Makefile temos a seguinte diretiva:
module:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
|
Essa linha será transformada em:
make -C /lib/modules/2.6.34.7-61.fc13.i686.PAE/build M=/root/DRIVER modules |
O comando para compilar o módulo será:
# make module |
Após a compilação, serão gerados diversos arquivos:
# ls helloworld.c helloworld.ko helloworld.mod.c helloworld.mod.o helloworld.o Makefile modules.order Module.symvers |
Entre todos estes arquivos, o que nos interessa é o arquivo com o sufixo .ko (helloworld.ko). Este é o arquivo que vamos usar no comando insmod para anexar o nosso driver no Kernel. Desta forma, para adicionarmos o módulo, usamos o seguinte comando:
# insmod helloworld.ko |
Para verificar se o modulo foi inserido com sucesso, use o comando lsmod para listar os módulos carregados:
# lsmod Module Size Used by helloworld 594 0 .... |
Durante o desenvolvimento do módulo, utilizamos o comando printk para imprimir algumas mensagens triviais. É importante notar que, ao programar em Kernel Space, não teremos acesso a algumas das funções mais convencionais, como printf e malloc.
O printf recebe um parâmetro a mais, que define o tipo de mensagem que será enviada pelo Kernel; no caso do módulo construído aqui, será uma mensagem de informação (KERN_INFO) e de acordo com as configurações do syslog, mensagens de informação enviadas pelo Kernel são escritas no arquivo /var/log/messages do Linux (esta configuração pode ser alterada facilmente no arquivo /etc/syslog.conf). No caso de não haver configuração no syslog referente à mensagem enviada pelo Kernel, a mensagem será enviada para o terminal serial do sistema.
Para verificar as mensagens, abra o arquivo /var/log/messages do Linux e procure por uma linha parecida com a que se segue:
# cat /var/log/messages ... Oct 31 00:25:07 vogon kernel: Hello World ... |
A remoção do módulo deve ser feita com o comando rmmod como a seguir:
# rmmod helloworld |
Ao remover o módulo, o Kernel imprimirá uma linha no arquivo /var/log/messages:
# cat /var/log/messages ... Oct 31 02:04:03 vogon kernel: Goodbye World |
Caso haja necessidade de limpar os arquivos criados no momento da compilação do módulo, execute o comando make passando como parâmetro a diretiva clean. Este parâmetro (e também o parâmetro module, usado no passo da compilação do módulo) pode ser modificado de acordo com a necessidade do usuário. Outras diretivas também podem ser criadas.
# make clean |
A criação de módulos do Kernel exige uma série passos e algumas ferramentas para que tudo funcione, entretanto não são passos complexos de se executar. A parte mais complexa do processo é a leitura e o entendimento do código fonte do próprio Kernel. A realização de depuração do Kernel também não é uma tarefa trivial de ser realizada, mas existem algumas ferramentas que auxiliam nesta tarefa.
Ferramentas de Depuração para Linux:
- GDB: http://www.gnu.org/software/gdb/
- KDB: http://oss.sgi.com/projects/kdb/
- kcore: http://www.centos.org/docs/5/html/5.2/Deployment_Guide/s2-proc-kcore.html
Mais Informações sobre Device Drivers:
[1] Corbet, Jonathan, Alessandro Rubini, Greg Kroah-Hartman, and Alessandro Rubini. Linux Device Drivers. Beijing: O'Reilly, 2005.
[2] Love, Robert. Linux Kernel Development. Upper Saddle River, NJ: Addison-Wesley, 2010.
[3] Bovet, Daniel P., and Marco Cesati. Understanding the Linux Kernel. Beijing: O'Reilly, 2006.
[4] Cooperstein, Jerry. Writing Linux Device Drivers: a Guide with Exercises. [Beaverton, OR.]: J. Cooperstein, 2009.
[5] Salzman, Peter Jay., Michael Burian, and Ori Pomerantz. The Linux Kernel Module Programming Guide. [United States]: CreateSpace, 2009.
Doutorando na Universidade de São Paulo, mestre em Eng. Elétrica e bacharel em Ciência da Computação. Experiência em administração de sistemas Unix e Linux, linguagem de programação C e ambientes de alta disponibilidade. Ver perfil no My developerWorks.