Flashsim中关于FAST算法实现的代码解析

前言

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的内存分配和初始化。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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是个表示块级映射的数组。

1
BMT[lbn]=pbn //其中下标lbn是逻辑块地址,pbn是实际底层nand数组的标识序号也是我们理解的物理块地址

针对日志块的页级映射关系如何实现,需要对结构体LogMap进行解读

1
2
3
4
5
6
7
8
/* 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:

1
2
3
4
5
// 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数组的下标)

1
2
3
4
5
6
7
8
9
10
11
12
一些其他相关的全局变量在文件的头部定义如下:
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的释放函数,就是完成对应初始化函数内存释放,源码如下:

1
2
3
4
5
6
7
8
9
10
11
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的读操作实现进行源码实现比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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] = s_lsn + h;
}

valid_flag = nand_oob_read(s_psn);

if(valid_flag == 1){
size = nand_page_read(s_psn, copy, 0);
}
else if(valid_flag == -1){

read_flag = 0;

for( k = 0; (k < total_log_blk_num) && (read_flag != 1); k++){
for( m = 0; m < PAGE_NUM_PER_BLK; m++){
if((PMT[k].lpn[m] == lpn) && (PMT[k].lpn_status[m] == 1)) {
s_psn = ((PMT[k].pbn * PAGE_NUM_PER_BLK + m) * SECT_NUM_PER_PAGE);
s_lsn = lpn * SECT_NUM_PER_PAGE;

for (i = 0; i < SECT_NUM_PER_PAGE; i++) {
copy[i] = s_lsn + i;
}

size = nand_page_read(s_psn, copy, 0);

read_flag = 1;
break;
}
}
}
}
else{
stat_read_num++;
flash_read_num++;

return 4;

}

ASSERT(size == SECT_NUM_PER_PAGE);

return sect_num;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
上述函数完成的可以理解为:
将lsn转化为对应的lpn和lbn
利用BMT[lbn]---->找到对应的pbn
如果对应的数据项存在数据块pbn中,那么其相对的偏移量应该是一样的,即可以得到对应的物理页地址s_psn

int offset = lpn%PAGE_NUM_PER_BLK;
s_psn = ((BMT[lbn] * PAGE_NUM_PER_BLK + offset) * SECT_NUM_PER_PAGE);

但对应的s_lsn并不是输入的lsn(因为经过了上述的模运行)

s_lsn = lpn * SECT_NUM_PER_PAGE;

因为底层的nand的读写是按一个个页大小操作,且输入的参数需要逻辑扇区地址,因此又设了copy这个int数组里面存的是一个页的起始逻辑页地址到结束
for (h = 0; h < SECT_NUM_PER_PAGE; h++) {
copy[h] = s_lsn + h;
}

因为日志块的数据更新,有效的数据页并不一定存在数据块(对应的偏移量位置上),还可能存在日志块上(lpn[x]=lpn上,且lpn_status[x]=1)。
首先查询对应的数据块上的数据页项oob标识是否为有效
valid_flag = nand_oob_read(s_psn);
如果无效,则在对应的日志块中寻找最新的有效数据项,但是可能出现的情况是valid_flag==0(不存在该数据页??)代码里面的操作是记一次读操作。

FAST的写操作就较为复杂了,写入有一个写入冲突,即如果写入更新的数据,要将数据块中的数据项标记为无效,将新的数据项写入到对应的RW、SW中。但是同时需要注意的是更新的数据可能还会存在上一次日志块中,因此需要将其置为无效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
size_t lm_write(sect_t lsn, sect_t size, int mapdir_flag)  
{
int lbn; int lpn; int offset; // logical page number
int pbn; int sect_num = SECT_NUM_PER_PAGE;
int s_psn, s_lsn; int i;
sect_t lsns[SECT_NUM_PER_PAGE];

lbn = lsn / SECT_NUM_PER_BLK;
lpn = lsn/SECT_NUM_PER_PAGE;
offset = (lsn % SECT_NUM_PER_BLK);

pbn = getPbnFromBMT(lbn);

memset (lsns, 0xFF, sizeof (lsns));

s_psn = SECTOR(pbn, offset);
s_lsn = lpn * SECT_NUM_PER_PAGE;

for (i = 0; i < SECT_NUM_PER_PAGE; i++) {
lsns[i] = s_lsn + i;
}
size = nand_oob_read(s_psn);

/* valid_flag = 1 -> valid sect num = 4 valid_flag = -1 -> valid sect num = 0;
valid_flag = 0 -> all are free (nothing written) */
if( size != 0 ) {
// Call writeToLogBlock
memset (lsns, 0xFF, sizeof (lsns));
writeToLogBlock(lsn,lbn,lpn);
}
else {
nand_page_write(s_psn, lsns, 0, 1);
}
return sect_num;
}

