Programación de drivers para dispositivos
- Programación de drivers para dispositivos
Mantenido por Nelson Castillo.
La idea de este documento es documentar el desarrollo de un driver simple para Linux. El código que se muestra en esta página ha sido probado en las arquitecturas ARM, Intel y User-Mode Linux.
¿Qué es un driver?
Un driver es una capa de código entre el dispositivo de hardware y la aplicación. Un driver usa los privilegios con los que se ejecuta su código para definir exactamente como se quiere que un dispositivo sea visto por una aplicación. Pueden existir diferentes drivers para un mismo dispositivo.
El driver está en el kernel. El kernel tiene las siguientes tareas:
- Manejo de procesos : Creación y destrucción de procesos, comunicación entre procesos, asignación de CPU.
- Manejo de memoria : La memoria es un recurso crítico, y el kernel administra su asignación.
- Sistemas de archivos
- Control de dispositivos (drivers)
- Networking
El kernel diferencia tres tipos de drivers:
- Drivers de carácter (char devices)
- Drivers de bloque (block devices)
- Drivers de red (network devices)
No todos los drivers son de dispositivos. Algunos son de software. Por ejemplo, el driver de un sistema de archivos como ext3 o reiserfs son drivers de software, que mapea estructuras de datos de bajo nivel a estructuras de datos de más alto nivel.
¿Por qué escribir un driver?
Existen muchas razones para querer escribir un driver.
- Para dar soporte a nuevo hardware
- Para mantener un producto propio
- Se está creando hardware a un ritmo rápido, y los programadores de drivers van a tener trabajo por un buen tiempo
Recomendaciones
Proveer mecanismos, y no políticas
La idea es que un driver permita acceder a los dispositivos de hardware, sin imponer restricciones arbitrarias a los que usan el driver. En UNIX esto es una regla de diseño, que se conoce como separación de mecanismos y políticas.
Para no escribir políticas en el driver, se puede hacer una aplicación de usuario que se encargue de configurar el dispositivo, y/o una librería para acceder a él. La idea es que el driver cambie muy poco.
Uso de módulos
Los módulos tienen la ventaja de permitir adicionar y remover funcionalidades del kernel mientras el sistema está corriendo.
Módulos de ejemplo
Para obtener los módulos de ejemplo, ejecute:
$ svn co http://svn.arhuaco.org/svn/src/linux/examples/modules/hello/ $ cd hello
Hola Mundo
El siguiente ejemplo hello1.c es el código de un módulo que puede ser cargado y descargado de memoria con insmod y rmmod.
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
printk(KERN_ALERT "Hola, mundo\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Adios, mundo cruel\n");
}
module_init(hello_init);
module_exit(hello_exit);
La macro MODULE_LICENSE especifica la licencia del módulo. Si no se especifica esta cadena, se produce un warning, por ejemplo:
hello1: module license 'unspecified' taints kernel.
Se recomienda especificar la licencia, a no ser que se esté creando un módulo propietario.
Las macros module_init y module_exit son para inicialización. Funcionan si código se compila como módulo y directamente en el kernel. Cuando el código se compila en el kernel module_init se comporta igual a __initcall().
Para compilar el módulo, se usa el siguiente Makefile:
obj-m += hello1.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
La compilación de los módulos se ha simplificado, antes era necesario especificar múltiples parámetros en un makefile para un módulo. El nuevo sistema para compilar el kernel facilita el proceso de compilación de módulos.
Para compilar un módulo para User-mode Linux, se escribiría solamente.
uml@uml:~/hello$ ARCH=um make make -C /lib/modules/2.6.16.18/build M=/home/uml/hello modules make[1]: Entering directory `/home/n/uml/linux-2.6.16.18' CC [M] /home/uml/hello/hello1.o Building modules, stage 2. MODPOST CC /home/uml/hello/hello1.mod.o LD [M] /home/uml/hello/hello1.ko make[1]: Leaving directory `/home/n/uml/linux-2.6.16.18'
Ahora podemos insertar el módulo en el kernel.
# insmod hello1.ko Hola, mundo
Al borrar el módulo, se llama la función especificada en la macro module_exit. En este caso, es la función hello_exit.
# rmmod hello1
Adios, mundo cruel
Ejercicio 1
En esta página hay código de ejemplo para la creación de una entrada en el filesystem /proc. La función remove_proc_entry borra una entrada de este filesystem.
remove_proc_entry(PROC_NAME, NULL);
Escribir un módulo que provea la entrada /proc/count, con un contador. (solución)
Módulo simple con un driver de caracter
Los drivers en el kernel son accedidos por medio de un archivo especial. Estos archivos se pueden crear con el comando mknod.
mknod [OPTION]... NAME TYPE [MAJOR MINOR]
# mknod prueba char 1 1 # ls -l prueba crw-r--r-- 1 root root 1, 1 2006-05-31 05:55 prueba # rm prueba
En el archivo Documentation/devices.txt se encuentra un listado de los dispositivos que ya tienen un nombre asignado en Linux.
Al hacer un driver existen dos opciones: tomar un número mayor y menor no asignado, o usar uno dinámico. Cuando se usa asignación dinámica, se puede leer los números asignados de /proc/devices.
$ cat /proc/devices Character devices: 1 mem 2 pty 3 ttyp 4 /dev/vc/0 4 tty 4 ttyS 5 /dev/tty 5 /dev/console 5 /dev/ptmx 7 vcs 10 misc 13 input 14 sound 29 fb 116 alsa 128 ptm 136 pts 180 usb 189 usb_device 216 rfcomm 226 drm $ ls -l /dev/console crw------- 1 root root 5, 1 2006-05-31 04:54 /dev/console $ ls -l /dev/input/mice crw-rw---- 1 root root 13, 63 2006-05-31 12:52 /dev/input/mice
Sintaxis de inicialización "C Tagged Structures"
En el código del kernel es normal encontrar incializaciones de estructuras que no asignan un valor a todos los campos de la estructura. Estas inicializaciones se hacen usando la siguiente sintaxis:
#include <stdio.h>
struct prueba
{
int a;
int b;
};
int main(int argc, char *argv[])
{
struct prueba x =
{
.a = 1,
};
printf("x.a = %d\n", x.a);
return 0;
}
Las inicializaciones de este estilo permiten inicializar los miembros de la estructura sin seguir un orden en particular, y sin que sea necesario inicializar todos los campos.
El siguiente es el código de ejemplo de un driver de caracter de lecto-escritura (código)
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h> /* get_user and put_user */
#define SUCCESS 0
#define DEVICE_FILE_NAME "char_dev"
#define DEVICE_NAME "char_dev"
#define MAJOR_NUM 100
#define BUF_LEN 80
MODULE_LICENSE("Dual BSD/GPL");
static atomic_t Device_Open = ATOMIC_INIT(1);
static char Message[BUF_LEN]; /* Buffer */
static char *Message_Ptr; /* ¿Qué tan lejos llegó la lectura? */
static int device_open(struct inode *inode, struct file *file)
{
#ifdef DEBUG
printk(KERN_INFO "device_open(%p)\n", file);
#endif
if (!atomic_dec_and_test (&Device_Open))
{
atomic_inc(&Device_Open);
return -EBUSY; /* ya está abierto */
}
Message_Ptr = Message;
try_module_get(THIS_MODULE);
return SUCCESS;
}
static int device_release(struct inode *inode, struct file *file)
{
#ifdef DEBUG
printk(KERN_INFO "device_release(%p,%p)\n", inode, file);
#endif
atomic_inc(&Device_Open); /* marca el dispositivo como no-abierto */
module_put(THIS_MODULE);
return SUCCESS;
}
static ssize_t device_read(struct file *file, /* ver include/linux/fs.h */
char __user * buffer, /* buffer a llenar con datos */
size_t length, /* longitud del buffer */
loff_t * offset)
{
int bytes_read = 0; /* bytes escritos en el buffer */
#ifdef DEBUG
printk(KERN_INFO "device_read(%p,%p,%d)\n", file, buffer, length);
#endif
if (*Message_Ptr == 0) /* retornar EOF */
return 0;
while (length && *Message_Ptr)
{
put_user(*(Message_Ptr++), buffer++); /* escribe al buffer del usuario */
length--;
bytes_read++;
}
#ifdef DEBUG
printk(KERN_INFO "Lei %d bytes, quedan %d\n", bytes_read, length);
#endif
return bytes_read; /* bytes escritos al buffer */
}
static ssize_t
device_write(struct file *file,
const char __user * buffer, size_t length, loff_t * offset)
{
int i;
#ifdef DEBUG
printk(KERN_INFO "device_write(%p,%p,%d)", file, buffer, length);
#endif
for (i = 0; i < length && i < BUF_LEN; i++)
get_user(Message[i], buffer + i);
Message_Ptr = Message;
return i;
}
struct file_operations Fops = {
.read = device_read,
.write = device_write,
.open = device_open,
.release = device_release,
};
int init_module()
{
int ret_val;
ret_val = register_chrdev(MAJOR_NUM, DEVICE_NAME, &Fops);
if (ret_val < 0)
{
printk(KERN_ALERT "El registro del dispositivo falló (%d)\n", ret_val);
return ret_val;
}
printk(KERN_INFO "Se registró el dispositivo. El major device number es %d.\n", MAJOR_NUM);
printk(KERN_INFO "mknod %s c %d 0\n", DEVICE_FILE_NAME, MAJOR_NUM);
return 0;
}
void cleanup_module()
{
int ret;
ret = unregister_chrdev(MAJOR_NUM, DEVICE_NAME);
if (ret < 0)
printk(KERN_ALERT "Error: unregister_chrdev: %d\n", ret);
}
# insmod chardev.ko # dmesg | tail Se registró el dispositivo. El major device number es 100. mknod char_dev c 100 0 # mknod /dev/char_dev c 100 0 # ls -lh /dev/char_dev crw-r--r-- 1 root root 100, 0 2006-05-31 07:21 /dev/char_dev # chmod a+w /dev/char_dev
Usando python podemos hacer pruebas. La idea es ver como van saliendo las líneas de depuración a medida que se hacen las llamadas.
$ python
>>> f = open("/dev/char_dev", "r")
>>> f.close()
>>> f = open("/dev/char_dev", "w")
>>> f.write("Hola\n")
>>> f.close()
>>> f = open("/dev/char_dev", "r")
>>> print f.readline()
Hola
>>> f.close()
Módulo para un driver de caracter
A diferencia de el módulo simple, este módulo:
Hecho:
- Usa un major number dinámico.
- Recibe parámetros cuando es insertado, y en un parámetro se puede especificar el major number.
- Usa memoria dinámica.
- Funciona llseek
- Soporta llamadas ioctl.
- Tiene un programa en el espacio del usuario para su configuración (ioctl).
Falta por hacer:
- Tiene una entrada en el proc filesystem con estadística
El código está en este repositorio. Tiene muchos comentarios. Si lee alguna parte y cree que falta un comentario, con gusto lo puedo agregar.
Módulo para un driver de con lectura bloqueante y no bloqueante
El módulo de ejemplo anterior siempre retorna algo al leer. Ahora trabajaremos haciendo que las funciones de lectura y escritura implementen la siguiente semántica estándar:
- Si un proceso llama a read pero no hay datos disponibles El proceso debe bloquearse. El proceso es despertado tan pronto llegue un dato, y los datos se retornan al proceso que hizo la llamada, incluso si hay menos datos que los que se solicitaron en la llamada.
- Si un proceso llama a write y no hay espacio en el buffer El proceso debe bloquearse, y debe quedar en una cola de espera diferente a la que se usa para las lecturas. Cuando algunos datos ya se han escrito por el hardware de salida y hay espacio en el buffer de salida, el proceso es despertado y la llamada a write tiene éxito, aunque la escritura puede ser parcial.
En este driver ya no aparece definida la llamada seek. Para que todas las llamadas a seek fallen, se ha llamado la función nonseekable_open en el método open, y se ha referenciado no_llseek (disponible en linux/fs.h) en la estructura file_operations del driver.
Algunas reglas para tener en cuenta:
- Nunca dormir si no se está seguro de que otro proceso nos despertará
- No dormir adquiriendo semáforos que impidan que nos despierten luego
El código está en este repositorio. Tiene muchos comentarios. Si lee alguna parte y cree que falta un comentario, con gusto lo adiciono.
Módulo para capturar una interrupción
Este módulo está en desarrollo
Comenzamos con el acceso a hardware. En muchas arquitecturas, el espacio de direcciones y el de los puertos de I/O es el mismo. No obstante, debido a que algunas arquitecturas implementan espacios de direcciones separados, Linux tiene funciones especiales para leer y escribir en puertos, incluso en las arquitecturas que no los tienen.
En la arquitectura ARM el espacio de memoria e I/O es el mismo.
Al programar es importante hacer que el compilador no optimice ciertas instrucciones, y para ello Linux provee las siguientes macros que garantizan que las lecturas o escrituras que están antes de la función se completen.
- void barrier(void); (en /linux/kernel.h)
- void rmb(void); (en /asm/system.h) – read memory barrier
- void wmb(void); (en /asm/system.h) – write memory barrier
- void mb(void); (en /asm/system.h) – memory barrier (más lenta que rmb y wmb).
También se recomienda usar variables con la palabra clave volatile donde haga falta para prevenir optimizaciones del compilador.
Para solicitar el acceso exclusivo a una región de I/O, se usan las funciones request_region y release_region.
Ver el módulo en http://svn.arhuaco.org/svn/src/linux/examples/modules/parallel/.
Para escribir y leer de estos puertos, Linux define en /asm/io.h las siguientes funciones:
- unsigned inb(unsigned port) : 8 bits
- unsigned inw(unsigned port) : 16 bits
- unsigned inl(unsigned port) : 32 bits
- unsigned outb(unsigned char byte, unsigned port)
- unsigned outw(unsigned short word, unsigned port)
- unsigned outl(unsigned long word, unsigned port)
En algunas arquitecturas (S390) sólo se permiten operaciones de I/O de 8 bits.
Para un ejemplo de acceso a estos puertos en espacio de usuario, ver count.c.
Note que las funciones que usan words y longs pueden cambiar el orden de los bytes dependiendo de la plataforma.
También se pueden leer strings de puertos, y para ello están las siguientes funciones:
- unsigned insb(unsigned port, void *addr, unsigned long count)
- unsigned outsb(unsigned port, void *addr, unsigned long count)
También hay funciones para leer strings de longs y de words, y estas no cambian el orden de los bytes.
Entradas en el /proc
Ver:
- /proc/interrupts
- /proc/ioports
Restricciones de un manejador de interrupción:
- No puede transferir datos del usuario
- No se puede dormir
- No puede obtener un semáforo
- Si aparta memoria, debe hacerlo con GPF_ATOMIC
- No pueden llamar a schedule
Work queues
En interrupciones, hay que retornar lo más rápido posible. En el kernel 2.6 se ha implementado una nueva interfaz llamada work queue.
Ver:
http://www.linuxjournal.com/article/6916
Y un ejemplo en:
/arch/um/drivers/port_kern.c y en /arch/um/drivers/net_kern.c.
Referencias
- http://lwn.net/Kernel/LDD3/ : Linux Device Drivers, Third Edition (source code)
- http://www.tldp.org/LDP/lkmpg/2.6/html/
Documentación del kernel:
- Documentation/ioctl-number.txt
- Documentation/devices.txt
Last update: 2007-03-31 (Rev 10985)