Qué hacer y qué no hacer en los controladores de dispositivo Linux (es)

Submitted by kernel-labs on Mon, 2006-09-25 07:02.
Javier M. García López, nuestro más estrecho colaborador, nos ha cedido esta magnífica traducción del conocido documento de KernelJanitors, HOWTO: Linux Device Driver Dos and Don'ts. Es un documento muy interesante que no podemos dejar de leer.

Una guía para escribir controladores de dispotivos robustos para Linux.
Version 1.1

Índice


Licencia , autor y versión

Copyright (c) 2003 de Intel Corporation.
Este material puede ser distribuido, con la única restricción de respetar los términos y condiciones de la Open Publication Licence, v1.0 o posterior.

La última versión está actualmente disponible en: http://www.opencontent.org/openpub.
Autor: Tariq Shureih
Versión: 1.1
Fecha de Actualización: Date: 2004/03/18 21:42:47

Introducción

Este documento no es realmente un HOWTO de controladores de dispositivo – hay libros por ahí sobre cómo escribir controladores de dispositivo.

Escribir un controlador de dispositivo del kernel es tan sencillo como escribir tres líneas de código, o tan complejo, que puede requerir entender cómo Linux maneja el hardware en las diferentes arquitecturas que soporta, así como entender conceptos sobre el PC (todas las referencias de este documento son relativas a la arquitectura x86 aunque no especificamente).

Lo que este documento aborda, es lo que debes y no debes hacer al escribir controladores de dispositivo para el kernel Linux. Estos consejos estan basados en la lista TODO del proyecto Kernel-Janitor. Incluyendo los conceptos introducidos por las publicaciones de las especificaciones de "Hardened Drivers“ (nota del traductor: "Blindando Controladores“) en "http://hardeneddrivers.sf.net, también presentes en este documento.

Para más información sobre el proyecto Kernel-Janitor, en siguiente enlace "http://kernel-janitor.sourceforge.net/".

1.Resumen

1.1. ¿Por qué este documento?

Quería recopilar la información que aprendí cuando comencé con el desarrollo del kernel, en un único documento y quizás hacer un guía para novatos y/o gente que buscaba esos pequeños trucos que son necesarios para escribir controladores de dispositivo robustos.

Este documento es básicamente una sencilla guía para conocer métodos y técnicas al escribir un controlador de dispositovo para Linux y puede servir como documentación adicional a otra disponible.

1.2. ¿Qué es un "Controlador de Dispositivo Blindado?

La respuesta a esta pregunta depente de a quién se la pregunte. Para algunos, un controlador de dispositivo blindado es un controlador de dispositivo estable, fiable, "instrumental“ y de alta disponibilidad. En un intento previo de definir un controlador de dispositivo blindado, fue descrito como aquel que contempla tres niveles:
  1. Estabilidad y fiabilidad: El uso de prácticas de codificación bien conocidas en el controlador para detectar y comunicar condiciones de error (Nota del traductor: "errores errantes“) en el software y el hardware, para proteger el controlador, el kernel y otros componentes del software, de corrupción, para proporcionar pruebas de inducción de errores ("to provide for fault injection testing“), y para hacer el código limpio y fácil de leer para el mantenimiento de la calidad.
  2. Instrumentación: Interfaces de comunicación para estadísticas, interfaces para pruebas de diagnóstico, y trazas de error conformes a POSIX para monitorizar y gestionar por sistemas de componentes de control de alto nivel.
  3. Alta disponibilidad: Capacidades especiales, tales como redundancia de dispositivos, cambio en caliente, y recuperacion ante fallos, para incrementear la disponibilidad.
Este documento intentará describir "Contoladores Blindados“ con una pequeña diferencia; si bien está basado en algunos de los puntos principales anteriormente citados, intentará describir lo que un "Controlador Blindado -robusto-“ debería significar y cómo debe ser creado y medido.

Para evitar confusión con intentos anteriores de especificar lo que es un "Controlador Blindado“ este documento se referirá a un controlador ideal como un "Controlador Robusto".

1.3. "Controladores de Dispositivo Robustos"

Un controlador de dispositivo robusto es realmente un ejemplo de código robusto, sin errores y mantenible a nivel del kernel.

