Linux Kernel 5.0-在blk-mq下编写简单块设备

大家好消息!

Linux内核5.0已经在这里并出现在实验发行版中,例如Arch,openSUSE Tumbleweed,Fedora。



而且,如果您查看Ubuntu Disko Dingo和Red Hat 8的RC发行版,则很清楚:很快内核5.0也将从风扇台式机转移到重要的服务器上。
有人会说-那么。 下一个版本,没什么特别的。 所以莱纳斯·托瓦尔兹本人说:
我想再次指出,我们不进行基于功能的发行,“ 5.0”的含义不外乎是4.x数字开始变得足够大,以至于我筋疲力尽和脚趾。

我再说一遍-我们的发行版不与任何特定功能绑定,因此新版本5.0的编号仅意味着对4.x版本编号,我已经没有足够的手指和脚趾了

但是,用于软盘的模块(不知道-这些是大小为前胸口袋衬衫的磁盘,容量为1.44 MB)-已更正...
这就是为什么:

这都是关于多队列块层(blk-mq)的。 互联网上有很多关于他的介绍性文章,所以让我们直接讲一下。 向blk-mq的过渡很久以前就开始了,并正在缓慢地向前发展。 多队列scsi(内核参数scsi_mod.use_blk_mq)出现了,新的调度程序mq-deadline,bfq等出现了……

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

顺便问一下,你是什么人?

减少了使用旧版本的块设备驱动程序的数量。 在5.0中,blk_init_queue()函数被不必要地删除了。 现在,2003年的旧光荣代码lwn.net/Articles/58720不仅不可用,而且失去了相关性。 此外,准备在今年发布的新发行版在默认配置中使用了多队列块层。 例如,在第18个Manjaro上,内核(尽管版本为4.19)默认为blk-mq。

因此,我们可以假设内核5.0中向blk-mq的过渡已经完成。 对我来说,这是一个重要事件,需要重写代码和进行其他测试。 它本身承诺会出现大小不一的错误以及几个崩溃的服务器(这是必要的,Fedya,这是必须的!(C))。

顺便说一句,如果有人认为对于rhel8来说,这个临界点没有到来,因为那里的内核是由4.18版“刷新”的,那么您就错了。 在rhel8上的最新RC中,已经移植了5.0以后的新产品,并且blk_init_queue()函数也已被删除(可能是在将另一个签入从github.com/torvalds/linux拖到其源时)。
通常,长期以来,Linux发行商(如SUSE和Red Hat)的内核的“冻结”版本。 例如,系统报告版本为4.4,实际上该功能来自新的4.8 vanilla。 同时,在官方网站上标有一个题词,例如:“在新发行版中,我们为您保留了稳定的4.4内核。”

但是我们分心了...

所以在这里。 我们需要一个新的简单块设备驱动程序,以使其更清楚地工作。
因此,源代码在github.com/CodeImp/sblkdev上 。 我建议讨论,提出拉动请求,开始发行-我将解决它。 质量检查尚未测试。

在本文的后面,我将尝试描述原因。 因此,有很多代码。
我很抱歉没有完全尊重Linux内核编码风格,是的-我不喜欢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_exit()时启动sblkdev_init()函数。
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; 

它包含有关内核模块必需的设备的所有信息,尤其是:块设备的容量,数据本身(这很简单),指向磁盘的指针和队列。

所有块设备初始化都在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()函数的源代码,您会发现这只是对4.20内核中出现的blk_mq_alloc_tag_set()和blk_mq_init_queue()函数的包装。 另外,它为我们隐藏了队列的许多参数,但看起来却简单得多。 您必须选择更好的选择,但是我更喜欢一个更明确的选择。

此代码中的关键是全局变量_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位内核和32位用户空间环境的系统版本似乎并不理想。

但是_ioctl允许您处理该驱动器的系统请求。 出现磁盘时,系统将尝试了解更多信息。 您可以自行决定回答一些查询(例如,假装是一张新CD),但是一般规则是:如果您不想回答您不感兴趣的查询,只需返回错误代码-ENOTTY。 顺便说一下,如有必要,您可以在此处添加有关此特定驱动器的请求处理程序。

因此,我们添加了设备-我们需要注意资源的释放。 Rust不在这里

 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 } 

首先,考虑参数。 第一个是struct 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不再记得的时代所提出的要求相同。 因此,我们接受了请求并开始处理它,然后我们通过调用blk_mq_start_request()通知内核。 完成请求处理后,我们将通过调用blk_mq_end_request()函数来通知内核。

这里有个小提示:blk_mq_end_request()函数本质上是对blk_update_request()+ __blk_mq_end_request()的调用的包装。 使用blk_mq_end_request()函数时,您无法指定实际处理的字节数。 相信一切都已处理。

替代选项具有另一个功能:仅针对仅GPL模块导出blk_update_request函数。 也就是说,如果要创建专有的内核模块(让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,它们都具有bio_vec结构,使我们能够访问带有请求数据的页面。

您的印象如何? 一切看起来都很简单? 通常,请求处理只是在请求的页面和内部缓冲区之间复制数据。 非常值得一个简单的块设备驱动程序,对吗?

但是有一个问题: 这不是真正的用途!

问题的实质是在处理列表中请求的循环中调用了queue_rq()请求处理函数。 我不知道该列表使用了哪个锁,Spin或RCU(我不想说谎-谁知道,请纠正我),但是例如,当您尝试在请求处理功能中使用互斥锁时,调试内核会发誓并警告:打ze睡在这里是不可能的。 也就是说,由于进程无法进入待机状态,因此无法使用常规的同步工具或虚拟连续内存(使用vmalloc分配的虚拟内存,并且可以与其所暗示的所有内容进行交换)。

因此,如.. \ linux \ drivers \ block \ brd.c中实现的那样,只有Spin或RCU锁以及页面,列表或树形式的缓冲区(如在.. \ linux \ drivers \ block \ brd.c中实现),或在另一个线程中的延迟处理(如在.. \ linux中实现)。 \ linux \驱动程序\块\ loop.c.

我认为无需描述如何组装模块,如何将其加载到系统中以及如何卸载。 这方面没有新产品,谢谢您:)因此,如果有人想尝试一下,我一定会弄清楚的。 只是不要立即在您喜欢的笔记本电脑上做! 举起virtualochka或至少在球上做个备份。

顺便说一句,适用于Linux的Veeam Backup 3.0.1.1046已经可用。 只是不要尝试在内核5.0或更高版本上运行VAL 3.0.1.1046。 veeamsnap无法组装。 而且一些多队列创新仍处于测试阶段。

Source: https://habr.com/ru/post/zh-CN446148/


All Articles