Tesi di Laurea di primo livello: I moduli in linux

POLITECNICO DI TORINO

 

 

III Facoltà di Ingegneria dell’Informazione

Corso di Laurea in Ingegneria Informatica

 

 

 

 

Monografia di Laurea di primo livello

 

 

 

I moduli in Linux

 

 

 

 

 

Candidato: Pianfetti Maurizio

Relatore: Tutor aziendale:

Prof. Pietro Laface ing. Francesco Rolando

Agosto 2003


Indice

Introduzione

1. I moduli

1.1. Device driver

1.1.1. I tipi di dispositivi hardware definiti dal kernel

1.1.2. Creazione e cancellazione di uno special device file

1.1.2.1. Esempio di shell script per la creazione e cancellazione di uno special device file per la scheda PCI “DSP2”

1.1.3. Le operazioni su file che possono essere richiamate sui special device file a carattere

1.2. Le funzioni principali

1.3. Differenze tra il kernel 2.2.x e 2.4.x

2. Implementazione di un modulo per un dispositivo PCI

2.1. Caratteristiche di un dispositivo PCI

2.1.1. Caratteristiche della scheda PCI “DSP2”

2.2. Init_module per un dispositivo PCI

2.2.1. Rilevamento del dispositivo e allocazione delle risorse

2.2.1.1. Allocazione della memoria

2.2.1.2. Gestione del segnale di interrupt e conclusione con successo della probe

2.2.1.3. Gestione degli errori

2.2.2. Registrazione del device driver e assegnazione del major number

2.3. Cleanup_module per un dispositivo PCI

2.3.1. Cancellazione della registrazione del device driver

2.3.2. Deallocazione delle risorse

2.4. Le funzioni principali definite dalla struttura my_fops

2.4.1. Le funzioni open e release

2.4.2. Le funzioni read e write

2.4.3. La funzione iocnt

2.5. Esempio conclusivo per un dispositivo PCI

3. Come usare il modulo dal livello applicativo

3.1. Compilazione di un modulo e regole di utilizzo del comando make

3.1.1. Creazione del file Makefile.am per la compilazione di un modulo

3.1.2. Creazione del file Makefile.am per la compilazione di una libreria dinamica

3.1.3. Creazione del file Makefile.am per la compilazione di una libreria statica

3.1.4. Compilazione di più progetti in directory diverse con un unico comando

3.2. Caricamento del modulo e utilizzo a livello applicativo

3.2.1. Accesso al dispositivo dal livello applicativo

3.2.1.1. Apertura del dispositivo

3.2.1.2. Uso del dispositivo

3.2.1.3. Chiusura del dispositivo

3.2.1.4. Esempio di un programma applicativo che usa il dispositivo

Bibliografia

Articoli e documenti online

 


Introduzione

 

La seguente monografia è indicata per un lettore che abbia conoscenze informatiche avanzate: in particolare che sappia cos’è un kernel e abbia una conoscenza base del sistema operativo Linux.

Verrà illustrato cosa sono i moduli di Linux e la loro implementazione. In particolare si partirà da un modulo per una scheda generica PCI per arrivare a descrivere un esempio pratico di un modulo per la scheda PCI “DSP2” creata dall’azienda Inxide S.R.L. .

L’esempio pratico è stato sviluppato nel corso di uno stage presso la Inxide S.R.L. nel quale l’autore ha potuto studiare i moduli di Linux e impararne l’implementazione.

Nell’implementazione del modulo verranno presentate le differenze tra il kernel 2.2.x e 2.4.x .

Questa monografia può essere utilizzata come guida per chi debba sviluppare un modulo in ambiente Linux.

Un doveroso ringraziamento va al Prof. Pietro Laface per l’aiuto dato, all’Ing. Francesco Rolando coordinatore del progetto del modulo per la scheda PCI “DSP2”, a Mariangela e alla mia famiglia per il sostegno morale dato.


1. I moduli

Una delle migliori caratteristiche di Linux è la capacità di espandere le funzionalità del kernel a run-time[1] attraverso parti di codice che possono essere compilate, caricate e scaricate indipendentemente dal resto del kernel. Queste sezioni di codice sono i moduli di Linux.

Un modulo del kernel può implementare un device driver, un file system, o un protocollo di networking. In questo contesto verranno trattati solo i moduli di tipo device driver.

Il supporto di Linux per la gestione dei moduli si divide in tre componenti:

  • il manager dei moduli: caricamento in memoria dei moduli, controllo della loro attività e loro interazione con il resto del kernel;
  • la registrazione dei driver: permette ai moduli di informare il kernel che un nuovo driver è disponibile aggiornando una tabella dinamica (questo argomento verrà trattato con maggior dettaglio nei prossimi capitoli);
  • la risoluzione dei conflitti: un meccanismo che permette a driver diversi di riservare risorse hardware e di proteggere queste risorse dall'uso accidentale da parte di un altro driver.

1.1. Device driver

 

Un device driver è il codice di livello più basso che viene eseguito su un elaboratore, in quanto è direttamente connesso alle caratteristiche hardware del dispositivo periferico.

Un elaboratore è costituito da uno o più processori e da un insieme di dispositivi periferici. In questo caso il kernel può essere considerato un'applicazione che si appoggia sui device driver: ogni driver si occupa di una singola parte dell’elaboratore, mentre il kernel fornisce il multitasking e l'accesso ai files usando le periferiche a disposizione.

L’utilizzo dei device driver rende astratta la manipolazione dei dispositivi, che vengono visti dal sistema come dei normali file: possono essere aperti, chiusi, letti e scritti con le stesse system-call[2] standard dei file. I file di questo tipo sono più comunemente chiamati special device file. Essi servono per permettere al sistema di supportare le varie risorse hardware. I file speciali permettono la comunicazione tra i dispositivi fisici e i device driver del sistema. Ad ogni device driver, che è responsabile di una particolare unità periferica, è assegnato un proprio codice principale. La lista dei drivers e dei loro codici principali è disponibile nella directory /proc/devices, esaminabile con il comando da terminale

cat /proc/devices.

Ad ogni dispositivo fisico gestito da un certo driver è assegnato un codice secondario.

Ogni dispositivo all’interno del sistema è rappresentato, attraverso il suo codice secondario, da uno special device file, che è creato nella directory /dev quando il sistema viene installato. Questo argomento verrà ampliato più avanti con l’esempio trattato.

 

1.1.1. I tipi di dispositivi hardware definiti dal kernel

Linux definisce tre tipi diversi di hardware supportati. Ogni tipo di hardware diverso definisce un tipo di special device file differente. Per ogni tipo vengono definite funzioni differenti per il modulo mentre a livello applicativo l’accesso al file non cambia.

I tre tipi di dispositivi possono essere:

  • a carattere ( character device ): tali dispositivi possono essere considerati dei file, per il fatto che possono venire letti o scritti. Esempi sono la console (il video e la tastiera) e le porte seriali e parallele. Un dispositivo a carattere di solito può solo venire letto o scritto sequenzialmente;
  • a blocchi ( block device ): storicamente si trattava di dispositivi che potevano essere letti o scritti solamente in dimensioni multiple della dimensione del blocco: spesso 512 o 1024 bytes. Si tratta di dispositivi sui quali è possibile montare un filesystem; i dispositivi a blocchi più importanti sono i dischi. I blocchi di un dispositivo sono gestiti mediante una strategia di “buffer cache” per migliorare l'efficienza nell'accesso “casuale” al dispositivo;
  • interfacce di rete ( network device ): le interfacce di rete non ricadono nell'astrazione dei file. Le interfacce di rete sono identificate da un nome (come eth0 o plip1), ma non vengono mappate nell'albero dei files. Tale mappatura sarebbe possibile, in teoria, ma non sarebbe comoda né per il programmatore né per la performance ottenuta: un'interfaccia di rete può solo trasferire dei pacchetti di dati, e l'astrazione del file in /dev non gestisce efficientemente il trasferimento di dati strutturati.

Nel corso di questa monografia si parlerà solo dei dispositivi di tipo carattere. Ogni sviluppatore di moduli che implementa un driver, dal primo momento deve decidere che tipo di special device file sviluppare.

Per la scheda PCI “DSP2” è stato scelto il tipo carattere poiché la trasmissione di dati è a dimensione variabile e non a blocchi e il dispositivo non ha una interfaccia di rete.

 

1.1.2. Creazione e cancellazione di uno special device file

Uno special device file – sia per block che character device - è creato dal comando mknod ( da supervisore o root ) e descrive il dispositivo utilizzando i codici principale ( major number ) e secondario ( minor number ). I dispositivi di network sono ugualmente rappresentati da uno special device file che però è creato da Linux nel momento in cui trova ed inizializza il rispettivo controller.

I parametri del comando mknod sono ( definiti nel seguente ordine ):

  • nome del dispositivo: il nome del file che verrà creato, e mediante il quale si avrà accesso al livello applicativo;
  • tipo di special device file: si indica con la lettera “c” un dispositivo di tipo carattere e con la lettera ”b” un dispositivo di tipo blocco;
  • major number: bisogna specificare il codice principale a cui fa riferimento lo special device file in modo da risalire al corretto device driver;
  • minor number: è importate specificare il numero secondario poiché potrebbero esserci sullo stesso elaboratore più dispositivi gestiti dallo stesso device driver. Quindi ogni dispositivo deve avere assegnato uno special device file differente.

Normalmente lo special device file viene creato dopo il caricamento del modulo e una cosa molto comoda da fare è creare un breve shell script[3] in modo da poter creare diversi special device file per più dispositivi uguali e ricavare in modo automatico il major number dal file /proc/devices.

Un consiglio è anche di cancellare i file creati una volta che non si vuole più utilizzare il device driver per qualche motivo, con il normale comando rm nomefile.

 

1.1.2.1. Esempio di shell script per la creazione e cancellazione di uno special device file per la scheda PCI “DSP2”

La scheda PCI “DSP2” ( le cui caratteristiche verranno trattate in seguito ) si può considerare come un dispositivo di tipo carattere. Per precauzione si è pensato di poter gestire fino ad un massimo di quattro schede contemporaneamente e si è deciso di chiamare il corrispondente special file /dev/DSP2pci .

Ecco il frammento di codice per la creazione dei file speciali:

 

#!/bin/sh

# Script per la creazione degli special device file

MODULE="DSP2pci"

DEVICE="DSP2pci"

MODE="764" # rw-rw-r—

# rimozione dei vecchi nomi

rm -f /dev/${DEVICE}[0-3]

# caricamento major number

MAJOR=`grep $MODULE /proc/devices | awk '{print $1}'`

# creazione nodi

cd /dev

mknod /dev/${DEVICE}0 c $MAJOR 0

mknod /dev/${DEVICE}1 c $MAJOR 1

mknod /dev/${DEVICE}2 c $MAJOR 2

mknod /dev/${DEVICE}3 c $MAJOR 3

# Attributi

chmod $MODE /dev/${DEVICE}[0-3]

Come si può vedere il major number viene ricavato ogni volta che si avvia lo script. Un’altra cosa importante sono i diritti che si danno ai files creati in modo che solo l’utente root potrà modificarlo e avviarlo.

Se per esempio il file si chiamasse crea_sdf.sh, potrà essere avviato nel seguente modo: sh crea_sdf.sh.

In modo analogo si possono cancellare i file creati in precedenza con il seguente script

!/bin/sh

# Script per la cancellazione degli special device file

MODULE="DSP2pci"

DEVICE="DSP2pci"

# rimozione dei nodi

rm -f /dev/${DEVICE}[0-3]

 

1.1.3. Le operazioni su file che possono essere richiamate sui special device file a carattere

Non appena il dispositivo a carattere è inizializzato,il suo device driver si registra con il kernel aggiungendo un record nel vettore chrdevs della struttura dati device_struct.

Ogni record del vettore chrdevs contiene due elementi, un puntatore al nome del device driver registrato ed un puntare al blocco che contiene le operazioni su file che possono essere richiamate, questa struttura è detta file_operations.

Questa struttura contiene gli indirizzi delle funzioni che i device driver utilizzano per compiere le operazioni sui dispositivi (open, close, read, write…).

Il vettore chrdevs mantiene il contenuto della directory /proc/devices per i character devices.

Per ogni tipo di dispositivo il codice per realizzare le operazioni su file è diverso; convenzionalmente però i nomi delle funzioni all’interno del file_operations è comune per tutti i dispositivi. Un driver che non implementa una certa operazione assegnerà un valore NULL al relativo puntatore.

Le operazioni su file più importanti sono le seguenti:

  • loff_t (*llseek) (struct file *, loff_t, int);
    questa funzione è utilizzata per cambiare la posizione corrente di scrittura/lettura nel file e restituisce la nuova posizione;
  • ssize_t (*read) (struct file *, char *, size_t, loff_t *);
    è usata per ricevere dati dal dispositivo;
  • ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
    è usata per inviare dati al dispositivo;
  • int (*readdir) (struct file *, void *, filldir_t);
    è usata per leggere le directory ed è utile solomente per i file system;
  • unsigned int (*poll) (struct file *, struct poll_table_struct *);
    questa funzione è utilizzata per vedere se un dispositivo è in stato di lettura, scrittura o in qualche altro stato speciale;
  • int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    è una system-call che permette di inviare specifici comandi ad un dispositivo, come resettare un dispositivo;
  • int (*mmap) (struct file *, struct vm_area_struct *);
    mappa lo spazio di memoria di un dispositivo utilizzato da un processo;
  • int (*open) (struct inode *, struct file *);
    è la prima operazione realizzata su di un dispositivo;
  • int (*release) (struct inode *, struct file *);
    è invocata per chiudere un dispositivo;
  • int (*lock) (struct file *, int, struct file_lock *);
    è utilizzata per impedire l’accesso ad un file;
  • ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
    ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
    sono utilizzate quando un’applicazione deve fare un accesso singolo in lettura o scrittura ad aree multiple di memoria.

La struttura file_operations e l’implementazione delle singole funzioni saranno approfondite nei prossimi capitoli.

 


1.2. Le funzioni principali

 

Un modulo non è altro che un insieme di funzioni in C. Poiché il kernel Linux è stato scritto in linguaggio C e poiché sono state introdotte nei sorgenti delle variabili col nome di direttive del C++ ( esempi sono new e delete[4] ) non è possibile creare un modulo in C++ poiché richiamando i sorgenti del kernel essi verrebbero compilati in C++ e il compilatore non potrebbe riconoscere queste variabili, ma le vedrebbe come direttive rilevando una serie di errori che farebbero fallire la compilazione. Di solito si inizia il sorgente includendo gli header necessari, ma in questo caso prima bisogna sempre definire i simboli __KERNEL__ e MODULE, l’alternativa sarebbe definirli come parametri del comando al tempo della compilazione

#define __KERNEL__

#define MODULE

le due librerie da includere per poter usare il modulo sono:

#include <linux/module.h>

#include <linux/kernel.h>

nella prima libreria sono anche definite le macro per risalire alla versione del kernel corrente, mentre nella seconda è definita per esempio la funzione printk l’analoga della funzione printf in C.

Module.h oltre a richiamare le funzioni base per un modulo, include anche version.h dove sono definite le macro per definire il kernel che si usa.

 

#if ((LINUX_VERSION_CODE >= KERNEL_VERSION(2,3,0))&&(LINUX_VERSION_CODE < KERNEL_VERSION(2,5,0)))

#define KERN 24

#endif

#if ((LINUX_VERSION_CODE >= KERNEL_VERSION(2,1,0))&&(LINUX_VERSION_CODE < KERNEL_VERSION(2,3,0)))

#define KERN 22

#endif

Nel frammento di codice vengono usate le macro LINUX_VERSION_CODE che restituisce un numero binario in base alla versione corrente del kernel caricato e KERNEL_VERSION che da una versione specificata dal programmatore restituisce il suo numero binario. Nell’esempio si verifica se il kernel caricato è il 2.3.x o il 2.4.x e si definisce KERN24 oppure se è il 2.1.x o 2.2.x e si definisce KERN22 . In ogni caso si definisce una define che servirà poi per implementare una funzione piuttosto che un’altra. In questo modo si tiene conto anche delle versioni non stabili ( la 2.1.x per il release[5] 2.2.x e la 2.3.x per la release 2.4.x ) per maggior completezza.

Poiché il modulo opera in kernel mode non è possibile stampare su video le informazioni ma si possono scrivere messaggi attraverso la printk nel file di log del kernel. Il file è /var/log/messages visibile attraverso il comando da terminale

cat /var/log/messages .

Un modulo del kernel deve avere al minimo due funzioni: init_module, chiamata al momento del caricamento, e cleanup_module, chiamata al momento della rimozione. La prima funzione si occupa dell'inizializzazione del modulo: ricerca dell'hardware e registrazione del nuovo device driver all'interno delle tabelle del kernel; la seconda ha invece il compito di rilasciare le risorse usate dal modulo e di cancellare il device driver dalle tabelle del kernel.

Se non si volesse usare i nome init_module e cleanup_module, sono definite due macro per richiamare funzioni analoghe con nomi diversi:

module_init(my_init);

module_exit(my_cleanup);

le due macro sono definite in “<linux/init.h>“ . Il vantaggio è che ogni procedura di inizializzazione e di rimozione può avere un nome unico.

Nella dichiarazione delle due funzioni si userà la seguente forma:

static int __init my_init(void)

{

....

}

static void __exit my_cleanup(void)

{

....

}

L’attributo __init, usato in questo modo, provocherà la rimozione della procedura d’inizializzazione e la deallocazione della memoria che occupava, una volta finito il suo compito L’attributo __exit provoca invece la rimozione della funzione a cui è riferito.

Si usa la direttiva static poiché è un modo per rendere privati tutti i simboli definiti. Il kernel è un insieme di procedure molto complesso, ed è importante mantenere il suo spazio dei nomi il più pulito possibile. Il device driver nella sua forma finale esporterà solo i simboli init_module e cleanup_module, che sono usati per caricare e scaricare il modulo. Niente altro deve essere pubblico, in quanto il modulo viene utilizzato tramite i puntatori registrati nel kernel, non tramite il nome delle funzioni.

Essenzialmente la init_module deve:

  • trovare il dispositivo da gestire;
  • allocare le risorse, memoria e IRQ;
  • registrare il nuovo dispositivo trovato nel kernel per poi poterlo gestire.

Le grandi differenze tra il kernel 2.2.x e 2.4.x sono a questo punto, nell’implementazione della init_module e cleanup_module. Inoltre le funzioni usate dipendono dal dispositivo in questione. Per questo motivo si preferisce evidenziare le differenze base nel prossimo paragrafo e presentare solo le funzioni per gestire una scheda PCI nel Capitolo 2. Il lettore potrà implementare un modulo analogo per un altro dispositivo sulla falsariga di ciò che viene presentato in questi capitoli.

Il passo successivo è la creazione della struttura file_operations. Essa va definita in coda alla definizione delle funzioni che si vogliono utilizzare al livello applicativo. Questo perché i nomi delle funzioni devono già essere definiti.

Dopo la dichiarazione delle due funzioni base, se per esempio si vogliono ridefinire le funzioni load, release, read, write, ioctl , si potrà scrivere nel seguente modo

// indirizzo da cui partire, dati, tipo lunghezza per dato, offset

static ssize_t my_read(struct file Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito filp, char Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito buf, size_t size, loff_t *poff)

{

return 0;

}

// indirizzo da cui partire, dati, tipo lunghezza per dato, offset

static ssize_t my_write(struct file Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito filp, const char Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito buf, size_t size, loff_t *poff)

{

return 0;

}

// struttura che lega dispositivo e device driver, struttura del disp, comando, argomenti

static int my_IoControl(struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg)

{

}

// struttura che lega dispositivo e driver, struttura del disp

static int my_release(struct inode Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito inode, struct file Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito file)

{…

return 0;

}

static struct file_operations my_fops = {

read: my_read, // manda dati al dispositivo

write: my_write, // legge dati dal dispositivo

ioctl: my_IoControl, // manda comandi al dispositivo

open: my_open, // prima operazione realizzata nel dispositivo, inizializzazione

release: my_release // invocata per chiudere un dispositivo

};

 

I nomi delle funzioni possono essere scelte a piacere e “collegate” ai nomi delle primitive attraverso la struttura file_operations.

È possibile definire al caricamento del modulo il valore di alcune variabili all’interno del modulo, ma esse devono essere inizializzate con le seguenti macro

MODULE_PARM (variabile, "tipo");

sono supportati cinque tipi di parametri:

  • b = byte;
  • h = short;
  • i = integer;
  • l = long;
  • s = string.

Infine - per completezza - ci sono alcune macro per definire l’autore del modulo e la descrizione di esso, visibili una volta che il modulo è caricato con il comando modinfo . Le macro sono

MODULE_AUTHOR (name); per inserire il nome dell’autore sul file,

MODULE_DESCRIPTION (desc); per inserire la descrizione del modulo nel file.



1.3. Differenze tra il kernel 2.2.x e 2.4.x

La grande differenza tra un modulo compilato per il kernel 2.2.x e il kernel 2.4.x è sostanzialmente che nel 2.4.x è supportata la modalità hotplugging[6]. Questa modalità serve per poter mantenere in memoria strutture dati o funzioni a partire dal momento in cui viene caricato il modulo. In questo modo, quando viene richiamata la funzione o utilizzata la struttura dati, essendo già parte del kernel il procedimento sarà più veloce.

Esistono tre direttive per usare la modalità hotplugging:

· __devinitdata: usata per le strutture dati, serve per conservare la struttura in memoria dopo la sua inizializzazione;

· __devinit: usata per le funzioni, indica che il codice della funzione deve essere conservato in memoria dopo la sua inizializzazione;

· __devexit: usata per le funzioni che vogliono deallocare la memoria quando hanno finito la loro mansione.

La modalità hotplugging è spesso supportata dai dispositivi PCI o Hard Disk SCSI che si possono disinserire dal sistema quando il sistema è ancora in esecuzione. Una funzione definita con __devinit può partire quando il dispositivo è inserito, oppure se una funzione è definita __devexit può essere richiamata quando il dispositivo è disinserito.

Per agevolare questa modalità nei device driver per il kernel 2.4.x si definisce sempre una particolare struttura che definisce il tipo di dispositivo. Inoltre, si definiscono altre due funzioni che servono per allocare le risorse nel momento in cui si rileva il dispositivo, e per liberarle al momento in cui il dispositivo è disinserito.

Il modulo quindi serve per caricare queste funzioni nel kernel e avviarle nel momento in cui il dispositivo è inserito.

Per esempio la struttura dati per una scheda PCI è

static __devinitdata struct pci_device_id my_idtable[]= {

// vendor, device, subvendor, subdevice, class, class_mask, driver_data

{ vendor: PCI_VENDOR,

device: PCI_DEVICE,

subvendor: PCI_SUBVENDOR,

subdevice: PCI_SUBDEVICE,

class: 0, // class e class_mask a zero perche' sono definite vendor e device

class_mask: 0,

driver_data: 0 },

{0,}

};

il significato di questi campi sarà dettagliato nel capitolo successivo. La struttura va definita parte del kernel attraverso una macro:

MODULE_DEVICE_TABLE(tipo dispositivo, my_idtable);

Le funzione richiamata al momento del rilevamento di un dispositivo è:

static int __devinit pcilaser_probe (struct pci_dev *pdev, const struct pci_device_id *pid)

{

return 0;

}

nel prossimo capitolo verrà illustrato un esempio d’uso, ma la cosa fondamentale in questo contesto è notare che la funzione ha come parametro la struttura ( in questo caso di tipo PCI ) del dispositivo.

L’analoga funzione per la liberazione delle risorse è:

static void __devexit pcilaser_remove (struct pci_dev *rdev)

{

}

 

Come si può capire nel kernel 2.2.x queste funzioni non esistono, ma normalmente si implementano ugualmente e vengono richiamate rispettivamente dalla init_module per allocare le risorse e dalla cleanup_module per liberarle. Il motivo della simulazione è rendere più chiare le operazioni da fare.

Mentre nel kernel 2.4.x i dispositivi sono rilevati automaticamente, nel kernel 2.2.x bisogna usare apposite funzioni in base al tipo di dispositivo.

Normalmente in un device driver, si usa una struttura di dati che è utilizzata nelle diverse funzioni, dove vengono annotati gli indirizzi di memoria, l’interrupt[7] del dispositivo e altre informazioni utili. Per esempio

typedef struct DEVICE_EXTENSIONtag

{

// Configurazione hardware

unsigned long SysAddr; // Indirizzo memoria di sistema

unsigned long MemAddress; // Indirizzo fisico della memoria

unsigned long MemLength;// Lunghezza di memoria

unsigned long IrqNumber; // Numero IRQ

void * DeviceObject; // puntatore alla struttura pdev

int index; // indice del dispositivo corrente ( in caso di multischede )

// Puntatori ai dati

unsigned long IdmaDataLen; // Max. lunghezza dati

unsigned long * IdmaData; // Dati memoria regione 0

unsigned long * IdmaLatch; // Latch Indirizzo porta

unsigned long * IdmaStatus; // Indirizzo del target

}DEVICE_EXTENSION, *PDEVICE_EXTENSION;

Questo esempio illustra una struttura possibile, ma per ogni device driver questa sarà diversa secondo le varie esigenze.

Nel kernel 2.4.x questa struttura verrà inizializzata e collegata al kernel con apposite funzioni e può essere immediatamente ricavata nelle altre funzioni, ma nel kernel 2.2.x la struttura deve essere di tipo globale visibile ovunque e quindi comporta una maggior uso di memoria statica. Quindi dopo gli header