Al tiempo que Linux evoluciona, las características de lo que es un controlador robusto también varían, pero las siguientes caracterésticas permanecen:
  • Siguen el estilo de codificaciones de Linux, haciendolo fácil de mantener y consistente con el estilo de codificación del kernel.
    Referencia: <linux-kernel-source>/Documentation/CodingStyle
    Referencia: "http://www.kroah.com/linux/talks/ols_2002_kernel_codingstyle_talk/html/"

    Nota : no habrá más discusiones al respecto sobre este tema ya que las citadas referencias lo cubren todo.
  • Eficiencia en la gestión y manejo de errores, comunicación y recuperación ante errores . (niveles printk , valores de retorno numéricos, etc.). Así como no panic() tontos. Los controladores de dispositivo no pueden dar 'panic' por pequeñas razones. Esto va de la mano con la correcta gestión de errores y la recuperación de los mismos.
  • Cumplir con las últimas interfaces y API's del kernel Linux. Esto significa que es mantenido periódicamente y en diferentes plataformas hardware, estar al tanto de funciones ya viejas, macros e interfaces.
  • Usar el nuevo modelo de controladores de dispositivo , kobjects y sysfs. Borrado de cli-sti.
  • Manejo de recursos adecuado (memoria, semáforos, interrupciones, etc).
  • Usar las funciones "C“ de E/S que el kernel proporciona en lugar del código en ensamblador (inb, outb, etc.)
  • No tener código o variables no usadas. Si existe código que no se usa, pero que en el futuro se usará, entonces debe ser marcado como tal, de modo que no se compile y de esta forma no ocupe espacio.
  • Optimizar el espacio una vez compilado (por ejemplo, usar funciones inline cuando sea apropiado).
Los puntos mencionados dan una descripción a alto nivel de varias áreas que un desarrollador debe tener en cuenta, o deberá revisar a posteriori tras escribir o llevar a otra plataforma hardware un controlador de dispositivo.

Tal y como se mencionó anteriormente, éstas son caracteristcas generales que deben tenerse en cuenta para unos controladores de dispositivos mas robustos.

2.0. ¿Dónde empezar?

Si eres nuevo desarrollando controladores de dispositivo para Linux sería mejor empezar por el libro de O'Reilly "Linux Device Drivers“, por linux-kernel-source/Documentation/driver-model y por algunos ejemplos básicos del propio kernel (como linux/drivers/char/nvram.c).

También se puede conseguir mucha información en "www.kernelnewbies.org".

Una vez que has escrito tu primer driver y has adquirido lo básico (compilar, cargar, descargar, consultar, operaciones sobre ficheros, etc.), puedes echar un vistazo a este documento de nuevo y asi darte cuenta de algunas cosas que puede que hayas dejado pasar, o que te hubieran ayudado con tu driver.

Tanto si estas migrando un driver a otra plataforma, o si estas experimentando escribiendo controladores nuevos, este documento te ayudará en puntos que requieren especial atención, o tareas comunes al llevar controladores a diferentes plataformas que necesitarías conocer.

Espero que este documento te evite algunos "errores“ comunes y te enseñe el camino correcto que llevarán a tu controlador a los más altos niveles de rendimiento.

3.0. Vale estoy preparado ...

Comencemos examinando aspectos sobre la robustez del código de un controlador de dispositivo y las diferentes técnicas para conseguir gran eficiencia y estabilidad.

3.1. Manejo eficiente, comunicación y recuperación de errores

