Linux Kernel 5.0 - écriture de Simple Block Device sous blk-mq

Bonne nouvelle à tous!

Le noyau Linux 5.0 est déjà là et apparaît dans des distributions expérimentales telles que Arch, openSUSE Tumbleweed, Fedora.



Et si vous regardez les distributions RC d'Ubuntu Disko Dingo et de Red Hat 8, alors cela devient clair: bientôt le noyau 5.0 sera également transféré des bureaux de fans aux serveurs sérieux.
Quelqu'un dira - alors quoi. La prochaine version, rien de spécial. Donc Linus Torvalds lui-même a dit:
Je voudrais souligner (encore une fois) que nous ne faisons pas de versions basées sur les fonctionnalités, et que "5.0" ne signifie rien de plus que le fait que les nombres 4.x ont commencé à devenir suffisamment gros pour que je manque de doigts et les orteils.

( Encore une fois, je le répète - nos versions ne sont liées à aucune fonctionnalité spécifique, donc le numéro de la nouvelle version 5.0 signifie seulement que pour la numérotation des versions 4.x, je n'ai déjà pas assez de doigts et d'orteils )

Cependant, le module pour les disquettes (qui ne sait pas - ce sont des disques de la taille d'une chemise de poche poitrine, d'une capacité de 1,44 Mo) - corrigé ...
Et voici pourquoi:

Il s'agit de la couche de bloc multi-file d'attente (blk-mq). Il existe de nombreux articles d'introduction à son sujet sur Internet, alors allons droit au but. La transition vers blk-mq a commencé il y a longtemps et progressait lentement. Scsi multi-file d'attente (paramètre du noyau scsi_mod.use_blk_mq) est apparu, de nouveaux ordonnanceurs mq-deadline, bfq et ainsi de suite sont apparus…

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

Au fait, quel est le vôtre?

Le nombre de pilotes de périphériques de blocs fonctionnant à l'ancienne a été réduit. Et dans 5.0, la fonction blk_init_queue () a été supprimée car inutile. Et maintenant, l'ancien code glorieux lwn.net/Articles/58720 de 2003 ne va pas non plus, mais perd également sa pertinence. De plus, les nouvelles distributions, qui sont en cours de préparation pour être publiées cette année, utilisent une couche de bloc à files d'attente multiples dans la configuration par défaut. Par exemple, le 18 Manjaro, le noyau, bien que la version 4.19, est blk-mq par défaut.