PDEVICE_EXTENSION disp;

Per i motivi elencati sopra è evidente che nel kernel 2.2.x è più difficile gestire più dispositivi, al momento del rilevamento bisogna identificarli e non sempre è facile. Nel kernel 2.4.x invece è il kernel che li rileva e basterà formare una lista di strutture dati differente per ogni dispositivo trovato. Oltre che inizializzare la lista si deve anche dichiarare una variabile globale che nell’esempio si chiamerà devindex a 0 per indicare il numero di dispositivi aperti. Essa verrà incrementata e decrementata nelle funzioni probe e remove.

Altre piccole differenze sono nella registrazione della struttura file_operations nel kernel. Nel kernel 2.2.x si usa la macro

MODULE_SUPPORTED_DEVICE(MODNAME)

come parametro gli si passa il nome del modulo con cui verrà registrato nel sistema, mentre nel kernel 2.4.x si usa la macro

SET_MODULE_OWNER(&my_fops);

Inoltre nel kernel 2.4.x è possibile assegnare come informazione ulteriore ( che può essere vista col comando modinfo ) la licenza del modulo con la macro

MODULE_LICENSE("GPL");

Una cosa interessante da scoprire è che se non si dichiara una licenza GPL ( licenza open source di GNU ) durante la compilazione del modulo si avrà un warning riferito alla licenza non GPL.


2. Implementazione di un modulo per un dispositivo PCI

 

PCI è uno standard per I/O su bus, introdotto nel 1991 dalla Intel e da diverse altre aziende, è ormai usato in quasi tutte le architetture.

Inizialmente era un bus interno a 32 bit passato a 64 bit in PCI 2.1. Anche la frequenza di clock[8] inizialmente di 33Mhz è passata a 66Mhz in PCI 2.1 fino 133MHz in PCI-X. Inoltre è supportato il trasferimento burst mode[9] e la modalità bus mastering[10]. È anche supportata la tecnologia ti tipo Plug-and-Play[11] e supporta dispositivi di tipo hot-plugging .

 


2.1. Caratteristiche di un dispositivo PCI

Ogni dispositivo PCI ha sempre un processore a bordo per elaborare dati e ottenere i risultati per il quale è stato progettato. Sul dispositivo, quindi, esistono sempre dei registri di stato I/O o memoria locale. Possono esistere fino a 6 regioni di memoria, ciascuna delle quali definisce o uno spazio di indirizzi di porte di I/O oppure uno spazio di indirizzi di memoria locale. Per risolvere i conflitti tra periferiche, in fase di configurazione, i dispositivi PCI possono associare diversi indirizzi alla propria memoria locale.

Durante la fase di bootstrap[12], il BIOS interroga tutti i dispositivi PCI ed assegna a ciascuno di essi alcuni spazi di indirizzi, registrandoli in appositi registri di configurazione. In genere i device driver leggono in fase di inizializzazione i registri di configurazione per conoscere quali spazi di indirizzi e vettori di IRQ sono assegnati alla scheda.

Ogni periferica PCI ha quattro diversi pin associati a linee di interruzione: #A, #B, #C e #D. Una periferica PCI è spesso costituita da più dispositivi ( per esempio 4 processori ), e ciascun dispositivo può far uso di un proprio segnale di IRQ. Tutti i segnali interruzione PCI vengono inviati separatamente all' Interrupt Controller del sistema, e quindi convertiti in segnali di interruzione per la CPU. I dispositivi PCI possono condividere lo stesso vettore di IRQ (ad esempio l'IRQ 9 può essere utilizzato contemporaneamente dal controller Ethernet e dalla scheda “DSP2” ) .

Per identificare un dispositivo PCI esistono dei registri di configurazione che permettono di capire di quale particolare periferica si tratta. I registri - tutti a 16 bit - sono

· Class: questo registro identifica il tipo di periferica. Le classi standard sono per esempio la classe video o SCSI. Il codice di classe SCSI è per esempio 0x0100;

· Vendor: descrive il venditore della periferica PCI. Per esempio quello di Intel è 0x8086;

· Device: un codice unico che descrive il dispositivo stesso.

Quest’ultimo è una chiave calcolata nel seguente modo

Leggendo la figura da destra a sinistra:

· Function number: identifica un dispositivo della periferica (da 0 e 7, bit 0-2);

· Device number: identifica una periferica (slot) sul bus (da 0 a 31, bit 3-7) ;

· Bus number: identifica il bus PCI (da 0 a 255, bit 8-16).

In particolare, i registri elencati sopra servono ad identificare la scheda se è supportata la modalità Plug and Play dal sistema.

 

2.1.1. Caratteristiche della scheda PCI “DSP2

La scheda “DSP2” controlla le marcature laser. L'hardware di questa scheda è composto da un DSP della ADSP ( Analogic Device System Processor ) modello 2185 montato su scheda PCI 2.2 che offre tutta la funzionalità per guidare un sistema di marcatura laser. Permette il completo pilotaggio di 3 motori “stepper” ad asse controllato per combinare lavori ottici e meccanici. E' stata creata completamente con tecnologia di SMT, con una potenzialità di calcolo offerta dal DSP di 75 MIPS che permette la correzione ottica e “micro-vettorizzata” per produrre un paio di coordinate ad una precisione di 16 bit ogni 1.5ms. La comunicazione delle coordinate di marcatura alla testa di scansione è totalmente in formato digitale per garantire l'immunità da disturbi elettromagnetici.

Il firmware[13] del ADSP ha il compito di correggere gli errori di marcatura dovuti agli sfasamenti ottici degli specchi e regolare la potenza del laser.

Per la scheda si dovrà gestire un segnale di interrupt e si ha a disposizione un’area di memoria da 16Kbytes ( regione di memoria 0 ). Una cosa molto importante è che non ha un suo controller DMA e quindi non bisognerà gestirlo semplificando di molto le cose.


2.2. Init_module per un dispositivo PCI

Dopo che il modulo viene caricato, con una struttura device driver già descritta, la prima cosa da fare è rilevare il dispositivo interessato e allocargli le risorse ( indirizzi di memoria locale e segnale di interrupt ). Poiché ci sono alcune differenze di gestione tra il kernel 2.2.x e il 2.4.x, ogni volta che sarà opportuno esse saranno evidenziate. Dopo questa fase bisogna assegnare il major number al dispositivo e registrare il nuovo dispositivo nel kernel. Nell’esempio seguente si gestirà un dispositivo di tipo carattere. Alla fine del Capitolo verrà presentato parte del codice del device driver sviluppato come esempio.

2.2.1. Rilevamento del dispositivo e allocazione delle risorse

 

Come è già stato spiegato, nel kernel 2.4.x il dispositivo viene rilevato quando esso è presente sul bus. La procedura che mette in ascolto il kernel è la funzione pci_module_init , definita in “<linux/pci.h> insieme a tutte le strutture per descrivere un dispositivo PCI.

int pci_module_init(struct pci_driver *drv);

in verità, se il dispositivo non è presente nel momento in cui si richiama questa funzione, essa restituisce un errore di tipo –ENODEV, costante definita in “<linux/errno.h>”, per indicare che non ha trovato il dispositivo. Come argomento bisogna passare la struttura my_driver definita precedentemente per poter richiamare le funzioni di probe e remove che verranno definite più avanti.

Per il kernel 2.2.x, per il quale la struttura my_driver non esiste, bisogna usare la funzione pci_find_device definita anch’essa in “<linux/pci.h>“ .

struct pci_dev *pci_find_device(unsigned int vendor, unsigned int device, struct pci_dev *from);

spesso prima ancora di cercare il dispositivo ( poiché in passato non era così diffusa la tecnologia PCI ) si usa la funzione pci_present definita in “<linux/pci.h>

int pci_present(void);

che serve per accertarsi che il supporto PCI nel sistema esista. Come si può vedere alla funzione pci_find_device si passano gli argomenti vendor e device in modo da trovare il dispositivo PCI esatto. Se il dispositivo viene rilevato, viene restituita una struttura dati contenente diverse informazioni sul dispositivo, a questa struttura verrà poi collegata la struttura dati utile per le varie funzioni. Quest’ultima è una struttura definita dal kernel, accessibile da tutte le funzioni base. In questo caso per far compilare il modulo sia per il kernel 2.2.x che per il 2.4.x si è mantenuta la stessa logica e si richiama la probe in caso di successo della funzione pci_find_device, la funzione per allocare le risorse.

Nel kernel 2.4.x la funzione di tipo probe deve avere la seguente struttura

static int __devinit pcilaser_probe (struct pci_dev *pdev, const struct pci_device_id *pid)

{

return 0;

}

dove come parametri si passano la struttura dati del dispositivo e le caratteristiche hardware definite dalla seconda struttura.

La struttura pdev è fondamentale, poiché è il puntatore alla periferica dove il kernel annota tutte le informazioni su come agire sul dispositivo. Se, per esempio, si vuole abilitare le funzioni wakeup o suspend[14] si deve usare la funzione pci_enable_device

int pci_enable_device(struct pci_dev *dev);

per verificare se il dispositivo supporta tale modalità, e come parametro si passa la struttura presentata prima. La funzione restituisce 0 se il supporto c’è, 1 se c’e qualche problema.

Il passo successivo è quello di allocare memoria per la struttura dati contenente le informazioni che servono nelle varie funzioni del device driver. Va ricordato che ogni modulo opera in kernel mode e quindi bisogna allocare memoria nel kernel space[15] . Dopo aver inizializzato un puntatore alla struttura interessata si usa la funzione kmalloc - analoga alla malloc -, definita in “<linux/malloc.h>

void *kmalloc(unsigned int size, int priority);

I parametri sono la dimensione dei dati da allocare e la priorità. Esistono diverse priorità definite in <linux/mm.h>, ma quella usata in questo caso è GFP_KERNEL per ovvi motivi. Il risultato sarà un puntatore all’area di memoria della struttura allocata.

Nel kernel 2.4.x dopo aver salvato tutte le informazioni sulle risorse in questa struttura allocata, bisognerà salvare nel kernel questa struttura collegandola alla struttura pdev in modo da potervi fare riferimento quando serve. La funzione per far questo è la pci_set_drvdata definita in “<linux/pci.h>”

void pci_set_drvdata (struct pci_dev *dev,void *driver_data);

dove il primo parametro sarà la struttura pdev e il secondo è la struttura dati da collegare, PDEVICE_EXTENSION in questo caso.

Nel kernel 2.2.x non ce n’è bisogno del collegamento col kernel perché la struttura è dichiarata a livello globale.

 

2.2.1.1. Allocazione della memoria

Come già spiegato, un dispositivo PCI ha fino a 6 regioni di memoria a cui bisogna accedere. Normalmente il BIOS della macchina su cui presente il dispositivo, in fase di bootstrap, avrà già assegnato dei puntatori fisici a questa area. Se si accedesse direttamente a questi indirizzi si potrebbe incorrere in problemi di lettura errata in fase di congestionamento del sistema al momento dell’arrivo di un interrupt, per questo motivo è preferibile riallocare la memoria con indirizzi dinamici gestiti interamente dal kernel. In questo modo il kernel si occupa di quella memoria come se fosse della RAM in più che in caso di congestionamento può deallocare se non è usata. La funzione che si occupa di questa procedura di riallocamento delle aree di memoria è la ioremap definita in “<asm/io.h>

void Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito ioremap(unsigned long offset, unsigned long size);

che utilizza come parametri l’indirizzo fisico della memoria da riallocare e la sua dimensione. Per ricavar l’indirizzo fisico inizializzato dal BIOS si usano due modi differenti nel kernel 2.2.x e 2.4.x .

Per il primo si usa direttamente un campo della struttura pdev: il campo base_address. Esso è un vettore di 6 elementi, ognuno contenente un valore per ogni regione di memoria. Fatta l’operazione logica AND con una costante PCI_BASE_ADDRESS_MEM_MASK definita in “<linux/pci.h> si ottiene l’indirizzo fisico della regione interessata.

Nel kernel 2.4.x la cosa è più semplice poiché si hanno a disposizione più funzioni che offrono maggiori controlli. Un primo controllo può essere fatto sulla dimensione della memoria a cui si vuole puntare. Sapendo la dimensione vera, con la funzione pci_resource_len definita in “<linux/pci.h>

unsigned long pci_resource_len (struct pci_dev *dev,int bar);

è possibile ricavare la dimensione secondo il kernel, che dovrà essere identica altrimenti c’è qualche errore. La funzione fa sempre riferimento alla struttura pdev inizializzata dal kernel al momento della rilevazione del dispositivo. Inoltre, è possibile verificare il tipo di memoria a cui punta la struttura con la funzione pci_resource_flags definita in “<linux/pci.h>

unsigned long pci_resource_flags(struct pci_dev*dev,int bar);

