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
}
}