User Space Device Drivers no Linux – Parte 1
- por Sergio Prado
Desenvolver um driver para Linux requer conhecimentos multidisciplinares. Quem já participou de uma seção do meu treinamento de drivers sabe muito bem do que estou falando…:)
Sim, você precisa saber ler datasheets e escovar bits. Precisa saber (muito bem) linguagem C, principalmente trabalhar com ponteiros e estruturas de dados mais complexas. Precisa conhecer também arquitetura de sistemas operacionais, e entender como o Linux escalona processos, gerencia a memória e controla o acesso aos dispositivos de I/O. Precisa conhecer os múltiplos sub-sistemas do kernel e sua API, frameworks e infraestruturas de barramento. Enfim, não será do dia para a noite que você se transformará em um desenvolvedor de drivers para o kernel Linux. O processo de aprendizado vai exigir de você muita força de vontade e perseverança.
Mas nem tudo são drivers em kernel space… Sim, é possível escrever uma aplicação em Linux que possa acessar diretamente um dispositivo de hardware! À essa aplicação (ou biblioteca) damos o nome de user space device driver.
E FUNCIONA?
Sim, funciona! E tem algumas vantagens:
- Mais fácil de implementar, já que temos acesso às ferramentas e bibliotecas que estamos acostumados a usar no desenvolvimento de aplicações.
- Pode ser desenvolvido em qualquer linguagem de programação: C, C++, até Java e Python!
- Qualquer bug no driver rodando em espaço de usuário não vai impactar o funcionamento do kernel.
- Processo de debugging fica bem mais fácil.
- Facilita a distribuição e manutenção do driver, já que o mesmo binário pode rodar em diferentes versões do kernel.
- A memória de processo pode ir para swap (o que também pode ser uma desvantagem!).
- Se você precisa escrever um driver proprietário, pode evitar problemas com licenças de módulos do kernel.
- O driver pode ter processamento intensivo, pois em user space sempre haverá preempção.
- Um driver em user space tem acesso completo ao sistema de arquivos.
Mas se fossem apenas vantagens, não existiriam drivers rodando em kernel space! Drivers que rodam em espaço de usuário possuem algumas desvantagens:
- Dependendo do acesso realizado pelo driver pode ser necessário ter privilégios de root.
- Se não for feito da forma correta, é possível ter problemas de segurança.
- Maior latência no tratamento de eventos e overhead de processamento devido às trocas de contexto, cópias de buffers entre kernel space e user space, swap de páginas de memória em disco, etc.
- Mais difícil de implementar acesso concorrente aos dispositivos de hardware (o driver deve ter uma arquitetura cliente/servidor).
- Sem as vantagens de ter um driver mantido pela comunidade (manutenção, revisão de código, etc).
- Problemas de portabilidade do driver em diferentes sistemas, já que cada driver poderá ter seu padrão de comunicação com as camadas mais altas da aplicação.
De qualquer forma, pela facilidade na implementação, um driver em user space pode ser a solução para aquele problema simples que você precisa resolver. Vamos ver então como isso funciona?
COMO FUNCIONA?
O kernel provê alguns mecanismos de acesso direto à um dispositivo de hardware por uma aplicação, dentre eles:
- Mapeamento de arquivo em memória com a função mmap().
- Userspace I/O (UIO).
- Frameworks do kernel (GPIO, USB, I2C, SPI).
Vamos dar uma olhada nestes mecanismos separadamente.
MMAP
O mmap é uma chamada de sistema que possibilita o mapeamento de um arquivo em memória. Tá, mas para que serve isso?
Um exemplo. Todo sistema Linux possui normalmente o arquivo /dev/mem. Esse arquivo é exportado por um módulo do kernel, e possibilita o acesso direto à memória física do hardware! Isso significa que, se você abrir este arquivo e escrever na posição 0 dele, estará escrevendo na primeira posição da memória física do seu hardware! Você precisa ter privilégios de root para executar tal operação.
Desta forma, fica fácil trabalhar com dispositivos de hardware que mapeiam seus registradores em memória. É só identificar o endereço do registrador mapeado em memória, abrir e indexar o /dev/mem, e voilá! Você tem acesso ao seu dispositivo de hardware diretamente de uma aplicação.
Mas abrir, indexar e acessar um arquivo é um procedimento bem mais complicado que simplesmente referenciar um ponteiro. Ele exige o uso da API de acesso à arquivos (open(), read(), write(), lseek(), etc), além de adicionar um overhead de processamento, já que toda operação de acesso à arquivo irá gerar uma chamada de sistema, passando obrigatoriamente pelo kernel.
E se você pudesse ter um ponteiro para referenciar diretamente esta região de memória? É aí que entra a chamada de sistema mmap(). Ela possibilita o mapeamento de qualquer região de um arquivo em memória!
Veja por exemplo o código abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#define MAP_SIZE 4096UL #define MAP_MASK (MAP_SIZE - 1) void main(void) { unsigned int *reg; void *map_base; int fd; off_t target = 0x53F04000; fd = open("/dev/mem", O_RDWR | O_SYNC); map_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, target & ~MAP_MASK); reg = (int *)(map_base + (target & MAP_MASK)); reg &= 0x01; munmap(map_base, MAP_SIZE); close(fd); } |
Na linha 11 o arquivo /dev/mem é aberto. Na linha 13 mapeio a região de memória que se encontra o registrador 0x53F04000, e na linha 17 eu seto o bit 1 deste registrador! Fácil!
Este mecanismo permite que uma aplicação acesse qualquer dispositivo de I/O mapeado em memória.
Obs: o exemplo acima não verifica o retorno das funções para simplificar o código. Se você for usar este código como base, não se esqueça de verificar o retorno das funções!
O acesso ao framebuffer por exemplo também funciona desta forma. Os drivers de interfaces de vídeo exportam um arquivo em /dev/fbX, possibilitando que uma aplicação ou driver em user space acesse diretamente a memória de vídeo do display.
Existe até uma ferramenta chamada devmem2 (ou devmem no Busybox) para facilitar este acesso na linha de comandos. Por exemplo, o comando abaixo escreve 0x10 no registrador 0x550F00010:
$ devmem2 0x550F00010 b 0x10 |
Para mais informações sobre a chamada de sistema mmap(), dê uma olhada em sua página de manual:
$ man mmap |
O uso do mmap para desenvolver um driver em espaço de usuário tem algumas deficiências. Se você precisar de acesso simultâneo ao dispositivo, precisará implementar um mecanismo cliente/servidor (é o que os drivers de placa de vídeo em user space implementam). Seu uso, se não for bem implementado, pode deixar o sistema inseguro e instável. Além disso, esta interface não provê nenhum mecanismo para tratar interrupções.
É aí que entra o Userspace I/O (UIO).
UIO
O UIO é um framework do kernel, disponível desde a versão 2.6.23, que possibilita acesso direto à um dispositivo de hardware, com suporte básico ao processamento de interrupções.
É composto por um pequeno componente em kernel space responsável pelo mapeamento de memória e pelo registro das interrupções, deixando a lógica do driver para ser implementada em user space. E diferentemente do mmap, permite mapear apenas a memória do dispositivo, e não de todo o sistema. Portanto, é mais seguro de usar.
Mas como ele funciona? Ele exporta um arquivo em /dev/uioX (onde X é o número do device) possibilitando mapear e acessar a memória do dispositivo em espaço de usuário, também via mmap(). E permite capturar interrupções lendo este mesmo arquivo, via funções read() ou select() por exemplo. Uma biblioteca chamada libuio esta disponível para facilitar o uso deste framework.
Veja o exemplo abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void main(void) { int32_t irq_count, enable_int = 1; void *map_addr int fd; fd = open("/dev/uio0", O_RDWR); map_addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); write(fd, &enable_int, 4); while (read(fd, &irq_count, 4) == 4) { printf("Interrupted!"); } munmap(map_addr, 4096); close(fd); } |
Na linha 7 o arquivo é aberto e na linha 9 o arquivo é mapeado em memória, retornando na variável map_addr um ponteiro para acessar diretamente a região de memória do dispositivo de hardware. Na linha 12 a interrupção é habilitada e na linha 14 o processo bloqueia esperando por uma interrupção.
Obs: este exemplo também não verifica o retorno das funções para simplificar o código. Se você for usar este código como base, não se esqueça de verificar o retorno das funções!
O UIO tem alguma deficiência? Sim, algumas. Ele só funciona quando o acesso ao dispositivo pode ser mapeado em memória. E não funciona com alguns tipos de dispositivos que precisam se integrar às camadas do kernel como placas de rede, dispositivos seriais, dispositivos USB e dispositivos de bloco. Além disso, o tempo de resposta para atender uma interrupção em user space pode ser um impeditivo, e ele não suporta DMA.
Na segunda parte deste artigo veremos como alguns frameworks do kernel para acesso à interfaces de hardware e barramentos (GPIO, USB, SPI, I2C, etc) possibilitam o desenvolvimento de drivers em user space.
Até lá!
Sergio Prado