Rust para sistemas de arquivos
(lwn.net)- Na LSFMM+BPF Summit de 2024, foram discutidas formas de aplicar Rust aos sistemas de arquivos do Linux, e o segundo patch RFC publicado após o RFC de dezembro de 2023 virou o centro do debate
- O lado do Rust-for-Linux busca expressar os requisitos das APIs de sistemas de arquivos no sistema de tipos do Rust, visando detectar erros em tempo de compilação, automatizar a limpeza de recursos e reduzir vulnerabilidades relacionadas à memória
- O caso de
iget_locked()mostra a direção em queget_or_create_inode()em Rust tenta impor, por meio de tipos e limpeza automática, verificações de null, distinção de estado de inode e tratamento de falhas que antes ficavam a cargo do chamador em C - Dave Chinner, Christian Brauner, James Bottomley, Ted Ts'o e outros manifestaram preocupação com a divergência de nomes entre as APIs C e Rust, a sincronização das APIs, diferenças no ciclo de vida dos objetos e o ônus de manutenção considerando mais de 50 sistemas de arquivos
- O ponto central do conflito não está tanto nas vantagens das abstrações em Rust em si, mas em quem arcará com o custo de manter bindings e abstrações Rust alinhados à medida que o código C continuar mudando
Sessão sobre Rust para sistemas de arquivos na LSFMM+BPF
- Na Linux Storage, Filesystem, Memory Management, and BPF Summit de 2024, Wedson Almeida Filho e Kent Overstreet trataram do uso de Rust em sistemas de arquivos do Linux
- Almeida publicou em dezembro de 2023 um conjunto de patches RFC de abstrações Rust para sistemas de arquivos, e houve divergências em torno dessa abordagem
- No mesmo dia, em meados de maio, em que a sessão foi realizada, Almeida publicou a segunda versão RFC do patch, com a intenção de discuti-la junto com outros temas relacionados a Rust
Objetivo das abstrações de sistemas de arquivos do Rust-for-Linux
- As abstrações de sistemas de arquivos propostas refletem a direção buscada pelo projeto Rust-for-Linux
- O ponto central é expressar mais requisitos das APIs de sistemas de arquivos por meio do sistema de tipos do Rust, capturando erros em tempo de compilação
- A proposta também tenta automatizar tarefas difíceis de oferecer facilmente em código C
- Ex.: limpeza de recursos
- O objetivo é tornar a experiência de desenvolvimento de sistemas de arquivos mais produtiva, reduzir o tempo gasto depurando problemas que o compilador poderia encontrar e diminuir vulnerabilidades relacionadas à memória
- Overstreet disse ter passado por bugs no bcachefs que exigiram duas semanas de investigação em excesso, e vê Rust como algo que oferece mais do que C
- Rust elimina comportamento indefinido
- Fornece recursos para enxergar o que acontece dentro do código
- Se for possível provar a correção do código Rust, ele acredita que haverá muito menos bugs bloqueando o desenvolvimento de funcionalidades
Exemplo do sistema de tipos em torno de iget_locked()
- Almeida deu, em seus slides, o exemplo da
iget_locked()atual do kernel como uma função com requisitos complexos - O chamador em C precisa tratar várias condições manualmente
- Precisa verificar se o valor retornado é null
- Precisa verificar se a
struct inoderetornada é um inode novo ou existente - Se for um inode novo, precisa inicializá-lo antes do uso
- Se a inicialização falhar, precisa chamar
iget_failed()
- Al Viro não concordou com parte dos requisitos do chamador de
iget_locked()nos slides de Almeida, e seguiu-se uma discussão sobre detalhes do comportamento - Overstreet vê essas regras encapsuladas em tipos e abstrações Rust como algo que permitiria ao compilador impor o tratamento correto
- A função correspondente em Rust apresentada por Almeida foi
get_or_create_inode()- Assim como em C, é preciso verificar se houve falha
- Em caso de sucesso, o chamador recebe um inode comum com contagem de referências ou um inode novo
- No caso de um inode comum, a contagem de referências é reduzida automaticamente quando o objeto deixa de ser referenciado
- No caso de um inode novo, se ele não for inicializado, o tratamento correspondente a
iget_failed()é chamado automaticamente - Quando um inode novo é inicializado uma vez, ele passa a ser um inode comum, e então se aplica a redução automática da contagem de referências
- Esses comportamentos são impostos pelo sistema de tipos
- Viro questionou onde, no código-fonte real, essas restrições seriam definidas
- Almeida respondeu que pretende entender as restrições com Viro e outros desenvolvedores de sistemas de arquivos e então criar tipos e abstrações que as imponham
A ruptura entre as APIs C e Rust
- Dave Chinner avalia que, se as APIs C e Rust tiverem nomes diferentes, será difícil para desenvolvedores existentes olharem o código C e saberem qual é a chamada correspondente em Rust
- Também surgiu a preocupação de que, sem usar os mesmos nomes, ela possa se tornar uma API completamente estranha para a comunidade de desenvolvimento existente
- Como o código C continuará mudando, o código Rust também terá de acompanhar, e permanece a questão de quem assumirá esse trabalho
- Almeida reconheceu que é um ponto que precisa ser discutido
- Ele não se opõe a mudanças de nomes
- Mas disse não considerar
iget_locked()um bom nome, e que isso também poderia ser visto como uma oportunidade de criar um nome melhor
- Viro considerou ruim a escolha do exemplo, pois
iget_locked()é uma função de biblioteca, não um método membro do objeto superblock - Almeida respondeu que
get_or_create_inode()também pode ser transformada em função de biblioteca, e que o exemplo buscava mostrar como codificar restrições em tipos
Escolha entre abstração genérica e abordagem centrada em sistemas de arquivos simples
- Christian Brauner disse que primeiro é preciso decidir se as abstrações Rust serão abstrações genéricas para todos os sistemas de arquivos do kernel ou centradas nos recursos necessários para sistemas de arquivos mais simples escritos em Rust
- No longo prazo, se uma função como
get_or_create_inode()carregar muito mais restrições queiget_locked(), isso pode causar problemas - O código C pode evoluir mais rapidamente que o código Rust, especialmente no início, então será preciso manter as duas APIs sempre sincronizadas
- Overstreet vê como questão central decidir se a adição de abstrações Rust virá acompanhada de refatoração e limpeza, e acredita fortemente que isso é necessário
- James Bottomley observa que o ciclo de vida dos objetos é codificado na API Rust, mas não há uma representação correspondente disso em C
- Se o ciclo de vida de um objeto mudar de um lado, isso pode introduzir bugs do outro
- Chinner disse que o ciclo de vida de objetos inode às vezes difere entre sistemas de arquivos
- Se uma compreensão única do ciclo de vida for inserida na API, essas funções podem não funcionar em alguns sistemas de arquivos
- Almeida respondeu que o exemplo seria usado apenas por sistemas de arquivos que atualmente chamam
iget_locked()e poderiam se beneficiar dele- Os desenvolvedores Rust não estão tentando forçar sistemas de arquivos a mudar a forma como fazem as coisas hoje
“Quem vai arcar com a dor?”
- Ted Ts'o disse que parece haver uma tentativa de converter todos à “religião” Rust, mas que o Linux tem mais de 50 sistemas de arquivos e eles não serão convertidos imediatamente
- O código C continuará sendo aprimorado, e se essas mudanças quebrarem bindings Rust, os sistemas de arquivos que dependem desses bindings também poderão quebrar
- Ts'o considera que, por enquanto, os bindings Rust são cidadãos de segunda classe, e bindings Rust quebrados são problema dos desenvolvedores do Rust-for-Linux, não de toda a comunidade de sistemas de arquivos
- Ele acredita que, com o desenvolvimento dos bindings Rust em paralelo à evolução do código C, ficará claro em um ou dois anos se a abordagem de colocar grande quantidade de semântica no sistema de tipos é boa ou ruim
- Para Ts'o, uma mudança grande é, no fim, uma questão de distribuição da dor
- Um desenvolvedor que altera a API C pode corrigir o código C afetado, mas dizer que não corrigirá os bindings Rust porque não conhece Rust
- Almeida respondeu que não está tentando congelar a API C, mas sim codificar em Rust a semântica da API quando os desenvolvedores de sistemas de arquivos a explicarem
- Bottomley vê que, quanto mais semântica for codificada nos bindings, mais frágeis eles podem se tornar do ponto de vista da sincronização
- Almeida respondeu que, quando uma API muda, seus usuários precisam ser atualizados como qualquer outro usuário
O que colocar em métodos, funções e tipos
- Viro voltou a questionar o fato de a substituta de
iget_locked()depender de um método- Para ele, ao usar métodos os argumentos não ficam explicitamente especificados
- Overstreet vê a insatisfação com métodos como algo vindo de linguagens como C++, que dependem excessivamente de herança
- Ele disse que Rust não faz isso, e que os métodos em Rust são, em grande parte, um elemento sintático
- Jan Kara distinguiu o comportamento associado ao próprio inode do comportamento embutido na função
iget_locked()- O inode tem comportamentos como contagem de referências e seu tratamento
- A função
iget_locked()tem comportamentos separados embutidos nela
- Overstreet e Almeida responderam que ambas as partes são codificadas em tipos, mas separadamente, e que outras funções que usam o tipo inode podem ter retornos com propriedades diferentes
- Viro explicou por que inodes no VFS funcionam da forma atual e concordou em começar pequeno para ver a direção a seguir
- Overstreet disse que este exemplo talvez fosse complexo demais e não fosse um bom ponto de partida; Viro respondeu “não, não é”, encerrando a sessão
1 comentários
Comentários no Hacker News
Não entendo a ideia de que cada sistema de arquivos teria um ciclo de vida de inode customizado e, ainda assim, usaria as mesmas funções de gerenciamento de ciclo de vida com apenas a semântica mudando
Se a mesma função precisa ser usada de forma diferente dependendo dos detalhes de implementação, isso soa como o oposto de uma camada de abstração
Se o ciclo de vida do inode é específico de cada sistema de arquivos, então ele deveria ser gerenciado por funções específicas de cada sistema de arquivos
Ao reunir esse tipo de informação, podem aparecer pontos que valham uma refatoração para que esse tipo de pergunta nem surja, e isso é algo bom
É uma visão geral da camada VFS que lida com comportamentos específicos de cada sistema de arquivos enquanto mantém uma interface consistente para o lado do kernel
O ciclo de vida do inode pode ter sido apenas um exemplo inicial para começar a discussão
O compilador pode ajudar com referências temporárias, mas o sistema de arquivos ainda precisa armazenar a contagem de links em disco
iget_locked()é apenas um padrão específico entre elasNem todos os sistemas de arquivos usam esse método, e em alguns casos ele não é usado dependendo da situação
Por exemplo, o FAT não usa isso porque gera números de inode e mantém seu próprio mapeamento da posição no FAT para o inode
Também existem sistemas de arquivos, como o
proc, que não armazenam em cache objetos inodeO fluxo de estados do próprio objeto inode parece ser o mesmo independentemente de sua origem, então, do ponto de vista do consumidor, a forma de usar
inodenão mudaO que muda é a forma como a camada do sistema de arquivos cria o objeto inode e o trata internamente
Acho que talvez a pergunta esteja sendo feita do jeito errado
Será que o Rust é que deveria mudar para chamar C com mais facilidade?
Já brinquei um pouco com Rust, mas, do ponto de vista de um desenvolvedor por hobby, ainda não está claro para mim como interoperar com C
Já em C++ ou Objective C, basta incluir o header correto e chamar a função
Swift pode incluir arquivos Objective C, e dali chamar C
Nesse caso, em vez de esperar que os desenvolvedores de kernel se adaptem à linguagem, talvez a linguagem Rust é que devesse ser um pouco mais flexível
Basta declarar funções externas e chamá-las
Por exemplo, como mostrado no livro de Rust em https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#usin..., é possível declarar com
extern "C"Se você não quiser escrever manualmente todas as declarações de uma biblioteca complexa, pode usar uma ferramenta como bindgen, que gera automaticamente declarações
externa partir de arquivos de header em C: https://github.com/rust-lang/rust-bindgenDá para defender que algo como o bindgen deveria vir incluído no Rust para poder ser usado sem dependências de terceiros nem configuração de
build.rs, mas esse não é o ponto principal do textoO problema não são bindings de baixo nível, e sim wrappers de alto nível no estilo Rust; e não pode existir uma ferramenta genérica que gere automaticamente esse tipo de wrapper a partir de código C arbitrário
O texto trata de encontrar formas de implementar de fato drivers de sistema de arquivos do kernel e afins em Rust
Também é importante notar que o código Rust dentro do kernel inevitavelmente consome interfaces C
Para o caso de uso imaginado, o bindgen se encaixa muito bem: https://github.com/rust-lang/rust-bindgen
Para chamar a partir de Rust, basta declarar
extern "C" fn foo() -> Te passar flags de link com o atributo#[link]ou combuild.rsVocê pode gerar os bindings antecipadamente com o crate
bindgenou criá-los embuild.rse incluí-los cominclude!()Em geral, cria-se um crate
-syscontendo apenas os bindings gerados, e o código real então usa normalmente os bindings desse crate sys comuseEm C++ e Objective C, não basta apenas incluir os headers corretos, também é preciso linkar com a biblioteca
Chamadas nos dois sentidos são possíveis, mas, se C não consegue expressar o que Rust consegue, isso tem impacto importante no desenho das APIs que os dois lados precisam compartilhar
Basta declarar a função C com
extern "C"e chamá-laNormalmente é preciso
unsafe, e talvez converter referências em ponteiros brutos ou fazer casts, mas a sintaxe em si é simplesTambém existem ferramentas que escaneiam arquivos de header em C e geram as declarações, sendo bindgen a mais usada
A questão deste texto está mais próxima de como usar Rust do que da linguagem em si
Os desenvolvedores do Rust-for-Linux querem usar os recursos e o sistema de tipos do Rust para codificar a semântica das chamadas de API de forma mais segura e menos sujeita a erros
O pessoal do lado C teme que isso possa dificultar a evolução do comportamento e da semântica da API C
Isso porque, quando a API C muda, a API Rust também precisa ser corrigida, e eles não querem assumir esse trabalho
Uma alternativa mais fácil de aceitar seria usar menos dos recursos e do sistema de tipos do Rust na hora de codificar a semântica na API Rust
Assim, quando a API C mudar, a atualização da API Rust se tornaria mecânica e simples, mas, se o Rust-for-Linux não puder usar os recursos do Rust para criar APIs melhores e mais seguras, surge a dúvida sobre qual é o sentido desse trabalho
Ainda assim, é um pouco estranho falar de forma tão conclusiva sobre esse tema ao mesmo tempo em que se admite não entender a linguagem o suficiente
Eu não tinha entendido claramente, por não conhecer bem os sistemas de arquivos do Linux, se essa API Rust envolve a API C ou se a reimplementa
Se for uma reimplementação ou uma API separada, manter exatamente os mesmos nomes da API C parece algo que vai gerar mais confusão com o tempo
Acho isso mesmo que, no começo, ajude desenvolvedores já familiarizados a entender mais rápido
iget_locked()em Rust, e o nome eraget_or_create_inode()A resposta é que se trata de uma reimplementação, e aparentemente não estão usando os mesmos nomes
Considerando a forma como esse tipo de discussão normalmente evolui e o tamanho da mudança, esta discussão foi surpreendentemente educada
Não concordo com o clima negativo deste fio
Estou até bem otimista pelo fato de os envolvidos terem conseguido transmitir com clareza, sem bobagem, quais são os principais pontos de dor
A discussão real provavelmente foi intensa, dispersa e cheia de picuinhas, como costuma acontecer em debates sobre linguagens de programação entre excêntricos de opiniões fortes
Jake Edge, que escreveu este resumo, parece ser muito bom em limpar essas partes e registrar apenas o essencial
Alguns comentários no fim da página da lwn.net são bem rudes
Basta imaginar receber um comentário como Science advances one funeral at a time em um projeto de código aberto para o qual você está contribuindo
Ter mais opções no kernel Linux é sempre algo positivo
Mas Rust pode não ser a resposta para tudo
Rust faz o possível para garantir um modelo de programação seguro, mas esse modelo também tem limites
Pode parecer que, se o problema é memória, usamos Rust, e se o problema é concorrência, trocamos para Rust, mas não dá para fazer tudo o que C faz sem blocos
unsafeRust pode oferecer uma nova perspectiva para esses problemas, mas não é uma solução completa
C, especialmente o C usado no kernel, empurra para cada pessoa a responsabilidade de conhecer completamente todas as regras implícitas
Isso não escala
Nem mesmo desenvolvedores do kernel usando a mesma estrutura de dados conseguiram chegar a um consenso total sobre essas regras numa mesma sala
Rust é forte em explicitar as regras que precisam ser conhecidas e, se outra pessoa puder garantir o cumprimento delas, fazer com que isso deixe de ser meu problema
Às vezes o resultado pode ser menos otimizado, mas mesmo no kernel Linux muitas vezes o padrão menos otimizado é o correto, e para quem puder aprender mais seis regras estranhas em troca de melhor desempenho, basta oferecer uma rota de escape com
unsafeunsafeSó é preciso usá-los quando necessário
Usar um bloco
unsafecom escopo de impacto bem limitado não faz desaparecer todas as garantias obtidas no restante do códigounsafeMas concluir que Rust não é adequado porque precisa de
unsafeé um mal-entendidoA distinção entre código seguro e inseguro em Rust existe para marcar claramente quais partes do código são inseguras, permitindo concentrar a auditoria em trechos pequenos e confiar que, se essas partes estiverem corretas, o restante vai funcionar
Existe um bom motivo para que código de sistema de arquivos precise obrigatoriamente ser
unsafe?Provavelmente é apenas um subconjunto bem pequeno, necessário em alguns poucos pontos
Não é intuitivo entender o que está acontecendo internamente
Pelas atas da reunião, Rust dentro do kernel parece um custo extra de complexidade
Se você estivesse escrevendo um sistema operacional do zero, poderia aproveitar toda a força da linguagem
Mas, ao colocá-la ao lado de uma base de código já gigantesca, surgem problemas adicionais, como se vê aqui
Basta ver o post sobre o driver de GPU em Rust do Asahi Linux ter sido criado em um mês
É só procurar por
tales of the m1 gpuno Google, e o autor tem uma visão muito negativa do Hacker NewsSe quiser, pode ler pelo link: https://asahilinux.org/2022/11/tales-of-the-m1-gpu/
Se isso vai se aplicar de forma geral, só saberemos nos próximos anos
Esse custo será visto como necessário para aceitar o futuro e o progresso
Fico me perguntando por que não limitar isso a um subconjunto seguro, em vez de mergulhar numa onda enorme de bugs desconhecidos e trade-offs
O projeto já é gigantesco demais, mas isso não significa que a inovação deva parar e ele deva entrar em modo de manutenção indefinidamente
Como nos impostos da vida real, se o custo de um lado for usado para compensar outros problemas, talvez não seja uma perda líquida
Olhar para uma única discussão ainda sem conclusão e dizer que ela não vai compensar problema nenhum parece um argumento curto demais
A parte sobre os nomes da API em C e da API em Rust não baterem, a ponto de você não conseguir olhar um código em C e saber qual é a chamada equivalente em Rust, parece uma briga com convenções antigas de nomenclatura
Já vi isso ser resolvido bem mantendo o mesmo nome e, quando se quer um nome alternativo, fazendo o nome novo envolver o antigo
Ainda assim, dar nome às coisas é difícil
Os outros dois são concorrência e erros de off-by-one