Il sottosistema di input

di Daniele Bellucci

Riprodotto con il permesso di Linux Magazine, Edizioni Master. Questo articolo spiega il sottosistema di input del kernel 2.6

Il sottosistema di input è senz'altro un interessante esempio di come gestire in maniera efficiente e modulare i dispositivi di input rendendo possibile la creazione di plugin sia in spazio kernel sia in spazio utente. Si pensi ad esempio ad un device driver per un dispositivo di input quale il mouse. Sia che si tratti di un mouse USB o di un mouse PS2, il device driver dovrà raccogliere l'evento segnalato dal dispositivo per poi analizzarlo e aggiornare le strutture dati interne che tengono traccia delle coordinate del puntatore. È chiaro che in entrambi i casi avremmo delle sezioni di codice in comune che in quanto tali risulteranno difficili da manutenere e inoltre sarebbe praticamente scomodo creare dei plugin per la gestione degli eventi che svolgano compiti particolari, quali ad esempio l'invio dell'evento “spostamento puntatore” attraverso un socket UDP. Il sottosistema di input raggiunge un ottimo livello di modularizzazione separando le operazioni di notifica e di gestione su due attori: input_dev e input_handler.

Ad un input_dev si può associare un certo numero di input_handler a cui notificare l'evento, i quali provvederanno ad aggiornare delle strutture dati interne o a richiamare procedure di gestione dell'evento. La modularità del sottosistema viene raggiunta consentendo di separare gli input_dev dagli input_handler in maniera tale che questi possano essere definiti su due moduli o, come vedremo, su due applicativi in spazio utente.

Per tener traccia dei due attori, nel kernel 2.6 è stata introdotta una sottodirectory in /proc (/proc/bus/input) all'interno della quale due nuove entry consentono di ottenere informazioni sugli input device driver registrati nel sottosistema di input. L'entry più interessante è /proc/bus/input/devices mediante la quale è possibile vedere sia gli input_handler di ogni singolo input_dev sia la tipologia degli eventi che vengono notificati.

Eventi

Ogni evento che fluisce all'interno del sottosistema di input è implementato su una struttura dati definita all'interno del file include/linux/input.h:

	struct input_event {
	        struct timeval time;
			  __u16 type;
			  __u16 code;
			  __s32 value;
	}

Un evento viene individuato in base al suo type, code, value ed eventualmente anche in base all'istante in cui è stato generato (time). Vediamo di chiarire meglio cosa rappresentano questi fantomatici valori. Prendiamo come esempio un normale mouse (indipendentemente dal fatto che sia USB o PS2) a tre pulsanti. Quando il mouse viene spostato, questo invierà al proprio device driver un pacchetto dati contenente l'evento verificatosi. Questo evento viene tradotto dal device driver su una struct input_event opportunamente riempita, per poi essere notificata a tutti gli input_handler interessati.

Nel sottosistema di input l'input_event segnalato dal device driver del mouse conterrà EV_REL o EV_KEY nel campo type per denotare rispettivamente “uno spostamento” oppure pressione/rilascio di un pulsante. Nel caso in cui si sia verificato uno “spostamento”, il campo code assume il valore di REL_X o di REL_Y a seconda dell'asse in cui si è verificato lo spostamento, mentre se l'evento è di tipo “pressione/rilascio” di un pulsante l'event code può assumere il valore di BTN_LEFT/BTN_MIDDLE/BTN_RIGHT a seconda del pulsante. Il campo value viene utilizzato per associare un valore all'evento in maniera tale da poter individuare la direzione lungo l'asse di spostamento o di poter discriminare tra pressione e rilascio di un pulsante.

Gli eventi che abbiamo visto sono definiti all'interno di include/linux/input.h dove sono raggruppati sia per event type sia per event code. Nel kernel 2.6 è stato introdotto all'interno del sottosistema di input un modulo evbug che consente di osservare gli eventi che viaggiano attraverso il sottosistema di input. evbug può essere compilato selezionando la voce “Event debugging” dal menu “Input device support”. Consiglio caldamente di compilarlo come modulo per poter essere attivato su necessità in quanto ogni volta che sfiorate la tastiera o il mouse, il kernel genera un messaggio attraverso la funzione printk.

Input_Dev