关于写入日志块操作函数writeToLogBlock(lsn,lbn,lpn) 源码就相当的冗余。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
其函数的代码流程可以简述为:
首先查看是否分配了SW块,即SW块对应的实际物理块
if(global_SW_blk.logblk.pbn == -1 ) {
//如果未分配实际的物理块,则寻找对应的空闲块进行绑定
global_SW_blk.logblk.pbn = nand_get_free_blk(1);
//以下是lopMap的结构体初始化
global_SW_blk.logblk.fpc = PAGE_NUM_PER_BLK;
global_SW_blk.data_blk = -1;
for( i = 0; i < PAGE_NUM_PER_BLK; i++) {
global_SW_blk.logblk.lpn[i] = -1; // -1: no data written
global_SW_blk.logblk.lpn_status[i] = 0; // 0: free
}
}
//再绑定PMT日志块数组标定
PMT[0] = global_SW_blk.logblk;
之后所有重新分配新的SW块都是进行上述同样的操作。

下面遵循FAST写入SW块的原则,首先判断偏移量offset是否为0,如果是,则直接将其写入SW块的第一个数据页位置,这里又得考虑当前的SW块是否为空,如果不为空,则需要将这个SW块进行部分(交换)合并操作(根据当前的SW的块大小进行合并操作),并未新的lsn分配新的SW块。代码片段如下:
if ( global_SW_blk.logblk.fpc == PAGE_NUM_PER_BLK) { // SW logblock is empty

//directly do write on SW block because data block has been written already !
ASSERT(global_SW_blk.data_blk == -1);

}
else {

// completely sequentially written -> switch merge
if(global_SW_blk.logblk.fpc == 0) { // no free pages in SW_BLk
merge_switch(global_SW_blk.logblk.pbn, global_SW_blk.data_blk);
merge_switch_num++;
}

// partially sequentially written -> partial merge
else {

merge_partial(global_SW_blk.logblk.pbn, global_SW_blk.data_blk, global_SW_blk.logblk.fpc,lpn*SECT_NUM_PER_PAGE);
merge_partial_num++;
}

//allocate new SW_blk and initialize it
global_SW_blk.logblk.pbn = nand_get_free_blk(1);
global_SW_blk.logblk.fpc = PAGE_NUM_PER_BLK;
global_SW_blk.data_blk = -1;
for( i = 0; i < PAGE_NUM_PER_BLK; i++) {
global_SW_blk.logblk.lpn[i] = -1; // -1: no data written
global_SW_blk.logblk.lpn_status[i] = 0; // 0: free
}
PMT[0] = global_SW_blk.logblk; // insert new SW_blk info into PMT

}

ASSERT(BMT[lbn] != -1);
//更新数据前,先将原数据块中的数据页置为无效(底层nand都是以扇区为操作单位,但是FTL是页操作单位,这个转化在整段代码中都需要注意的)
// invalidate page in data block
s_psn = ((BMT[lbn] * PAGE_NUM_PER_BLK) * SECT_NUM_PER_PAGE);
s_lsn = lpn * SECT_NUM_PER_PAGE;

for(i = 0; i<SECT_NUM_PER_PAGE; i++){
nand_invalidate(s_psn + i, s_lsn + i);
}
nand_stat(OOB_WRITE);

// write page in SW_blk
//global_SW_blk.logblk.pbn是这个SW对应真实的物理块号pbn
pbn = global_SW_blk.logblk.pbn;
//global_SW_blk.data_blk是这个SW对应的数据块的物理块号,和逻辑块号lbn存在对应的关系
global_SW_blk.data_blk = BMT[lbn];
global_SW_blk.logblk.fpc--;
global_SW_blk.logblk.lpn[page_offset] = lpn; //store lpn of the request
global_SW_blk.logblk.lpn_status[page_offset] = 1; // 1: valid
PMT[0] = global_SW_blk.logblk;

