块设备是Linux三大设备之一,其驱动模型主要针对磁盘,Flash等存储类设备,块设备(blockdevice)是一种具备必定结构的随机存取设备,对这种设备的读写是按块(因此叫块设备)进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。做为存储设备,块设备驱动的核心问题就是哪些page->segment->block->sector与哪些sector有数据交互,本文以3.14为蓝本,探讨内核中的块设备驱动模型。node
下图是Linux中的块设备模型示意图,应用层程序有两种方式访问一个块设备:/dev和文件系统挂载点,前者和字符设备同样,一般用于配置,后者就是咱们mount以后经过文件系统直接访问一个块设备了。linux
做为一种存储设备,和字符设备相比,块设备有如下几种不一样:算法
字符设备 | 块设备 |
---|---|
1byte | 块,硬件块各有不一样,可是内核都使用512byte描述 |
顺序访问 | 随机访问 |
没有缓存,实时操做 | 有缓存,不是实时操做 |
通常提供接口给应用层 | 块设备通常提供接口给文件系统 |
是被用户程序调用 | 由文件系统程序调用 |
此外,大多数状况下,磁盘控制器都是直接使用DMA方式进行数据传送。数据库
就是电梯算法。咱们知道,磁盘是的读写是经过机械性的移动磁头来实现读写的,理论上磁盘设备知足块设备的随机读写的要求,可是出于节约磁盘,提升效率的考虑,咱们但愿当磁头处于某一个位置的时候,一块儿将最近须要写在附近的数据写入,而不是这写一下,那写一下而后再回来,IO调度就是将上层发下来的IO请求的顺序进行从新排序以及对多个请求进行合并,这样就能够实现上述的提升效率、节约磁盘的目的。这种解决问题的思路使用电梯算法,一个运行中的电梯,一我的20楼->1楼,另一我的15->5楼,电梯不会先将第一我的送到1楼再去15楼接第二我的将其送到5楼,而是从20楼下来,到15楼的时候停下接人,到5楼将第二个放下,最后到达1楼,一句话,电梯算法最终服务的优先顺序并不按照按按钮的前后顺序。Linux内核中提供了下面的几种电梯算法来实现IO调度:api
咱们能够经过内核传参的方式指定使用的调度算法数组
kernel elevator=deadline
或者,使用以下命令改变内核调度算法缓存
echo SCHEDULER > /sys/block/DEVICE/queue/scheduler
VS左面的是数据交互中的内存部分,Page就是内存映射的最小单位; Segment就是一个Page中咱们要操做的一部分,由若干个相邻的块组成; Block是逻辑上的进行数据存取的最小单位,是文件系统的抽象,逻辑块的大小是在格式化的时候肯定的, 一个 Block 最多仅能容纳一个文件(即不存在多个文件同一个block的状况)。若是一个文件比block小,他也会占用一个block,于是block中空余的空间会浪费掉。而一个大文件,能够占多个甚至数十个成百上千万的block。Linux内核要求 Block_Size = Sector_Size * (2的n次方),而且Block_Size <= 内存的Page_Size(页大小), 如ext2 fs的block缺省是4k。若block太大,则存取小文件时,有空间浪费的问题;若block过小,则硬盘的 Block 数目会大增,而形成 inode 在指向 block 的时候的一些搜寻时间的增长,又会形成大文件读写方面的效率较差,block是VFS和文件系统传送数据的基本单位。block对应磁盘上的一个或多个相邻的扇区,而VFS将其当作是一个单一的数据单元,块设备的block的大小不是惟一的,建立一个磁盘文件系统时,管理员能够选择合适的扇区的大小,同一个磁盘的几个分区可使用不一样的块大小。此外,对块设备文件的每次读或写操做是一种"原始"访问,由于它绕过了磁盘文件系统,内核经过使用最大的块(4096)执行该操做。Linux对内存中的block会被进一步划分为Sector,Sector是硬件设备传送数据的基本单位,这个Sector就是512byte,和物理设备上的概念不同,若是实际的设备的sector不是512byte,而是4096byte(eg SSD),那么只须要将多个内核sector对应一个设备sector便可app
VS右边是物理上的概念,磁盘中一个Sector是512Byte,SSD中一个Sector是4K框架
block_device_operations描述磁盘的操做方法集,block_device_operations之于gendisk,相似于file_operations之于cdevdom
bvec_iter描述一个bio_vec中的一个sector信息
blk_queue_make_request()绑定处理函数到一个没有IO调度的request_queue,处理函数函数是void (make_request_fn) (struct request_queue q, struct bio bio);类型
rq_for_each_segment()遍历一个request中的全部的bio中的全部的segment
最后三个遍历算法都是用在request_queue绑定的处理函数中,这个函数负责对上层请求的处理。
一样是面向对象的设计方法,Linux内核使用gendisk对象描述一个系统的中的块设备,相似于Windows系统中的磁盘分区和物理磁盘的关系,OS眼中的磁盘都是逻辑磁盘,也就是一个磁盘分区,一个物理磁盘能够对应多个磁盘分区,在Linux中,这个gendisk就是用来描述一个逻辑磁盘,也就是一个磁盘分区。
165 struct gendisk { 169 int major; /* major number of driver */ 170 int first_minor; 171 int minors; 174 char disk_name[DISK_NAME_LEN]; /* name of major driver */ 175 char *(*devnode)(struct gendisk *gd, umode_t *mode); 177 unsigned int events; /* supported events */ 178 unsigned int async_events; /* async events, subset of all */ 185 struct disk_part_tbl __rcu *part_tbl; 186 struct hd_struct part0; 188 const struct block_device_operations *fops; 189 struct request_queue *queue; 190 void *private_data; 192 int flags; 193 struct device *driverfs_dev; // FIXME: remove 194 struct kobject *slave_dir; 196 struct timer_rand_state *random; 197 atomic_t sync_io; /* RAID */ 198 struct disk_events *ev; 200 struct blk_integrity *integrity; 202 int node_id; 203 };
struct gendisk
--169-->驱动的主设备号
--170-->第一个次设备号
--171-->次设备号的数量,即容许的最大分区的数量,1表示不容许分区
--174-->设备名称
--185-->分区表数组首地址
--186-->第一个分区,至关于part_tbl->part[0]
--188-->操做方法集指针
--189-->请求对象指针
--190-->私有数据指针
--193-->表示这是一个设备
gendisk是一个动态分配的结构体,因此不要本身手动来分配,而是使用内核相应的API来分配,其中会作一些初始化的工做
struct gendisk *alloc_disk(int minors); //注册gendisk类型对象到内核 void add_disk(struct gendisk *disk); //从内核注销gendisk对象 void del_gendisk(struct gendisk *gp);
上面几个API是一个块设备驱动中必不可少的部分,下面的两个主要是用来内核对于设备管理用的,一般不要驱动来实现
//对gendisk的引用计数+1 struct kobject *get_disk(struct gendisk *disk); //对gendisk的引用计数-1 void put_disk(struct gendisk *disk);
这两个API最终回调用kobject *get_disk() 和kobject_put()来实现对设备的引用计数
和字符设备同样,若是使用/dev接口访问块设备,最终就会回调这个操做方法集的注册函数
//include/linux/blkdev.h 1558 struct block_device_operations { 1559 int (*open) (struct block_device *, fmode_t); 1560 void (*release) (struct gendisk *, fmode_t); 1561 int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); 1562 int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); 1563 int (*direct_access) (struct block_device *, sector_t, 1564 void **, unsigned long *); 1565 unsigned int (*check_events) (struct gendisk *disk, 1566 unsigned int clearing); 1568 int (*media_changed) (struct gendisk *); 1569 void (*unlock_native_capacity) (struct gendisk *); 1570 int (*revalidate_disk) (struct gendisk *); 1571 int (*getgeo)(struct block_device *, struct hd_geometry *); 1573 void (*swap_slot_free_notify) (struct block_device *, unsigned long); 1574 struct module *owner; 1575 };
struct block_device_operations
--1559-->当应用层打开一个块设备的时候被回调
--1560-->当应用层关闭一个块设备的时候被回调
--1562-->至关于file_operations里的compat_ioctl,不过块设备的ioctl包含大量的标准操做,因此在这个接口实现的操做不多
--1567-->在移动块设备中测试介质是否改变的方法,已通过时,一样的功能被check_event()实现
--1571-->即get geometry,获取驱动器的几何信息,获取到的信息会被填充在一个hd_geometry结构中
--1574-->模块所属,一般填THIS_MODULE
每个gendisk对象都有一个request_queue对象,前文说过,块设备有两种访问接口,一种是/dev下,一种是经过文件系统,后者通过IO调度在这个gendisk->request_queue上增长请求,最终回调与request_queue绑定的处理函数,将这些请求向下变成具体的硬件操做
294 struct request_queue { 298 struct list_head queue_head; 300 struct elevator_queue *elevator; 472 };
struct request_queue
--298-->请求队列的链表头
--300-->请求队列使用的IO调度算法, 经过内核启动参数来选择: kernel elevator=deadline
request_queue_t和gendisk同样须要使用内核API来分配并初始化,里面大量的成员不要直接操做, 此外, 请求队列若是要正常工做还须要绑定到一个处理函数中, 当请求队列不为空时, 处理函数会被回调, 这就是块设备驱动中处理请求的核心部分!
从驱动模型的角度来讲, 块设备主要分为两类须要IO调度的和不须要IO调度的, 前者包括磁盘, 光盘等, 后者包括Flash, SD卡等, 为了保证模型的统一性 , Linux中对这两种使用一样的模型可是经过不一样的API来完成上述的初始化和绑定
//初始化+绑定 struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
//初始化 struct request_queue *blk_alloc_queue(gfp_t gfp_mask) //绑定 void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
针对请求队列的操做是块设备的一个核心任务, 其实质就是对请求队列操做函数的编写, 这个函数的主要功能就是从请求队列中获取请求并根据请求进行相应的操做 内核中已经提供了大量的API供该函数使用
//清除请求队列, 一般在卸载函数中使用 void blk_cleanup_queue(struct request_queue *q) //从队列中去除请求 blkdev_dequeue_request() //提取请求 struct request *blk_fetch_request(struct request_queue *q) //从队列中去除请求 struct request *blk_peek_request(struct request_queue *q) //启停请求队列, 当设备进入到不能处理请求队列的状态时,应通知通用块层 void blk_stop_queue(struct request_queue *q) void blk_start_queue(struct request_queue *q)
97 struct request { 98 struct list_head queuelist; 104 struct request_queue *q; 117 struct bio *bio; 118 struct bio *biotail; 119 120 struct hlist_node hash; /* merge hash */ 126 union { 127 struct rb_node rb_node; /* sort/lookup */ 128 void *completion_data; 129 }; 137 union { 138 struct { 139 struct io_cq *icq; 140 void *priv[2]; 141 } elv; 142 143 struct { 144 unsigned int seq; 145 struct list_head list; 146 rq_end_io_fn *saved_end_io; 147 } flush; 148 }; 149 150 struct gendisk *rq_disk; 151 struct hd_struct *part; 199 };
struct request
--98-->将这个request挂接到链表的节点
--104-->这个request从属的request_queue
--117-->组成这个request的bio链表的头指针
--118-->组成这个request的bio链表的尾指针
--120-->内核hash表头指针
bio用来描述单一的I/O请求,它记录了一次I/O操做所必需的相关信息,如用于I/O操做的数据缓存位置,,I/O操做的块设备起始扇区,是读操做仍是写操做等等
46 struct bio { 47 struct bio *bi_next; /* request queue link */ 48 struct block_device *bi_bdev; 49 unsigned long bi_flags; /* status, command, etc */ 50 unsigned long bi_rw; /* bottom bits READ/WRITE, 51 * top bits priority 52 */ 54 struct bvec_iter bi_iter; 59 unsigned int bi_phys_segments; 65 unsigned int bi_seg_front_size; 66 unsigned int bi_seg_back_size; 68 atomic_t bi_remaining; 70 bio_end_io_t *bi_end_io; 72 void *bi_private; 85 unsigned short bi_vcnt; /* how many bio_vec's */ 91 unsigned short bi_max_vecs; /* max bvl_vecs we can hold */ 104 struct bio_vec bi_inline_vecs[0]; 105 };
struct bio
--47-->指向链表中下一个bio的指针bi_next
--50-->bi_rw低位表示读写READ/WRITE, 高位表示优先级
--90-->bio对象包含bio_vec对象的数目
--91-->这个bio能承载的最大的io_vec的数目
--95-->该bio描述的第一个io_vec
--104-->表示这个bio包含的bio_vec变量的数组,即这个bio对应的某一个page中的一"段"内存
描述指定page中的一块连续的区域,在bio中描述的就是一个page中的一个"段"(segment)
25 struct bio_vec { 26 struct page *bv_page; 27 unsigned int bv_len; 28 unsigned int bv_offset; 29 };
struct bio_vec
--26-->描述的page
--27-->描述的长度
--28-->描述的起始地址偏移量
用于记录当前bvec被处理的状况,用于遍历bio
31 struct bvec_iter { 32 sector_t bi_sector; /* device address in 512 byt 33 sectors */ 34 unsigned int bi_size; /* residual I/O count */ 35 36 unsigned int bi_idx; /* current index into bvl_ve 37 38 unsigned int bi_bvec_done; /* number of bytes completed 39 current bvec */ 40 };
遍历一个request中的每个bio
738 #define __rq_for_each_bio(_bio, rq) \ 739 if ((rq->bio)) \ 740 for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
遍历一个bio中的每个segment
242 #define bio_for_each_segment(bvl, bio, iter) \ 243 __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
遍历一个request中的每个segment
742 #define rq_for_each_segment(bvl, _rq, _iter) \ 743 __rq_for_each_bio(_iter.bio, _rq) \ 744 bio_for_each_segment(bvl, _iter.bio, _iter.iter)
遍历request_queue,绑定函数的一个必要的工做就是将request_queue中的数据取出, 因此遍历是必不可少的, 针对有IO调度的设备, 咱们须要从中提取请求再继续操做, 对于没有IO调度的设备, 咱们能够直接从request_queue中提取bio进行操做, 这两种处理函数的接口就不同,下面的例子是对LDD3中的代码进行了修剪而来的,相应的API使用的是3.14版本,能够看出这两种模式的使用方法的不一样。
sbull_init
└── setup_device
├──sbull_make_request
│ ├──sbull_xfer_bio
│ └──sbull_transfer
└──sbull_full_request
├──blk_fetch_request
└──sbull_xfer_request
├── __rq_for_each_bio
└── sbull_xfer_bio
└──sbull_transfer
/* * Handle an I/O request. * 实现扇区的读写 */ static void sbull_transfer(struct sbull_dev *dev, unsigned long sector,unsigned long nsect, char *buffer, int write) { unsigned long offset = sector*KERNEL_SECTOR_SIZE; unsigned long nbytes = nsect*KERNEL_SECTOR_SIZE; if (write) memcpy(dev->data + offset, buffer, nbytes); else memcpy(buffer, dev->data + offset, nbytes); } /* * Transfer a single BIO. */ static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio) { struct bvec_iter i; //用来遍历bio_vec对象 struct bio_vec bvec; sector_t sector = bio->bi_iter.bi_sector; /* Do each segment independently. */ bio_for_each_segment(bvec, bio, i) { //bvec会遍历bio中每个bio_vec对象 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(bio, KM_USER0); } return 0; /* Always "succeed" */ } /* * Transfer a full request. */ static int sbull_xfer_request(struct sbull_dev *dev, struct request *req) { struct bio *bio; int nsect = 0; __rq_for_each_bio(bio, req) { sbull_xfer_bio(dev, bio); nsect += bio->bi_size/KERNEL_SECTOR_SIZE; } return nsect; } /* * Smarter request function that "handles clustering".*/ static void sbull_full_request(struct request_queue *q) { struct request *req; int nsect; struct sbull_dev *dev ; int i = 0; while ((req = blk_fetch_request(q)) != NULL) { dev = req->rq_disk->private_data; nsect = sbull_xfer_request(dev, req); __blk_end_request(req, 0, (nsect<<9)); printk ("i = %d\n", ++i); } } //The direct make request version static void sbull_make_request(struct request_queue *q, struct bio *bio) { struct sbull_dev *dev = q->queuedata; int status; status = sbull_xfer_bio(dev, bio); bio_endio(bio, status); return; } /* * The device operations structure. */ static struct block_device_operations sbull_ops = { .owner = THIS_MODULE, .open = sbull_open, .release= sbull_release, .getgeo = sbull_getgeo, }; /* * Set up our internal device. */ static void setup_device(struct sbull_dev *dev, int which) { /* * Get some memory. */ memset (dev, 0, sizeof (struct sbull_dev)); dev->size = nsectors * hardsect_size; dev->data = vmalloc(dev->size); /* * The I/O queue, depending on whether we are using our own * make_request function or not. */ switch (request_mode) { case RM_NOQUEUE: dev->queue = blk_alloc_queue(GFP_KERNEL); blk_queue_make_request(dev->queue, sbull_make_request); break; case RM_FULL: dev->queue = blk_init_queue(sbull_full_request, &dev->lock); break; } dev->queue->queuedata = dev; /* * And the gendisk structure. */ 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); return; } static int __init sbull_init(void) { int i; /* * Get registered. */ sbull_major = register_blkdev(sbull_major, "sbull"); /* * Allocate the device array, and initialize each one. */ Devices = (struct sbull_dev *)kmalloc(ndevices*sizeof (struct sbull_dev), GFP_KERNEL); for (i = 0; i < ndevices; i++) setup_device(Devices + i, i); return 0; }