Linux块设备开发详解及代码(版本3.10.0)

不同于字设备,高效的块驱动对于性能至关重要,它是核心内存和二级存储之间的管道,所以块层的设计必定围绕性能。

  找源码的,请直接往下翻(在3.10.0版本可编译使用)。

1.   注册编号

大部分块驱动采取的第一步是注册它们自己到内核. 这个任务的函数是 register_blkdev(在 <block/genhd. h> 中定义):

int register_blkdev(unsigned int major, const char *name)

参数是设备要使用的主编号和关联的名子(内核将显示它在 /proc/devices). 如果 major 传递为 0, 内核分配一个新的主编号并且返回它给调用者,用于临时性的设备注册,如果自己选择的话该值在1-255之间。参数name在系统中必须唯一。 执行成功返回0,否则返回负的返回值。

对应的注销函数是:

void unregister_blkdev(unsigned int major, const char *name)

注册的作用是:分配一个动态主编号,并且在/proc/devices创建一个入口。

2.   注册磁盘

register_blkdev 可用来获得一个主编号, 但没有磁盘驱动器对系统可用 。需要注册一个磁盘,先来看下块设备的操作集合。

块设备上是 struct block_device_operations(字符设备通过 file_ 操作结构), 定义在 <include/linux/blkdev.h>

struct block_device_operations {

        int (*open) (struct block_device *, fmode_t);

        void (*release) (struct gendisk *, fmode_t);

        int (*rw_page)(struct block_device *, sector_t, struct page *, bool);

        int (*ioctl) (struct block_device *, fmode_t, unsignedunsigned long);

        int (*compat_ioctl) (struct block_device *, fmode_t, unsignedunsigned long);

        unsigned int (*check_events) (struct gendisk *disk,

                                      unsigned int clearing);

        /* ->media_changed() is DEPRECATED, use ->check_events() instead */

        int (*media_changed) (struct gendisk *);

        void (*unlock_native_capacity) (struct gendisk *);

        int (*revalidate_disk) (struct gendisk *);

        int (*getgeo)(struct block_device *, struct hd_geometry *);

        /* this callback is with swap_lock and sometimes page table lock held */

        void (*swap_slot_free_notify) (struct block_device *, unsigned long);

        struct module *owner;

        const struct pr_ops *pr_ops;

};

       其中有打开open/关闭release,ioctl系统调用,media_changed函数用来被内核调用检查介质,一般用于可移除设备例如USB,其参数是gendisk表示一个设备。revalidate_disk函数用来响应设备介质改变,让驱动进行需要的工作来准备新介质。倒数第二个参数owner是个指针,它指向拥有这个结构的模块。

注意该结构体中没有实际读写函数,这些操作由请求函数处理,后面会介绍。

再来看下磁盘的数据机构gendisk (定义于 <include/linux/genhd.h>) ,是单独一个磁盘驱动器的内核表示

struct gendisk {

        /* major, first_minor and minors are input parameters only,

         * don't use directly.  Use disk_devt() and disk_max_parts().

         */

        int major;                      /* major number of driver */

        int first_minor;

        int minors;                     /* maximum number of minors, =1 for

                                         * disks that can't be partitioned. */

 

        char disk_name[DISK_NAME_LEN];  /* name of major driver */

        char *(*devnode)(struct gendisk *gd, umode_t *mode);

 

        unsigned int events;            /* supported events */

        unsigned int async_events;      /* async events, subset of all */

 

        /* Array of pointers to partitions indexed by partno.

         * Protected with matching bdev lock but stat and other

         * non-critical accesses use RCU.  Always access through

         * helpers.

         */

        struct disk_part_tbl __rcu *part_tbl;

        struct hd_struct part0;

 

        const struct block_device_operations *fops;

        struct request_queue *queue;

        void *private_data;

 

        int flags;

        struct rw_semaphore lookup_sem;

        struct kobject *slave_dir;

 

        struct timer_rand_state *random;

        atomic_t sync_io;               /* RAID */

        struct disk_events *ev;

#ifdef  CONFIG_BLK_DEV_INTEGRITY

        struct kobject integrity_kobj;

#endif  /* CONFIG_BLK_DEV_INTEGRITY */

        int node_id;

        struct badblocks *bb;

