Sunday, July 12, 2009

CES 33 - Sistemas Operacionais

A matéria "CES33 - Sistemas Operacionais" é ministrada no curso de Engenharia da Computação do Instituto Tecnológico de Aeronáutica - ITA.

Este ano, 2009, esta cadeira esta sendo ministrada pelo professor Edgar Toshiro Yano.

A pedido do professor, cada aluno deve postar em um blog os resultados das experiências de laboratório realizadas durante o curso.

Este espaço corresponde às experiências realizadas por mim.

Mãos à obra!

Saturday, July 11, 2009

Projetando um device-driver USB para o Linux

Neste post iremos discutir os fundamentos necessários para se realizar o projeto de um device-driver USB para o Linux.

Este post foi elaborado em dupla, juntamente com o aluno José Gerardo Arruda Júnior.


  • A importância dos device drivers
Os device drivers são componentes fundamentais para o funcionamento de qualquer sistema operacional (SO). A principal função de um device driver é traduzir as chamadas realizadas pelo SO a um determinado dispositivo através da implementação de uma interface definida pelo próprio SO, que estabelece como é que o controle de um dispositivo deve ser realizado. Dessa forma, os device-drivers podem ser entendidos como os responsáveis pelo controle dos periféricos conectados a um computador, tais como HDs, mouses, teclados, monitores, placas de vídeo e rede, dispositivos USB, etc... Exemplos de operações implementadas pelos drivers são: Open, Release, Write, Read, Init e Cleanup.

Eles atuam como uma camada de abstração entre os dispositivos de hardware e o SO, ou aplicativos, e simplificam a programação, tornando possível a escrita de softwares independentes do tipo de hardware que está sendo utilizado (para citar um rápido exemplo, ao escrever um programa que necessita enviar um comando de impressão para uma impressora, é desejável que o fato da impressora ser da HP ou da Lexmark não faça diferença).

Além de prover uma camada de abstração e de mapear as chamadas do SO ao hardware, os device drivers também são importantes por otimizarem o desempenho de operações de IO, incluir tratamento de erro a nível de hardware e software e permitir acesso paralelo, por diversos dispositivos, ao hardware.

  • Arquitetura de device drivers do Linux
Figura 1. Diagrama esquemático da arquitetura de device drivers do Linux

No Linux, os device drivers se comportam como "caixas pretas" responsáveis por fazer com que determinado componente de hardware responda às chamadas de uma API bem definida, que é a interface de comunicação entre os drivers e o SO, já vista anteriormente.

A arquitetura de device drivers no Linux apresenta modularidade, ou seja, os drivers são construídos separadamente do restante do Kernel e, quando necessário, são "plugados" em tempo de execução. Essa modularidade facilita o desenvolvimento de drivers para Linux.

Assim, podemos dizer que no Linux os device drivers são representados como módulos, que são programas que estendem a funcionalidade do Kernel e que realizam sua comunicação através de chamadas de funções/métodos das interfaces públicas disponibilizadas por eles.

  • O que é desejável de um device-driver?

Alguns dos requisitos essenciais de um device driver vistos até agora foram: implementação de uma interface disponibilizada pelo SO para mapear as requisições do SO ao hardware, fornecimento de uma camada de abstração ao programador, otimização de desempenho de operações de IO, tratamento de erros a nível de software e hardware, e gerenciamento de acesso paralelo, por diversos dispositivos, ao hardware. Contudo, falta explicitar uma característica muito importante desejável aos device drivers, que é a flexibilidade.

A palavra flexibilidade enfatiza o papel do device driver de prover um mecanismo e não uma política. A idéia de mecanismo vem de "Quais funcionalidades devem ser fornecidas?" enquanto a de política vem de "Como essas funcionalidades podem ser utilizadas?" .

Dessa forma, quando se está desenvolvendo um driver, o programador deve estar atento ao conceito de escrever código para acessar o hardware, mas não forçar políticas ao usuário, uma vez que diferentes usuários possuem diferentes necessidades. Dessa forma, o driver deve lidar com o problema de tornar o hardware acessível, deixando o problema de como usar o hardware para o SO/aplicativos.


  • O que é necessário para poder desenvolver um device-driver no Linux?
Inicialmente, em termos de pré-requisitos, para desenvolver device drivers para o Linux, é necessário que se tenha algum conhecimento mais aprofundado em programação C, (como ponteiros, funções de manipulação de bits, etc) e programação de microprocessadores (saber como os microcomputadores funcionam internamente: endereçamento de memória, interrupções, etc).

