Torna indietro   Hardware Upgrade Forum > Software > Programmazione > Corsi, Tutorial e FAQ

Apple MacBook Air M3: chi deve davvero comprarlo? La recensione
Apple MacBook Air M3: chi deve davvero comprarlo? La recensione
A distanza di circa 8 mesi arriva l’importante aggiornamento dei MacBook Air: nessun cambiamento estetico, ma una revisione hardware interna con l’upgrade al processore M3. Le prestazioni migliorano rispetto alle generazioni precedenti, e questo fa sorgere una domanda spontanea: a chi è rivolto oggi questo laptop? Cerchiamo di capirlo nella nostra recensione 
ASUS ROG Swift OLED PG49WCD: quando QD-OLED e ultrawide si fondono
ASUS ROG Swift OLED PG49WCD: quando QD-OLED e ultrawide si fondono
Da ASUS un monitor particolare ma molto completo: principalmente indirizzato al videogiocatore, può essere sfruttato con efficacia anche per attività creative e di produzione multimediale
Dreame L10s Pro Ultra Heat: la pulizia di casa tutta sostanza
Dreame L10s Pro Ultra Heat: la pulizia di casa tutta sostanza
Il nuovo robot aspirapolvere domestico di Dreame abbina funzionalità complete a un moccio flottante che raggiunge al meglio gli angoli delle pareti. Un prodotto tutto in uno semplice da utilizzare ma molto efficace, in grado di rispondere al meglio alle necessità di pulizia della casa
Tutti gli articoli Tutte le news

Vai al Forum
Discussione Chiusa
 
Strumenti
Old 07-01-2005, 10:19   #1
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Introduzione alla scrittura di driver per linux

Questo thread è destinato esclusivamente ai programmatori che intendono imparare le basi di programmazione in kernel space. Assumo che i dettagli della programmazione in c e di compilazione del kernel siano noti.
Coprirò esclusivamente i kernel della serie 2.6, anche se le differenze con i kernel 2.4 non sono sempre profonde.

Argomenti trattati:
- Documentazione
- Utilizzo di kbuild
- Hello world in kernel space
- Esempio di kernel process
- Passare parametri a un driver
- Esempio di interfaccia con lo userspace: i miscdevice
- Esempio di interfaccia con lo userspace: read e write
- Tecniche di locking: semafori
- Esempio di interfaccia con lo userspace: mmap
- Le tecniche di allocazione della memoria
- Ancora sulle tecniche di locking
- Introduzione ai block device
- Strategie di sincronizzazione
- Demandare una operazione ad un altro contesto: le workqueue
- Demandare una operazione ad un altro contesto: i tasklet
- Esempio: driver di acquisizione video
- Organizzazione degli oggetti nel kernel: kobject e sysfs. Il nuovo Device Model.
- Device Model: revisione del driver di acquisizione video
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 17-01-2005 alle 11:01.
ilsensine è offline  
Old 07-01-2005, 10:34   #2
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Documentazione

Esistono alcuni libri di riferimento, scritti però per i kernel 2.4. Sono un utile strumento di riferimento:
Linux Device Drivers (seconda edizione) scritto da Rubini: (update: è uscita la terza edizione, aggiornata al kernel 2.6.10)
http://www.xml.com/ldd/chapter/book/
Understanding the Linux Kernel (Bovet, Cesati):
http://www.amazon.com/exec/obidos/tg...=UTF8&v=glance
Linux kernel internals (aa.vv.)
http://www.amazon.com/exec/obidos/tg...35837?v=glance
Understanding the linux virtual memory manager (Mel Gorman):
http://phptr.com/title/0131453483

Differenze con il kernel 2.6:
Esistono una serie di articoli che coprono le differenze tra le due serie. Gli articoli sono stati pubblicati da lwn, e costituiscono un importante completamento dei libri suggeriti in precedenza:
http://lwn.net/Articles/driver-porting/

Mailing list
The Linux Kernel Mailing List:
http://marc.theaimsgroup.com/?l=linux-kernel&r=1&w=2
Kernelnewbies mailing list:
http://mail.nl.linux.org/kernelnewbies/

Sorgenti del kernel
Al di là dei libri, un driver ben scritto vale più di mille pagine. I sorgenti del kernel sono una risorsa inestimabile di documentazione.
Esiste anche la directory Documentation dentro all'albero dei sorgenti, che copre argomenti specifici.

Altri documenti
Sono disponibili presso kernelnewbies.org; alcuni però sono diventati obsoleti:
http://www.kernelnewbies.org/documents/

Uno script utilissimo
Questo script:
Codice:
#!/bin/bash
FILTER=".[chsS]"
if [ "$1" = "" ]; then
  echo $0 \<string\>
  exit
fi
find . \( -path './*' \) \
  -name \*${FILTER} -exec grep -l -- "$*" {} \; 2>/dev/null
cerca ricorsivamente, a partire dalla directory corrente, i file sorgente che contengono la stringa indicata. E' utilissimo per rintracciare tra i sorgenti del kernel degli esempi di come è usata una determinata funzione.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 14-04-2005 alle 12:12.
ilsensine è offline  
Old 07-01-2005, 10:46   #3
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
kbuild

Il kernel dispone di un sistema di compilazione flessibile ed efficiente. E' molto facile utilizzarlo per compilare un driver esterno: tutto ciò di cui avete bisogno, è dei sorgenti correttamente configurati del vostro kernel in esecuzione.

Per utilizzare il sistema kbuild del kernel per compilare un driver esterno, occorre
- i sorgenti del driver, ovviamente
- preparare un semplice Makefile
- invocare kbuild

Un esempio varrà come spiegazione: create una directory che dovrà contenere il vostro driver; all'interno della directory, create un file sorgente vuoto (touch hello.c ad esempio). Create quindi il Makefile, contenente solo questa riga:
obj-m := hello.o

kbuild viene invocato eseguendo make con alcuni parametri; visto che si tratta di una operazione comune per i driver esterni, vi consiglio di creare uno script che automatizza il tutto. Create quindi lo script kbuild, che renderete eseguibile, contenente queste righe:
Codice:
#!/bin/bash
KDIR=/lib/modules/`uname -r`/build
make -C $KDIR M=`pwd` $@
Copiate questo script nel path (lo utilizzeremo anche per compilare gli esempi successivi).
Da dentro la directory contenente il Makefile e hello.c appena creati, eseguite quindi
kbuild modules
Vi verrà generato il file hello.ko, il vostro "driver" che potete inserire e rimuovere con insmod/rmmod (provate!). Ovviamente questo modulo non farà nulla, in quanto non contiene codice, ma vi illustra quanto sia semplice compilare un driver esterno.
Per ripulire i sorgenti, basta che eseguite
kbuild clean
dentro la directory da ripulire.

Per ulteriore documentazione, leggete Documentation/kbuild/modules.txt dentro i sorgenti del kernel.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 17-01-2005 alle 10:59.
ilsensine è offline  
Old 07-01-2005, 11:11   #4
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Hello world in kernel space

Ora che avete capito come compilare un modulo per inserirlo nel kernel, vediamo come è fatto lo scheletro di un driver. Partiamo dal comune hello world:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

static int __init hello_init(void)
{
	printk(KERN_INFO "Hello world!\n");
	return 0;
}

