例如这样的地方
server.clients = listCreate();
listCreate内部有内存申请,但无须判断client是否为NULL,后面如果用到clients的地方访问到了null,会有内存访问错,触发SIGSEGV信号,详见 http://blog.ddup.us/?p=89 2. 提供宏来访问结构对象,保持执行效率和代码内聚性的平衡
typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr); void (*free)(void *ptr); int (*match)(void *ptr, void *key); unsigned int len; } list; /* Functions implemented as macros */ #define listLength(l) ((l)->len) #define listFirst(l) ((l)->head) #define listLast(l) ((l)->tail)
3.哈希表的实现
http://huangz.iteye.com/blog/1455808
这里有比较深入浅出的代码说明,redis的实现特色在于平摊式的rehash,避免一次操作过多数据。
值得一提的是,这篇文章里面说
唯一的遗憾是,dictht 作为字典的内部实现,本该被隐藏起来的,现在却完全暴露了出来
其实看第2条,暴露内部实现同时通过宏来访问数据,可以提高效率,因为函数调用是有开销的。
“隐藏内部实现“是面向对象的教条,只是怕多人合作的时候有人不知道某些数据不能访问。
只要写代码的人约定通过函数或者宏来访问对象,而非直接操作对象的数据,那么封装原则就是满足的。
这再次证明了java的优点在于“限制不靠谱的人写不靠谱的代码”。
另,关于hash表的大小
typedef struct dictht { dictEntry **table; // 节点指针数组 unsigned long size; // 桶的数量 unsigned long sizemask; // mask 码,用于地址索引计算 unsigned long used; // 已有节点数量 } dictht;
其中size是桶的数量,redis的实现把这个数量定为2^n,也就是说每次hash表扩展的时候,桶的数量定为4,8,16,32这样的递增序列。为什么要这么定呢?
继续看代码
n.sizemask = realsize-1; h = dictHashKey(d, de->key) & d->ht[1].sizemask;
也就是说,sizemask恒为size-1,size为2^n,则sizemask的2进制表示为11111..(n个1)。
一般来说,计算一个节点的hash值后,将这个 (hash值 % 指针数组的长度) 得到应该把这个节点放到数组的第几个元素为头指针的链表上。而redis的聪明在于避免%的操作,用快速的&操作来代替,因为
X & (2^n -1) == X % 2^n
X/2相当于将这个二进制数的X右移n位,而X%2相当于取X最低的N位的值,例如
9 / 4 = 01001 >> 2 = 010 = 2 9 % 4 = 01001 & 011 = 01 = 1
其中 01000是4可以整除的部分,01是余数部分。这样就能够快速求值,避免%的操作。
继续说,hash表的各种操作都要兼顾内部有两个表,同时还要保持功能的正交,迭代器的实现,只有在单线程的情况下能写出这样的hash表。
4.代码规范
私有函数以下划线开头
变量和函数命名为驼峰法(其实我不喜欢这个)
先include公共头文件,后include私有头文件
5.lzf压缩算法
pass,对算法一窍不通,也许以后能用一下。
6.ae.c这是个专门的话题,看到接口,我就有问题:
/* State of an event based program */ typedef struct aeEventLoop { int maxfd; long long timeEventNextId; aeFileEvent events[AE_SETSIZE]; /* Registered events */ aeFiredEvent fired[AE_SETSIZE]; /* Fired events */ aeTimeEvent *timeEventHead; int stop; void *apidata; /* This is used for polling API specific data */ aeBeforeSleepProc *beforesleep; } aeEventLoop; /* Prototypes */ aeEventLoop *aeCreateEventLoop(void); /* 创建,毫无疑问的 */ void aeDeleteEventLoop(aeEventLoop *eventLoop); /* 销毁,也毫无疑问 */ void aeStop(aeEventLoop *eventLoop); /* 1.停止是什么意思? */ int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData); /* 注册一个文件事件,因该是网络套接字的fd,2.对普通文件效果如何? */ void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask); /* 删除事件,3. mask是什么用的? */ int aeGetFileEvents(aeEventLoop *eventLoop, int fd); /* 4. get并不返回,有什么用? */ long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds, aeTimeProc *proc, void *clientData, aeEventFinalizerProc *finalizerProc); /* 5.final最终是干什么的? */ int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id); /*删掉某个id的时间事件,无问题 */ int aeProcessEvents(aeEventLoop *eventLoop, int flags); /*6.语义不明*/ int aeWait(int fd, int mask, long long milliseconds); /* 7.不明 */ void aeMain(aeEventLoop *eventLoop); /* 8.应该是启动事件主循环*/ char *aeGetApiName(void); /* 9. 不明 */ void aeSetBeforeSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *beforesleep); /*设置一个sleep之前的处理函数,10.程序怎么知道自己要sleep? */
在浏览完ae.c后,我来回答这些问题:
6.1 简单的把eventLoop->stop = 1;在aeMain里面做判断,用于终止事件循环
while (!eventLoop->stop) { if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); aeProcessEvents(eventLoop, AE_ALL_EVENTS); }
6.2 eventLoop->events[fd]是用来存储某个事件的,并没有标志位的功能,epoll(假定用epoll),返回一个事件的数组,所以无须担心还需要遍历的问题。普通文件也能用,这样就引申出一个问题,6.11如何区分客户端命令的边界?
6.3 maxfd是用在process里面的,如果maxfd为-1,说明目前没有注册的描述符,就不用处理了
if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
mask是用来减去的,并不一点是清0,可能是减掉。
6.4 返回的是相应fd的mask
6.5 用于在aeDeleteTimeEvent的时候调用,不过不知道实际例子是什么。
6.6 wait完了之后,处理已经产生的文件事件,或者时间事件。这个函数首先遍历时间事件的链表,找到最近需要处理的时间事件,然后把未来时间-当前时间作为wait的参数(等待n秒),wait完毕后执行读或者写的事件。同时遍历TimeEvent的链表,寻找当前已经过时的TimeEvent来执行。
6.7 select 的简单包装,用于syncio.c,在指定时间内同步读写相关函数。
6.8 启动事件主循环函数,唯一的问题是eventLoop->beforesleep(eventLoop);不知道在实际中怎么用6.12
6.9 返回api的名字,epoll,select或者kqueue,给客户端的INFO命令显示用
6.10 程序用epoll等方法来等待有事件产生。
6.11
6.12
值得一体的是,ae_epoll.c里面用的是水平触发(LT),不是边缘触发(ET),效率稍低一点。不知道有什么意义?