分类: C/C++
2014-05-05 15:51:09
原文地址:Memcached软件源码级执行流程解读 作者:CUKdd
写在前面:解读过程中使用的术语或变量名称,尽量和源码中的保持一致。这样,便于大家学习参考。若是某些名称和源码中的存在冲突,则以源码为准。该软件版本是:Memcached-1.4.5
软件执行时,首先设置一些默认值,如下:
1. 软件入口参数
Memcached运行时,接受如下所示的参数,其含义如下:
这里需要注意的是,开启的线程总数量不应超过主机的CPU核数,item的最大尺寸不应小于1024字节。另外,-B选项可以指定的协议类型如下:
如果开启了Sas1认证,那么binding_protocol必须是"binary_prot"。
2. 系统内部选项参数的设置
通过前一步参数的读取,那么程序就需要根据这些参数进行相应的系统参数调整。
对于监听端口的设置,除非两个都有明确的指定值,那么就会被设置成相同的值。
如果软件运行参数中有-r选项的话,那么需要调用带有{.rlim_cur = .rlim_max = RLIM_INFINITY}参数的setrlimit()函数(此时第一个参数应该是RLIMIT_CORE),以取消系统对core文件的限制。若是这个调用失败了,那么就把.rlim_cur和.rlim_max都设置成系统中原有的.rlim_max值。最后,还要调用一下getrlimit(),查看.rlim_cur是否非零。若非零,则一切正常;否则那将不会产生core文件了。这种情况在Memcached中是不被允许的。
如果需要的话,需要根据maxconns的值,调用setrlimit(RLIMIT_NOFILE)改变系统允许进程打开的最大文件描述符数。注意,getrlimit和setrlimit函数若是调用成功的话,返回值将是0,否则是-1。具体的出错原因保存在errno里。
接下来判断程序的执行者是谁。方法如下,通过调用getuid()或geteuid()来得到执行者的用户ID或是有效用户ID,若他们中有一个为0,就可以判断当前执行者为root。那么,而后就可以通过调用getpwnam(username)来获取password文件中username所指定用户的信息。getpwnam()将返回如下的一个结构:
struct passwd {
char *pw_name; /* user name */
char *pw_passwd; /* user password */
uid_t pw_uid; /* user ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* real name */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};
通过该结构就可以利用setuid(pw_uid)和setgid(pw_gid)来设置程序的用户ID和组ID了,那么现在若是在通过ps查看该进程的信息的话,就可以发现程序的执行用户为username。
若是-S选项被传递到软件中的话,那么这时就需要通过init_sas1()对Sas1认证进行初始化了。这里不展开讲解了,详细分析见其他篇章。
再继续往下分析,若是入口参数选项中有-d,那么接下来软件将进入守护进程模式,否则仍在前台执行。由于该缓存系统需要占用大量的内存,因此我们可以通过参数-k来调用mlockall(MCL_CURRENT | MCL_FUTURE)将进程中全部或部分虚拟地址空间锁定到RAM,而避免其被唤出到swap区域。这样可以减少程序运行时不必要的内存也调度,从而也可以加快程序的运行。
好了,到了这里,入口参数的设置也基本完成了。接下来就是对libevent库的初始化工作,及其他的一些设置。
3. 系统内部基本数据结构的初始化
主线程为了维护整个系统的正常运行,这里需要通过调用libevent库中的event_init()函数返回一个main_base结构,它是struct event_base结构体,具体定义见
接下来初始化其他成员,如struct stats, struct _stritem **primary_hashtable, struct conn **freeconns, struct slabclass slabclass[MAX_NUMBER_OF_SLAB_CLASSES]等重要数据结构中的属性值。下面我们逐一对这些重要数据结构的初始化进行介绍,不要小瞧这些过程,他们是非常重要的,否则等到解读后面使用这些结构时,就会由于不知道他们是如何组织的,而把思路乱作一团。
struct stats结构定义在
该处初始化的数据结构是整个系统的关键说在,需要详细的解读一下。对primary_hashtable的初始化是在assoc_init()函数中进行的,其结构定义在
primary_hashtable=calloc(hashsize(hashpower), sizeof(void *));
其中,hashpower的值为16。而hashsize()是一个宏函数,定义如下:
#define hashsize(n) ((ub4)1<<(n))
那么,这里可以得到hashsize(hashpower)等于65536。于是,primary_hashtable就具有了65536个sizeof(void *)的哈西空间,也就是使用了256KB字节的空间来维护一个初始的哈西数组。
该结构也是系统中一个给常重要的数据结构,其定义在
对该结构的初始化是在函数conn_init()中进行的。freeconns的初始值是一个拥有200个struct conn*的指针存储空间。
该结构是系统中管理内存的重要数据结构,其定义在源文件slabs.c中。对该结构的初始化是在函数slabs_init()中进行的。函数原型如下:
void slabs_init(const size_t limit, const double factor, const bool prealloc);
其中,limit就是入口参数选项-m指定的值,即settings.maxbytes,默认值为64MB. factor参数是相邻内存块之间的缩放因子,起作用见下文。prealloc参数指示limit大小的内存是否需要预分配。
slab的初始化是个复杂而又精细的过程,这里我们对其将进行详细的讲解。
首先,若是prealloc为true的话,那么这里就需要预分配内存空间,并将其存入mem_current变量中,而其可用大小由mem_avail变量指示。
我们看看size的值是如何得到的。它通过语句size=sizeof(item)+settings.chunk_size;获得。这里的item结构原型很有意思,其定义在
好,接下来我们看slabclass的初始化过程。首先我们从上文中可以看到slabclass是一个具有MAX_NUMBER_OF_SLAB_CLASSES个struct slabclass结构的数组。其中,MAX_NUMBER_OF_SLAB_CLASSES的值为POWER_LARGEST+1(=201)。对其中的每一项进行初始化时,都需要先把size按CHUNK_ALIGN_BYTES(=8B)对齐。其中,slabclass[i].size = size; slabclass[i].perslab=settings.item_size_max/size;这两项指明,对于每一个slabclass[]中的chunk集合来讲,每个chunk块大小为size字节,数量为perslab个。每轮初始化完成后,size的大小都要增大factor倍,那么相应地对每个slab中的chunk块数量就会减少factor倍。因此,此时得到的slabclass中的每一个slab都具有从前往后chunk尺寸在增大,数量在减少的变化规律。而且对于第i个slab,其chunk的大小可以通过slabclass[0].size*factor^i而轻松得到。
最后,为了能把剩余的最后一个slab(其大小可能不具有上述关系)也存入slabclass中,这里就直接特意将slabclass[power_largest].size设置成了setting.item_size_max, .perslab=1;其中,power_largest为slab的数量减一,即最后一个slab的位置。
接下来,开始预分配真实的slab空间。在这之前,需要通过getenv()获得T_MEMD_SLABS_ALLOC的值,即已经malloc的内存大小,并将其存于mem_malloced变量中。而后试图探测环境变量T_MEMD_SLABS_ALLOC是否存在,若否则调用slabs_preallocate(power_largest)函数分配实际的slab空间;若存在但是其值为非零的话,那么同样也要调用slabs_preallocate(power_largest)函数分配实际的slab空间。
对slab内存空间的预分配是通过slabs_preallocate()->do_slabs_newslab()的调用路径而到达do_slabs_newslab()函数的。在这里,将为每一个slabclass[i]分配内存空间。由于该函数在系统内部任何时候发生内存短缺时都会被调用,所以在开始时需要检查该slab中是否是已经分配空间了,若是宾并且已经分配的内存超过了settings.maxbytes(这里是mem_limit),那么此次内存空间的分配操作将会失败。否则,通过函数grow_slab_list()检查当前slab是否需要扩充内存。
grow_slab_list()函数的作用是重新布局内存池的总大小。在该函数中,通过检查已经被使用的内存块(由slabclass.slabs指明)是否已经到达了当前内存池中的数量上限(由slabclass.list_size指明),若是,则通过realloc()函数将内存空间重新调整到原先的2倍。从源码中可以看到,对于第一次初始化时,内存池中内存块的数量是16,那么此后的任何时候此内存池中的内存块数量都将是16的2^i倍个。
通过grow_slab_list()函数重新布局好slab池的大小后,我们返回到do_slabs_newslab()函数。那么,这里就可以开始分配slab了。操作函数是memory_allocate()。
在函数memory_allocate()函数中,首先查看是否预分配了大的内存块(在slabs_init()函数刚开始的地方)。若否,那么我们调用malloc()函数获得size大小的slab空间;否则我们在预分配的空间(由mem_current指定)上获得size大小的slab空间。这里需要注意的是:在预分配空间上分配slab时,仍需要将size按CHUNK_ALIGN_BYTES(=8)进行对齐。
接下来我们继续返回到do_slabs_newslab()函数。这时ptr就保存了memory_allocate()函数返回的内存空间首地址。此后应该将ptr设置到slabclass中,通过slabclass.end_page_ptr=ptr,slabclass.end_page_free=slabclass.perslab进行设置。其中.end_page_ptr用于指示下一个空闲的item,也就是刚才申请的slab空间。
最后,再将ptr在slabclass.slab_list[]的slabclass.slabs个位置处存档,并将slabclass.slabs++就完成了此次slabclass[i]的内存空间分配的工作了。
下图是Memcached中slab结构的格式布局图示。
4. worker线程初始化
worker线程的初始化工作主要是在thread_init()函数中进行的。其原型如下:
void thread_init(int nthreads, struct event_base *main_base);
该函数主要是用来初始化线程子系统的,它可以创建各种工作线程。其中,nthreads是指要产生的事件处理句柄工作线程的数量;main_base是指主线程的event base。
在该函数中,通过调用setup_thread()函数注册将系统内部各线程之间的通信时处理函数。这里的处理函数是整个系统中及其关键的部分,这里就不展开讲了。欲知详解,请看Memcached的多线程技术。
而后,通过调用create_worker()函数将worker_libevent线程逐一启动,那么这时就完成了worker线程的初始化工作了。
5. 启动维护线程
对primary_hashtable起维护工作的线程,是通过执行assoc_maintenance_thread()函数而生成的线程。它是在start_assoc_maintenance_thread()函数中被启动的。
6. 注册memcached系统时钟处理函数
系统时钟处理函数clock_handler()的主要作用就是通过向libevent注册一个定时器事件,然后定期的更新系统中的current_time。
向libevent注册一个定时器事件,可以通过顺序调用evtimer_set(),event_base_set(),evtimer_add()三个函数来完成。这三个函数的原型如下:
#define evtimer_set(ev, callback, arg) event_set((ev), -1, 0, (callback), (arg))
void event_set(struct event *ev, evutil_socket_t fd, short events, void (*callback)(evuail_socket_t, short, void *), void *arg);
int event_base_set(struct event_base *base, struct event *ev);
#define evtimer_add(ev, tv) event_add((ev), (tv))
int event_add(struct *ev, const struct timeval *tv);
该处理函数每当OS时钟经历的1秒中的时间,那么clock_handler()函数就会执行一次。每次执行都要先检查该定时器是否还存在,若是则将其删除。而后再将该处理函数注册到libevent中,如此往复。
7. 创建Unix域套接字
如果入口参数中设置了settings.socketpath了的话,那么此时就会调用server_socket_unix()函数创建Unix域套接字。
在该函数中,首先会调用new_socket_unix()函数利用socket(AF_UNIX, SOCK_STREAM, 0)创建一个套接字sfd, 然后再利用fcntl(sfd, F_GETFL, 0)获得文件锁标志flags,最后利用fcntl(sfd, F_SETFL, flags|O_NONBLOCK)将该套接口设置成非阻塞状态。而后我们就得到了一个非阻塞的Unix域套接字sfd。
在绑定该套接字之前,需要先检查待绑定的文件是否已经存在了,若是则需要将其删除(调用unlink())。
接下来假设有一个struct sockaddr_un类型的变量addr,那么我们将需要进行如下方式的设置才能完成绑定和监听操作。
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, path, sizeof(addr.sun_path));
bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
listen(sfd, setting.backlog);
而后,我们把sfd注册到libevent中,同时执行事件发生时应该执行的回调函数为event_handler()。该函数是用来接受新连接的处理函数。对该函数的详细解读请看后续连载文章。
到了这里,我们就完成了Unix域监听套接字的安装过程了。
8. 安装TCP/UDP监听套接口
这里将分别通过调用server_socket(SOCK_STREAM)和server_socket(SOCK_DGRAM)完成TCP和UDP监听套接口的创建工作。
创建过程中将通过getaddrinfo()获得host上所有可用的接口地址,并一一绑定。无论是TCP还是UDP套接口,他们都处于非阻塞状态。
注意这里有两个不同的地方:
9. 主线程进入调度管理模式
通过以上各个步骤的工作,主线程即将可以进入调度管理模式了。
该过程的完成主要是通过调用libevent库函数event_base_loop()来完成的。
这里,我们汇总一下worker, maintenance, dispatcher thread最终执行的函数:
worker: thread_libevent_process()
maintenance: assoc_maintenance_thread()
dispatcher: event_base_loop()
10. 软件退出
当系统内收到退出信号时,主线程将从event_base_loop()函数中返回,通过调用stop_assoc_maintenance_thread(),利用pthread_cond_signal(&maintenance_cond)将assoc_maintenance_thread线程唤醒,并等待该线程成功退出后而结束整个软件的运行。
以上详细讲述了Memcached从启动到结束的全部执行流程,但是对其中的关键技术部分只是做了简单的介绍,欲知其详细内幕,请关注后续的连载文章。
祝好!