Esistono diversi tipi definiti in “<linux/ioport.h>“, ma in questo contesto interessa che il valore trovato dalla funzione in AND con la costante IORESOURCE_MEM dia come risultato 0.

Fatte queste verifiche, si può risalire all’indirizzo fisico con la funzione pci_resource_start definita nella solita libreria

unsigned long pci_resource_start(struct pci_dev*dev,int bar);

che prende come parametro la struttura che si è sempre chiamata pdev e la regione di memoria interessata.

 

2.2.1.2. Gestione del segnale di interrupt e conclusione con successo della probe

Ogni dispositivo ha uno o più segnali di interrupt, identificati da un numero da 1 a 15. Questo segnale è un avviso che il dispositivo manda al processore per avvisarlo che è successo qualcosa, il device driver deve accorgersi che il segnale è del dispositivo per il quale è stato creato e dare un servizio al dispositivo.

Per ricavare il numero di interrupt a cui è assegnato il dispositivo e gestire il servizio, per entrambi i kernel, si utilizza la funzione request_irq definita in “<linux/sched.h>

int request_irq(unsigned int irq, void (*handler)(),unsigned long flags, const char *dev_name, void *dev_id);

la funzione ha molti parametri di ingresso poiché gli interrupt possono essere diversi e bisogna identificare la funzione opportuna da attivare al momento della ricezione di un segnale di interrupt del dispositivo interessato. Il parametro irq è il numero di irq di cui si fa la richiesta, in effetti nel campo irq di pdev, c’è già il valore assegnato dal BIOS nel momento di bootstrap . Il parametro handler identifica la funzione da richiamare per servire l’interrupt. Il parametro flags identifica il tipo di interrupt. Un segnale di interrupt per un dispositivo PCI è condiviso con tutti i dispositivi PCI e quindi va settato come tale. La costante da mettere in questo caso è SA_SHIRQ, le altre sono comunque definite nella libreria citata prima. Il parametro dev_name è il nome del modulo in cui è implementata la funzione handler. Infine l’ultimo campo serve per identificare in modo univoco il dispositivo a cui è assegnato il segnale di interrupt. Questo va settato se si usano gli interrupt condivisi e fa riferimento ad una struttura dati univoca per ogni dispositivo, altrimenti va settato al valore NULL. La funzione restituirà 0 se l’assegnazione è avvenuta con successo, e un valore negativo in caso di errore. Va precisato che la prima cosa da fare, quando si richiama la funzione header, è verificare l’effettiva richiesta controllando direttamente i registri del dispositivo. Se la richiesta è effettivamente del dispositivo per cui è stato implementato il device driver, si deve resettare la richiesta sul dispositivo per indicare che è stato rilevato il segnale, altrimenti si deve uscire subito dalla funzione. Dopo questa registrazione, una volta avviato il modulo è interessante esaminare il file /proc/interruptus con

cat /proc/interrupts

e vedere l’assegnazione al nuovo dispositivo di un segnale di irq.

Nel kernel 2.4.x sono state aggiunte alcune procedure per la gestione di interrupt annidati o in coda. Se si spende troppo tempo nell'elaborare dati è possibile che vengano perduti uno o più interrupt successivi. La soluzione escogitata per questo problema è quella di dividere il lavoro di gestione dell'interrupt in due parti:

· Top half: gestisce la parte hardware e deve terminare prima che un nuovo interrupt si presenti. Normalmente, in questa routine si fa molto poco e quindi è veloce;

· Bottom half: viene eseguito con gli interupt nuovamente attivati e può impiegare tutto il tempo necessario per completare qualsiasi tipo di operazione richiesta. Questa routine viene eseguita il più presto possibile dopo che la richiesta di interrupt è stata soddisfatta.

Fortunatamente, il kernel prevede un particolare sistema per effettuare lo scheduling[16] del bottom half, che non è necessariamente legato ad un processo in particolare; questo significa che sia la richiesta di esecuzione della funzione che la esecuzione stessa sono fatte fuori del contesto di qualsivoglia processo. Per fare ciò è necessario un meccanismo speciale perché le altre funzioni del kernel operano tutte nel contesto di un processo mentre la gestione degli interrupt è asincrona, e non correlata ad un particolare processo.

Per la scheda PCI “DSP2” non è stata adottata nessuna di queste tecniche poiché per il funzionamento della scheda non è possibile che arrivino interrupt vicini in termini di tempo e quindi non è necessario accodarli.

Se si usa più di una scheda, alla fine della funzione probe bisognerà incrementare la variabile globale devindex e copiarla nel campo index della solita struttura. Conviene prima copiarla e poi incrementarla poiché i minor number partono da 0.

Nel kernel 2.2.x non c’è bisogno dell’incremento di devindex poiché si gestisce una sola scheda.

Poiché non c’è stato nessun problema la funzione restituirà 0.

 

2.2.1.3. Gestione degli errori

Poiché si allocano delle risorse, in caso di errori bisogna deallocarle, e uscire dalla funzione di tipo probe. In questo caso è molto comoda la direttiva goto definita dall’ANSI C.

Si è visto che i possibili errori possono verificarsi nella allocazione della memoria per la struttura PDEVICE_EXTENSION. In questo caso bisognerà solo deallocare la memoria usando la funzione kfree analoga alla free definita in “<linux/malloc.h>

void kfree(void *obj);

come parametro si passerà il puntatore alla struttura dati. Da qui si potrà saltare direttamente alla fine e prima di uscire con un valore negativo eseguire la funzione kfree.

Solo per il kernel 2.4.x, se si era usata la funzione pci_enable_device, bisognerà usare la funzione inversa pci_disable_device definita in “<linux/pci.h>“ per disattivare il dispositivo

int pci_disable_device(struct pci_dev *dev);

che come parametro ha la solita struttura pdev.

Altri possibili errori dipenderanno dalla rilevazione di errori prima di allocare la memoria delle regioni interessate del dispositivo. A questo punto bisognerà saltare al punto di prima.

Per deallocare le risorse di memoria, tra il kernel 2.2.x e 2.4.x esistono differenti funzioni. Nel primo caso, per dire al kernel di non considerare più un’area di memoria, si usa solo la funzione iounmap, definita in “<asm/io.h>

void iounmap(void *addr);

che prende come unico parametro il puntatore alla memoria.

Per il kernel 2.4.x bisognerà usare in più la funzione pci_release_regions definita in “<linux/pci.h>

void pci_release_regions (struct pci_dev *dev);

questo per dire ancora al kernel che non si ha bisogno di questa memoria.

L’ultimo problema potrebbe sorgere dopo aver allocato il numero di irq. Per liberare questa risorsa bisogna prima bloccare la ricezione di segnali irq e poi liberarla. Per bloccare l’arrivo di segnali di interrupt si usa la funzione synchronize_irq definita in “<asm/hardirq.h>

void synchronize_irq ();

è importante notare che questa procedura è bloccante per tutti i segnali di interrupt (anche se solo per qualche microsecondo nei sistemi odierni ) e per questo motivo solo dal kernel 2.6.x è stata variata e le si può passare come parametro il numero di irq interessato da liberare, in modo da bloccare solo questo tipo di interrupt.

La funzione per liberare la irq, cioè per non far più eseguire la funzione di servizio definita al momento dell’assegnazione, è la free_irq definita in ”<linux/sched.h>

void free_irq(unsigned int irq, void *dev_id);

che usa come parametri il numero dell’irq da liberare e la struttura definita al momento della registrazione, che rendeva univoca l’assegnazione al dispositivo interessato, cioé la struttura PDEVICE_EXTENSION . Il salto dall’errore questa volta sarà prima della restituzione delle altre risorse, in modo da liberarle tutte prima di uscire.

 

2.2.2. Registrazione del device driver e assegnazione del major number

La seconda fase della funzione init_module è registrare il modulo nel kernel di Linux. Linux mantiene un vettore di device driver, ciascuno dei quali è identificato da un numero, chiamato major number. Tale numero non è altro che l'indice del device driver all'interno del vettore dei device driver. Il major number di un dispositivo come già stato spiegato nei capitoli precedenti è assegnato al momento della creazione dello special device file. Tutti i dispositivi con lo stesso major number vengono serviti dallo stesso device driver. Il kernel usa un vettore statico per mantenere le informazioni dei device driver, e tale vettore contiene 255 device driver. Per la sua assegnazione si usa la funzione register_chrdev definita in “<linux/fs.h>

int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

il primo argomento passato a questa funzione può essere o il numero che viene richiesto oppure 0, nel qual caso viene eseguita una allocazione dinamica. La funzione ritorna un numero negativo per indicare un errore, e un numero maggiore o uguale a zero in caso di successo. Se è stata chiesta un'allocazione dinamica, il valore di ritorno, se positivo, è il numero che è stato assegnato. L'argomento name è il nome del device driver. Infine, fops è la struttura che viene usata per chiamare ogni altra funzione all'interno del device driver, quella che è stata inizializzata come my_fops.

L'allocazione dinamica del major number è una scelta vincente, si ha la sicurezza di avere un numero che non entra in conflitto con altri device driver all'interno del sistema: register_chrdev riesce sicuramente nel suo compito, a meno che si siano caricati così tanti device driver da esaurire i numeri disponibili, situazione alquanto improbabile. La scelta dinamica si può implementare nel kernel 2.4.x . Nel kernel 2.2.x il consiglio è quello di creare un ciclo e ogni volta assegnare un numero progressivo fino a quando la funzione non restituisca un numero positivo, il che significa che quel numero progressivo è disponibile. In ogni caso questo numero andrà memorizzato poiché servirà in seguito.

Finita la registrazione, la init_module può uscire con valore 0, cioè con nessun errore.

 


2.3. Cleanup_module per un dispositivo PCI

Questa funzione è l’inversa della init_module e viene richiamata dal kernel al momento dello scaricamento del modulo.

I compiti di questa funzione sono cancellare la registrazione del device driver e deallocare le risorse occupate.

 

2.3.1. Cancellazione della registrazione del device driver

Per poter cancellare il riferimento al modulo bisogna usare la funzione int unregister_chrdev definita in “<linux/fs.h>

int unregister_chrdev(unsigned int major, const char *name);

che usa come primo parametro il major number da liberare, e come secondo il nome del modulo. La funzione restituirà 0 in caso di successo e un valore negativo in caso di insuccesso, per esempio se trova il major number occupato.

 

2.3.2. Deallocazione delle risorse

 

Nel kernel 2.4.x viene richiamata la funzione my_remove

 

static void __devexit my_remove (struct pci_dev *rdev)

{

return 0;

}

che richiede come parametro la struttura che è sempre stata chiamata pdev da cui si può riferire la struttura di tipo PDEVICE_EXTENSION attraverso la funzione pci_get_drvdata definita in “<linux/pci.h>

void Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito pci_get_drvdata (struct pci_dev *dev);

che ha come parametro la struttura pdev e come valore di ritorno il puntatore alla struttura interessata.

Nel kernel 2.2.x la funzione my_remove potrà essere chiamata dopo la funzione unregister_chrdev, mentre la struttura dati essendo globale è già disponibile in tutto il programma.

Questa funzione dovrà usare tutte le funzioni descritte, quando le risorse venivano allocate, per la gestione degli errori. Vanno usate nel seguente ordine:

· Per il kernel 2.2.x : synchronize_irq, free_irq, iounmap, kfree;

· Per il kernel 2.4.x : synchronize_irq, free_irq, iounmap, kfree, pci_disable_device, pci_set_drvdata;

pci_disable_device e pci_set_drvdata sono le funzioni in più che servono rispettivamente per disabilitare il dispositivo PCI e per settare a NULL il puntatore alla struttura dati PDEVICE_EXTENSION che ormai non serve più. Per questo kernel andrà anche decremento il contatore devindex, poiché c’è un dispositivo in meno.


2.4. Le funzioni principali definite dalla struttura my_fops

 

In questa monografia verranno implementate e spiegate solo alcune delle funzioni che si possono usare dal livello applicativo. Quelle definite in my_fops sono comunque le principali funzioni usate in quasi tutti i device driver, e sono rispettivamente la open e la release, la read e la write e la ioctl: esse sono l’interfaccia tra il livello applicativo e l’hardware. Queste funzioni hanno una struttura definita dal kernel che interagisce tra la chiamata a livello applicativo e il livello modulo.

Una cosa importante è che queste funzioni operano su file, che in questo caso è lo special device file e quindi le strutture a disposizione saranno quelle definite per accedere ai file.

Con l’esempio finale si capiranno meglio questi concetti e i successivi.

 

2.4.1. Le funzioni open e release

Queste due funzioni sono rispettivamente la prima e l’ultima ad essere chiamate dal livello applicativo.

La open definita nel modo seguente

static int my_open(struct inode Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito inode, struct file Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito file)

{

return 0;

}

ha il compito di inizializzare il campo private_data della struttura file, che dovrà puntare alla struttura di tipo PDEVICE_EXTENSION, in modo che essa possa essere ricavata facilmente anche da tutte le altre funzioni.

