清华课题 Octopus 源码分析(四)
前言
由于项目工作的需要,我们团队阅读了清华在文件系统方面的一个比较新颖的工作:Octopus。Octopus是一个基于持久内存 NVM 和远程直接内存访问 RDMA 技术的分布式内存文件系统。清华的陆游游老师现已将代码开源,可 点击此处 阅读。
这一工作中的是 ATC-17 (CCF A类),可 点击此处 阅读论文。
我们团队希望通过学习清华的这个优秀的同行工作,来进一步开展自己对于分布式持久内存文件系统的研究。关于论文的分析,虽然有做PPT给同伴们介绍过,但具体的博客分析可能会晚些才放上来。这一系列的内容主要是分析Octopus的源码设计(少许会结合论文内容来讲,希望有兴趣的同学可以自己先读一读),总结Octopus的框架、结构、设计组件及代码创新点等。
系列分析总共包括 个部分。第一部分是 论文摘要,相当于Octopus系统的一个简介;第二部分是 设计框架,在这一部分我们会宏观地介绍Octopus的组成体系及各部分的功能及相互间的联系;第三部分是 代码分析,也是本博客的重中之重。在这一部分我们首先介绍头文件体系(在include文件夹中),了解Octopus的存储结构,表结构,主要数据结构,内存池划分等等。接下来我们介绍方法实现代码(在src文件夹中),我们会通过比对头文件内的函数名称来看每一个方法是采用何种方式实现,好处是什么,取舍考虑是什么。进一步地,我们会通过代码文件间的依赖关系,函数依赖关系去深入探讨Octopus的创新性、局限性并留出进一步讨论的空间。
论文摘要
(内容请见系列上一篇博客)
设计框架
(内容请见系列上一篇博客)
src目录源码分析
fs 模块
bitmap.cpp
这个代码文件是对照 bitmap.hpp 所做的位图功能实现。我们知道,在文件系统中,位图的主要作用是记录 inode 块与数据块的空闲状态,以便于新的数据分配时不发生冲突。
首先,我们来看 Bitmap 的构造函数实现,代码段如下所示:
/* Constructor of bitmap. Here use existed buffer as bitmap and initialize other parameter based on buffer.
(If fail in constructing, error information will be printed in standard error output.)
@param count The count of total bits in the bitmap.
@param buffer The buffer to contain bitmap. */
Bitmap::Bitmap(uint64_t count, char *buffer) /* Constructor of bitmap. */
{
if (count % 8 == 0) {
if (buffer != NULL) { /* Judge if buffer is null or not. */
bytes = (uint8_t *)buffer; /* Assign byte array of bitmap. */
varCountTotal = count; /* Initialize count of total bits. */
varCountFree = 0; /* Initialize count of free bits. */
for (unsigned int index = 0; index < varCountTotal / 8; index++) { /* Search in all bytes in array. */
for (int offset = 0; offset <= 7; offset++) { /* Search in all offsets in byte. */
if ((bytes[index] & (1 << (7 - offset))) == 0) { /* Judge if bit is cleared. */
varCountFree++; /* Increment of free bit. */
}
}
}
} else {
fprintf(stderr, "Bitmap: buffer pointer is null.\n");
exit(EXIT_FAILURE); /* Fail due to null buffer pointer. */
}
} else {
fprintf(stderr, "Bitmap: count should be times of eight.\n");
exit(EXIT_FAILURE); /* Fail due to count alignment. */
}
}
其中,输入参数buffer
是用来存储具体位图信息的结构,bytes = (uint8_t *)buffer
是将 bytes
的地址强制转义成 buffer
首地址 。注意,bytes
是8比特结构,符字节之义。
初始化过程值得注意的是 for循环的内容,index
表示所在的字节数(即在第几个字节),offset
表示所在的比特数(即在第几个比特位)。
if ((bytes[index] & (1 << (7 - offset))) == 0)
这行代码是用来判断某一比特位是否为0,如果是就对 varCountFree
进行自增操作。比如,index
当前值为1,offset当前值为2,bytes[1] = 10
,那么
bytes[1]=10 & (1 << 5)
等价于 00001010 & 00100000
,按位与的结果当然是0,因此 varCountFree
应当自增。需要注意的是,对于每一个 bytes
,offset
都是从高位开始遍历的。
其他函数,诸如 get, set, clear, findFree
等也都可以如法炮制去理解,在此就不再赘述。
lock.cpp
这个代码文件是对照 lock.h 所实现的读写锁功能。首先,我们来简单看看构造函数,代码如下所示:
extern RPCServer *server; // 外部全局变量声明
LockService::LockService(uint64_t _MetaDataBaseAddress)
: MetaDataBaseAddress(_MetaDataBaseAddress){}
可以看出,该构造函数所做的事情仅仅是将元数据基地址赋值到 LockService
对象中,这便于后续锁地址的计算。
接下来,我们具体看看读写锁的上锁与解锁过程。
首先是写锁的上锁代码,如下所示:
/* 写上锁参数:节点号,锁地址偏移(相对元数据基地址)
* 基本流程:
* 1. 获取线程ID
* 2. 计算锁地址(=元数据基地址+锁地址偏移)
* 3. 计算key值
* 4. 比较LockAddress与旧值(0ULL)
* 5. 如果上一步返回false,则server执行requestPoller动作
*/
uint64_t LockService::WriteLock(uint16_t NodeID, uint64_t Address) {
int workerid = server->getIDbyTID();
uint16_t ID = __sync_fetch_and_add(&WriteID, 1ULL);
uint64_t key = (uint64_t)NodeID;
uint64_t LockAddress = MetaDataBaseAddress + Address;
key = key << 16;
key += ID;
key = key << 32;
while (true) {
if (__sync_bool_compare_and_swap((uint64_t *)LockAddress, 0ULL, key))
break;
//if (workerid == 0) {
server->RequestPoller(workerid);
//}
}
return key;
}
工作流程已在注释中给出,这一个函数里比较难理解的是这样两个函数:__sync_fetch_and_add
, __sync_bool_compare_and_swap
。
它们两个的实际功能其实很简单,就是一种编译器级别的原子操作,释义分别如下:
type __sync_fetch_and_add(type *ptr, type value, …); // m+n
bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, …);
/* 对应的伪代码 */
{ if (*ptr == oldval) { *ptr = newval; return true; } else { return false; } }
比较值得一提的是while循环体内的逻辑:如果LockAddress
等于旧值(0ULL,可以理解为初值),则将新的 key
值赋值给LockAddress
;否则服务器执行 RequestPoller
操作,查询工作区以接收信息,并准备要发送的信息。
key的计算看起来复杂,但实际上我们明白其根本是由 NodeID
(节点号) 和 WriteID
(线程号) 唯一指定即可。
看完了写上锁,我们接着看写解锁,读上锁,读解锁,代码如下所示。
bool LockService::WriteUnlock(uint64_t key, uint16_t NodeID, uint64_t Address) {
uint64_t LockAddress = MetaDataBaseAddress + Address;
uint32_t *p = (uint32_t *)(LockAddress + 4);
*p = 0</