Linux Kernel 5.0 - كتابة Simple Block Device تحت blk-mq

أخبار جيدة ، الجميع!

يوجد Linux kernel 5.0 بالفعل ويظهر في توزيعات تجريبية مثل Arch و openSUSE Tumbleweed و Fedora.



وإذا نظرت إلى توزيعات RC في Ubuntu Disko Dingo و Red Hat 8 ، يصبح الأمر واضحًا: قريبًا ستنقل kernel 5.0 أيضًا من أسطح مكتب المعجبين إلى خوادم خطيرة.
سيقول شخص ما - ماذا في ذلك. الإصدار التالي ، لا شيء خاص. لذلك قال لينوس تورفالدس نفسه:
أود أن أوضح (مرة أخرى) أننا لا نقوم بالإصدارات القائمة على الميزات ، وأن الرقم "5.0" لا يعني أي شيء أكثر من أن أرقام 4.x بدأت تصبح كبيرة بما يكفي لدرجة أنني نفدت أصابعها وأصابع القدم.

( أكرر مرة أخرى - إصداراتنا ليست مرتبطة بأي ميزات محددة ، وبالتالي فإن رقم الإصدار الجديد 5.0 يعني فقط أنه بالنسبة إلى ترقيم الإصدارات 4.x ليس لدي أصابع وأصابع قدم كافية )

ومع ذلك ، تم تصحيح الوحدة النمطية للأقراص المرنة (التي لا تعرف - هذه هي الأقراص بحجم قميص جيب الصدر ، بسعة 1.44 ميغابايت) - ...
وهنا السبب:

الأمر كله يتعلق بطبقة بلوك قائمة الانتظار المتعددة (blk-mq). هناك الكثير من المقالات التمهيدية حوله على الإنترنت ، لذلك دعونا نصل إلى هذه النقطة. بدأ الانتقال إلى blk-mq منذ فترة طويلة وكان يتقدم ببطء إلى الأمام. ظهرت SCSI متعدد الطواب (معلمة kernel scsi_mod.use_blk_mq) ، وظهرت جدولة جديدة mq-الموعد النهائي ، bfq وهلم جرا ...

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

بالمناسبة ، ما هو لك؟

تم تخفيض عدد برامج تشغيل الأجهزة التي تعمل بالطريقة القديمة. وفي 5.0 ، تمت إزالة الدالة blk_init_queue () غير ضرورية. والآن الكود المجيد القديم lwn.net/Articles/58720 من عام 2003 ليس فقط لن ، ولكن أيضا فقدت أهميتها. علاوة على ذلك ، فإن التوزيعات الجديدة ، التي يتم إعدادها للإصدار هذا العام ، تستخدم طبقة كتلة قائمة انتظار متعددة في التكوين الافتراضي. على سبيل المثال ، في 18 Manjaro ، النواة ، على الرغم من أن الإصدار 4.19 ، هو blk-mq بشكل افتراضي.

لذلك ، يمكننا أن نفترض أن الانتقال إلى blk-mq في kernel 5.0 قد اكتمل. وهذا بالنسبة لي حدث مهم يتطلب إعادة كتابة الرمز واختبار إضافي. وهو في حد ذاته يعد ظهور الخلل كبيرًا وصغيرًا ، بالإضافة إلى العديد من الخوادم المتعطلة (من الضروري ، Fedya ، إنه ضروري! (C)).

بالمناسبة ، إذا اعتقد شخص ما أنه بالنسبة لـ rhel8 ، فإن نقطة التحول هذه لم تأت ، نظرًا لأن النواة "تم وميضها" بالإصدار 4.18 هناك ، فأنت مخطئ. في تطبيق RC الجديد على rhel8 ، تم بالفعل ترحيل منتجات جديدة من الإصدار 5.0 ، وتم أيضًا حذف وظيفة blk_init_queue () (ربما عند سحب تسجيل وصول آخر من github.com/torvalds/linux إلى مصادرها).
بشكل عام ، لطالما كان الإصدار "التجميد" من kernel لموزعي Linux مثل SUSE و Red Hat مفهومًا للتسويق. يبلغ النظام أن الإصدار ، على سبيل المثال ، هو 4.4 ، والواقع أن الوظيفة هي من 4.8 فانيليا جديدة. في الوقت نفسه ، يتكبر نقش على الموقع الرسمي مثل: "في التوزيع الجديد ، حافظنا على نواة مستقرة 4.4".

ولكن كنا مشتتا ...

لذلك هنا. نحتاج إلى برنامج تشغيل جهاز كتلة بسيط جديد لجعله أكثر وضوحا كيف يعمل هذا.
لذلك ، المصدر على github.com/CodeImp/sblkdev . أقترح مناقشة ، وجعل طلبات السحب ، وبدء المشكلة - أنا سوف إصلاحه. ضمان الجودة لم تختبر بعد.

