Rust en el kernel de Linux


En nuestro Publicación anterior, anunciamos que Android ahora es compatible con Oxido lenguaje de programación para desarrollar el propio sistema operativo. En relación con esto, también participamos en el esfuerzo de evaluar el uso de Rust como lenguaje compatible para desarrollar el kernel de Linux. En esta publicación, discutimos algunos aspectos técnicos de este trabajo usando algunos ejemplos simples.

C ha sido el lenguaje elegido para escribir núcleos durante casi medio siglo porque ofrece el nivel de control y rendimiento predecible que requiere un componente tan crítico. La densidad de errores de seguridad de la memoria en el kernel de Linux es generalmente bastante baja debido a la alta calidad del código, los altos estándares de revisión del código y las salvaguardas implementadas cuidadosamente. Sin emabargo, errores de seguridad de la memoria todavía ocurren con regularidad. En Android, las vulnerabilidades en el kernel generalmente se consideran de alta gravedad porque pueden resultar en una omisión del modelo de seguridad debido al modo privilegiado en el que se ejecuta el kernel.

Creemos que Rust ahora está listo para unirse a C como lenguaje práctico para implementar el kernel. Puede ayudarnos a reducir la cantidad de errores potenciales y vulnerabilidades de seguridad en el código privilegiado mientras juega bien con el núcleo del núcleo y preserva sus características de rendimiento.

Desarrollamos un prototipo inicial del controlador Binder para permitirnos hacer comparaciones significativas entre las características de seguridad y rendimiento de la versión C existente y su contraparte Rust. El kernel de Linux tiene más de 30 millones de líneas de código, por lo que, naturalmente, nuestro objetivo no es convertirlo todo a Rust, sino permitir que se escriba nuevo código en Rust. Creemos que este enfoque incremental nos permite beneficiarnos de la implementación de alto rendimiento existente del kernel al tiempo que brinda a los desarrolladores del kernel nuevas herramientas para mejorar la seguridad de la memoria y mantener el rendimiento en el futuro.

Nos unimos al Rust para Linux organización, donde la comunidad ya había hecho y continúa haciendo un gran trabajo para agregar soporte de Rust al sistema de compilación del kernel de Linux. También necesitamos diseños que permitan que el código en los dos lenguajes interactúe entre sí: estamos particularmente interesados ​​en abstracciones seguras y de costo cero que permitan que el código de Rust use la funcionalidad del kernel escrita en C, y cómo implementar la funcionalidad en Rust idiomático que se puede llamar sin problemas desde las partes C del kernel.

Dado que Rust es un nuevo lenguaje para el kernel, también tenemos la oportunidad de aplicar las mejores prácticas en términos de documentación y uniformidad. Por ejemplo, tenemos requisitos específicos verificados por máquina en torno al uso de código inseguro: para cada función insegura, el desarrollador debe documentar los requisitos que deben satisfacer las personas que llaman para garantizar que su uso sea seguro; Además, por cada llamada a funciones inseguras (o el uso de construcciones inseguras como desreferenciar un puntero sin formato), el desarrollador debe documentar la justificación de por qué es seguro hacerlo.

Tan importante como la seguridad, el soporte de Rust debe ser conveniente y útil para que lo utilicen los desarrolladores. Veamos algunos ejemplos de cómo Rust puede ayudar a los desarrolladores del kernel a escribir controladores que sean seguros y correctos.

Usaremos una implementación de un semáforo dispositivo de carácter. Cada dispositivo tiene un valor actual; escribe de norte bytes dan como resultado que el valor del dispositivo se incremente en norte; las lecturas disminuyen el valor en 1 a menos que el valor sea 0, en cuyo caso se bloquearán hasta que puedan disminuir el recuento sin bajar de 0.

Suponer semáforo es un archivo que representa nuestro dispositivo. Podemos interactuar con él desde el shell de la siguiente manera:

> semáforo de gato

Cuándo semáforo es un dispositivo recién inicializado, el comando anterior se bloqueará porque el valor actual del dispositivo es 0. Se desbloqueará si ejecutamos el siguiente comando desde otro shell porque incrementa el valor en 1, lo que permite que se complete la lectura original:

> echo -n a> semáforo

También podríamos incrementar el recuento en más de 1 si escribimos más datos, por ejemplo:

> echo -n abc> semáforo

incrementa el recuento en 3, por lo que las siguientes 3 lecturas no se bloquearán.

Para permitirnos mostrar algunos aspectos más de Rust, agregaremos las siguientes características a nuestro controlador: recuerde cuál fue el valor máximo durante la vida útil de un dispositivo y recuerde cuántas lecturas de cada archivo emitido en el dispositivo.

Ahora mostraremos cómo sería un conductor así implementado en Rust, contrastando con un Implementación de C. Sin embargo, observamos que todavía estamos en una etapa temprana, por lo que todo esto está sujeto a cambios en el futuro. La forma en que Rust puede ayudar al desarrollador es el aspecto que nos gustaría enfatizar. Por ejemplo, en el momento de la compilación nos permite eliminar o reducir en gran medida las posibilidades de introducir clases de errores, mientras que al mismo tiempo permanece flexible y tiene una sobrecarga mínima.

Dispositivos de personajes

Un desarrollador debe hacer lo siguiente para implementar un controlador para un nuevo dispositivo de caracteres en Rust:

  1. Implementar el Operaciones de archivo rasgo: todas las funciones asociadas son opcionales, por lo que el desarrollador solo necesita implementar las relevantes para su escenario. Se relacionan con los campos en C struct file_operations.
  2. Implementar el FileOpener rasgo: es un equivalente de tipo seguro a C abierto campo de struct file_operations.
  3. Registre el nuevo tipo de dispositivo con el kernel: esto le permite al kernel saber qué funciones deben llamarse en respuesta a los archivos de este nuevo tipo que se están operando.

A continuación, se describe cómo se comparan los dos primeros pasos de nuestro ejemplo en Rust y C:

impl FileOpener> for FileState 
    fn open(
        shared: &Arc
    ) -> KernelResult<Box<Self>> 
        (...)
    

 
impl FileOperations for FileState 
    type Wrapper = Box<Self>;
 
    fn read(
        &self,
        _: &File,
        data: &mut UserSlicePtrWriter,
        offset: u64
    ) -> KernelResult<usize> 
        (...)
    
 
    fn write(
        &self,
        data: &mut UserSlicePtrReader,
        _offset: u64
    ) -> KernelResult<usize> 
        (...)
    
 
    fn ioctl(
        &self,
        file: &File,
        cmd: &mut IoctlCommand
    ) -> KernelResult<i32> 
        (...)
    
 
    fn release(_obj: Box<Self>, _file: &File) 
        (...)
    
 
    declare_file_operations!(read, write, ioctl);
static 
int semaphore_open(struct inode *nodp,
                   struct file *filp)

    struct semaphore_state *shared =
        container_of(filp->private_data,
                     struct semaphore_state,
                     miscdev);
    (...)

 
static
ssize_t semaphore_write(struct file *filp,
                        const char __user *buffer,
                        size_t count, loff_t *ppos)

    struct file_state *state = filp->private_data;
    (...)

 
static
ssize_t semaphore_read(struct file *filp,
                       char __user *buffer,
                       size_t count, loff_t *ppos)

    struct file_state *state = filp->private_data;
    (...)

 
static
long semaphore_ioctl(struct file *filp,
                     unsigned int cmd,
                     unsigned long arg)

    struct file_state *state = filp->private_data;
    (...)

 
static
int semaphore_release(struct inode *nodp,
                      struct file *filp)

    struct file_state *state = filp->private_data;
    (...)

 
static const struct file_operations semaphore_fops = 
        .owner = THIS_MODULE,
        .open = semaphore_open,
        .read = semaphore_read,
        .write = semaphore_write,
        .compat_ioctl = semaphore_ioctl,
        .release = semaphore_release,
;

Los dispositivos de personajes en Rust se benefician de una serie de características de seguridad:

  • Gestión de por vida del estado por archivo: FileOpener :: abrir devuelve un objeto cuya vida útil pertenece a la persona que llama a partir de ese momento. Cualquier objeto que implemente el PointerWrapper el rasgo se puede devolver, y proporcionamos implementaciones para Caja y Arco, por lo que los desarrolladores que utilizan los punteros idiomáticos asignados en montón o contados por referencias de Rust no tienen requisitos adicionales.

    Todas las funciones asociadas en Operaciones de archivo recibir referencias no mutables a uno mismo (más sobre esto a continuación), excepto el lanzamiento function, que es la última función que se llama y recibe el objeto plano (y su propiedad con él). La lanzamiento La implementación puede entonces aplazar la destrucción del objeto transfiriendo su propiedad a otro lugar, o destruirlo en ese momento; en el caso de un objeto con recuento de referencia, "destrucción" significa disminuir el recuento de referencia (y la destrucción real del objeto si el recuento llega a cero).

    Es decir, usamos la disciplina de propiedad de Rust cuando interactuamos con el código C al entregar la propiedad de la parte C de un objeto Rust, lo que le permite llamar a funciones implementadas en Rust y, finalmente, devolver la propiedad. Entonces, siempre que el código C sea correcto, la vida útil de los objetos de archivo de Rust también funciona sin problemas, con el compilador aplicando una administración de vida útil correcta en el lado de Rust, por ejemplo: open no puede devolver punteros asignados a la pila ni objetos asignados al montón que contengan punteros a la pila, ioctl/leer/escribir no puede liberar (o modificar sin sincronización) el contenido del objeto almacenado en filp-> datos_privadosetc.

  • Referencias no mutables: las funciones asociadas llamadas entre abierto y lanzamiento todos reciben referencias inmutables a uno mismo porque pueden ser llamados simultáneamente por varios subprocesos y las reglas de alias de Rust prohíben más de una referencia mutable a un objeto en un momento dado.

    Si un desarrollador necesita modificar algún estado (y generalmente lo hace), puede hacerlo a través de mutabilidad interior: el estado mutable se puede envolver en un Mutex o SpinLock (o atomística) y modificados de forma segura a través de ellos.

    Esto evita, en tiempo de compilación, errores en los que un desarrollador no logra adquirir el candado apropiado al acceder a un campo (el campo es inaccesible), o cuando un desarrollador no puede envolver un campo con un candado (el campo es de solo lectura).

  • Estado por dispositivo: cuando las instancias de archivos necesitan compartir el estado por dispositivo, que es una ocurrencia muy común en los controladores, pueden hacerlo de forma segura en Rust. Cuando se registra un dispositivo, se puede proporcionar un objeto mecanografiado y se proporciona una referencia no mutable al mismo cuando FileOperation :: abrir se llama. En nuestro ejemplo, el objeto compartido está envuelto en Arco, para que los archivos se puedan clonar de forma segura y conservar una referencia a ellos.

    La razón FileOperation es su propio rasgo (a diferencia de, por ejemplo, abierto ser parte de la Operaciones de archivo rasgo) es permitir que la implementación de un solo archivo se registre de diferentes maneras.

    Esto elimina las oportunidades para que los desarrolladores obtengan los datos incorrectos cuando intentan recuperar el estado compartido. Por ejemplo, en C cuando un misceláneo está registrado, hay un puntero disponible en filp-> datos_privados; cuando una cdev está registrado, hay un puntero disponible en inodo-> i_cdev. Estas estructuras suelen estar incrustadas en una estructura externa que contiene el estado compartido, por lo que los desarrolladores suelen utilizar la contenedor_de macro para recuperar el estado compartido. Rust encapsula todo esto y el puntero potencialmente problemático se convierte en una abstracción segura.

  • Escritura estática: aprovechamos el soporte de Rust para genéricos para implementar todas las funciones y tipos anteriores con tipos estáticos. Por lo tanto, no hay oportunidades para que un desarrollador convierta una variable o campo sin tipo en el tipo incorrecto. El código C en la tabla anterior tiene conversiones de una (vacío *) puntero al tipo deseado al comienzo de cada función: es probable que funcione bien cuando se escribe por primera vez, pero puede provocar errores a medida que el código evoluciona y las suposiciones cambian. Rust detectaría tales errores en tiempo de compilación.

  • Operaciones de archivo: como mencionamos antes, un desarrollador necesita implementar el Operaciones de archivo rasgo para personalizar el comportamiento de su dispositivo. Hacen esto con un bloque que comienza con impl FileOperations para dispositivo, dónde Dispositivo es el tipo que implementa el comportamiento del archivo (FileState en nuestro ejemplo). Una vez dentro de este bloque, las herramientas saben que solo se puede definir un número limitado de funciones, por lo que pueden insertar automáticamente los prototipos. (Personalmente, uso neovim y el analizador de herrumbre Servidor LSP.)

    Si bien usamos este rasgo en Rust, la parte C del kernel aún requiere una instancia de struct file_operations. La caja del núcleo genera automáticamente uno a partir de la implementación del rasgo (y, opcionalmente, el declare_file_operations macro): aunque tiene código para generar la estructura correcta, es todo constante, por lo que se evalúa en tiempo de compilación con un costo de tiempo de ejecución cero.

Manejo de Ioctl

Para que un conductor proporcione un ioctl controlador, necesita implementar el ioctl función que es parte de la Operaciones de archivo rasgo, como se ejemplifica en la tabla siguiente.

fn ioctl(
    &self,
    file: &File,
    cmd: &mut IoctlCommand
) -> KernelResult<i32> 
    cmd.dispatch(self, file)

 
impl IoctlHandler for FileState 
    fn read(
        &self,
        _file: &File,
        cmd: u32,
        writer: &mut UserSlicePtrWriter
    ) -> KernelResult<i32> 
        match cmd 
            IOCTL_GET_READ_COUNT => 
                writer.write(
                    &self
                    .read_count
                    .load(Ordering::Relaxed))?;
                Ok(0)
            
            _ => Err(Error::EINVAL),
        
    
 
    fn write(
        &self,
        _file: &File,
        cmd: u32,
        reader: &mut UserSlicePtrReader
    ) -> KernelResult<i32> 
        match cmd 
            IOCTL_SET_READ_COUNT => 
                self
                .read_count
                .store(reader.read()?,
                       Ordering::Relaxed);
                Ok(0)
            
            _ => Err(Error::EINVAL),
        
    
#define IOCTL_GET_READ_COUNT _IOR('c', 1, u64)
#define IOCTL_SET_READ_COUNT _IOW('c', 1, u64)
 
static
long semaphore_ioctl(struct file *filp,
                     unsigned int cmd,
                     unsigned long arg)

    struct file_state *state = filp->private_data;
    void __user *buffer = (void __user *)arg;
    u64 value;
 
    switch (cmd) 
    case IOCTL_GET_READ_COUNT:
        value = atomic64_read(&state->read_count);
        if (copy_to_user(buffer, &value, sizeof(value)))
            return -EFAULT;
        return 0;
    case IOCTL_SET_READ_COUNT:
        if (copy_from_user(&value, buffer, sizeof(value)))
            return -EFAULT;
        atomic64_set(&state->read_count, value);
        return 0;
    default:
        return -EINVAL;
    

Los comandos de Ioctl están estandarizados de modo que, dado un comando, sabemos si se proporciona un búfer de usuario, su uso previsto (lectura, escritura, ambos, ninguno) y su tamaño. En Rust, proporcionamos un despachador (accesible llamando cmd.dispatch) que utiliza esta información para crear automáticamente ayudantes de acceso a la memoria del usuario y pasarlos a la persona que llama.

Un conductor no es requerido para usar esto sin embargo. Si, por ejemplo, no usa la codificación estándar ioctl, Rust ofrece la flexibilidad de simplemente llamar cmd.raw para extraer los argumentos sin procesar y usarlos para manejar el ioctl (potencialmente con código inseguro, que deberá justificarse).

Sin embargo, si la implementación de un controlador usa el despachador estándar, se beneficiará de no tener que implementar ningún código inseguro y:

  • El puntero a la memoria del usuario nunca es un puntero nativo, por lo que el desarrollador no puede desreferenciarlo accidentalmente.
  • Los tipos que permiten que el controlador lea desde el espacio del usuario solo permiten que los datos se lean una vez, por lo que eliminamos el riesgo de errores de tiempo de verificación a tiempo de uso (TOCTOU) porque cuando un controlador necesita acceder a los datos dos veces, necesita copiarlo a la memoria del kernel, donde un atacante no puede modificarlo. Excluyendo los bloques inseguros, no hay forma de introducir esta clase de errores en Rust.
  • Sin desbordamiento accidental del búfer del usuario: nunca leeremos o escribiremos más allá del final del búfer del usuario porque esto se aplica automáticamente en función del tamaño codificado en el comando ioctl. En nuestro ejemplo anterior, la implementación de IOCTL_GET_READ_COUNT solo tiene acceso a una instancia de UserSlicePtrWriter, que limita el número de bytes de escritura a tamaño de (u64) como se codifica en el comando ioctl.
  • Sin mezcla de lecturas y escrituras: nunca escribiremos búferes para ioctls que solo estén destinados a leer y nunca leamos búferes para ioctls que solo estén destinados a escribir. Esto se aplica mediante controladores de lectura y escritura que solo obtienen instancias de UserSlicePtrWriter y UserSlicePtrReader respectivamente.

Potencialmente, todo lo anterior también podría hacerse en C, pero es muy fácil para los desarrolladores romper (probablemente sin querer) los contratos que conducen a la inseguridad; El óxido requiere inseguro bloques para esto, que solo deben usarse en casos raros y conllevan un escrutinio adicional. Además, Rust ofrece lo siguiente:

  • Los tipos utilizados para leer y escribir la memoria del usuario no implementan la Enviar y Sincronizar rasgos, lo que significa que ellos (y los apuntadores a ellos) no son seguros para ser utilizados en otro contexto de hilo. En Rust, si un desarrollador de controladores intentaba escribir código que pasaba uno de estos objetos a otro hilo (donde no sería seguro usarlos porque no está necesariamente en el contexto correcto del administrador de memoria), obtendría una compilación error.
  • Al llamar IoctlCommand :: despacho, uno podría pensar comprensiblemente que necesitamos un envío dinámico para alcanzar la implementación del controlador real (lo que incurriría en un costo adicional en comparación con C), pero no es así. Nuestro uso de genéricos llevará al compilador a monomorfizar la función, lo que resultará en llamadas a funciones estáticas que incluso se pueden insertar si el optimizador así lo desea.

Variables de bloqueo y condición

Permitimos que los desarrolladores utilicen mutex y spinlocks para proporcionar mutabilidad interior. En nuestro ejemplo, usamos un mutex para proteger los datos mutables; En las tablas a continuación, mostramos las estructuras de datos que usamos en C y Rust, y cómo implementamos una espera hasta que el conteo sea distinto de cero para que podamos satisfacer una lectura:

struct SemaphoreInner 
    count: usize,
    max_seen: usize,

 
struct Semaphore 
    changed: CondVar,
    inner: Mutex,

 
struct FileState 
    read_count: AtomicU64,
    shared: Arc,
struct semaphore_state 
    struct kref ref;
    struct miscdevice miscdev;
    wait_queue_head_t changed;
    struct mutex mutex;
    size_t count;
    size_t max_seen;
;
 
struct file_state 
    atomic64_t read_count;
    struct semaphore_state *shared;
;

fn consume(&self) -> KernelResult 
    let mut inner = self.shared.inner.lock();
    while inner.count == 0 
        if self.shared.changed.wait(&mut inner) 
            return Err(Error::EINTR);
        
    
    inner.count -= 1;
    Ok(())
static int semaphore_consume(
    struct semaphore_state *state)

    DEFINE_WAIT(wait);
 
    mutex_lock(&state->mutex);
    while (state->count == 0) 
        prepare_to_wait(&state->changed, &wait,
                        TASK_INTERRUPTIBLE);
        mutex_unlock(&state->mutex);
        schedule();
        finish_wait(&state->changed, &wait);
        if (signal_pending(current))
            return -EINTR;
        mutex_lock(&state->mutex);
    
 
    state->count--;
    mutex_unlock(&state->mutex);
 
    return 0;