static void __exit hello_exit(void)
{
	printk(KERN_INFO "Goodbye!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("hello-world module");
Esaminiamo in dettaglio le varie righe:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
Gli include sono ricercati nella directory "include/" dei sorgenti. Dentro questa esistono, tra tutte, due sottodirectory principali:
linux/ contenente gli header piattaforma-indipendenti del kernel
asm/ contenente gli header piattaforma-dipendenti
Per nessun motivo un driver può includere gli header userspace, ovviamente.
I tre #include inseriti sono il minimo necessario per un modulo.
Quote:
static int __init hello_init(void)
static void __exit hello_exit(void)
Queste due funzioni sono invocate al caricamento e allo scaricamento del driver. La funzione di caricamento può fallire, ritornando con un valore negativo l'errore avvenuto (ad es. -ENOMEM, -ENODEV...). La seconda funzione non può mai fallire: notate però che lo scaricamento di un driver è consentito solo se non esistono reference count (quindi "utenti") del driver. Il controllo lo fa automaticamente il kernel, quindi non dovete preoccuparvene.
Le macro __init ed __exit servono ad indicare che le funzioni hanno validità solo in fase di inizializzazione o di finalizzazione: quindi ad esempio la funzione hello_init può essere scartata dopo l'inizializzazione del modulo (liberando così un pò di memoria); la hello_exit invece può non essere proprio compilata se inserite il vostro driver staticamente nel kernel.
Codice:
printk(KERN_INFO "Hello world!\n");
Questa funzione è equivalente alla funzione printf in user space. Tuttro ciò che stampa finisce nel kernel log, e può essere visualizzato tramite dmesg.
La macro (opzionale) KERN_INFO indica il livello di priorità del messaggio; esistono 8 livelli di priorità, definiti in kernel.h.
Codice:
module_init(hello_init);
module_exit(hello_exit);
Tramite queste macro informiamo il kernel sui nomi delle nostre funzioni di inizializzazione e finalizzazione.
Codice:
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("hello-world module");
Informazioni opzionali descrittive. Vengono visualizzate se eseguite un modinfo sul vostro driver.
Codice:
MODULE_LICENSE("GPL");
Non avete intenzione di "tainteggiare" il vostro kernel, vero?
Notate inoltre che alcune funzionalità del kernel sono disponibili solo ai driver con licenza GPL.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 07-01-2005 alle 11:16.
ilsensine è offline  
Old 07-01-2005, 11:31   #5
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Esempio di kernel process

Stranamente, alcune operazioni semplici in user space (come ad esempio la lettura di un file) risultano molto più complicate se effettuate dentro al kernel. Una operazione che, con le dovute cautele, risulta ancora molto semplice è la creazione di un thread.
Un modulo di per se non è un kernel thread: quando ne vengono invocate le funzionalità, il codice viene eseguito nel contesto del chiamante (che può essere un processo utente, o chiunque altro). A volte risulta utile creare un processo apposito, esistente solo in kernel space, per effettuare con calma alcune operazioni che risulterebbe troppo oneroso eseguire in altri contesti (ad es. in contesto di interruzione, o in una sezione atomica).
Questo esempio crea un processo che stampa "tick" ogni secondo:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kthread.h>
#include <linux/delay.h>

static struct task_struct *ktask;

static int kt_thread(void *arg)
{
	while (!kthread_should_stop()) {
		printk(KERN_INFO "tick\n");
		msleep(1000);
	}
	return 1;
}

static int __init kt_init(void)
{
	ktask = kthread_create(kt_thread, NULL, "kthread");
	if (IS_ERR(ktask)) {
		printk(KERN_ERR "Unable to create a new kernel thread!\n");
		return PTR_ERR(ktask);
	} else
		wake_up_process(ktask);
	return 0;
}

static void __exit kt_exit(void)
{
	int ret = kthread_stop(ktask);
	printk(KERN_INFO "thread killed (retval=%d)\n", ret);
}

module_init(kt_init);
module_exit(kt_exit);

MODULE_AUTHOR("ilsensine");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("sample kernel process");
Il thread viene creato con la funzione kthread_create. A questa funzione passiamo la funzione thread, il parametro eventuale per questa funzione, ed il nome. Il processo creato verrà visualizzato tramite top insieme agli altri processi.
La kthread_create crea il processo nello stato di "sospeso"; per lanciarlo, è sufficiente "svegliarlo" con wake_up_process.
La terminazione del processo avviene invocando la kthread_stop: questa funzione è _bloccante_ fino alla terminazione del processo, e ne restituisce il valore di ritorno. Notate che il processo deve attivamente esaminare la richiesta di terminazione (kthread_should_stop).
Nota importante: tutti i segnali diretti al nuovo processo sono bloccati di default (compreso l'imbloccabile SIGKILL!). Sbloccarne alcuni è semplice, ma non entro nei dettagli ora. Notate infine che la funzione msleep non può essere interrotta da alcun segnale; se vi interessa uno sleep interrompibile, occorre usare una tecnica leggermente diversa.
Un'ultima nota riguardo questa riga:
Codice:
static struct task_struct *ktask;
La dichiarazione di una variabile globale per il driver, non inizializzata, finisce nella sezione elf .bss del modulo. Al caricamento del driver, è garantito che tutte le variabili nella .bss sono inizializzate a 0.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 16:42.
ilsensine è offline  
Old 07-01-2005, 11:42   #6
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Passare parametri a un driver

Piccola variazione sull'esempio precedente: rendiamo configurabili il messaggio stampato e il delay:

Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kthread.h>
#include <linux/delay.h>

static struct task_struct *ktask;

static char *kmsg = "tick";
static ushort kdelay = 1000;
 
static int kparm_thread(void *arg)
{
	while (!kthread_should_stop()) {
		printk(KERN_INFO "%s\n", kmsg);
		msleep(kdelay);
	}
	return 0;
}

static int __init kparm_init(void)
{
	ktask = kthread_create(kparm_thread, NULL, "kthread");
	if (IS_ERR(ktask))
		return PTR_ERR(ktask);
	else {
		wake_up_process(ktask);
		printk(KERN_INFO "msg=%s delay=%d ms\n", kmsg, kdelay);
	}
	return 0;
}

static void __exit kparm_exit(void)
{
	kthread_stop(ktask);
}

module_param(kmsg, charp, 0444);
MODULE_PARM_DESC(kmsg, "Message to print");
module_param(kdelay, ushort, 0644);
MODULE_PARM_DESC(kdelay, "Printing delay (ms)");

module_init(kparm_init);
module_exit(kparm_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("modparm test module");
L'utilizzo di module_param e MODULE_PARM_DESC è abbastanza intuitivo. I parametri del vostro driver compariranno magicamente in /sys/module/<nome del vostro modulo>/. Quelli che risultano scrivibili (kdelay nell'esempio precedente, per il quale ho impostato i permission bits a 0644), possono essere modificabili runtime con un semplice echo (quindi nell'esempio possiamo modificare runtime il tempo di delay). Nota: non rendete una stringa scrivibile. Siete stati avvertiti
I parametri disponibili saranno visualizzati eseguendo modinfo sul modulo. Possono essere impostati in fase di insmod; ad es:
insmod <modulo.ko> kdelay=500 kmsg="Ciao"
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 13-12-2006 alle 10:11.
ilsensine è offline  
Old 07-01-2005, 12:08   #7
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Esempio di interfaccia con lo userspace: i miscdevice

Esistono diversi modi di interfacciarsi con lo userspace. La scelta dell'interfaccia è una scelta di design, prima che tecnica. I modi più comuni sono:
- syscall
- char device
- block device
Le syscall non possono essere gestite da un modulo esterno; è improbabile che vi capiterà mai di aggiungerne una. Non ne parleremo.
I char device sono dispositivi molto semplici, tramite i quali lo scambio di dati (tramite read/write/ioctl) è molto intuitivo.
I block device sono molto più complicati, ne parleremo più avanti.
Per ora parleremo di un tipo particolare di char device, i misc device: sono a tutti gli effetti dei char device (con major fisso pari a 10), ma con alcune semplificazioni per lo sviluppatore. Sono l'ideale per la creazione di driver semplici, oppure per iniziare a capire di cosa si sta parlando.
Questo codice crea un misc device che stampa un messaggio in syslog ogniqualvolta qualcuno apre e chiude il dispositivo. Visto che ho detto al sistema di assegnare un minor automatico, è consigliabile utilizzarlo con il devfs o udev:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>

static int kmisc_open(struct inode *ino, struct file *filp)
{
	printk(KERN_INFO "%s\n", __func__);
	return 0;
}

static int kmisc_release(struct inode *ino, struct file *filp)
{
	printk(KERN_INFO "%s\n", __func__);
	return 0;
}

static struct file_operations kops = {
	.owner		= THIS_MODULE,
	.open		= kmisc_open,
	.release	= kmisc_release,
};

static struct miscdevice kmisc = {
	.minor 	= MISC_DYNAMIC_MINOR,
	.name 	= "kmisc",
	.fops	= &kops
};

static int __init kmisc_init(void)
{
	int ret = misc_register(&kmisc);
	if (!ret)
		printk(KERN_INFO "kmisc registered on minor %d\n", kmisc.minor);
	return ret;
}

static void __exit kmisc_exit(void)
{
	misc_deregister(&kmisc);
}


module_init(kmisc_init);
module_exit(kmisc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("miscdevice module");
Tramite devfs/udev, il kernel si occupa della creazione del device (con devfs avrà il nome /dev/misc/kmisc). Il fulcro del driver è la struttura file_operations, ovvero la definizione dei metodi che possono essere invocati da userspace:
Codice:
static struct file_operations kops = {
	.owner		= THIS_MODULE,
	.open		= kmisc_open,
	.release	= kmisc_release,
};
Questa struttura, contenente in realtà parecchi altri metodi, è dichiarata in kernel/fs.h. Normalmente dovremo preoccuparci di scrivere solo alcuni dei metodi. Il campo "owner" indica il modulo proprietario del dispositivo; consente al kernel di tracciare i reference count dei moduli quando un programma utilizza un dispositivo. Gli altri due metodi implementati vengono invocati tramite la sys_open e la sys_close: notate che release viene chiamato solo quando l'ultima reference al file viene rilasciata dal programma (più reference possono aversi in seguito a un dup ad esempio).
A queste funzioni vengono passati due parametri: l'inode e il file. L'inode è il "dispositivo": ne esiste uno solo per il nostro miscdevice, comune a tutto il sistema. Il "file" è una istanza dell'inode, ovvero un inode aperto. E' direttamente legato al concetto di "file descriptor" ritornato dalla open: dentro al kernel infatti la "open" viene effettuata su un "inode", e restituisce un "file" (aperto). Possono ovviamente esistere più istanze "file" per un dato "inode".
A questo punto vi consiglio di dare una occhiata a fs.h (uno dei file più importanti del kernel), per avere una idea di come siano definite le strutture "inode", "file", "file_operations".

Compilato e inserito il modulo, potete forzare la stampa dei messaggi tramite un semplice "cat" sul dispositivo. Il cat ovviamente ritornerà errore, in quanto non abbiamo ancora implementato alcun metodo di lettura, ma in syslog potremo vedere i messaggi stampati all'apertura e chiusura del device.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 16:45.
ilsensine è offline  
Old 07-01-2005, 12:47   #8
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Esempio di interfaccia con lo userspace: read e write

Ora che abbiamo visto come creare un nodo di comunicazione con i programmi user space, implementiamo delle semplici operazioni di lettura/scrittura. L'idea è di allocare una certa quantità di memoria, e utilizzarla per scrivere e leggere da userspace. Il dispositivo quindi si comporterà come un file di dimensione fissa.
Dico subito che questo driver ha una grossolana race condition, che userò come spunto in seguito per introdurre un esempio di lock.
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <asm/uaccess.h>

static char *buf;
static int bsize = PAGE_SIZE;

ssize_t kmisc_read(struct file *filp, char __user *dst, size_t count, loff_t *off)
{
	ssize_t ret = 0;
	printk(KERN_INFO "%s: reading up to %d bytes at offset %lld\n", __func__,
		count, *off);
	if ((*off+count)>bsize)
		count = bsize-*off;
	if (count) {
		if (copy_to_user(dst, buf+*off, count))
			ret = -EFAULT;
		else {
			ret = count;
			*off += count;
		}
	}
	return ret;
}

ssize_t kmisc_write(struct file *filp, const char __user *src, size_t count, loff_t *off)
{
	ssize_t ret = 0;
	if ((*off+count)>bsize) {
		printk(KERN_WARNING "%s: discarding %lld bytes on %d\n", __func__,
			(*off+count)-bsize, count);
		count = bsize-*off;
	}
	printk(KERN_INFO "%s: writing %d bytes at offset %lld\n", __func__,
		count, *off);
	if (count) {
		if (copy_from_user(buf+*off, src, count))
			ret = -EFAULT;
		else {
			ret = count;
			*off += count;
		}
	}
	return ret;
}

static struct file_operations kops = {
	.owner		= THIS_MODULE,
	.read		= kmisc_read,
	.write		= kmisc_write,
};

static struct miscdevice kmisc = {
	.minor 	= MISC_DYNAMIC_MINOR,
	.name 	= "kmisc",
	.fops	= &kops
};

static int __init kmisc_init(void)
{
	int ret;
	buf = kzalloc(bsize, GFP_KERNEL);
	if (!buf)
		return -ENOMEM;
	ret = misc_register(&kmisc);
	if (!ret)
		printk(KERN_INFO "kmisc registered on minor %d\n", kmisc.minor);
	else
		kfree(buf);
	return ret;
}

static void __exit kmisc_exit(void)
{
	misc_deregister(&kmisc);
	kfree(buf);
}


module_init(kmisc_init);
module_exit(kmisc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("miscdevice module");
Ho rimosso i metodi open/release in quanto non dobbiamo fare cose particolari in quel contesto. Ho aggiunto i metodi read/write. Questi metodi non forniscono più il parametro inode; se necessario, può essere ottenuto da filp->f_dentry->d_inode. Entrambi i metodi restituiscono il numero di byte letti/scritti, se non si verificano errori.
La parte più importante del codice è come vengono letti e scritti i valori da userspace. I buffer userspace forniti alle funzioni hanno l'attributo __user, una macro che viene espansa in una direttiva per il compilatore che sostanzialmente significa "sevizia a sangue lo sviluppatore se tenta di accedere direttamente a questo puntatore". Accedere allo spazio di memoria utente è una operazione molto delicata, per due principali ragioni:
- sicurezza: l'utente può fornire alla read un puntatore puntante alla memoria del kernel; occorre controllare che appartenga al suo spazio di indirizzamento
- gestione della memoria: il buffer indicato dall'utente può non essere fisicamente in memoria: potrebbe essere finito in swap, oppure essere una pagina COW. In questo caso tentare di accedere a quella zona provoca un page fault, che va gestito.
Di queste operazioni se ne occupano le funzioni di accesso alla memori autente: copy_to_user, copy_from_user, get_user, put_user. Queste funzioni restituiscono 0 se l'operazione è andata a buon fine (notate che può essere necessario gestire un page fault, quindi queste funzioni possono causare un context switch verso altre parti del kernel senza preavviso), oppure un valore non nullo se si è verificato un errore (ad es. indirizzo non valido o riferito a memoria non mappata; in questo caso occorre restituire l'errore -EFAULT -- notate che questo _non_ causa un segmentation fault del programma).
Per ultimo, notate che la posizione attuale del file è presente nel parametro loff_t *off: proprio qui ho messo una bella race, in quanto tutte le funzioni lo utilizzano e modificano allegramente senza alcuna sicurezza che qualche altro thread del programma utente stia facendo lo stesso.

Potete testare questo modulo scrivendo qualcosa tramite echo, e rileggendola tramite cat.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 16:49.
ilsensine è offline  
Old 07-01-2005, 15:09   #9
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Tecniche di locking: semafori

Esistono diverse tecniche di locking a disposizione degli sviluppatori; le principali sono gli spinlock e i semafori. Questi ultimi possono essere usati come mutex.
L'esempio seguente aggiunge al driver precedente il locking per-file utilizzando i semafori:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <asm/uaccess.h>

static char *buf;
static int bsize = PAGE_SIZE;

struct kmisc_data {
	struct semaphore ksem;
};

static int kmisc_open(struct inode *ino, struct file *filp)
{
	struct kmisc_data *kdata = kmalloc(sizeof(*kdata), GFP_KERNEL);
	if (!kdata)
		return -ENOMEM;
	init_MUTEX(&kdata->ksem);
	filp->private_data = kdata;
	return 0;
}

static int kmisc_release(struct inode *ino, struct file *filp)
{
	kfree(filp->private_data);
	return 0;
}

ssize_t kmisc_read(struct file *filp, char __user *dst, size_t count, loff_t *off)
{
	ssize_t ret = 0;
	struct kmisc_data *kdata = filp->private_data;
	if (filp->f_flags & O_NONBLOCK) {
		if (down_trylock(&kdata->ksem))
			return -EAGAIN;
	} else {
		if (down_interruptible(&kdata->ksem))
			return -EINTR;
	}
	printk(KERN_INFO "%s: reading up to %d bytes at offset %lld\n", __func__,
		count, *off);
	if ((*off+count)>bsize)
		count = bsize-*off;
	if (count) {
		if (copy_to_user(dst, buf+*off, count))
			ret = -EFAULT;
		else {
			ret = count;
			*off += count;
		}
	}
	up(&kdata->ksem);
	return ret;
}

ssize_t kmisc_write(struct file *filp, const char __user *src, size_t count, loff_t *off)
{
	ssize_t ret = 0;
	struct kmisc_data *kdata = filp->private_data;
	if (filp->f_flags & O_NONBLOCK) {
		if (down_trylock(&kdata->ksem))
			return -EAGAIN;
	} else {
		if (down_interruptible(&kdata->ksem))
			return -EINTR;
	}
	if ((*off+count)>bsize) {
		printk(KERN_WARNING "%s: discarding %lld bytes on %d\n", __func__,
			(*off+count)-bsize, count);
		count = bsize-*off;
	}
	printk(KERN_INFO "%s: writing %d bytes at offset %lld\n", __func__,
		count, *off);
	if (count) {
		if (copy_from_user(buf+*off, src, count))
			ret = -EFAULT;
		else {
			ret = count;
			*off += count;
		}
	}
	up(&kdata->ksem);
	return ret;
}

static struct file_operations kops = {
	.owner		= THIS_MODULE,
	.open		= kmisc_open,
	.release	= kmisc_release,
	.read		= kmisc_read,
	.write		= kmisc_write,
};

static struct miscdevice kmisc = {
	.minor 	= MISC_DYNAMIC_MINOR,
	.name 	= "kmisc",
	.fops	= &kops
};

static int __init kmisc_init(void)
{
	int ret;
	buf = kzalloc(bsize, GFP_KERNEL);
	if (!buf)
		return -ENOMEM;
	ret = misc_register(&kmisc);
	if (!ret)
		printk(KERN_INFO "kmisc registered on minor %d\n", kmisc.minor);
	else
		kfree(buf);
	return ret;
}

static void __exit kmisc_exit(void)
{
	misc_deregister(&kmisc);
	kfree(buf);
}


module_init(kmisc_init);
module_exit(kmisc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("miscdevice module");
Da notare che il locking non viene fatto tramite down(), ma tramite alcune varianti. Innanzitutto se il file è stato aperto come non bloccante (O_NONBLOCK), dobbiamo onorare questa richiesta: dobbiamo quindi _tentare_ il lock con down_trylock e ritornare -EAGAIN se il nostro codice non è in grado di acquisire il lock senza attendere. Se il file è stato aperto normalmente, è invece preferibile la forma down_interruptible: questa down ritorna anche nel caso che il processo riceva un segnale, consentendo quindi un rapido ritorno in userspace per la gestione dello stesso. L'errore da ritornare in questo caso è il classico -EINTR.
La down() pura invece non ritorna mai, a meno di non aver acquisito il semaforo. Neanche un SIGKILL riuscirà ad uccidere un processo fermo dentro una down().
Notate che tutte le down, tranne la versione trylock, possono bloccare se il semaforo non è acquisibile: quindi non possono essere utilizzate in regioni del codice che non possono essere schedulate (notoriamente irq service routine, dentro uno spinlock, con gli irq disabilitati, ecc). E' legale, per contro, schedulare con un semaforo acquisito (alcuni driver, che non gradiscono multiple open, bloccano un semaforo dentro la open e lo rilasciano nella release).

Notare infine che il semaforo può essere usato anche come...semaforo, invece che come mutex; in questo caso per inizializzarlo usare sema_init (init_MUTEX non fa altro che inizializzare il semaforo a 1).
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 16:48.
ilsensine è offline  
Old 07-01-2005, 15:27   #10
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Esempio di interfaccia con lo userspace: mmap

Un mmap (memory map) consente di accedere direttamente da userspace a una zona di memoria del kernel (oppure memoria fisica, o anche memoria di i/o). Un driver che supporta l'mmap deve implementare la chiamata omonima delle file_operations.
Il modo classico per effettuare una mappatura è la funzione remap_page_range (in trasformazione nella più recente remap_pfn_range), che non tratterò (i driver sono pieni di esempi). Mostrerò invece una tecnica molto elegante (e semplice!), chiamata "nopage".
Per semplicità ho rimosso le altre file_operations, lasciando la sola mmap:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/vmalloc.h>
#include <asm/uaccess.h>

static char *buf;
static int bsize = 4*PAGE_SIZE;

static struct page *kmisc_vm_nopage(struct vm_area_struct *vma,
		unsigned long address, int *type);

static struct vm_operations_struct kmisc_vm_operations = {
	.nopage		= kmisc_vm_nopage,
};

static struct page *kmisc_vm_nopage(struct vm_area_struct *vma,
		unsigned long address, int *type)
{
	struct page *pg;
	unsigned long pg_base = vma->vm_pgoff<<PAGE_SHIFT;
	unsigned long ofs = pg_base+(address-vma->vm_start);

	ofs &= PAGE_MASK;
	printk(KERN_INFO "%s: page fault at %08lx [vma %08lx-%08lx pgoff %08lx]\n",
		__func__, address, vma->vm_start, vma->vm_end,
		vma->vm_pgoff<<PAGE_SHIFT);
	if (ofs>=bsize)
		return NOPAGE_SIGBUS;
	pg = vmalloc_to_page(buf+ofs);
	if (pg)
		get_page(pg);
	if (type)
		*type = VM_FAULT_MINOR;
	return pg;
}

static int kmisc_mmap(struct file *filp, struct vm_area_struct *vma)
{
	unsigned long size  = vma->vm_end - vma->vm_start;
	unsigned long pg_base = vma->vm_pgoff<<PAGE_SHIFT;

	if ((pg_base+size)>bsize)
		return -EINVAL;
	if (!(vma->vm_flags&VM_SHARED))
		return -EINVAL;
	vma->vm_flags |= VM_RESERVED;
	vma->vm_ops = &kmisc_vm_operations;
	return 0;
}

static struct file_operations kops = {
	.owner		= THIS_MODULE,
	.mmap		= kmisc_mmap,
};

static struct miscdevice kmisc = {
	.minor 	= MISC_DYNAMIC_MINOR,
	.name 	= "kmisc",
	.fops	= &kops
};

static int __init kmisc_init(void)
{
	int ret;
	buf = vmalloc(bsize);
	if (!buf)
		return -ENOMEM;
	memset(buf, 0, bsize);
	ret = misc_register(&kmisc);
	if (!ret)
		printk(KERN_INFO "kmisc registered on minor %d\n", kmisc.minor);
	else
		vfree(buf);
	return ret;
}

static void __exit kmisc_exit(void)
{
	misc_deregister(&kmisc);
	vfree(buf);
}


module_init(kmisc_init);
module_exit(kmisc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("miscdevice module");
Quando un utente effettua la chiamata mmap, il kernel prepara una vma (virtual memory area) già pronta per contenere la mappatura alle pagine di memoria che vogliamo. La funzione che gestisce la nostra mmap ha questo prototipo:
Codice:
static int kmisc_mmap(struct file *filp, struct vm_area_struct *vma)
Della vma, ci interessano sostanzialmente questi campi:
- vma->vm_start, vma->vm_end: limiti (indirizzi _virtuali_!!) della regione
- vma->vm_pgoff: offset di pagina (il parametro "offset" della syscall mmap, diviso per la dimensione di pagina).
In questo esempio la funzione kmisc_mmap fa veramente poco, anzi nulla: controlla la dimensione della regione, controlla che la mappatura sia "condivisa" (non può essere altrimenti, stiamo condividendo memoria tra kernel e user space; non possiamo gestire una MAP_PRIVATE qui), imposta il flag VM_RESERVED (informa il sistema di gestione della vm che non deve mettere in swapout questa vma) e imposta le vm_operations per la vma. Nessuna mappatura fisica viene creata tra la memoria di sistema e la vma del processo. Non ancora.
La prima volta che il processo tenterà di accedere fisicamente a questa vma, non essendoci ancora nessuna pagina di memoria mappata, verrà generato un page fault. Il kernel controllerà quindi se per questa vma esiste il metodo "nopage", e lo invocherà per chiedere l'assegnazione della pagina di memoria richiesta. Il kernel vuole solo la pagina, penserà lui al resto (gestione mmu, ecc.)
Alla funzione
Codice:
static struct page *kmisc_vm_nopage(struct vm_area_struct *vma,
		unsigned long address, int *type)
viene passata la vma di interesse e l'indirizzo al quale è avvenuto il page fault. Il terzo parametro serve per restituire il tipo di page fault (minor - la pagina è in memoria e deve essere solo rimappata - o major - è stato necessario i/o da un dispositivo di storage per recuperare la pagina).
Se il gestore nopage non può reperire la pagina, restituirà NOPAGE_SIGBUS (sostanzialmente NULL) e il programma si beccherà un SIGBUS.

Per testare la mmap di quero modulo si può usare il seguente programma, che legge il byte indicato nella forma (offset, posizione), e lo incrementa:
Codice:
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

#define fail(x) do { \
	perror(x); \
	exit(-1); \
} while(0)

int main(int argc, char **argv)
{
	unsigned long ofs;
	unsigned long pos;
	unsigned long pg = getpagesize();
	int fd;
	void *map;
	unsigned char *bitmap;

	if (argc<3) {
		fprintf(stderr, "%s <offset> <byte pos>\n", argv[0]);
		return -1;
	}
	ofs = atoi(argv[1]);
	pos = atoi(argv[2]);
	if (ofs&(pg-1)) {
		fprintf(stderr, "L'offset deve essere allineato alla pagina\n");
		return -1;
	}
	fd = open("/dev/misc/kmisc", O_RDWR);
	if (fd<0)
		fail("open");
	map = mmap(NULL, pos+1, PROT_READ|PROT_WRITE, MAP_SHARED, fd, ofs);
	if (map==MAP_FAILED)
		fail("mmap");
	close(fd);
	bitmap = map;
	fprintf(stderr, "Vecchio valore: %02x\n", bitmap[pos]);
	bitmap[pos]++;
	fprintf(stderr, "Nuovo valore: %02x\n", bitmap[pos]);
	munmap(map, pos+1);
	return 0;
}
Se reintroducete le funzioni read/write, potrete effettuare un dump (con cat o dd) del device e analizzare dove sono finite le modifiche effettuate dal programma di test, al variare di offset+posizione.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 16:53.
ilsensine è offline  
Old 07-01-2005, 16:56   #11
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Le tecniche di allocazione della memoria

Negli esempi precedenti ho utilizzato le funzioni kmalloc (o kzalloc)/kfree e vmalloc/vfree senza dare particolari spiegazioni. E' ora di ritornare sull'argomento.

Consiglio di leggere questo articolo per avere una idea più precisa di come funzionano le cose:
http://www.csn.ul.ie/~mel/projects/v...ml/understand/
Va ben oltre quello che può interessarvi, ma vale la pena leggerlo. nb Non tratta le tabelle di pagina di quarto livello (page upper directory, PUD), introdotte recentemente.

- Struttura della memory map del kernel:
Il kernel risiede nell'ultimo GB di memoria, da 0xc0000000 (PAGE_OFFSET) in poi. I 3 GB precedenti sono riservati per lo user space. E' possibile effettuare mappature diverse (io ad es. ho usato 2.5/1.5), oppure la totale separazione 4/4, ma il concetto è lo stesso.
Questo è il layout di indirizzi, visto dal kernel linux:

http://spazioinwind.libero.it/ilsens...ages/img18.png

La RAM fisica è divisa in tre zone: zona DMA "legacy", corrispondente ai primi 16MB di indirizzi fisici (utilizzata dai vecchi dispositivi ISA che potevano fare DMA solo in questi indirizzi); la zona NORMAL (utilizzabile anche per DMA a 32 bit), e la zona HIGHMEM. Le prime due zone sono permanentemente mappate in memoria: ovvero il kernel ha già indirizzi virtuali che corrispondono agli indirizzi fisici di queste regioni. La zona HIGHMEM invece è mappata, dinamicamente, solo quando necessario.
Per ogni pagina di memoria il kernel tiene una piccola struttura (struct page; v. linux/mm.h) ordinate nell'array mem_map. La funzione page_address(page) restituisce l'indirizzo di una pagina, se mappata (come accade per le zone NORMAL o DMA).
Le pagine sono gestite, al livello più basso, dal buddy allocator. Questo signore gestisce la memoria in pagine (la dimensione di una pagina è 4kb su x86), raggruppandole in blocchi contigui composti da un numero potenza di due di pagine.

Per richiedere delle pagine al buddy allocator si possono usare le funzioni alloc_pages/free_pages (linux/gfp.h):
Codice:
struct page *alloc_pages(unsigned int gfp_mask, unsigned int order)
dove "order" è l'ordine di pagine da allocare (significato: "voglio allocare 2^order di pagine" -- il buddy allocator lavora in potenze di 2), e gfp_mask contiene indicazioni da dove (preferibilmente) prendere le pagine, se l'allocazione deve essere atomica o se possiamo "aspettare un pò". Il valore restituito è la prima pagina del gruppo allocato. Se le pagine NON appartengono alla zona HIGHMEM, l'indirizzo virtuale corrispondente può essere ottenuto chiamando la funzione page_address(page). Per le pagine HIGHMEM questo non è possibile, in quanto non possiedono mappatura fissa (questo piccolo dettaglio è tra i motivi per cui è molto complicato per un sistema a 32 bit gestire decine di GB di memoria). Per gli interessati che cercano di accedere a pagine highmem, cercate le funzioni k(un)map[_atomic]. Non abusarne.
Nota importante: la memoria allocata da alloc_pages è memoria fisicamente continua. L'ordine massimo è MAX_ORDER (da 10 a 11 su x86, a seconda del kernel), che ci consente di allocare fino a PAGE_SIZE*2^MAX_ORDER byte di memoria fisicamente contigua. Per dimensioni superiori, occorrono altre tecniche (ovvero: è un casino). La memoria fisicamente contigua è importante per operazioni DMA.

Sotto il buddy allocator siede lo slab allocator (v. linux/slab.h e /proc/slabinfo), il cui scopo è consentire e gestire l'allocazione di oggetti più piccoli di una pagina. E' difficile che dobbiate usarlo direttamente.
Allo slab allocator accede la funzione kmalloc, la tecnica preferita di allocazione dentro al kernel: questa funzione consente di allocare velocemente piccole quantità di memoria (tipicamente fino a 128 KB) dalle zone NORMAL o DMA. Si tratta di memoria contigua fisicamente, volendo la si può usare per operazioni DMA. La sintassi è
kmalloc(size, GFP_KERNEL) per le allocazioni normali e
kmalloc(size, GFP_ATOMIC) per le operazioni atomiche (ad es. dentro un irq). Aggiungete GFP_DMA se vi serve memoria dalla zona DMA riservata alle schede legacy.
La variante kzalloc è equivalente a kmalloc, ma ritorna memoria già inizializzata a 0.
*Importante*: E' perfettamente normale (non è necessariamente un errore!) che una allocazione atomica possa fallire. Se usare GFP_ATOMIC, siate preparati. O meglio, cercate di evitarle se non è indispensabile.

Altra tecnica di allocazione è la funzione vmalloc/vfree. Caratteristiche:
- L'allocazione avviene solo per multipli di pagina (chiedete 1 byte, ne sprecate 4095)
- L'allocazione proviene da tutte le zone: non è importante se la memoria è già mappata o meno, viene comunque creata una nuova mappatura virtuale.
- La memoria restituita è virtualmente contigua, ma non fisicamente
- Può bloccare: non usare in contesti atomici!
- Può allocare regioni enormi, da VMALLOC_START a VMALLOC_END. Non esagerare con l'uso: appesantisce le tabelle di pagine.
- E' la funzione più simile a malloc: ciò nonostante, va usata solo quando necessario (nell'esempio sulla mmap ne ho in effetti "abusato").

Direi che non è il caso di addentrarsi oltre. Gli interessati possono leggere il link che ho consigliato, dove viene spiegato anche il significato delle regioni kmap e fixed map e tante altre cose.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 16:55.
ilsensine è offline  
Old 10-01-2005, 11:16   #12
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Ancora sulle tecniche di locking

Una delle maggiori differenze tra la programmazione in user space e in kernel space è come fare il locking. Abbiamo già visto un esempio di semafori, praticamente uguale ai classici semafori user space; presentano di diverso sostanzialmente la primitiva down_interruptible(), che consente di interrompere il tentativo di lock in presenza di un segnale. I semafori sono solo una delle tecniche a disposizione, e non possono essere usati ovunque: per questioni tecniche (non possiamo "dormire" aspettando che un semaforo si liberi in certe sezioni critiche), e per questioni di scalabilità. Esistono diverse altre tecniche che illustrerò brevemente.

- rwsem
Sono molto simili ai semafori, con la differenza di poter specificare se il tipo di lock è per operazioni di lettura o di scrittura. Sono pensati per la scalabilità, essendo idonei in situazioni dove ci sono molti "lettori" e pochi "scrittori" dell'oggetto protetto sal semaforo. E' possibile acquisire più "read lock" contemporanei, ma un "write lock" blocca qualsiasi altro tipo di lock. L'utilizzo è simile ai semafori (v. linux/rwsem.h):
struct rw_semaphore sem;
init_rwsem(&sem);
down_read[_trylock](&sem); up_read(&sem);
down_write[_trylock](&sem); up_write(&sem);
downgrade_write(&sem); (trasforma un write lock in un read lock)
Non esiste la versione _interruptible di queste funzioni. Presentano la stessa limitazione dei semafori riguardo le sezioni atomiche.

- seqlock
I rwsem scalano meglio dei semafori in lettura, ma rischiano di causare un "write starving" se ci sono molti lettori all'opera. Per ovviare a questa situazione si possono usare i seqlock, leggermente più complessi dei rwsem. Non ne parlerò in dettaglio; gli interessati possono consultare linux/seqlock.h

- Gestione irq
Non è una tecnica di locking vera e propria.
Per disabilitare/riabilitare gli irq sulla cpu corrente, usare
local_irq_disable(); local_irq_enable();
Se gli irq possono essere già disabilitati nella funzione corrente (ad es. perché la funzione può essere invocata da contesti diversi) utilizzare questa variante:
unsigned long flags;
local_irq_save(flags);
local_irq_restore(flags);
Note:
- Gli irq vengono disabilitati solo sul processore corrente. Attenzione agli ambienti smp (usare gli spinlock)
- Se possibile, è più efficiente disabilitare gli irq solo del proprio dispositivo.
- Disabilitare gli irq aumenta la latenza: usare solo per lo stretto indispensabile.
- Una sezione con gli irq disabilitati diventa una sezione atomica (non schedulare in tale contesto, e non chiamare funzioni che possono schedulare, come ad es. down() )

- Gestione della preemption
Versione più blanda della disabilitazione degli irq:
preempt_disable();
preempt_enable();
Disabilitano la preemption in kernel space. Come corollario, si è certi che il codice protetto non cambierà cpu di esecuzione (v. anche get_cpu(); put_cpu() )
Note:
- E' possibile annidare più preempt_disable().
- Aumenta la latenza delle syscall e dei wakeup, ma non della gestione degli irq

- the Big Kernel Lock (BKL)
Aiutate l'umanità a liberarsi definitivamente di questa bestia ignorandone l'esistenza.

- Gli spinlock
Tecnica di locking importantissima. E' l'unica che può essere usata con sicurezza all'interno di un irq handler.
Il comportamento degli spinlock è molto diverso a seconda di come è compilato il kernel: UP, UP+preempt, SMP, SMP+preempt. Per spiegare il loro funzionamento, partiamo dal motivo della loro introduzione: i sistemi SMP.
Uno spinlock per SMP è simile a un semaforo, con la differenza importante di tenere il processore in loop finché lo spinlock non risulta acquisibile. Il corollario ovvio è non tenere uno spinlock bloccato a lungo, ma solo per alcune istruzioni.
Con l'introduzione dei sistemi preempt, gli spinlock sono stati modificati: un sistema UP preempt si comporta in modo simile a un sistema multiprocessore. Uno spinlock per un sistema UP preempt è semplicemente un preempt_disable; per un sistema SMP preempt, la preemption viene disabilitata oltre ad acquisire il lock.
Per sistemi UP ovviamente gli spinlock sono dei no-op.
Sintassi:
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
spin_unlock(&lock);
E' disponibile anche la versione con disabilitazione degli irq:
unsigned long flags;
spin_lock_irqsave(&lock, flags); <== compilato come local_irq_save nei sistemi UP
spin_lock_irqrestore(&lock, flags); <== compilato come local_irq_restore nei sistemi UP
Gli spinlock rappresentano la tecnica di locking corretta tra process context e irq context.

Per altre informazioni sulle tecniche di locking illustrate (e alcune non illustrate, come le RCU o i lock per le Bottom Half) una buona guida sintetica di riferimento è la "Unreliable guide to Locking" di Rusty Russell:
http://www.kernel.org/pub/linux/kern...ernel-locking/
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 17:12.
ilsensine è offline  
Old 10-01-2005, 14:46   #13
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Introduzione ai block device

I char device offrono dei metodi di interfaccia (file_operations) molto completi, che coprono praticamente tutte le necessità. Perché allora sono stati introdotti i "block device"?
Immaginate il driver per una unità disco. Un simile driver deve soddisfare una serie di requisiti:
- supporto per il partizionamento
- supporto per il caching
- supporto per il read ahead
- supporto per una famiglia di ioctl specifiche per operazioni su disco
- semplicità di interfacciamento con altri layer del kernel (ad es. i file system)
- generalmente i dispositivi di storage permettono un accesso ai dati "per blocchi di byte"
Questi requisiti sono comuni con tutte le unità a disco, è superfluo che ogni driver ne duplichi le funzionalità. Il kernel di linux supporta automaticamente tutto questo; in particolare, supporta
- gestione avanzata della cache (buffer cache)
- ottimizzazione delle operazioni di i/o (i/o scheduler; al momento ne abbiamo quatto: anticipatory scheduler, cfq scheduler, deadline scheduler, null scheduler)
Il modello di servizi che il driver deve fornire è dunque diverso: anziché gestire direttamente le operazioni di accesso, il driver deve poter rispondere a delle operazioni che di tanto in tanto il kernel ci chiede di compiere (ad es. "leggi questa regione, scrivi quest'altra"...). Il resto è automaticamente svolto dal kernel.
Dicevo prima che l'implementazione di un block device è complicata: la complicazione sta appunto nel come il driver deve svolgere le richieste. L'esempio seguente è molto banale; implementeremo un driver che usa la memoria di sistema come storage per emulare un disco, e su di questo compie le operazioni di lettura/scrittura richieste dal kernel:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>

#define KERNEL_SECTOR_SIZE	512
#define DEVICE_SECTOR_SIZE	512
#define DEVICE_SECTORS		1024

static struct kblock_data {
	unsigned long ssize;
	unsigned long sectors;
	spinlock_t lock;
	unsigned char *map;
	struct gendisk *disk;
} kbdata;

static void kblock_do_request(struct kblock_data *kb, struct request *req)
{
	unsigned long offset = req->sector*KERNEL_SECTOR_SIZE;
	unsigned long nbytes = req->current_nr_sectors*KERNEL_SECTOR_SIZE;
	if ((req->sector+req->current_nr_sectors) >
			kb->sectors*(kb->ssize/KERNEL_SECTOR_SIZE)) {
		printk(KERN_ERR "kblock: Attempting to access beyond end of device\n");
		end_request(req, 0);
		return;
	}
	if (rq_data_dir(req)==WRITE)
		memcpy(kb->map+offset, req->buffer, nbytes);
	else
		memcpy(req->buffer, kb->map+offset, nbytes);
	end_request(req, 1);
}

static void kblock_request(request_queue_t *q)
{
	struct request *req;
	while ((req = elv_next_request(q)) != NULL) {
		if (!blk_fs_request(req)) {
			printk (KERN_INFO "Non-CMD request\n");
			end_request(req, 0);
			continue;
		}
		kblock_do_request(q->queuedata, req);
	}
}

static int kblock_open(struct inode *ino, struct file *filp)
{
	filp->private_data = &kbdata;
	return 0;
}

static int kblock_ioctl(struct inode *inode, struct file *filp,
		unsigned int cmd, unsigned long arg)
{
	struct hd_geometry geo;
	struct kblock_data *kb = filp->private_data;
	unsigned long  sz = kb->sectors*(kb->ssize/KERNEL_SECTOR_SIZE);
	int ret = 0;
	switch (cmd) {
	case HDIO_GETGEO:
		geo.cylinders = sz >> 6;
		geo.heads = 4;
		geo.sectors = 16;
		geo.start = 0;
		if(copy_to_user((void __user*) arg, &geo, sizeof(geo)))
			ret = -EFAULT;
		break;
	default:
		ret = -ENOTTY;
    }
    return ret;
}

static struct block_device_operations kblock_ops = {
	.owner	= THIS_MODULE,
	.open	= kblock_open,
	.ioctl	= kblock_ioctl
};

static int __init kblock_init(void)
{
	int ret = -ENOMEM;

	kbdata.ssize = DEVICE_SECTOR_SIZE;
	kbdata.sectors = DEVICE_SECTORS;
	spin_lock_init(&kbdata.lock);

	kbdata.map = vmalloc(kbdata.ssize*kbdata.sectors);
	if (!kbdata.map)
		return -ENOMEM;
	memset(kbdata.map, 0, kbdata.ssize*kbdata.sectors);

	kbdata.disk = alloc_disk(16);
	if (!kbdata.disk)
		goto out;
	kbdata.disk->first_minor = 0;
	kbdata.disk->fops = &kblock_ops;
	kbdata.disk->private_data = &kbdata;
	sprintf(kbdata.disk->disk_name, "kblock");
	sprintf(kbdata.disk->devfs_name, "kblock");
	set_capacity(kbdata.disk, kbdata.sectors*(kbdata.ssize/KERNEL_SECTOR_SIZE));

	kbdata.disk->major = register_blkdev(0, "kblock");
	if (kbdata.disk->major<0) {
		ret = kbdata.disk->major;
		goto out_disk;
	}

	kbdata.disk->queue = blk_init_queue(kblock_request, &kbdata.lock);
	if (!kbdata.disk->queue)
		goto out_unregister;
	blk_queue_hardsect_size(kbdata.disk->queue, kbdata.ssize);
	kbdata.disk->queue->queuedata = &kbdata;

	add_disk(kbdata.disk);
	printk(KERN_INFO "Device kblock registered on major %d\n", kbdata.disk->major);
	return 0;
out_unregister:
	unregister_blkdev(kbdata.disk->major, "kblock");
out_disk:
	put_disk(kbdata.disk);
out:
	vfree(kbdata.map);
	return ret;
}

static void __exit kblock_exit(void)
{
	request_queue_t *rq = kbdata.disk->queue;
	int major = kbdata.disk->major;
	del_gendisk(kbdata.disk);
	put_disk(kbdata.disk);
	unregister_blkdev(major, "kblock");
	blk_cleanup_queue(rq);
	vfree(kbdata.map);
}

module_init(kblock_init);
module_exit(kblock_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("Sample block device driver");
In questo driver sono presenti parecchie cose nuove, che esamineremo in dettaglio. Innanzitutto notate le file_operations per i block device (struct block_device_operations, definita in linux/fs.h): questa struttura è molto piu semplice delle fops relative a un char device. Nel nostro esempio i metodi open e ioctl non fanno praticamente nulla (l'ioctl emula semplicemente una rappresentazione chs del disco, non effettuabile in maniera generale dal kernel). Concentriamoci quindi sul resto.

Nella funzione di inizializzazione, viene dapprima allocata la memoria che conterrà il nostro disco:
Codice:
	kbdata.ssize = DEVICE_SECTOR_SIZE;
	kbdata.sectors = DEVICE_SECTORS;
	spin_lock_init(&kbdata.lock);
	kbdata.map = vmalloc(kbdata.ssize*kbdata.sectors);
	if (!kbdata.map)
		return -ENOMEM;
	memset(kbdata.map, 0, kbdata.ssize*kbdata.sectors);
Ogni block device è identificato da un certo numero di settori, e dalla dimensione del settore. Il kernel internamente accede a un block device in settori multipli di 512 byte; questo valore è indipendente dalla dimensione fisica del settore del nostro disco (nell'esempio sono stati impostati uguali). L'unico vincolo, è che la dimensione "hadrware" del settore deve essere un multimplo di 512.

Proseguiamo oltre. Tutta la gestione della "interfaccia come disco" del block device, compreso il supporto per il partizionamento, è gestito dall'interfaccia "gendisk" del kernel. Possiamo allocare un gendisk tramite la funzione alloc_disk:
Codice:
	kbdata.disk = alloc_disk(16);
che accetta come parametro il massimo numero di minor destinati al disco (pari al numero massimo delle partizioni più uno). Se utilizziamo "1", rinunciamo al supporto automatico per il partizionamento. Più avanti impostiamo la dimensione del disco:
Codice:
	set_capacity(kbdata.disk, kbdata.sectors*(kbdata.ssize/KERNEL_SECTOR_SIZE));
specificata in unità da 512 byte (KERNEL_SECTOR_SIZE).
Quindi dobbiamo "registrare" il nostro block device, riservando un major number:
Codice:
	kbdata.disk->major = register_blkdev(0, "kblock");
Avendo indicato "0", il major number verrà assegnato dinamicamente.
La funzione successiva è molto importante, è la vera differenza con i char device ed entra nel cuore del driver:
Codice:
	kbdata.disk->queue = blk_init_queue(kblock_request, &kbdata.lock);
Quello che il nostro driver dovrà fare non è gestire direttamente read/write/ecc, ma delle richieste di i/o provenienti dal kernel. Le richieste sono rappresentate da una "request queue" (v. linux/blkdev.h). Per allocare una request queue, dobbiamo indicare la funzione che svolgerà le richieste, e uno spinlock. Lo spinlock sarà automaticamente bloccato dal kernel prima di ogni chiamata alla funzione kblock_request.

Soffermiamoci quindi su questa funzione, che è sorprendentemente (ingannevolmente) semplice:
Codice:
static void kblock_request(request_queue_t *q)
{
	struct request *req;
	while ((req = elv_next_request(q)) != NULL) {
		if (!blk_fs_request(req)) {
			printk (KERN_INFO "Non-CMD request\n");
			end_request(req, 0);
			continue;
		}
		kblock_do_request(q->queuedata, req);
	}
}
Eseguiamo il ciclo while finché ci sono richieste preparate per noi dall'elevator (altro nome dell'i/o scheduler). Gestire bene una richiesta è complesso: ognuna è composta da una serie di richieste elementari (BIO) che possiamo demandare al dispositivo. Non seguiremo questa strada ma ci limiteremo a gestire la richiesta come un tutt'uno, così come ci è stata gentilmente accorpata dall'elevator. Di questo se ne occupa la funzione kblock_do_request, che è autoesplicativa vista la sua semplicità. Un driver hw ben scritto probabilmente vorrà sottomettere all'hw direttamente le singole richieste BIO, per sfruttare eventuali parallelismi, operazioni dma in scatter-gather o altro. Non entriamo in ulteriori dettagli.
Note:
- Dove scrivono le funzioni memcpy in kblock_do_request? Nella buffer cache del kernel.
- Occhio che kblock_request (quindi kblock_do_request) è eseguita sotto spinlock (v. drivers/block/ll_rw_blk.c). Quindi deve essere veloce, e in particolare non deve per nessun motivo schedulare. Sarebbe una buona idea demandare a "qualcun altro" lo svolgimento fisico delle operazioni, per motivi di latenza. Farò un esempio più avanti, tempo permettendo.
- blk_init_queue non è l'unico modo di gestire una request queue, ma è il più semplice. Esistono infatti diversi modi di lavorare con le richieste, dal più semplice (il nostro) al più dettagliato, a basso livello. Ad es. il driver loop.c utilizza un altro approccio.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 16:58.
ilsensine è offline  
Old 11-01-2005, 12:32   #14
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Strategie di sincronizzazione

La sincronizzazione tra diversi eventi asincroni fa uso, implicito o esplicito, delle code di attesa (wait queue).
In questo esempio, ricco di tecniche nuove, vengono illustrati l'attesa su eventi e l'attesa di completamento:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/list.h>
#include <linux/kthread.h>
#include <linux/wait.h>
#include <linux/completion.h>
#include <linux/delay.h>
#include <asm/uaccess.h>

struct kmisc_request {
	struct list_head head;
	char *buf;
	int bsize;
	struct completion comp;
};

static struct kmisc_data {
	struct list_head reqlist;
	spinlock_t lock;
	wait_queue_head_t wq;
	struct task_struct *ktask;
} kdata;

static int kmisc_thread(void *arg)
{
	struct kmisc_data *kdata = arg;
	struct kmisc_request *kreq;
	int pos;
	while (1) {
		wait_event_interruptible(kdata->wq, !list_empty(&kdata->reqlist));
		if (unlikely(signal_pending(current)))
			break;

		BUG_ON(list_empty(&kdata->reqlist));
		kreq = list_entry(kdata->reqlist.next, struct kmisc_request, head);
		spin_lock(&kdata->lock);
		list_del(&kreq->head);
		spin_unlock(&kdata->reqlist);
		for (pos=0; pos<kreq->bsize; ++pos)
			kreq->buf[pos] = '0'+pos%10;
		complete(&kreq->comp);
	}
	while (!kthread_should_stop())
		msleep(1);
	return 0;
}

static ssize_t kmisc_read(struct file *filp, char __user *dst, size_t count, loff_t *off)
{
	struct kmisc_request *kreq;
	if (unlikely(!kdata.ktask))
		return -ENODEV;
	if (count>(PAGE_SIZE-sizeof(*kreq)))
		count=PAGE_SIZE-sizeof(*kreq);
	kreq = kmalloc(sizeof(*kreq)+count, GFP_KERNEL);
	if (unlikely(!kreq))
		return -ENOMEM;
	kreq->bsize = count;
	kreq->buf = (char *) (kreq+1);
	init_completion(&kreq->comp);
	spin_lock(&kdata.lock);
	list_add_tail(&kreq->head, &kdata.reqlist);
	spin_unlock(&kdata.lock);
	wake_up_interruptible(&kdata.wq);
	wait_for_completion(&kreq->comp);
	if (unlikely(copy_to_user(dst, kreq->buf, count)))
		count = -EFAULT;
	kfree(kreq);
	return count;
}

static struct file_operations kops = {
	.owner		= THIS_MODULE,
	.read		= kmisc_read,
	.llseek		= no_llseek
};

static struct miscdevice kmisc = {
	.minor 	= MISC_DYNAMIC_MINOR,
	.name 	= "kmisc",
	.fops	= &kops
};

static int __init kmisc_init(void)
{
	int ret;
	struct task_struct *ktask;
	INIT_LIST_HEAD(&kdata.reqlist);
	spin_lock_init(&kdata.lock);
	init_waitqueue_head(&kdata.wq);
	ret = misc_register(&kmisc);
	if (!ret) {
		ktask = kthread_create(kmisc_thread, &kdata, "kmisc");
		if (IS_ERR(ktask)) {
			misc_deregister(&kmisc);
			ret = PTR_ERR(ktask);
		} else {
			wake_up_process(ktask);
			kdata.ktask = ktask;
		}
	}
	if (!ret)
		printk(KERN_INFO "kmisc registered on minor %d\n", kmisc.minor);
	return ret;
}

static void __exit kmisc_exit(void)
{
	misc_deregister(&kmisc);
	WARN_ON(!list_empty(&kdata.reqlist));
	force_sig(SIGKILL, kdata.ktask);
	kthread_stop(kdata.ktask);
}


module_init(kmisc_init);
module_exit(kmisc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("miscdevice module");
Spiegazione:
Questo modulo simula la lettura di dati da un dispositivo. La creazione effettiva dei dati fittizzi avviene in un kernel thread. Il modulo mantiene una lista di richieste di lettura dai processi utente (kdata.reqlist; v. linux/list.h per la gestione delle liste su linux). Il thread kmisc_thread rimane "a dormire" sulla waitqueue kdata.wq finché non viene "svegliato" dalla funzione read, tramite la wake_up_interruptible. Il secondo parametro della funzione wait_event_interruptible è una condizione, che viene esaminata prima di andare "a dormire" -- se risulta vera, il codice continua senza bloccare (serve ad evitare una race condition -- riuscite a vederla?). La funzione ritorna anche in presenza di un segnale, che utilizzeremo per terminare il thread (v. force_sig in kmisc_exit -- notate che force_sig invia il segnale anche se è normalmente bloccato).
La read, prima di svegliare il thread, accoda la richiesta alla lista di richieste. Visto che sia la read che il kernel thread girano in contesto di processo, possiamo proteggere la lista tramite un semaforo o uno spinlock (senza disabilitare gli irq). Visto che il codice da proteggere è veramente breve, utilizziamo lo spinlock (un semaforo richiederebbe uno schedule se risulta bloccato, e uno schedule richiede molte più istruzioni di quelle eseguite nella regione critica. Molto più efficiente un veloce spinlock, in questa situazione). Una volta aggiornata la lista, il kernel thread viene eventualmente svegliato, nel caso stia "dormendo", dalla wake_up_interruptible. Quindi, la read deve aspettare che la richiesta venga soddisfatta, e attende quindi il completamento dell'operazione andando a "dormire" su un oggetto completion (tramite wait_for_completion). Ogni volta che una richiesta è ultimata, il kernel thread lo segnala tramite una chiamata a complete(). Notate che un oggetto completion ha una waitqueue all'interno (v. linux/completion.h)

Come funzionano le waitqueue?
Ogni processo è contraddistinto da uno "stato" di esecuzione (current->state). Questo stato può essere "task in esecuzione" (TASK_RUNNING), "task non in esecuzione, non interrompibile da segnali" (TASK_UNINTERRUPTIBLE), "task non in esecuzione, interrompibile da segnali" (TASK_INTERRUPTIBLE). Ad ogni ciclo di scheduling, solo i task RUNNING vengono presi in considerazione per l'esecuzione. Gli altri sono semplicemente ignorati. Quando poniamo il nostro task "a dormire" su una waitqueue, semplicemente "cambiamo stato di esecuzione" in INTERRUPTIBLE oppure UNINTERRUPTIBLE e invochiamo lo scheduler. Lo aggiungiamo anche ad una lista di task in attesa, lista contenuta nell'oggetto wait_queue_head_t (v. linux/wait.h), in modo da poterlo rintracciare con le apposite funzioni di wake. Quando lo "svegliamo", semplicemente riportiamo il suo stato in RUNNING. Semplice ed efficiente.
Per un semplice esempio di come invocare direttamente lo scheduler, v. il codice di msleep in kernel/timer.c.

Q&A
- Cosa sono BUG_ON e WARN_ON?
Sono delle macro di debug, che vengono ottimizzate via se non si abilita il kernel debugging. La prima causa l'uccisione del processo se la condizione è vera, la seconda solo un warning. In entrambi i casi viene stampato il file e la riga di errore, e lo stack trace. Attenzione che un BUG in irq context implica un kernel panic (un irq non ha un "contesto di processo").

- Cosa è "unlikely"?
likely e unlikely sono delle macro che si espandono in direttive del compilatore, che indicano che la condizione è probabile o improbabile. Servono al compilatore per ottimizzare il codice, indicando quale è il path di esecuzione "più probabile".

- Posso usare le funzioni di wake up/completion in irq context?


- Posso dormire su una wait queue in irq context?
Mai

- Cosa posso allora fare se dentro un irq devo "attendere un evento"?
Probabilmente cambiare design. Oppure, demandare l'operazione ad una workqueue da eseguire successivamente in contesto di processo.

- Possono esserci più task in attesa su una wait queue?
Sì. wake_up ne sveglia solo uno però. Per svegliarli tutti, usare wake_up_all & co.

- Voglio che il task svegliato sia eseguito immediatamente. Cosa devo fare?
Usare la variante wake_up_sync.

- Hai usato un buffer temporaneo; perché la read non ha passato direttamente il puntatore dst al kernel thread?
Quel puntatore è valido solo per il contesto di processo corrente, ed è tabù altrove. Il kernel thread ha un proprio contesto.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 17:00.
ilsensine è offline  
Old 12-01-2005, 10:36   #15
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Demandare una operazione ad un altro contesto: le workqueue

Una workqueue è - lo dice il nome - una coda di operazioni. Ad ogni workqueue è associato un kernel thread (o meglio, un thread per cpu) che esegue le operazioni indicate in contesto di processo.
Il loro utilizzo è molto semplice; riprendiamo l'esempio sui kernel thread:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kthread.h>
#include <linux/workqueue.h>
#include <linux/delay.h>

static struct task_struct *ktask;
static struct workqueue_struct *wq;

static void kt_work(void *data)
{
	struct work_struct *work = data;
	printk(KERN_INFO "%s\n", __func__);
	kfree(work);
}

static int kt_thread(void *arg)
{
	struct work_struct *work;
	struct workqueue_struct *wq = arg;
	while (!kthread_should_stop()) {
		work = kmalloc(sizeof(*work), GFP_KERNEL);
		if (work) {
			INIT_WORK(work, kt_work, work);
			queue_work(wq, work);
		} else
			printk(KERN_ERR "%s: OOM!\n", __func__);
		msleep(1000);
	}
	return 0;
}

static int __init kt_init(void)
{
	wq = create_workqueue("kworker");
	if (!wq)
		return -ENOMEM;
	ktask = kthread_create(kt_thread, wq, "kthread");
	if (IS_ERR(ktask)) {
		destroy_workqueue(wq);
		return PTR_ERR(ktask);
	} else
		wake_up_process(ktask);
	return 0;
}

static void __exit kt_exit(void)
{
	kthread_stop(ktask);
	flush_workqueue(wq);
	destroy_workqueue(wq);
}

module_init(kt_init);
module_exit(kt_exit);

MODULE_AUTHOR("ilsensine");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("sample kernel process");
La workqueue viene creata tramite la funzione create_workqueue. I thread assegnati alla workqueue saranno visibili tramite top. Un task viene accodato alla queue tramite la funzione queue_work. Un singolo work è descritto dalla struttura work_struct, inizializzata dalla macro INIT_WORK(struttura, funzione, parametro della funzione).

Note:
- In questo esempio la workqueue viene creara e distrutta esternamente al kernel thread. Potevamo crearla e distruggerla direttamente nel thread? La risposta è no: il nostro thread viene terminato invocando la funzione kthread_stop. Anche la funzione destroy_workqueue chiama kthread_stop per terminare i propri thread. Orbene, kthread_stop blocca un semaforo finché il thread non è terminato; quindi se il thread in fase di chiusura chiama a sua volta kthread_stop (o una funzione che la richiama, come destroy_workqueue), abbiamo un bel deadlock.
- Un work può essere schedulato nel futuro tramite la funzione queue_delayed_work, che ha il prototipo (v. linux/workqueue.h):
Codice:
int queue_delayed_work(struct workqueue_struct *wq,
	struct work_struct *work, unsigned long delay)
dove delay è espresso in jiffies (tick di scheduling. In un secondo occorrono HZ tick)
- Solo i moduli con licenza GPL possono creare proprie workqueue. I moduli proprietari possono utilizzare le workqueue di keventd, tramite le funzioni schedule_[delayed_]work.
- flush_workqueue attende la terminazione dei work in coda. Viene chiamata anche da destroy_workqueue, quindi nel codice illustrato risulta superflua (è stata messa solo come esempio).
- Visto che le workqueue sono eseguite in contesto di processo, è possibile chiamare funzioni che possono schedulare (come ad es. down).
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 17:01.
ilsensine è offline  
Old 12-01-2005, 11:24   #16
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Demandare una operazione ad un altro contesto: i tasklet

Le workqueue sono utili per la maggior parte delle operazioni. Vengono eseguite con priorità molto alta (nice -10).
Se avete bisogno di un work da eseguire con priorità VERAMENTE alta, potete utilizzare i tasklet. Occorre però rinunciare a qualcosa: i tasklet sono eseguiti in contesto di irq (softirq), quindi c'è qualche limitazione.
Questi link descrivono sinteticamente i tasklet e lo scopo delle bottom half in generale:
http://www.opentech.at/papers/embedd...ces/node8.html
http://www.cs.utexas.edu/users/ygz/3...lecture11.html

L'uso è molto simile alle workqueue:
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kthread.h>
#include <linux/interrupt.h>
#include <linux/delay.h>

static struct task_struct *ktask;

static void kt_tasklet(unsigned long data)
{
	printk(KERN_INFO "%s\n", __func__);
}

static DECLARE_TASKLET(tlet, kt_tasklet, 0);

static int kt_thread(void *arg)
{
	while (!kthread_should_stop()) {
		tasklet_schedule(&tlet);
		msleep(1000);
	}
	return 0;
}

static int __init kt_init(void)
{
	
	ktask = kthread_create(kt_thread, NULL, "kthread");
	if (IS_ERR(ktask))
		return PTR_ERR(ktask);
	else
		wake_up_process(ktask);
	return 0;
}

static void __exit kt_exit(void)
{
	kthread_stop(ktask);
	tasklet_kill(&tlet);
}

module_init(kt_init);
module_exit(kt_exit);

MODULE_AUTHOR("ilsensine");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("sample kernel process");
Note:
- Per disabilitare i tasklet sulla cpu corrente, occorre disabilitare i softirq. Questo avviene tramite le funzioni local_bh_disable. Per sincronizzare da contesto di processo con un tasklet, il lock da usare è spin_lock_bh in contesto di processo e spin_lock in contesto di softirq (dentro il tasklet). La guida sul locking linkata in precedenza ne parla.
- Non esiste un equivalente di flush_workqueue per i tasklet. Utilizzare tasklet_kill.
- L'oggetto tasklet_struct deve essere ancora vivo alla fine del tasklet, quindi non possiamo usare il trucco con il kfree utilizzato con le workqueue.
- Possono essere allocati nuovi softirq, al pari di nuove workqueue. Il loro uso è fortemente scoraggiato. I tasklet non sono però workqueue, anche se ingannevolmente simili; vanno trattati con la dovuta cautela.
- Il contesto softirq può essere interrotto solo da un hardirq.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 17:02.
ilsensine è offline  
Old 12-01-2005, 15:34   #17
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Esempio: driver di acquisizione video

Applichiamo quanto visto fin'ora a un caso semi-pratico. L'esempio seguente è un modulo che emula un dispositivo di acquisizione video. La sequenza video generata è prodotta da un codice preso in prestito dal file output_example.c di ffmpeg.

Un driver di acquisizione video è un char device; come fatto con i miscdevice, non gestiremo direttamente il char device, ma ci affideremo al framework v4l già presente nel kernel. Per motivi di semplicità useremo le API v4l v.1, ancora presenti nel kernel.

La novità principale introdotta qui è la ioctl: questo metodo consente di eseguire tutte quelle operazioni che non possono rientrare nel paradigma read/write. Una ioctl ha questo prototipo:
Codice:
int ioctl(struct inode *inode, struct file *file,
	unsigned int cmd, unsigned long arg);
L'ultimo parametro, arg, dipende dal tipo di operazione richiesta in cmd. E' innumerevole fonte di guai, in quanto non è dato sapere a priori cosa c'è dentro: normalmente è un puntatore userspace a una struttura. Inoltre, la differenza nella dimensione del tipo "long" tra i sistemi a 32 e 64 bit completa la frittata. Per accedere al puntatorfe passato in arg, occorrerebbe usare le classiche funzioni di accesso allo spazio utente (copy_*_user & co); fortunatamente, il framework v4l offre un helper (video_usercopy) che fa automaticamente queste noiose operazioni.

Le ioctl v4l sono elencate in include/videodev.h; ne implementeremo solo alcune. Oltre a quelle di inizializzazione e di query, le ioctl principali sono la VIDIOCMCAPTURE e la VIDIOCSYNC. Un driver v4l normalmente mette a disposizione dell'applicazione almeno due buffer di scambio dati, utilizzabili tramite mmap; con la VIDIOCMCAPTURE scheduliamo l'acquisizione su un buffer specificato, con la VIDIOCSYNC attendiamo che il buffer sia pieno. Tra le due l'applicazione può fare altro, l'acquisizione è asincrona. Un programma di acquisizione video normalmente esegue queste operazioni:

- richiede i buffer A e B, e le loro dimensioni
- mmap dei buffer
- VIDIOCMCAPTURE(A)

- VIDIOCMCAPTURE(B)
- VIDIOCSYNC(A)
- visualizza A

- VIDIOCMCAPTURE(A)
- VIDIOCSYNC(B)
- visualizza B

- VIDIOCMCAPTURE(B)
- VIDIOCSYNC(A)
- visualizza A

- VIDIOCMCAPTURE(A)
...

Notate infine che questo modulo utilizza le funzioni esportate dal modulo videodev.ko: occorre caricarlo (modprobe videodev) prima di caricare questo driver. Le immagini prodotte possono essere visualizzate tramite un qualsiasi programma di acquisizione, come xawtv.
Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/videodev.h>
#include <linux/vmalloc.h>
#include <linux/wait.h>
#include <linux/workqueue.h>
#include <linux/mm.h>
#include <asm/atomic.h>

#define VID_HARDWARE_VSAMPLE 255

#define MAX_X 352
#define MAX_Y 288
#define MIN_X 16
#define MIN_Y 16
#define IMAGE_AREA PAGE_ALIGN((MAX_X*MAX_Y*3)/2)

enum istate {
	img_ready,
	img_grabbing,
	img_done
};

struct v_data;
struct v_image {
	unsigned char *map;
	int x;
	int y;
	enum istate state;
	wait_queue_head_t queue;
	struct v_data *vdata;
	spinlock_t lock;
};

struct v_work_data {
	struct work_struct work;
	struct v_image *image;
};

static struct v_data {
	struct video_device *vdev;
	struct video_picture vpict;
	atomic_t frame_index;
	unsigned char *vma;
	struct v_image image[2];
} vdata;



static void v_work_image(void *data)
{
	struct v_work_data *wdata = data;
	struct v_image *image = wdata->image;
	int i, px, py;
	unsigned char *y, *u, *v;

	kfree(wdata);
	WARN_ON(image->state!=img_grabbing);
	y = image->map;
	u = y+(image->x*image->y);
	v = u+(image->x*image->y)/4;
	i = atomic_read(&image->vdata->frame_index);
	atomic_inc(&image->vdata->frame_index);
	for (py=0; py<image->y; py++) {
		for (px=0; px<image->x; px++) {
			y[py*image->x+px] = px + py + i * 3;
		}
	}
	for (py=0; py<image->y/2; py++) {
		for (px=0; px<image->x/2; px++) {
			u[py*image->x/2+px] = 128 + py + i * 2;
			v[py*image->x/2+px] = 64 + px + i * 5;
		}
	}
	smp_wmb();
	image->state = img_done;
	wake_up_interruptible_all(&image->queue);
}

static int v_create_work(struct v_data *vdata, struct video_mmap *map)
{
	struct v_work_data *work_data;
	struct v_image *image;
	if (map->frame<0 || map->frame>1 ||
			map->format!=VIDEO_PALETTE_YUV420P ||
			map->width<MIN_X || map->width>MAX_X ||
			map->height<MIN_Y || map->height>MAX_Y ||
			map->width&1 || map->height&1) {
		return -EINVAL;
	}
	image = vdata->image+map->frame;
	spin_lock(&image->lock);
	if (image->state!=img_ready) {
		spin_unlock(&image->lock);
		return -EBUSY;
	}
	image->state = img_grabbing;
	spin_unlock(&image->lock);
	image->x = map->width;
	image->y = map->height;

	work_data = kmalloc(sizeof(*work_data), GFP_KERNEL);
	if (!work_data)
		return -ENOMEM;
	INIT_WORK(&work_data->work, v_work_image, work_data);
	work_data->image = image;

	schedule_delayed_work(&work_data->work, msecs_to_jiffies(25));
	return 0;
}

static int v_ioctl(struct inode *inode, struct file *file,
	unsigned int cmd, void *arg)
{
	struct video_device *vdev = video_devdata(file);
	struct v_data *vdata = video_get_drvdata(vdev);
	int ret = 0;
	switch (cmd) {
	case VIDIOCGCAP: {
		struct video_capability *vcap = arg;
		memset(vcap, 0, sizeof(*vcap));
		snprintf(vcap->name, sizeof(vcap->name), "v4l_dummy");
		vcap->type = VID_TYPE_CAPTURE | VID_TYPE_SUBCAPTURE;
		vcap->channels = 1;
		vcap->audios = 0;
		vcap->maxwidth = MAX_X;
		vcap->maxheight = MAX_Y;
		vcap->minwidth = MIN_X;
		vcap->minheight = MIN_Y;
		break;
	}
	case VIDIOCGCHAN: {
		struct video_channel *vc = arg;
		if (vc->channel!=0)
			return -EINVAL;
		vc->norm = VIDEO_MODE_PAL;
		vc->type = VIDEO_TYPE_CAMERA;
		vc->flags = 0;
		vc->tuners = 0;
		snprintf(vc->name, sizeof(vc->name),
			"channel %d", vc->channel);
		break;
	}
	case VIDIOCSCHAN: {
		struct video_channel *vc = arg;
		if (vc->channel!=0)
			return -EINVAL;
		if (vc->norm != VIDEO_MODE_PAL &&
				vc->norm != VIDEO_MODE_NTSC &&
				vc->norm != VIDEO_MODE_SECAM &&
				vc->norm != VIDEO_MODE_AUTO)
			return -EINVAL;
		break;
	}
	case VIDIOCGPICT: {
		struct video_picture *pict = arg;
		memcpy(pict, &vdata->vpict, sizeof(*pict));
		break;
	}
	case VIDIOCSPICT: {
		struct video_picture *pict = arg;
		if (pict->palette!=VIDEO_PALETTE_YUV420P ||
				pict->depth!=12)
			return -EINVAL;
		memcpy(&vdata->vpict, pict, sizeof(*pict));
		break;
	}
	case VIDIOCGMBUF: {
		struct video_mbuf *vm = arg;
		memset(vm, 0, sizeof(*vm));
		vm->frames = 2;
		vm->size = vm->frames*IMAGE_AREA;
		vm->offsets[0] = 0;
		vm->offsets[1] = IMAGE_AREA;
		break;
	}
	case VIDIOCGFBUF: {
		struct video_buffer *vb = arg;
		memset(vb, 0, sizeof(*vb));
		break;
	}
	case VIDIOCMCAPTURE: {
		struct video_mmap *map = arg;
		ret = v_create_work(vdata, map);
		break;
	}
	case VIDIOCSYNC: {
		int ipos = *(int *) arg;
		struct v_image *image;
		if (ipos<0 || ipos>1)
			return -EINVAL;
		image = &vdata->image[ipos];
		if (image->state==img_ready)
			return -EINVAL;
		if (wait_event_interruptible(image->queue, image->state==img_done))
			ret = -EINTR;
		else image->state = img_ready;
		break;
	}
	default:
		ret = -ENOIOCTLCMD;
	}
  return ret;
}

static int v_ioctl_if(struct inode *inode, struct file *file,
	unsigned int cmd, unsigned long arg)
{
	return video_usercopy(inode, file, cmd, arg, v_ioctl);
}

static int v_release(struct inode *inode, struct file *file)
{
	struct video_device *vdev = video_devdata(file);
	struct v_data *vdata = video_get_drvdata(vdev);
	flush_scheduled_work();
	vdata->image[0].state = img_ready;
	vdata->image[1].state = img_ready;
	return video_exclusive_release(inode, file);
}

static struct page *v_vm_nopage(struct vm_area_struct *vma,
	unsigned long address, int *type)
{
	unsigned long ofs = (vma->vm_pgoff<<PAGE_SHIFT)+(address-vma->vm_start);
	struct page *ret;
	struct video_device *vdev = vma->vm_private_data;
	struct v_data *vdata =  video_get_drvdata(vdev);

	ofs &= PAGE_MASK;
	if (ofs>=(IMAGE_AREA*2))
		return NOPAGE_SIGBUS;
	ret = vmalloc_to_page(vdata->vma+ofs);
	if (ret)
		get_page(ret);
	if (type)
		*type = VM_FAULT_MINOR;
	return ret;
}

static struct vm_operations_struct v_vm_operations = {
	.nopage = v_vm_nopage,
};

static int v_mmap(struct file *file, struct vm_area_struct *vma)
{
	struct video_device *vdev = video_devdata(file);
	unsigned long size  = vma->vm_end - vma->vm_start;
	unsigned long pg_base = vma->vm_pgoff<<PAGE_SHIFT;

	if ((pg_base+size)>(IMAGE_AREA*2))
		return -EINVAL;
	if (!(vma->vm_flags&VM_SHARED))
		return -EINVAL;
	vma->vm_private_data = vdev;
	vma->vm_flags |= VM_RESERVED;
	vma->vm_ops = &v_vm_operations;
	return 0;
}

static void v_dummy_release(struct video_device *vdev)
{
}

static struct file_operations v_fops = {
	.owner		= THIS_MODULE,
	.open		= video_exclusive_open,
	.release	= v_release,
	.ioctl		= v_ioctl_if,
	.mmap		= v_mmap,
	.llseek		= no_llseek,
};

static struct video_device vdev = {
	.owner =	THIS_MODULE,
	.name =		"v4l_sample",
	.type =		VID_TYPE_CAPTURE,
	.hardware =	VID_HARDWARE_VSAMPLE,
	.fops =		&v_fops,
	.release =	v_dummy_release,
};

static int __init v_init(void)
{
	int ret;
	video_set_drvdata(&vdev, &vdata);
	vdata.vdev = &vdev;
	vdata.vpict.palette = VIDEO_PALETTE_YUV420P;
	vdata.vpict.depth = 12;
	atomic_set(&vdata.frame_index, 0);
	vdata.vma = vmalloc(IMAGE_AREA*2);
	if (!vdata.vma)
		return -ENOMEM;
	vdata.image[0].map = vdata.vma;
	vdata.image[1].map = vdata.image[0].map+IMAGE_AREA;
	vdata.image[0].vdata = &vdata;
	vdata.image[1].vdata = &vdata;
	vdata.image[0].state = img_ready;
	vdata.image[1].state = img_ready;
	init_waitqueue_head(&vdata.image[0].queue);
	init_waitqueue_head(&vdata.image[1].queue);
	spin_lock_init(&vdata.image[0].lock);
	spin_lock_init(&vdata.image[1].lock);
	ret = video_register_device(&vdev, VFL_TYPE_GRABBER, -1);
	if (ret) {
		vfree(vdata.vma);
		return ret;
	}
	printk(KERN_INFO "%s: Using device /dev/video%d\n", __func__, vdev.minor);
	return 0;
}

static void __exit v_exit(void)
{
	video_unregister_device(&vdev);
	vfree(vdata.vma);
}

MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("sample v4l driver");
MODULE_LICENSE("GPL");

module_init(v_init);
module_exit(v_exit);
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 17:05.
ilsensine è offline  
Old 14-01-2005, 15:53   #18
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Organizzazione degli oggetti nel kernel: kobject e sysfs. Il nuovo Device Model.

Una delle novità maggiori introdotti nel kernel 2.6 è la generalizzazione di concetti quali "driver", "dispositivo", "classe di dispositivi". E' stata introdotta una entità, kobject, che rappresenta in maniera astratta ogni oggetto del kernel. Potete pensare al kobject come alla classe base astratta in un toolkit c++, ad esempio. I vari kobject sono raggruppati gerarchicamente in insiemi (kset), raggruppabili a loro volta in altre gerarchie. La struttura è alquanto sofisticata.
L'organizzazione gerarchica interna del kernel viene esportata in userspace tramite il "sysfs". Questo file system virtuale mostra la gerarchia degli oggetti, consentendone di modificare o esaminare eventuali attributi (non usate più /proc nei vostri driver!).
Una descrizione generale e più esauriente del sysfs è pubblicata da lwn, che riporto qui per comodità:
http://lwn.net/Articles/driver-porting/

Qui parleremo solo degli oggetti device e driver, che sono ciò che maggiormente interessano al programmatore.

Un "device" è un oggetto connesso fisicamente (o logicamente) a un qualche "bus". E' descritto a livello astratto dalla "struct device" (v. linux/device.h), che è la base per la descrizione di altre classi di device (ad es. pci_dev, usb_device...). Descrivere un "device" vuol dire descrivere anche l'oggetto bus (struct bus_type) a cui è collegato o può essere collegato.
Facciamo un esempio. Un tipo di bus presente ovunque è il "platform_bus_type". Questo bus è pensato per contenere gli oggetti intrinsecamente presenti nella piattaforma (ad es. controller dma, controller floppy ecc.) che non possono essere pensati come connessi ad altri bus. Altri esempi di bus sono il bus pci, il bus usb, il bus i2c...
Descrivere un dispositivo connesso al platform_bus_type è semplice; questo modulo ad esempio dichiara e registra nel sistema due dispositivi virtuali:

Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>

static void dev_release(struct device *dev)
{
	struct platform_device *pdev = to_platform_device(dev);
	printk(KERN_INFO "%s: releasing %s:%d\n",
		__func__, pdev->name, pdev->id);
}

static struct platform_device pdev0 = {
	.name = "dummy_dev",
	.id = 0,
	.dev = {
		.release = dev_release
	}
};

static struct platform_device pdev1 = {
	.name = "dummy_dev",
	.id = 1,
	.dev = {
		.release = dev_release
	}
};

static int __init dev_init(void)
{
	platform_device_register(&pdev0);
	platform_device_register(&pdev1);
	return 0;
}

static void __exit dev_exit(void)
{
	platform_device_unregister(&pdev1);
	platform_device_unregister(&pdev0);
}

module_init(dev_init);
module_exit(dev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("dummy (platform) device");
Un platform_device "è anche" una "struct device" (campo dev), più alcune altre cose specifiche per il tipo di bus cui è connesso. Tra queste cose è necessario specificare delle informazioni per identificare univocamente il device (ad es. il pci id per i device pci) e, opzionalmente, le risorse assegnate (irq, iomem...). I platform device sono molto semplici, non hanno un id univoco come il pci_id, ma sono distinti dal campo "name". Notate che la registrazione di una struct device non implica affatto che il dispositivo sia gestito dal sistema (abbia un driver), ma semplicemente che "esiste". Ad esempio ogni qual volta inserite un winmodem usb, viene dinamicamente generato e registrato un device usb che lo rappresenta, anche se il dispositivo in realtà è un paperweight.
La (de)registrazione può essere fatta dinamicamente: il driver model prevede intrinsecamente l'hotplug.
Potete compilare e inserire il driver, notando come vengano creati i device dummy_dev0 e dummy_dev1 in /sys/bus/platform/device.

Accanto ai device esistono i device_driver. Sono anch'essi specifici per tipo di bus (dialogare con un dispositivo usb è per forza diverso dal dialogare con un dispositivo pci). Questo modulo è lo skull di un driver per il dispositivo creato nel modulo precedente:

Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>

static int __devinit drv_probe(struct device * dev)
{
	struct platform_device *pdev = to_platform_device(dev);
	printk(KERN_INFO "%s: probing device %s:%d\n", __func__, pdev->name, pdev->id);
	return 0;
}

static int __devexit drv_remove(struct device * dev)
{
	struct platform_device *pdev = to_platform_device(dev);
	printk(KERN_INFO "%s: removing device %s:%d\n", __func__, pdev->name, pdev->id);
	return 0;
}

static void drv_shutdown(struct device * dev)
{
	struct platform_device *pdev = to_platform_device(dev);
	printk(KERN_INFO "%s: shutting down device %s:%d\n", __func__,
		pdev->name, pdev->id);
}

static int drv_suspend(struct device * dev, u32 state, u32 level)
{
	struct platform_device *pdev = to_platform_device(dev);
	printk(KERN_INFO "%s: suspending device %s:%d state=%d lev=%d\n",
		__func__, pdev->name, pdev->id, state, level);
	return 0;
}

static int drv_resume(struct device * dev, u32 level)
{
	struct platform_device *pdev = to_platform_device(dev);
	printk(KERN_INFO "%s: resuming device %s:%d lev=%d\n", __func__,
		pdev->name, pdev->id, level);
	return 0;
}

static struct device_driver drv = {
	.name 		= "dummy_dev",
	.bus	 	= &platform_bus_type,
	.probe 		= drv_probe,
	.remove		= __devexit_p(drv_remove),
	.shutdown	= drv_shutdown,
	.suspend	= drv_suspend,
	.resume		= drv_resume
};

static int __init drv_init(void)
{
	return driver_register(&drv);
}

static void __exit drv_exit(void)
{
	driver_unregister(&drv);
}

module_init(drv_init);
module_exit(drv_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("dummy (platform) device driver");
All'atto dell'inserimento del driver, il kernel controlla tutti i device su quel bus non gestiti da un driver, verifica che l'identificativo del device coincida con quello specificato nel driver (per i platform_device l'identificativo univoco è semplicemente il "nome"), e in caso di match chiama la funzione "probe" del driver. Il driver può quindi verificare la gestibilità del device, allocare le risorse necessarie ecc, oppure restituire un codice d'errore nel caso che il dispositivo non sia gestibile per qualche motivo.
*importante*: come un "device" può esistere senza un "driver", è vero anche il viceversa: il modulo "driver" può essere presente in memoria anche se il "device" non esiste, ed essere invocato solo quando il "device" compare. Questo comportamento è fondamentale per supportare l'hotplug. Potete ad esempio provare a caricare il secondo modulo, e verificare che compare in /sys/bus/platform/drivers.
Potete giocare con i due moduli illustrati, controllando i messaggi stampati alla rimozione/caricamento degli stessi.
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 16-01-2005 alle 19:55.
ilsensine è offline  
Old 17-01-2005, 10:51   #19
ilsensine
Senior Member
 
L'Avatar di ilsensine
 
Iscritto dal: Apr 2000
Città: Roma
Messaggi: 15625
Device Model: revisione del driver di acquisizione video

L'esempio seguente, molto importante, mostra l'aspetto finale di un driver classico di linux.
Non ho introdotto altri concetti oltre un esempio di creazione di un attributo modificabile runtime: per ogni device v4l, ho definito l'attributo "frame_inc"; un attributo numerico, che ci consentirà di variare l'incremento dei frame del filmato. Visto che il device v4l è in effetti un class_device, ho dichiarato un attributo per questo tipo di device. L'attributo sarà accessibile e modificabile, tramite comuni echo e cat, dal file /sys/class/video4linux/video<n>/frame_inc; potete inserire qualsiasi valore compreso tra -16 e 16. Ognuno dei due device creati ne possiederà uno, indipendente dall'altro. Come esercizio utile, potete cercare di definire ed implementare l'attributo "frame_rate".

Driver testato su un kernel 2.6.8.1:

Codice:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/videodev.h>
#include <linux/vmalloc.h>
#include <linux/wait.h>
#include <linux/workqueue.h>
#include <linux/mm.h>
#include <asm/atomic.h>

/* Identificativo fittizzio per i nostri device video */
#define VID_HARDWARE_VSAMPLE 255

/* Limiti alla dimensione dell'immagine */
#define MAX_X 352
#define MAX_Y 288
#define MIN_X 16
#define MIN_Y 16

/* Dimensione massima di una immagine, in yuv420, arrotondata per eccesso alla pagina */
#define IMAGE_AREA PAGE_ALIGN((MAX_X*MAX_Y*3)/2)

/* Stato di acquisizione dell'immagine */
enum istate {
	img_ready,	/* acquisibile */
	img_grabbing,	/* in acquisizione */
	img_done	/* pronta per essere letta in user space */
};

struct v_data;

/* Teniamo un buffer classico di due immagini, per il double buffering. */
/* La struct v_image descrive un singolo buffer. */
struct v_image {
	unsigned char *map;
	int x;
	int y;
	enum istate state;
	wait_queue_head_t queue;
	struct v_data *vdata;
	spinlock_t lock; /* Protegge la delicata transizione ready->grabbing */
};

struct v_work_data {
	struct work_struct work;
	struct v_image *image;
};

/* Struttura descrittiva per ogni istanza del nostro device v4l */
struct v_data {
	struct video_device *vdev;
	struct video_picture vpict;
	atomic_t frame_index;
	int frame_inc;
	unsigned char *vma;
	struct v_image image[2];
};

/* Crea una nuova immagine.
   In un driver reale questa funzione
   probabilmente sara' una irq service routine. */
static void v_work_image(void *data)
{
	struct v_work_data *wdata = data;
	struct v_image *image = wdata->image;
	int i, px, py;
	unsigned char *y, *u, *v;

	kfree(wdata);
	WARN_ON(image->state!=img_grabbing);
	y = image->map;
	u = y+(image->x*image->y);
	v = u+(image->x*image->y)/4;
	i = atomic_read(&image->vdata->frame_index);
	atomic_add(image->vdata->frame_inc, &image->vdata->frame_index);

	for (py=0; py<image->y; py++) {
		for (px=0; px<image->x; px++) {
			y[py*image->x+px] = px + py + i * 3;
		}
	}
	for (py=0; py<image->y/2; py++) {
		for (px=0; px<image->x/2; px++) {
			u[py*image->x/2+px] = 128 + py + i * 2;
			v[py*image->x/2+px] = 64 + px + i * 5;
		}
	}

	smp_wmb();
	image->state = img_done;
	wake_up_interruptible_all(&image->queue);
}

/* Schedula l'acquisizione di una immagine.
   Un driver reale preparerà qui una transazione dma. */
static int v_create_work(struct v_data *vdata, struct video_mmap *map)
{
	struct v_work_data *work_data;
	struct v_image *image;
	if (map->frame<0 || map->frame>1 ||
			map->format!=VIDEO_PALETTE_YUV420P ||
			map->width<MIN_X || map->width>MAX_X ||
			map->height<MIN_Y || map->height>MAX_Y ||
			map->width&1 || map->height&1) {
		return -EINVAL;
	}
	image = vdata->image+map->frame;
	work_data = kmalloc(sizeof(*work_data), GFP_KERNEL);
	if (!work_data)
		return -ENOMEM;
	INIT_WORK(&work_data->work, v_work_image, work_data);
	work_data->image = image;
/* Lo spinlock non e' realmente necessario, in quanto la ioctl viene eseguita sotto BKL.
   A breve pero' le ioctl verranno liberate da questo lock, quindi teniamoci pronti.
   Notate che le altre parti che modificano image->state non e' necessario che siano
   protette da questo lock, in quanto effettuate su uno stato noto che non
   puo' venire modificato altrove. */
	spin_lock(&image->lock);
	if (image->state!=img_ready) {
/* Giungiamo qui solo se lo user space sta facendo qualcosa di sbagliato.
   Non dobbiamo mai fidarci delle applicazioni user space. */
		spin_unlock(&image->lock);
		kfree(work_data);
		return -EBUSY;
	}
	image->state = img_grabbing;
	spin_unlock(&image->lock);
	image->x = map->width;
	image->y = map->height;
	schedule_delayed_work(&work_data->work, msecs_to_jiffies(25));
	return 0;
}

/* Interfaccia v4l per la ioctl. arg punta a un buffer interno del kernel,
   non occorre usare copy_*_user. */
static int v_ioctl(struct inode *inode, struct file *file,
	unsigned int cmd, void *arg)
{
	struct video_device *vdev = video_devdata(file);
	struct v_data *vdata = video_get_drvdata(vdev);
	int ret = 0;

	switch (cmd) {
	case VIDIOCGCAP: { /* get capability */
		struct video_capability *vcap = arg;
		memset(vcap, 0, sizeof(*vcap));
		snprintf(vcap->name, sizeof(vcap->name), "v4l_dummy");
		vcap->type = VID_TYPE_CAPTURE | VID_TYPE_SUBCAPTURE;
		vcap->channels = 1;
		vcap->audios = 0;
		vcap->maxwidth = MAX_X;
		vcap->maxheight = MAX_Y;
		vcap->minwidth = MIN_X;
		vcap->minheight = MIN_Y;
		break;
	}
	case VIDIOCGCHAN: { /* get channel format */
		struct video_channel *vc = arg;
		if(vc->channel!=0)
			return -EINVAL;
		vc->norm = VIDEO_MODE_PAL;
		vc->type = VIDEO_TYPE_CAMERA;
		vc->flags = 0;
		vc->tuners = 0;
		snprintf(vc->name, sizeof(vc->name),
			"channel %d", vc->channel);
		break;
	}
	case VIDIOCSCHAN: { /* set channel format */
		struct video_channel *vc = arg;
		if (vc->channel!=0)
			return -EINVAL;
		if (vc->norm != VIDEO_MODE_PAL &&
				vc->norm != VIDEO_MODE_NTSC &&
				vc->norm != VIDEO_MODE_SECAM &&
				vc->norm != VIDEO_MODE_AUTO)
			return -EINVAL;
		break;
	}
	case VIDIOCGPICT: { /* get picture type */
		struct video_picture *pict = arg;
		memcpy(pict, &vdata->vpict, sizeof(*pict));
		break;
	}
	case VIDIOCSPICT: { /* set picture type */
		struct video_picture *pict = arg;
		if (pict->palette!=VIDEO_PALETTE_YUV420P ||
				pict->depth!=12)
			return -EINVAL;
		memcpy(&vdata->vpict, pict, sizeof(*pict));
		break;
	}
	case VIDIOCGMBUF: { /* get memory buffer info */
		struct video_mbuf *vm = arg;
		memset(vm, 0, sizeof(*vm));
		vm->frames = 2;
		vm->size = vm->frames*IMAGE_AREA;
		vm->offsets[0] = 0;
		vm->offsets[1] = IMAGE_AREA;
		break;
	}
	case VIDIOCGFBUF: {
		struct video_buffer *vb = arg;
		memset(vb, 0, sizeof(*vb));
		break;
	}
	case VIDIOCMCAPTURE: { /* schedula l'acquisizione del buffer indicato */
		struct video_mmap *map = arg;
		ret = v_create_work(vdata, map);
		break;
	}
	case VIDIOCSYNC: { /* sincronizza l'acquisizione del buffer indicato */
		int ipos = *(int *) arg;
		struct v_image *image;
		if (ipos<0 || ipos>1)
			return -EINVAL;
		image = &vdata->image[ipos];
		if (image->state==img_ready)
			return -EINVAL;
		if (wait_event_interruptible(image->queue, image->state==img_done))
			ret = -EINTR;
		else
			image->state = img_ready;
		break;
	}
	default:
/* Il framework v4l richiede il ritorno di -ENOIOCTLCMD, non di -ENOTTY. */
		ret = -ENOIOCTLCMD;
	}
  return ret;
}

static int v_ioctl_if(struct inode *inode, struct file *file,
	unsigned int cmd, unsigned long arg)
{
	return video_usercopy(inode, file, cmd, arg, v_ioctl);
}

static struct page *v_vm_nopage(struct vm_area_struct *vma,
	unsigned long address, int *type)
{
	unsigned long ofs = (vma->vm_pgoff<<PAGE_SHIFT)+(address-vma->vm_start);
	struct page *ret;
	struct video_device *vdev = vma->vm_private_data;
	struct v_data *vdata = video_get_drvdata(vdev);

	ofs &= PAGE_MASK;
	if (ofs>=(IMAGE_AREA*2))
		return NOPAGE_SIGBUS;
	ret = vmalloc_to_page(vdata->vma+ofs);
	if (ret)
		get_page(ret);
	if (type)
		*type = VM_FAULT_MINOR;
	return ret;
}

static struct vm_operations_struct v_vm_operations = {
	.nopage = v_vm_nopage,
};

static int v_mmap(struct file *file, struct vm_area_struct *vma)
{
	struct video_device *vdev = video_devdata(file);
	unsigned long size  = vma->vm_end - vma->vm_start;
	unsigned long pg_base = vma->vm_pgoff<<PAGE_SHIFT;

	if ((pg_base+size)>(IMAGE_AREA*2))
		return -EINVAL;
	if (!(vma->vm_flags&VM_SHARED))
		return -EINVAL;
	vma->vm_private_data = vdev;
	vma->vm_flags |= VM_RESERVED;
	vma->vm_ops = &v_vm_operations;
	return 0;
}

static int v_open(struct inode *inode, struct file *file)
{
	struct video_device *vdev = video_devdata(file);
	struct v_data *vdata = video_get_drvdata(vdev);
	int ret = video_exclusive_open(inode, file);
	if (ret)
		return ret;

	vdata->vma = vmalloc(IMAGE_AREA*2);
	if (!vdata->vma) {
		video_exclusive_release(inode, file);
		return -ENOMEM;
	}
	vdata->image[0].map = vdata->vma;
	vdata->image[1].map = vdata->image[0].map+IMAGE_AREA;
	vdata->image[0].state = img_ready;
	vdata->image[1].state = img_ready;
	return 0;
}

static int v_release(struct inode *inode, struct file *file)
{
	struct video_device *vdev = video_devdata(file);
	struct v_data *vdata = video_get_drvdata(vdev);
/* Potrebbero esserci ancora work all'opera senza che l'utente abbia
   effettuato una VIDIOCSYNC. Occorre attenderne la terminazione prima del vfree! */
	flush_scheduled_work();
	vfree(vdata->vma);
	video_exclusive_release(inode, file);
	return 0;
}

static void v_dev_release(struct video_device *vdev)
{
	struct v_data *vdata = video_get_drvdata(vdev);
	video_device_release(vdev);
	kfree(vdata);
}

static struct file_operations v_fops = {
	.owner		= THIS_MODULE,
	.open		= v_open,
	.release	= v_release,
	.ioctl		= v_ioctl_if,
	.mmap		= v_mmap,
	.llseek		= no_llseek,
};

static struct video_device vdev_skull = {
	.owner =	THIS_MODULE,
	.name =		"v4l_sample",
	.type =		VID_TYPE_CAPTURE,
	.hardware =	VID_HARDWARE_VSAMPLE,
	.fops =		&v_fops,
	.release =	v_dev_release,
};

/* Alloca e inizializza una struttura v_data */
static __devinit struct v_data *v_dev_alloc(struct device *dev)
{
	struct v_data *vdata = kmalloc(sizeof(*vdata), GFP_KERNEL);
	struct platform_device *pdev = to_platform_device(dev);
	if (!vdata)
		return NULL;
	vdata->vdev = video_device_alloc();
	if (!vdata->vdev) {
		kfree(vdata);
		return NULL;
	}
	vdata->frame_inc = 1;
	*vdata->vdev = vdev_skull;
	video_set_drvdata(vdata->vdev, vdata);
	snprintf(vdata->vdev->name, sizeof(vdata->vdev->name), "%s:%d",
		pdev->name, pdev->id);
	vdata->vpict.palette = VIDEO_PALETTE_YUV420P;
	vdata->vpict.depth = 12;
	atomic_set(&vdata->frame_index, 0);
	vdata->image[0].vdata = vdata;
	vdata->image[1].vdata = vdata;
	init_waitqueue_head(&vdata->image[0].queue);
	init_waitqueue_head(&vdata->image[1].queue);
	spin_lock_init(&vdata->image[0].lock);
	spin_lock_init(&vdata->image[1].lock);
	vdata->vdev->dev = dev;
	return vdata;
}

/* Mostra il contenuto di frame_inc */
static ssize_t show_frame_inc(struct class_device *cd, char *buf)
{
	struct video_device *vdev = to_video_device(cd);
	struct v_data *vdata = video_get_drvdata(vdev);
/* buf e' sempre di una pagina */
	return sprintf(buf, "%d\n", vdata->frame_inc);
}

/* Memorizza un nuovo valore per frame_inc */
static ssize_t store_frame_inc(struct class_device *cd, const char * buf, size_t count)
{
	struct video_device *vdev = to_video_device(cd);
	struct v_data *vdata = video_get_drvdata(vdev);
	int frame_inc;
/* buf proviene direttamente da user space, quindi è inaffidabile per definizione.
   Aggiungiamo un paio di controlli, almeno per essere certi che la strtol legga
   entro il buffer fornito. */
	if (count<=0)
		return -EINVAL;
	if (buf[count-1]!='\0' && buf[count-1]!='\n')
		return -EINVAL;
	frame_inc = simple_strtol(buf, NULL, 0);
	if (frame_inc<-16 || frame_inc>16)
		return -EINVAL;
	vdata->frame_inc = frame_inc;
	return count;
}

/* Questa macro crea l'attributo con il nome class_device_attr_frame_inc.
   Possiamo modificare runtime /sys/class/video4linux/videoX/frame_inc. */
static CLASS_DEVICE_ATTR(frame_inc, 0644, show_frame_inc, store_frame_inc);

/* Il kernel ha trovato un nuovo dispositivo e ci chiede di gestirlo */
static int __devinit v_probe(struct device *dev)
{
	struct v_data *vdata = v_dev_alloc(dev);
	int ret;
	if (!vdata)
		return -ENOMEM;
	dev_set_drvdata(dev, vdata);
	ret = video_register_device(vdata->vdev, VFL_TYPE_GRABBER, -1);
	if (ret) {
		dev_set_drvdata(dev, NULL);
		video_device_release(vdata->vdev);
		kfree(vdata);
		return ret;
	}
	video_device_create_file(vdata->vdev, &class_device_attr_frame_inc);
	printk(KERN_INFO "%s: Using device /dev/video%d\n", __func__, vdata->vdev->minor);
	return 0;
}

/* Questa funzione e' semplice in quanto sappiamo che il device non ci scomparira'
   sotto i piedi runtime. Se non fossimo certi di questo, dovremmo prendere
   altre accortezze. */
static int __devexit v_remove(struct device *dev)
{
	struct v_data *vdata = dev_get_drvdata(dev);
	dev_set_drvdata(dev, NULL);
	video_unregister_device(vdata->vdev);
	return 0;
}

static struct device_driver drv = {
	.name		= "dummy_dev",
	.bus		= &platform_bus_type,
	.probe		= v_probe,
/* I puntatori __devexit devono essere inseriti tramite __devexit_p */
	.remove		= __devexit_p(v_remove),
};

static void dummy_dev_release(struct device *dev)
{
}

/* I nostri device fittizzi */
static struct platform_device pdev0 = {
	.name =	"dummy_dev",
	.id =	0,
	.dev =	{
		.release = dummy_dev_release
	}
};

static struct platform_device pdev1 = {
	.name =	"dummy_dev",
	.id =	1,
	.dev =	{
		.release = dummy_dev_release
	}
};

/* Regitra i device fittizzi. In un driver reale (ad es. un driver usb), saranno
   altre parti del kernel ad occuparsene. */
static int __init register_dummy_devices(void)
{
	int ret = platform_device_register(&pdev0);
	if (ret)
		return ret;
	ret = platform_device_register(&pdev1);
	if(ret)
		platform_device_unregister(&pdev0);
	return ret;
}

/* Non puo' essere __exit in quanto e' chiamata anche da v_init,
   che e' dichiarata __init. */
static int unregister_dummy_devices(void)
{
	platform_device_unregister(&pdev0);
	platform_device_unregister(&pdev1);
	return 0;
}

static int __init v_init(void)
{
	int ret = register_dummy_devices();
	if (ret)
		return ret;
	ret = driver_register(&drv);
	if (ret)
		unregister_dummy_devices();
	return ret;
}

static void __exit v_exit(void)
{
	driver_unregister(&drv);
	unregister_dummy_devices();
}

MODULE_AUTHOR("ilsensine");
MODULE_DESCRIPTION("sample v4l driver");
MODULE_LICENSE("GPL");

module_init(v_init);
module_exit(v_exit);
__________________
0: or %edi, %ecx; adc %eax, (%edx); popf; je 0b-22; pop %ebx; fadds 0x56(%ecx); lds 0x56(%ebx), %esp; mov %al, %al
andeqs pc, r1, #147456; blpl 0xff8dd280; ldrgtb r4, [r6, #-472]; addgt r5, r8, r3, ror #12

Ultima modifica di ilsensine : 23-02-2006 alle 17:10.
ilsensine è offline  
 Discussione Chiusa


Apple MacBook Air M3: chi deve davvero comprarlo? La recensione Apple MacBook Air M3: chi deve davvero comprarlo...
ASUS ROG Swift OLED PG49WCD: quando QD-OLED e ultrawide si fondono ASUS ROG Swift OLED PG49WCD: quando QD-OLED e ul...
Dreame L10s Pro Ultra Heat: la pulizia di casa tutta sostanza Dreame L10s Pro Ultra Heat: la pulizia di casa t...
HONOR Magic6 Pro: come funziona Magic Portal, il modo ''intelligente'' di condividere HONOR Magic6 Pro: come funziona Magic Portal, il...
L'innovazione richiede fiducia: Workday si propone come guida nell'era dell'IA L'innovazione richiede fiducia: Workday si propo...
NIO inizia la produzione del sistema a 9...
Microsoft Edge consentirà di limi...
I driver NVIDIA crashano al torneo milio...
Rinviato l'ultimo lancio del razzo spazi...
CMF Buds by Nothing: gli auricolari econ...
Accordo di intesa firmato, saranno di SK...
iPhone, il supporto alla messaggistica R...
Una GeForce RTX 3080 a meno di 800 euro ...
Ecco le migliori offerte sui processori ...
Polestar presenta Polestar Charge, il se...
PyPI bersagli di un attacco malware: reg...
Putin ha chiesto al governo russo di pen...
Un display AMOLED curvo su un dissipator...
Speciale sedie da ufficio e gaming in of...
Aggiornamenti per Sony Alpha 1, Alpha 9 ...
Chromium
GPU-Z
OCCT
LibreOffice Portable
Opera One Portable
Opera One 106
CCleaner Portable
CCleaner Standard
Cpu-Z
Driver NVIDIA GeForce 546.65 WHQL
SmartFTP
Trillian
Google Chrome Portable
Google Chrome 120
VirtualBox
Tutti gli articoli Tutte le news Tutti i download

Strumenti

Regole
Non Puoi aprire nuove discussioni
Non Puoi rispondere ai messaggi
Non Puoi allegare file
Non Puoi modificare i tuoi messaggi

Il codice vB è On
Le Faccine sono On
Il codice [IMG] è On
Il codice HTML è Off
Vai al Forum


Tutti gli orari sono GMT +1. Ora sono le: 13:54.


Powered by vBulletin® Version 3.6.4
Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
Served by www1v