في وقت لاحق من المقالة سأحاول وصف ما هو السبب. لذلك ، هناك الكثير من التعليمات البرمجية.
أعتذر على الفور عن عدم احترام أسلوب ترميز Linux kernel بشكل كامل ، ونعم - لا أحب goto.

لذلك ، لنبدأ من نقاط الدخول.

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

من الواضح ، عند تحميل الوحدة النمطية ، يتم تشغيل وظيفة sblkdev_init () ، عند إلغاء تحميل sblkdev_exit ().
تسجل الدالة register_blkdev () جهاز كتلة. تم تخصيص رقم كبير له. unregister_blkdev () - يحرر هذا الرقم.

الهيكل الرئيسي لوحدتنا هو 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; 

أنه يحتوي على جميع المعلومات حول الجهاز اللازمة لوحدة kernel ، على وجه الخصوص: سعة جهاز الكتلة ، والبيانات نفسها (وهذا بسيط) ، ومؤشرات على القرص وقائمة الانتظار.

يتم إجراء كل تهيئة جهاز الكتلة في دالة 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; } 

نحن نخصص ذاكرة للهيكل ، ونخصص مخزنًا مؤقتًا لتخزين البيانات. لا شيء خاص هنا.
بعد ذلك ، نقوم بتهيئة قائمة انتظار معالجة الطلب إما باستخدام دالة blk_mq_init_sq_queue () واحدة أو اثنتين في آن واحد: blk_mq_alloc_tag_set () + blk_mq_init_queue ().

بالمناسبة ، إذا نظرت إلى مصادر الدالات blk_mq_init_sq_queue () ، فسترى أن هذا مجرد غلاف على دالات blk_mq_alloc_tag_set () و blk_mq_init_queue () التي ظهرت في 4.20 kernel. بالإضافة إلى ذلك ، فإنه يخفينا العديد من معلمات قائمة الانتظار ، ولكن يبدو أبسط بكثير. يجب عليك اختيار الخيار الأفضل ، لكنني أفضل خيارًا أكثر وضوحًا.

المفتاح في هذا الرمز هو المتغير العمومي _mq_ops.

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

هذا هو المكان الذي توجد فيه الوظيفة التي توفر معالجة الطلبات ، ولكن المزيد عنها في وقت لاحق. الشيء الرئيسي هو أننا قمنا بتعيين نقطة الدخول إلى معالج الطلب.

الآن وقد أنشأنا قائمة الانتظار ، يمكننا إنشاء مثيل للقرص.

لا توجد تغييرات كبيرة. يتم تخصيص القرص ، ويتم تعيين المعلمات ، ويضاف القرص إلى النظام. أريد أن أشرح حول المعلمة disk-> الأعلام. يسمح لك بإخبار النظام أن القرص قابل للإزالة ، أو على سبيل المثال ، أنه لا يحتوي على أقسام ولا تحتاج إلى البحث عنها هناك.

هناك بنية _fops لإدارة القرص.

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

نقاط الدخول _open و _release بالنسبة لنا لوحدة جهاز كتلة بسيطة ليست مثيرة للاهتمام للغاية حتى الآن. بالإضافة إلى العداد الذري والنقصان ، لا يوجد شيء هناك. لقد تركت compat_ioctl أيضًا دون تطبيق ، لأن إصدار الأنظمة التي تحتوي على 64-bit kern وبيئة المستخدم 32 بت لا يبدو واعداً بالنسبة لي.

لكن _ioctl يسمح لك بمعالجة طلبات النظام لمحرك الأقراص هذا. عندما يظهر قرص ، يحاول النظام معرفة المزيد عنه. وفقًا لتقديرك الخاص ، يمكنك الإجابة على بعض الاستعلامات (على سبيل المثال ، التظاهر بأنها قرص مضغوط جديد) ، لكن القاعدة العامة هي: إذا كنت لا ترغب في الإجابة على استفسارات لا تهمك ، فقم فقط بإرجاع رمز الخطأ -ENOTTY. بالمناسبة ، إذا لزم الأمر ، يمكنك هنا إضافة معالجات طلبك فيما يتعلق بمحرك الأقراص هذا.

لذلك ، أضفنا الجهاز - نحن بحاجة لرعاية الافراج عن الموارد. الصدأ ليس هنا من أجلك.

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

من حيث المبدأ ، كل شيء واضح: نقوم بحذف كائن القرص من النظام وتحرير قائمة الانتظار ، وبعد ذلك نقوم أيضًا بتحرير مخازننا المؤقتة (مناطق البيانات).

