治肾虚不含糖,专注内核性能优化二十年。 https://github.com/KnightKu
分类: LINUX
2016-10-10 11:06:02
转载地址:http://blog.csdn.net/yuzhihui_no1/article/details/51325360
有关trim命令的简介 可以看下http://blog.csdn.net/yuzhihui_no1/article/details/46519701
这里就大概的说下驱动中对trim命令的实现吧,由于对公司代码的保密性,这里就不沾贴代码了,就大概的说下实现原理;
条理会有点乱,也借此机会复习整理下一些相关知识点;
首先是块设备的基本框架:队列 queue和绑定队列的函数
先说说一般的块设备框架:
dev->queue = blk_init_queue(request, &dev->lock);
每个块设备都需要一个请求队列(后面会讲有的不需要队列),这是因为对块设备请求数据的传人和输出发生的时间,与内核请求的时间相差太大了,所以需要队列 来对一些请求做处理。比如合并req、调整req的请求顺序之类的(后面有时间会注重分析io请求的合并和排序)。
从这里就可以看出,为什么要有队列?就是因为请求的传人和输出时间与内核的请求时间相差太大。反之,如果块设备请求数据的传入和传出时间,与内核请求的时间相差不大,是不是就意味着可以不用队列?
答案是肯定的,SSD的驱动就是不需要队列的。
原因:1、SSD设备的响应时间和请求时间相差不大(其实对CPU来说还是比较大的);
2、也是最主要的,或者说最本质的,最根本的,就是SSD是电子设备,而普通盘是机械设备。普通硬盘读写速度之所以慢的原因是机械臂的移动耗费的时间,所 以就有电梯算法之类的,来减少机械臂的移动。而SSD卡是电子设备,不存在寻址(机械臂移动就是为了寻址)耗费时间,可以类比下内存,也可以类比下 hash算法,或者再类比下查字典,其实原理大致一样的,不需要顺序的一个一个的去寻址;
介于上面的原因,就不需要对io请求进行排序,或者合并之类的(驱动中请求合并还是会有的,但不是因为寻址的原因,而是为了提高读性能,和内存管理中的预读页是一样的原理)
下面接着说普通块设备的队列和请求函数,当请求队列生成的时候,请求函数就已经和队列绑定了。而且还赠送了一个把自旋锁。当request()函数被调 用时,这把锁会由内核来控制,也就是说request()函数是在原子上下文中运行的(所以定义request函数时,要牢记遵守原子上下文的规则)。
request()函数有自旋锁时,可以防止内核再给他安排其他的请求;request()函数内要开锁时,一定要记得先禁止其他线程对队列和包含数据的访问,而request()函数返回时,必须要得到该锁。
对普通块设备来说request()函数就承载了,设备的读写请求的处理了。在线程调用该函数返回前,不需要把所有的请求都执行完,但request函数必须有返回响应,而且还得保证能完成所有的请求;
接下来说说SSD驱动中使用的无请求队列的块设备框架:
首先还是自己创建一个队列:queue = blk_alloc_queue(GFP_NOIO);//该函数会告诉块设备子系统,驱动使用定制的make_request函数。该队列不保存请求;
然后还是绑定请求函数:blk_queue_make_request(queue, make_request);
最后是构造一个请求函数:make_request(q, bio);
从上层一直往下走的话,到block层会有__generic_make_request(struct bio *bio)函数,该函数有两行代码可以引出块设备驱动的运行;
q = bdev_get_queue(bio->bi_bdev);//从块设备结构体中获取到queue
ret = q->make_request_fn(q, bio);//开始执行队列绑定的处理函数了
现在来说说trim命令了,在驱动中首先会判断这个bio请求是否是丢弃的bio,看bio->bi_rw标识 ,如果是丢弃的,就是trim命令了。
接着就调用get_bi_sector(bio)来获取到bio->bi_sector第一个请求的sector,再根据 bio->bi_size 来得到最后一个请求的sector,其中的一些对齐转换,根据不同需求再做决定。最后就是实际的丢弃操作了;
目前已经得到了要丢弃的数据范围,循环去执行丢弃操作,分析丢弃一个lba的动作,其他的循环丢弃就可以了;
丢弃一个lba动作:首先获取到ftl映射表中对应pba地址,如果是空的,表示该位置上实际就没有数据,那就不需要操作,直接返回;
如果lba对应的ftl映射表pba地址,已经存在,则修改映射表,使lba对应的pba为空,接着就需要把实际的pba(开始映射表对应的pba)设置 为无效。各个驱动设计的不一样,但总的思路就是把pba标记为无效,然后再根据该pba所在的sb是什么状态,再做一些状态的调整和处理。
总之,最后就会把该sb从其他链表中拿出来,挂载到待擦除链表上,接着就会唤醒gc线程去做gc工作。而gc线程做的工作是搜索需要回收的sb,还有就是 回收sb。从链表上的sb中去判断每个pba是否有效,如果有效就读取pba上的数据,然后再写入到新的地址上。一直循环,直到把sb上的所有有效的 pba数据搬运完,然后就开始真正的擦除该sb,擦除后再把该sb挂到free_block链表上。
最后就调用下 bio_end(bio, bio->bi_size, 0)函数返回,trim命令就这么愉快的结束了,但后台gc线程还在跑。