Par conséquent, nous pouvons supposer que la transition vers blk-mq dans le noyau 5.0 est terminée. Et pour moi, c'est un événement important qui nécessitera la réécriture du code et des tests supplémentaires. Ce qui en soi promet l'apparition de bugs grands et petits, ainsi que plusieurs serveurs en panne (Il faut, Fedya, c'est nécessaire! (C)).

Soit dit en passant, si quelqu'un pense que pour rhel8, ce point de basculement n'est pas venu, puisque le noyau a été "flashé" par la version 4.18, alors vous vous trompez. Dans la nouvelle RC sur rhel8, les nouveaux produits de 5.0 avaient déjà migré et la fonction blk_init_queue () avait également été supprimée (probablement lors du déplacement d'un autre enregistrement de github.com/torvalds/linux vers ses sources).
En général, la version «gel» du noyau pour les distributeurs Linux tels que SUSE et Red Hat est depuis longtemps un concept marketing. Le système signale que la version, par exemple, est 4.4, et en fait la fonctionnalité provient d'une nouvelle vanille 4.8. Dans le même temps, une inscription s'affiche sur le site officiel comme: "Dans la nouvelle distribution, nous avons gardé un noyau 4.4 stable pour vous."

Mais nous étions distraits ...

Alors voilà. Nous avons besoin d'un nouveau pilote de périphérique de bloc simple pour rendre plus clair comment cela fonctionne.
Donc, la source sur github.com/CodeImp/sblkdev . Je propose de discuter, de faire des demandes de tirage, de lancer le problème - je vais le réparer. QA n'a pas encore testé.

Plus loin dans l'article, j'essaierai de décrire ce pourquoi. Par conséquent, il y a beaucoup de code.
Je m'excuse tout de suite que le style de codage du noyau Linux n'est pas entièrement respecté, et oui - je n'aime pas goto.

Commençons donc par les points d'entrée.

 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); 

Évidemment, lorsque le module est chargé, la fonction sblkdev_init () est lancée, lorsque sblkdev_exit () est déchargée.
La fonction register_blkdev () enregistre un périphérique de bloc. Il se voit attribuer un numéro majeur. unregister_blkdev () - libère ce nombre.

La structure clé de notre module est 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; 

Il contient toutes les informations sur le périphérique nécessaires au module noyau, en particulier: la capacité du périphérique bloc, les données elles-mêmes (c'est simple), les pointeurs vers le disque et la file d'attente.

Toutes les initialisations de périphérique de bloc sont effectuées dans la fonction 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; } 

Nous allouons de la mémoire à la structure, allouons un tampon pour stocker les données. Rien de spécial ici.
Ensuite, nous initialisons la file d'attente de traitement des demandes avec soit une fonction blk_mq_init_sq_queue (), soit deux à la fois: blk_mq_alloc_tag_set () + blk_mq_init_queue ().

Soit dit en passant, si vous regardez le code source de la fonction blk_mq_init_sq_queue (), vous verrez qu'il ne s'agit que d'un wrapper sur les fonctions blk_mq_alloc_tag_set () et blk_mq_init_queue (), qui sont apparues dans le noyau 4.20. De plus, il nous cache de nombreux paramètres de la file d'attente, mais il semble beaucoup plus simple. Vous devez choisir quelle option est la meilleure, mais je préfère une option plus explicite.

La clé de ce code est la variable globale _mq_ops.

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

C'est là que se trouve la fonction qui assure le traitement des demandes, mais plus à ce sujet plus tard. L'essentiel est que nous ayons désigné le point d'entrée du gestionnaire de requêtes.

Maintenant que nous avons créé la file d'attente, nous pouvons créer une instance du disque.

Il n'y a pas de changements majeurs. Le disque est alloué, les paramètres sont définis et le disque est ajouté au système. Je veux expliquer le paramètre disk-> flags. Il vous permet d'indiquer au système que le disque est amovible ou, par exemple, qu'il ne contient pas de partitions et que vous n'avez pas besoin de les rechercher à cet endroit.

Il existe une structure _fops pour la gestion des disques.

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

Les points d'entrée _open et _release pour nous pour un module de module bloc simple ne sont pas encore très intéressants. En plus du compteur d'incrémentation et de décrémentation atomique, il n'y a rien. J'ai également laissé compat_ioctl sans implémentation, car la version des systèmes avec un noyau 64 bits et un environnement d'espace utilisateur 32 bits ne me semble pas prometteuse.

Mais _ioctl vous permet de traiter les demandes du système pour ce lecteur. Lorsqu'un disque apparaît, le système essaie d'en savoir plus. Vous pouvez répondre à certaines requêtes comme bon vous semble (par exemple, pour faire semblant d'être un nouveau CD), mais la règle générale est la suivante: si vous ne souhaitez pas répondre à des requêtes qui ne vous intéressent pas, renvoyez simplement le code d'erreur -ENOTTY. Au fait, si nécessaire, vous pouvez ajouter ici vos gestionnaires de demandes concernant ce lecteur particulier.

Nous avons donc ajouté l'appareil - nous devons nous occuper de la libération des ressources. La rouille n'est pas là pour vous.

 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 principe, tout est évident: nous supprimons l'objet disque du système et libérons la file d'attente, après quoi nous libérons également nos tampons (zones de données).

Et maintenant, la chose la plus importante est le traitement des requêtes dans la fonction 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 } 

Considérons d'abord les paramètres. Le premier est struct blk_mq_hw_ctx * hctx - l'état de la file d'attente matérielle. Dans notre cas, nous nous passons de la file d'attente matérielle, donc inutilisée.

Le deuxième paramètre est const struct blk_mq_queue_data * bd - un paramètre avec une structure très concise, que je n'ai pas peur de présenter à votre attention dans son intégralité:

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

Il s'avère que c'est essentiellement la même demande, qui nous est venue de moments dont le chroniqueur elixir.bootlin.com ne se souvient plus. Nous prenons donc la requête et commençons à la traiter, dont nous informons le noyau en appelant blk_mq_start_request (). À la fin du traitement de la demande, nous en informerons le noyau en appelant la fonction blk_mq_end_request ().

Voici une petite note: la fonction blk_mq_end_request () est essentiellement un wrapper sur les appels à blk_update_request () + __blk_mq_end_request (). Lorsque vous utilisez la fonction blk_mq_end_request (), vous ne pouvez pas spécifier le nombre d'octets réellement traités. Estime que tout est traité.

L'option alternative a une autre fonctionnalité: la fonction blk_update_request est exportée uniquement pour les modules GPL uniquement. Autrement dit, si vous souhaitez créer un module de noyau propriétaire (laissez PM vous sauver de ce chemin épineux), vous ne pouvez pas utiliser blk_update_request (). Donc, le choix vous appartient.

Décalant directement les octets de la demande vers le tampon et vice versa, je mets dans la fonction 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; } 

