Documente Academic
Documente Profesional
Documente Cultură
had
5.1 Introdução
Neste capítulo será apresentada uma introdução à programação de módulos do kernel Linux.
Um módulo do kernel, ou LKM de Linux Kernel Module, é um segmento de código que pode
ser inserido ou removido a quente, i.e., sem necessidade de reinicializar o sistema, e que
adiciona funcionalidade ao kernel.
Os módulos correm no chamado espaço do kernel (kernel space), enquanto uma aplicação
corre no espaço de utilizador (user space), mesmo que o utilizador corrente seja o supervisor.
No espaço do kernel tudo é permitido ao passo que no espaço de utilizador existe protecção
quanto ao acesso não autorizado a recursos como periféricos e memória.
Esta funcionalidade é possível porque os processadores de hoje em dia implementam pelo
menos dois níveis, ou modos de operação. A família x86 tem mesmo mais do que dois níveis
de operação. Nos níveis inferiores algumas operações não são permitidas.
Assim em Linux, e de um modo geral em Unix, o kernel executa-se no nível mais alto do
CPU, também chamado modo supervisor (supervisor mode), enquanto as aplicações são
executadas no nível mais baixo, o nível de utilizador, onde o processador regula o acesso
directo aos recursos de hardware.
Cada modo de operação pode ter o seu próprio mapeamento de memória, isto é o espaço de
endereçamento diferente. Endereços virtuais iguais em modo de utilizador e modo supervisor
podem ser mapeados em endereços físicos diferentes.
No primeiro caso o kernel opera em benefício do processo que efectuou a chamada, e é capaz
de aceder a dados no espaço de endereçamento do processo.
No segundo caso, o código que trata as interrupções não está sincronizado com nenhum
processo. Poderá estar a responder a uma interrupção de um periférico.
Um tipo de módulo é o device driver, que permite ao kernel aceder a dispositivos de entrada-
saída. Este tipo de módulos serão abordados num capítulo posterior.
lsmod
Na primeira coluna é indicado o nome do modulo, que normalmente é o nome do ficheiro sem
extensão. Na segunda coluna é indicado o seu tamanho em bytes, na terceira um contador de
utilização do módulo. Tipicamente refere-se a dispositivos abertos ou sistemas de ficheiros
montados. Um módulo não pode ser removido enquanto o contador de utilização não for zero.
A última coluna indica que módulos dependem do módulo corrente. No exemplo anterior o
módulo usb_storage depende do módulo usbcore, isto é de algum modo usb_storage acede
a alguma funcionalidade disponibilizada por usbcore.
Para remover módulos com dependências, primeiro terão de ser removidos os módulos que
dele dependem.
Se esta coluna indicar [permanet] o módulo não pode ser removido. Se indicar (unused) o
módulo nunca foi usado, i.e, que nunca esteve numa situação em que não poderia ser
removido.
cat /proc/modules
Para inserir um modulo pode-se usar o comando insmod que como argumento espera o
caminho do código objecto do módulo a inserir. como o hello.ko deve-se usar:
A partir da versão 2.6 do kernel o código objecto dos módulos tem a extensão *.ko.
Para remover é usado o comando rmmod onde o argumeno é o nome do módulo como
indicado por lsmod.
A inserção de módulos pode ser efectuada de um modo inteligente pelo comando insmod:
Neste caso o argumento é apenas o nome do ficheiro com o código objecto do módulo sem
extensão. O caminho completo do ficheiro com o código objecto do módulo encontra-se no
ficheiro:
/lib/modules/<versão do kernel/modules.dep
/lib/modules/2.6/c.ko: /lib/modules/2.6/b.ko
Assim após o caminho para o código do módulo, tem-se as lista de dependências precedida
por dois pontos (:).
Deste modo, o comando modprobe antes de carregar o módulo pedido carrega os módulos na
lista de dependências, da direita para a esquerda. A remoção será efectuada em sentido
inverso.
Este ficheiro pode ser automaticamente criado com o comando depmod. Este comando
analisa todos os módulos na directoria:
/lib/modules/<versão do kernel
Os módulos podem ser automaticamente carregados pelo kernel quando necessário. Para isso
existe o processo kerneld, que quando é necessário um módulo que não está carregado
executa modprobe para o carregar e indica ao kernel se a operação teve ou não sucesso.
/* hello.c
*/
#include <linux/module.h> /* porque é um módulo */
#include <linux/kernel.h> /* para KERN_ALERT */
int init_module(void) {
printk("<1>Hello world\n");
return 0; // Um valor não nulo indica que init_module falhou
} // não tendo sido carregado.
void cleanup_module(void) {
printk(KERN_ALERT "Goodbye world\n");
}
int init_module(void)
void cleanup_module(void)
Ao ser removido o módulo a função cleanup_module ( ) deve desfazer tudo o que foi feito
por init_module ( ).
Um uso possível também para os módulos é substituir o código de uma função do kernel.
Normalmente é efectuado qualquer operação e depois chamada a função original. Este desvio
da chamada para código do módulo e depois para a função original é efectuado por
init_module ( ). Assim, ao ser removido o módulo deve repôr o estado original do sistema de
modo que este não fique instável. Tal deve ser efectuado por cleanup_module ( ).
A novas versões do kernel definem duas macros que permitem alterar o nome destas duas
funções para outro definido pelo programador. Se o objectivo fosse alterar o nome das
funções de entrada e saída respectivamente para hello_init ( ) e hello_exit ( ), deveriam ser
colocadas no final do código as macros definidas em <linux/init.h>:
module_init (hello_init);
module_exit (hello_exit);
Como já foi referido este módulo é muito simples mas tem alguma funcionalidade, imprime
uma mensagem quando é inserido e outra quando é removido, respectivamente:
Hello world e
Goodbye world
Ambas as funções usam printk ( ) para imprimir as mensagens. No entanto esta função
consiste num mecanismo de log para o kernel. De facto todas as mensagem “impressas” com
printk são adicionadas ao registo que se encontra em /var/log, e que pode ser consultado com
o comando dmesg.
Assim as mensagens são impressas apenas se o módulo for inserido ou removido numa
consola TTY (modo texto). Se os comandos forem dados num terminal virtual como por
exemplo o xterm as mensagens não são impressas, tendo de ser usado dmesg para as
visualizar. O prótotipo de printk ( ) é semelhante ao de printf:
No entanto, como mostra o exemplo anterior, no ínicio da string de formatação fmt, pode ser
indicado um nível de prioridade numérico, ou usada uma macro como definido em
<linux/kernel.h>:
Supondo que o modulo estava dividido em dois ficheiros, por exemplo a função
init_module() num ficheiro chamado init.c e a função cleanup_module() num ficheiro
chamado exit.c, bastaria alterar as linhas que indicam os ficheiros a compilar, no início do
makefile:
Após compilar o módulo pode-se aceder a informação sobre este com o comando:
modinfo hello.ko
filename: hello.ko
vermagic: 2.6.14-mini 386 gcc-3.3
depends:
Que indica o nome do ficheiro, a versão do kernel, o processador para o qual foi compilado
(neste caso seria para 386 e sucessores), e qual o compilador em vermagic:.
Na última linha indicaria dependências se existissem. Para módulos mais complexos este
comando pode retornar mais informação.
que indica que o kernel pode ter sido corrompido. Esta mensagem aparece porque não foi
especificada uma licença no módulo. Este mecanismo surgiu no kernel 2.4 para indicar se o
código é aberto ou proprietário. A licença pode ser indicada com a macro seguinte. Pode ser
colocada logo após a inclusão dos headers no início do código, mas não é obrigatório:
MODULE_LICENSE("GPL");
ou
MODULE_LICENSE("Proprietary");
MODULE_AUTHOR("Nome do autor");
MODULE_DESCRIPTION("Simples módulo de demonstração: Hello world");
MODULE_VERSION("1.0");
MODULE_ALIAS("Olá!");
As macros acima indicadas, e de um modo geral todas as começadas por MODULE_ podem
ser usadas em qualquer parte do código, existe no entanto uma convenção de as colocar no
fim do código. É conveniente que quando se desenvolvem módulos as 3 primeiras macros
sejam usada.
filename: hello.ko
author: Nome do autor
description: Simples módulo de demonstração: Hello world
version: 1.0
alias: Olá
vermagic: 2.6.14-mini 386 gcc-3.3
depends:
srcversion: 6D52F7CBC1A3718D85C980E
O campo srcversion é uma soma dos ficheiros que compõe os módulos. Isto ajuda quem
desenvolve os códigos a verificar se os ficheiros fonte disponíveis foram os usados para
compilar um dado módulo, pois é possível que um programador faça alguma alteração num
módulo e não indique uma nova versão com MODULE_VERSION.
Fig. 5-1: Empilhamento de módulos do driver do porto paralelo (in Corbet et. al. 2005)
No caso do porto paralelo, o módulo de mais baixo nível parport disponibiliza funções de
baixo nível para os módulos de mais alto nível: parport_pc e lp, o driver da impressora.
Todos os módulos exportam também símbolos para a API do kernel.
Nestes casos é conveniente usar o comando modprobe para carregar um módulo como por
exemplo lp, e todos os outros do qual este módulo depende.
Como já foi referido, as dependências podem ser verificas com o comando modinfo ou no
ficheiro /lib/modules/<versão do kernel/modules.dep.
EXPORT_SYMBOL (identificador);
EXPORT_SYMBOL_GPL (identificador);
A segunda faz com que o símbolo seja acessível fora do módulo apenas para módulos com
licença GPL.
Assim nem todas as funções da libc estão disponíveis para módulos. Estes usam apenas os
símbolos disponibilizados pelo kernel. Estes símbolos poderão ser listados com:
cat /proc/kallsyms
Repare-se que parte das funções da libc são wrappers para chamadas ao sistema, e não é
necessário usá-las em modo kernel. Por exemplo a função puts() efectua a chamada write. Se
se pretender determinar quais as chamadas ao sistema efectuadas por uma aplicação, pode-se
usar o comando strace. Por exemplo compilando o programa seguinte:
/* hello.c
*/
#include <stdio.h>
main () {
puts(“Hello world”);
}
E executando:
strace hello
Têm-se um traçado das chamadas ao sistema. As primeiras têm a ver com a inicialização do
processo pelo sistema operativo. No final encontra-se uma linha com a chamada write
efectuada por puts().
A definição da função de inicialização, embora não seja obrigatório, deve ser no formato:
Como já foi visto por defeito não os símbolos locais dos módulos não são exportados, por isso
não mesmo que a função não seja declarada como static não é visível fora do módulo.
O modificador __init, indica ao carregador de módulos que após o módulo ter sido
inicializado, a função de inicialização pode removida da memória, deixando-a disponível.
Existe também uma etiqueta semelhante __initdata para libertar da memória dados utilizados
apenas na incialização:
Deve-se tomar cuidado para não se utilizar __init e __initdata com funções e dados
necessários após a inicialização.
A função de encerramento, que liberta recursos utilizados pelo módulo antes deste ser
descarregado, é definida como:
<linux/moduleparam.h >. Esta macro recebe três parâmetros: o nome da variável, o seu tipo e
as permissões para expôr os parâmetros do módulo numa entrada sysfs, se o valor for
diferente de zero.
Convém referir que sysfs é um sistema de ficheiros virtual, que surgiu com o kernel 2.6, e que
exporta informação sobre dispositivos e drivers para o espaço de utilizador. Pode ser usado
também para configuração.
Se o valor deste último parâmetro for nulo não existe entrada em sysfs. Para dar permissões
devem ser usadas as macros definidas em <linux/stat.h>. Por exemplo:
Então para se poder aceder aos argumentos passados no exemplo anterior, o código seria:
Repare-se que os nomes dos parâmetros da linha de comando têm de coincidir com as
variáveis do código. Além disso devem ser dados valores por defeito aos argumentos, pois o
módulo poderá ser carregado sem que todos os parâmetros sejam especificados na linha de
comando.
(...)
depends:
parm: str:Uma cadeia de caracteres (charp)
parm: count:int
Assim se não for usada esta macro é apenas indicado o nome do parâmetro e o seu tipo. Se for
usada é também apresentada a descrição.
ulong
ushort Inteiros. O prefixo u indica sem sinal.
Podem também ser definidos arrays como parâmetros, onde os valores são separados por
vírgulas:
insmod modulo.ko array = 10,3,4
Se o tentar utilizador passar mais itens do que a dimensão do array um erro é apresentado na
consola e o módulo não é carregado. Por isso talvez seja boa prática descrever estes
parâmetros indicando também a sua capacidade.
Seguidamente é apresentado o exemplo de um módulo completo:
/* modpar.c
* Demonstração de passagem de argumentos da linha de comandos para módulo.
*/
#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_ALERT */
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/stat.h>
module_init(start);
module_exit(stop);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Helder Daniel");
MODULE_VERSION("1.0");
MODULE_DESCRIPTION("Demonstração de passagem de argumentos da linha de
comandos para módulo.");
Agora em sysfs é possível obter informação sobre o módulo. Para visualizar o valor dos
parâmetros basta editar o conteúdo dos ficheiros com o mesmo nome, por exemplo::
cat /sys/module/modpar/parameters/count
As aplicações, i.e, processos em modo de utilizador, são mapeadas na memória virtual com
uma pilha muito grande. Nesta pilha é colocado não só o endereço de retorno das funções,
mas também todas as variáveis automáticas criadas pelas funções, i.e., os argumentos das
funções e as variáveis declaradas localmente nas funções.
O kernel por outro lado tem uma pilha que pode ser tão pequena como uma única página de
4KBytes. As funções de um módulo partilham esta pilha com as funções de todos os outros
módulos e do restante código do kernel. Por isso não se deve declarar variáveis locais muito
grandes, como por exemplo:
int init_module(void) {
unsigned char localbuffer [1000];
// ....
}
A alocação de variáveis desta natureza deve ser efectuada dinamicamente quando o módulo
for chamado para efectuar uma qualquer função.
O código do kernel não pode efectuar operações de vírgula flutuante. Refira-se que não são
necessárias na programação do kernel e que a sua utilização implica uma sobrecarga. É fácil
verificar a natureza desta sobrecarga quando um processo efectua uma chamada ao sistema.
Antes de se entrar em modo kernel o contexto do processo deve ser preservado, pelo menos os
registo do CPU, pois não é possível determinar quais registos o código do kernel vai utilizar.
Ao terminar a chamada, o conteúdo dos registos é reposto antes de se devolver a execução ao
processo.
Assim existiria uma perda de tempo desnecessária se se tivesse de guardar e repor também os
valores dos registos de vírgula flutuante.
Normalmente um módulo, por exemplo um driver, executa uma dada tarefa a pedido de um
processo que corre em modo utilizador. Este é o caso de uma chamada ao sistema como open
ou write. Um modo de se aceder a partir do módulo ao descritor do processo que invocou a
chamada, i.e, à estrutura task_struct, definida em <linux/sched.h>, é através do ponteiro
global current, definido em <asm/sched.h>. O seguinte código permite retornar o nome do
processo corrente, i.e, os primeiros 15 caracteres do nome do ficheiro executável, e o seu PID:
make menuconfig
Para que se possa inserir um módulo no kernel a versão do módulo e do kernel devem ser
iguais, mais precisamente a vermagic. A vermagic do kernel pode ser determinada com:
uname –r
Para que este comando possa ser utilizado, como já foi referido, pelo menos o caminho para
módulo terá de estar referido no ficheiro:
http://w3.ualg.pt/~hdaniel/pin PIN 2005-6 T05 - 13/15
Periféricos e Interfaces
had
/lib/modules/2.6.14-mini/modules.dep
Além disso este comando é potencialmente inseguro, pois permite forçar a instalação de um
módulo compilado para uma versão anterior do kernel presente no sistema.
Um meio mais seguro consiste em instalar no sistema as fontes do kernel que está a ser
executado é alterar a vermagic para que seja igual. Repare-se que a vermagic original do
kernel indica a sua versão, e é tão simples como por exemplo 2.6.14, no entanto algumas
distribuições alteram a vermagic. Por exemplo a distribuição Mandrake adiciona –mdk ao
número da versão: 2.6.14-mdk.
Para alterar a vermagic pode-se aceder à configuração do kernel, como já foi referido, e no
menu principal escolher a opção General Setup, entrando-se no menu:
A primeira opção permite que seja adicionada uma string à versão do kernel. No exemplo será
–mini, ficando 2.6.14-mini.
Agora é necessário actualizar os ficheiros do código fonte. Para isso basta executar make até
que seja compilada a 3ª linha SPLIT include/linux/autoconf.h -> include/config/*,
interrompendo a compilação com <ctrl-c>:
CHK include/linux/version.h
UPD include/linux/version.h
SPLIT include/linux/autoconf.h -> include/config/*
HOSTCC scripts/mod/modpost.o
HOSTLD scripts/mod/modpost
CC init/main.o
Como nota final deve ser referido que a opção Module versioning support no menu de
configuração apresentado na figura Fig. 5-3, é possível inserir alguns módulos mesmo que as
vermagics sejam diferentes. No entanto esta opção é experimental, não garante que todos os
módulos possam ser inseridos. Também não existe garantia que um kernel pré-compilado, o
tenha sido com esta opção habilitada.
5.7 Bibliografia
Corbet, Jonathan, Alessandro Rubini e Kroah-Hartman, Greg (2005). “Linux Device Drivers
3rd edition”, O’Reilly, http://lwn.net/Kernel/LDD3/
Salzman, Peter Jay, Michael Burian and Ori Pomerantz (2005). “The Linux Kernel Module
Programming Guide”, http://www.tldp.org/LDP/lkmpg/2.6/lkmpg.pdf