2 puntos por GN⁺ 2024-07-16 | 1 comentarios | Compartir por WhatsApp
  • En la LSFMM+BPF Summit de 2024 se debatió cómo aplicar Rust a los sistemas de archivos de Linux, y el segundo parche RFC publicado después del RFC de diciembre de 2023 fue el centro de la discusión
  • El equipo de Rust-for-Linux busca expresar los requisitos de las API de sistemas de archivos en el sistema de tipos de Rust para detectar errores en tiempo de compilación, automatizar la limpieza de recursos y reducir vulnerabilidades relacionadas con la memoria
  • El caso de iget_locked() muestra la dirección en la que get_or_create_inode() de Rust intenta imponer mediante tipos y limpieza automática tareas que antes el llamador en C debía manejar directamente, como verificar nulos, distinguir el estado del inode y gestionar fallos
  • Dave Chinner, Christian Brauner, James Bottomley, Ted Ts'o y otros expresaron preocupación por la falta de correspondencia entre nombres de API en C y Rust, la sincronización de API, las diferencias en el ciclo de vida de objetos y la carga de mantenimiento considerando más de 50 sistemas de archivos
  • El núcleo del conflicto no está tanto en las ventajas de las abstracciones de Rust en sí, sino en quién asumirá el dolor de mantener alineados los bindings y abstracciones de Rust cuando el código C siga cambiando

Sesión sobre Rust para sistemas de archivos en LSFMM+BPF

  • En la Linux Storage, Filesystem, Memory Management, and BPF Summit de 2024, Wedson Almeida Filho y Kent Overstreet trataron el uso de Rust en los sistemas de archivos de Linux
  • Almeida había publicado en diciembre de 2023 un conjunto de parches RFC de abstracciones en Rust para sistemas de archivos, y hubo diferencias de opinión en torno a ese enfoque
  • El mismo día de mediados de mayo en que se realizó la sesión, Almeida publicó una segunda versión RFC del parche para discutirla junto con otros temas relacionados con Rust

Objetivos de las abstracciones de sistemas de archivos en Rust-for-Linux

  • La abstracción de sistemas de archivos propuesta refleja la dirección que persigue el proyecto Rust-for-Linux
  • La idea central es expresar más requisitos de las API de sistemas de archivos mediante el sistema de tipos de Rust para detectar errores en tiempo de compilación
  • También se busca automatizar tareas que son difíciles de ofrecer fácilmente en código C
    • Ej.: limpieza de recursos
  • El objetivo es hacer más productiva la experiencia de desarrollo de sistemas de archivos, reducir el tiempo dedicado a depurar problemas que el compilador puede encontrar y disminuir las vulnerabilidades relacionadas con la memoria
  • Overstreet dijo que en bcachefs había pasado demasiadas veces por rastreos de bugs de dos semanas, y considera que Rust ofrece más que C
    • Rust elimina el comportamiento indefinido
    • Proporciona capacidades para ver qué ocurre dentro del código
    • Cree que, si se puede demostrar la corrección del código Rust, habrá muchos menos bugs que bloqueen el desarrollo de funcionalidades

Caso del sistema de tipos en torno a iget_locked()

  • Almeida dio como ejemplo en sus diapositivas que el iget_locked() actual del kernel tiene requisitos complejos
  • El llamador en C debe manejar directamente varias condiciones
    • Debe comprobar si el valor de retorno es null
    • Debe comprobar si el struct inode devuelto es un inode nuevo o uno existente
    • Si es un inode nuevo, debe inicializarlo antes de usarlo
    • Si la inicialización falla, debe llamar a iget_failed()
  • Al Viro no estuvo de acuerdo con parte de los requisitos del llamador de iget_locked() en las diapositivas de Almeida, y la discusión continuó sobre detalles de comportamiento
  • Overstreet considera que, si estas reglas se encapsulan en tipos y abstracciones de Rust, el compilador puede imponer el manejo correcto
  • La función equivalente del lado de Rust presentada por Almeida fue get_or_create_inode()
    • Al igual que en C, se debe comprobar si hubo fallo
    • Si tiene éxito, el llamador recibe un inode con conteo de referencias normal o un inode nuevo
    • En un inode normal, el conteo de referencias se reduce automáticamente cuando el objeto deja de estar referenciado
    • Si un inode nuevo no se inicializa, se invoca automáticamente el manejo equivalente a iget_failed()
    • Una vez que un inode nuevo se inicializa, pasa a ser un inode normal, y desde entonces se aplica la reducción automática del conteo de referencias
    • Estos comportamientos se imponen mediante el sistema de tipos
  • Viro preguntó dónde se definirían realmente estas restricciones en el código fuente
  • Almeida respondió que, después de identificar las restricciones con Viro y otros desarrolladores de sistemas de archivos, su intención era crear tipos y abstracciones que las impusieran