        struct lockdep_map lockdep_map;

};

    该结构体中有设备号、次编号(标记不同分区)、磁盘驱动器名字(出现在/proc/partitionssysfs中)、 设备的操作集(block_device_operations)、设备IO请求结构、驱动器状态、驱动器容量、驱动内部数据指针private_data等。

       和gendisk相关的函数有,alloc_disk函数用来分配一个磁盘,del_gendisk用来减掉一个对结构体的引用。

分配一个 gendisk 结构不能使系统可使用这个磁盘。还必须初始化这个结构并且调用 add_disk。一旦调用add_disk后, 这个磁盘是"活的"并且它的方法可被在任何时间被调用了,内核这个时候就可以来摸设备了。实际上第一个调用将可能发生, 也可能在 add_disk 函数返回之前; 内核将读前几个字节以试图找到一个分区表。在驱动被完全初始化并且准备好之前,不要调用add_disk来响应对磁盘的请求。下面来看初始话。

3.   初始化

这里初始化分为两部分,第一部分是设备的结构体数据,另一个是磁盘的数据结构体gendisk。

本文用sbull_dev数据结构来表示设备,一个内部结构描述如下(摘自ldd3原著):

struct sbull_dev {

        int size;                       /* Device size in sectors */

        u8 *data;                       /* The data array */

        short users;                    /* How many users */

        short media_change;             /* Flag a media change? */

        spinlock_t lock;                /* For mutual exclusion */

        struct request_queue *queue;    /* The device request queue */

        struct gendisk *gd;             /* The gendisk structure */

        struct timer_list timer;        /* For simulated media changes */

};

需要初始化这个结构,

        memset (dev, 0sizeof (struct sbull_dev));

        dev->size = nsectors*hardsect_size;

        dev->data = vmalloc(dev->size);

spin_lock_init(&dev->lock);

       通过memset将设备结构初始化为0,设置设备大小(nsectors和hardsect_size是扇区数量和扇区大小),分配内存获得虚拟地址,这个分配的内存是模拟磁盘设备的空间用来被用户使用的。分配并初始化结构体的自旋锁。

       初始化设备定时器

        init_timer(&dev->timer);

        dev->timer.data = (unsigned long) dev;

        dev->timer.function = sbull_invalidate;

       设置定时的回调函数为sbull_invalidate。

       分配请求队列,包括队列的处理函数

dev->queue = blk_init_queue(sbull_request, &dev->lock);

       其中sbull_request是请求函数负责读写,同时使用自旋锁来控制对队列的访问。

dev->queue是数据结构request_queue定义在include/linux/blkdev.h

有了设备内存和请求队列后,可以继续初始化gendisk结构。

dev->gd = alloc_disk(SBULL_MINORS);//

        dev->gd->major = sbull_major;

        dev->gd->first_minor = which*SBULL_MINORS;

        dev->gd->fops = &sbull_ops;

        dev->gd->queue = dev->queue;

        dev->gd->private_data = dev;

        snprintf (dev->gd->disk_name, 32"sbull%c", which + 'a');

        set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));

        add_disk(dev->gd);

       其中SBULL_MINORS是次编号的数据,磁盘名为sbulla, 第二个为sbullb, 以此类推。初始化完gendisk,设备和磁盘关联起来了。初始化接收,最后调用add_disk函数。

 

4.   块设备操作

为实现模拟的介质移出, sbull 需要知道最后一个用户关闭设备。驱动中open 和 close 方法来保持一个用户计数被,保持这个计数最新.

4.1     open/release

open方法用相关的节点和文件结构指针作为参数. 当一个节点引用一个块设备, i_bdev->bd_disk 包含一个指向关联 gendisk 结构的指针; 这个指针可用来获得一个驱动的给设备的内部数据结构。 open函数会删除设备定时器,递增用户计数并且返回。
release函数会递减用户计数,如果没有用户在使用了,就会添加定时器。当然,在一个处理真实的硬件设备的驱动中, open 和 release 方法应当相应地设置驱动和硬件的状态。当最后一个用户关闭设备后, 一个 30 秒的定时器被设置; 如果设备在这个时间内不被打开, 设备的内容被清除, 并且内核被告知介质已被改变。

