Implementação e Gerenciamento de Drivers PCI e de Caractere em Rust para o Kernel Linux

A preparação do ambiente para compilação do kernel com suporte a Rust exige a ativação das opções apropriadas no sistema de configuração. Inicie gerando a configuração padrão para a arquitetura alvo. Em seguida, execute o menu de configuração especificando o uso do backend LLVM. Caso o vinculador lld não seja encontrado no caminho do sistema, instale-o via gerenciador de pacotes. Finalmente, inicie o processo de compilação paralela.

make x86_64_defconfig
make LLVM=1 menuconfig
sudo apt-get install -y lld
make LLVM=1 -j$(nproc)

Para compilar um módulo fora da árvore principle do kernel, o arquivo Makefile deve definir o alvo como objeto de módulo. Isso permite que o compilador gere o arquivo de objeto do kernel de forma independente. Após a compilação, o módulo pode ser inserido na tabela de drivers do sistema, permitindo acesso direto ao espaço de endereçamento privilegiado. Para testar a funcionalidade de rede, é necessário desativar o driver nativo, carregar o novo componente e configurar a interface de rede manualmente.

obj-m += modulo_rede_rust.o

insmod modulo_rede_rust.ko
ip link set eth0 up
ip addr add 192.168.56.10/24 dev eth0
ip route add default via 192.168.56.1
ping -c 4 192.168.56.2

O gerenciamento correto do ciclo de vida de um driver PCI é fundamental para evitar vazamentos de recursos e instabilidades no kernel. A rotina de remoção deve liberar as regiões de memória mapeadas, desregistrar a estrutura do dispositivo de rede e cancelar o manipulador de interrupções. O mecanismo do kernel invoca um callback que passa a estrutura do dispositivo PCI ao driver. Utilizando essa referência, é possível acessar os recursos alocados durante a fase de inicialização e revertê-los de forma segura.

Para o manipulador de interrupções, o kernel fornece uma estrutura de registro encapsulada. Embora a implementação interna possua a lógica de destruição, o tipo wrapper público pode exigir uma implementação explícita para garantir a chamada segura da rotina de liberação de IRQ. No método de parada, deve-se interromper a fila de transmissão, indicar a perda de conexão e liberar o registro de interrupção antes de desalocar o dispositiov.

impl Drop for RegistroIrq<T: Manipulador> {
    fn drop(&mut self) {
        unsafe {
            bindings::free_irq(self.identificador, self.dados);
            T::extrair_dados(self.dados);
        }
    }
}

fn desativar_driver(device: &pci::Device) {
    unsafe { bindings::netif_stop_queue(device.rede) };
    unsafe { bindings::netif_carrier_off(device.rede) };
    
    for bar in &device.mapeamentos {
        unsafe { bindings::pci_iounmap(bar.endereco) };
    }
    
    if let Some(registro) = device.ir.take() {
        drop(registro);
    }
    
    unsafe { bindings::unregister_netdev(device.rede) };
}

A criação de um dispositivo caractere requer a alocação explícita ou implícita de um número major. Ao listar os arquivos de dispositivo, é possível observar a associação entre o identificador major e o driver registrado. O sistema mantém esse mapeamento em proc/devices. Durante a definição do módulo via macro, o parâmetro de nome vincula automaticamente o identificador major ao arquivo de dispositivo, permitindo a interação do espaço de usuário.

stat -c "%t" /dev/dispositivo_rust
cat /proc/devices | grep rust_chrdev

r4l_module! {
    type: DispositivoRust,
    name: "rust_chrdev",
    major: 248,
}

Para validar a comunicação bidirecional no espaço de usuário, o sistema de arquivos pode ser montado via NFS no ambiente virtualizado. A estrutura de sincronização entre as operações de leitura e escrita deve ser compartilhada entre todas as instâncias de abertura do arquivo. Definir primitivas de sincronização como campos do estado de cada arquivo aberto resulta em condições de corrida e bloqueios permanentes, pois cada chamada de open isolraia o semáforo. A solução consiste em elevar essas primitivas ao escopo global do módulo, utilizando um Mutex para proteção e um Condvar para sinalização de dados disponíveis. As funções de E/S devem implementar a lógica de espera bloqueante conforme a disponibilidade de dados no buffer interno.

mount -t nfs -o nolock,vers=3 HOST:/caminho/absoluto /mnt/ponto

use std::sync::{Mutex, Condvar};
use std::sync::atomic::{AtomicBool, Ordering};

static PRIMITIVAS_IO: (Mutex<Vec<u8>>, Condvar) = (Mutex::new(Vec::new()), Condvar::new());
static DADOS_PRONTOS: AtomicBool = AtomicBool::new(false);

fn escrever_usuario(buf: &[u8]) -> isize {
    let mut dados = PRIMITIVAS_IO.0.lock().unwrap();
    dados.clear();
    dados.extend_from_slice(buf);
    DADOS_PRONTOS.store(true, Ordering::SeqCst);
    PRIMITIVAS_IO.1.notify_one();
    buf.len() as isize
}

fn ler_usuario(dest: &mut [u8]) -> isize {
    let (mutex, cvar) = &PRIMITIVAS_IO;
    let mut dados = mutex.lock().unwrap();
    
    while dados.is_empty() && !DADOS_PRONTOS.load(Ordering::SeqCst) {
        dados = cvar.wait(dados).unwrap();
    }
    
    let copia = std::mem::take(&mut dados);
    DADOS_PRONTOS.store(false, Ordering::SeqCst);
    
    if copia.len() <= dest.len() {
        dest[..copia.len()].copy_from_slice(&copia);
        copia.len() as isize
    } else {
        -1
    }
}

Tags: Rust linux-kernel pci-driver char-device Synchronization

Publicado em 10-11 07:14