Se pueden seguir unas formas simples y probadas de manejar errores y fallos en el código:
  • printk()

    De acuerdo con el TODO de la lista kernel-janitor, deberías siempre comprobar que las llamadas a printk() incluyen las constantes KERN_* apropiadas.

    int res;
    ...
    if (res = register_chrdev(MAJOR, MOD_NAME, &fops)) {
            printk(KERN_ERR "Fallo al registrar el dispositovp <%s> con major <%d>\n",
                                  MOD_NAME, MAJOR);
            return res;
    }
    ...

    KERN_* están definidas en en linux/include/linux/kernel.h de la siguiente forma:

    #define KERN_EMERG  "<0>"  /* el sistema no es usable          */
    #define KERN_ALERT  "<1>"  /* se debe hacer algo inmediatamente*/
    #define KERN_CRIT  "<2>"  /* condiciones criticas            */
    #define KERN_ERR    "<3>"  /* situacion de error              */
    #define KERN_WARNING"<4>"  /* aviso                            */
    #define KERN_NOTICE "<5>"  /* normal pero con condicion inmportante*/
    #define KERN_INFO  "<6>"  /* informacion                      */
    #define KERN_DEBUG  "<7>"  /* mesages de depuracion            */

    Nota: Usa printk sólo cuando sea necesario. Se informativo y evita llenar la consola con mensajes de error cuando se encuentren condiciones erróneas (controla la cantidad, "flood control“).
  • goto

    Las instruciones goto están limitadas en la comunidad Linux para las siguientes situaciones:

    - Se usa menos código repetido.
    - Debe ser tan entendible como otras soluciones.
    - Debe ser menos susceptible de error.
    - Debe ser mas fácil de cambiar y mantener para el próximo que tenga que tocar el código.
    - Mantiene el código de manejo de errores lejos para mayor eficiencia en el uso de las cachés, teniendo (normalmente) el código de más frecuente éxito "cercano", quizá evitando saltos. [en gcc existen directivas likely/unlikely para elegir los caminos adecuados en caso de código repetido.]
    - Es usado a lo largo del código del kernel y usarlo hace uniforme el manejo de errores.

    int res;
    ...
    res = tty_register_driver (&serial_driver);
    if (res) {
            printk (KERN_ERR "serial.c: Error al registrar el controlador serie\n");
            goto out;
    }

    res = tty_register_driver (&callout_driver);
    if (res) {
            printk(KERN_ERR "serial.c: Error al registrar el controlador de llamada\n");
            goto out_callout;
    }
    ...
    out_callout:
            tty_unregister_driver (&serial_driver);
    out:
            return res;

  • Códigos de retorno numéricos, devolver ELOQUESEA en vez de devolver -ELOQUESEA

    Tu controlador debe devolver siempre un estado si recibe una petición. Se recomienda devolver siempre códigos numéricos como valores normales de retorno. Devolver 0 = acertado, ó -1 = fallado, es vago y sin sentido.

    Todos los valores devueltos por un controlador deben ser los estándar que proporcionan los siguientes ficheros de cabecera del kernel de Linux:

    linux/include/linux/errno.h linux/include/asm/errno.h linux/include/asm-generic/errno-base.h linux/include/asm-generic/errno.h

    Además muchos controladores tienen sentencias de retorno como la siguiente:

    return EIO; /* Error de E/S */

    cuando deberia ser:

    return -EIO; /* Error de E/S */

  • No se necesita llamar a panic()

    Un controlador de dispositivo debe hacer todo lo que esté en su mano para no llamar a panic(). El hecho de que un dispositivo hardware este roto, no responda y/o tenga un problema irresoluble no significa que haya que llamar a panic().

    Más aún, el controlador debería tratar de recuperar, reinicializar, o si todo lo anterior no es posible simplemente devolver el código de error apropiado, liberar los recursos que tuviera ocupados, grabar un mensaje y salir tranquilamente.

    Sólo en casos extremos, donde el sistema operativo y todo el sistema en general es irrecuperable se debería llamar a panic().

    Como regla general no se debe invocar a panic() y dejar a los subsistemas del kernel esa tarea como se muestra a continuacion:

    linux/drivers/char/watchdog/pcwd.c

    Este es un ejemplo real de un controlador del dispositivo en el kernel linux-2.5.52:

    if (revision == PCWD_REVISION_A) {
            if (cdat & WD_WDRST)
                    rv |= WDIOF_CARDRESET;

            if (cdat & WD_T110) {
                    rv |= WDIOF_OVERHEAT;

                    if (temp_panic)
                            panic("pcwd: Temperature overheat trip!\n");
            }
    }

    En este caso, la tarjeta watchdog esta llamando a panic() cuando detecta que la CPU está sobrecalentada.
    Si miras a la funcion panic() en linux/kernel/panic.c encontrarás que a no ser que la variable int panic_timeout > 0; donde la función panic() llamaría a machine_restart después de esperar la cantidad de tiempo panic_timeout; panic() podria poner el sistema en dead-lock (bucle infinito) que en este caso, podría perjudicar al sistema mas que ayudar.

    Una forma mas apropiada de manejar esta situación es simplemente apagar el sistema:

    if (revision == PCWD_REVISION_A) {
            if (cdat & WD_WDRST)
                    rv |= WDIOF_CARDRESET;

            if (cdat & WD_T110) {
                    rv |= WDIOF_OVERHEAT;

                    if (temp_panic) {
                            printk (KERN_EMERG "pcwd: Temperature overheat trip!\n");
                            machine_power_off();
                    }
            }
    }

    Es importante no confundir los panic "lazy“ (nota del traductor: "suaves") con un uso apropiado de panic(). Contra lo que se advierte en este documento es contra los "lazy“ panic(), cuando el desarrollador no tiene un conocimiento completo de la situación y opta por llamar a panic() para simplificar.

3.2. Al día con las interfaces/Apis del kernel

Este documento fue escrito mientras se estaba desarrollando la version 2.6 del kernel Linux. Durante el desarrollo de la versión y las diferentes versiones 2.5.x, varios conceptos, interfaces y subsistemas fueron introducidos, modificados o eliminados.

Tu nuevo, o modificado controlador necesita estar actualizado con respecto a estos cambios para funcionar mejor y para evitar posibles efectos no contemplados que afecten a la estabilidad, o a la degradación de la calidad.

Este documento aborda al máximo, de acuerdo con la habilidad del autor, todas las áreas de importancia y en consonancia al proyecto Janitor del kernel, la lista de desarrollo del kernel y la experiencia personal en lo que respecta a la contribución al kernel y añade experiencias de desarrolladores experimentados.

3.2.1 Cambios en las interfaces de los módulos

  • Inicialización y limpieza

    Antes del kernel 2.5 un controlador de dispositivo podia declarar lo siguiente:

    static int mi_funcion_de_inicializacion(void) { ...... }
    static void mi_funcion_de_limpieza(void) { ........ }

    module_init(mi_funcion_de_inicializacion);
    module_exit mi_funcion_de_limpieza);

    La nueva forma es:

    static int __init mi_funcion_de_inicializacion(void) { ...... }
    static void __exit mi_funcion_de_limpieza(void) { ...... }

    module_init(mi_funcion_de_inicializacion);
    module_exit(mi_funcion_de_limpieza);

    MODULE_LICENSE("license_type"); /* Esto ahora se pide en la mayoria de los casos */
                                    /* Puede ser GPL, BSD o cualquier otra          */

  • Macros de módulos para cuenta de referencias:

    Referencia: linux/include/linux/module.h
    Referencia: "ftp.us.kernel.org/pub/linux/kernel/people/rusty/modules/FAQ"

    Estas macros no están ya soportadas en 2.5.x y 2.6. En su lugar inicializa el campo .owner como parte de la estructura file_operations. En el kernel 2.4 se solía tener que ajustar la cuenta de referencias a los módulos para saber qué hacer en el caso de que el módulo se mueriera o se descargara mientras era usado, normalmente esto se hacía en la función open:

    static int mi_funcion_open (struct inode *inode, struct file *file) {
        ........
        MOD_INC_USE_COUNT;
        ........
    }

    Ahora ya no se necesita ajustar uno mismo las referencias a su propio módulo
  • Cuenta de referencia de/desde otros módulos:

    Un regla dorada: si estás llamando desde un puntero a función a un módulo diferente, debes guardar una referencia a ese módulo. Sino, te arriesgas a quedarte esperando si ese modulo se descarga. Puedes obtener una referencia a ese modulo usando:

    try_module_get(owner);

    y no tienes que comprobar si OWNER != NULL, porque esto lo hace try_module_get. Para devolver/guardar la referencia:

    module_put(owner);  // Para FAQ, si owner == NULL es correcto

    De igual forma si obtienes un punto de de entrada a otro módulo a través de symbol_get(), u obtienes un puntero a función, o el módulo exporta con EXPORT_SYMBOL una función, entonces necesitas llamar a try_module_get(owner) primero y no llamar al módulo si esto falla.

    Evita usar GET_USE_COUNT(module) especialmente si la descarga del modulo está deshabilitada en el kernel. Entonces no hay cuenta de las referencias y nada a lo que asignar.

    Es seguro llamar a try_module_get y module_put desde una interrupción o una softirq. (Fixme: example)

    Los controladores de red deberían llamar a SET_MODULE_OWNER en lugar de MOD_*_USE_COUNT.

    No es necesario tener toda la informacion del módulo en tu código: MODULE_AUTHOR, MODULE_DESCRIPTION, SUPPORTED_DEVICE, PARM_DESC, ...

    Ejemplo:

    MODULE_AUTHOR("The Linux Kernel Team");
    MODULE_DESCRIPTION("Digital 21*4* Tulip ethernet driver");
    MODULE_LICENSE("GPL");

    Nota: MODULE_PARM fue reemplazado con module_param y está definido en linux/include/linux/moduleparam.h de la siguiente forma: module_param(name, type, perm), que trabaja si el controlador esta empotrado o si el módulo no requere un #ifndef MODULE para tratar los argumentos de la línea de comandos del kernel.
  • Modulos, Kobjects y sysfs

    Mirar "3.2.2 Sysfs y el nuevo modelo de controladores" a continuación.

