2 points par GN⁺ 2024-07-16 | 1 commentaires | Partager sur WhatsApp
  • Lors du LSFMM+BPF Summit 2024, l’usage de Rust dans les systèmes de fichiers Linux a été discuté, et la deuxième série de patchs RFC publiée après celle de décembre 2023 a été au centre des débats
  • Côté Rust-for-Linux, l’objectif est d’exprimer les exigences des API de systèmes de fichiers dans le système de types de Rust afin de détecter les erreurs à la compilation, d’automatiser le nettoyage des ressources et de réduire les vulnérabilités liées à la mémoire
  • L’exemple de iget_locked() montre l’orientation visée : get_or_create_inode() en Rust chercherait à imposer par les types et le nettoyage automatique des vérifications de nullité, la distinction d’état des inodes et la gestion des échecs que les appelants C devaient traiter eux-mêmes
  • Dave Chinner, Christian Brauner, James Bottomley, Ted Ts'o et d’autres ont exprimé des inquiétudes sur l’écart de nommage entre API C et API Rust, la synchronisation des API, les différences de cycle de vie des objets et la charge de maintenance à l’échelle de plus de 50 systèmes de fichiers
  • Le cœur du conflit porte moins sur les avantages des abstractions Rust que sur la question de savoir qui supportera la douleur d’aligner les bindings et abstractions Rust lorsque le code C continue d’évoluer

Session Rust sur les systèmes de fichiers au LSFMM+BPF

  • Lors du Linux Storage, Filesystem, Memory Management, and BPF Summit 2024, Wedson Almeida Filho et Kent Overstreet ont abordé l’usage de Rust dans les systèmes de fichiers Linux
  • Almeida avait publié en décembre 2023 une série de patchs RFC pour des abstractions Rust destinées aux systèmes de fichiers, une approche qui avait déjà suscité des divergences d’opinion
  • Le jour même de la session, à la mi-mai, Almeida a publié une deuxième version de cette RFC afin de la discuter avec d’autres sujets liés à Rust

Objectifs des abstractions de systèmes de fichiers dans Rust-for-Linux

  • Les abstractions proposées pour les systèmes de fichiers reflètent l’orientation poursuivie par le projet Rust-for-Linux
  • L’idée centrale est d’exprimer davantage d’exigences des API de systèmes de fichiers dans le système de types de Rust pour détecter les erreurs dès la compilation
  • Le projet vise aussi à automatiser des tâches difficiles à fournir proprement en C
    • Exemple : le nettoyage des ressources
  • L’objectif est de rendre le développement de systèmes de fichiers plus productif, de réduire le temps passé à déboguer des problèmes que le compilateur peut trouver et de diminuer les vulnérabilités liées à la mémoire
  • Overstreet a expliqué avoir trop souvent passé deux semaines à traquer des bugs dans bcachefs, et estime que Rust apporte davantage que C
    • Rust élimine les comportements indéfinis
    • Il fournit des moyens de voir ce qui se passe dans le code
    • S’il devient possible de prouver la correction du code Rust, les bugs qui freinent le développement de fonctionnalités pourraient fortement diminuer

L’exemple du système de types autour de iget_locked()

  • Dans ses diapositives, Almeida a pris comme exemple iget_locked() du noyau actuel, en expliquant qu’il impose des exigences complexes
  • Les appelants C doivent gérer eux-mêmes plusieurs conditions
    • vérifier si la valeur de retour est nulle
    • déterminer si le struct inode renvoyé est un nouvel inode ou un inode existant
    • s’il s’agit d’un nouvel inode, l’initialiser avant utilisation
    • si l’initialisation échoue, appeler iget_failed()
  • Al Viro n’était pas d’accord avec une partie des exigences des appelants de iget_locked() décrites dans les diapositives d’Almeida, et le débat a continué sur les détails du comportement
  • Overstreet estime qu’en encapsulant ces règles dans des types et abstractions Rust, le compilateur peut imposer un traitement correct
  • La fonction Rust correspondante proposée par Almeida était get_or_create_inode()
    • comme en C, il faut toujours vérifier l’échec éventuel
    • en cas de succès, l’appelant reçoit soit un inode normal avec comptage de références, soit un nouvel inode
    • pour un inode normal, le comptage de références est automatiquement décrémenté quand l’objet n’est plus référencé
    • pour un nouvel inode, si l’initialisation n’a pas lieu, le traitement équivalent à iget_failed() est appelé automatiquement
    • une fois le nouvel inode initialisé, il devient un inode normal, et la décrémentation automatique du comptage de références s’applique ensuite
    • tous ces comportements sont imposés par le système de types
  • Viro s’est demandé où, dans le code source réel, ces contraintes seraient définies
  • Almeida a répondu qu’il voulait identifier ces contraintes avec Viro et d’autres développeurs de systèmes de fichiers, puis créer les types et abstractions capables de les imposer