Ora che abbiamo visto come si rappresenta un evento iniziamo a vedere un po' di codice tenendo sotto mano il sorgente del modulo skifu (Simple Kernel Input From Userspace) allegato all'articolo. Il modulo è veramente semplice e mostra come sia possibile simulare un piccolo tastierino composto dai pulsanti l, s, enter da utilizzare per stampare il contenuto delle directory corrente.

Non potendo allegare un dispositivo reale ho preferito simulare la pressione/rilascio dei pulsanti dallo spazio utente, creando un entry all'interno di /proc: /proc/skifu-input. L'idea è che scrivendo un qualunque valore in “/proc/skifu-input” viene simulato il comportamento di un dispositivo reale che notifica gli eventi di pressione/rilascio dei pulsanti al sottosistema di input. Per compilare e caricare il modulo:

[belch@charlie:skifu/] make
...
[belch@charlie:skifu/] insmod ./skifu.ko

Il compito di skifu è di registrare un input_dev che simuli la pressione e il rilascio dei pulsanti che entrano in gioco quando vogliamo stampare dalla shell il contenuto della directory corrente. Un input device driver deve dichiarare e registrare una struttura dati di tipo struct input_dev (include/linux/input.h). I campi più importanti della struct sono quelli che individuano gli eventi che verranno notificati dal nostro device driver in maniera tale da consentire al sottosistema di input di individuare gli input_handler interessati.

Il campo evbit individua una bitmap associata agli event type. Le rimanenti bitmap (keybit, relbit, absbit, eccetera) fanno riferimento agli event code e quindi andranno inizializzate sulla base degli event type codificati in evbit. Nel nostro caso dobbiamo abilitare i bit relativi a KEY_L, KEY_S e KEY_PENTER in keybit.

static int __init skifu_init(void)
{
   ...
	skifu_input_dev.evbit[0] = BIT(EV_KEY);
	set_bit(KEY_L, skifu_input_dev.keybit);
	set_bit(KEY_S, skifu_input_dev.keybit);
	set_bit(KEY_KPENTER, skifu_input_dev.keybit);	
	input_register_device(&skifu_input_dev); 
   ...
}

module_init(skifu_init);

Chiaramente, se il nostro input device driver fosse stato intenzionato a notificare eventi di tipo EV_REL, avremo dovuto abilitare il bit EV_REL in evbit ed inizializzare in maniera opportuna la corrispondente bitmap per gli event code (relbit).

Ogni qual volta scriviamo all'interno di /proc/skifu-input il device driver skifu notificherà la pressione e il rilascio dei tasti della tastiera l, s, enter mediante la macro input_report_key (include/linux/input.h).

...

	input_report_key(&skifu_input_dev, KEY_L, 1);
	input_report_key(&skifu_input_dev, KEY_L, 0);
	input_report_key(&skifu_input_dev, KEY_S, 1);	
	input_report_key(&skifu_input_dev, KEY_S, 0);	
	input_report_key(&skifu_input_dev, KEY_PENTER, 1);	
	input_report_key(&skifu_input_dev, KEY_PENTER, 0);	
...

La macro viene espansa in una chiamata alla funzione input_event (drivers/input/input.c) il cui compito è quello di inviare l'evento su tutti gli input_handler collegati al nostro input_dev. Se vogliamo provare skifu dopo averlo compilato e caricato:

[belch@charlie:skifu/] echo ls > /proc/skifu-input

Il collegamento tra un input_dev e un input_handler viene gestito dal sottosistema di input sulla base delle bitmap dell'input_dev e sulla base di una id_table definita nella struttura dati associata ad un input_handler. In questo modo il sottosistema riesce a tener traccia di chi è interessato a essere notificato di un certo evento.

L'input_handler relativo alla tastiera è definito all'interno di drivers/char/keyboard.c. La input_report_key andrà quindi a richiamare la routine kbd_event la quale si prenderà cura di gestire gli eventi di pressione/rilascio tasti e anche della gestione delle combinazione speciali quali ad esempio Alt-SysRq. Gli eventi che vengono ricevuti da kbd_handler vengono poi inviati al terminale.


Riquadro 1 - input_event

