Device Drivers e gerenciamento dinâmico de dispositivos
- por Sergio Prado
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:
- 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).
- 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:
- 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.
- 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