Chinaunix首页 | 论坛 | 博客
  • 博客访问: 153840
  • 博文数量: 110
  • 博客积分: 127
  • 博客等级: 入伍新兵
  • 技术积分: 839
  • 用 户 组: 普通用户
  • 注册时间: 2012-11-05 16:20
文章分类

全部博文(110)

分类: C/C++

2014-05-05 15:51:09

   写在前面:解读过程中使用的术语或变量名称,尽量和源码中的保持一致。这样,便于大家学习参考。若是某些名称和源码中的存在冲突,则以源码为准。该软件版本是:Memcached-1.4.5 

   软件执行时,首先设置一些默认值,如下:

  • Unix域套接字的默认访问权限掩码access为0770(八进制)
  • tcp和udp监听端口(port, udpport)默认是11211
  • 缓存中数据块的最大尺寸maxbytes为64MB
  • 为了限制与链接相关联的内存大小(5MB),那么默认的最大连接数maxconns为1024
  • 数据块之间的缩放因子factor是1.25
  • 每事件(event)的请求处理数(reqs_per_event)为20
  • 用于listen()的backlog默认值为1024
  • item大小的最大尺寸(item_size_max)为1MB
  • 用于存放key和value的空间大小(chunk_size)为48B
  • ... ...

1. 软件入口参数

    Memcached运行时,接受如下所示的参数,其含义如下:

  • -a mask: 用于设置Unix域套接字访问权限的掩码
  • -p port: TCP连接的监听端口
  • -s path: Unix域套接字的监听路径
  • -U port: UDP服务器的监听端口
  • -m max_mem: 系统内部每个item的最大内存大小(单位:MB)
  • -c conns: 最大并发连接数
  • -k: 锁定所有内存页
  • -h: 获得帮助
  • -i: 获得licence信息
  • -r: 最大化core文件的最大大小限制
  • -v: 跟踪执行过程
  • -d: 守护进程模式
  • -l ip_addr: 用于监听的接口(默认为INADDR_ANY)
  • -u user_name: 用于执行Memcached的用户名
  • -P pidfile: 用于保存pid的文件名
  • -f factor: 缩放因子
  • -n min_space: 为'key+value+flags'申请的最小空间
  • -t threads: 工作线程的数量,默认为4
  • -D prefix: 前缀分界符,默认为':'
  • -L: 大内存页
  • -R reqs_per_event: 每事件最大请求处理数
  • -C: 禁用CAS命令
  • -b backlog: backlog for listen
  • -B proto: 绑定的协议
  • -I max_item_size: items的最大大小 e.g. 50M 12K etc.
  • -S: 开启Sas1认证

   这里需要注意的是,开启的线程总数量不应超过主机的CPU核数,item的最大尺寸不应小于1024字节。另外,-B选项可以指定的协议类型如下:

  • auto: 系统内部自动识别协议类型
  • binary: 二进制格式
  • ascii: 文本格式

   如果开启了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结构体,具体定义见头文件中。这一步对使用libevent库函数来说是相当重要的。

   接下来初始化其他成员,如struct stats, struct _stritem **primary_hashtable, struct conn **freeconns, struct slabclass slabclass[MAX_NUMBER_OF_SLAB_CLASSES]等重要数据结构中的属性值。下面我们逐一对这些重要数据结构的初始化进行介绍,不要小瞧这些过程,他们是非常重要的,否则等到解读后面使用这些结构时,就会由于不知道他们是如何组织的,而把思路乱作一团。

  • struct stats stats初始化

   struct stats结构定义在头文件中。stats主要是用来记录并统计系统内部数据参数的,并用于向外界展示的作用。对stats的初始化是在stats_init()函数中进行的。stats结构体中出了.accepting_conns=true;外,其余的成员均被置成了零。其中accepting_conns=true表示开始接受连接,这也是每个初始任务开始阶段所处于的状态。在stats_init()函数内部还着重初始化了另外两个变量:process_started和prefix_stats。process_started这个参数初始化很有意思,其初始值是time(0)-2;这样可以使time(0)-time.started的值永远不会为零。

  • struct _stritem **primary_hashtable初始化

   该处初始化的数据结构是整个系统的关键说在,需要详细的解读一下。对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字节的空间来维护一个初始的哈西数组。

  • struct conn **freeconns初始化

   该结构也是系统中一个给常重要的数据结构,其定义在中。由于系统内部需要维护每个连接的相关信息,并且memcached本身也是使用在高并发的环境中的,自然地就会在短时间内产生大量的连接,那么此时就需要一个数据结构来维护该连接,freeconns就运用而生了。freeconns可以理解为系统内部的连接池,处在这里的连接结构都是空闲的。可供系统在高并发连接环境下,在O(1)时间内获取一个struct conn结构,从而可以快速的跟踪活动连接的活动过程。

    对该结构的初始化是在函数conn_init()中进行的。freeconns的初始值是一个拥有200个struct conn*的指针存储空间。

  • struct slabclass slabclass[MAX_NUMBER_OF_SLAB_CLASSES]初始化

   该结构是系统中管理内存的重要数据结构,其定义在源文件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结构原型很有意思,其定义在中。其中一个末尾的成员void *end[];很有意思。这是什么意思呢?看了下文你就知道了^_^

     好,接下来我们看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套接口,他们都处于非阻塞状态。

   注意这里有两个不同的地方:

  1. UDP通过调用dispatch_conn_new()函数向管道中发送一个字节的""到各个线程中,从而实现剥离新连接到另外一个线程的目的。另外,该函数的调用仅仅发生在主线程中,或者是在UDP监听套接口的初始化阶段或者是有一个新的TCP连接到达的时候。
  2. 对于关联到每一个UDP监听端口的连接管理数据结构是CQ_ITEM,并通过cp_push()将CP_ITEM挂接到LIBEVENT_THREAD.new_conn_queue队列上。而关联到每一个TCP监听到套接口的连接管理数据结构是struct conn,并通过conn_new()获得,随后直接将其挂接到listen_conn全局链表头上。
  3. TCP会把sfd注册到libevent中,同时执行事件发生时应该执行的回调函数为event_handler()。该函数是用来接受新连接的处理函数。而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从启动到结束的全部执行流程,但是对其中的关键技术部分只是做了简单的介绍,欲知其详细内幕,请关注后续的连载文章。

祝好!

阅读(1368) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~