天天看点

SSD卡驱动中trim命令的实现原理

有关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线程还在跑。

         转载地址:http://blog.csdn.net/yuzhihui_no1/article/details/51325360