The Linux Kernel Module Programming Guide

Sommario

  1. Prefazione
  2. Introduzione
  3. Ciao Mondo
  4. Fasi preliminari
  5. Comportamento dei driver dei device
  6. Il filesystem /proc
  7. Usare /proc per l'input
  8. Comunicare con i file dei device
  9. Chiamate di sistema
  10. Bloccare i processi
  11. Rimpiazzare printk
  12. Pianificare compiti
  13. I gestori degli interrupt
  14. Multi-processing simmetrico
  15. Insidie comuni
  16. Appendice A
  17. Appendice B


Ciao, Mondo (parte 1): Il modulo più semplice

Quando il primo programmatore delle caverne scolpì il primo programma sul muro del primo computer delle caverne, fu un programma che disegnava la stringa "Ciao, mondo" con i graffiti. I libri di testo romani sulla programmazione iniziano con il programma "Salut, Mundi". Non so cosa possa succedere alle persone che rompono questa tradizione, ma penso sia più sicuro non scoprirlo. Inizieremo con una serie di programmi "hello world" che mostrano vari aspetti delle conoscenze di base nella scrittura di moduli del kernel.

Qui vediamo il modulo più semplice possibile. Non compilarlo ancora; analizzeremo la compilazione di un modulo nella prossima sezione.

Esempio 3-1. hello-1.c

/*  
 *  hello-1.c - Il modulo più semplice.
 */
#include <linux/module.h>	/* Necessario a tutti i moduli */
#include <linux/kernel.h>	/* Necessario per la macro KERN_INFO */

int init_module(void)
{
	printk(KERN_INFO "Ciao mondo 1.\n");

	/* 
	 * Un valore di ritorno diverso da 0 indica che init_module è fallita; il modulo non può essere caricato
	 */
	return 0;
}

void cleanup_module(void)
{
	printk(KERN_INFO "Arrivederci mondo 1.\n");
}

I moduli del kernel devono avere almeno due funzioni. Una funzione "iniziale" (inizializzazione) detta init_module() che è chiamata quando il modulo è inserito nel kernel tramite insmod, e una funzione "finale" (di pulizia) detta cleanup_module() che è chiamata prima che il modulo venga rimosso con rmmoded. In realtà, le cose sono cambiate a partire dal kernel 2.3.13. Si può utilizzare qualunque nome che piaccia per la funzione di inizializzazione e di cleanup, e si imparerà a farlo nella Sezione XXXX2.3XXXX. Infatti, il nuovo metodo è quello preferito. Comunque, molti continuano ad utilizzare init_module() e cleanup_module() per le proprie funzioni iniziali e finali.

Tipicamente, init_module() o registra un handler con il kernel per fare una serie di azioni, oppure rimpiazza una della funzioni del kernel stesso con un codice proprio (usualmente questo codice fa una serie di azioni e poi chiama la funzione originale). Si suppone che la funzione cleanup_module() disfaccia tutto ciò che ha fatto init_module(), in modo tale che il modulo possa essere rimosso tranquillamente.

Infine, ogni modulo del kernel ha bisogno di includere linux/module.h. Nell'esempio abbiamo dovuto includere linux/kernel.h solo per la macro KERN_ALERT utilizzata per definire il livello di log della printk(), che imparerai nella Sezione XXXX.

Introduzione a printk()

Nonostante quello che si potrebbe pensare, printk() non aveva lo scopo di comunicare informazioni all'utente, anche se l'abbiamo utilizzato esattamente per questo in hello-1! E' stato pensato, invece, per essere un meccanismo di logging per il kernel, ed è utilizzato per loggare informazioni e mostrare dei warning. Perciò, ogni dichiarazione di printk() viene fornita di un livello di priorità, che, nell'esempio, è la KERN_ALERT (<1>). Ci sono 8 priorità e il kernel possiede delle macro per queste, quindi non c'è il bisogno di utilizzare numeri criptici. Si possono vedere le macro (e il loro significato) nel file linux/kernel.h. Se non si specifica un livello di priorità, verrà utilizzata quella di default: DEFAULT_MESSAGE_LOGLEVEL.

E' importante prestare una certa attenzione nel leggere le macro di priorità. L'header file descrive anche cosa significa ogni priorità. In pratica, è bene non utilizzare i numeri, come <4>. Si utilizzino sempre le macro, come KERN_WARNING.

Se la priorità è minore di int console_loglevel, il messaggio è stampato sul proprio terminale corrente. Se sia syslogd che klogd stanno girando, allora il messaggio verrà inserito in coda a /var/log/messages, indipendentemente dal fatto che venga stampato sulla console. Nell'esempio abbiamo utilizzato una priorità alta, come KERN_ALERT, per essere sicuri che il messaggio di printk() venga stampato sulla console piuttosto che inserito nel file di log. Quando si scrivono moduli reali, si utilizzano le priorità che meglio si adattano alla situazione in esame.

Compilare i moduli del kernel

I moduli del kernel devono essere compilati un po' differentemente dalle applicazioni userspace regolari. Le vecchie versioni del kernel richiedevano molta attenzione riguardo queste impostazioni, che sono, di solito, memorizzate nei Makefiles. Sebbene siano organizzate gerarchicamente, sono presenti molte impostazioni ridondanti nei Makefiles e ciò fa in modo che sia abbastanza difficile da mantenere. Fortunatamente, c'è un nuovo modo di fare queste cose, chiamato kbuild, e il processo di generazione dei moduli esterni è adesso completamente integrato nel meccanismo standard. Per saperne di più su come compilare moduli che non fanno parte del kernel ufficile (come ad esempio tutti gli esempi di questa guida), si veda il file linux/Documentation/kbuild/modules.txt.

Analizziamo ora un semplice Makefile per compilare un modulo chiamato hello-1.c:

Esempio 3-2. Makefile per un modulo basilare del kernel

obj-m += hello-1.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Da un punto di vista tecnico solamente la prima linea è realmente necessaria, i target "all" e "clean" sono stati aggiunti per convenienza.

Adesso si può compilare il modulo digitando il comando make. Dovresti ottenere un output che somigli al seguente:

hostname:~/lkmpg-examples/02-HelloWorld# make
make -C /lib/modules/2.6.11/build M=/root/lkmpg-examples/02-HelloWorld modules
make[1]: Entering directory `/usr/src/linux-2.6.11'
  CC [M]  /root/lkmpg-examples/02-HelloWorld/hello-1.o
 Building modules, stage 2.
  MODPOST
  CC      /root/lkmpg-examples/02-HelloWorld/hello-1.mod.o
  LD [M]  /root/lkmpg-examples/02-HelloWorld/hello-1.ko
make[1]: Leaving directory `/usr/src/linux-2.6.11'
hostname:~/lkmpg-examples/02-HelloWorld#

Si noti che il kernel 2.6 introduce una nuova convenzione sui nomi dei file: i moduli del kernel hanno adesso l'estensione .ko (invece della vecchia estensione .o) che permette una facile distinzione dai file oggetto convenzionali. La ragione di ciò risiede nel fatto che i file .ko contengono una sezione addizionale (.modinfo) in cui vengono mantenute informazioni addizionali. Vedremo tra breve a cosa possono servire queste informazioni.

Per vedere di che tipo di informazione si tratta viene usato il comando: modinfo hello-*.ko

hostname:~/lkmpg-examples/02-HelloWorld# modinfo hello-1.ko
filename:       hello-1.ko
vermagic:       2.6.11 preempt PENTIUMII 4KSTACKS gcc-3.3
depends:

Niente di spettacolare, finora. Però se utilizziamo modinfo su uno degli esempi futuri, hello-5.ko, vedremo:

hostname:~/lkmpg-examples/02-HelloWorld# modinfo hello-5.ko
filename:       hello-5.ko
license:        GPL
author:         Peter Jay Salzman
vermagic:       2.6.11 preempt PENTIUMII 4KSTACKS gcc-3.3
depends:
parm:           myintArray:An array of integers (array of int)
parm:           mystring:A character string (charp)
parm:           mylong:A long integer (long)
parm:           myint:An integer (int)
parm:           myshort:A short integer (short)
hostname:~/lkmpg-examples/02-HelloWorld# 

Sono presenti moltissime informazioni. Il nome dell'autore per la segnalazione di eventuali bug, informazioni sulla licenza, persino una piccola descrizione dei parametri che il modulo accetta.

Ulteriori dettagli sui Makefiles dei moduli del kernel sono disponibili in linux/Documentation/kbuild/makefiles.txt. Bisogna essere sicuri di aver letto quanto appena esposto e il file specificato prima di iniziare a mettere mano sui Makefiles: in questo si rispagna molto lavoro.

Adesso è arrivato il momento di inserire il nostro modulo fresco di compilazione all'interno del kernel con insmod ./hello-1.ko (ignora qualsiasi cosa che sembra non funzionare, ne discuteremo a breve).

Tutti i moduli caricati nel kernel sono listati in /proc/modules. Va alla fine del file e utilizza cat per vedere che il tuo modulo fa veramente parte del kernel. Congratulazioni, adesso sei l'autore di parte del codice del kernel Linux. Quando la novità sparisce, rimuovi il modulo dal kernel usando rmmod hello-1. Dai un'occhiata a /var/log/messages per vedere se effettivamente l'operazione di rimozione è stata loggata nel file di log del sistema.

Adesso c'è un altro esercizio per il lettore. Avete presente il commento sul valore di return in init_module()? Cambiate il valore con qualcosa di negativo, rimcompilate e caricate il modulo di nuovo. Cosa succede?

Ciao, Mondo (parte 2)

Dalla versione 2.4 del kernel Linux, si possono rinominare le funzioni di init e cleanup dei propri moduli; infatti non devono essere chiamate per forza rispettivamente init_module() e cleanup_module. Questo è reso possibile della macro module_init() e module_exit(). Queste macro sono definite in linux/init.h. L'unica attenzione da avere è quella di definire le funzioni di init e cleanup prima di chiamare le macro, pena errori di compilazione. Vediamo un esempio di questa tecnica:

Esempio 3-3. hello-2.c

/*  
 *  hello-2.c - Dimostrazione dell'utilizzo delle macro module_init() e module_exit().
 *  Questa tecnica è da preferire all'uso di init_module() e cleanup_module()
 */
#include <linux/module.h>	/* Necessario per tutti i moduli */
#include <linux/kernel.h>	/* Necessario per KERN_INFO */
#include <linux/init.h>		/* Necessario per le macro */

static int __init hello_2_init(void)
{
	printk(KERN_INFO "Ciao, mondo 2\n");
	return 0;
}

static void __exit hello_2_exit(void)
{
	printk(KERN_INFO "Arrivederci, mondo 2\n");
}

module_init(hello_2_init);
module_exit(hello_2_exit);

Quindi, a questo punto, abbiamo due moduli reali sotto le nostre mani. Aggiungere un altro modulo è semplice:

Esempio 3-4. Makefile for entrambi i nostri moduli

obj-m += hello-1.o
obj-m += hello-2.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Adesso diamo un occhiata a linux/drivers/char/Makefile per un esempio concreto. Come si può vedere, qualcosa è stata inserita nel kernetl (obj-y) ma dove sono andate tutte le obj-m? Chi ha familiarità con gli script di shell può facilmente individuarli. Per quelli che non hanno familiarità, le voci obj-$(CONFIG_FOO) che si vedono ovunque vengono espanse in obj-y o in obj-m a seconda del fatto che la variabile CONFIG_FOO è stata settata a 'y' o a 'm'. Già che ci siamo, queste erano esattamente il tipo di variabili che avete impostato nel file linux/.config l'ultima volta che avete dato il comando make menuconfig o qualcosa di simile.