Em termos práticos, de ambiente de desenvolvimento, a partir da versão 2.6.x do Kernel do Linux, a compilação de drivers tornou-se um pouco mais complicada do que era anteriormente, tornando-se necessário que se tenha a árvore de código fonte do kernel completa compilada, uma vez que é necessário compilar o módulo (driver) utilizando-se o mesmo kernel no qual você irá instalar o seu módulo (driver) (ver abaixo).

Vamos descrever aqui como fazer para criar e compilar um device driver bem simples (inútil na verdade, mas serve a título de exemplo), a ser incorporado ao kernel como um módulo.

Primeiramente crie um arquivo .c (driverTest.c) com o seguinte conteúdo:

#include [linux/module.h]
#include [linux/init.h]
#include [linux/kernel.h]
MODULE_LICENSE("GPL")
static
int hello_init(void){
printk(" [1] Hello World!\n");
return
0;
}


static
void hello_exit(void){
printk(" [1] Bye World!\n");
}


module_init(hello_init);
module_exit(hello_exit);
Explicando um pouco do código:
Quando um device driver é carregado no kernel, algumas tarefas preliminares são executadas, como resetar o dispositivo, reservar memória RAM, reservar interrupções, reservar ports de IO, etc. Essas tarefas são executadas em kernel-space por duas funções que precisam estar presentes (e explicitamente declaradas) no código do seu módulo (driver): module_init e module_exit. Elas correspondem aos comandos insmod e rmmod do user-space (ver abaixo), que são utilizados para instalar e remover módulos do kernel, respectivamente.

As funções hello_init e hello_exit podem receber qualquer nome. Contudo, para que elas sejam identificadas como sendo as funções de instalação e remoção do driver, elas precisam ser passadas como parâmetro para as funções module_init e module_exit. É isso que está acontecendo no nosso exemplo.

A função printk é bem parecida com a conhecida função printf, com a diferença que ela só funciona dentro do kernel.

Para compilar o seu módulo vc precisa gerar um makefile
(chame-o de Makefile) com o seguinte conteúdo:

obj-m := driverTest.o

e para compilar o módulo digite (assumindo que a versão do kernel utilizado é 2.6.8):

$ make -C /usr/src/kernel-source-2.6.8 M=pwd modules

Esse módulo agora pertence ao kernel-space e irá se tornar parte dele assim que for carregado.


  • Como se instalar um device-driver USB?
Assim como qualquer outro módulo carregável, no user-space, como root, pode-se instalar o módulo (driver) através do comando insmod, que permite a instalação do módulo (driver) no kernel.

Para remover o módulo (driver) do kernel utiliza-se o comando rmmod.

Através do comando lsmod é possível verificar se o módulo (driver) foi instalado/desinstalado corretamente.

  • Descrevendo uma implementação de driver USB
Figura 2. Diagrama representativo do suporte oferecido pelo Linux à tecnologia USB

A necessidade de facilitar a conexão de dispositivos periféricos a um computador caracteriza um dos principais motivos que acarretaram na criação da tecnologia USB (Universal Serial Bus).

Inicialmente especificada em 1996 e muito difundida na atualidade, a tecnologia USB apresenta diversas características muito vantajosas como por exemplo: a padronização de conexão, utilização de tecnologia Plug and Play, alimentação elétrica própria, conexão de vários aparelhos ao mesmo tempo (é possível conectar até 127 dispositivos ao mesmo tempo em uma única porta USB), ampla compatibilidade (relativo a plataformas e sistemas operacionais), cabos longos (até cinco metros de comprimento), hot-swap (possibilidade de conectar e desconectar o dispositivo com o computador ligado).

O protocolo de comunicação entre os dispositivos conectados via USB é tal que o host, ou o computador ou equipamento que recebe as conexões, emite um sinal para encontrar os dispositivos conectados e estabelece um endereço para cada um deles.
Uma vez estabelecida a comunicação, o host recebe informação sobre qual tipo de conexão o dispositivo conectado utiliza.
Há quatro tipos distintos de conexões USB, a saber: Bulk (utilizado por dispositivos que manipulam grandes volumes de informações, como scanners e impressoras), Control (utilizado para controle e configuração de dispositivos), Interrupt (utilizado para dispositivos que manipulam/transferem poucos dados, como teclados e mouses) e Isochronous (utilizado em transmissões contínuas, onde os dados são transferidos a todo momento, como em caixas de som).