一些操作可导致一个块设备从用户空间直接打开; 例如分区一个磁盘, 在一个分区上建立一个文件系统, 或者运行一个文件系统检查器。当加载一个分 区时, 块驱动也可看到一个 open 调用,这个情况下, 没有用户空间进程持有一个这个设备的打开的文件描述符; 相反, 打开的文件被内核自身持有。块驱动无法知道一个加载操作(它从内核打开设备)和调用如mkfs 工具(从用户空间打开它)之间的差别.

4.2     热插拔

检测磁盘变化函数check_disk_changed(位于文件fs/block_dev.c)调用media_changed函数来查看设备是否变化,如果发生变化就返回一个非零值。而revalidate方法在介质改变后被调用。

4.3     ioctl

块设备可提供一个 ioctl 方法来进行设备控制函数。很多用户层工具是用ioctl的方式来设置驱动的。 高层的块子系统代码在驱动能见到它们之 前解释许多的 ioctl 命令,现代的 块驱动不必实现许多的 ioctl 命令.

5.   关于队列

块驱动的核心是请求函数,也是系统整个性能的关键部分。无论何时内核认为驱动是时候处理对设备的读, 写, 或者其他操作. 请求函数在返回之前实际不需要完成所有的在队列中的请求; 但是实际上大部分真实设备,可能不完成任何一个请求。但是,驱动会确保这些请求最终被驱动全部处理.

每个设备都有一个请求队列。当这个队列被创建时, 请求函数和它关联到一起,一个自旋锁作为队列创建过程的一部分。无论何时请求函数被调用, 内核持有这个锁。结果, 请求函数在原子上下文中运行;

在请求函数持有锁时, 队列锁还阻止内核去排队任何对设备的其他请求. 在一些条件下,可能考虑在请求函数运行时丢弃这个锁。如果这样做,必须保证不存取请求队列, 或者任何其他被这个锁保护的数据结构。

请求函数的启动(常常地)与任何用户空间进程之间是完全异步的。不能假设内核运行在发起当前请求的进程上下文。我们不知道由这个请求提供的 I/O 缓冲是否在内核或者用户空间。驱动需要知道的关于请求的所有事情, 都包含在通过请求队列传递给的结构中。

request结构体就是请求操作块设备的请求结构体,该结构体被放到request_queue队列中。request_queue结构体定义在include/linux/blkdev.h文件中。队列结构非常复杂,不过幸运的是驱动不用太关注。请求队列存储参数来描述这个设备能够支持什么类型的请求它们的最大大小多少不同的段可进入一个请求硬件扇区 大小对齐要求等等

内核提供函数 elv_next_request 来获得队列中第一个未完成的请求。在3.10.0版本中是blk_fetch_request函数了。

一个块请求队列可包含不从磁盘发起或者磁盘移动块的请求。这些请求可包括供应商特定的, 低层的诊断操作或者和特殊设备模式相关的指令, 例如给可记录介质的报文写模式。大部分块驱动不知道 如何处理这样的请求, 就简单地失败它们。

       请求队列的创建函数为:

struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)

关闭一个请求队列的函数如下:

void blk_cleanup_queue(struct request_queue *q)

都位于文件block/blk-core.c   

当设备到达一个状态以致不能处理等待的命令,需要调用blk_stop_queue函数,这样请求函数将不会被调用,直到调用blk_start_queue函数。

6.   关于请求

每个请求结构代表一个块 I/O 请求,可能是由几个独立的请求在更高层次合并而成。

对任何特殊的请求而传送的扇区可能分布在整个主内存, 尽管它们常常对应块设备中的多个连续的扇区. 这个请求被表示为多个段, 每个对应一个内存中的缓冲。内核可能合并多个涉及磁盘上邻近扇区的请求, 但是它从不合并在单个请求结构中的读和写操作。但是,如果结果会破坏任何队列限制,内核确保不合并请求。具体可见Linux块IO总体概况

bio结构体是request结构体的实际数据,一个request结构体中包含一个或者多个bio结构体,被实现为一个 bio 结构的链表,在底层实际是按bio来对设备进行操作的,传递给驱动。

代码会把它合并到一个已经存在的request结构体中,或者需要的话会再创建一个新的request结构体;bio结构体包含了驱动程序执行请求的全部信息,使驱动可以跟踪它的位置。

结构接着被递给块 I/O 代码,合并它到一个存在的请求结构,如果需要, 创建一个新的. bio 结构包含一个块驱动需要进行请求的任何东西, 而不必涉及使这个请求启动的用户空间进程.