//重新更新s_psn的地址(因为新的地址位置更新在新的SW块上了)
s_psn = SECTOR(pbn, 0);
s_lsn = lpn * SECT_NUM_PER_PAGE;

memset (lsns, 0xFF, sizeof (lsns));

for (i = 0; i < SECT_NUM_PER_PAGE; i++)
{
lsns[i] = s_lsn + i;
}
//nand_page_write 需要写入的物理扇区地址,逻辑扇区号(一组地址)
nand_page_write(s_psn, lsns, 0, 1);

但是如果偏移量offset不为0,则需要判断这个偏移量是否紧接着SW块上的下一个空闲位置。这里首先需要判断当前唯一的SW块是否是更新数据块对应的SW块,如果不是,则直接可以判断不是连续的,将该请求更新到RW块即可,如果是对应的SW块,查看该LPN是否紧挨着上一个请求的地址,如果是,那好极了直接插入SW块,如果不是,则需要判断这个请求地址是在之前的还是在更新数据后面的(会采用不同的合并策略)。先看对应的SW块是否为当前数据块的对应SW部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// when lbn is the "owner" of SW log block  
if( BMT[lbn] == global_SW_blk.data_blk) {
//last_lpn表示上一次写访问的lpn
last_lpn = getLastlpnfromPMT();

//sequential writing
if(lpn == (last_lpn+1) ) {

// write page
pbn = global_SW_blk.logblk.pbn;
//分配的新的物理扇区地址是SW块对应的物理块当前空闲扇区的地址
s_psn = SECTOR(pbn,(PAGE_NUM_PER_BLK - global_SW_blk.logblk.fpc)* SECT_NUM_PER_PAGE );
s_lsn = lpn * SECT_NUM_PER_PAGE;

memset (lsns, 0xFF, sizeof (lsns));

for (i = 0; i < SECT_NUM_PER_PAGE; i++)
{
lsns[i] = s_lsn + i;
}

nand_page_write(s_psn, lsns, 0, 1);
//更新SW相关的索引
global_SW_blk.logblk.fpc--;
global_SW_blk.logblk.lpn[page_offset] = lpn; //store lpn of the request
global_SW_blk.logblk.lpn_status[page_offset] = 1; // 1: valid
PMT[0] = global_SW_blk.logblk;

//将原来的旧数据页标记为无效,找到就数据页的物理地址,和索引,注意这个旧数据可能存在RW块上,所以检索对应的RW是否有过更新,有则直接标记为无效
// invalidate page in data block

// invalidate page in log block if some data written in some log block
invalid_flag = 0;
//检索RW块上是否有过对应lpn的数据更新
for( i = 1; (i < total_log_blk_num) && (invalid_flag != 1); i++){
for( j = 0; j < PAGE_NUM_PER_BLK; j++){
if((PMT[i].lpn[j] == lpn) && (PMT[i].lpn_status[j] == 1)) {
// invalidate
PMT[i].lpn_status[j] = -1; // -1: invalid

s_psn = ((PMT[i].pbn * PAGE_NUM_PER_BLK + j) * SECT_NUM_PER_PAGE);
s_lsn = lpn * SECT_NUM_PER_PAGE;

for(k = 0; k<SECT_NUM_PER_PAGE; k++){
nand_invalidate(s_psn + k, s_lsn + k);
}
nand_stat(OOB_WRITE);
invalid_flag = 1;
break;
}
}
}

if(invalid_flag == 0 ) {

//如果对应的RW块没有更新过,那么数据块中的数据是最新的,则将其置为无效
// invalidate page in data block

s_psn = ((BMT[lbn] * PAGE_NUM_PER_BLK + page_offset) * SECT_NUM_PER_PAGE);
s_lsn = lpn * SECT_NUM_PER_PAGE;


for(i = 0; i<SECT_NUM_PER_PAGE; i++){
nand_invalidate(s_psn + i, s_lsn + i);
}
nand_stat(OOB_WRITE);
}
}
//上面是满足连续地址请求,但是可能出现例如lpn为1,2,3,2或1,2,3,5这两种情况中断,有不同的回收操作
//random writing
else {
//1,2,3,2这种情况就比较糟糕了,需要将原来的日志块中1,3和旧数据块中4,和新的2一起写到新的块,形成新的数据块
if( lpn <= (last_lpn)) {
//SW全合并和下面的RW不一样,这个全合并只对应一个数据块
merge_full_SW(lpn * SECT_NUM_PER_PAGE);
missing_cnt++;
}
else {
//1,2,3,5这里就执行部分合并,将数据块中的有效数据移动到日志块中,将数据块标记为新的数据块
// Note that during partial merge, new write will be taken care of
merge_partial(global_SW_blk.logblk.pbn, global_SW_blk.data_blk, global_SW_blk.logblk.fpc,lpn*SECT_NUM_PER_PAGE);
merge_partial_num++;
}
//因为旧的SW连续中断,所以需要分配新的SW空块
global_SW_blk.logblk.pbn = nand_get_free_blk(1);
global_SW_blk.logblk.fpc = PAGE_NUM_PER_BLK;
//不绑定新的数据块
global_SW_blk.data_blk = -1;
for( i = 0; i < PAGE_NUM_PER_BLK; i++) {
global_SW_blk.logblk.lpn[i] = -1; // -1: no data written
global_SW_blk.logblk.lpn_status[i] = 0; // 0: free
}
PMT[0] = global_SW_blk.logblk; // insert new SW_blk info into PMT

}
}else{
.........不是对应SW块,则随机写
}