3.2.2 Sysfs y el nuevo modelo de controladores

Referencia: linux-kernel-source/Documentation/driver-model/
Referencia: linux-kernel-source/Documentation/filesystems/sysfs.txt
Referencia: linux-kernel-source/Documentation/kobject.txt

Sysfs fue diseñado con sencillez e implementación uniforme en el espacio del kernel. No se simplifican las interfaces de usuario a objetos del kernel, sin embargo se trata de mantener la misma estructura aunque dividiendo la información en /proc que se había convertido en algo muy confuso.

Primero asegurate de leer "kobject.txt", mencionado mas arriba. Sysfs esta fuertemente unido a kobject. En esta sección trataré de mostrar la forma simple más rápida de usar Sysfs, y me centraré en los detalles que parecen menos documentados o escondidos.

Sysfs esta siempre compilado (kernel 2.5 y posteriores) y puede ser accedido con:

mount -t sysfs sysfs /sys

Donde /sys es el punto de montaje y se crea con:
mkdir /sys

Estoy escribiendo un nuevo controlador, ¿cómo lo añado a Sysfs? Estoy llevando un controlador a linux 2.5 y sysfs, ¿qué hago?

La idea es exponer los atributos de tu dispositivo, o controlador de bus, o posiblemente una clase (una colección de objetos similares o dispositivos lógicamente agrupados bajo el mismo subdirectorio sysfs tal como "input". Un ratón puede ser PS/2 o USB pero sigue siendo un dispsitivo de entrada y pertenece a la clase "input" ) a través de ficheros creados bajo /sys. Estos atributos corresponden a la funcionalidad específica de tu dispositivo y el espacio de usuario puede acceder escribiendo directamente sobre estos ficheros.

