Device Drivers e gerenciamento dinâmico de dispositivos

- por Sergio Prado

Categorias: Linux, Mini2440 Tags: , , , ,

Conforme expliquei no meu primeiro artigo sobre device drivers, tudo em Linux são arquivos. Expliquei também que todo acesso ao hardware é realizado através de arquivos no diretório /dev. Portanto, sempre que você precisar acessar determinado hardware, seu arquivo de dispositivo deve estar presente em /dev. Obs: em alguns casos bem específicos, o hardware pode ser acessado via /proc ou /sys.

Nos primeiros anos de vida do Linux, a quantidade de dispositivos suportados era apenas uma pequena fração do que é hoje. Naquela época, para cada hardware suportado pelo sistema operacional, criava-se um arquivo estático em /dev.

À medida que o kernel foi evoluindo com a adição de novos drivers de dispositivo, a quantidade de arquivos criados em /dev começou a crescer exponencialmente, tornando difícil seu gerenciamento, e acabando aos poucos com a disponibilidade de major e minor numbers que podiam ser atribuídos à outros dispositivos. No RedHat 9, por exemplo, existiam 19.000 arquivos de dispositivo em /dev!

Além disso, sempre que você adicionasse um hardware não suportado nativamente pelo kernel, você precisaria adicionar um arquivo de dispositivo manualmente com o comando mknod. Foi o que fizemos no segundo artigo sobre device drivers, para acessar os leds do kit FriendlyARM mini2440:

$ mknod /dev/led1 c 253 0
$ mknod /dev/led2 c 253 1
$ mknod /dev/led3 c 253 2
$ mknod /dev/led4 c 253 3

Em determinado momento, os desenvolvedores do kernel precisaram buscar uma solução que possibilitasse a criação dinâmica de arquivos de dispositivo. Então nasceu o devfs.

DEVFS

O devfs é um sistema de arquivos virtual criado para representar dinamicamente todos os dispositivos conectados ao sistema. Isso significa que, no boot, bastaria você montar o devfs no /dev, conforme abaixo:

$ mount -t devfs none /dev

Para cada hardware adicionado ou removido, o kernel atualizaria este sistema de arquivos virtual, refletindo diretamente no diretório /dev. Pareceu uma solução boa, e resolveu o problema de ter uma lista enorme de arquivos criados em /dev.

Porém, toda sua implementação estava em kernel space. Isso significa que, se por exemplo inserirmos um dispositivo não suportado pelo kernel, ainda teríamos que usar o comando mknod para criá-lo manualmente. Além disso, o nome dos dispositivos eram estáticos e não poderiam ser mudados, além da implementação ter alguns bugs.

Foi então que, para corrigir estas deficiências, criou-se a arquitetura de hotplug do kernel, e um tempo depois o udev foi implementado.

HOTPLUG E UDEV

A arquitetura de hotplug do kernel levou toda a implementação do devfs para userspace. A única responsabilidade do kernel nesta arquitetura é notificar o userspace quando um hardware é inserido, removido, etc. Esta implementação deu uma flexibilidade muito maior para o gerenciamento dos arquivos de dispositivo.

Nesta arquitetura, o kernel pode trabalhar de duas formas:

  1. O kernel notifica o userspace através da chamada à um processo definido em /proc/sys/kernel/hotplug (no começo usava-se o /sbin/hotplug).
  2. O kernel notifica o userspace através de um socket do tipo netlink. Qualquer processo que estiver “escutando” este socket irá receber as notificações (que também são chamadas de uevents).

Então surgiu a implementação do udev para se beneficiar destas facilidades do kernel. No começo, o udev usava o primeiro mecanismo, trocando o /sbin/hotplug pelo /sbin/udevsend. Depois, ele passou a usar o segundo mecanismo, através da implementação do processo udevd para escutar os eventos no netlink socket. Desde então, o uso do udevd é padrão na maioria das distribuições Linux atuais.