如果SW不是对应当前的数据块,且offset又不是0,肯定可以判读就是随机写了,这就找RW块上空闲的位置写入,首先判读原来的数据是否存在旧的数据块还是RW上,先判断是否在RW块,在则置该数据为无效,不在则找对应旧数据块上的数据为无效。之后利用getRWblk()找到当前可用的RW日志块,写到对应的空闲位置上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
if(BMT[lbn] == global_SW_blk.data_blk){
..........
}else{
// invalidate page in log block if some data written in some log block
invalid_flag = 0;

for( i = 1; (i < total_log_blk_num) && (invalid_flag != 1); i++){
for( j = 0; j < PAGE_NUM_PER_BLK; j++){
if((PMT[i].lpn[j] == lpn) && (PMT[i].lpn_status[j] == 1)) {
// invalidate
PMT[i].lpn_status[j] = -1; // -1: invalid

s_psn = ((PMT[i].pbn * PAGE_NUM_PER_BLK + j) * SECT_NUM_PER_PAGE);
s_lsn = lpn * SECT_NUM_PER_PAGE;

for(k = 0; k<SECT_NUM_PER_PAGE; k++){
nand_invalidate(s_psn + k, s_lsn + k);
}
nand_stat(OOB_WRITE);
invalid_flag = 1;
break;
}
}
}

if(invalid_flag == 0 ) {

// invalidate page in data block
s_psn = ((BMT[lbn] * PAGE_NUM_PER_BLK + page_offset) * SECT_NUM_PER_PAGE);
s_lsn = lpn * SECT_NUM_PER_PAGE;

for(i = 0; i<SECT_NUM_PER_PAGE; i++){
nand_invalidate(s_psn + i, s_lsn + i);
}
nand_stat(OOB_WRITE);
}

currRWblk = getRWblk();

//这里的RW块的回收是被动的,当RW不够用的时候,回收FirstRWblk(这里就涉及到了全合并操作)
// no available RW log block
if(currRWblk == -1){
//这里的firstRWblk是1-total_log_num-1的取值,就是PMT[]的下标
firstRWblk = getFirstRWblk();
//这个merge_full是专门回收RW日志块的,找到PMT[x]---->logMap就有了
merge_full(firstRWblk);
//全合并完,重新为该下标PMT[x]分配绑定新的物理块和初始化
//initialize
PMT[firstRWblk].pbn = nand_get_free_blk(1);
PMT[firstRWblk].fpc = PAGE_NUM_PER_BLK;
memset(PMT[firstRWblk].lpn, 0xFF, sizeof(int)*PAGE_NUM_PER_BLK);
memset(PMT[firstRWblk].lpn_status, 0x00, sizeof(int)*PAGE_NUM_PER_BLK);
//初始化后就当新的块来使用
global_currRWblk = firstRWblk;
currRWblk = firstRWblk;
}

//如果当前的currRW有空的位置,则写入空闲位置上
currRWpageoffset = PAGE_NUM_PER_BLK - PMT[currRWblk].fpc;

// write page
pbn = PMT[currRWblk].pbn;
s_psn = SECTOR(pbn, currRWpageoffset * SECT_NUM_PER_PAGE );
s_lsn = lpn * SECT_NUM_PER_PAGE;

PMT[currRWblk].lpn[currRWpageoffset] = lpn;
PMT[currRWblk].lpn_status[currRWpageoffset] = 1; // 1: valid

memset (lsns, 0xFF, sizeof (lsns));

for (i = 0; i < SECT_NUM_PER_PAGE; i++)
{
lsns[i] = s_lsn + i;
}

nand_page_write(s_psn, lsns, 0, 1);
PMT[currRWblk].fpc--;
}