bio结构体是request结构体的实际数据,一个request结构体中包含一个或者多个bio结构体,在底层实际是按bio来对设备进行操作的,传递给驱动。

代码会把它合并到一个已经存在的request结构体中,或者需要的话会再创建一个新的request结构体;bio结构体包含了驱动程序执行请求的全部信息。

一个块 I/O 请求被转换为一个 bio 结构后, 已被分为单独的物理内存页.


直接使用 bi_io_vec 数组不被推荐, 为了内核开发者可以在以后改变 bio 结构而不会引起破坏. 为此, 一组宏被提供来简化使用 bio 结构. 开始的地方是 bio_for_each_segment, 它简单地循环 bi_io_vec 数组中 每个未被处理的项.

一个带有部分被处理请求的请求队列:


块层在驱动见到请求之前重新排序来提高 I/O 性能.如果需要,驱动也可以重新排序请求。在一个 I/O 请求的设备,已经完成传送一些或者全部扇区,必须通知块子系统。

代码中使用__rq_for_each_bio来获得请求中的每个BIO,如下:

__rq_for_each_bio(bio, req) {

                sbull_xfer_bio(dev, bio);

                nsect += bio->bi_size/KERNEL_SECTOR_SIZE;

        }

       然后使用bio_for_each_segment来获得BIO中的段,最后进行IO处理,此处是sbull_transfer函数。

        bio_for_each_segment(bvec, bio, i) {

                char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);

                sbull_transfer(dev, sector, bio_cur_bytes(bio) >> 9,

                                buffer, bio_data_dir(bio) == WRITE);

                sector += bio_cur_bytes(bio) >> 9;

                __bio_kunmap_atomic(buffer, KM_USER0);

        }

7.   请求模式

有些设备, 例如软件 RAID 阵列或者被逻辑卷管理者创建的虚拟磁盘, 没有块层请求队列优化的条件。对于这类设备,最好直接从块层接收请求,不去用请求队列。

这个时候驱动必须提供一个"制作请求"函数, 而不是一个请求函数。需要注意的是其请求队列仍然存在, 虽然不会真正有任何请求。make_request 函数用一个 bio 结构作为它的主要参数, 这个 bio 结构表示一个或多个要传送的缓冲。make_request 函数可以直接进行传输, 或者重定向这个请求到另一个设备。

代码中的请求模式如下:

enum {

        RM_SIMPLE  = 0/* The extra-simple request function */

        RM_FULL    = 1/* The full-blown version */

        RM_NOQUEUE = 2/* Use make_request */

};

RM_SIMPLE使用简单的请求处理函数,请求处理函数为sbull_request,通过函数memcpy来实现简单进行读写复制。

RM_FULL使用了bio,一个request结构作为一个bio结构的链表实现的,request中的bio指针就负责指向bio链表,而bio结构则描述I/O请求。RM_FULL对应的请求处理函数就是直接对bio进行操作完成I/O请求的, 请求处理函数为sbull_request

RM_NOQUEUE表示不适用队列,适用软件 RAID 阵列或者被逻辑卷管理者创建的虚拟磁盘。请求处理函数为sbull_make_request

代码中请求队列的函数逻辑如下图:


8.   块驱动源码

改编自《Linux设备驱动-第三版》又名LDD3

代码太长,放到了github上,直接下载使用。

https://github.com/kernel-z/ldd3/tree/master/snull

9.   小结

本片中的块驱动虽可以运行使用,并模拟了硬盘的功能,但和实际的块驱动差异挺大。

源码中的sbull 同步执行请求, 一次处理一个,而现实中高性能的磁盘设备能够在同时有很多个请求停留。另外磁盘控制器可以优化IO的顺序,提高性能。如果只处理队列中的第一个请求, 那么在给定时间不能有多个请求被满足。

另外,驱动中一次只有一个缓冲被传送, 意味着最大的单次传送不会超过单个页的大小.

10.      参考

《Linux设备驱动-第三版》又名LDD3

https://www.kernel.org/

众多书籍源码:

https://github.com/EternalPeace/Linux-Network-Program-Samples

Linux块IO总体概况


猜你喜欢

转载自blog.csdn.net/notbaron/article/details/80177590
今日推荐