Ruptura entre la API de C y la API de Rust

  • Dave Chinner opinó que, si los nombres de la API de C y la de Rust son distintos, a los desarrolladores existentes les resultaría difícil mirar el código C y saber cuál es la llamada equivalente en Rust
  • También surgió la preocupación de que, si no se usan los mismos nombres, podría convertirse en una API completamente desconocida para la comunidad de desarrollo existente
  • Como el código C cambia, el código Rust también deberá seguirlo, por lo que sigue siendo un problema quién se hará cargo de ese trabajo
  • Almeida reconoció que es un asunto que requiere discusión
    • No se opone a cambiar nombres
    • Sin embargo, no considera que iget_locked() sea un buen nombre, y dijo que también podría verse como una oportunidad para crear uno mejor
  • Viro consideró que la elección del ejemplo no era buena porque iget_locked() no es un método miembro del objeto superblock, sino una función de biblioteca
  • Almeida respondió que get_or_create_inode() también podría convertirse en una función de biblioteca, y que el ejemplo buscaba mostrar cómo codificar restricciones en tipos

Elegir entre una abstracción general y un enfoque centrado en sistemas de archivos simples

  • Christian Brauner opinó que primero hay que decidir si las abstracciones de Rust serán abstracciones generales para todos los sistemas de archivos del kernel o si estarán centradas en las funciones necesarias para sistemas de archivos más simples escritos en Rust
  • A largo plazo, podría haber problemas si funciones como get_or_create_inode() contienen muchas más restricciones que iget_locked()
  • El código C puede evolucionar más rápido que el código Rust, especialmente al principio, por lo que ambas API deberán mantenerse sincronizadas
  • Overstreet dijo que el punto central es si, al agregar abstracciones de Rust, se hará refactorización y limpieza al mismo tiempo, y considera firmemente que eso es necesario
  • James Bottomley opinó que el ciclo de vida de los objetos se codifica en la API de Rust, pero no existe una representación equivalente en C
    • Si el ciclo de vida de un objeto cambia de un lado, podrían aparecer bugs del otro
  • Chinner dijo que el ciclo de vida de los objetos inode a veces varía según el sistema de archivos
    • Si se incorpora una única interpretación del ciclo de vida en la API, esas funciones podrían no funcionar en algunos sistemas de archivos
  • Almeida respondió que el ejemplo se usaría solo en sistemas de archivos que actualmente llaman a iget_locked() y podrían beneficiarse de él
    • Los desarrolladores de Rust no intentan obligar a los sistemas de archivos a cambiar la forma en que operan actualmente

“Quién asumirá el dolor”

  • Ted Ts'o dijo que parece haber un intento de convertir a todos a la “religión” de Rust, pero Linux tiene más de 50 sistemas de archivos y no se convertirán de inmediato
  • El código C seguirá mejorando, y si esos cambios rompen los bindings de Rust, también podrían romperse los sistemas de archivos que dependan de esos bindings
  • Ts'o considera que, por ahora, los bindings de Rust son ciudadanos de segunda clase, y que los bindings rotos son un problema de los desarrolladores de Rust-for-Linux, no de toda la comunidad de sistemas de archivos
  • Cree que, mientras se desarrollan bindings de Rust en paralelo con la evolución del código C, en uno o dos años quedará claro si el enfoque de incorporar mucha semántica al sistema de tipos es bueno o malo
  • Para Ts'o, un gran cambio es, en definitiva, un problema de distribución del dolor
    • Un desarrollador que cambie la API de C puede arreglar el código C afectado, pero decir que no arreglará los bindings de Rust porque no sabe Rust
  • Almeida respondió que no busca congelar la API de C, sino codificar en Rust el significado de la API cuando los desarrolladores de sistemas de archivos lo expliquen
  • Bottomley considera que, cuanto más significado se codifique en los bindings, más frágiles pueden volverse desde el punto de vista de la sincronización
  • Almeida respondió que, si una API cambia, los usuarios de esa API deben actualizarse, igual que cualquier otro usuario