合并操作代码

  • SW的交换合并(void merge_partial(int log_pbn, int data_pbn, int fpc, int req_lsn)
  • SW的部分合并(void merge_partial(int log_pbn, int data_pbn, int fpc, int req_lsn)
  • SW的全合并(void merge_full_SW(int req_lsn)
  • RW的全合并(void merge_full(int pmt_index)
    需要注意的是SW块的全合并只是对应一个绑定的数据块,只涉及2次块擦除和寻找一个新的空闲块备份数据,和RW的全合并是完全不同的。RW因为全关联,所以是不存在交换和部分合并的操作的!!!。先从简单的交换合并代码开始理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// for sequential
void merge_switch(int log_pbn, int data_pbn)
{
//1. search & update pointers
//2. update BMT
int i;
for(i = 0; i< total_blk_num; i++){
if( BMT[i] == data_pbn ){
BMT[i] = log_pbn;
break;
}
}

ASSERT(i != total_blk_num);

//3. erase (data_pbn)
nand_erase(data_pbn);
}

SW交换合并只需要将SW标记为新的数据块就行,但是这里注意要更新对应的块映射(BMT[?]!!!),同时擦除旧的数据块。
SW的部分合并,就需要从旧的数据块中读出有效的数据,依次添入SW后面的空闲位置,同时将SW标记为新的数据块,更新对应的块映射(BMT[?]!!!),擦除旧的数据块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
void merge_partial(int log_pbn, int data_pbn, int fpc, int req_lsn)
{
//1. copy valid pages from data_pbn to log_pbn
int i,j,k,h,m;
int lpn;
int s_psn, s_lsn;
int sect_index = 0;
int valid_sect_num;
//找到RW缺的有效数据页的起始地址
int start = PAGE_NUM_PER_BLK - fpc;

int invalid_flag,valid_flag;

int copy[SECT_NUM_PER_PAGE];
memset(copy, 0xFF, sizeof copy);
//根据数据块的物理地址,倒推找到对应的逻辑块地址lpn(j),就可以计算起始的lpn到结束的lpn
for(j=0; j < total_blk_num; j++) {
if(BMT[j] == data_pbn) {
break;
}
}

ASSERT(j != total_blk_num);

lpn = j*PAGE_NUM_PER_BLK;
//因为SW块是严格得依次写入的,所以偏移量和lpn的偏移量是一致的,所以就可以确定缺的lpn有哪些,依次将这些lpn从原来的旧数据块中读出来,并置位原来的数据块的页无效
//但还有一种情况,就是lpn不存在原来的数据块中,则从RW块中找到最新有效的数据
for (i = start; i < PAGE_NUM_PER_BLK; i++)
{
s_lsn = (lpn+i) * SECT_NUM_PER_PAGE;
for (m = 0; m < SECT_NUM_PER_PAGE; m++) {
copy[m] = s_lsn + m;
}

valid_flag = nand_oob_read( SECTOR(data_pbn, i * SECT_NUM_PER_PAGE));
if(valid_flag == 1)
{
//如果原旧数据块数据为有效,还需要判断该数据是否当前的请求是否冲突,如果冲突,则以当前的请求为最新(不需要读操作了)
if(s_lsn != req_lsn){

valid_sect_num = nand_page_read( SECTOR(data_pbn, i * SECT_NUM_PER_PAGE), copy, 1);
nand_page_write(SECTOR(log_pbn, i*SECT_NUM_PER_PAGE), copy, 1, 1);
}
else{
nand_page_write(SECTOR(log_pbn, i*SECT_NUM_PER_PAGE), copy, 0, 1);
}

}
else if( valid_flag == -1)
{
//如果当前的旧数据块该数据页为无效,则从RW块中找最新的数据,也要判读是否和当前的请求冲突,如果冲突,则以当前的请求为最新(不需要读操作了)
invalid_flag = 0;
for( j = 0; j < total_log_blk_num && invalid_flag != 1; j++) {
for( k = 0; k < PAGE_NUM_PER_BLK;k++) {
if(PMT[j].lpn[k] == (lpn+i)) {
//invalidate in log block

PMT[j].lpn_status[k] = -1; // -1: invalid

s_psn = ((PMT[j].pbn * PAGE_NUM_PER_BLK + k) * SECT_NUM_PER_PAGE);
s_lsn = (lpn+i) * SECT_NUM_PER_PAGE;

// copy the page in log block into new data block
valid_sect_num = nand_page_read(s_psn, copy, 1);

//不同的nand_page_write操作
if(s_lsn != req_lsn){
nand_page_write(SECTOR(log_pbn, i*SECT_NUM_PER_PAGE), copy, 1, 1);
}
else{
nand_page_write(SECTOR(log_pbn, i*SECT_NUM_PER_PAGE), copy, 0, 1);
}

// invalidate the page in log block
for(h = 0; h<SECT_NUM_PER_PAGE; h++){
nand_invalidate(s_psn + h, s_lsn + h);
}
nand_stat(OOB_WRITE);

invalid_flag = 1;
break;
}
}
}
}
else
{
if(s_lsn == req_lsn){
nand_page_write(SECTOR(log_pbn, i*SECT_NUM_PER_PAGE), copy, 0, 1);
}
}
}
//更新对应的数据块索引
//2. update BMT
for(i = 0; i<total_blk_num; i++){
if( BMT[i] == data_pbn ){
BMT[i] = log_pbn;
break;
}
}
//擦除旧的数据块
ASSERT(i != total_blk_num);
//3. erase (data_pbn)
nand_erase(data_pbn);
}

SW块全合并,其实和SW的交换合并很像,但是需要找到一个全新空块的数据块,将SW中有效的数据页和旧数据块(也可能在其他RW块上)中的数据页读出来
如1,2,3,2这样引发的全合并(如果块大小为4),则SW的1,3为有效,req_lpn=2为最新的,从其他地方(旧数据块中或RW)找到4,一起写到新的块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
void merge_full_SW(int req_lsn)
{
int i,h;
int s_lsn, s_psn, s_psn1, lpn, valid_flag = 0;
int new_pbn,pbn,lbn = -1;
sect_t lsns[SECT_NUM_PER_PAGE];

merge_full_num++;

PMT[0] = global_SW_blk.logblk;
//pbn是对应的旧数据块
pbn = global_SW_blk.data_blk;
//分配新的数据块,为数据迁移做准备
new_pbn = nand_get_free_blk(1);

//同样利用原来的BMT的映射关系找到对应的lbn,利用lbn反过来查找最新的数据在哪里
for(i = 0; i<total_blk_num; i++){
if( BMT[i] == pbn){
lbn = i;
break;
}
}
//更新BMT的映射关系为新分配的物理块<---->lbn
BMT[lbn] = new_pbn;
ASSERT( lbn != -1);

//先依次读出RW块中的有效数据(这个都是按次序的,除了发生冲突的req_lsn),直到出现空白页(status==0)
for( i =0 ; i < PAGE_NUM_PER_BLK; i++) {

if(PMT[0].lpn_status[i] == -1){ // -1: invalid, 0: free, 1: valid
ASSERT(0);
continue;
}
else if(PMT[0].lpn_status[i] == 1) {
lpn = (lbn * PAGE_NUM_PER_BLK) + i;
s_lsn = lpn * SECT_NUM_PER_PAGE;
s_psn = SECTOR(new_pbn,i* SECT_NUM_PER_PAGE);
memset (lsns, 0xFF, sizeof (lsns));

for (h = 0; h < SECT_NUM_PER_PAGE; h++) {
lsns[h] = s_lsn + h;
}

if(req_lsn == s_lsn) {
nand_page_write(s_psn, lsns, 0, 1);
}
else{
s_psn1 = (global_SW_blk.logblk.pbn * PAGE_NUM_PER_BLK + i) * SECT_NUM_PER_PAGE;
nand_page_read(s_psn1, lsns, 1); // read from log block
nand_page_write(s_psn, lsns, 1, 1);
}
}
else {
lpn = (lbn * PAGE_NUM_PER_BLK) + i;
s_lsn = lpn * SECT_NUM_PER_PAGE;
//s_psn是要写入的新的物理地址
s_psn = SECTOR(new_pbn,i* SECT_NUM_PER_PAGE);
//s_psn1是旧的数据块物理地址
s_psn1 = (pbn * PAGE_NUM_PER_BLK + i) * SECT_NUM_PER_PAGE;
for (h = 0; h < SECT_NUM_PER_PAGE; h++) {
lsns[h] = s_lsn + h;
}
valid_flag = nand_oob_read(s_psn1);
if( valid_flag == 1){ // read from data block
nand_page_read(s_psn1,lsns,1);
nand_page_write(s_psn,lsns,1,1);
}
//万一vaild_flag!=1就有bug了!!,这是问题啊?不知道源码作者怎么处理的
}
}
//擦除旧的数据块和RW块(2次块擦除)
nand_erase(pbn);
nand_erase(global_SW_blk.logblk.pbn);
}

RW块的全合并操作就相当复杂了。就是要找这个RW到底对应了多少个数据块。

  • 找到对应的数据块data_pbn
    logMap.lpn[x]项存的lpn满足logMap.lpn[x].status=1时,则可以将用该lpn计算—–>lbn—BMT[lbn]–>data_pbn
    统计不同的数据data_pbn就可以知道关联多少不同的数据块
  • 针对不同的data_pbn分配新的物理块地址 new_pbn
  • 从data_pbn<---->lbn—>lpn,依次找到最新的数据页写入到新的new_pbn
    瞎话讲那么多还是,先看代码讲下来:
    关于输入的pmt_index是PMT[?]的下标
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    void merge_full(int pmt_index)  
    {
    int i,j,k,m,h;
    int size;
    int old_pbn;

    int lbn,lpn,new_pbn,pbn,offset, invalid_flag;
    int s_lsn, s_psn;
    sect_t lsns[SECT_NUM_PER_PAGE];

    //这个错误判断,首先回收的RW块必须是耗净的,且不能回收PMT[0](专门映射SW块的)
    if(PMT[pmt_index].fpc != 0 && pmt_index == 0) {
    printf("something sucks");
    ASSERT(0);
    }


    // Check with all page in a log block
    //依次查找当前要回收的RW中的有效页`PMT[pmt_index].lpn_status[i] == 1`
    for(i = 0; i<PAGE_NUM_PER_BLK; i++)
    {
    if(PMT[pmt_index].lpn_status[i] != 1){ // -1: invalid, 0: free, 1: valid
    continue;
    }
    else{
    offset = PMT[pmt_index].lpn[i] % PAGE_NUM_PER_BLK;
    //依据lpn--->lbn--BMT-->old_pbn(但是这个数据块可能同时还关联了一个SW块这需要处理)
    lbn = PMT[pmt_index].lpn[i] / PAGE_NUM_PER_BLK;
    old_pbn = BMT[lbn];

    //如果还关联一个SW块,则先对该old_pbn块进行一次SW的部分合并
    if(old_pbn == global_SW_blk.data_blk) {
    merge_partial(global_SW_blk.logblk.pbn, global_SW_blk.data_blk, global_SW_blk.logblk.fpc,-1);
    merge_partial_num++;

    global_SW_blk.logblk.pbn = nand_get_free_blk(1);
    global_SW_blk.logblk.fpc = PAGE_NUM_PER_BLK;
    global_SW_blk.data_blk = -1;
    for( h = 0; h < PAGE_NUM_PER_BLK; h++) {
    global_SW_blk.logblk.lpn[h] = -1; // -1: no data written
    global_SW_blk.logblk.lpn_status[h] = 0; // 0: free
    }
    PMT[0] = global_SW_blk.logblk; // insert new SW_blk info into PMT
    continue;

    }
    //OK,经过上述处理以后,关联的旧数据块不可能出现最新数据在SW上,可以开始分配新的数据块,开始数据整合
    new_pbn = nand_get_free_blk(1);
    BMT[lbn] = new_pbn;
    merge_full_num++;
    //对该lbn所属于的lpn全部遍历重写入新的块
    for(j =0 ; j < PAGE_NUM_PER_BLK ; j++) {

    lpn = (lbn * PAGE_NUM_PER_BLK) + j;
    /* for nand_oob_read */
    //s_psn是旧数据块相同偏移位置位置上,物理扇区地址,下面读取该页数据是否要使用
    s_psn = SECTOR(old_pbn, j*SECT_NUM_PER_PAGE); // chk if correct
    s_lsn = lpn * SECT_NUM_PER_PAGE;
    memset (lsns, 0xFF, sizeof (lsns));

    for (h = 0; h < SECT_NUM_PER_PAGE; h++) {
    lsns[h] = s_lsn + h;
    }

    size = nand_oob_read(s_psn);
    //旧的数据块中的psn为有效,读取写入到新的块地址,将其旧地址置为无效
    if(size == 1) // valid -> invalidate page in the data block
    {
    // invalidate page in data block
    s_psn = SECTOR(old_pbn, j*SECT_NUM_PER_PAGE); // chk if correct
    s_lsn = lpn * SECT_NUM_PER_PAGE;


    // read from data block - youkim
    memset (lsns, 0xFF, sizeof (lsns));
    for (h = 0; h< SECT_NUM_PER_PAGE; h++)
    {
    lsns[h] = s_lsn + h;
    }

    nand_page_read(s_psn, lsns, 1);

    // invalidate page in data block
    for(h = 0; h<SECT_NUM_PER_PAGE; h++){
    nand_invalidate(s_psn + h, s_lsn + h);
    }
    nand_stat(OOB_WRITE);

    // write into new pbn
    //此时s_psn为新块的写入物理扇区地址
    s_psn = SECTOR(new_pbn,j* SECT_NUM_PER_PAGE);
    s_lsn = lpn * SECT_NUM_PER_PAGE;

    memset (lsns, 0xFF, sizeof (lsns));

    for (h = 0; h< SECT_NUM_PER_PAGE; h++)
    {
    lsns[h] = s_lsn + h;
    }
    nand_page_write(s_psn, lsns, 1, 1);
    }
    else if(size == -1)
    {
    //如果旧的数据块中数据页为无效,则遍历RW日志块找到其中最新数据
    invalid_flag = 0;

    for( k = 1; (k < total_log_blk_num) && (invalid_flag != 1); k++){
    for( m = 0; m < PAGE_NUM_PER_BLK; m++){
    if((PMT[k].lpn[m] == lpn) && (PMT[k].lpn_status[m] == 1)) {
    // invalidate page in log block
    PMT[k].lpn_status[m] = -1; // -1: invalid

    s_psn = ((PMT[k].pbn * PAGE_NUM_PER_BLK + m) * SECT_NUM_PER_PAGE);
    s_lsn = lpn * SECT_NUM_PER_PAGE;

    // read from data block - youkim
    memset (lsns, 0xFF, sizeof (lsns));
    for (h = 0; h< SECT_NUM_PER_PAGE; h++)
    {
    lsns[h] = s_lsn + h;
    }
    nand_page_read(s_psn, lsns, 1);

    // invalidate
    for(h = 0; h<SECT_NUM_PER_PAGE; h++){
    nand_invalidate(s_psn + h, s_lsn + h);
    }
    nand_stat(OOB_WRITE);
    invalid_flag = 1;
    break;
    }
    }
    }


    // write into new pbn
    s_psn = SECTOR(new_pbn,j* SECT_NUM_PER_PAGE);
    s_lsn = lpn * SECT_NUM_PER_PAGE;

    memset (lsns, 0xFF, sizeof (lsns));

    for (h = 0; h< SECT_NUM_PER_PAGE; h++)
    {
    lsns[h] = s_lsn + h;
    }
    nand_page_write(s_psn, lsns, 1, 1);
    }
    else{

    }
    }
    // erase the data block 错误判断
    if(old_pbn == PMT[0].pbn){
    printf("1. something sucks");
    ASSERT(0);
    }
    nand_erase(old_pbn);
    }
    }
    // erase the log block
    nand_erase(PMT[pmt_index].pbn);

    free_RW_blk_num++;
    }

关于从全合并操作其实可以找到关联的对应的数据块个数,fast.c源码的关键部分就这么些了

热评文章