La tarea más común es probablemente portar controladores existentes a sysfs o escribir un nuevo controlador que aproveche las ventajas de sysfs. Pensando en que estos dispositivo pertenecen a un bus especifico como PCI o USB y que probablemente también pertenecen a una clase de dispositivos comunes en funcionalidad pero no de bus (puedes tener un raton PS/2 y otro USB; ambos son de la clase "input“ pero cada uno se conecta a un bus diferente, el trabajo de convertir los buses, las clases, etcétera, está hecho o casi hecho. La clase de red todavía no esta terminada en el momento de escribir este documento).

Cuando escribes un nuevo controlador para un dispositivo PCI y reservas tu estructura pci_dev *pdev, por ejemplo, en tu estructura privada de dispositivos hay un puntero a struct pci_driver *driver el cual tiene empotrado struct device dev dentro. Esto es proporcionado por sysfs para el controlador de dispositivo. Para acceder a la instancia del dispositivo desde el controlador debes acceder a xxx_driver->pdev->dev.

struct xxx_driver {
        char *xxx_name;
        int chip_id;
        ....
        struct pci_dev *pdev;
        ....
}

Entonces tu dispositivo es descubierto por el bus (en este caso pci; durante el proceso de registro el subsistema pci automáticamente registra tu controlador de dispositivo en sysfs y crea los ficheros correspondientes en esa estructura bajo /sys/bus/pci/drivers/xxx_driver/, que es en realidad un enlace simbólico a /sys/devices/pci0/device_address).

Bajo esos directorios los atributos del controlador son accesibles. Hay ficheros por defecto que pueden corresponder a características no implementadas pero todos los dispositivos tienen nombre, potencia, clase, fabricante, irq, ... y hay caracteristicas específicas del dispositivo que necesitarás implementar. Estas características son accesibles bajo el directorio de la instancia del dispositivo y no bajo el disrectorio del controlador.

Ejemplo básico 1: Ejemplo de cómo se puede crear un controlador de una clase ya existente. Es un controlador “vacío”, un driver tipo carácter, que he creado bajo la clase “input” como un dispositivo be bus de sistema. Este controlador tiene la estructura habitual file_operations fops: open, close, seek, etc. Este registra un dispositivo de caracteres en /dev/mydriver con valor major 42 y minor 0. Normalmente se registra con:


register_chrdev(MAJOR, name, &fops);

Para el sysfs ya he declarado:

struct device_driver mydriver = {
        .name = "mydriver",
        .bus = &system_bus_type,        //From linux/device.h
        .devclass = &input_devclass    //exported from the input subsystem
}

Nota: se ha hecho un gran esfuerzo en la comunidad del kernel para usar el estilo C99 en la declaracion de estructuras. El ejemplo de arriba muestra como puedes hacerlo y como al mismo tiempo también cumples con el estilo de codificacion de linux.

También quiero poder “preguntar” al controlador por el flag de debug y poder activarlo haciendo lo siguiente:

echo "1" > /sys/class/input/drivers/mydriver/debug  // poniendo 1 se activa y 0 se desactiva

El sistema de ficheros sysfs da dos metodos (“show” y “store”) para leer y escribir información de atributos. Implemento dos funciones:

static ssize_t show_debug (struct device_driver *d, char * buf)
{
        return sprintf(buf, "debug: %i\n", debug);
}

static ssize_t store_debug (struct device_driver *d, const char * buf,ssize_t count)
{
        int tmp;

        if ((sscanf(buf, "%i", &tmp)) != 1)
                return -EINVAL;

        debug = tmp;

        return strnlen(buf, count);
}

Hay algunos comentarios importantes de linux/Documentation/filesystems/sysfs.txt a continuación:

- sysfs reserva memoria de tamaño (PAGE_SIZE) y la pasa al método.
- la memoria reservada será siempre de tamaño PAGE_SIZE bytes. En el i386 es 4096.
- las funciones show() deben revolver el número de bytes mostrados en este buffer. Este es el valor de snprintf().
- las funciones store() deben revolver el número de bystes usados. Esto se puede hacer usando strlen().

Nota: El sysfs no permite cambiar el nombe del controlador de dispositovo así que no crees funciones para hacerlo :)

