Linux Kernel 5.0 - Schreiben von Simple Block Device unter blk-mq

Gute Nachrichten, alle zusammen!

Der Linux-Kernel 5.0 ist bereits vorhanden und erscheint in experimentellen Distributionen wie Arch, openSUSE Tumbleweed und Fedora.



Und wenn Sie sich die RC-Distributionen von Ubuntu Disko Dingo und Red Hat 8 ansehen, wird klar: Bald wird Kernel 5.0 auch von Fan-Desktops auf seriöse Server übertragen.
Jemand wird sagen - na und. Die nächste Veröffentlichung, nichts Besonderes. Also sagte Linus Torvalds selbst:
Ich möchte (noch einmal) darauf hinweisen, dass wir keine funktionsbasierten Releases machen und dass „5.0“ nichts anderes bedeutet, als dass die 4.x-Zahlen so groß wurden, dass mir die Finger ausgegangen sind und Zehen.

( Ich wiederhole noch einmal: Unsere Releases sind nicht an bestimmte Funktionen gebunden, daher bedeutet die Nummer der neuen Version 5.0 nur, dass ich für die Nummerierung der Versionen 4.x bereits nicht genügend Finger und Zehen habe. )

Das Modul für Disketten (wer weiß nicht - das sind Platten von der Größe eines Brusttaschenhemdes mit einer Kapazität von 1,44 MB) - korrigiert ...
Und hier ist warum:

Es dreht sich alles um die Blockschicht mit mehreren Warteschlangen (blk-mq). Im Internet gibt es viele einführende Artikel über ihn. Kommen wir also gleich zur Sache. Der Übergang zu blk-mq wurde vor langer Zeit begonnen und ging langsam voran. Multi-Queue-scsi (Kernel-Parameter scsi_mod.use_blk_mq) erschien, neue Scheduler mq-Deadline, bfq und so weiter erschienen ...

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

Übrigens, was ist deins?

Die Anzahl der Blockgerätetreiber, die auf die alte Art und Weise funktionieren, wurde reduziert. In 5.0 wurde die Funktion blk_init_queue () als unnötig entfernt. Und jetzt wird der alte glorreiche Code lwn.net/Articles/58720 aus dem Jahr 2003 nicht nur nicht, sondern auch an Relevanz verlieren. Darüber hinaus verwenden die neuen Distributionen, die für die Veröffentlichung in diesem Jahr vorbereitet werden, in der Standardkonfiguration eine Blockschicht mit mehreren Warteschlangen. Beispielsweise ist der Kernel auf dem 18. Manjaro, obwohl Version 4.19, standardmäßig blk-mq.

Daher können wir davon ausgehen, dass der Übergang zu blk-mq in Kernel 5.0 abgeschlossen ist. Und für mich ist dies ein wichtiges Ereignis, bei dem der Code neu geschrieben und zusätzliche Tests durchgeführt werden müssen. Was an sich das Auftreten von großen und kleinen Fehlern sowie von mehreren abgestürzten Servern verspricht (Es ist notwendig, Fedya, es ist notwendig! (C)).

Übrigens, wenn jemand denkt, dass dieser Wendepunkt für rhel8 nicht gekommen ist, da der Kernel dort durch Version 4.18 "geflasht" wurde, dann irren Sie sich. In frischem RC auf rhel8 waren bereits neue Produkte von 5.0 migriert, und die Funktion blk_init_queue () wurde ebenfalls deaktiviert (wahrscheinlich beim Ziehen eines weiteren Check-ins von github.com/torvalds/linux zu seinen Quellen).
Im Allgemeinen ist die "Freeze" -Version des Kernels für Linux-Distributoren wie SUSE und Red Hat seit langem ein Marketingkonzept. Das System meldet, dass die Version beispielsweise 4.4 ist und die Funktionalität tatsächlich von einer frischen 4.8-Vanille stammt. Gleichzeitig wird auf der offiziellen Website eine Inschrift angezeigt: "In der neuen Distribution haben wir einen stabilen 4.4-Kernel für Sie aufbewahrt."

Aber wir waren abgelenkt ...

Also. Wir brauchen einen neuen einfachen Blockgerätetreiber, um klarer zu machen, wie das funktioniert.
Also die Quelle auf github.com/CodeImp/sblkdev . Ich schlage vor zu diskutieren, Pull-Anfragen zu stellen, Probleme zu starten - ich werde es beheben. QA wurde noch nicht getestet.

