Redis源码阅读
版本tag:1.3.6
2010年的版本了。
如有错误,感谢指正!
如有建议和意见,欢迎提出!
从main开始
第一次看源码,所以先从最简单的版本看起,找了很久,没有1.0.0版本的,就找了个看似是最早的版本了。
跟着main函数一点点看吧,看见感兴趣的函数就查一下,尽量查看官方文档,不依靠csdn和百度!
int main(int argc, char **argv) {
time_t start;
//0. 初始化Server配置参数,可能会被2覆盖
initServerConfig();
if (argc == 2) {
//1. 重置自动保存的参数,暂时先不管
resetServerSaveParams();
//2. 如果启动的时候传入了配置文件,则依照配置文件初始化Server的配置
loadServerConfig(argv[1]);
} else if (argc > 2) {
fprintf(stderr,"Usage: ./redis-server [/path/to/redis.conf]n");
exit(1);
} else {
redisLog(REDIS_WARNING,"Warning: no config file specified, using the default config. In order to specify a config file use 'redis-server /path/to/redis.conf'");
}
// 3. daemon化,变成一个后台进程,否则断开会话时,进程也会被终止
if (server.daemonize) daemonize();
// 4. 初始化服务
initServer();
redisLog(REDIS_NOTICE,"Server started, Redis version " REDIS_VERSION);
#ifdef __linux__
linuxOvercommitMemoryWarning();
#endif
start = time(NULL);
if (server.appendonly) {
if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK)
redisLog(REDIS_NOTICE,"DB loaded from append only file: %ld seconds",time(NULL)-start);
} else {
if (rdbLoad(server.dbfilename) == REDIS_OK)
redisLog(REDIS_NOTICE,"DB loaded from disk: %ld seconds",time(NULL)-start);
}
redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port);
aeSetBeforeSleepProc(server.el,beforeSleep);
// 进入事件循环
aeMain(server.el);
aeDeleteEventLoop(server.el);
return 0;
}
daemonize
daemon守护进程,像很多服务都可以看见这个意思,比如mysqld、dockerd等,就是一个后台进程,比如我们ssh到一台linux服务器,然后启动./redis-server,然后断开ssh连接,此时,redis-server进程会收到一个SIGHUP(hang up挂断)的信号,也会关闭。
而我们实际想要的结果是让这个server成为一个后台进程。这就是daemonize函数的作用
如何daemon化呢,可以参考一下,Redis的Daemonize没有:
- How to daemonize a process:代码层面如何实现以及一些关于Terminal、Process的背景知识
- How to keep redis server running:Redis如何跑在后台
代码实现涉及到的API
- setsid:简单来说就是创建一个新的session,调用进程成为了该session的leader(如果它不是一个leader),且这个session没有对应的控制终端(controling terminal)。
- 当然你也可以申请一个open /dev/tty,所以防止出现这种情况,上面提到的资料中还推荐再fork一次,这样fork出来的子进程就不是session leader,也不能申请控制终端了。
- 当然fork以后还可以再setid,所以break这个递归的前提就是不要瞎搞!
- ……
- 当然fork以后还可以再setid,所以break这个递归的前提就是不要瞎搞!
- 当然你也可以申请一个open /dev/tty,所以防止出现这种情况,上面提到的资料中还推荐再fork一次,这样fork出来的子进程就不是session leader,也不能申请控制终端了。
- dup2:
dup2
的意思就是dup
后面跟2个参数,用来复用文件描述符,代码中打开了一个特殊的文件/dev/null
,把STDIN
、STDOUT
、STDERR
都重定向到这个无底洞中。
static void daemonize(void) {
int fd;
FILE *fp;
// 退出父进程,使得子进程变成孤儿进程,最后变成init的子进程
if (fork() != 0) exit(0); /* parent exits */
// 创建一个新的session,没有controling terminal与之对应
setsid(); /* create a new session */
/* Every output goes to /dev/null. If Redis is daemonized but
* the 'logfile' is set to 'stdout' in the configuration file
* it will not log at all. */
// 所有的输出都重定向至无底洞/dev/null中
if ((fd = open("/dev/null", O_RDWR, 0)) != -1) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO) close(fd);
}
/* Try to write the pid file */
// 将pid写入到/var/run/redis.pid这个文件中
fp = fopen(server.pidfile,"w");
if (fp) {
fprintf(fp,"%dn",getpid());
fclose(fp);
}
}
initServer
此处的initServer
不一样了,主要是初始化一些运行时需要的容器(list)、事件循环eventLoop。
而之前的initServerConfig
和loadServerConfig
,是设置默认参数值和载入参数值。
初始化结构体redisServer里的一些成员变量,挑几个感兴趣的
list *objfreelist
:A list of freed objects to avoid malloc(),需要被回收的,可以拿来复用的obj,避免重复的malloc和free。aeEventLoop *el
:aeEventLoop
是事件循环的结构体,在initServer
里创建,然后通过aeMain(server.el);
开始事件循环。事件循环相关的代码都以ae开头,与操作系统也有关联,不同的操作系统使用的网络模型也不同,官方文档中提到的是epoll,但是我使用Windows+vscode看代码时,ctrl+左键点击相关函数,发现使用的是select。因为在ae.c这个文件中有如下宏定义
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
static void initServer() {
int j;
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
setupSigSegvAction();
server.devnull = fopen("/dev/null","w");
if (server.devnull == NULL) {
redisLog(REDIS_WARNING, "Can't open /dev/null: %s", server.neterr);
exit(1);
}
// 初始化结构体redisServer的成员变量
server.clients = listCreate(); // 连接的客户端列表
server.slaves = listCreate(); // slave列表
server.monitors = listCreate();// TODO:
server.objfreelist = listCreate(); // 需要被回收的,可以拿来复用的obj,避免重复的malloc
createSharedObjects();
server.el = aeCreateEventLoop(); // 创建事件循环
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
server.sharingpool = dictCreate(&setDictType,NULL);
server.fd = anetTcpServer(server.neterr, server.port, server.bindaddr);
if (server.fd == -1) {
redisLog(REDIS_WARNING, "Opening TCP port: %s", server.neterr);
exit(1);
}
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType,NULL);
server.db[j].expires = dictCreate(&keyptrDictType,NULL);
server.db[j].blockingkeys = dictCreate(&keylistDictType,NULL);
if (server.vm_enabled)
server.db[j].io_keys = dictCreate(&keylistDictType,NULL);
server.db[j].id = j;
}
server.cronloops = 0;
server.bgsavechildpid = -1;
server.bgrewritechildpid = -1;
server.bgrewritebuf = sdsempty();
server.lastsave = time(NULL);
server.dirty = 0;
server.stat_numcommands = 0;
server.stat_numconnections = 0;
server.stat_starttime = time(NULL);
server.unixtime = time(NULL);
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,
acceptHandler, NULL) == AE_ERR) oom("creating file event");
if (server.appendonly) {
server.appendfd = open(server.appendfilename,O_WRONLY|O_APPEND|O_CREAT,0644);
if (server.appendfd == -1) {
redisLog(REDIS_WARNING, "Can't open the append-only file: %s",
strerror(errno));
exit(1);
}
}
if (server.vm_enabled) vmInit();
}
zmalloc
redis源码封装了内存的动态分配函数,提供了一系列z开头的内存管理函数
zmalloc:每次分配堆空间时会额外在头部分配一个size_t(win64下是8个字节)的空间,以记录该指针对应的堆空间大小;同时,更新全局变量used_memory,记录下本次分配的空间
void *zmalloc(size_t size) {
// #define PREFIX_SIZE sizeof(size_t)
// 额外分配一个prefix,用于记录buff的大小
void *ptr = malloc(size+PREFIX_SIZE);
if (!ptr) zmalloc_oom(size);
#ifdef HAVE_MALLOC_SIZE // MacOS下才有
increment_used_memory(redis_malloc_size(ptr));
return ptr;
#else
// 在prefix中写入buff的字节数(不包括prefix)
*((size_t*)ptr) = size;
// 更新已分配的字节数(包括prefix)
increment_used_memory(size+PREFIX_SIZE);
// 最后返回的是实际buff的起始指针
return (char*)ptr+PREFIX_SIZE;
#endif
}
SDS
Hacking Strings
sds:sds
stands for Simple Dynamic Strings,但是实际类型是char *
typedef char *sds;
struct sdshdr {
long len;
long free;
char buf[]; // 注意!这里是char[],和C++里好像不太一样哦!
};
起初我比较奇怪,为什么用[],然后搜了一下这是一个C里的一个写法,叫做flexible array member
SO上关于这个写法的讨论:What are the real benefits of flexible array member?
struct h1 {
size_t len;
char* data;
};
struct h2 {
size_t len;
char data[];
};
size_t payloadLen = 1024;
// init h1
h1 *instance1 = (h1 *)malloc(sizeof(h1));
instance1->data = (char *)malloc(payloadLen);
instance1->len = payloadLen;
// free
free(instance1->data);
free(instance1);
// init h2
h2 *instance2 = (h2 *)malloc(sizeof(h1)+payloadLen);
instance2->len = payloadLen;
// free
free(instance2);
相比之下,h2的操作更加简洁。
有了上面的基础,再看下面这个sdsnewlen的代码,就稍微看得懂一些了。
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
// 申请内存
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
#ifdef SDS_ABORT_ON_OOM
if (sh == NULL) sdsOomAbort();
#else
if (sh == NULL) return NULL;
#endif
// 记录长度
sh->len = initlen;
sh->free = 0;
if (initlen) {
// 拷贝data
if (init) memcpy(sh->buf, init, initlen);
else memset(sh->buf,0,initlen);
}
// buff的末尾置'0'
sh->buf[initlen] = '0';
// 返回buff的指针,也就是实际字符串的起始位置
return (char*)sh->buf;
}
官方给出的对于sds的说明文档:Hacking Strings
这样的结构在获取字符串长度时,反推就能获取len,时间复杂度是O(1)
sdsnewlen("redis", 5);
/*
-----------
|5|0|redis|
-----------
^ ^
sh sh->buf
*/
size_t sdslen(const sds s) {
//根据buff的位置,反推sdshdr的指针,从而获取到len,就不需要strlen了!
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
return sh->len;
}
总结一下:
- zmalloc主要是为了记录当前程序分配的所有堆空间之和,所以对malloc进行了一次封装,添加上了一个prefix。
- sdshdr为了记录字符串buff的长度和空闲空间大小,所以加了Len和Free两个Long类型的头。
感觉有点像TCP/IP协议栈,就是一个对象,不同层次,封装了好多个头,不同层解决不同的问题。
参考资料
- Data Type Ranges
- Dynamic Strings in C