Il n'y a rien de nouveau: rq_for_each_segment itère sur tout ce qui est bio, et ils ont tous des structures bio_vec, nous permettant d'accéder aux pages avec les données de la requête.

Quelles sont vos impressions? Tout semble simple? Le traitement des demandes en général consiste simplement à copier des données entre les pages de la demande et le tampon interne. Tout à fait digne d'un simple pilote de périphérique de bloc, non?

Mais il y a un problème: ce n'est pas pour une utilisation réelle!

L'essence du problème est que la fonction de traitement des demandes queue_rq () est appelée dans une boucle qui traite les demandes de la liste. Je ne sais pas quel verrou pour cette liste est utilisé ici, Spin ou RCU (je ne veux pas mentir - qui sait, corrigez-moi), mais lorsque vous essayez d'utiliser, par exemple, mutex dans la fonction de traitement des demandes, le noyau de débogage jure et avertit: somnoler ici c'est impossible. Autrement dit, il est impossible d'utiliser des outils de synchronisation conventionnels ou de la mémoire virtuelle contiguë - celle qui est allouée à l'aide de vmalloc et peut basculer avec tout ce qu'elle implique -, car le processus ne peut pas passer en état de veille.

Par conséquent, seuls les verrous Spin ou RCU et un tampon sous la forme d'un tableau de pages, ou une liste, ou une arborescence, comme implémenté dans .. \ linux \ drivers \ block \ brd.c, ou un traitement retardé dans un autre thread, comme implémenté dans .. \ linux \ drivers \ block \ loop.c.

Je pense qu'il n'est pas nécessaire de décrire comment assembler le module, comment le charger dans le système et comment le décharger. Il n'y a pas de nouveaux produits sur ce front, et merci pour ça :) Donc, si quelqu'un veut l'essayer, je serai sûr de le découvrir. Ne le faites pas tout de suite sur votre ordinateur portable préféré! Soulevez une virtualochka ou faites au moins une sauvegarde sur une balle.

Soit dit en passant, Veeam Backup pour Linux 3.0.1.1046 est déjà disponible. N'essayez simplement pas d'exécuter VAL 3.0.1.1046 sur un noyau 5.0 ou ultérieur. veeamsnap ne s'assemble pas. Et certaines innovations multi-files d'attente sont encore au stade des tests.

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


All Articles