Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读。由于我用c也是好几年以前了,些许错误在所难免,希望读者能不吝指出。
曹工说Redis源码(1)-- redis debug环境搭建,使用clion,达到和调试java一样的效果
一些补充知识 项目结构及入口除了大学那些玩具,一个真正的项目,都是由大量源代码文件组成一个工程。在Java里,一个 java 文件要使用其他 java 文件中的函数、类型、变量等,都需要使用import语句来引入。在c语言里,也是一样的,在c语言中,要引入其他文件的功能,需要使用include语句。
比如,在redis的主入口,redis.c文件中,就包含了如下一堆语句:
#include "redis.h" #include "cluster.h" #include "slowlog.h" #include "bio.h" #include <time.h> #include <signal.h>其中,以<开头的,比如<time.h>是标准库的头文件,会在系统指定的路径下查找,可类比为jdk官方的class;"bio.h"这种,以""包裹的,则是工程里自定义的。
比如,time.h,我在linux的以下路径查找到了:
[root@mini1 src]# locate time.h /usr/include/time.h其他include相关知识,可以参考:
https://www.runoob.com/cprogramming/c-header-files.html
我对头文件的理解一般来说,我们会在.c文件中,去编写我们的业务逻辑方法,其中,一些方法,可能是只在本文件内部用到的,类似于java class的private方法;一些方法呢,可能是需要在外部的其他源码文件中,也需要用到的,这些方法,要怎么才能让外部可以使用呢?
就是通过头文件机制,可以理解为各大高级语言中的接口,在java中,定义一个class,虽然可以直接把方法设为public,其他类可以直接访问;但是,在平时的业务开发中,我们一般并不会直接访问一个实现类,而是通过它实现的接口去访问;一个好的实现类,也不应该把没在接口中定义的方法,设为public权限。
说回头文件,比如有个源码文件test.c 如下:
long long ustime(void) { struct timeval tv; long long ust; gettimeofday(&tv, NULL); ust = ((long long)tv.tv_sec)*1000000; ust += tv.tv_usec; return ust; } /* Return the UNIX time in milliseconds */ // 返回毫秒格式的 UNIX 时间 // 1 秒 = 1 000 毫秒 long long mstime(void) { return ustime()/1000; }这个文件里,定义了2个方法,但假设我们只需要对外暴露mstime(void)方法,那么,头文件test.h应该是下面这样的:
long long mstime(void);这样的话,我们的另一个方法,ustime,对外就不可见了。
总之,大家可以把头文件理解为实现类要对外暴露的接口;大家可能觉得我的比喻不恰当,为啥把c文件,说成实现类,实际上,我们之前在华为的时候,确实是用c++的思想,面向对象的思想,来写c语言的。
我看到网上一篇文章,这里引用一下(https://zhuanlan.zhihu.com/p/57882822):
反观Redis,他是纯C编码,但是融入了面向对象的思想。和上述观点截然相反,可谓是『用C++去设计,用C编码』。当然本文目的并非挑起语言之争,各种语言自有其利弊,开源项目的语言选择也主要是由于项目作者的个人经历和主观意愿。
但是c语言中的头文件,和java这些语言中的接口,还是不同的;在java中,接口和实现类一样,最终都是编译为独立的class文件。
在c语言中,在编译实现类之前,会有一个预处理的过程,预处理的过程,就是把include语句,直接替换为被include的头文件的内容,比如,以菜鸟教程中的例子举例:
header.h char *test (void);在如下的 program.c中,需要使用上面的header.h中的test方法,则需要include:
int x; #include "header.h" int main (void) { puts (test ()); }经过预处理后,(就是进行简单的replace),效果如下:
int x; char *test (void); int main (void) { puts (test ()); }我们可以使用如下命令,来演示这个过程:
[root@mini1 test]# gcc -E program.c int x; # 1 "header.h" 1 char *test (void); # 3 "program.c" 2 int main (void) { puts (test ()); }从上面可以看到,已经replace进去了;如果我们include两次,会怎样?
[root@mini1 test]# gcc -E program.c int x; # 1 "header.h" 1 char *test (void); # 3 "program.c" 2 # 1 "header.h" 1 char *test (void); # 4 "program.c" 2 int main (void) { puts (test ()); }可以发现,这个header的内容,出现了2次,重复了。但是上面这种情况,并不会报错,无非是方法被定义了两次。
为什么头文件里都要来一句ifndef大家看头文件,都会发现如下语句,比如在redis.h中:
#ifndef __REDIS_H #define __REDIS_H #include "fmacros.h" #include "config.h" ... typedef struct redisObject { // 类型 unsigned type:4; // 编码 unsigned encoding:4; // 对象最后一次被访问的时间 unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ // 引用计数 int refcount; // 指向实际值的指针 void *ptr; } robj; ... #endif可以看到,最开始,有一句:
#ifndef __REDIS_H #define __REDIS_H结尾有一句:
#endif这个就是为了解决如下问题: