Linux Kernel 5.0: escribir un dispositivo de bloque simple en blk-mq

¡Buenas noticias para todos!

Linux kernel 5.0 ya está aquí y aparece en distribuciones experimentales como Arch, openSUSE Tumbleweed, Fedora.



Y si nos fijamos en las distribuciones RC de Ubuntu Disko Dingo y Red Hat 8, queda claro: pronto el kernel 5.0 también se transferirá de los escritorios de los fanáticos a los servidores serios.
Alguien dirá, y qué. El próximo lanzamiento, nada especial. Entonces Linus Torvalds mismo dijo:
Me gustaría señalar (una vez más) que no hacemos lanzamientos basados ​​en funciones, y que "5.0" no significa nada más que eso, los números 4.x comenzaron a ser lo suficientemente grandes como para que se me acabaran los dedos y dedos de los pies

( Una vez más, repito: nuestras versiones no están vinculadas a ninguna característica específica, por lo que el número de la nueva versión 5.0 significa solo que para la numeración de versiones 4.x ya no tengo suficientes dedos de manos y pies )

Sin embargo, el módulo para disquetes (quién no sabe, estos son discos del tamaño de una camisa de bolsillo con una capacidad de 1,44 MB), corregido ...
Y aquí está el por qué:

Se trata de una capa de bloque de múltiples colas (blk-mq). Hay muchos artículos introductorios sobre él en Internet, así que vayamos directamente al grano. La transición a blk-mq se inició hace mucho tiempo y avanzaba lentamente. Multi-cola scsi (parámetro del núcleo scsi_mod.use_blk_mq) apareció, aparecieron nuevos planificadores mq-deadline, bfq y así sucesivamente ...

[root@fedora-29 sblkdev]# cat /sys/block/sda/queue/scheduler [mq-deadline] none 

Por cierto, ¿cuál es el tuyo?

Se redujo el número de controladores de dispositivos de bloque que funcionan a la antigua usanza. Y en 5.0, la función blk_init_queue () se eliminó como innecesaria. Y ahora el antiguo código glorioso lwn.net/Articles/58720 de 2003 no solo no va a, sino que también pierde relevancia. Además, las nuevas distribuciones, que se están preparando para su lanzamiento este año, usan una capa de bloque de múltiples colas en la configuración predeterminada. Por ejemplo, en el 18º Manjaro, el núcleo, aunque la versión 4.19, es blk-mq por defecto.

Por lo tanto, podemos suponer que la transición a blk-mq en el kernel 5.0 se ha completado. Y para mí este es un evento importante que requerirá reescribir el código y realizar pruebas adicionales. Lo que en sí mismo promete la aparición de errores grandes y pequeños, así como varios servidores bloqueados (¡Es necesario, Fedya, es necesario! (C)).

Por cierto, si alguien piensa que para rhel8 este punto de inflexión no llegó, ya que el núcleo fue "flasheado" por la versión 4.18 allí, entonces está equivocado. En RC nuevo en rhel8, los nuevos productos de 5.0 ya habían migrado, y la función blk_init_queue () también se cortó (probablemente al arrastrar otro registro desde github.com/torvalds/linux a sus fuentes).
En general, la versión "congelada" del kernel para distribuidores de Linux como SUSE y Red Hat ha sido durante mucho tiempo un concepto de marketing. El sistema informa que la versión, por ejemplo, es 4.4, y de hecho la funcionalidad es de un nuevo 4.8 de vainilla. Al mismo tiempo, una inscripción hace alarde en el sitio web oficial como: "En la nueva distribución, hemos mantenido un núcleo 4.4 estable para usted".

Pero estábamos distraídos ...

Entonces aquí. Necesitamos un nuevo controlador de dispositivo de bloque simple para aclarar cómo funciona esto.
Entonces, la fuente en github.com/CodeImp/sblkdev . Propongo discutir, hacer solicitudes de extracción, iniciar el problema; lo solucionaré. El control de calidad aún no se ha probado.

Más adelante en el artículo intentaré describir por qué. Por lo tanto, hay mucho código.
Me disculpo de inmediato porque el estilo de codificación del kernel de Linux no se respeta por completo, y sí, no me gusta ir a.

Entonces, comencemos por los puntos de entrada.

 static int __init sblkdev_init(void) { int ret = SUCCESS; _sblkdev_major = register_blkdev(_sblkdev_major, _sblkdev_name); if (_sblkdev_major <= 0){ printk(KERN_WARNING "sblkdev: unable to get major number\n"); return -EBUSY; } ret = sblkdev_add_device(); if (ret) unregister_blkdev(_sblkdev_major, _sblkdev_name); return ret; } static void __exit sblkdev_exit(void) { sblkdev_remove_device(); if (_sblkdev_major > 0) unregister_blkdev(_sblkdev_major, _sblkdev_name); } module_init(sblkdev_init); module_exit(sblkdev_exit); 

Obviamente, cuando se carga el módulo, se inicia la función sblkdev_init (), cuando se descarga sblkdev_exit ().
La función register_blkdev () registra un dispositivo de bloque. Se le asigna un número mayor. unregister_blkdev (): libera este número.

La estructura clave de nuestro módulo es sblkdev_device_t.

 // The internal representation of our device typedef struct sblkdev_device_s { sector_t capacity; // Device size in bytes u8* data; // The data aray. u8 - 8 bytes atomic_t open_counter; // How many openers struct blk_mq_tag_set tag_set; struct request_queue *queue; // For mutual exclusion struct gendisk *disk; // The gendisk structure } sblkdev_device_t; 

Contiene toda la información sobre el dispositivo necesaria para el módulo del núcleo, en particular: la capacidad del dispositivo de bloque, los datos en sí (esto es simple), los punteros al disco y la cola.