Poiché nel kernel 2.4.x la struttura è sicuramente diversa per ogni dispositivo aperto che usa lo stesso device driver, per distinguere ogni dispositivo si ricava il minor number dalla struttura di tipo inode dal campo i_rdev con la macro MINOR definita in “<linux/fs.h>

MINOR(kdev_t dev);

in questo modo se si era definita una lista di strutture PDEVICE_EXTENSION la si può scorrere fino al dispositivo con lo stesso minor number e collegarlo alla struttura file. Va ricordato che il valore da confrontare è il campo index della struttura.

Per il kernel 2.2.x il problema non si pone poiché si gestisce un solo dispositivo e quindi si ha una unica struttura.

La funzione release è l’inversa della open e quindi bisognerà soltanto far puntare il campo private_data ad un valore NULL poiché il dispositivo sarà chiuso e rilasciato.

 

2.4.2. Le funzioni read e write

Queste due funzioni vengono usate per leggere dati dal dispositivo ( la read ) e per scrivere dati in esso ( la write ). La definizione della funzione di tipo read è la seguente

static ssize_t my_read(struct file Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito filp, char Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito buf, size_t size, loff_t *poff)

{

return 0;

}

Quando il kernel utilizza questa funzione, passa la struttura file da cui si può ricavare - dal campo private_data - la struttura utile da cui attingere informazioni per il dispositivo, il puntatore allo spazio di memoria dove si scriveranno i dati letti dal dispositivo e la lunghezza dei dati da leggere.

La funzione di tipo write è dichiarata nel seguente modo

static ssize_t my_write(struct file Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito filp, const char Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito buf, size_t size, loff_t *poff)

{

return 0;

}

i parametri sono gli stessi della funzione read ma vanno letti con chiave diversa. Oltre al puntatore alla struttura file si ha il puntatore ai dati che devono essere scritti sulla scheda e la loro lunghezza, dove andare a scriverli sicuramente sarà specificato in qualche campo della struttura PDEVICE_EXTENSION che il programmatore avrà settato precedentemente.

Va detto che i puntatori alle aree di memoria si riferiscono all’area user. Infatti il kernel può accedere ad entrambe le aree di memoria, ma non lo user. Se si vuole copiare dati da un’area ad un'altra occorre utilizzare le funzioni __copy_to_user e __copy_from_user definite in ”<asm/uaccess.h>” rispettivamente

unsigned long __copy_from_user (void *to, const void *from, unsigned long count);

unsigned long __copy_to_user (void *to, const void *from, unsigned long count);

la prima copia dati dallo user space al kernel space il secondo dal kernel space allo user space.

Quando si scrivono o leggono dati da un dispositivo si trasferiscono gruppi di 1, 2 o 4 byte. Normalmente, essendo il bus PCI a 32 o 64 bit per i dispositivi PCI, si leggono o scrivono 4 byte.

Per poter scrivere o leggere effettivamente i dati sul dispositivo esistono due gruppi di funzioni che dipendono dall’utilizzo dell’indirizzo fisico oppure di quello rimappato. Per completezza verranno presentati tutti e due i gruppi di funzioni, ma il consiglio è di rimpaginare sempre la memoria per i motivi già menzionati. La funzione per leggere 4 byte da un indirizzo fisico è la inl definita in “<asm/io.h>

unsigned inl(unsigned port);

che per parametro ha l’indirizzo da cui leggere il dato che poi restituisce.

Per scrivere si utilizza la outl definita nella stessa libreria della funzione inl dove sono anche definite tutte le funzioni per leggere dati di dimensioni diverse.

void outl(unsigned longword, unsigned port);

i parametri di questa funzione sono rispettivamente il dato da scrivere e l’indirizzo in cui scriverlo.

Per poter leggere dall’indirizzo rimappato la funzione è readl definita in “<linux/io.h>

unsigned readl(address);

che per parametro ha l’indirizzo da cui leggere il dato che poi restituisce.

Per scrivere si utilizza la funzione writel, definita nella stessa libreria della readl, dove sono anche definite tutte le funzioni per leggere dati di dimensioni diverse.

void writel(unsigned value, address);

i parametri di questa funzione sono rispettivamente il dato da scrivere e l’indirizzo in cui scriverlo.

 

2.4.3. La funzione iocnt

La funzione “I/O control” è normalmente la funzione principale del device driver, quella che gestisce tutti i comandi del dispositivo. I comandi possono essere una serie di read e write o semplici calcoli fatti all’interno di un case determinato da un controllo switch. La funzione è definita del seguente modo

static int my_IoControl (struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg)

{

return 0;

}

questa funzione oltre ad avere come parametri le due strutture riferite al file, ha il parametro cmd che è una costante che determina il tipo di comando da eseguire e arg gli argomenti del comando.

La costante cmd ha una particolare struttura definita dal kernel. Essa viene formata usando apposite macro definite in “<asm/ioctl.h>“ che discriminano se il comando è di tipo scrittura o lettura verso il dispositivo e rendono la costante univoca all’interno del kernel.

La macro principale è _IOC

_IOC(dir,type,nr,size)

i cui parametri sono dir, che specifica se il comando serve per leggere o scrivere o nessuno dei due, usando una delle costanti _IOC_READ, _IOC_WRITE, _IOC_NONE. Il parametro type è un numero esadecimale univoco per ogni modulo uguale per tutti i suoi comandi. Prima di assegnare un valore consultare il documento Documentation/iotcl-number.txt definito nella documentazione dei sorgenti del kernel per cui dovrà compilare il device driver e assegnare una base che sommata al numero di comandi che si dovranno implementare risulti un numero ancora libero. Il parametro nr è il numero secondario che normalmente parte da 1, fino al massimo numero di comandi. Infine size è il parametro che tiene conto della dimensione dell’argomento del comando che verrà passato dal livello applicativo: serve per fare ulteriori controlli, ma è poco usato. E’ buona norma creare un header separato per la creazione di queste costanti in modo che la stessa libreria possa essere inclusa al livello applicativo per poter usare i comandi.

Poiché si ha a disposizione solo una variabile per gli argomenti, che spesso sono numerosi, essa spesso assume la forma di un puntatore ad una struttura di dati, in questo modo i risultati si potranno salvare nella stessa struttura dell’argomento.

Come tutte le altre funzioni restituisce 0 se è terminata con successo e con un valore negativo diversamente, ad esempio se dal livello applicativo si usa un comando inesistente.


2.5. Esempio conclusivo per un dispositivo PCI

Si riportano frammenti di codice del device driver creato per la scheda PCI “DSP2”. Per motivi di copyright i nomi delle funzioni e delle variabili sono state cambiati e inoltre non sono stati implementati i comandi della I/O control, ma vengono evidenziate le funzioni usate e la struttura che un modulo, e più precisamente un device driver, deve avere.

File : modulo.c

 

#define __KERNEL__

#define MODULE

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/pci.h>

#include <linux/init.h>

#include <linux/fs.h>

#include <asm/hardirq.h>

#include <linux/ioport.h>

#include <linux/sched.h>

#include <asm/io.h>

#include <linux/errno.h>

#include "device_driver.h"

#if((LINUX_VERSION_CODE>=KERNEL_VERSION(2,5,0))&&(LINUX_VERSION_CODE<KERNEL_VERSION(2,7,0)))

#define KERN 26

#endif

#if((LINUX_VERSION_CODE>=KERNEL_VERSION(2,3,0))&&(LINUX_VERSION_CODE<KERNEL_VERSION(2,5,0)))

#define KERN 24

#endif

#if((LINUX_VERSION_CODE>=KERNEL_VERSION(2,1,0))&&(LINUX_VERSION_CODE<KERNEL_VERSION(2,3,0)))

#define KERN 22

#endif

// inizializzazioni diverse in base al kernel

#if (KERN==22)

PDEVICE_EXTENSION disp;

#include <linux/malloc.h>

#endif

#if ((KERN==24)||(KERN==26))

#include <linux/interrupt.h>

// inizializzo la lista di dispositivi

// struttura che lega dispositivo e driver,struttura del disp

static LIST_HEAD(my_pci_devs);

// mutua esclusione per leggere la lista

static rwlock_t my_pci_devs_lock = RW_LOCK_UNLOCKED;

// indice del dispositivo ( numero di dispositivi attivi )

static unsigned int devindex = 0;

#endif

static int major = -1; // Numero "major" associato al tipo di dispositivo

// tutte le funzioni in comune

// indirizzo da cui partire,dati,tipo lunghezza per dato, offset

static ssize_t my_read(struct file Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito filp, char Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito buf, size_t size, loff_t *poff)

{PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)file->private_data;

int i=0;

for(i=0;i<(int)poff;i++)

buf[i]=(char)readl(pdx-> SysAddr);

return 0;

}

// indirizzo da cui partire , dati , tipo lunghezza per dato , offset

static ssize_t my_write(struct file Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito filp, const char Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito buf, size_t size, loff_t *poff)

{PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)file->private_data;

int i=0;

for(i=0;i<(int)poff;i++)

writel((unsigned long)buffer[i],pdx-> SysAddr);

return 0;

}

// struttura che lega dispositivo e driver , struttura del disp, comando, argomenti

static int my_IoControl(struct inode *inode, struct file *file,unsigned int cmd, unsigned long arg)

{

PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)file->private_data;

parametri *p=(parametri *)arg;

Status =0;

switch(cmd)

{

case PROVA:

{printk(“La il numero dal livello applicativo è %d , verrà poi moltiplicato per 2\n”, *((int *)p->numero));

*((int *)p->numero)= *((int *)p->numero)*2;

}

break;

default:{ printk("Parametro errato\n");

Status = -EINVAL;

}

break;

}

return Status;

}

static int my_open(struct inode Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito inode, struct file Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito file)

{

#if (KERN==22)

PDEVICE_EXTENSION pdx = disp;

#endif

#if ((KERN==24)||(KERN==26))

PDEVICE_EXTENSION pdx;

int minor;

struct list_head *list;

minor = MINOR(inode->i_rdev);

read_lock(&my_pci_devs_lock);

list_for_each(list, &my_pci_devs) {

// punto al dispositivo

pdx = list_entry(list, DEVICE_EXTENSION, devs);

if (pdx->index==minor)

break;

}

read_unlock(&my_pci_devs_lock);

if (list == & my_pci_devs)

return -ENODEV;

#endif

// collego il puntatore del file alla struttura dati del dispositivo

file->private_data = pdx;

return 0;

}

// struttura che lega dispositivo e driver , struttura del disp

static int my_release(struct inode Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito inode, struct file Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito file)

{

file->private_data = NULL;

return 0;

}

// funzioni che implementano lettura scrittura e accesso alla scheda pci

static struct file_operations my_fops = {

read: my _read, // manda dati al dispositivo

write: my _write, // legge dati dal dispositivo

ioctl: my_IoControl, // manda comandi al dispositivo

open: my_open, // prima operazione realizzata nel dispositivo, inizializzazione

release: my_release // invocata per chiudere un dispositivo

};

static void my_InterruptHandle(int irq, void *dev_id, struct pt_regs *regs)

{

PDEVICE_EXTENSION pdx = dev_id;

unsigned long flags;

spin_lock_irqsave(&pdx->hwlock, flags);

// Verifica la sorgente della richiesta, INTA_FLAG maschera dipendente dal dispositivo

if((readl((unsigned long)(pdx->IdmaStatus))&INTA_FLAG)==0)

{ // IRQ non del dispositivo per cui è creato questo driver

spin_unlock_irqrestore(&pdx->hwlock, flags);

return;

}

// Resetta la richiesta di interrupt

writel(INTA_RESET,(unsigned long)(pdx->IdmaStatus));

spin_unlock_irqrestore(&pdx->hwlock, flags);

// Servizio

printk(“ Ok segnale di interrupt accettato \n”);

return;

}

MODULE_AUTHOR ("Programmatore Linux");

MODULE_DESCRIPTION ("Modulo per un dispositivo PCI");

// funzioni per il kernel 2.2

#if (KERN==22)

static int my_probe (struct pci_dev *pdev)

{ int err=0;

if (!(disp = kmalloc(sizeof(struct DEVICE_EXTENSIONtag), GFP_KERNEL))) {

printk(“Allocazione della struttura fallita\n");

return -1;

}

// allocazione di memoria

disp->MemLength = MAX_MEMORY; // imposto io la quantità di memoria

disp->MemAddress = pdev->base_address[PCILASER_MEMREGION] & PCI_BASE_ADDRESS_MEM_MASK;

disp->SysAddr =(unsigned long *)ioremap(disp->MemAddress, disp->MemLength);

if (disp->SysAddr == NULL)

{ printk("Memoria gia in uso\n");

err = -ENOMEM;

goto err_region;

}

printk("Assegnati indirizzi di memoria da %lx fino a %lx\n", disp->MemAddress, disp->MemLength+ disp->MemAddress -1);

// registrazione del gestore delle IRQ

disp->IrqNumber = (unsigned int)pdev->irq

if ((err=request_irq(disp->IrqNumber, my_InterruptHandle, SA_SHIRQ, MODNAME, disp)))

{

printk("Irq %lu in uso\n", disp->IrqNumber);

err= -EINTR;

goto err_irq;

}

printk("Assegnato IRQ: %lu\n", disp->IrqNumber);

// collego il dispositivo alla mia struttura e inizializzo il semaforo

disp->DeviceObject = pdev;

disp->ListLock=RW_LOCK_UNLOCKED;

// calcolo degli indirizzi dei registri specifici per la “DSP2”, sono solo di esempio

disp->IdmaDataLen = (disp->MemLength / sizeof(unsigned long)) / 2;

disp->IdmaData = disp->SysAddr;

disp->IdmaLatch = disp->SysAddr + disp->IdmaDataLen;

disp->IdmaStatus = disp->SysAddr + disp->IdmaDataLen + disp->IdmaDataLen / 2;

return 0;

err_irq:

synchronize_irq();

free_irq((unsigned int)Irql, disp);

err_region:

iounmap((void *)disp->SysAddr);

kfree(disp);

printk("Essendoci stato qualche errore sono state liberate le risorse occupate\n");

return err;

}

static void my_remove (struct pci_dev *rdev)

{

// se non esiste alcun dispositivo caricato esci

if (!rdev)

{

printk(Risorse non liberate\n");

return;

}

// rilascio delle risorse occupate

synchronize_irq();

free_irq((unsigned int)disp->IrqNumber, disp);

iounmap((void *)disp->SysAddr);

kfree(disp);

printk("Risorse liberate\n");

}

static int __init my_init_module (void)

{

int rc=-1, maj;

struct pci_dev *moddev=NULL;

// esiste almeno una scheda pci ?

if(!pci_present())

{ return -ENODEV;

}

if((moddev = pci_find_device(PCI_VENDOR, PCI_DEVICE, moddev))!=NULL)

{printk("Dispositivo trovato\n");

rc=my_probe(moddev);

}

else

{ printk("Non è stato trovato nessun dispositivo PCI\n");

return -ENODEV;

}

// registrazione dispositivo

maj = 1;

while((rc=register_chrdev(maj, MODNAME, &pcilaser_fops))!=0)

{ // cerca numero major libero e assegnalo

maj++;

}

if (rc < 0) {

printk("Non è possibile registrare il dispositivo\n");

return -1;

}

major = maj;

printk("Major : %d \n",major);

printk("Modulo caricato\n");

return rc;

}

static void my_cleanup_module (void)

{

// rilascio del dispositivo

unregister_chrdev(major,MODNAME);

// rilascio risorse

my_remove (disp->DeviceObject);

printk("Modulo scaricato\n");

}

// serve a indicare quale dispositivi gestisce questo modulo tramite la struttura my_fops

MODULE_SUPPORTED_DEVICE(MODNAME);

#endif

/* funzioni per il kernel 2.4 o 2.6 */

#if ((KERN==24)||(KERN==26))

static struct pci_device_id my_idtable[]= {

// vendor , device , subvendor , subdevice , class , class_mask , driver_data

{ vendor: PCI_VENDOR,

device: PCI_DEVICE,

subvendor: PCI_SUBVENDOR,

subdevice: PCI_SUBDEVICE,

class: 0, // class e class_mask a zaro perche' sono definite vendor e device

class_mask: 0,

driver_data: 0 },

{0,}

};

static int __devinit my_probe (struct pci_dev *pdev, const struct pci_device_id *pid)

{ PDEVICE_EXTENSION disp=NULL;

int err=0;

// abilitazione periferica per poter usare funzioni suspend e wake up

if (pci_enable_device(pdev))

{ printk("Abilitazione dispositivo fallita\n");

return -1;

}

if (!(disp = kmalloc(sizeof(struct DEVICE_EXTENSIONtag), GFP_KERNEL))) {

printk("Allocazione della struttura fallita\n");

goto err_mall;

}

// controllo spazio di memoria del dispositivo

if((disp->MemLength=pci_resource_len(pdev, MY_MEMREGION))!=MAX_MEMORY)

{printk("Memoria non esatta %lx \n", disp->MemLength);

goto err_region;

}

if ((err=pci_resource_flags(pdev, MY_MEMREGION)) & IORESOURCE_MEM)

{disp->MemAddress=pci_resource_start(pdev, MY_MEMREGION);

disp->SysAddr=(unsigned long *)ioremap(disp->MemAddress, disp->MemLength);

if (disp->SysAddr == NULL)

{ printk("Memoria gia in uso\n");

err = -ENOMEM;

goto err_region;

}}

else

{printk("Errore nella allocazione di memoria errore: %d\n",err);

err = -ENOMEM;

goto err_region;

}

printk("Assegnati indirizzi di memoria da %lx fino a %lx\n", disp->MemAddress, disp->MemAddress+disp->MemLength-1);

// registrazione del gestore delle IRQ

disp-> IrqNumber =(unsigned int)pdev->irq;

if ((err=request_irq(disp-> IrqNumber, my_InterruptHandle, SA_SHIRQ,MODNAME, disp))) {

printk("Irq %lu in uso\n", disp->IrqNumber);

err= -EINTR;

goto err_irq;

}

printk(“Assegnato IRQ: %lu\n", disp->IrqNumber);

disp->DeviceObject = pdev;

pci_set_drvdata(pdev, disp);

disp->ListLock=RW_LOCK_UNLOCKED;

disp->index = devindex;

devindex++;

write_lock(&dsp2pci_devs_lock);

list_add_tail(&disp->devs, &dsp2pci_devs);

write_unlock(&dsp2pci_devs_lock);

// calcolo degli indirizzi dei registri specifici per la DSP2, sono solo di esempio

disp->IdmaDataLen = (disp->MemLength / sizeof(unsigned long)) / 2;

disp->IdmaData = disp->SysAddr;

disp->IdmaLatch = disp->SysAddr + disp->IdmaDataLen;

disp->IdmaStatus = disp->SysAddr + disp->IdmaDataLen + disp->IdmaDataLen / 2;

return 0;

err_irq:

synchronize_irq();

free_irq((unsigned int)Irql, disp);

err_region:

iounmap((void *)disp->SysAddr);

pci_release_regions(pdev);

kfree(disp);

err_mall:

pci_disable_device(pdev);

printk("Essendoci stato qualche errore sono state liberate le risorse occupate\n");

return err;

}

static void __devexit my_remove (struct pci_dev *rdev)

{

PDEVICE_EXTENSION disp = pci_get_drvdata(rdev);

// se non esiste alcun dispositivo caricato esci

if (!disp)

{

printk("Risorse non liberate\n");

}

write_lock(&my_devs_lock);

list_del(&disp->devs);

write_unlock(&my_devs_lock);

devindex--;

// rilascio delle risorse occupate

iounmap((void *)disp->SysAddr);

#if (KERN==26)

synchronize_irq((unsigned int)disp->IrqNumber);

#else

synchronize_irq();

#endif

free_irq((unsigned int)disp->IrqNumber, disp);

kfree(disp);

pci_set_drvdata(rdev, NULL);

pci_disable_device(rdev);

printk("Risorse liberate\n");

}

// funzioni usate dal driver per controllare la scheda

static struct pci_driver my_driver = {

name: MODNAME,

id_table: my_idtable,

probe: my_probe,

remove: __devexit_p(my_remove),

};

static int __init my_init_module (void)

{

int rc, maj, err;

// registrazione driver e ricerca del dispositivo

rc =pci_module_init(&my_driver);

if (rc == -ENODEV) {

printk("Non è stato trovato il dispositivo PCI\n");

return rc;

}

// registrazione dispositivo

maj = 0; // maj=0 viene assegnato in modo dinamico il major number

err = register_chrdev(maj, MODNAME, &my_fops);

if (err < 0) {

printk("Non è possibile registrare il dispositivo \n");

pci_unregister_driver(&my_driver);

return err;

}

major = (err == 0 ? maj : err);

printk("Major : %d\n",major);

SET_MODULE_OWNER(&my_fops);

printk("Modulo caricato\n");

return rc;

}

static void __exit my_cleanup_module (void)

{

// rilascio del dispositivo

unregister_chrdev(major,MODNAME);

// rilascio del driver

pci_unregister_driver(&my_driver);

printk("Modulo scaricato\n");

}

MODULE_LICENSE("GPL");

MODULE_DEVICE_TABLE(pci, my_idtable);

#endif

module_init(my_init_module);

module_exit(my_cleanup_module);

File: device_driver.h

#ifndef __DEVICE_DRIVER__

#define __ DEVICE_DRIVER __

#define MODNAME "my_module"

// parametri dipendenti dal dispositivo PCI, qui sono citati come esempio

#define PCI_VENDOR 0x1767

#define PCI_DEVICE 0x0003

#define PCI_SUBVENDOR 0x0100

#define PCI_SUBDEVICE 0x0031

#define MY_MEMREGION 0

typedef struct DEVICE_EXTENSIONtag

{

// Configurazione hardware

unsigned long SysAddr; // Indirizzo memoria di sistema

unsigned long MemAddress // Indirizzo fisico della memoria

unsigned long MemLength;// Lunghezza di memoria

unsigned long IrqNumber; // Numero IRQ

void * DeviceObject // puntatore alla struttura pdev

int index; // indice del dispositivo corrente ( in caso di multischede )

// Puntatori ai dati

unsigned long IdmaDataLen; // Max. lunghezza dati

unsigned long * IdmaData; // Dati memoria regione 0

unsigned long * IdmaLatch; // Latch Indirizzo porta

unsigned long * IdmaStatus; // Indirizzo del target

}DEVICE_EXTENSION, *PDEVICE_EXTENSION;

typedef struct PARAMETRItag

{

int Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito numero ;

}parametri ;

#include <asm/ioctl.h>

#define PROVA _IOC(IOC_NONE,0xD7,1,0)

#endif

Nell’esempio vengono anche usate funzioni per mettersi in mutua esclusione in caso della gestione di più dispositivi contemporaneamente. In questo contesto queste funzioni ( come quelle per la gestione delle liste ) non verranno spiegate.


3. Come usare il modulo dal livello applicativo

Finita la codifica del modulo, esso deve essere compilato e caricato nel kernel. Esistono diverse funzioni, sia per il caricamento che per lo scaricamento. Una volta avviato il modulo e creato lo special device file si potrà usufruire dei servizi del modulo tramite questo ultimo file. Ampia parte di questo capitolo spiegherà proprio l’utilizzo delle funzioni di interfaccia del modulo.

Durante lo sviluppo del device driver per la scheda PCI “DSP2”, si è pensato di usufruire del comando make, tecnica molto diffusa in Linux e indipendente dall’architettura sottostante. In seguito verranno spiegate le regole base per poter usare questo comando e si farà un accenno alle librerie dinamiche e statiche per completezza, in modo da arrivare ad avere un esempio di una pila di software a diversi livelli, compilabili tutti da un unico comando.


3.1. Compilazione di un modulo e regole di utilizzo del comando make

Un modulo deve essere compilato con specifici parametri da fornire al compilatore, il risultato sarà un file oggetto. Se il modulo è composto di più file sorgenti occorre tenere conto delle loro dipendenze, questo compito è preso in carico automaticamente dal comando make.

Il comando make legge ciò che deve fare dal file Makefile, nella directory corrente. make non ha alcuna conoscenza della sintassi dei file su cui lavora e decide le azioni da compiere in base alle istruzioni presenti in Makefile. Poiché non è facile da creare una direttiva Makefile esistono strumenti che permettono di creare file di tipo Makefile, specificando solo i sorgenti e le opzioni base da dare al compilatore. Inoltre, attraverso questi strumenti si verifica se sul sistema in esecuzione esiste il software e le librerie necessarie per compilare i sorgenti. Molto spesso, anche tra due sistemi Unix, sia i file di libreria sia i file header sono posti in directory diverse, e talvolta esse hanno nomi diversi. Si rende quindi necessario uno strumento per facilitare il compito degli sviluppatori che hanno l’esigenza di rendere portabile il proprio software. Per questo motivo è stato introdotto il tool autoconf che permette di automatizzare e gestire la compilazione e la distribuzione di qualsiasi progetto software.

La prima cosa da fare è creare un semplice script di configurazione: configure.in. Con questo file verrà creato il file configure. Quet’ultimo serve per testare la presenza del compilatore e di altre utility (flex, bison, awk, sed) di cui si ha bisogno in fase di compilazione. Inoltre provvede a controllare l'esistenza delle librerie statiche o dinamiche e dei file di header utilizzati dal programma, segnalando eventuali anomalie. Lo script configure genera quindi il file Makefile esaminando le caratteristiche del sistema su cui si sta eseguendo la compilazione

Makefile.in --> ./configure --> Makefile

 

Lo script configure è molto complesso, viene quindi utilizzato il tool autoconf per generarlo automaticamente partendo da un file di configurazione. In particolare, autoconf legge le impostazioni dal file configure.in in cui, tramite una serie di macro, si descrivono ad autoconf le operazioni che lo script configure dovrà compiere.

configure.in --> autoconf --> configure

Per la scrittura del file configure.in si potrà usare il tool autoscan. Questo programma esaminerà i file sorgenti per cercare di individuare elementi che possono generare problemi di portabilità e, per questi, generare automaticamente i comandi necessari da inserire in configure.in. Lanciando autoscan verrà generato il file configure.scan che sarà necessario rinominare come configure.in per apportargli le modifiche necessarie. In particolare andranno cambiate le seguenti righe

AC_INIT(…) à AC_INIT(configure.in)

aggiunta … AM_CONFIG_HEADER(config.h)

AC_OUTPUT(…) à AC_OUTPUT(Makefile)

Nonostante il file Makefile.in possa essere scritto manualmente, il tool automake è, come suggerisce il nome, uno script per la generarzione in automatico del Makefile.in tramite un file di configurazione chiamato Makefile.am

Makefile.am --> automake --> Makefile.in

Mentre il configure.in può essere univoco per qualsiasi programma compilato sulla stessa macchina, Makefile.am è dipendente dal tipo di programma da compilare.

E’ importante notare che autoconf e automake non sono necessari per la compilazione del programma, ma alla sola generazione dello script configure. Inoltre per utilizzare autoconf ed automake è necessaria la presenza del programma m4 e del Perl. Tutti questi tool sono disponibili in tutte le distribuzioni Linux.

Il prossimo paragrafo si occuperà della creazione del file Makefile.am, ma per concludere il discorso ed arrivare alla effettiva compilazione, bisogna eseguire una serie di comandi. Prima però è necessario creare anche il file acconfig.h (che autoheader inserirà nel file config.h.in) includendo le seguenti definizioni:

/* nome del pacchetto */

#undef PACKAGE

/* versione */

#undef VERSION

acconfig.h viene utilizzato da autoheader per gestire variabili definite dal programmatore (diverse da quelle standard recuperabili da configure.in), e, per un bug, PACKAGE e VERSION di automake.

Fatto questo, si possono lanciare i seguenti comandi:

aclocal

autoconf

autoheader

automake

Il primo serve per generare delle macro di automake che serviranno ad autoconf, è poi necessario rilanciare autoheader, ed infine automake che tra l'altro aggiungerà altri file, molto utili (si evita in questo contesto di entrare nel dettaglio delle opzioni).

Nonostante le dimensioni di Makefile.am siano ridotte, viene creato un enorme Makefile.in (la cui lettura potrebbe non essere semplice per chi non conosce approfonditamente make), in cui si notano numerosissime macro nella forma @nome_variabile@ ed in effetti si hanno a disposizione diverse opzioni aggiuntive. Poi va lanciato per i motivi già detti il file

./configure

e infine si potrà compilare con

make

La procedura è lunga e faticosa anche se va fatta una sola volta, e se cambiano i file sorgenti basterà soltanto eseguire make ed il risultato è garantito. Inoltre i file creati possono essere mantenuti per altri file sorgenti e basterà soltanto cambiare il file Makefile.am . Un consiglio per non sbagliare è appoggiarsi su programmi di sviluppo ( se si dispone della modalità grafica ) come KDevelop ( arrivato alla versione 3.0 ) dove creando un semplice progetto vengono creati tutti i file menzionati prima.

A differenza di un normale programma, non è possibile debuggare un modulo perché non si hanno tutti i simboli necessari, come spiegato nel Capitolo 1 di questa monografia e quindi conviene creare un progetto semplice e cambiare il Makefile.am come si vedrà nel prossimo paragrafo.

 

3.1.1. Creazione del file Makefile.am per la compilazione di un modulo

Un Makefile.am è formato da una serie di linee con la seguente forma variabile=valore dove la variabile ha un nome specifico, una parola chiave poi riconosciuta da automake. Variabili comuni a tutti i programmi e librerie della directory corrente sono:

· INCLUDES: serve per aggiungere directory al progetto dove sono contenuti file header da compilare;

· LDFLAGS: specifica le opzioni da passare al compilatore;

· AM_CFLAGS: specifica le opzioni da passare al linker;

· bin_PROGRAMS: specifica il nome del file programma eseguibile;

· SOURCES: indica l’elenco dei file sorgenti da compilare;

· include_HEADERS: specifica l’elenco dei file header da compilare, nella directory corrente.

Con un esempio si capirà la sintassi di un Makefile.am per compilare un modulo. Per rendere più generale l’esempio, si immagina che serva anche un file sorgente stampa.c contenente qualche funzione utile.

File: Makefile.am

AM_CFLAGS = $(all_includes) -Wall -DMODULE -D__KERNEL__ -I/usr/src/linux/include -O4

bin_PROGRAMS = My_module

My_module_SOURCES = modulo.c stampa.c

include_HEADERS = device_driver.h

My_module_LDFLAGS = -r -o My_module.o

Da notare la relazione tra il nome del file binario eseguibile ( anche se per un modulo non serve, va messo ) e il nome delle variabili di tipo SOURCES e LDFLAGS.

I flag passati al compilatore sono:

· all_includes: è una variabile di sistema e specifica di includere tutti gli header di base;

· -Wall: si vuole specificare al compilatore di far visualizzare tutti i possibili warning;

· -DMODULE -D__KERNEL__: l’opzione –D specifica una define, se nel sorgente non si erano definite vanno specificate qui;

· -I/usr/src/linux/include: l’opzione –I specifica dove cercare gli header, normalmente il sorgente del kernel che si carica viene linkato con la directory /usr/src/linux e nella directory include ci sono gli header che servono per la compilazione.

Maggiori informazioni si possono avere con il comando man gcc da terminale.

Per quanto riguarda i flag per il linker ciò che conta è che il risultato della compilazione, cioè che viene creato un file oggetto - che in questo esempio si chiama My_module.o -l’opzione usata è –o mentre –r serve per fare un link parziale, nuovamente per approfondire il significato di questi flag, da terminale si può dare il comando man ld.

3.1.2. Creazione del file Makefile.am per la compilazione di una libreria dinamica

Per sfruttare al meglio le funzionalità di una libreria è meglio caricarla solo nel momento in cui è necessaria, e questo è ciò che avviene per una libreria dinamica. Le librerie dinamiche sono molto usate in Linux, e ci sono diversi tool per poterle usare e configurare. Esse normalmente sono memorizzate nella directory /lib oppure /usr/lib del sistema.

Le librerie dinamiche non sono altro che una collezione di funzioni, in un file oggetto che viene linkato insieme al programma che usa queste funzioni. All'interno dell'eseguibile ci sono solo delle informazioni che permettano di agganciare le funzioni della libreria che potrebbe già essere precaricata in memoria da altri programmi, senza doverla ricaricare nuovamente.

Per creare una libreria dinamica bisogna compilare il file sorgente con appositi flag. Ecco un esempio di Makefile.am

File: Makefile.am

AM_CPPFLAGS = $(all_includes) -D _LINUX

lib_LTLIBRARIES = libMy_Library_shared.la

libMy_Libriry_shared _la_LDFLAG = $(all_libraries) -version-info 1:0:0 -shared

include_HEADERS = libFunction_shared.h

libMy_Library_shared_la_SOURCES = libFunction_shared.cpp

In questo caso il sorgente è in linguaggio C++ e la libreria è stata creata per diversi sistemi operativi, e per specificare il sistema si è usata la definizione _LINUX che viene definita dal compilatore come nell’esempio precedente.

La variabile LTLIBRARIES specifica che il risultato non sarà un comune programma ma una libreria col nome libMy_Library_shared.la, come regola una libreria deve iniziare col nome lib e si deve creare inizialmente l’archivio ( estensione .la ) .

Una libreria ha sempre una versione e le opzioni -version-info 1:0:0 –shared specificano che la libreria ha come versione 1.0.0 ed è dinamica e condivisa.

In questo contesto era utile capire come compilare una libreria dinamica, ma per poterla usare essa deve essere caricata ed occorre richiamare le singole funzioni con apposite funzioni che il lettore interessato potrà trovare definite in “<dlfcn.h>“.

 

3.1.3. Creazione del file Makefile.am per la compilazione di una libreria statica

Il codice di una libreria statica viene inserito nel codice del programma eseguibile, incrementandone la dimensione, a vantaggio però della portabilità. Il file eseguibile, infatti, potrà essere copiato ed usato anche in altri sistemi, mentre un eseguibile che utilizza librerie dinamiche non potrà essere eseguito su un sistema nel quale esse non siano installate.

Per creare una libreria statica bisogna compilare il file sorgente con appositi flag. Il Makefile.am è molto simile a quello per una libreria dinamica. Eccone un esempio

File: Makefile.am

AM_CPPFLAGS = $(all_includes) -D _LINUX -ggdb3

noinst_LTLIBRARIES = libMy_Library_static.la

libMy_Library_static_la_LDFLAG = $(all_libraries) -static –ansi

METASOURCES = AUTO

include_HEADERS = libFunction_static.h

libMy_Library_static_la_SOURCES = libFunction_static.cpp

Anche in questo caso la libreria è stata creata in linguaggio C++, portabile in diversi sistemi operativi, è stato usato il flag ggdb3 per avere i simboli per il debugger.

La libreria creata sarà libMy_Library_static.la, si passano al linker i flag static per indicare che è una libreria statica e ansi per indicare che si garantisce la creazione di simboli ANSI C.

 

3.1.4. Compilazione di più progetti in directory diverse con un unico comando

Dagli esempi fatti si supponga di avere tre directory modulo, libshared, libstatic. Nelle directory sono contenuti i file elencati prima con i Makefile.am di esempio usati.

Ammesso di essere posizionati nella directory alla radice delle prime tre, è possibile compilare il tutto eseguendo il make una volta sola da questa directory. Il Makefile.am corrispondente sarà

File: Makefile.am

AUTOMAKE_OPTIONS = foreign 1.4

SUBDIRS modulo = libshared libstatic

In questo modo il comando make verrà eseguito in modo ricorsivo nelle directory specificate. In questo caso si specifica anche la versione dell’automake da usare.


3.2. Caricamento del modulo e utilizzo a livello applicativo

Come già specificato più volte, il modulo deve diventare parte del kernel che è caricato all’avvio della macchina. Per far questo si hanno a disposizione due comandi: insmod oppure modprobe. Quest’ultimo è un comando più sofisticato del primo e tiene conto delle dipendenze del modulo da altri moduli. Entrambi sono programmi con linking dei simboli a run-time, che risolvono i simboli non definiti nel modulo utilizzando la tabella dei simboli del kernel.

L’argomento di entrambi i comandi è il file oggetto del modulo, negli esempi il nome del file era My_module.o. Il file viene cercato sia nella directory corrente, sia nella directory di installazione dei moduli che normalmente è /lib/modules .

Ricordando che per utilizzare il device driver bisogna sfruttare l’interfaccia utilizzata per il file, è opportuno implementare uno script shell per creare lo special device file. L’esempio che è stato presentato nel Capitolo 1 si riferiva alla scheda PCI “DSP2”, qui, invece, verrà fatto un esempio di un generico device My_dev, con l’aggiunta dei comandi per la compilazione del modulo ed il suo caricamento. Dopo l’esecuzione di questo script si potrà accedere al dispositivo dal livello applicativo.

File : load_device.sh

#!/bin/sh

# Script per l'installazione del modulo My_module.o

# inclusa la creazione dei nodi

MODULE=" my_module"

DEVICE=" My_dev"

MODE="764" # rw-rw-r--

FILE=" My_module.o"

DIR=$(pwd)

make

if test $? -ne 0

then

echo "$MODULE: Problemi nella compilazione ";

exit 1;

else

echo "$MODULE: Modulo compilato correttamente";

fi

// caricamento del modulo

/sbin/insmod ./$FILE $*

if test $? -ne 0

then

echo "$MODULE: Non Inserito" ;

exit 1;

else

echo "$MODULE: Modulo caricato";

fi

# rimozione nodi vecchi

rm -f /dev/${DEVICE}[0-3]

# caricamento major

MAJOR=`grep $MODULE /proc/devices | awk '{print $1}'`

# creazione nodi

cd /dev

mknod /dev/${DEVICE}0 c $MAJOR 0

mknod /dev/${DEVICE}1 c $MAJOR 1

mknod /dev/${DEVICE}2 c $MAJOR 2

mknod /dev/${DEVICE}3 c $MAJOR 3

// avvio automatico all’avvio del computer

echo "alias char-major-$MAJOR ${DEVICE}" >> /etc/modules.conf

echo “/sbin/insmod $DIR/$FILE ” >> /etc/rc.d/rc.modules

# Attributi

chmod $MODE /dev/${DEVICE}[0-3]

Lo script, in linguaggio bash, è avviabile col comando

sh load_device.sh

ed esegue anche una serie di controlli che verificano se il tutto funziona correttamente. Un altro punto importante è l’installazione automatica all’avvio del sistema. Per rendere possibile questo occorre modificare due file /etc/modules.conf, /etc/rc.d/rc.modules nel modo specificato nello script.

Dopo l’avvio dello script, digitando il comando lsmod, che visualizza i moduli caricati, si potrà verificare l’esistenza delle seguenti informazioni:

Module Size Used by

sr_mod 15264 0 (autoclean)

3.2.1. Accesso al dispositivo dal livello applicativo

Una caratteristica essenziale del sistema Linux, è la visione dell'hardware a livello applicativo come un insieme di files, contenuti nella directory /dev. E’ possibile quindi aprire, chiudere, leggere e scrivere sul dispositivo tramite le classiche system call open, close, read, write utilizzate per files normali. In più, per un device driver è definita la funzione ioctl ed altre che consentono di avere il pieno controllo del dispositivo.

Per poter usare queste system call bisogna includere nel programma l’header “<fcntl.h>“ e “<sys/ioctl.h>” . Normalmente la sequenza per accedere al dispositivo ( dando per scontato che il device driver che lo gestisca sia già caricato nel kernel ) è aprire lo special device file, effettuare le operazioni sul dispositivo e chiudere lo special device file quando l’applicativo viene chiuso.

3.2.1.1. Apertura del dispositivo

La system call open definita nell’header specificato nel paragrafo precedente

int open (char *nomefile, int tipo, int permessi);

dove il nomefile è lo special device file e i permessi possono essere:

· O_RDONLY: file aperto in lettura;

· O_WRONLY: file aperto in scrittura;

· O_RDWR: file aperto in scrittura e lettura.

in questi casi si accede allo special device file in modalità scrittura e lettura per poter sia mandare dati al dispositivo che ricevere informazioni da esso.

La system call open restituisce un file descriptor - un numero intero positivo che rappresenta in modo univoco il file aperto - esso verrà chiamato in futuro fd. Se è negativo significa che la chiamata non è andata a buon fine, per esempio perché non è stato caricato il modulo oppure perché non è stato creato lo special device file. Se si hanno più dispositivi, occorre, ovviamente, effettuare la open di ciascuno degli special file corrispondenti.

Ad ogni chiamata della system call open corrisponde l’esecuzione della finzione open definita nel modulo che serviva per collegare la struttura dati del dispositivo alla struttura del file aperto.

 

3.2.1.2. Uso del dispositivo

Le funzioni principali per accedere al dispositivo che verranno presentate sono la ioctl, la read e la write, tutte definite in “<fcntl.h>.

La system call ioctl

int ioctl(int fd, int request, ...);

usa come parametri il file descriptor per definire con quale dispositivo comunicare e il comando request. Quest’ultimo è la una delle costanti definite al momento della creazione della ioctl nel modulo. Negli esempi fatti è definito un header device_driver.h che bisognerà includere anche nel programma che usa la ioctl. Il terzo parametro (generalmente un puntatore di tipo void oppure char ) non e' definito a priori. Come si è detto può essere una struttura definita dall’utente.

La funzione ioctl ritorna 0 in caso di successo e un valore negativo in caso di errore. In quest'ultimo caso è a cura del programmatore del modulo restituire una costante di errore definite nell’header “<linux/errno.h>“ in base al tipo di errore .

Per inviare dati al dispositivo si usa la system call write

ssize_t write(int fd, const void *buf, size_t count);

che usa come parametri il file descriptor per identificare il dispositivo, il puntatore all’area di memoria in cui sono contenuti i dati da inviare, ed il numero di byte da trasferire.

La system call write ritorna il numero di byte trasferiti. In caso di errore viene restituito -1.

La system call per leggere dati dal dispositivo è la funzione read

ssize_t read(int fd, void *buf, size_t count);

che legge dal dispositivo individuato da fd un numero di byte pari a count immagazzinandoli nell’area di memoria puntata da buf. Da quale indirizzo leggere i dati non è competenza della read, ma del device driver. Il protocollo spesso utilizzato in questi casi è impartire un comando al dispositivo mediante una ioctl e successivamente effettuare – mediante la read - la lettura dei risultati.

La system call read restituisce il numero di byte effettivamente letti. In caso di errore viene restituito -1.

 

3.2.1.3. Chiusura del dispositivo

Quando il programma applicativo conclude, è buona norma chiudere anche tutti i file utilizzati, questo è tanto più necessario per gli special device file. La system call da utilizzare è close

int close(int fd);

che ha come unico parametro il file descriptor.

La funzione ritorna 0 in caso di successo e -1 in caso di errore. Essa richiama la funzione release definita nel modulo, che annullerà il collegamento tra la struttura del file e del dispositivo.

 

3.2.1.4. Esempio di un programma applicativo che usa il dispositivo

In questo paragrafo conclusivo, si riporta un esempio di programma applicativo che utilizza il dispositivo ipotetico My_dev. Non è conveniente riportare l’esempio che si riferisce alla scheda PCI “DSP2”, poiché si sarebbero dovuti introdurre troppi dettagli irrilevanti per la comprensione dello sviluppo ed uso di un device driver.

File : programma_applicativo.c

// prima funzione livello applicativo che usa il dispositivo

#include <fcntl.h>

#include <linux/ioctl.h>

#include <stdio.h>

#include "device_driver.h"

#include <linux/errno.h>

int main()

{

int fd;

int status;

int n;

int lungbuf;

char device[]="/dev/My_dev0";

char buffer_w[]=”dati da scrivere”;

char buffer_r[10];

parametri Chat Cose_divertenti Costruzione.html _notes _vti_pvt add_meta.sh chi_sono cut_enter cut_enter.c files fphover.class fphoverx.class hacking home images index.htm index3.htm index3.htm.BAK lavoro meta not_found.html progetti redirect.html scuola siti_utili temp1 temp2 temp3 temp6 temp_non_e_nel_sito p;

// apertura special device file

fd = open(device,O_WRONLY);

if (fd < 0) {

perror(device);

printf("Errore open: %d\n",status); }

status=fd;

return status;

}

// impostazione parametri

n=3;

p->numero=&n;

// uso ioctl

printf("Numero prima della ioctl %d\n”,*(p->numero));

status = ioctl(fd, PROVA,p);

if (fd < 0) {

perror(device);

printf("Errore ioctl: %d\n",status); }

status=fd;

return status;

}

// uso write

status = write(fd,buffer_w,strlen(buffer));

if(status!=0)

{ perror(device);

printf("Errore write: %d\n",status);

return status;

}

// uso read

status = read(fd,buffer_r,10);

if(status!=0)

{ perror(device);

printf("Errore read: %d\n",status);

return status;

}

// uso close

status = close(fd);

if(status!=0)

{ perror(device);

printf("Errore close: %d\n",status);

return status;

}

}

Il programma accede al dispositivo, esegue il comando PROVA, effettua la scrittura di un messaggio e legge 10 bytes. Poi il programma viene chiuso.

Una funzione molto utile per il debug dei programmi applicativi è la perror, definita in “<sys/types.h>

void perror (char *msg);

La funzione stampa su stderr[17] la stringa corrispondente all'ultimo errore di sistema, preceduta dalla stringa passata come parametro.


Bibliografia

 

· Daniel P. Bovet & Marco Cesati, Understanding the Linux kernel, 2nd edition, O'Reilly, 2003.
[ Linux 2.4]

· M. Beck, H. Böhme, M. Dziadzka, U. Kunitz, R. Magnus, C. Schröter & D. Verworner, Linux kernel programming 3rd ed., Addison Wesley, 2002.

· Daniel P. Bovet & Marco Cesati, Understanding the Linux kernel, O'Reilly, 2001.
[ Linux 2.2.]

· R. Card, E. Dumas & F. Mevel, The Linux Kernel Book, John Wiley and Sons, 1998.

· Kurt Wall , Linux Programming Unleashed, Second Edition . SAMS , 2000
[ programmazione in ambiente Linux ]

Articoli e documenti online

 

· http://pficheux.free.fr/articles/lmf/pci/
[ articolo in francese ma molto completo con esempi pratici ]

· http://www.geocities.com/SiliconValley/Pines/1406/parte1.html
[ esempio in italiano per la creazione di un device driver ]

· http://blaad.altervista.org/tuts/rubini.html
[ articolo in italiano sulla programmazione dei moduli ]

· http://www.faqs.org/docs/kernel/x429.html
[ articolo in inglese che mette a confronto i programmi con i moduli ]

· http://www.tldp.org/LDP/lkmpg/x571.html
[ spiegazione dei dispositivi a caratteri in inglese ]

· http://linuxday.lugroma.org/materiale/cesati/cesati-LD2002.tar.gz
[ mini corso per la creazione di un modulo per un dispositivo PCI ]

· http://www.iglu.org.il/lxr/source/Documentation/kernel-docs.txt
[ informazioni dettagliate in inglese sul kernel 2.4.x ]

· http://www.linux-mag.com/2000-03/gear_01.html
[ articolo in inglese che spiega come creare un modulo per un dispositivo PCI per il kernel 2.2.x ]

· http://www.reed-electronics.com/tmworld/index.asp?layout=article&articleid=CA187432&rid=0&rme=0&cfd=1
[ comandi per utilizzare i moduli ]

· http://vertigo.hsrl.rutgers.edu/ug/make_help.html
[ how-to in inglese su come scrivere un Makefile ]

· http://www.mc.man.ac.uk/LDP/HOWTO/Program-Library-HOWTO
[how-to in inglese su come creare librerie statiche e dinamiche ]

· http://www.lilik.it/~mirko/gapil/gapil.html
[ guida online molto completa sulla programmazione in Linux ]

 



[1] Run-time: durante il funzionamento.

[2] System-call: Chiamate di sistema, funzioni primitive messe a disposizione dal sistema operativo Linux, utilizzabili in modalità utente (user mode).

[3] Shell script: Breve programma sviluppato in linguaggio bash avviabile dal terminale Linux.

[4] New e delete: New e delete sono due direttive definite dal C++ rispettivamente per allocare e deallocare della memoria .

[5] Release: sono le versioni pari (2.0.x, 2.2.x, . . . ) quelle stabili, mentre quelle dispari (2.1.x, 2.3.x, . . . ) sono le versioni di sviluppo nelle quali si implementano via via le nuove caratteristiche della successiva versione stabile.

[6] Hotplugging: Letteralmente significa “inserimento a caldo” ed è una modalità supportata da alcuni dispositivi hardware che possono essere inseriti o tolti mentre il sistema è avviato.

[7] Interrupt: Interruzione, segnale che viene inviato da un oggetto al processore per segnalare qualcosa. L’oggetto può essere o hardware o software.

[8] Frequenza di clock: Frequenza di trasmissione dei dati sul bus.

[9] Burst mode: E’ un tipo di trasferimento dati usato dai DMA controller su alcuni dispositivi PCI. In particolare questo è un trasferimento di dati a blocco.

[10] Bus mastering: E’ il bus che decide quando far comunicare i dispositivi e non il DMA controller, è usato a volte quando quest’ultimo non c’è.

[11] Plug and play: Plug-and-Play (PnP) ha diversi significati. In senso lato è semplicemente l'autoconfigurazione che avviene quando uno installa un nuovo dispositivo e questo si configura da solo. Normalmente se abilitato dal BIOS, quest’ultimo assegna IRQ e mappatura della memoria del dispositivo trovato.

[12] Bootstrap : Prima fase all’accensione del calcolatore, dopo il caricamento del BIOS vengono interrogati tutti i dispositivi presenti sulla macchina.

[13] Firmware : Programma di basso livello per processori. In esso sono contenute tutte le istruzioni per la comunicazioni con i dispositivi con cui comunica e le mansioni che deve svolgere per cui è stato progettato.

[14] Wakeup e Suspend : Sono due funzioni usate per la sospensione e riattivazione del dispositivo, ma non tutti i dispositivi supportano tali funzioni;

[15] Kernel space: In un sistema operativo esistono due modalità di operare, kernel mode e user mode. Il kernel mode è il livello priviegiato. In entrambe le modalità viene riservata un’area di memoria. Nel kernel space vengono allocati i dati usati dal kernel, mentre nella user space i dati dei programmi.

[16] Scheduling: Letteralmente schedulazione,cioè gestire i vari processi in modo ordinato in sequenza.

[17] Stderr: Standard error, nel sistema operativo Linux esistono tre canali di comunicazione, standard input dove vengono interpretati i dati di input (normalmente la tastiera ), standard output , dove vengono stampati i risultati e standard error dove vengono stampati gli errori ( normalmente il monitor ) .