La funzione input_event (drivers/input/input.c) è responsabile della notifica di un evento. La funzione sostanzialmente notifica l'evento segnalato su ogni input_handler collegato all'input_dev passato come parametro. Esistono degli eventi la cui natura è tale da dover essere notificati anche sull'input_dev. È il caso degli eventi che individuano il cambiamento di stato di un led della tastiera per i quali si rende necessario l'invio di comandi al dispostivo da parte del device driver che lo gestisce. Se andiamo a guardare il prototipo della struct input_dev noteremo la presenza di un campo “event” il cui significato è lo stesso dell'omonimo campo della struct input_handler.


Input_Handler

Un input_handler si registra nel sottosistema di input dichiarando una struttura dati struct input_handler (include/linux/input.h). Questa volta prendiamo in considerazione l'input_handler del mouse definito in drivers/input/mousedev.c.

static struct input_handler mousedev_handler = {
	.event =	mousedev_event,
	.connect =	mousedev_connect,
	.disconnect =	mousedev_disconnect,
	.fops =		&mousedev_fops,
	.minor =	MOUSEDEV_MINOR_BASE,
	.name =		"mousedev",
	.id_table =	mousedev_ids,
};

Un input_handler viene registrato nel sottosistema di input tramite input_register_handler e viene rimosso tramite input_unregister_handler.

La prima cosa che va notata è la presenza di una id_table che serve al sottosistema di input per individuare gli input_dev da collegare all'input_handler.

Se si guarda l'implementazione di input_register_handler (drivers/input/input.c) si noterà che la routine per ogni input_dev registrato nel sottosistema (ogni input_dev è inserito nella lista input_dev_list) controlla se la bitmap degli eventi dell'input_dev soddisfa l'id_table dell'input_handler e in caso di esito positivo la funzione di connect dell'input_handler viene chiamata. Questa funzione, nel caso di mousedev è mousedev_connect che si prende cura di inizializzare un input_handle (include/linux/input.h) e di restituirlo:

static struct input_handle *mousedev_connect(struct input_handler *handler, struct input_dev *dev, struct input_device_id *id)
{
	...
	mousedev->handle.dev = dev;
	mousedev->handle.name = mousedev->name;
	mousedev->handle.handler = handler;
	mousedev->handle.private = mousedev;
	...
	return &mousedev->handle;

}

È proprio questa struttura dati che consente al sottosistema

di input di tenere traccia di chi è l'input_dev di un certo input_handler e viceversa. Se riprendiamo l'implementazione di input_register_handler, noteremo che l'handle restituito da mousedev_connect viene passato come parametro alla input_link_device la quale provvederà ad inserirlo nella lista degli handle sia dell'input_dev sia dell'input_handler.

	list_add_tail(&handle->d_node, &handle->dev->h_list);
	list_add_tail(&handle->h_node, &handle->handler->h_list);


Riquadro 2 - Liste circolari
Per facilitare le operazioni su liste circolari il kernel mette a
disposizione una famiglia di macro utili per la creazione, per
l'inserimento e la rimozione ed anche per eseguire una
operazione su tutti gli elementi della lista. Il file di
riferimento all'interno dell'alberatura è include/linux/list.h.


Un input_handler può eventualmente interagire con lo spazio utente tramite un file speciale di tipo “a caratteri” come avviene per mousedev che interagisce con gpm. L'implementazione delle operazioni sul file speciale è individuata dal campo fops.

La routine individuata nel campo event rappresenta il nucleo dell'input_handler in quanto si occupa di raccogliere gli eventi che vengono inviati da un input device driver associato.

Evdev: un'interfaccia per gli eventi

Per usare il modulo evdev occorre selezionare la voce “Event interface” dal sottomenu relativo al sottosistema di input. evdev è molto interessante perché registra un input_handler per raccogliere gli eventi di ogni dispositivo di input registrato nel sottosistema, eventi che possono essere prelevati dallo spazio utente interagendo attraverso dei file speciali.

Se abilitiamo evdev, possiamo interagire con le sue interfacce esportate tramite un file speciale di tipo” a caratteri”. Se vogliamo ricavare il nome dell'interfaccia esportata da evdev per un mouse USB possiamo consultare il contenuto di /proc/bus/input/devices:

I: Bus=0003 Vendor=1241 Product=1111 Version=0100
N: Name="USB HIDBP Mouse 1241:1111"
P: Phys=usb-0000:00:1f.2-1/input0
H: Handlers=mouse1 event3 
B: EV=7 
B: KEY=1f0000 0 0 0 0 0 0 0 0 
B: REL=103 

per poi recuperarne il MAJOR_NUMBER e il MINOR_NUMBER tramite l'attributo dev esportato da sysfs

[belch@charlie:belch/] cat /sys/class/input/event3/dev 
13:67

Ora possiamo creare il file speciale di cui abbiamo bisogno (qualora non fosse presente) tramite mknod:

[belch@charlie:belch/] mknod /dev/input/event3 c 13 67

Una volta aperto il file speciale, possiamo eseguire su di esso la syscall read per catturare gli eventi che vengono notificati (l'operazione di read verrà gestita dal “Virtual File System” in maniera tale da richiamare l'operazione di read esportata da evdev cioè evdev_read). È ovvio che ciò che andremo a leggere dall'interfaccia è una struttura dati di tipo struct input_event.

evdev non soltanto permette di ricevere in spazio utente gli eventi che vengono notificati, ma ci permette anche di eseguire una serie di operazioni ioctl per ottenere informazioni sull'input device driver. Ad esempio se vogliamo recuperare le maschere evbit e relbit di un interfaccia associata ad evdev:

    ioctl(fd, EVIOCGBIT(0, EV_MAX), evbit)
    ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relbit)), relbit)

In allegato all'articolo è disponibile il pacchetto udpmouse. Il pacchetto si compone due applicativi che consentono di usare un mouse da remoto. L'idea è quella di inviare gli eventi catturati tramite evdev su un sistema remoto, il quale provvederà poi ad inoltrarli al suo sottosistema di input. Mediante udpmouse è quindi possibile usare un unico mouse su due sistemi collegati in rete.

Il lato client è composto da udpmouse-send il quale preleva gli eventi da un'interfaccia di evdev per poi inoltrarli all'interno di un socket UDP. Ovviamente prima di iniziare l'invio degli eventi udpmouse-send controlla se l'interfaccia di evdev a cui stiamo accedendo è associato ad un input_dev di un dispositivo mouse. Il pacchetto si compila con un semplice “make” dopo di che per esportare un:interfaccia su 192.168.5.2:5555:

[belch@charlie:udpmouse/] ./udpmouse-send 192.168.5.2 5555 /dev/input/event3 


Riquadro 3 - Il filesystem sysfs

sysfs è un filesystem implementato nel kernel 2.6 la cui compilazione è mandatoria. sysfs consente di esportare attributi in spazio utente in maniera tale da poter reperire informazioni relative sia a dispositivi sia ai device driver che li governano. Possiamo ad esempio reperire le informazioni dell'interfaccia di rete eth0 in /sys/class/net/eth0.

Mediante l'uso combinato di sysfs e di un applicativo userspace denominato udev è possibile rimpiazzare completamente devfs. Questa caratteristica viene realizzata proprio grazie al fatto che tra gli attributi esportati da sysfs ci sono i MAJOR_NUMBER e i MINOR_NUMBER dei devices registrati.

udev è reperibile all'indirizzo http://www.kernel.org/pub/linux/utils/kernel/hotplug/ mentre se desiderate sapere di più sull'implementazione di devfs in spazio utente consiglio la lettura dell'ottimo articolo di Greg KH “udev - A Userspace Implementation of devfs” reperibile all'indirizzo http://archive.linuxsymposium.org/ols2003/Proceedings/


Uinput: user level input driver

Una caratteristica interessante del sottosistema riguarda la possibilità di costruire degli input driver direttamente nello spazio utente senza dover creare alcun modulo. Questa funzionalità viene messa a disposizione da uinput che viene compilato selezionando la voce “User level driver support” dal menu di configurazione degli “Input device driver”.

È possibile interagire attraverso un file speciale per definire un proprio

user level driver, dopo di che sarà uinput a prendersi l'onore di allocare e di registrare un input_dev. Vediamo ora un esempio di input_dev creato in spazio utente che riceve gli eventi da un socket UDP e li notifica agli input_handler registrati nel sottosistema di input (l'esempio di riferimento è il sorgente udpmouse-get.c contenuto all'interno del pacchetto udpmouse).