والأهم الآن هو معالجة الاستعلام في دالة 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 } 

أولا ، النظر في المعلمات. الأول هو البنية blk_mq_hw_ctx * hctx - حالة قائمة انتظار الأجهزة. في حالتنا ، ونحن نفعل دون قائمة انتظار الأجهزة ، لذلك غير مستخدمة.

المعلمة الثانية هي const struct blk_mq_queue_data * bd - معلمة ذات بنية موجزة للغاية ، ولا أخشى أن أوجه انتباهكم إليها بالكامل:

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

اتضح أنه في جوهره ، هذا هو نفس الطلب الذي جاء إلينا من الأوقات التي لم يعد يتذكرها المؤرخ elixir.bootlin.com . لذلك نحن نأخذ الطلب ونبدأ في معالجته ، والذي نعلم به kernel عن طريق الاتصال بـ blk_mq_start_request (). عند الانتهاء من معالجة الطلب ، سنبلغ kernel بهذا عن طريق استدعاء دالة blk_mq_end_request ().

فيما يلي ملاحظة صغيرة: الدالة blk_mq_end_request () هي أساسًا التفاف على المكالمات إلى blk_update_request () + __blk_mq_end_request (). عند استخدام دالة blk_mq_end_request () ، لا يمكنك تحديد عدد البايتات التي تمت معالجتها بالفعل. يعتقد أن كل شيء يتم معالجته.

يحتوي الخيار البديل على ميزة أخرى: يتم تصدير الدالة blk_update_request فقط للوحدات النمطية GPL فقط. وهذا هو ، إذا كنت ترغب في إنشاء وحدة kernel خاصة (اسمح PM بحفظك من هذا المسار الشائك) ، لا يمكنك استخدام blk_update_request (). وبالتالي فإن الخيار لك.

تحويل البايت مباشرة من الطلب إلى المخزن المؤقت والعكس بالعكس أضعه في الدالة 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; } 

لا يوجد شيء جديد: تتكرر rq_for_each_segment على كل شيء حيوي ، ولديها جميعًا هياكل bio_vec ، مما يسمح لنا بالوصول إلى الصفحات التي تحتوي على بيانات الطلب.

ما هي انطباعاتك؟ كل شيء يبدو بسيطا؟ معالجة الطلبات بشكل عام هي ببساطة نسخ البيانات بين صفحات الطلب والمخزن المؤقت الداخلي. يستحق تماما لبرنامج تشغيل جهاز كتلة بسيطة ، أليس كذلك؟

ولكن هناك مشكلة: هذا ليس للاستخدام الحقيقي!

يتمثل جوهر المشكلة في استدعاء دالة معالجة طلب queue_rq () في حلقة تقوم بمعالجة الطلبات من القائمة. لا أعرف أي قفل لهذه القائمة يتم استخدامه هناك ، Spin أو RCU (لا أريد أن أكذب - من يدري ، يصححني) ، لكن عندما تحاول استخدام ، على سبيل المثال ، mutex في وظيفة معالجة الطلب ، فإن kernel تصحيح الأخطاء يحذر: doze off هنا هو مستحيل. وهذا يعني أن استخدام أدوات المزامنة التقليدية أو الذاكرة المتجاورة الافتراضية - تلك التي يتم تخصيصها باستخدام vmalloc ويمكن أن تقع في المبادلة بكل ما تتضمنه - أمر مستحيل ، حيث لا يمكن أن تدخل العملية في حالة الاستعداد.

لذلك ، إما تأمين Spin أو RCU فقط ومخزن مؤقت في شكل صفيف من الصفحات ، أو قائمة ، أو شجرة ، كما هو مطبق في .. \ linux \ drivers \ block \ brd.c ، أو تأخر المعالجة في مؤشر ترابط آخر ، كما هو مطبق في .. \ linux \ drivers \ block \ loop.c.

أعتقد أنه ليست هناك حاجة لوصف كيفية تجميع الوحدة ، وكيفية تحميلها في النظام وكيفية التفريغ. لا توجد منتجات جديدة في هذا المجال ، شكرًا على ذلك :) لذا إذا كان هناك من يريد تجربته ، فستكون متأكدًا من ذلك. لا تفعل ذلك على الفور على الكمبيوتر المحمول المفضل لديك! رفع virtualochka أو على الأقل عمل نسخة احتياطية على الكرة.

بالمناسبة ، Veeam Backup for Linux 3.0.1.1046 متاح بالفعل. فقط لا تحاول تشغيل VAL 3.0.1.1046 على نواة 5.0 أو في وقت لاحق. سوف veeamsnap لا تجميع. وما زالت بعض الابتكارات متعددة الصفوف في مرحلة الاختبار.

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


All Articles