Toda la inicialización del dispositivo de bloque se realiza en la función sblkdev_add_device ().

 static int sblkdev_add_device(void) { int ret = SUCCESS; sblkdev_device_t* dev = kzalloc(sizeof(sblkdev_device_t), GFP_KERNEL); if (dev == NULL) { printk(KERN_WARNING "sblkdev: unable to allocate %ld bytes\n", sizeof(sblkdev_device_t)); return -ENOMEM; } _sblkdev_device = dev; do{ ret = sblkdev_allocate_buffer(dev); if(ret) break; #if 0 //simply variant with helper function blk_mq_init_sq_queue. It`s available from kernel 4.20 (vanilla). {//configure tag_set struct request_queue *queue; dev->tag_set.cmd_size = sizeof(sblkdev_cmd_t); dev->tag_set.driver_data = dev; queue = blk_mq_init_sq_queue(&dev->tag_set, &_mq_ops, 128, BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_SG_MERGE); if (IS_ERR(queue)) { ret = PTR_ERR(queue); printk(KERN_WARNING "sblkdev: unable to allocate and initialize tag set\n"); break; } dev->queue = queue; } #else // more flexible variant {//configure tag_set dev->tag_set.ops = &_mq_ops; dev->tag_set.nr_hw_queues = 1; dev->tag_set.queue_depth = 128; dev->tag_set.numa_node = NUMA_NO_NODE; dev->tag_set.cmd_size = sizeof(sblkdev_cmd_t); dev->tag_set.flags = BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_SG_MERGE; dev->tag_set.driver_data = dev; ret = blk_mq_alloc_tag_set(&dev->tag_set); if (ret) { printk(KERN_WARNING "sblkdev: unable to allocate tag set\n"); break; } } {//configure queue struct request_queue *queue = blk_mq_init_queue(&dev->tag_set); if (IS_ERR(queue)) { ret = PTR_ERR(queue); printk(KERN_WARNING "sblkdev: Failed to allocate queue\n"); break; } dev->queue = queue; } #endif dev->queue->queuedata = dev; {// configure disk struct gendisk *disk = alloc_disk(1); //only one partition if (disk == NULL) { printk(KERN_WARNING "sblkdev: Failed to allocate disk\n"); ret = -ENOMEM; break; } disk->flags |= GENHD_FL_NO_PART_SCAN; //only one partition //disk->flags |= GENHD_FL_EXT_DEVT; disk->flags |= GENHD_FL_REMOVABLE; disk->major = _sblkdev_major; disk->first_minor = 0; disk->fops = &_fops; disk->private_data = dev; disk->queue = dev->queue; sprintf(disk->disk_name, "sblkdev%d", 0); set_capacity(disk, dev->capacity); dev->disk = disk; add_disk(disk); } printk(KERN_WARNING "sblkdev: simple block device was created\n"); }while(false); if (ret){ sblkdev_remove_device(); printk(KERN_WARNING "sblkdev: Failed add block device\n"); } return ret; } 

Asignamos memoria para la estructura, asignamos un búfer para almacenar datos. Nada especial aquí.
A continuación, inicializamos la cola de procesamiento de solicitudes con una función blk_mq_init_sq_queue (), o dos a la vez: blk_mq_alloc_tag_set () + blk_mq_init_queue ().

Por cierto, si observa las fuentes de la función blk_mq_init_sq_queue (), verá que esto es solo un contenedor sobre las funciones blk_mq_alloc_tag_set () y blk_mq_init_queue (), que aparecieron en el núcleo 4.20. Además, nos oculta muchos parámetros de la cola, pero parece mucho más simple. Tienes que elegir qué opción es mejor, pero prefiero una más explícita.

La clave en este código es la variable global _mq_ops.

 static struct blk_mq_ops _mq_ops = { .queue_rq = queue_rq, }; 

Aquí es donde se encuentra la función que proporciona el procesamiento de solicitudes, pero más sobre esto un poco más tarde. Lo principal es que hemos designado el punto de entrada para el controlador de solicitudes.

Ahora que hemos creado la cola, podemos crear una instancia del disco.

No hay cambios importantes. El disco se asigna, los parámetros se configuran y el disco se agrega al sistema. Quiero explicar sobre el parámetro disk-> flags. Le permite decirle al sistema que el disco es extraíble o, por ejemplo, que no contiene particiones y que no necesita buscarlas allí.

Hay una estructura _fops para la administración de discos.

 static const struct block_device_operations _fops = { .owner = THIS_MODULE, .open = _open, .release = _release, .ioctl = _ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = _compat_ioctl, #endif }; 

Los puntos de entrada _open y _release para nosotros para un módulo de dispositivo de bloque simple aún no son muy interesantes. Además del contador de incremento y decremento atómico, no hay nada allí. También dejé compat_ioctl sin implementación, ya que la versión de sistemas con un núcleo de 64 bits y un entorno de espacio de usuario de 32 bits no me parece prometedora.

Pero _ioctl le permite procesar solicitudes del sistema para esta unidad. Cuando aparece un disco, el sistema intenta obtener más información al respecto. Puede responder algunas consultas como mejor le parezca (por ejemplo, pretender ser un nuevo CD), pero la regla general es esta: si no desea responder consultas que no le interesan, simplemente devuelva el código de error -ENOTTY. Por cierto, si es necesario, aquí puede agregar sus controladores de solicitudes con respecto a esta unidad en particular.

Por lo tanto, agregamos el dispositivo: debemos ocuparnos de la liberación de los recursos. El óxido no está aquí para ti.

 static void sblkdev_remove_device(void) { sblkdev_device_t* dev = _sblkdev_device; if (dev){ if (dev->disk) del_gendisk(dev->disk); if (dev->queue) { blk_cleanup_queue(dev->queue); dev->queue = NULL; } if (dev->tag_set.tags) blk_mq_free_tag_set(&dev->tag_set); if (dev->disk) { put_disk(dev->disk); dev->disk = NULL; } sblkdev_free_buffer(dev); kfree(dev); _sblkdev_device = NULL; printk(KERN_WARNING "sblkdev: simple block device was removed\n"); } } 

En principio, todo es obvio: eliminamos el objeto de disco del sistema y liberamos la cola, después de lo cual también liberamos nuestros búferes (áreas de datos).

Y ahora lo más importante es el procesamiento de consultas en la función queue_rq ().

 static blk_status_t queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data* bd) { blk_status_t status = BLK_STS_OK; struct request *rq = bd->rq; blk_mq_start_request(rq); //we cannot use any locks that make the thread sleep { unsigned int nr_bytes = 0; if (do_simple_request(rq, &nr_bytes) != SUCCESS) status = BLK_STS_IOERR; printk(KERN_WARNING "sblkdev: request process %d bytes\n", nr_bytes); #if 0 //simply and can be called from proprietary module blk_mq_end_request(rq, status); #else //can set real processed bytes count if (blk_update_request(rq, status, nr_bytes)) //GPL-only symbol BUG(); __blk_mq_end_request(rq, status); #endif } return BLK_STS_OK;//always return ok } 

Primero, considere los parámetros. El primero es struct blk_mq_hw_ctx * hctx: el estado de la cola del hardware. En nuestro caso, lo hacemos sin la cola de hardware, por lo que no se utiliza.

El segundo parámetro es const struct blk_mq_queue_data * bd, un parámetro con una estructura muy concisa, que no tengo miedo de presentarle a su atención en su totalidad:

 struct blk_mq_queue_data { struct request *rq; bool last; }; 

Resulta que, en esencia, esta es la misma solicitud que nos llegó desde tiempos que el cronista elixir.bootlin.com ya no recuerda. Entonces tomamos la solicitud y comenzamos a procesarla, sobre lo cual notificamos al kernel llamando a blk_mq_start_request (). Al finalizar el procesamiento de la solicitud, informaremos al núcleo sobre esto llamando a la función blk_mq_end_request ().

Aquí hay una pequeña nota: la función blk_mq_end_request () es esencialmente una envoltura sobre las llamadas a blk_update_request () + __blk_mq_end_request (). Al usar la función blk_mq_end_request (), no puede especificar cuántos bytes se procesaron realmente. Cree que todo se procesa.

La opción alternativa tiene otra característica: la función blk_update_request se exporta solo para módulos solo GPL. Es decir, si desea crear un módulo de kernel propietario (deje que PM lo salve de esta ruta espinosa), no puede usar blk_update_request (). Entonces la elección es tuya.

Cambiando directamente los bytes de la solicitud al búfer y viceversa, puse en la función do_simple_request ().

 static int do_simple_request(struct request *rq, unsigned int *nr_bytes) { int ret = SUCCESS; struct bio_vec bvec; struct req_iterator iter; sblkdev_device_t *dev = rq->q->queuedata; loff_t pos = blk_rq_pos(rq) << SECTOR_SHIFT; loff_t dev_size = (loff_t)(dev->capacity << SECTOR_SHIFT); printk(KERN_WARNING "sblkdev: request start from sector %ld \n", blk_rq_pos(rq)); rq_for_each_segment(bvec, rq, iter) { unsigned long b_len = bvec.bv_len; void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset; if ((pos + b_len) > dev_size) b_len = (unsigned long)(dev_size - pos); if (rq_data_dir(rq))//WRITE memcpy(dev->data + pos, b_buf, b_len); else//READ memcpy(b_buf, dev->data + pos, b_len); pos += b_len; *nr_bytes += b_len; } return ret; } 

No hay nada nuevo: rq_for_each_segment itera sobre todo bio, y todos tienen estructuras bio_vec, lo que nos permite llegar a las páginas con los datos de la solicitud.

¿Cuáles son tus impresiones? ¿Todo parece simple? El procesamiento de solicitudes en general es simplemente copiar datos entre las páginas de la solicitud y el búfer interno. Muy digno de un simple controlador de dispositivo de bloque, ¿verdad?

Pero hay un problema: ¡ Esto no es para uso real!

La esencia del problema es que la función de procesamiento de solicitudes queue_rq () se llama en un bucle que procesa las solicitudes de la lista. No sé qué bloqueo para esta lista se usa allí, Spin o RCU (no quiero mentir, quién sabe, corríjame), pero cuando intenta usar, por ejemplo, mutex en la función de procesamiento de solicitudes, el núcleo de depuración jura y advierte: dormita Aquí es imposible. Es decir, usar herramientas de sincronización convencionales o memoria contigua virtual, una que se asigna usando vmalloc y puede intercambiarse con todo lo que implica, es imposible, ya que el proceso no puede pasar al estado de espera.

Por lo tanto, ya sea solo bloqueos Spin o RCU y un búfer en forma de una matriz de páginas, o una lista, o un árbol, como se implementa en .. \ linux \ drivers \ block \ brd.c, o el procesamiento retrasado en otro hilo, como se implementa en .. \ linux \ drivers \ block \ loop.c.

Creo que no hay necesidad de describir cómo ensamblar el módulo, cómo cargarlo en el sistema y cómo descargarlo. No hay nuevos productos en este frente, y gracias por eso :) Así que si alguien quiere probarlo, me aseguraré de resolverlo. ¡Simplemente no lo hagas de inmediato en tu computadora portátil favorita! Levanta un virtualochka o al menos haz una copia de seguridad de una pelota.

Por cierto, Veeam Backup para Linux 3.0.1.1046 ya está disponible. Simplemente no intente ejecutar VAL 3.0.1.1046 en un kernel 5.0 o posterior. veeamsnap no se ensamblará. Y algunas innovaciones de múltiples colas todavía están en la etapa de prueba.

Source: https://habr.com/ru/post/446148/


All Articles