La prima cosa da fare è di assicurarsi che esista un file speciale creato da uinput i cui MAJOR e MINOR NUMBER possono essere ricavati con:

[belch@charlie belch/] cat /sys/class/misc/uinput/dev
10:223

Anche in questo caso il file speciale può essere creato tramite:

[belch@charlie:belch/] mknod /dev/input/uinput c 10 223

La struttura dati di riferimento per un user level input driver è la struct uinput_user_dev (include/linux/uinput.h). Vediamo ora la parte server del pacchetto udpmouse (udpmouse-get). L'applicativo crea un input device driver per notificare gli eventi di un dispositivo mouse direttamente dallo spazio utente. Gli eventi vengono ricevuti attraverso un socket UDP. La parte di codice che inizializza e registra l'input driver è in uinput_mdev_init.

La prima cosa da fare è aprire il file speciale esportato da uinput dopo di che la struttura uinput_user_dev viene inizializzata e registrata. Nella fase di inizializzazione anche in questo caso dobbiamo effettuare delle operazioni su bitmap che consentano al sottosistema di input di individuare gli input_handler a cui notificare gli eventi. In questo caso si tratta semplicemente di interagire con uinput attraverso delle chiamate ad ioctl eseguite sul file speciale che abbiamo precedentemente aperto:

   fd = open("/dev/input/uinput", O_WRONLY);
   ...
   ioctl(fd, UI_SET_EVBIT, EV_REL);
   ioctl(fd, UI_SET_EVBIT, EV_KEY);

Mediante le due chiamate a ioctl abbiamo comunicato che il nostro device driver intende notificare eventi di tipo EV_REL e EV_KEY. Ora il passo successivo sarà quello di riferire gli event code REL_X, REL_Y e BTN_LEFT, BTN_RIGHT e BTN_MIDDLE:

   ioctl(fd, UI_SET_RELBIT, REL_X);
   ioctl(fd, UI_SET_RELBIT, REL_Y);
   ioctl(fd, UI_SET_KEYBIT, BTN_LEFT);
   ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT);
   ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE);

La registrazione del input_dev nel sottosistema di input avviene dopo aver effettuato in sequenza una write e una ioctl sul file speciale precedentemente aperto

     retval = write(fd, &dev, sizeof(struct uinput_user_dev))
     ....
     if ioctl(fd, UI_DEV_CREATE) {
     ....
     }

L'analogo della input_event in userspace viene effettuata scrivendo nel file speciale l'evento che intendiamo far fluire all'interno del sottosistema di input (questa funzione è realizzata da uinput_mdev_event.

    write (fd, event, sizeof(struct input_event));

Per eseguire udpmouse-get in maniera tale che riceva gli eventi inviati da udpmouse-send sulla porta 5555 :

[belch@charlie:udpmouse/] ./udpmouse-get 5555


Approfondimenti

In questo articolo si è parlato dei “file speciali” di tipo a caratteri ovvero dei cosiddetti “char dev”. Un char dev è un device driver che consente di trasferire sequenzialmente un flusso di byte attraverso un char dev.

Ciò viene realizzato dichiarando una struct file_operations che racchiude le implementazioni delle operazioni di open, read, write, eccetera che si vuol far eseguire al device driver in maniera tale che quando viene eseguita una read sul file speciale il Virtual File System andrà a richiamare l'operazione di read definita nella struct file_operations del char dev. Un esempio interessante di har dev è il misc device per il quale è reperibile un ottimo articolo che seppur datato tratta di argomenti che sono rimasti immutati nel nuovo kernel. L'articolo è reperibile all'indirizzo http://www.linux.it/kerneldocs/misc/misc.html.

Un altro punto di approfondimento è la documentazione relativa al sottosistema di input presente all'interno della directory Documentation/input/. Un buon punto di partenza verso lo studio di Input device driver reali è dato dal PC Speaker (drivers/input/misc/pcspkr.c). Per chi vuole approfondire la programmazione del PC Speaker consiglio la lettura del documento presente in http//fly.cc.fer.hr/GDM/articles/sndmus/speaker1.html.


L'autore

Daniele Bellucci si interessa nel tempo libero dello studio del kernel Linux. Partecipa in maniera attiva a vari progetti tra cui Kernel Janitor ed ETLinux.