前言
Disksim3.0上安装的Flashsim,能够进行SSD模拟仿真,Flashsim上的FTL层算法可供选择的有DFTL算法,FTL(纯页级映射),和FAST(fully associative sector Translation)混合FTL映射算法。因为最近的缓冲区仿真需要知道底层的FTL全合并的开销,对FAST的源码进行了阅读和理解,进行以下的总结。
FAST的算法基础
在理解代码前。首先需要理解FAST算法的机理和流程。FAST算法是源自这篇ACM会议的论文
《A Log Buffer-Based Flash Translation Layer Using Fully-Associative Sector Translation》
。这篇论文对BAST的混合FTL映射算法进行了分析和改进,指出针对随机写请求的时候,BAST算法性能表现不佳,因为日志块的空间利用率不高会出现块抖动的现象。提出将日志块划分为RW(随机写)日志块和SW(顺序写)日志块。所谓的SW日志块秉承了BAST算法数据块和日志块一对一的关系,映射实现也与BAST算法一致。而RW(随机写)日志块,采用了全关联映射的关系,即任何数据块的更新数据页都可以写在该日志块上(但这在垃圾回收无效块的时候,会产生巨大的全合并开销)。
FAST算法将日志块划分为SW块和RW块,SW块是存储顺序写请求更新,FAST对写入SW块有以下两个规则:
逻辑页地址LPN(or 逻辑扇区地址LSN)模掉一个块包含的页数(扇区数)为0,这样的写请求地址为其分配新的SW块,并将该请求的数据项写在该SW块第一个空白页上。
如果上一个LPN请求更新写入到对应的SW上,当前请求的LPN为上一个请求LPN地址相邻的位置(即LPN’=LPN+1),才将该请求写入到对应的SW位置的后一位上。
下面是截取上述论文针对FAST算法的举例说明:
这里假设一个块包含4个页,SW日志块的数量为1,RW日志块的数量为2。FAST会依次选择不同的日志块来处理,下面分五种情况来讨论,与上文类似,数字表示逻辑页号。(1)当请求4到来时,由于其满足写入SW日志块的条件(逻辑页号在块中的偏移量为0),但SW日志块非空,故需要垃圾回收操作,以产生空白的SW日志块。(2)当请求4、5依次到来时,请求4已由上述分析可知,满足写入SW日志块的条件,并构建了新的SW日志块。当请求5到来时,其满足写入SW日志块的第二个条件,故可以直接写入SW日志块的相应位置。(3)当请求4、6依次到来时,同样,请求4写入SW日志块,但请求6不满足写入SW日志块的两个条件,故通过SW日志块的全合并,将请求6直接写入新的数据块,并产生空的SW日志块。(4)当请求4、5、5依次到来时,请求4和第一个请求5依次写入SW日志块,但第二个请求5已不满足写入SW日志块的条件,故通过全合并,将第二个请求5直接写入新的数据块,并产生空的SW日志块。(5)当请求6到来时,由于不满足写入SW日志块的条件,故按顺序写入RW日志块的第一个空白页中。
FAST算法的垃圾回收算法较为复杂,在此之前需要先理解SSD中的垃圾回收数据块中存在的3种回收方式:全合并,部分合并,交换合并的概念。因为SSD不支持同步更新,数据需要异地更新,多次异地更新以后,会产生带有很多标记为无效的数据页的数据块,为了产生可用的数据块,SSD的底层垃圾回收算法会回收这些数据块,进行有效数据页备份和整块擦除,产生新的空白数据块供上层使用。所谓的全合并,部分合并,交换合并是因为采用混合FTL算法,导致有效数据页在不同回收的块中分布,采用不同的有效数据页备份和块擦除操作。具体操作如下图所示:
因为数据块和日志块中的数据页分布分散难以整理成有序的数据块存储,因此另外寻找一个新的数据块,依次读入日志块和数据块中的有效数据成为新的数据块,并回收擦除旧的数据块和空闲块,这种回收方式的开销是最大的。
第二种是最理想的回收方式,如果存在大量的顺序写更新,日志块中的数据页被顺序写入,且原来的数据块被标记为无效,因此只需要将日志块标记为数据块,擦除旧的数据块回收即可。块擦除次数仅为一次。
第三种回收方式是从数据块中顺序读出有效数据页写入日志块中,并将日志块标记为新的数据块,擦除旧的数据块为空白块。
理解了上述的三种垃圾回收方式,结合FAST算法的SW和RW块的写入操作,我们可以分析SW块可以实现理想的交换合并和部分合并,而RW块只能用全合并进行垃圾回收。继续以上述第一幅图针对FAST写入机制的举例说明进行垃圾回收的理解:
第(1)种情况对应SW日志块的部分合并,将日志块作为新的数据块,并将旧数据块中的页14、15复制到新数据块中,再擦除旧数据块,并分配一个空白块作为新的SW日志块;(3)(4)对应SW日志块的全合并,将日志块和数据块中的数据复制到新的数据块中,然后擦除两个“脏”(dirty)数据块,并为SW日志块分配一个新的空白块。RW日志块的垃圾回收情况由于FAST的全关联策略,而相对复杂。当日志块用尽时,FAST会启动垃圾回收机制,选择一个合适的日志块作为受害块,由于每个日志块可能与多个数据块关联,因此需要擦除N+1个(N为与该日志块关联的数据块个数)块,并分配N个空白块来存储各块的数据,以及一个新的日志块。这过程需要大量的读取、写入数据和擦除操作,产生了巨大的开销,而且只产生一个日志块。同时,若上述各数据块中的更新数据不在在同一个日志块中,还需从其他日志块复制有效数据到新的数据块中,并标记该日志块中相应的更新数据为无效。在最坏情况下,FAST会频繁的进行日志块的回收操作而无法顾及新的写入操作。
上述就是FAST算法的基础。下面就对仿真器的源码进行讲解。
FAST源码理解
在Flashsim上关于实现fast算法的在fast.c中实现,部分定义关键变量在fast.h中,其中会调用底层nand的oob读取和nand的读写操作,这些相关函数在flash.c有定义。
Flashsim中针对不同FTL算法的实现都需要做到四个函数操作,初始化,结束函数,读取FTL操作函数,写入FTL操作函数。在fast.c中依次是lm_init()
lm_end()
lm_read()
lm_write
函数
lm_init()
函数主要初始化算法需要用到的内存分配和初始化,主要是完成日志块结构体数组logblk和块级映射数据表BMT,日志块中的页级映射表数组PMT的内存分配和初始化。源码如下:
int lm_init(blk_t blk_num, blk_t extra_num)
{
int i;
total_blk_num = blk_num;
BMT = (int *)malloc(sizeof(int) * blk_num);
PMT = (struct LogMap*)malloc(sizeof(struct LogMap)*extra_num);
total_log_blk_num = extra_num;
if ((BMT== NULL) || (PMT == NULL)) { return -1; }
memset(BMT, -1, sizeof(int) * blk_num);
for(i = 0; i < total_log_blk_num; i++){
PMT[i].pbn = -1;
PMT[i].fpc = PAGE_NUM_PER_BLK;
memset(PMT[i].lpn, 0xFF, sizeof(int)*PAGE_NUM_PER_BLK);
memset(PMT[i].lpn_status, 0x00, sizeof(int)*PAGE_NUM_PER_BLK);
}
free_SW_blk_num = 1;
free_RW_blk_num = (total_log_blk_num - free_SW_blk_num);
global_SW_blk.logblk.pbn = -1;
return 0;
}
关于输入函数blk_num是数据块的个数,extra_num是日志块的个数。其中SW的个数为1。BMT是个表示块级映射的数组。
BMT[lbn]=pbn //其中下标lbn是逻辑块地址,pbn是实际底层nand数组的标识序号也是我们理解的物理块地址
针对日志块的页级映射关系如何实现,需要对结构体LogMap进行解读
/* Log blocks are composed of ONE sequential log block
and random log blocks for the rest */
struct LogMap{
int fpc; // free page count within a block
int pbn; // physical blk no of the log block
int lpn[PAGE_NUM_PER_BLK];
int lpn_status[PAGE_NUM_PER_BLK]; // -1: invalid, 0: free, 1: valid
};
该结构体中包含了该日志块中当前可用的空白页数fpc,该日志块对应的底层nand数组的标识(也就是我们理解的物理块,即哪个(pbn)物理块是当日志块在使用),lpn[PAGE_NUM_PER_BLK]
是当前的日志块中每个数据页位置上存放的对应的lpn(不存放数据,则初始化为-1),这个lpn存放的并不是有效的,只有结合下面的标识符lpn_status[PAGE_NUM_PER_BLK]
才能判断该lpn是否有效(-1无效,0空闲,1有效)。
简单的RW块可以用上述的结构体LogMap进行表述了,但是SW需要实现日志块和数据块一对一的关系表示,因此在此结构进行扩展为seq_log_blk:
// This is only for ONE sequential log block
struct seq_log_blk{
struct LogMap logblk;
int data_blk; // sequential log block owner
};
其中data_blk表示对应的数据块(即数据块的物理地址,底层nand数组的下标)
一些其他相关的全局变量在文件的头部定义如下:
struct LogMap *PMT; // page mapping table for log blocks
int *BMT; // block mapping table for data blocks
struct seq_log_blk global_SW_blk; // pbn which is being used as SW_blk
int total_log_blk_num;
int total_blk_num;
int global_currRWblk = 1;
int global_firstRWblk = 0;
int free_SW_blk_num;
int free_RW_blk_num;
其中PMT这个日志块数组中PMT[0]永远是SW块,其他是RW块。
针对FAST的释放函数,就是完成对应初始化函数内存释放,源码如下:
void lm_end()
{
printf("switch_merge : %d\n", merge_switch_num);
printf("partial_merge : %d\n", merge_partial_num);
printf("full_merge : %d\n", merge_full_num);
if ((BMT != NULL) || (PMT != NULL)) {
free(BMT);
free(PMT);
}
}
FAST的读操作实现进行源码实现比较简单:
size_t lm_read(sect_t lsn, sect_t size, int mapdir_flag)
{
int i, k, m, h;
int read_flag;
int lpn = lsn/SECT_NUM_PER_PAGE;
int lbn = lsn/SECT_NUM_PER_BLK;
int ppn;
int pbn;
int size_page = size/SECT_NUM_PER_PAGE;
int offset = lpn%PAGE_NUM_PER_BLK;
int valid_flag;
int sect_num;
sect_t s_lsn;
sect_t s_psn;
sect_t copy[SECT_NUM_PER_PAGE];
memset (copy, 0xFF, sizeof (copy));
if(BMT[lbn] == -1){
ASSERT(0);
}
sect_num = 4;
s_psn = ((BMT[lbn] * PAGE_NUM_PER_BLK + offset) * SECT_NUM_PER_PAGE);
s_lsn = lpn * SECT_NUM_PER_PAGE;
for (h = 0; h < SECT_NUM_PER_PAGE; h++) {
copy[h]