A notificação enviada em ambos os casos (processo ou socket) é idêntica. O evento gerado pelo kernel, ao invés de trazer todas as informações do dispositivo conectado, traz apenas um identificador do dispositivo. A partir deste identificador, o udev busca as informações necessárias no diretório /sys. Este diretório, que também é montado a partir de um sistema de arquivos virtual (sysfs), contém dados detalhados do hardware do sistema, e é de suma importância para a arquitetura de hotplug do kernel.

Basicamente, o que acontece em um sistema com udev é o seguinte:

  1. No boot, o kernel gera eventos para todos os dispositivos identificados no /sys, o udevd captura estes eventos e cria no /dev os arquivos de dispositivo para o hardware presente no sistema. Este procedimento é chamado de coldplugging.
  2. Para cada novo evento, o kernel envia uma nova notificação, o udev captura esta notificação trata. Este procedimento é chamado de hotplugging.

Uma explicação mais detalhada sobre o mecanismo de hotplug pode ser encontrado aqui.

O usuário poderá definir regras para o tratamento das notificações enviadas pelo kernel, e a grande vantagem desta arquitetura é a criação de regras persistentes. O udev resolve problemas do tipo: quando conectar este pendrive, quero que seja sempre atribuído a ele o arquivo de dispositivo /dev/mypendrive.

Outro exemplo: imagine que você tenha duas impressoras, uma jato de tinta e outra à laser. A primeira impressora identificada pelo kernel será a /dev/lp0, e a segunda a /dev/lp1. Isso significa que o nome do arquivo associado à impressora vai depender da ordem de identificação do hardware pelo kernel. Isso pode causar um problema para as aplicações, já que não existe uma garantia de que a impressora à laser estará sempre em /dev/lp0, por exemplo. Mas podemos resolver este problema com a criação de uma regra no udev.

No udev, as regras são salvas em /etc/udev/rules.d, e para cada evento recebido, o udev procura uma regra que se encaixe. No exemplo abaixo, conseguimos garantir que a impressora à laser, que possui o número serial “L72010011070626380”, será sempre associada ao arquivo de dispositivo /dev/printer/laser:

SUBSYSTEM=="usb", ATTRS{serial}=="L72010011070626380", SYMLINK+="printer/laser"

Uma documentação bem completa sobre como criar regras no udev pode ser lida aqui. Além da criação de arquivos de dispositivo, o udev também é capaz de carregar drivers ou baixar um firmware no hardware conectado.

A implementação do udev é bem compacta (no meu desktop com Ubuntu ela tem apenas 100K), mas para sistemas embarcados com recursos bem escassos, podemos recorrer implementações mais enxutas como o mdev do nosso bom e velho amigo Busybox.

MDEV

O mdev é uma implementação do udev dentro do Busybox.

Vamos ver então um exemplo prático de como usar o mdev para criar dinamicamente os arquivos de dispositivo do device driver que desenvolvemos para controlar os leds do kit FriendlyARM mini2240.

PREPARADO O KERNEL

O kernel precisa ser compilado com a opção CONFIG_HOTPLUG habilitada. Para verificar se seu kernel foi compilada com esta opção habilitada, veja se o arquivo /proc/sys/kernel/hotplug existe. Se não existir, você precisará habilitá-lo e recompilar o kernel.

PREPARANDO O BUSYBOX

O Busybox precisa estar configurado para gerar o mdev. Esta opção vem habilitada no config padrão do Busybox, mas se não estiver, você precisará habilitá-la com as opções abaixo:

Linux System Utilities
    [*] mdev
    [*]   Support /etc/mdev.conf

COMANDOS NO BOOT

Altere uns dos scripts de inicialização e insira os dois comandos abaixo:

$ echo /sbin/mdev > /proc/sys/kernel/hotplug
$ mdev -s

O primeiro comando serve para configurar o processo que irá receber os eventos do kernel. O segundo comando serve para inicializar o /dev no boot com os dispositivos listados no /sys.

Obs: Antes de executar estes comandos, certifique-se de que os diretórios /proc, /sys e /dev estejam montados e disponíveis.

ALTERANDO O DEVICE DRIVER

Para quem não acompanhou, desenvolvemos um device driver para controlar os leds do kit FriendlyARM mini2440 neste artigo aqui. Agora, iremos alterá-lo para criar as entradas necessárias no /sys e enviar eventos ao udev (no nosso caso mdev), para que este gere automaticamente os arquivos de dispositivo em /dev.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/* class for the sysfs entry */
struct class *leds_class;
 
