从flashcache的创建开始,介绍flashcache在SSD上的layout和内存数据结构,简单地说就是数据组织形式。
sprintf(dmsetup_cmd, "echo 0 %lu flashcache %s %s %s %d 2 %lu %lu %d %lu %d %lu" " | dmsetup create %s", disk_devsize, disk_devname, ssd_devname, cachedev, cache_mode, block_size, cache_size, associativity, disk_associativity, write_cache_only, md_block_size, cachedev);
从flashcache之后的参数算起:
dmc的成员 | dmsetup create中的参数 | 默认值 | 含义 | |
---|---|---|---|---|
disk_dev | disk_devname | 无 | 慢速块设备的名字 | |
cache_dev | ssd_devname | 无 | SSD设备的名字 | |
dm_vdevname | flashcache的名字 | 无 | flashcache起的名字 | |
cache_mode | cache_mode | 无 | 三种合法值:write_back,write_through和write_around | |
persistence(非dmc的成员变量) | 2 | 2 | 实际flashcache_ctr函数,即为flashcache_create服务,也为flashcache_load服务 | |
block_size | block_size | 8 | 8个扇区即4K | |
size | cache_size | 设备扇区总数/block_size, | 注意这个值的含义是block的个数,即总扇区除以block_size. | |
assoc | associativity | 512 | 合法值为(256,8192)之间的2的整数幂,不包含256和8192 | |
disk_assoc | ||||
write_only_cache | write_cache_only | 0 | write_back模式有一个子模式,即write_only | |
md_block_size | 8 | |||
num_sets | dmc->size » dmc->assoc_shift | |||
影响flashcache布局的几个参数有:
- block_size: 默认情况下值为8,即8个扇区组成一个block,即block的大小为4KB
- size : block的个数
注意,注意在
//截止到此处,dmc->size是SSD设备的扇区个数, //后面调用dmc->size /= (dmc->block_size)执行之后,才变成block的个数。 dmc->md_blocks = INDEX_TO_MD_BLOCK(dmc, dmc->size / dmc->block_size) + 1 + 1; /*总扇区数减去md_block需要的扇区数,得到最多可以用于存放数据的扇区数*/ dmc->size -= dmc->md_blocks * MD_SECTORS_PER_BLOCK(dmc); /*可以用来存放cache数据的block个数,默认情况下即4K的个数*/ dmc->size /= dmc->block_size; /*注意,block是要组成set的,因此有assoc的概念,默认512个block组成一个set *因此block的个数需要向下对齐512的倍数*/ dmc->size = (dmc->size / dmc->assoc) * dmc->assoc; /*有了准确的block的个数,需要的meta data block重新计算*/ dmc->md_blocks = INDEX_TO_MD_BLOCK(dmc, dmc->size) + 1 + 1; DMINFO("flashcache_writeback_create: md_blocks = %d, md_sectors = %dn", dmc->md_blocks, dmc->md_blocks * MD_SECTORS_PER_BLOCK(dmc)); dev_size = to_sector(dmc->cache_dev->bdev->bd_inode->i_size); cache_size = dmc->md_blocks * MD_SECTORS_PER_BLOCK(dmc) + (dmc->size * dmc->block_size); if (cache_size > dev_size) { DMERR("Requested cache size exceeds the cache device's capacity" "(%lu>%lu)", cache_size, dev_size); vfree((void *)header); return 1; }
这段代码的执行过后,我们基本就能对flashcache的组织形式有一定的了解了。首先是8个扇区做成一个block,然后是512个block组成一个set,这样的话,一个set的大小为2M,将SSD整体空间扣除meta需要的部分之后,组织成这样的结构:
注意,这只是cache block的部分,对于flashcache来说,还有metadata block和superblock。和文件系统一样,flashcache也有superblock,介绍flashcache的组织形式:
header = (struct flash_superblock *)vmalloc(MD_BLOCK_BYTES(dmc)); if (!header) { DMERR("flashcache_writeback_create: Unable to allocate sector"); return 1; } struct flash_superblock { sector_t size; /* Cache size */ u_int32_t block_size; /* Cache block size */ u_int32_t assoc; /* Cache associativity */ u_int32_t cache_sb_state; /* Clean shutdown ? */ char cache_devname[DEV_PATHLEN]; /* Contains dm_vdev name as of v2 modifications */ sector_t cache_devsize; char disk_devname[DEV_PATHLEN]; /* underlying block device name (use UUID paths!) */ sector_t disk_devsize; u_int32_t cache_version; u_int32_t md_block_size; u_int32_t disk_assoc; u_int32_t write_only_cache; };
尽管flashcache的superblock需要的空间比较小,但是flashcache给superblock预留了一个meta data block的大小,即默认情况下4KB的大小,为将来可能的扩展预留的空间。
flash_superblock这个数据结构存在在SSD这个设备的第一个4K,当机器重启之后,flashcache_load会阅读该设备,确保头部扇区中存放的内容,即superblock的内容:
ssd_devname = argv[optind++]; cache_fd = open(ssd_devname, O_RDONLY); if (cache_fd < 0) { fprintf(stderr, "Failed to open %sn", ssd_devname); exit(1); } lseek(cache_fd, 0, SEEK_SET); if (read(cache_fd, buf, 512) < 0) { fprintf(stderr, "Cannot read Flashcache superblock %sn", ssd_devname); exit(1); } if (!(sb->cache_sb_state == CACHE_MD_STATE_DIRTY || sb->cache_sb_state == CACHE_MD_STATE_CLEAN || sb->cache_sb_state == CACHE_MD_STATE_FASTCLEAN || sb->cache_sb_state == CACHE_MD_STATE_UNSTABLE)) { fprintf(stderr, "%s: Invalid Flashcache superblock %sn", pname, ssd_devname); exit(1); }
创建flashcache的时候,flashcache_create也会阅读SSD设备的第一个扇区,来确保SSD是不是已经创建了flashcache。
对于上面网格图中的任何一个cache block,都需要数据结构来描述其状态,比如值是否有效,是否DIRTY等,其数据结构如下:
#ifdef FLASHCACHE_DO_CHECKSUMS struct flash_cacheblock { sector_t dbn; /* Sector number of the cached block */ u_int64_t checksum; u_int32_t cache_state; /* INVALID | VALID | DIRTY */ } __attribute__ ((aligned(32))); #else struct flash_cacheblock { sector_t dbn; /* Sector number of the cached block */ u_int32_t cache_state; /* INVALID | VALID | DIRTY */ } __attribute__ ((aligned(16))); #endif
对于我们而言,flash_cacheblock的大小为16字节,因此,每个cache block都会有16字节的元数据。这16字节描述了一个cache block。每个meta data block 默认有4KB,即每个meta data block可以存放256 个cache block的元数据信息。
综合上述讨论,一个完整的SSD layout如下所示:
上面的布局,主要是块设备上的布局,除此外,flashcache正常运行期间,需要消耗内存,内存中有数据结构管理这些cache block,如下所示:
order = dmc->size * sizeof(struct cacheblock); struct cacheblock { u_int16_t cache_state; int16_t nr_queued; /* jobs in pending queue */ u_int16_t lru_prev, lru_next; u_int8_t use_cnt; u_int8_t lru_state; sector_t dbn; /* Sector number of the cached block */ u_int16_t hash_prev, hash_next; #ifdef FLASHCACHE_DO_CHECKSUMS u_int64_t checksum; #endif } __attribute__((packed));
目前来讲,不考虑checksum,内存中18 Byte 描述一个cache block(默认4KB)。
order = dmc->size * sizeof(struct cacheblock); DMINFO("Allocate %luKB (%luB per) mem for %lu-entry cache" "(capacity:%luMB, associativity:%u, block size:%u " "sectors(%uKB))", order >> 10, sizeof(struct cacheblock), dmc->size, cache_size >> (20-SECTOR_SHIFT), dmc->assoc, dmc->block_size, dmc->block_size >> (10-SECTOR_SHIFT)); dmc->cache = (struct cacheblock *)vmalloc(order); if (!dmc->cache) { vfree((void *)header); DMERR("flashcache_writeback_create: Unable to allocate cache md"); return 1; } memset(dmc->cache, 0, order); /* Initialize the cache structs */ for (i = 0; i < dmc->size ; i++) { dmc->cache[i].dbn = 0; #ifdef FLASHCACHE_DO_CHECKSUMS dmc->cache[i].checksum = 0; #endif dmc->cache[i].cache_state = INVALID; dmc->cache[i].lru_state = 0; dmc->cache[i].nr_queued = 0; }
通过这个18 Byte的内存描述一个flashcache 的cache block,我们可以估算,一个400G 的SSD作为flashcache的SSD部分,消耗的内存约为:
400G/4KB*18 = 1.8GB
cache_set
dmc的assoc 默认是512,表示512个block组成一个set,即512*4K= 2MB:
init: /*计算整个flashcache set的个数*/ dmc->num_sets = dmc->size >> dmc->assoc_shift; order = dmc->num_sets * sizeof(struct cache_set); dmc->cache_sets = (struct cache_set *)vmalloc(order); if (!dmc->cache_sets) { ti->error = "Unable to allocate memory"; r = -ENOMEM; vfree((void *)dmc->cache); goto bad3; } memset(dmc->cache_sets, 0, order); for (i = 0 ; i < dmc->num_sets ; i++) { dmc->cache_sets[i].set_fifo_next = i * dmc->assoc; dmc->cache_sets[i].set_clean_next = i * dmc->assoc; dmc->cache_sets[i].fallow_tstamp = jiffies; dmc->cache_sets[i].fallow_next_cleaning = jiffies; dmc->cache_sets[i].hotlist_lru_tail = FLASHCACHE_NULL; dmc->cache_sets[i].hotlist_lru_head = FLASHCACHE_NULL; dmc->cache_sets[i].warmlist_lru_tail = FLASHCACHE_NULL; dmc->cache_sets[i].warmlist_lru_head = FLASHCACHE_NULL; spin_lock_init(&dmc->cache_sets[i].set_spin_lock); }
对于每个set有单独的数据结构描述:
struct cache_set { spinlock_t set_spin_lock; u_int32_t set_fifo_next; u_int32_t set_clean_next; u_int16_t clean_inprog; u_int16_t nr_dirty; u_int16_t dirty_fallow; unsigned long fallow_tstamp; unsigned long fallow_next_cleaning; /* * 2 LRU queues/cache set. * 1) A block is faulted into the MRU end of the warm list from disk. * 2) When the # of accesses hits a threshold, it is promoted to the * (MRU) end of the hot list. To keep the lists in equilibrium, the * LRU block from the host list moves to the MRU end of the warm list. * 3) Within each list, an access will move the block to the MRU end. * 4) Reclaims happen from the LRU end of the warm list. After reclaim * we move a block from the LRU end of the hot list to the MRU end of * the warm list. */ u_int16_t hotlist_lru_head, hotlist_lru_tail; u_int16_t warmlist_lru_head, warmlist_lru_tail; u_int16_t lru_hot_blocks, lru_warm_blocks; #define NUM_BLOCK_HASH_BUCKETS 512 u_int16_t hash_buckets[NUM_BLOCK_HASH_BUCKETS]; u_int16_t invalid_head; };
注意,对于同一个set的cache block而言,根据状态,位于三个不同的链表之中:
- INVALID
- invalid_head为头部的invalid 链表
- VALID
- hot:
- hotlist_lru_head为头部,hotlist_lru_tail为尾部的hot链表
- warm
- warmlist_lru_head为头部,warmlist_lru_tail为尾部的warm链表
- hot:
注意,一个cacheblock只会位于其中的一条链表之中,不会同时属于hot和warm,更不会同时属于invalid和warm。
在64位系统上,指针的长度是8Byte,如果用普通的链表,prev next就要消耗16B的空间,这样是比较浪费的,flashcache使用是的u_int16_t类型的,每一个cacheblock通过一个2字节的short值,记录前一个cacheblock的值和后一个cacheblock的值。注意该值是同一个set的index值,因为默认set只有512,所以,2Byte的short足够记录下。
注意,当cacheblock中没有任何数据的时候,它位于invalid链表中,即这个链表里面都没啥数据。毫无疑问,当新建的flashcache里面,其实并没有任何有用的数据,并不和SATA DISK的数据相关联,因此,都会位于invalid 链表。在flashcache_ctr之中有如下的语句:
for (i = 0 ; i < dmc->size ; i++) { dmc->cache[i].hash_prev = FLASHCACHE_NULL; dmc->cache[i].hash_next = FLASHCACHE_NULL; /*注意,flashcache_ctr并非只有创建flashcache一种情况, *还有flashcache使用了一段时间之后,重启机器后的flashcache_load *因此,需要判断对应的cacheblock的cache_state状态值,来初始化到合适的链表*/ /*如果cache_state状态中VALID置位,则插入的flashcache_hash,方便查找*/ if (dmc->cache[i].cache_state & VALID) { flashcache_hash_insert(dmc, i); atomic_inc(&dmc->cached_blocks); } /*如果dirty,则dirty统计增加*/ if (dmc->cache[i].cache_state & DIRTY) { dmc->cache_sets[i / dmc->assoc].nr_dirty++; atomic_inc(&dmc->nr_dirty); } /*如果是新创建,或者该cacheblock并无有效数据,则插入Invalid链表 *对应新创建的flashcahce,所有的cacheblock都在invalid链表, *注意,并不是1条链表,而是每个cacheset都有1条链表*/ if (dmc->cache[i].cache_state & INVALID) flashcache_invalid_insert(dmc, i);
下面来介绍hotlist和warmlist,flashcache采用的缓存置换算法是LRU算法,它维护着2条链表:hot和warm。当然了,顾名思义,hot链表的数据更热,更不应该被置换出去。每条链表有head和tail,约靠近尾部的cacheblock,越热,越不应该被置换出去。
数据是被访问的,因此,频繁访问的数据,可能会从warm迁到(premote)hot,如果hot链表中最冷的数据(即靠近head的数据),也可能会被降级(demote)到warm中。
除此以外,可能某个cacheblock中存在合法的数据(VALID),但是由于新的io进来,第一反应肯定是会不会我请求的IO对应的地址 dbn恰好在flashcahce的中并且状态为VALID,如果找到皆大欢喜;如果找不到,第二反应是寻找一个无人用的cacheblock,即位于INVALID链表的cacheblock。如果很不幸,没有INVALID的cache block,所有的block都已经用了(VALID),这时候,就必须要寻找牺牲品了,即reclaim策略。
接下来我们以flashcache_read为例,详细介绍寻找cacheblock的方法。
寻找cacheblock
对于读请求,由函数flashcache_read负责处理,注意,对于那些注定不会进入cacheblock的读写,在进入flashcache_read之前都已经过滤掉了:
uncacheable = (unlikely(dmc->bypass_cache) || (to_sector(bio->bi_size) != dmc->block_size) || /* * If the op is a READ, we serve it out of cache whenever possible, * regardless of cacheablity */ (bio_data_dir(bio) == WRITE && ((dmc->cache_mode == FLASHCACHE_WRITE_AROUND) || flashcache_uncacheable(dmc, bio)))); spin_unlock_irqrestore(&dmc->ioctl_lock, flags); if (uncacheable) { flashcache_setlocks_multiget(dmc, bio); queued = flashcache_inval_blocks(dmc, bio); flashcache_setlocks_multidrop(dmc, bio); if (queued) { if (unlikely(queued < 0)) flashcache_bio_endio(bio, -EIO, dmc, NULL); } else { /* Start uncached IO */ /*绕过flashcache,直接访问慢速设备*/ flashcache_start_uncached_io(dmc, bio); } } else { /*如果io类型可以走flashcache,那么根据类型分别调用 *flashcache_read和flashcache_write*/ if (bio_data_dir(bio) == READ) flashcache_read(dmc, bio); else flashcache_write(dmc, bio); } return DM_MAPIO_SUBMITTED;
剩下内容的重点是cacheblock的查找 置换的策略,什么io走flashcache,什么io直接访问慢速设备,并不是我们关心的内容。我们继续以flashcache_read为例,介绍寻找cacheblock的过程。
下面代码是查找cacheblock的方法,主要的寻找过程位于flashcache_lookup函数。
flashcache_setlocks_multiget(dmc, bio); res = flashcache_lookup(dmc, bio, &index); /* Cache Read Hit case */ if (res > 0) { cacheblk = &dmc->cache[index]; if ((cacheblk->cache_state & VALID) && (cacheblk->dbn == bio->bi_sector)) { flashcache_read_hit(dmc, bio, index); return; } } /* * In all cases except for a cache hit (and VALID), test for potential * invalidations that we need to do. */ queued = flashcache_inval_blocks(dmc, bio); if (queued) { if (unlikely(queued < 0)) flashcache_bio_endio(bio, -EIO, dmc, NULL); if ((res > 0) && (dmc->cache[index].cache_state == INVALID)) /* * If happened to pick up an INVALID block, put it back on the * per cache-set invalid list */ flashcache_invalid_insert(dmc, index); flashcache_setlocks_multidrop(dmc, bio); return; }
因为数据是流动的,因此整个flashcache N个cacheset,每个cacheset M个cache block,其状态都是流动的,刚才我可能是invalid,可能很快我就位于warmlist了,再有数据访问,我可能就迁移到了hotlist了。因此理解flashcache_lookup,知道当用户某一个请求要访问sector_t dbn = bio->bi_sector 这个扇区的时候,如何查找cacheblock是理解状态流动的非常关键的一步。
static int flashcache_lookup(struct cache_c *dmc, struct bio *bio, int *index) { sector_t dbn = bio->bi_sector; #if DMC_DEBUG int io_size = to_sector(bio->bi_size); #endif unsigned long set_number = hash_block(dmc, dbn); int invalid, oldest_clean = -1; int start_index; start_index = dmc->assoc * set_number; DPRINTK("Cache lookup : dbn %llu(%lu), set = %d", dbn, io_size, set_number); find_valid_dbn(dmc, dbn, start_index, index); if (*index >= 0) { DPRINTK("Cache lookup HIT: Block %llu(%lu): VALID index %d", dbn, io_size, *index); /* We found the exact range of blocks we are looking for */ return VALID; } invalid = find_invalid_dbn(dmc, set_number); if (invalid == -1) { /* We didn't find an invalid entry, search for oldest valid entry */ find_reclaim_dbn(dmc, start_index, &oldest_clean); } /* * Cache miss : * We can't choose an entry marked INPROG, but choose the oldest * INVALID or the oldest VALID entry. */ *index = start_index + dmc->assoc; if (invalid != -1) { DPRINTK("Cache lookup MISS (INVALID): dbn %llu(%lu), set = %d, index = %d, start_index = %d", dbn, io_size, set_number, invalid, start_index); *index = invalid; } else if (oldest_clean != -1) { DPRINTK("Cache lookup MISS (VALID): dbn %llu(%lu), set = %d, index = %d, start_index = %d", dbn, io_size, set_number, oldest_clean, start_index); *index = oldest_clean; } else { DPRINTK_LITE("Cache read lookup MISS (NOROOM): dbn %llu(%lu), set = %d", dbn, io_size, set_number); } if (*index < (start_index + dmc->assoc)) return INVALID; else { dmc->flashcache_stats.noroom++; return -1; } }
注意,这就是寻找cacheblock的算法了。第一步是要寻找合适的set,因为flashcache默认情况下,每个set 512个cache block,首先要定位到那个cacheset,然后再cacheset中确定合适的cache block。通俗点说,就是分两步走:
- 找到合适的cache set
- 从该cache set中找到合适的cache block
第一步比较简单,根据bio的扇区号,计算hash,然后映射到对应的cache set:
unsigned long hash_block(struct cache_c *dmc, sector_t dbn) { unsigned long set_number, value; int num_cache_sets = dmc->size >> dmc->assoc_shift; /* * Starting in Flashcache SSD Version 3 : * We map a sequential cluster of disk_assoc blocks onto a given set. * But each disk_assoc cluster can be randomly placed in any set. * But if we are running on an older on-ssd cache, we preserve old * behavior. */ if (dmc->on_ssd_version < 3 || dmc->disk_assoc == 0) { value = (unsigned long) (dbn >> (dmc->block_shift + dmc->assoc_shift)); } else { /*我们走本分支*/ value = (unsigned long) (dbn >> dmc->disk_assoc_shift); /* Then place it in a random set */ value = jhash_1word(value, 0xbeef); } set_number = value % num_cache_sets; DPRINTK("Hash: %llu(%lu)->%lu", dbn, value, set_number); return set_number; }
我们走else分支,这里面有一个参数,初看flashcache不容易理解,即disk_assoc_shift,这个参数在创建flashcache的时候可以指定disk_associativity :
root@XMT-S02:~# dmsetup table osd4: 0 70316455903 flashcache conf: ssd dev (/dev/disk/by-partlabel/osd4-ssd), disk dev (/dev/disk/by-partlabel/osd4-data) cache mode(WRITE_BACK) capacity(446572M), associativity(512), data block size(4K) metadata block size(4096b) disk assoc(256K) skip sequential thresh(32K) total blocks(114322432), cached blocks(96119380), cache percent(84) dirty blocks(41155646), dirty percent(35) nr_queued(0)
我们看到,默认情况下,disk assoc的值是256K,事实上这个控制选项发挥作用也就是在寻找合适的cache set中发挥控制作用,如果没有这个选项,直接拿dbn进行hash,然后map到cacheset,相邻的两个dbn,可能压根就不会位于同一个cache set,那么将来对同一个cache set的io进行merge也就没啥必要了,因为相邻的dbn在同一个set的可能性并不大。
有了这个disk assoc参数就不同了,它hash之前,首先执行:
value = (unsigned long) (dbn >> dmc->disk_assoc_shift);
它确保的是,在同一个256KB块内的扇区,最终会得到同一个value,然后hash会map到同一个cache set,将来就有可能将相邻的请求merge,从而提高性能。
除了此处不太好理解意外,其他基本就是算出hash值,然后对cache set的个数求余,来决定落在那个cache set中。
第一步已经解决了,接下来是第二部,如何在cache set中找到
其算法核心可以分成三部:
- find_valid_dbn
- find_invalid_dbn
- find_reclaim_dbn
find_valid_dbn
static void find_valid_dbn(struct cache_c *dmc, sector_t dbn, int start_index, int *index) { *index = flashcache_hash_lookup(dmc, start_index / dmc->assoc, dbn); if (*index == -1) return; if (dmc->sysctl_reclaim_policy == FLASHCACHE_LRU && ((dmc->cache[*index].cache_state & BLOCK_IO_INPROG) == 0)) flashcache_lru_accessed(dmc, *index); /* * If the block was DIRTY and earmarked for cleaning because it was old, make * the block young again. */ flashcache_clear_fallow(dmc, *index); } int flashcache_hash_lookup(struct cache_c *dmc, int set, sector_t dbn) { struct cache_set *cache_set = &dmc->cache_sets[set]; int index; struct cacheblock *cacheblk; u_int16_t set_ix; #if 0 int start_index, end_index, i; #endif set_ix = *flashcache_get_hash_bucket(dmc, cache_set, dbn); while (set_ix != FLASHCACHE_NULL) { index = set * dmc->assoc + set_ix; cacheblk = &dmc->cache[index]; /* Only VALID blocks on the hash queue */ VERIFY(cacheblk->cache_state & VALID); VERIFY((cacheblk->cache_state & INVALID) == 0); if (dbn == cacheblk->dbn) return index; set_ix = cacheblk->hash_next; } return -1; } static inline u_int16_t * flashcache_get_hash_bucket(struct cache_c *dmc, struct cache_set *cache_set, sector_t dbn) { unsigned int hash = jhash_1word(dbn, 0xfeed); return &cache_set->hash_buckets[hash % NUM_BLOCK_HASH_BUCKETS]; }
我们已经找到了cache set,默认情况下cache set中有512个cache block,这些cache block中是否有我们需要的扇区呢?
最容易想到的是,逐个cache block比对,看下dbn号是否一致,状态是否是VALID。但是这种方法太蠢,效率太低。正确的方法是hash。
如果cache block中存在有效数据,他会根据对应的扇区号 dbn来计算hash,放入cache set中的合适bucket中。这种hash的做法,加速了cache set内部对某dbn是否存在在某个cacheblock的查找。
对于读来讲最完美的情况是,请求要求的数据块,恰巧位于flashcache的SSD 设备中,这种情况称为读命中。如果命中的话,因为该cacheblock的数据,相当于获得一次有效的访问,那么当空间吃紧的时候,应该降低该block被替换出去的概率,即提升其热度。
if (dmc->sysctl_reclaim_policy == FLASHCACHE_LRU && ((dmc->cache[*index].cache_state & BLOCK_IO_INPROG) == 0)) flashcache_lru_accessed(dmc, *index);
这个flashcache_lru_accessed函数,即某个cacheblock最近被访问时,需要执行的操作,代码中有一段注释,言简意赅地介绍了这部分的算法:
/* * Block is accessed. * * Algorithm : if (block is in the warm list) { block_lru_refcnt++; if (block_lru_refcnt >= THRESHOLD) { clear refcnt Swap this block for the block at LRU end of hot list } else move it to MRU end of the warm list } if (block is in the hot list) move it to MRU end of the hot list */
- 如果block目前在warm list
- 引用计数++
- 如果引用计数大于等于门限值(sysctl_lru_promote_thresh),一般是2,则从warm list 移入 hot list的LRU端(最左端)
- 如果引用计数低于门限值,则从移入 warm list的MRU端,即最右端
- 引用计数++
- 如果block 目前在hot list
- 将该block移入hot list的MRU端,即最右端。
两个链表host list和warm list,其最左端都是LRU端(Least Recent Used), 其最右端是MRU端(Most Recent Used)。一旦需要置换,将某些cacheblock中的内容踢出出去,选择的顺序如下:
Worm List LRU -------->Worm List MRU--------->Hot List LRU -------------> Hot List MRU
代码部分就不列了,简单的链表操作。
从cache set中寻找cache block的第一步就完成,这种情况是最幸运的一种,即要读取的内容所在的扇区,恰好在flashcache的 SSD部分中,数据有效VALID,可以拿到cache block的index,因为本次访问,将该cache block的热度提升到合适的位置。
但是也许并没有这么幸运,SSD中没有dbn对应扇区的内容,这种情况下,需要选择一个cache block来盛放即将从 慢速设备的扇区中读取上来的内容。这种情况下,第一选择是选择一个并且投入使用的cache block,即INVALID状态的cache block。
Why?
如果不这么做,选择一个VALID状态的cache block,该cache block的内容就会被新的dbn的内容替换,那么该cache block中老的内容,就被逐出SSD了,如果紧接着发来一个访问cache block 中老的dbn扇区的内容的请求,就会造成miss。更恶劣的情况是该cache block的内容是dirty,flashcache 可能不得不先等待dirty内容flush下去之后,方能使用该cache block。
所以当命中已成不可能的时候,选择INVALID状态的cache block是上策:
find_invalid_dbn
static int find_invalid_dbn(struct cache_c *dmc, int set) { int index = flashcache_invalid_get(dmc, set); if (index != -1) { if (dmc->sysctl_reclaim_policy == FLASHCACHE_LRU) flashcache_lru_accessed(dmc, index); VERIFY((dmc->cache[index].cache_state & FALLOW_DOCLEAN) == 0); } return index; }
寻找INVALID状态的cache block比较简单,因为对于一个cache set而言,所有的invalid都位于invalid_head为头部的链表,只需要摘下头部的cache block就可以了
int flashcache_invalid_get(struct cache_c *dmc, int set) { struct cache_set *cache_set; int index; struct cacheblock *cacheblk; cache_set = &dmc->cache_sets[set]; index = cache_set->invalid_head; if (index == FLASHCACHE_NULL) return -1; index += (set * dmc->assoc); cacheblk = &dmc->cache[index]; VERIFY(cacheblk->cache_state == INVALID); flashcache_invalid_remove(dmc, index); return index; }
同样的道理,因为该cache block从INVALID迁移到了warm list的MRU端。
其实这种情况还不错,因为还能找到闲置的cache block。随着flashcahe的使用,很可能这种情况也不可得。很可能该cache set下的所有的cache block都投入了战局,在该cache set已经找不到一块闲置的cache block了。
find_reclaim_dbn
这种情况下,就需要从投入使用的cacheblock中寻找一个牺牲品了,也就是cache block要回收了。优于SSD Dev和DIsk Dev大小的关系,不可能所有数据都存入SSD,所有的缓存算法都需要缓存替换算法,高效的缓存替换算法,能够获得更大的性能提升。
当选择牺牲品的时候,长期以来,我们维护hot list 和warm list的操作,就有了价值,这些信息给了我们选择牺牲品的依据。
static void find_reclaim_dbn(struct cache_c *dmc, int start_index, int *index) { if (dmc->sysctl_reclaim_policy == FLASHCACHE_FIFO) flashcache_reclaim_fifo_get_old_block(dmc, start_index, index); else /* flashcache_reclaim_policy == FLASHCACHE_LRU */ flashcache_reclaim_lru_get_old_block(dmc, start_index, index); }
Flashcache目前支持两种策略,FIFO和LRU。我们此处讨论LRU,这种算法就是将最近最不常使用的cache block替换出去。
代码给了注释:
/* * Get least recently used LRU block * * Algorithm : * Always pick block from the LRU end of the warm list. * And move it to the MRU end of the warm list. * If we don't find a suitable block in the "warm" list, * pick the block from the hot list, demote it to the warm * list and move a block from the warm list to the hot list. */
总是从worm list的LRU端找,然后把它移到MRU端。如果在warm list找不到合适的,那么从hot list的LRU端找,如果找到,执行demote操作,即将hot list的LRU和worm list MRU互换位置。
都是一些简单的链表操作,就不在此处贴代码了。
上一篇 下一篇