Später in diesem Artikel werde ich versuchen zu beschreiben, warum. Daher gibt es viel Code.
Ich entschuldige mich sofort dafür, dass der Codierungsstil des Linux-Kernels nicht vollständig respektiert wird, und ja - ich mag goto nicht.

Beginnen wir also mit den Einstiegspunkten.

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

Wenn das Modul geladen wird, wird natürlich die Funktion sblkdev_init () gestartet, wenn sblkdev_exit () entladen wird.
Die Funktion register_blkdev () registriert ein Blockgerät. Ihm wird eine Hauptnummer zugewiesen. unregister_blkdev () - gibt diese Nummer frei.

Die Schlüsselstruktur unseres Moduls ist 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; 

Es enthält alle Informationen über das Gerät, die für das Kernelmodul erforderlich sind, insbesondere: die Kapazität des Blockgeräts, die Daten selbst (es ist einfach), Zeiger auf die Festplatte und die Warteschlange.

Alle Blockgeräteinitialisierungen werden in der Funktion sblkdev_add_device () durchgeführt.

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

Wir weisen der Struktur Speicher zu, weisen einen Puffer zum Speichern von Daten zu. Hier ist nichts Besonderes.
Als Nächstes initialisieren wir die Anforderungsverarbeitungswarteschlange entweder mit einer blk_mq_init_sq_queue () -Funktion oder mit zwei gleichzeitig: blk_mq_alloc_tag_set () + blk_mq_init_queue ().

Übrigens, wenn Sie sich den Quellcode der Funktion blk_mq_init_sq_queue () ansehen, werden Sie sehen, dass dies nur ein Wrapper über die Funktionen blk_mq_alloc_tag_set () und blk_mq_init_queue () ist, die im Kernel 4.20 erschienen sind. Außerdem werden viele Parameter der Warteschlange ausgeblendet, aber es sieht viel einfacher aus. Sie müssen wählen, welche Option besser ist, aber ich bevorzuge eine explizitere.

Der Schlüssel in diesem Code ist die globale Variable _mq_ops.

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

Hier befindet sich die Funktion, die die Verarbeitung von Anforderungen ermöglicht, aber etwas später mehr dazu. Die Hauptsache ist, dass wir den Einstiegspunkt für den Request-Handler festgelegt haben.

Nachdem wir die Warteschlange erstellt haben, können wir eine Instanz der Festplatte erstellen.

Es gibt keine wesentlichen Änderungen. Die Festplatte wird zugewiesen, Parameter werden festgelegt und die Festplatte wird dem System hinzugefügt. Ich möchte den Parameter disk-> flags erläutern. Auf diese Weise können Sie dem System mitteilen, dass die Festplatte entfernbar ist oder beispielsweise keine Partitionen enthält und Sie dort nicht danach suchen müssen.

Es gibt eine _fops-Struktur für die Datenträgerverwaltung.

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

Die Einstiegspunkte _open und _release für uns für ein einfaches Blockgerätemodul sind noch nicht sehr interessant. Zusätzlich zum atomaren Inkrement- und Dekrementzähler gibt es dort nichts. Ich habe auch compatible_ioctl ohne Implementierung gelassen, da mir die Version von Systemen mit einem 64-Bit-Kernel und einer 32-Bit-User-Space-Umgebung nicht vielversprechend erscheint.

Mit _ioctl können Sie jedoch Systemanforderungen für dieses Laufwerk verarbeiten. Wenn eine Festplatte angezeigt wird, versucht das System, mehr darüber zu erfahren. Sie können einige Fragen nach Belieben beantworten (z. B. um sich als neue CD auszugeben). Die allgemeine Regel lautet jedoch: Wenn Sie keine Fragen beantworten möchten, die für Sie nicht von Interesse sind, geben Sie einfach den Fehlercode -ENOTTY zurück. Übrigens können Sie hier bei Bedarf Ihre Request-Handler zu diesem bestimmten Laufwerk hinzufügen.

Also haben wir das Gerät hinzugefügt - wir müssen uns um die Freigabe von Ressourcen kümmern. Rust ist nicht für dich da.

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

Im Prinzip ist alles klar: Wir löschen das Festplattenobjekt aus dem System und geben die Warteschlange frei. Danach geben wir auch unsere Puffer (Datenbereiche) frei.

Und jetzt ist das Wichtigste die Abfrageverarbeitung in der Funktion 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 } 