Observamos que tales esperas no son infrecuentes en el código C existente, por ejemplo, un tubo esperando para que un "socio" escriba, socket de dominio Unix en espera para datos, un búsqueda de inodo en espera para completar una eliminación o un ayudante en modo de usuario esperando para el cambio de estado.

Los siguientes son beneficios de la implementación de Rust:

  • La Semáforo :: interior El campo solo es accesible cuando se mantiene el candado, a través del guardia devuelto por el cerrar con llave función. Por lo tanto, los desarrolladores no pueden leer o escribir datos protegidos accidentalmente sin bloquearlos primero. En el ejemplo de C anterior, contar y max_seen en semaphore_state están protegidos por mutex, pero no hay ninguna aplicación de que el bloqueo se mantenga mientras se accede a ellos.
  • La adquisición de recursos es inicialización (RAII): la cerradura se desbloquea automáticamente cuando el guardia (interno en este caso) queda fuera de alcance. Esto asegura que las cerraduras estén siempre desbloqueadas: si el desarrollador necesita mantener una cerradura cerrada, puede mantener la guardia viva, por ejemplo, devolviendo la guardia misma; a la inversa, si necesitan desbloquear antes del final del alcance, pueden hacerlo explícitamente llamando al soltar función.
  • Los desarrolladores pueden utilizar cualquier bloqueo que implemente Cerrar con llave rasgo, que incluye Mutex y SpinLock, sin costo de tiempo de ejecución adicional en comparación con una implementación de C. Otras construcciones de sincronización, incluidas las variables de condición, también funcionan de manera transparente y sin costo adicional de tiempo de ejecución.
  • Rust implementa variables de condición usando colas de espera del kernel. Esto permite a los desarrolladores beneficiarse de la liberación atómica del bloqueo y poner el hilo en suspensión sin tener que razonar sobre las funciones del programador del kernel de bajo nivel. En el ejemplo de C anterior, semaphore_consume es una mezcla de lógica de semáforo y programación sutil de Linux: por ejemplo, el código es incorrecto si mutex_unlock se llama antes prepare_to_wait porque puede resultar en que no te despiertes.
  • Sin acceso no sincronizado: como mencionamos anteriormente, las variables compartidas por múltiples subprocesos / CPU deben ser de solo lectura, y la mutabilidad interior es la solución para los casos en que se necesita mutabilidad. Además del ejemplo con bloqueos anterior, el ejemplo de ioctl en la sección anterior también tiene un ejemplo de uso de una variable atómica; Rust también requiere que los desarrolladores especifiquen cómo se debe utilizar la memoria. estar sincronizado por accesos atómicos. En la parte C del ejemplo, usamos atomic64_t, pero el compilador no alertará al desarrollador sobre esta necesidad.

Manejo de errores y flujo de control

En las tablas siguientes, mostramos cómo abierto, leer, y escribir se implementan en nuestro controlador de ejemplo:

fn read(
    &self,
    _: &File,
    data: &mut UserSlicePtrWriter,
    offset: u64
) -> KernelResult<usize>  offset > 0 
        return Ok(0);
    
 
    self.consume()?;
    data.write_slice(&(0u8; 1))?;
    self.read_count.fetch_add(1, Ordering::Relaxed);
    Ok(1)

 
static
ssize_t semaphore_read(struct file *filp,
                       char __user *buffer,
                       size_t count, loff_t *ppos)
 *ppos > 0)
        return 0;
 
    ret = semaphore_consume(state->shared);
    if (ret)
        return ret;
 
    if (copy_to_user(buffer, &c, sizeof(c)))
        return -EFAULT;
 
    atomic64_add(1, &state->read_count);
    *ppos += 1;
    return 1;

fn write(
    &self,
    data: &mut UserSlicePtrReader,
    _offset: u64
) -> KernelResult<usize> 
   
        let mut inner = self.shared.inner.lock();
        inner.count = inner.count.saturating_add(data.len());
        if inner.count > inner.max_seen 
            inner.max_seen = inner.count;
        
    
 
    self.shared.changed.notify_all();
    Ok(data.len())
