最近,我想通过redis的源码来学习redis。虽然平时工作中用得不多,不过对redis还是比较感兴趣的,毕竟它的性能是不错的。redis是一个开源的项目,我们可以通过源代码去了解redis。我后面会通过自己的学习,写一些关于redis源码的帖子。帖子的主要内容是分析代码设计,而并不会对源码进行详细解说。如果有不对的地方,请指正。源码是reids 3.0.3版本。
adlist
一、adlist,双链列表
数据结构定义如下:
//节点 typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode; //迭代器 typedef struct listIter { listNode *next; int direction;//迭代器方向 } listIter; //链表 typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr);//复制函数 void (*free)(void *ptr);//销毁函数 int (*match)(void *ptr, void *key);//匹配函数 unsigned long len; } list;
跟一般的双向链表差别不大。redis的双向链表是设计成一个比较通用的链表,所以value是指针,不限value的实际类型,另外给链表提供了用于复制、销毁、匹配的函数,使得链表的value可支持不同的内存管理方法,或者进行一些回调。
另外,还提供了迭代器,链表的迭代器通常都不是随机迭代器,只能一个元素一个元素地跳。由于这是双向链表,所以迭代器可以向前跳,也可以向后跳,不过redis只提供了向后跳的函数。迭代器也支持正向、反向,具体需要通过direction字段来指定方向。结合direction也可以实现迭代器向前跳,大概是这样adlist便没有提供向前跳的函数吧。
二、adlist的相关操作函数
主要有两类,一类是宏,一类是函数,看代码都比较简单,这里不对代码进行详解。只是简单列举一下函数。
/* Prototypes */ list *listCreate(void);//创建链表 void listRelease(list *list);//销毁链表 list *listAddNodeHead(list *list, void *value);//往表头插入元素 list *listAddNodeTail(list *list, void *value);//往表尾插入元素 list *listInsertNode(list *list, listNode *old_node, void *value, int after); //插入到指定位置 void listDelNode(list *list, listNode *node);//删除节点 listIter *listGetIterator(list *list, int direction);//迭代器 listNode *listNext(listIter *iter);//迭代器向后跳,下一个 void listReleaseIterator(listIter *iter);//销毁迭代器 list *listDup(list *orig);//复制链表 listNode *listSearchKey(list *list, void *key);//查询 listNode *listIndex(list *list, long index);//获取指定位置的节点 void listRewind(list *list, listIter *li);//迭代器返回表头,正向迭代器 void listRewindTail(list *list, listIter *li);//迭代器到表尾,反向迭代器 void listRotate(list *list);//旋转链表,把表尾元素放到表头
三、adlist提供对象的内存管理
这里讲一下adlist如何通过dup,free进行内存管理。
有三个地方需要注意的:
a. 插入元素时,node中的value直接赋值为入参的value,不会调用dup函数。
b. listDup中,如果dup函数存在,调用dup函数去复制value。
c. listDelNode和listRelease中,如果free函数存在,通过free方法去销毁value。
通过上面三点,可以实现一些简单的内存管理方法,主要是对象内存的申请和销毁方面的管理。下面介绍一下简单的用法。
1) adlist不负责申请和销毁
如下面代码(代码没有通过编译测试,可能有误)
char s[] = “hello world”; list *lst = listCreate(); //list的free,dup函数为空 listAddNodeTail(lst, &s[0]); listAddNodeTail(lst, &s[1]); listAddNodeTail(lst, &s[2]); listAddNodeTail(lst, &s[3]); //do some thing with lst list *lst2 = listDup(lst); //do some thing with lst2 listRelease(lst); listRelease(lst2);
上面例子中,adlist不负责value内存空间的申请和销毁,完全由调用方管理。这时要求value所指定的元素的生存周期要比adlist的长,否则adlist中的value指定可能会失效。
2) adlist负责动态申请和销毁
如下面代码,(代码没有通过编译测试,可能有误):
void addStringToListHead(list *lst, char *s) { char *snew = (char*)malloc(strlen(s)+1); assert(snew); strcpy(snew,s); listAddNodeHead(list,snew); } void *stringDup(void *s) { char *sorg = (char*)s; char *snew = (char*)malloc(strlen(sorg)+1); assert(snew); strcpy(snew,sorg); return snew; } void *stringFree(void *s) { if (s) free(s); } int stringMatch(void *s1, void *s2) { if (s1 && s2) return strcmp((char*)s1,(char*)s2) == 0 ? 1 : 0; if (!s1 && !s2) return 1; return 0; } list *lst = listCreate(); listSetDupMethod(lst, stringDup); listSetFreeMethod(lst, stringFree); listSetMatchMethod(lst, stringMatch); addStringToListHead(lst,”hello”); addStringToListHead(lst,”world”); // do something list *lst2 = listDup(lst); // do something listRelease(lst); listRelease(lst2);
上面例子中动态生成的空间插入到adlist后,完全由adlist来负责管理,调用方不应该再长期持有或者删除动态空间的指针,否则会造成管理混乱。需要注意的是,adlist的dup函数需在复制时进行深度复制,否则一个动态申请的空间同时被两个指针长期保存,只要其中一个指针被free后,另一个指针就失效,再次访问或者free失效指针都将出错。
3) 未能友好地支持reference counting
未能友好支持reference counting的原因在于插入元素时,没有调用相关函数以增加引用计数。在复制list时通过dup来实现引用计数加1,在删除节点时通过free进行引用计数减1。但是插入元素时由于没有调用dup或是进行其他函数使引用计数加1,value便持有了指针,这使adlist不能友好地支持reference counting。不过这是可以通过其他方式实现支持的,如在插入前进行引用计数器加1,如果插入失败则引用计数减1。
四、容易产生内存碎片。
先不考虑value,只看node。可以看到,adlist进行插入元素或是复制链表时都需要申请节点空间,删除节点时需要释放空间。对于不断地进行插入和删除,如果所有的zmalloc、zfree函数没能作出很好的内存管理策略,很容易造成内容碎片。
五、总结
除了在内存管理上提供了调用方自定义的方法外,adlist跟一般的双向链表没有太大的区别,其特点就如上面所述,这里不再介绍了。