Na figura 2, temos um esquema representativo do suporte oferecido pelo Linux à tecnologia USB. Na figura podemos ver o USB Core, que é um subsistema do kernel que fornece uma API específica para suportar dispositivos USB e controladores host. A função do USB Core é abstrair todo o hardware ou partes dependentes de dispositivos através da definição de estruturas de dados, macros e funções.

Para exemplificar como isso é feito, podemos apresentar algumas das funções mais utiilzadas, como por exemplo a função interface_to_usbdev() que
muitos drivers utilizam para acessar sua estrutura usb_device a partir da estrutura usb_interface, que é fornecida pelo USB Core.

Na API do USB Core, dispositivos são representados pela estrutura usb_device e drivers USB devem definir uma estrutura usb_driver como a seguir:

//Nome único ao driver.
//Normalmente utiliza-se o nome do módulo.
const char *name;
const struct
usb_device_id *id_table;

int
(*probe)
(
struct usb_interface *intf,
const struct
usb_device_id *id);

void
(*disconnect)
(
struct usb_interface *intf);

Para se registrar o seu driver, deve-se utilizar a função usb_register(), como no exemplo a seguir:
static struct usb_driver mtouchusb_driver = {
.
name = "mtouchusb",
.
probe = mtouchusb_probe,
.
disconnect = mtouchusb_disconnect,
.
id_table = mtouchusb_devices,
};


static
int __init mtouchusb_init(void){
dbg("%s - celled", __FUNCTION__);
return
usb_register(&mtouchusb_driver);
}

e para se desregistrar o driver deve-se utilizar usb_deregister(), como a seguir:
static int __exit mtouchusb_cleanup(void){
dbg("%s - celled", __FUNCTION__);
return
usb_deregister(&mtouchusb_driver);
}

Vale ressaltar que o código do kernel do Linux direcionado para USB se comunica com todos os dispositivos USB através de algo chamado urb (USB request block). Esse bloco é descrito com a estrutura struct_urb e pode ser encontrada no arquivo /linux/usb.h.

Para saber mais respeito de como utilizar a API do USB Core, recomendo [12] e [13]

  • Como se testar um device-driver?
Executar testes em device drivers pode fazer com que seu sistema se comporte de maneira inesperada e possivelmente danificar o kernel. A seguir estão descritos alguns passos aconselháveis a serem seguidos no momento de testar seu device driver
.

1) Instale o seu driver em um local temporário:
Instale os drivers no diretório /tmp até que se tenha concluído as modificações e testes das rotinas __info(), __init() e attach(). Copie o binário do seu driver no diretório /tmp e faça o link para o driver do diretório de drivers do kernel.
2) Utilize uma conexão serial para controlar sua máquina de testes através de um sistema host separado.
3) Utilize um kernel alternativo.
4) Utilize um kernel adicional para realizar experimentos com configurações diferentes de variáveis de kernel.
5) Construir planos de contingência para potenciais perdas de dados durante a execução dos testes do sistema.


Referências:
Todos os sites utilizados como referência foram visitados no dia 12 de Julho de 2009.

[1] http://en.wikipedia.org/wiki/Device_driver

[2] http://www.inf.pucrs.br/~eduardob/disciplinas/ProgPerif/sem09.1/trabalhos/Seminarios/g3/Device%20Drivers%20no%20Windows%20e%20Linux.doc

[3] http://www.linuxjournal.com/article/2476

[4] http://www.lrr.in.tum.de/Par/arch/usb/usbdoc/

[5] http://lwn.net/Kernel/LDD3/

[6] http://www.freesoftwaremagazine.com/articles/drivers_linux?page=0%2C0

[7] http://matthias.vallentin.cc/2007/04/writing-a-linux-kernel-driver-for-an-unknown-usb-device/

[8] http://linux.about.com/od/commands/l/blcmdl8_insmod.htm

[9] http://linux.about.com/library/cmd/blcmdl8_rmmod.htm

[10] http://linux.about.com/library/cmd/blcmdl8_lsmod.htm

[11] http://www.infowester.com/usb.php

[12] http://free-electrons.com/docs/linux-usb/

[13] http://www.lrr.in.tum.de/Par/arch/usb/usbdoc/node18.html

[14] http://docs.sun.com/app/docs/doc/819-3159/fsfqv?a=view

[15] http://docs.sun.com/app/docs/doc/819-3159/fdlbq?a=view