static
ssize_t semaphore_write(struct file *filp,
                        const char __user *buffer,
                        size_t count, loff_t *ppos)

    struct file_state *state = filp->private_data;
    struct semaphore_state *shared = state->shared;
 
    mutex_lock(&shared->mutex);
    shared->count += count;
    if (shared->count < count)
        shared->count = SIZE_MAX;
 
    if (shared->count > shared->max_seen)
        shared->max_seen = shared->count;
 
    mutex_unlock(&shared->mutex);
 
    wake_up_all(&shared->changed);
    return count;

fn open(
    shared: &Arc
) -> KernelResult<Box<Self>> 
    Ok(Box::try_new(Self 
        read_count: AtomicU64::new(0),
        shared: shared.clone(),
    )?)
static 
int semaphore_open(struct inode *nodp,
                   struct file *filp)

    struct semaphore_state *shared =
        container_of(filp->private_data,
                     struct semaphore_state,
                     miscdev);
    struct file_state *state;
 
    state = kzalloc(sizeof(*state), GFP_KERNEL);
    if (!state)
        return -ENOMEM;
 
    kref_get(&shared->ref);
    state->shared = shared;
    atomic64_set(&state->read_count, 0);
 
    filp->private_data = state;
 
    return 0;

Ilustran otros beneficios aportados por Rust:

  • La ? operador: es utilizado por el Rust abierto y leer implementaciones para hacer el manejo de errores implícitamente; el desarrollador puede centrarse en la lógica del semáforo, el código resultante es bastante pequeño y legible. Las versiones C tienen ruido de manejo de errores que puede hacerlas menos legibles.
  • Inicialización requerida: Rust requiere que todos los campos de una estructura se inicialicen en la construcción, por lo que el desarrollador nunca puede fallar accidentalmente al inicializar un campo; C no ofrece tal facilidad. En nuestro abierto ejemplo anterior, el desarrollador de la versión C podría fácilmente no llamar kref_get (aunque todos los campos se habrían inicializado); en Rust, el usuario debe llamar clon (que incrementa el recuento de referencias), de lo contrario, obtienen un error de compilación.
  • Alcance de RAII: la implementación de escritura de Rust usa un bloque de instrucciones para controlar cuándo interno sale del alcance y, por lo tanto, se libera el bloqueo.
  • Comportamiento de desbordamiento de enteros: Rust alienta a los desarrolladores a considerar siempre cómo se deben manejar los desbordamientos. En nuestro escribir Por ejemplo, queremos una saturación para que no terminemos con un valor cero al agregar a nuestro semáforo. En C, necesitamos verificar manualmente los desbordamientos, no hay soporte adicional del compilador.

Los ejemplos anteriores son solo una pequeña parte de todo el proyecto. Esperamos que les brinde a los lectores una idea de los tipos de beneficios que brinda Rust. Por el momento, tenemos casi toda la funcionalidad del kernel genérica que Binder necesita cuidadosamente envuelta en abstracciones seguras de Rust, por lo que estamos en el proceso de recopilar comentarios de la comunidad más amplia del kernel de Linux con la intención de mejorar el soporte existente de Rust.

También continuamos avanzando en nuestro prototipo de Binder, implementando abstracciones adicionales y suavizando algunos aspectos ásperos. Este es un momento emocionante y una oportunidad única para influir potencialmente en cómo se desarrolla el kernel de Linux, así como informar la evolución del lenguaje Rust. Invitamos a los interesados ​​a unirse a nosotros Rust para Linux y asista a nuestra charla planificada en Conferencia de fontaneros de Linux 2021!

Gracias a Nick Desaulniers, Kees Cook y Adrian Taylor por sus contribuciones a esta publicación. Un agradecimiento especial a Jeff Vander Stoep por sus contribuciones y edición, y a Greg Kroah-Hartman por revisar y contribuir a los ejemplos de código.



Enlace a la noticia original