La rupture entre API C et API Rust

  • Dave Chinner estime que si les API C et Rust portent des noms différents, les développeurs existants auront du mal à retrouver l’appel Rust correspondant à partir du code C
  • Il craint aussi qu’en n’utilisant pas les mêmes noms, on aboutisse à une API totalement étrangère à la communauté de développement existante
  • Comme le code C évolue, le code Rust doit suivre, et reste alors la question de savoir qui prendra en charge ce travail
  • Almeida a reconnu qu’il s’agit d’un point qui demande discussion
    • il ne s’oppose pas à un changement de nom
    • mais il ne considère pas iget_locked() comme un bon nom et y voit aussi l’occasion d’en choisir un meilleur
  • Viro a jugé le choix de cet exemple peu pertinent, car iget_locked() n’est pas une méthode d’un objet superblock mais une fonction de bibliothèque
  • Almeida a répondu que get_or_create_inode() pouvait aussi devenir une fonction de bibliothèque, et que l’exemple visait surtout à montrer comment encoder des contraintes dans les types

Choisir entre abstraction générique et approche centrée sur des systèmes de fichiers simples

  • Christian Brauner estime qu’il faut d’abord trancher si l’abstraction Rust doit être une abstraction générique pour tous les systèmes de fichiers du noyau, ou si elle doit surtout couvrir les besoins de systèmes de fichiers plus simples écrits en Rust
  • À long terme, si une fonction comme get_or_create_inode() embarque bien plus de contraintes que iget_locked(), cela peut devenir problématique
  • Le code C peut évoluer plus vite que le code Rust, surtout au début, ce qui oblige à synchroniser en permanence les deux API
  • Overstreet considère que l’enjeu central est de savoir si l’ajout d’abstractions Rust doit s’accompagner d’un travail de refactoring et de nettoyage, et il juge cela nécessaire
  • James Bottomley souligne que le cycle de vie des objets est encodé dans l’API Rust, alors qu’il n’existe pas d’expression équivalente côté C
    • si le cycle de vie d’un objet change d’un côté, cela peut introduire des bugs de l’autre
  • Chinner a ajouté que le cycle de vie des objets inode diffère parfois selon les systèmes de fichiers
    • si l’API intègre une compréhension unique de ce cycle de vie, certaines fonctions pourraient ne pas fonctionner pour certains systèmes de fichiers
  • Almeida a répondu que l’exemple ne serait utilisé que pour les systèmes de fichiers qui appellent aujourd’hui iget_locked() et qui peuvent en tirer bénéfice
    • les développeurs Rust ne cherchent pas à forcer les systèmes de fichiers à changer leur manière actuelle de fonctionner