Ya tengo la estructura (sysfs) device_driver y las funciones. Ahora necesito registrar mis atributos. Sysfs "linux/device.h" nos da macros para simplificar este proceso:

Simplemente declaras:

DRIVER_ATTR(debug, 0644, show_debug, store_debug);

Después tenemos que crear los ficheros asociados con los atributos, para dispositivos llamamos a:

driver_create_file(&mydriver, &driver_attr_debug);

donde:
mydriver es la estructura device_driver.
driver_attr es el prefijo de la función.
debug es el nombre del atributo que se añade a las declaraciones de las funciones show_ y store_ .

Claro, también está:

driver_remove_file(&mydriver, &driver_attr_debug);

para descargar el controlador.

Una pregunta muy normal es "¿dónde pongo las funciones para el resto del código?". La respuesta es, que no tienes que registrar atributos o crear ficheros hasta que el controlador ha terminado la inicialización y está completamente cargado. En la instanciación del controlador de dispositivo habitualmente llamas a estas funciones cuando la instancia se ha registrado sin errores. Lo mismo para borrar ficheros.

Ejemplo básico 2: Añadir atributos a un controlador ya existente. En este ejemplo voy a usar el controlador de dispositivo “tulip” actualmente implementado en el kernel. Este ejemplo asume que el desarrollador quiere añadir un nuevo atributo que es especifico del driver para el hardware tulip y no uno común, o por defecto que venga con el pci-sysfs.

El nombre del atributo es “ctrl” y en realidad es un atributo “vacío”, que no hace nada. Pero sirve como ejemplo.

Los prototipos de las tres nuevas funciones son:

static ssize_t show_ctrl (struct device *d, char *ctrl_buf);        // método Show
static ssize_t store_ctrl (struct device *d, const char *ctrl_buf,size_t ctrl_count);  //método Store
static void attr_reg (struct tulip_private *device);    // se llama para crear ficheros

También declaro una variable global: static int tulip_ctrl = 0;

La implementaciones de las funciones son:

static ssize_t show_ctrl (struct device *d, char *ctrl_buf)
{
        return snprintf(ctrl_buf,4096, "%i\n", tulip_ctrl);
}

static ssize_t store_ctrl (struct device *d, const char *ctrl_buf, size_t ctrl_count)
{
        int ret;

        if (sscanf(ctrl_buf, "%i", &ret) != 1) {
                printk (KERN_ALERT "Invalid flag for tulip ctrl\n");
                return -EINVAL;
        }

        if (ret) {
                tulip_ctrl = 1;
        } else {
                tulip_ctrl = 0;
        }

        return strnlen(ctrl_buf, ctrl_count);
}

Hay que añadir los atributos y crear los ficheros para estos en la estructura del controlador de tulip. El controlador de tulip esta en: /sys/buc/pci/drivers/tulip/<device_id>/.

Quiero que mi atributo "ctrl" esté en la estrutura arriba mencionada. ¿Recuerdas lo que comenté más arriba sobre la estructura pci_dev que tenía la estructura del dispositivo dentro? Ahora podemos utilizarla en la estrutura de dispositivo de sysfs. Nota: el prototipo de attr_reg, tiene un puntero a la estructura tulip_private que es algo como esto:

struct tulip_private {
        const char *product_name;
        struct net_device *next_module;
        struct tulip_rx_desc *rx_ring;
        struct tulip_tx_desc *tx_ring;
        ......
        struct pci_dev *pdev;
        .......
};

Dentro de la estructura tulip_private, se puede ver el puntero a la estructura pci_dev que es algo como:

struct pci_dev {
        struct list_head global_list;      /* node in list of all PCI devices */
        struct list_head bus_list;      /* node in per-bus list */
        struct pci_bus  *bus;          /* bus this device is on */
        struct pci_bus  *subordinate;      /* bus this device bridges to */
        ........
        struct  device  dev;            /* Generic device interface */
        ........
};

Entonces tenemos acceso a la entrada del dispositivo en sysfs y podemos acceder de la siguiente forma:

static void attr_reg(struct tulip_private *device) {
        device_create_file(&device->pdev->dev, &dev_attr_ctrl);
}

Pero todavía no hemos terminado. No hemos declarado las macros para los atributos y tampoco colocamos las llamadas para attr_reg y device_remove_file en el código tulip todavía. Suelo colocar la llamada a la macro DEVICE_ATTR justo después de los prototipos de las funciones. Recuerda que las macros declaran tus estructuras de atributos pero nada se crea hasta que no llamas a device_create_file. También recuerda que tu no debes llamar a attr_reg hasta que la instancia del controlador se haya inicilizado y cargado completamente.

Código: (pricipio del fichero después de los includes en la declaracion de prototipos)

