Desenvolvendo Device Drivers no Linux

Uma das principais características do Linux é a facilidade de criação de novos Drivers de Dispositivos. Neste artigo, serão abordados alguns conceitos sobre sistemas operacionais e como utilizar as ferramentas e interfaces disponibilizadas pelo Linux para dar o primeiro passo na criação de novos Drivers.

Artur Baruchi, Engenheiro de Software, IBM

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.



28/Dez/2010 (Primeira publicação 28/Dez/2010)

Introdução

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.


Kernel Space e User Space

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).


Preparando o Ambiente

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.


Implementação

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

Conclusão

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.


Referencias

Ferramentas de Depuração para Linux:

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.

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Linux
ArticleID=605306
ArticleTitle=Desenvolvendo Device Drivers no Linux
publish-date=12282010