/* driver initialization */
int __init leds_init(void)
{
    int ret, i;
 
    /* request device major number */
    if ((ret = alloc_chrdev_region(&leds_dev_number, 0, NUM_LEDS, DEVICE_NAME) < 0)) {
        printk(KERN_DEBUG "Error registering device!\n");
        return ret;
    }
 
    /* create /sys entry */
    leds_class = class_create(THIS_MODULE, DEVICE_NAME);
 
    /* init leds GPIO port */
    initLedPort();
 
    /* init each led device */
    for (i = 0; i < NUM_LEDS; i++) {
 
        /* init led status */
        leds_dev[i].number = i + 1;
        leds_dev[i].status = LED_OFF;
 
        /* connect file operations to this device */
        cdev_init(&leds_dev[i].cdev, &leds_fops);
        leds_dev[i].cdev.owner = THIS_MODULE;
 
        /* connect major/minor numbers */
        if ((ret = cdev_add(&leds_dev[i].cdev, (leds_dev_number + i), 1))) {
            printk(KERN_DEBUG "Error adding device!\n");
            return ret;
        }
 
        /* init led status */
        changeLedStatus(leds_dev[i].number, LED_OFF);
 
        /* send uevent to udev to create /dev node */
        device_create(leds_class, NULL, MKDEV(MAJOR(leds_dev_number), i), NULL, "leds%d", i);
    }
 
    printk("Leds driver initialized.\n");
 
    return 0;
}
 
/* driver exit */
void __exit leds_exit(void)
{
    int i;
 
    /* delete devices */
    for (i = 0; i < NUM_LEDS; i++) {
        device_destroy(leds_class, MKDEV(MAJOR(leds_dev_number), i));
        cdev_del(&leds_dev[i].cdev);
    }
 
    /* destroy class */
    class_destroy(leds_class);
 
    /* release major number */
    unregister_chrdev_region(leds_dev_number, NUM_LEDS);
 
    printk("Exiting leds driver.\n");
}

Na linha 2 declaramos o ponteiro que irá armazenar a estrutura com as informações do dispositivo no /sys. Na linha 16, com a função class_create(), criamos esta estrutura. Esta chamada criará a entrada /sys/class/leds.

Na linha 42, a função device_create() é chamada para cada led. É na chamada desta função que o kernel irá enviar uma notificação de evento para o mdev. Ao receber este evento, o mdev irá automaticamente criar um arquivo de dispositivo no /dev.

Ao inserir o módulo, serão criadas automaticamente as seguintes entradas:

$ ls -la /sys/class/leds/
total 0
drwxr-xr-x    6 root     root             0 Oct 29 17:46 .
drwxr-xr-x   29 root     root             0 Oct 29 17:46 ..
drwxr-xr-x    2 root     root             0 Oct 29 17:46 leds0
drwxr-xr-x    2 root     root             0 Oct 29 17:46 leds1
drwxr-xr-x    2 root     root             0 Oct 29 17:46 leds2
drwxr-xr-x    2 root     root             0 Oct 29 17:46 leds3
$ ls -la /dev/leds*
crw-rw----    1 root     root      251,   0 Jan 21  2011 /dev/leds0
crw-rw----    1 root     root      251,   1 Jan 21  2011 /dev/leds1
crw-rw----    1 root     root      251,   2 Jan 21  2011 /dev/leds2
crw-rw----    1 root     root      251,   3 Jan 21  2011 /dev/leds3

Da mesma forma, ao remover o modulo, a função leds_exit() será executada, e a chamada à device_destroy() na linha 60 fará com que o kernel envie uma mensagem para o mdev remover os arquivos de dispositivo previamente criados.

Vejam que, com poucas linhas de código, conseguimos tornar dinâmico o gerenciamento de arquivos de dispositivo do nosso device driver.

No próximo artigo iremos aprender sobre mais algumas funções da API kernel, usar timers e transformar os leds em pisca-piscas. Até lá!

Um abraço,

Sergio Prado

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.