« Qui supportera la douleur ? »

  • Ted Ts'o a déclaré qu’on avait parfois l’impression d’une tentative de convertir tout le monde à la « religion » Rust, alors que Linux compte plus de 50 systèmes de fichiers et qu’aucune bascule immédiate n’aura lieu
  • Le code C continuera d’être amélioré, et si ces changements cassent les bindings Rust, les systèmes de fichiers qui en dépendent peuvent eux aussi se casser
  • Pour Ts'o, les bindings Rust resteront pendant un temps des citoyens de seconde zone, et des bindings Rust cassés relèveront du problème des développeurs Rust-for-Linux, pas de l’ensemble de la communauté des systèmes de fichiers
  • Il estime que, d’ici un ou deux ans, on verra si l’approche consistant à injecter beaucoup de sémantique dans le système de types est bonne ou mauvaise, à mesure que le développement des bindings Rust progresse en parallèle de l’évolution du code C
  • Pour Ts'o, tout grand changement revient finalement à une question de répartition de la douleur
    • un développeur qui modifie une API C peut corriger le code C affecté, mais dire qu’il ne corrigera pas les bindings Rust parce qu’il ne connaît pas Rust
  • Almeida a répondu qu’il ne cherchait pas à figer les API C, mais à encoder en Rust la sémantique que les développeurs de systèmes de fichiers voudront bien expliciter
  • Bottomley estime que plus la sémantique est encodée dans les bindings, plus ceux-ci peuvent devenir fragiles du point de vue de la synchronisation
  • Almeida a répliqué que lorsqu’une API change, il faut de toute façon mettre à jour ses utilisateurs, comme pour n’importe quel autre utilisateur

Que mettre dans les méthodes, fonctions et types

  • Viro est revenu sur le fait que le remplacement proposé pour iget_locked() repose sur une méthode
    • selon lui, avec une méthode, les arguments ne sont pas explicitement indiqués
  • Overstreet a répondu que cette méfiance vis-à-vis des méthodes vient de langages comme C++ qui s’appuient trop fortement sur l’héritage
    • Rust ne fonctionne pas ainsi, et ses méthodes relèvent surtout d’un choix syntaxique
  • Jan Kara a distingué les comportements attachés à l’inode lui-même et ceux intrinsèques à la fonction iget_locked()
    • à l’inode sont associés des comportements comme le comptage de références et sa gestion
    • la fonction iget_locked() porte, elle, un comportement distinct
  • Overstreet et Almeida ont répondu que ces deux aspects sont bien encodés dans les types, mais séparément, et que d’autres fonctions manipulant le type inode peuvent renvoyer des valeurs avec d’autres propriétés
  • Viro a expliqué pourquoi les inodes fonctionnent aujourd’hui ainsi dans le VFS, et s’est dit d’accord pour commencer modestement et voir dans quelle direction avancer
  • Overstreet a reconnu que cet exemple était peut-être trop complexe pour servir de point de départ, et Viro a conclu par « non, pas du tout », mettant fin à la session