Qué poner en métodos, funciones y tipos

  • Viro volvió a cuestionar que la alternativa a iget_locked() dependa de métodos
    • Considera que, al usar métodos, los argumentos no se especifican explícitamente
  • Overstreet opinó que las quejas sobre los métodos provienen de lenguajes como C++, que dependen demasiado de la herencia
    • Dijo que Rust no hace eso y que los métodos de Rust son en gran medida un elemento sintáctico
  • Jan Kara distinguió entre el comportamiento asociado al propio inode y el comportamiento inherente a la función iget_locked()
    • Un inode tiene comportamientos asociados, como el conteo de referencias y su manejo
    • La función iget_locked() tiene un comportamiento inherente distinto
  • Overstreet y Almeida respondieron que ambas partes se codifican en los tipos, pero están separadas, y que otras funciones que usen el tipo inode pueden tener valores de retorno con propiedades diferentes
  • Viro explicó por qué los inodes funcionan actualmente de esa manera en VFS, y aceptó empezar en pequeño y ver hacia dónde avanzar
  • Overstreet dijo que este ejemplo quizá era complejo y no tan bueno como punto de partida, y Viro respondió “no, no es así”, con lo que terminó la sesión

1 comentarios

 
GN⁺ 2024-07-16
Opiniones de Hacker News
  • No entiendo que cada sistema de archivos tenga su propio ciclo de vida del inode y, aun así, use las mismas funciones de gestión del ciclo de vida, cambiando solo la semántica.
    Si la misma función debe usarse de forma distinta según los detalles de implementación, suena a lo contrario de una capa de abstracción.
    Si el ciclo de vida del inode depende de cada sistema de archivos, debería gestionarse con funciones específicas para cada sistema de archivos.

    • Tuve la misma duda, y parece que están intentando entender o documentar toda la API en C para el trabajo en Rust.
      Al recopilar ese tipo de información, pueden aparecer puntos de refactorización que hagan que estas preguntas ni siquiera surjan desde el principio, y eso es algo bueno.
    • Si es material nuevo para ti, puede servirte https://www.kernel.org/doc/html/latest/filesystems/vfs.html.
      Es una descripción general de la capa VFS, que mantiene una interfaz consistente del lado del kernel mientras maneja comportamientos específicos de cada sistema de archivos.
    • Entiendo que buscan abstraer lo más posible en la capa VFS las partes que pueden generalizarse, y manejar en la capa específica de cada sistema de archivos las excepciones que no encajan.
      Puede que el ciclo de vida del inode haya sido un ejemplo inicial para empezar la discusión.
    • Parece que quieren hacer que funcione de modo que el compilador rastree la vida útil del inode.
      El compilador ayudaría con las referencias temporales, pero el sistema de archivos todavía tendría que guardar en disco el conteo de enlaces.
    • Hay varias funciones que se pueden usar para crear un inode y ponerlo en caché, y iget_locked() —en la que se enfocan aquí— es un patrón específico entre ellas.
      No todos los sistemas de archivos usan ese método y, según la situación, algunos no lo usan.
      Por ejemplo, FAT no lo usa porque genera números de inode y mantiene su propio mapeo desde la ubicación FAT hacia el inode.
      También hay sistemas de archivos, como proc, que no almacenan en caché los objetos inode.
      El flujo de estados del objeto inode en sí parece ser el mismo sin importar de dónde venga, así que, desde la perspectiva del consumidor, la forma de usar inode no cambia.
      Lo que cambia es la manera en que la capa del sistema de archivos crea el objeto inode y lo maneja internamente.
  • Me pregunto si quizá no estamos haciendo la pregunta correcta.
    ¿Rust debería cambiar para que llamar a C sea más fácil?
    He probado un poco Rust, pero desde la perspectiva de un desarrollador aficionado todavía no me queda claro cómo debería interoperar con C.
    En cambio, en C++ u Objective C basta con incluir el encabezado correcto y llamar a la función.
    Swift puede incluir archivos de Objective C, y desde ahí puede llamar a C.
    En este caso, quizá el lenguaje Rust debería volverse un poco más flexible, en vez de esperar que los desarrolladores del kernel se adapten al lenguaje.

    • Llamar a C desde Rust es bastante simple.
      Declaras la función externa y la llamas.
      Por ejemplo, como aparece en el libro de Rust en https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#usin..., se puede declarar con extern "C".
      Si no quieres escribir a mano todas las declaraciones de una biblioteca compleja, puedes usar una herramienta como bindgen, que genera automáticamente declaraciones extern a partir de archivos de encabezado de C: https://github.com/rust-lang/rust-bindgen
      Se puede argumentar que sería bueno que algo como bindgen viniera incluido en Rust y pudiera usarse sin dependencias de terceros ni configuración de build.rs, pero ese no es el punto central del artículo.
      El problema no son los bindings de bajo nivel, sino los wrappers de alto nivel idiomáticos de Rust; y no puede existir una herramienta general que genere automáticamente ese tipo de wrappers a partir de código C arbitrario.
    • Esto no es una dificultad importante en Rust ni está relacionado con el tema del artículo.
      El artículo trata sobre encontrar una forma de implementar realmente drivers de sistemas de archivos del kernel, etc., en Rust.
      También es importante que el código Rust dentro del kernel inevitablemente consuma interfaces de C.
      Para el caso de uso que tienes en mente, bindgen encaja bastante bien: https://github.com/rust-lang/rust-bindgen
    • En la práctica es bastante fácil.
      Para llamar desde Rust, declaras extern "C" fn foo() -> T y pasas las flags de enlace con el atributo #[link] o mediante build.rs.
      Puedes generar los bindings de antemano con el crate bindgen, o generarlos en build.rs e incluirlos con include!().
      Normalmente se crea un crate -sys que solo contiene los bindings generados, y el código real usa los bindings de ese crate sys con use, como de costumbre.
      En C++ y Objective C tampoco basta con incluir el encabezado correcto: también hay que enlazar contra la biblioteca.
    • El punto central es que Rust puede modelar invariantes que C no puede expresar.
      Las llamadas en ambos sentidos son posibles, pero si C no puede expresar lo que Rust sí puede, eso tiene un impacto importante en el diseño de una API que ambas partes deban usar en común.
    • Las llamadas a C ya son muy simples, así que Rust no necesita cambiar para hacerlas más fáciles.
      Declaras una función de C con extern "C" y la llamas.
      Normalmente se necesita unsafe y hay que convertir referencias a punteros crudos o hacer casts, pero la sintaxis en sí es sencilla.
      También existen herramientas que escanean archivos de encabezado de C y generan declaraciones; bindgen es la más usada.
      El punto en discusión en este artículo se parece más a cómo usar Rust que al lenguaje en sí.
      Los desarrolladores de Rust-for-Linux quieren usar las capacidades y el sistema de tipos de Rust para codificar la semántica de las llamadas a la API, de modo que sean más seguras y menos propensas a errores.
      La gente del lado de C teme que hacerlo vuelva más difícil evolucionar el comportamiento y la semántica de la API de C.
      Porque cuando cambie la API de C, también habrá que corregir la API de Rust, y no quieren cargar con ese trabajo.
      Una alternativa más fácil de aceptar es usar menos las capacidades y el sistema de tipos de Rust para codificar semántica en la API de Rust.
      Así, cuando cambie la API de C, actualizar la API de Rust sería mecánico y sencillo; pero si Rust-for-Linux no puede usar las capacidades de Rust para crear una API mejor y más segura, surge la duda de cuál es el sentido de este trabajo.
      Dicho eso, resulta un poco raro hablar de forma tajante sobre este tema mientras se reconoce no entender lo suficiente el lenguaje.
  • No me quedaba claro, porque no conozco bien los sistemas de archivos de Linux, si esta API de Rust envuelve la API de C o si la reimplementa.
    Si es una reimplementación o una API aparte, mantener los mismos nombres que la API de C parece algo que con el tiempo generará más confusión.
    Aunque al principio ayude a que los desarrolladores familiarizados la entiendan más rápido.

    • Almeida mostró el equivalente en Rust de iget_locked(), y se llamaba get_or_create_inode().
      La respuesta parece ser que es una reimplementación y que no se están usando los mismos nombres.
  • Considerando cómo suelen desarrollarse este tipo de discusiones y la magnitud del cambio, esta discusión fue sorprendentemente educada.
    No estoy de acuerdo con el tono negativo de este hilo.
    Soy bastante optimista, porque los involucrados comunicaron claramente los puntos centrales de dolor, sin decir tonterías.

    • Terminé leyendo más por el excelente resumen que por el contenido en sí.
      La discusión real seguramente fue intensa, dispersa y llena de objeciones minuciosas, como corresponde a un debate sobre lenguajes de programación entre geeks con opiniones fuertes.
      Parece que Jake Edge, quien escribió este resumen, es muy bueno para quitar todo eso y quedarse solo con lo esencial.
  • Algunos comentarios al pie de la página de lwn.net son bastante groseros.
    Imaginen recibir en un proyecto open source al que están contribuyendo un comentario como Science advances one funeral at a time.

  • Que el kernel de Linux tenga más opciones siempre es beneficioso
    Pero Rust quizá no sea la respuesta para todo
    Rust hace todo lo posible por garantizar un modelo de programación seguro, pero ese modelo también tiene límites
    Puede parecer que, si el problema es de memoria, hay que usar Rust, y si el problema es de concurrencia, hay que pasarse a Rust, pero no se puede hacer todo lo que hace C sin bloques unsafe
    Rust puede aportar una nueva perspectiva a estos problemas, pero no es una solución completa

    • Una gran ventaja de Rust en este trabajo es que apunta activamente a encapsular en tipos esos problemas de seguridad, y este artículo trata justamente de eso
      C, especialmente el C usado en el kernel, le carga a cada persona la responsabilidad de conocer por completo todas las reglas implícitas
      Eso no escala
      Ni siquiera los desarrolladores del kernel que usan las mismas estructuras de datos, reunidos en una misma sala, pudieron ponerse completamente de acuerdo sobre esas reglas
      Rust es fuerte para hacer visibles las reglas que hay que conocer y, si alguien más puede garantizar que se cumplan, hacer que eso deje de ser mi problema
      A veces el resultado puede ser menos óptimo, pero incluso en el kernel de Linux muchas veces un valor predeterminado menos óptimo es lo correcto, y para quien pueda aprender seis reglas raras más a cambio de mejor rendimiento, basta con ofrecer una vía de escape con unsafe
    • Se pueden usar bloques unsafe
      Solo que hay que usarlos únicamente cuando sea necesario
      Usar un bloque unsafe con un alcance muy limitado no hace que desaparezcan todas las garantías obtenidas en el resto del código
    • Es cierto que el trabajo de bajo nivel requiere código unsafe
      Pero es un malentendido decir que, como hay que usar unsafe, Rust no es adecuado
      La separación de Rust entre seguro/no seguro busca marcar claramente qué partes del código no son seguras, para poder concentrar la auditoría en una sección pequeña y, si esa parte está bien, confiar en que el resto funciona
    • Me da curiosidad cuánto de esto es realmente 100% evidentemente necesario
      ¿Hay una buena razón para que el código de sistemas de archivos tenga que ser necesariamente unsafe?
      Probablemente sea un subconjunto muy pequeño necesario en unos pocos lugares
    • Me gusta Rust porque a veces encaja muy bien con la forma de entender las cosas, pero siento que el lado asíncrono todavía tiene muchas asperezas
      No es intuitivo qué ocurre por dentro
  • Al ver las actas de la reunión, Rust dentro del kernel parece un costo de complejidad adicional
    Si uno reescribiera el sistema operativo desde cero, podría usar toda la potencia del lenguaje
    Pero si se lo agrega junto a una base de código enorme ya existente, aparecen problemas adicionales como los que se ven aquí

    • Es cierto, pero ese costo debería compensarse con un desarrollo de drivers más fácil
      Basta ver el blog donde cuentan que el driver de GPU en Rust de Asahi Linux se hizo en un mes
      Se puede buscar en Google tales of the m1 gpu, y el autor tiene una opinión muy negativa de Hacker News
      Si se quiere, se puede leer en el enlace: https://asahilinux.org/2022/11/tales-of-the-m1-gpu/
      Habrá que ver en los próximos años si se aplica de forma general
    • Da mucho la sensación de que, por buscar la perfección, se está dejando pasar lo bueno
    • Estoy de acuerdo con las ventajas de Rust, pero tiendo a pensar que es difícil que la razón le gane al hype
      Ese costo se considerará necesario para aceptar el futuro y el progreso
      Me pregunto por qué no se limita a un subconjunto seguro, en lugar de subirse a una gran ola con muchos bugs desconocidos y concesiones
    • También se podría decir que cualquier código adicional, no solo Rust, introduce complejidad
      Que ya sea demasiado grande no significa que haya que detener la innovación y entrar en un estado de mantenimiento indefinido
      Como ocurre con los impuestos en el mundo real, si el costo de un lado se usa para compensar otro problema, quizá no sea una pérdida neta
      Decir, a partir de una sola discusión inconclusa, que no va a compensar ningún problema parece un argumento corto
  • La parte de que los nombres de la API de C y la API de Rust no coinciden, por lo que al mirar el código C no se puede saber cuál es la llamada equivalente en Rust, parece una pelea contra viejas convenciones de nomenclatura
    Ha habido casos en los que funcionó bien mantener el mismo nombre y, cuando se quería un nombre alternativo, hacer que el nuevo nombre envolviera al antiguo
    Aun así, nombrar cosas es difícil

    • Es uno de los dos grandes problemas de la informática
      Los otros dos son la concurrencia y los errores off-by-one