Betrachten Sie zunächst die Parameter. Die erste ist struct blk_mq_hw_ctx * hctx - der Status der Hardware-Warteschlange. In unserem Fall verzichten wir auf die Hardware-Warteschlange, die nicht verwendet wird.

Der zweite Parameter ist const struct blk_mq_queue_data * bd - ein Parameter mit einer sehr prägnanten Struktur, den ich Ihnen nicht in seiner Gesamtheit vorstellen möchte:

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

Es stellt sich heraus, dass dies im Wesentlichen dieselbe Anfrage ist, die uns aus Zeiten gestellt wurde, an die sich der Chronist elixir.bootlin.com nicht mehr erinnert. Also nehmen wir die Anfrage und beginnen mit der Verarbeitung, über die wir den Kernel benachrichtigen, indem wir blk_mq_start_request () aufrufen. Nach Abschluss der Anforderungsverarbeitung werden wir den Kernel darüber informieren, indem wir die Funktion blk_mq_end_request () aufrufen.

Hier ein kleiner Hinweis: Die Funktion blk_mq_end_request () ist im Wesentlichen ein Wrapper für Aufrufe von blk_update_request () + __blk_mq_end_request (). Bei Verwendung der Funktion blk_mq_end_request () können Sie nicht angeben, wie viele Bytes tatsächlich verarbeitet wurden. Glaubt, dass alles verarbeitet wird.

Die alternative Option verfügt über eine weitere Funktion: Die Funktion blk_update_request wird nur für Nur-GPL-Module exportiert. Das heißt, wenn Sie ein proprietäres Kernelmodul erstellen möchten (lassen Sie sich von PM vor diesem heiklen Pfad retten), können Sie blk_update_request () nicht verwenden. Sie haben also die Wahl.

Direktes Verschieben der Bytes von der Anfrage in den Puffer und umgekehrt habe ich in die Funktion do_simple_request () eingefügt.

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

Es gibt nichts Neues: rq_for_each_segment iteriert über alles Bio, und alle haben bio_vec-Strukturen, sodass wir zu den Seiten mit den Anforderungsdaten gelangen können.

Was sind deine Eindrücke? Alles scheint einfach? Bei der Anforderungsverarbeitung werden im Allgemeinen einfach Daten zwischen den Seiten der Anforderung und dem internen Puffer kopiert. Für einen einfachen Blockgerätetreiber durchaus würdig, oder?

Aber es gibt ein Problem: Dies ist nicht für den wirklichen Gebrauch!

Das Problem besteht im Wesentlichen darin, dass die Anforderungsverarbeitungsfunktion queue_rq () in einer Schleife aufgerufen wird, die Anforderungen aus der Liste verarbeitet. Ich weiß nicht, welche Sperre für diese Liste dort verwendet wird, Spin oder RCU (ich möchte nicht lügen - wer weiß, korrigiere mich), aber wenn Sie versuchen, beispielsweise Mutex in der Anforderungsverarbeitungsfunktion zu verwenden, schwört und warnt der Debugging-Kernel: Einschlafen hier ist es unmöglich. Das heißt, die Verwendung herkömmlicher Synchronisationstools oder eines virtuellen zusammenhängenden Speichers - einer, der mit vmalloc zugewiesen wird und mit allem, was dies impliziert, in einen Austausch geraten kann - ist unmöglich, da der Prozess nicht in den Standby-Zustand versetzt werden kann.

Daher entweder nur Spin- oder RCU-Sperren und ein Puffer in Form eines Seitenarrays oder einer Liste oder eines Baums, wie in .. \ linux \ drivers \ block \ brd.c implementiert, oder verzögerte Verarbeitung in einem anderen Thread, wie in .. implementiert. \ linux \ drivers \ block \ loop.c.

Ich denke, es ist nicht nötig zu beschreiben, wie man das Modul zusammenbaut, wie man es in das System lädt und wie man es entlädt. An dieser Front gibt es keine neuen Produkte, und danke dafür :) Wenn also jemand es ausprobieren möchte, werde ich es sicher herausfinden. Mach es einfach nicht sofort auf deinem Lieblingslaptop! Erhöhen Sie eine Virtualochka oder machen Sie zumindest ein Backup auf einem Ball.

Veeam Backup für Linux 3.0.1.1046 ist übrigens bereits verfügbar. Versuchen Sie einfach nicht, VAL 3.0.1.1046 auf einem Kernel 5.0 oder höher auszuführen. veeamsnap wird nicht zusammengebaut. Einige Innovationen in mehreren Warteschlangen befinden sich noch in der Testphase.

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


All Articles