1 commentaires

 
GN⁺ 2024-07-16
Avis sur Hacker News
  • Je ne comprends pas que chaque système de fichiers ait son propre cycle de vie des inodes, tout en utilisant les mêmes fonctions de gestion du cycle de vie avec seulement une sémantique différente
    Si les mêmes fonctions doivent être utilisées différemment selon les détails d’implémentation, cela ressemble à l’inverse d’une couche d’abstraction
    Si le cycle de vie des inodes dépend de chaque système de fichiers, il devrait être géré par des fonctions propres à chaque système de fichiers

    • Je me suis posé la même question, et il semble qu’ils cherchent à comprendre ou documenter toutes les API C pour le travail autour de Rust
      En rassemblant ce type d’informations, on peut faire apparaître des points à refactorer afin que ce genre de question ne se pose même plus au départ, et c’est une bonne chose
    • Si c’est un document que vous découvrez, https://www.kernel.org/doc/html/latest/filesystems/vfs.html peut être utile
      C’est un aperçu de la couche VFS, qui traite les comportements propres à chaque système de fichiers tout en maintenant une interface cohérente côté noyau
    • Je comprends qu’il s’agit d’abstraire autant que possible dans la couche VFS ce qui peut l’être de manière générale, tout en gérant dans la couche propre à chaque système de fichiers les exceptions qui ne s’y prêtent pas
      Le cycle de vie des inodes n’était peut-être qu’un premier exemple pour lancer la discussion
    • Il semble qu’ils veuillent faire fonctionner cela de façon que le compilateur suive la durée de vie des inodes
      Le compilateur aiderait avec les références temporaires, mais le système de fichiers devrait toujours stocker le nombre de liens sur le disque
    • Il existe plusieurs fonctions utilisables pour créer un inode et le mettre en cache, et iget_locked(), sur laquelle l’attention se porte ici, correspond à un schéma particulier
      Tous les systèmes de fichiers n’utilisent pas cette approche, et certains ne l’utilisent pas selon les situations
      Par exemple, FAT ne l’utilise pas, car il génère des numéros d’inode et maintient son propre mapping entre les emplacements FAT et les inodes
      Certains systèmes de fichiers, comme proc, ne mettent pas non plus en cache les objets inode
      L’objet inode lui-même semble suivre le même flux d’états, quelle que soit son origine ; du point de vue du consommateur, l’utilisation de inode ne change donc pas
      Ce qui change, c’est la façon dont la couche du système de fichiers crée l’objet inode et le manipule en interne
  • Je me demande si la question n’est pas mal posée
    Est-ce que Rust devrait changer pour appeler C plus facilement ?
    J’ai un peu pratiqué Rust, mais du point de vue d’un développeur amateur, la manière d’interopérer avec C n’est pas encore claire pour moi
    À l’inverse, en C++ ou en Objective C, il suffit d’inclure le bon header et d’appeler la fonction
    Swift peut inclure des fichiers Objective C, et appeler C depuis là
    Dans ce cas, plutôt que d’attendre des développeurs du noyau qu’ils s’adaptent au langage, je me demande si le langage Rust ne devrait pas devenir un peu plus flexible

    • Appeler C depuis Rust est assez simple
      Il suffit de déclarer une fonction externe et de l’appeler
      Par exemple, comme dans le livre Rust https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#usin..., on peut la déclarer avec extern "C"
      Si l’on ne veut pas écrire à la main toutes les déclarations pour une bibliothèque complexe, on peut utiliser un outil comme bindgen, qui génère automatiquement les déclarations extern à partir de fichiers header C : https://github.com/rust-lang/rust-bindgen
      On peut soutenir qu’il serait bien que quelque chose comme bindgen soit inclus dans Rust et utilisable sans dépendance tierce ni configuration build.rs, mais ce n’est pas le cœur de l’article
      Le problème n’est pas les bindings bas niveau, mais les wrappers haut niveau idiomatiques en Rust, et il ne peut pas exister d’outil général capable de produire automatiquement de tels wrappers à partir de code C arbitraire
    • Ce n’est ni une difficulté importante de Rust, ni lié au sujet de l’article
      L’article cherche comment implémenter réellement en Rust des pilotes de systèmes de fichiers du noyau, etc.
      Il est aussi important de noter que le code Rust dans le noyau consomme nécessairement des interfaces C
      Pour les cas d’usage évoqués, bindgen convient assez bien : https://github.com/rust-lang/rust-bindgen
    • En pratique, c’est assez facile
      Pour appeler depuis Rust, il suffit de déclarer extern "C" fn foo() -> T et de passer les flags de linkage via l’attribut #[link] ou build.rs
      On peut générer les bindings à l’avance avec la crate bindgen, ou les créer dans build.rs puis les inclure avec include!()
      En général, on crée une crate -sys qui ne contient que les bindings générés, puis le code réel fait simplement use des bindings de cette crate sys comme d’habitude
      En C++ et Objective C aussi, il ne suffit pas d’inclure le bon header : il faut également linker la bibliothèque
    • Le point essentiel est que Rust peut modéliser des invariants que C ne peut pas exprimer
      Les appels dans les deux sens sont possibles, mais si C ne peut pas exprimer ce que Rust peut exprimer, cela a un impact important sur la conception des API que les deux côtés doivent partager
    • Appeler C est déjà très simple, donc Rust n’a pas besoin de changer pour rendre cela plus facile
      Il suffit de déclarer une fonction C avec extern "C" et de l’appeler
      Il faut généralement utiliser unsafe et convertir des références en pointeurs bruts, ou faire des casts, mais la syntaxe elle-même est simple
      Il existe aussi des outils qui scannent les fichiers header C pour produire les déclarations, et bindgen est le plus utilisé
      Le débat de cet article porte moins sur le langage lui-même que sur la manière d’utiliser Rust
      Les développeurs de Rust-for-Linux veulent utiliser les fonctionnalités et le système de types de Rust pour encoder la sémantique des appels d’API, afin de les rendre plus sûrs et moins sujets aux erreurs
      Les personnes côté C craignent que cela rende plus difficile l’évolution du comportement et de la sémantique des API C
      Quand l’API C change, l’API Rust doit aussi être corrigée, et elles ne veulent pas assumer ce travail
      Une alternative plus facile à accepter serait d’utiliser moins les fonctionnalités et le système de types de Rust pour encoder la sémantique dans l’API Rust
      Ainsi, lorsqu’une API C change, la mise à jour de l’API Rust devient mécanique et simple, mais si Rust-for-Linux ne peut pas utiliser les fonctionnalités de Rust pour créer des API meilleures et plus sûres, on peut se demander quel est l’intérêt de tout ce travail
      Cela dit, il est un peu étrange de parler de ce sujet de façon catégorique tout en reconnaissant qu’on ne comprend pas suffisamment le langage
  • Comme je ne connais pas bien les systèmes de fichiers Linux, il ne m’était pas clair si cette API Rust enveloppe la C API ou si elle la réimplémente
    S’il s’agit d’une réimplémentation ou d’une API distincte, conserver les mêmes noms que l’API C risque à mon avis de créer de plus en plus de confusion avec le temps
    Même si, au départ, cela aide les développeurs déjà familiers à comprendre plus vite

    • Almeida a montré l’équivalent de iget_locked() en Rust, et son nom était get_or_create_inode()
      La réponse semble être que c’est une réimplémentation, et qu’ils ne reprennent pas les mêmes noms
  • Vu la façon dont ce genre de discussion se déroule généralement et l’ampleur du changement, celle-ci est étonnamment courtoise
    Je ne partage pas l’ambiance négative de ce fil
    Je suis plutôt optimiste, dans la mesure où les personnes concernées ont clairement fait passer les principaux points douloureux, sans baratin

    • J’ai continué à lire davantage pour la qualité du compte rendu que pour le contenu lui-même
      La discussion réelle devait être intense, dispersée et pleine d’ergotages, comme on peut s’y attendre d’un débat sur les langages de programmation entre geeks aux opinions bien arrêtées
      Jake Edge, qui a écrit ce résumé, semble très doué pour écarter ces éléments et ne garder que l’essentiel
  • Certains commentaires en bas de la page lwn.net sont assez impolis
    Imaginez recevoir un commentaire du genre Science advances one funeral at a time sur un projet open source auquel vous contribuez

  • Le fait que le noyau Linux ait davantage d’options est toujours bénéfique
    Mais Rust n’est peut-être pas la réponse à tout
    Rust fait de son mieux pour garantir un modèle de programmation sûr, mais ce modèle a aussi ses limites
    On pourrait avoir l’impression que, s’il s’agit d’un problème de mémoire, il faut utiliser Rust, et que, s’il s’agit d’un problème de concurrence, il faut passer à Rust, mais sans blocs unsafe, on ne peut pas faire tout ce que C fait
    Rust peut apporter un nouveau point de vue sur ces problèmes, mais ce n’est pas une solution complète

    • Le grand avantage de Rust dans ce travail, c’est qu’il cherche activement à encapsuler ces problèmes de sûreté dans les types, et c’est précisément le sujet de cet article
      C, en particulier le C utilisé dans le noyau, rejette sur chacun la responsabilité de connaître parfaitement toutes les règles implicites
      Cela ne passe pas à l’échelle
      Même des développeurs du noyau utilisant les mêmes structures de données, réunis dans la même pièce, n’ont pas réussi à se mettre totalement d’accord sur ces règles
      Rust est fort pour rendre visibles les règles à connaître et, si quelqu’un d’autre peut garantir qu’elles sont respectées, faire en sorte que ce ne soit plus mon problème
      Parfois, le résultat peut être moins optimal, mais même dans le noyau Linux, un choix par défaut moins optimal est souvent le bon ; et pour ceux qui ont la capacité d’apprendre six règles bizarres de plus afin d’obtenir de meilleures performances, il suffit de fournir une échappatoire avec unsafe
    • On peut utiliser des blocs unsafe
      Il faut simplement ne les utiliser que lorsqu’ils sont nécessaires
      Utiliser un bloc unsafe dont la portée est très limitée ne fait pas disparaître toutes les garanties obtenues dans le reste du code
    • Il est vrai que les tâches de bas niveau nécessitent du code unsafe
      Mais en conclure que Rust n’est pas adapté parce qu’il faut utiliser unsafe est une idée fausse
      La distinction sûr/non sûr de Rust sert à indiquer clairement quelles parties du code sont non sûres, afin de concentrer l’audit sur de petites portions et, si celles-ci sont correctes, de pouvoir faire confiance au reste
    • Je me demande quelle part de tout cela est réellement nécessaire de manière absolument évidente
      Y a-t-il une bonne raison pour que du code de système de fichiers doive forcément être unsafe ?
      Ce sera probablement un très petit sous-ensemble nécessaire à quelques endroits
    • J’aime bien Rust parce qu’il y a des moments où sa manière de raisonner tombe juste, mais côté asynchrone, j’ai l’impression qu’il reste encore beaucoup d’aspérités
      Ce qui se passe en interne n’est pas intuitif
  • À lire le compte rendu de réunion, Rust dans le noyau ressemble à un coût de complexité supplémentaire
    Si l’on réécrivait un système d’exploitation depuis zéro, on pourrait pleinement exploiter la puissance du langage
    Mais lorsqu’on l’ajoute à côté d’une base de code déjà énorme, cela crée des problèmes supplémentaires, comme on le voit ici

    • C’est vrai, mais ce coût devrait être compensé par un développement de pilotes plus facile
      Il suffit de lire le billet expliquant que le pilote GPU Rust d’Asahi Linux a été réalisé en un mois
      Cherchez tales of the m1 gpu sur Google ; l’auteur a des opinions très négatives sur Hacker News
      Si vous voulez, vous pouvez le lire via ce lien : https://asahilinux.org/2022/11/tales-of-the-m1-gpu/
      Il faudra observer les prochaines années pour voir si cela se généralise
    • J’ai fortement l’impression qu’à force de chercher la perfection, on passe à côté de ce qui est bon
    • Je reconnais les avantages de Rust, mais j’ai tendance à penser qu’il est difficile pour la raison de l’emporter sur le battage médiatique
      Ce coût sera considéré comme nécessaire pour accepter l’avenir et le progrès
      Je me demande pourquoi on ne se limite pas à un sous-ensemble sûr, plutôt que de se jeter dans un énorme mouvement rempli de bugs inconnus et de compromis
    • On pourrait aussi dire que tout code ajouté, pas seulement Rust, introduit de la complexité
      Le fait que ce soit déjà trop énorme ne signifie pas qu’il faille arrêter d’innover et passer indéfiniment en mode maintenance
      Comme pour les impôts dans le monde réel, si le coût d’un côté sert à compenser un autre problème, ce n’est pas forcément une perte nette
      Dire, sur la base d’une seule discussion non conclue, que cela ne compensera aucun problème me semble être un argument un peu court
  • Le passage disant que les noms des API C et Rust ne correspondent pas, si bien qu’en lisant le code C on ne peut pas savoir quel appel Rust équivalent utiliser, ressemble à une lutte contre de vieilles conventions de nommage
    On a déjà vu cela bien se résoudre en conservant le même nom et, lorsqu’un autre nom est souhaité, en faisant en sorte que le nouveau nom enveloppe l’ancien
    Mais le nommage reste difficile

    • C’est l’un des deux grands problèmes de l’informatique
      Les deux autres sont la concurrence et les erreurs off-by-one