static ssize_t show_ctrl (struct device *d, char *pm_buf);
static ssize_t store_ctrl (struct device *d, const char *pm_buf,size_t pm_count);

DEVICE_ATTR(ctrl,0644,show_ctrl,store_ctrl);



static void attr_reg(struct tulip_private *device);

En la función tulip_init_one he añadido una llamada a attr_reg:

[cortado de un patch]
@@ -1687,6 +1699,7 @@
            /* No initialization necessary. */
            break;
      }
+      attr_reg (tp);

/* poner el chip en modo ~@~\snooze~@~] hasta que se abra */
tulip_set_power_state (tp, 0, 1);

Por supuesto no olvides borrar los ficheros cuando se descarge la instancia del controlador:

static void __devexit tulip_remove_one (struct pci_dev *pdev)
{
@@ -1757,6 +1793,9 @@
        return;

    tp = dev->priv;
+
+  device_remove_file(&tp->pdev->dev, &dev_attr_ctrl);
+
    pci_free_consistent (pdev,
                        sizeof (struct tulip_rx_desc) * RX_RING_SIZE +
                        sizeof (struct tulip_tx_desc) * TX_RING_SIZE,
@@ -1774,7 +1813,6 @@
    /* pci_power_off (pdev, -1); */
}

Cuando compiles y carges el controlador podrás encontrar la entrada de "ctrl" en: /sys/bus/pci/driver/tulip/0:11.0/ctrl.

Recuerda que el ejemplo implementa un atributo específico del controlador y eso significa que no es un atributo común y que sólo estará disponible (en este ejemplo) si el dispositivo lo proporciona y no lo proporciona el controlador genérico.

3.2.3 Cli() y Sti()

Desde linux-2.5.x y también para linux-2.6.x, las siguientes funciones (macros) están obsoletas y por tanto están en desuso: cli(), sti(), save_flags(), save_flags_cli and restore_flags().

Esto es especialmente importante para sistemas SMP. Los controladores tendrán que utilizar las funciones primitivas de bloqueo y hacer uso de los semáforos que proporciona el kernel. Esto se detalla en linux/Documentation/cli-sti-removal.txt

Una cosa que hay que tener en mente cuando se convierte un controlador existente para que sea apto en sistemas SMP al manejar interrupciones y sincronización de bloqueos: En la mayoría de los casos no es un simple buscar y cambiar de viejas llamadas a las nuevas. Cuando se trata con interrupciones y bloqueos necesitas analizar el código del controlador en conjunto y comprender el objetivo de esas llamadas.

Mirando en linux/include/linux/interrupt.h:

Las viejas macros para sistemas mono-procesador se cambian por las nuevas hasta que se adapta todo el código.

#if !CONFIG_SMP
# define cli()          local_irq_disable()
# define sti()          local_irq_enable()
# define save_flags(x)      local_save_flags(x)
# define restore_flags(x)  local_irq_restore(x)
# define save_and_cli(x)    local_irq_save(x)
#endif

Estas se eliminarán en cuando aparareca la versión oficial de linux-2.6. Tanto si tu controlador es mono-procesador (UP) o multi-procesador (SMP), es más apropiado usar el correspondiente código para proteger (bloquear) las secciones críticas, manejar las interrupciones dentro de tu controlador, y usar la apropiada sincronización.

Los spinlocks aportan mejoras de rendimiento siendo así más rápidos de obtener. Los actuales funciones (macros) para los spinlock están definidos en: linux/include/linux/spinlock.h

Algunos apuntes sobre la nueva API:

  • proc_register están muerto, en su lugar usa create_proc_entry().
  • La nueva API de PCI tiene los siguientes cambios:
    - Antes de leer del dispositivos utilizando pdev->irq or pdev->resource[] , pci_enable_device se debe llamar dado que sino esos punteros tendrán valores incorrectos.
    - Llama a pci_set_drvdata() para inicializar dev->driver_data, de la misma forma para pci_get_drvdata() en lugar de leer directamente.
    - Se debe inicializar PCI_CACHE_LINE_SIZE durante la inicialización de PCI.
  • Debes cambiar Replace check_region por request_region y comprobar el valor de retorno:

    Ejemplo: [extraído del parche enviado a KJP]

        if ((state->type != PORT_UNKNOWN) && state->port) {
    -      request_region(state->port,8,"serial(set)");
    +      if(!request_region(state->port,8,"serial(set)")) {
    +          printk(KERN_WARNING "serial port request region %d failed.\n", state->port);
    +          return -EBUSY;
    +  }
    }

  • La función sleep_on() desaparecerá en las series de kernels 2.5/2.6 .

3.3. Gestionando recursos

En esta sección miraremos las diferentes formas de reserva de recursos, inicialización de variables, llamadas a funciones de balanceo y las funciones adecuadas de cadenas para controladores.

3.3.1. Funciones de cadenas

  • La función strtok() no es segura para programación de hilos y multi-procesdor en su lugar se debe utilizar strsep():

    -      for (opt = strtok(options, ",");
    -          opt != NULL;
    -          opt = strtok(NULL, ",")) {
    +      while (opt = strsep(&options, ",")) {
    +              if (!*opt)
    +                      continue;

  • Las funciones strlen y sprintf se deben evitar en la programación de controladores, especialmente desde que sysfs especifica en linux/Documentation/filesystems/sysfs.txt, que la función show() debe usar siempre snprintf para devolver el número de bytes impresos en el buffer. Lo mismo para la funciçon store(), debe devolver el número de bytes usados del buffer usando strnlen().

3.3.2. Declaración e inicialización de variables

  • Para generar un código optimo en ensamblador:

    [const] char foo[] = "blah";

    es mejor que

    [const] char *foo = "blah";

    dado que el segundo ejemplo podría generar dos variables al generar el código en ensamblador, una cadena estática y un puntero apuntando a ella.
  • Es mejor "Unsigned int" que "int" ya que genera mejor código en ensamblador en todas las plataformas excepto en sh5. Aunque, si se necesita para recoger codigos de error negativos "int" está bien.
  • Si tienes un puntero a una estructura y necesitas utilizar “sizeof” es mejor usar “sizeof(*var)” que “sizeof(type)” ya que frente a un cambio de tipo no necesitarás ir por todo el código adaptandolo.

3.3.3. Funciones de balanceo/reparto

Es importante asegurarse que de todas las llamadas a funciones para reserva o liberación de recursos están balanceadas y coinciden unas con otras sobre todo durante caidas. Las llamadas habituales son kmalloc junto con kfree y de igual modo kfree_skbs al librerar skbs.

Áreas comunes que necesitan atención son:
  • pci_alloc_consistent() y pci_free_consistent().
  • La llamada a ioremap() se debe balancear con iounmap(). Este es un punto de perdida de memoria habitual.
  • Las funciones con nombre *_lock_* se deben balancear con sus correspondientes *_unlock_* para evitar bloqueos infinitos (dead-locks).

3.4. Operaciones de E/S

3.4.1. Acceso al espacio de E/S

Estas funciones son:

Lecturas/Escrituras I/O de tipo byte:

unsigned inb(unsigned port);                    //lectura
void outb(unsigned char byte, unsigned port);  //escritura
Lecturas/Escrituras I/O de tipo word:

unsigned inw(unsigned port);                    //lectura
void outw(unsigned short word, unsigned port);  //escritura

Lecturas/Escrituras I/O de tipo long:

unsigned inl(unsigned port);                    //lectura
void outl(unsigned doubleword, unsigned port);  //escritura

Las correspondientes versiones para cadenas son:

void insb(unsigned port,void *addr,unsigned long count);    //lectura
void outsb(unsigned port,void *addr,unsigned long count);  //escritura
void insw(unsigned port,void *addr,unsigned loing count);  //lectura
void outsw(unsigned port,void *addr,unsigned long count);  //escritura
void insl(unsigned port,void *addr,unsigned long count);    //lectura

void outsl(unsigned port,void *addr,unsigned long count);  //escritura

Ejemplo:

__asm__( "mov $0, %al\n\t" "outb %al, $0x70");

En su lugar debe ser:

outb(0x00, 0x70);

3.4.2. Acceso a la memoria mapeada de E/S

Las funciones para esto son:

unsinged readb(address);
unsigned readw(address);
unsinged readl(address);

void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);

Ejemplo:

*(u8 *) (IOBASE+IER) = 0x80;

En su lugar debe ser:

writeb(0x80, IOBASE+IER);

Nota: debes utilizar la opción de compilación -O2 para que estas funciones (macros en realidad) se expandan.

3.5. Lo que obviamente no debes

  • Los controladores de dispositivos nunca deben añadir una "syscall". Las llamadas al sistema (syscall) se deciden por los mantenedores del kernel e implica código en ensamblador, manejo de software de interrupciones (int 0x80), que es algo que los desarrolladores de controladores de dispositivo nunca deben hacer.
  • Por supuesto nunca debes tratar con la BIOS a no ser que sepas exactamente lo que haces.
  • No accedas a ningún recurso sin haberlo reservado y chequedado.
  • No añadas IOCTLs que dupliquen las existentes.
  • No elimines los mesajes de aviso (warnings) en los makefiles. Esto esconde mensajes de aviso que pueden terminar resultando fatales.
  • No uses __SMP__ nunca más, ya no existe o no existirá.
  • No mezcles tipos o hagas conversión de tipos en el espacio del kernel.
  • No añadas nuevas APIs sin estar seguro de que ya existen, investiga.