免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: lenky0401
打印 上一主题 下一主题

[C] nginx核心讲解 [复制链接]

论坛徽章:
0
31 [报告]
发表于 2012-08-08 14:47 |只看该作者
由于主进程也拥有那些监听sockets,所以的确它也能收到客户端的请求,但是主进程并不会去响应这些请求,也就是说它没有监视这些监听sockets上的事件,没有去读取客户端的请求数据,但是并不会导致客户端请求得不到处理,因为既然主进程没有去读监听sockets上的数据,那么数据就阻塞在那,等待子进程就可以去读到,进而去响应客户端请求。

主进程之所以要保留(不关闭)那些监听sockets,是因为后续再创建新子进程时,要把这些监听sockets传承过去。

论坛徽章:
0
32 [报告]
发表于 2012-08-08 16:34 |只看该作者
嗯 ,和我理解差不多,同时通过代码验证确属如此,但之前没有确认过,还是比较担心主进程拿到请求后不处理会导致超时丢弃,呵呵:wink:

另外,这个机制应该是OS系统层面来保证的吧?

论坛徽章:
8
亥猪
日期:2014-02-09 10:55:252015小元宵徽章
日期:2015-03-06 15:57:20数据库技术版块每日发帖之星
日期:2015-06-08 22:20:00综合交流区版块每日发帖之星
日期:2015-06-14 22:20:002015亚冠之阿尔沙巴布
日期:2015-09-01 20:23:45IT运维版块每日发帖之星
日期:2015-09-04 06:20:00IT运维版块每日发帖之星
日期:2015-11-04 06:20:00IT运维版块每日发帖之星
日期:2015-12-04 06:20:00
33 [报告]
发表于 2012-08-08 18:15 |只看该作者
lenky0401 发表于 2012-07-22 08:19
弄个精华帖,木有人反对吧?


非常好的内容,下载下来学习一下

论坛徽章:
2
操作系统版块每日发帖之星
日期:2015-08-05 06:20:0015-16赛季CBA联赛之北控
日期:2019-02-13 22:56:03
34 [报告]
发表于 2012-08-09 10:14 |只看该作者
我用的web服务器也是ngix,我觉得他简单明了,在消耗内存方面最少,而且访问速度也是强悍,不知道ngix如何与sql数据库连接呢

论坛徽章:
0
35 [报告]
发表于 2012-08-09 10:23 |只看该作者
nginx本身不连数据库,你可以通过类似fastcgi模块,让nginx连接到php引擎,作为跳板再到其它数据库,比如mysql。
google下“Linux nginx php mysql”

论坛徽章:
0
36 [报告]
发表于 2012-08-09 10:59 |只看该作者
UP

貌似深入讲解web服务器的书很少,楼主有推荐的吗

论坛徽章:
0
37 [报告]
发表于 2012-08-09 16:08 |只看该作者
正好需要一些学习资料,多谢分享!

论坛徽章:
0
38 [报告]
发表于 2012-08-15 13:02 |只看该作者
楼主,辛苦了~~

论坛徽章:
0
39 [报告]
发表于 2012-08-16 07:31 |只看该作者
第一章 进程解析
进程模型
nginx的进程模型和大多数后台服务程序一样,按职责将进程分成监控进程和工作进程两类,启动nginx的主进程充当监控进程,而由主进程fork出来的子进程则充当工作进程。工作进程的任务自然是完成具体的业务逻辑,而监控进程充当整个进程组的对外接口,同时对工作进程进行监护,比如如果某工作进程意外退出,监控进程将重新fork生成一个新的工作进程。nginx也可以单进程模型执行,在这种进程模型下,主进程就是工作进程,此时没有监控进程,单进程模型比较简单且官方建议仅供测试使用,所以下面主要分析多进程模型。
        分析nginx多进程模型的入口函数为主进程的ngx_master_process_cycle()函数,在该函数做完信号处理设置等之后就会调用一个名为ngx_start_worker_processes()的函数用于fork产生出子进程(子进程数目通过函数调用的第二个实参指定),子进程作为一个新的实体开始充当工作进程的角色执行ngx_worker_process_cycle()函数,该函数主体为一个无限for循环,持续不断的处理客户端的服务请求,而主进程继续执行ngx_master_process_cycle()函数,也就是作为监控进程执行主体for循环,这也是一个无限循环,直到进程终止才退出,服务进程基本都是这种写法,所以不用详述,下面先看看这个模型的图示:

上图中表现得很明朗,监控进程和工作进程各有一个无限for循环,以便进程持续的等待和处理自己负责的事务,直到进程退出。
监控进程的无限for循环内有一个关键的sigsuspend()函数调用,该函数的调用使得监控进程的大部分时间都处于挂起等待状态,直到监控进程接收到信号为止,当监控进程接收到信号时,信号处理函数ngx_signal_handler()就会被执行,我们知道信号处理函数一般都要求足够简单(关于信号处理函数的实现准则请Google),所以在该函数内执行的动作主要也就是根据当前信号值对相应的旗标变量做设置,而实际的处理逻辑必须放在主体代码里来处理,所以该for循环接下来的代码就是判断有哪些旗标变量被设置而需要处理的,比如ngx_reap(有子进程退出?)、ngx_quit或ngx_terminate(进行要退出或终止?注意:虽然两个旗标都是表示结束nginx,不过ngx_quit的结束更优雅,它会让nginx监控进程做一些清理工作且等待子进程也完全清理并退出之后才终止,而ngx_terminate更为粗暴,不过它通过使用SIGKILL信号能保证在一段时间后必定被结束掉)、ngx_reconfigure(重新加载配置?)等。当所有信号都处理完时又挂起在函数sigsuspend()调用处继续等待新的信号,如此反复,构成监控进程的主要执行体。
82:        Filename : ngx_process_cycle.c
83:        void
84:        ngx_master_process_cycle(ngx_cycle_t *cycle)
85:        {
86:        …
146:            for ( ;; ) {
147:                …
170:                sigsuspend(&set);
171:                …
177:                if (ngx_reap) {
178:                …
184:                if (!live && (ngx_terminate || ngx_quit)) {
185:                …
188:                if (ngx_terminate) {
189:                …
210:                if (ngx_quit) {
211:                …
212:            }
213:        …
工作进程的执行主体与监控进程类似,不过工作进程既名之为工作进程,那么它的主要关注点就是与客户端或后端真实服务器(此时nginx作为中间代理)之间的数据可读/可写等交互事件,而不是进程信号,所以工作进程的阻塞点是在像select()、epoll_wait()等这样的I/O多路复用函数调用处,以等待发生数据可读/可写事件,当然,也可能被新收到的进程信号中断。关于I/O多路复用的更多细节,请参考其他章节。
721:        Filename : ngx_process_cycle.c
722:        static void
723:        ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
724:        {
725:        …
780:            for ( ;; ) {
781:       
782:                if (ngx_exiting) {
783:                …
806:                ngx_process_events_and_timers(cycle);
807:       
808:                if (ngx_terminate) {
809:                …
810:            }
811:        …

整体架构
如前面介绍的那样,正常执行起来后的Nginx会有多个进程,最基本的有master_process和worker_process,还可能会有cache相关进程(这在后面会具体讲到)。除了自身进程之间的相互通信,Nginx还凭借强悍的模块功能与外界四通八达,比如通过upstream与web server通信、依靠fastcgi与application server通信等等。一个较为完整的整体架构框图如下所示:


进程通信
运行在多进程模型的nginx在正常工作时,自然就会有多个进程实例,比如下图是在配置“worker_processes  4;”情况下的显示,nginx设置的进程title能很好的帮助我们区分监控进程与工作进程,不过带上选项f的ps命令以树目录的形式打印各个进程信息也能帮助我们做这个区分。多进程联合工作必定要牵扯到进程之间的通信问题,下面就来看看nginx是如何做的。

采用socketpair()函数创造一对未命名的UNIX域套接字来进行Linux下具有亲缘关系的进程之间的双向通信是一个非常不错的解决方案。nginx就是这么做的,先看fork生成新工作进程的ngx_spawn_process()函数以及相关代码:
21:        Filename : ngx_process.h
22:        typedef struct {
23:            ngx_pid_t           pid;
24:            int                 status;
25:            ngx_socket_t        channel[2];
26:        …
27:        } ngx_process_t;
28:        …
47:        #define NGX_MAX_PROCESSES         1024

35:        Filename : ngx_process.c
36:        ngx_process_t    ngx_processes[NGX_MAX_PROCESSES];
37:       
86:        ngx_pid_t
87:        ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
88:            char *name, ngx_int_t respawn)
89:        {
90:        …
117:            if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes.channel) == -1)
118:        …
186:            pid = fork();
187:        …
在该函数进行fork()之前,先调用了socketpair()创建一对socket描述符存放在变量ngx_processes.channel内(其中s标志在ngx_processes数组内第一个可用元素的下标,比如最开始产生第一个工作进程时,可用元素的下标s为0),而在fork()之后,由于子进程继承了父进程的资源,那么父子进程就都有了这一对socket描述符,而nginx将channel[0]给父进程使用,channel[1]给子进程使用,这样分别错开的使用不同socket描述符,即可实现父子进程之间的双向通信:

除此之外,对于各个子进程之间,也可以进行双向通信。如前面所述,父子进程的通信channel设定是自然而然的事情,而子进程之间的通信channel设定就涉及到进程之间文件描述符(socket描述符也属于文件描述符)的传递,因为虽然后生成的子进程通过继承的channel[0]能够往前生成的子进程发送信息,但前生成的子进程无法获知后生成子进程的channel[0]而不能发送信息,所以后生成的子进程必须利用已知的前生成子进程的channel[0]进行主动告知,下面来看看这个具体是怎样的。
在子进程的启动初始化函数ngx_worker_process_init()里,会把ngx_channel(也就是channel[1])加入到读事件监听集里,对应的回调处理函数为ngx_channel_handler():
834:        Filename : ngx_process_cycle.c
835:        static void
836:        ngx_worker_process_init(ngx_cycle_t *cycle, ngx_uint_t priority)
837:        {
838:        …
994:            if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,
995:                                      ngx_channel_handler)
996:                == NGX_ERROR)
997:            {
998:        …
而在父进程fork()生成一个新子进程后,就会立即通过ngx_pass_open_channel()函数把这个子进程的相关信息告知给其前面已生成的子进程:
430:        Filename : ngx_process_cycle.c
431:        static void
432:        ngx_pass_open_channel(ngx_cycle_t *cycle, ngx_channel_t *ch)
433:        {
434:       
436:            for (i = 0; i < ngx_last_process; i++) {
437:        …
453:                ngx_write_channel(ngx_processes.channel[0],
454:                                  ch, sizeof(ngx_channel_t), cycle->log);
455:            }
456:        }
其中参数ch里包含了刚创建的新子进程(假定为A)的pid、进程信息在全局数组里存储下标、socket描述符channel[0]等信息,这里通过for循环遍历所有存活的其它子进程,然后调用函数ngx_write_channel()通过继承的channel[0]描述符进行信息主动告知,而收到这些消息的子进程将执行设置好的回调函数ngx_channel_handler(),把接收到的新子进程A的相关信息存储在全局变量ngx_processes内:
1066:        Filename : ngx_process_cycle.c
1067:        static void
1068:        ngx_channel_handler(ngx_event_t *ev)
1069:        {
1070:        …
1126:                case NGX_CMD_OPEN_CHANNEL:
1127:        …
1132:                    ngx_processes[ch.slot].pid = ch.pid;
1133:                    ngx_processes[ch.slot].channel[0] = ch.fd;
1134:                    break;
1135:        …
这样,前后子进程都有了对方的相关信息,相互通信也就没有问题了,这其中还有一些没讲到的具体实现细节,请以关键字“进程之间文件描述符传递”进行Google搜索。直接看一下实例,就以上面显示的各个父子进程为例:
ngx_processes        父-8706        子-8707        子-8708        子-8709        子-8710
[0]-8707-channel        {3, 7}*        {-1, 7}**        {3, -1}        {3, -1}        {3, -1}
[1]-8708-channel        {8, 9}        {3, 0}        {-1, 9}        {8, -1}        {8, -1}
[2]-8709-channel        {10, 11}        {9, 0}        {7, 0}        {-1, 11}        {10, -1}
[3]-8710-channel        {12, 13}        {10, 0}        {8, 0}        {7, 0}        {-1, 13}
上表格中,{a, b}分别表示channel[0]和channel[1]的值,-1表示这之前是描述符,但在其后被主动close()掉了,0表示这一直都无对应的描述符,其它数字表示对应的描述符值。比如,带*的{3, 7}表示如果父进程8706向子进程8707发送消息,需使用channel[0],即描述符3,它的channel[1]为7,没有被close()关闭掉,但一直也都没有被使用,所以没有影响,不过按道理应该关闭才是;而带**的{-1, 7}表示如果子进程8707向父进程8706发送消息,需使用channel[1],即描述符7,它的channel[0]为-1表示已经close()关闭掉了(nginx某些地方调用close()时并没有设置对应变量为-1,我这里为了好说明,对已经close()掉的描述符全部标记为-1了);
越是后生成的子进程,其channel[0]与父进程的对应channel[0]值相同的越多,因为基本都是继承而来,但前面生成的子进程的channel[0]是通过传递获得的,所以与父进程的对应channel[0]不一定相等。比如如果子进程8707向子进程8710发送消息,需使用channel[0],即描述符10,而对应的父进程channel[0]却是12,虽然它们在各自进程里却表现为不同的整型数字,但在内核里表示同一个描述符结构,即不管是子进程8707往描述符10写数据还是父进程8706往描述符12写数据,子进程8710都能通过描述符13正确读取到这些数据,至于子进程8710怎么识别它读到的数据是来之子进程8707还是父进程8706,就得靠其收到的数据特征(比如pid字段)来做标记区分。
最后,就目前nginx代码来看,子进程并没有往父进程发送任何消息,子进程之间也没有相互通信的逻辑,也许是因为nginx有其它一些更好的进程通信方式,比如共享内存等,所以这种channel通信目前仅做为父进程往子进程发送消息使用,但由于有这个基础在这,如果未来要使用channel做这样的事情,的确是可以的。

共享内存
共享内存是Linux下进程之间进行数据通信的最有效方式之一,而nginx就为我们提供了统一的操作接口来使用共享内存。
在nginx里,一块完整的共享内存以结构体ngx_shm_zone_t来封装表示,这其中包括的字段有共享内存的名称(shm_zone.shm.name)、大小(shm_zone.shm.size)、标签(shm_zone.tag)、分配内存的起始地址(shm_zone.shm.addr)以及初始回调函数(shm_zone.init)等:
24:        Filename : ngx_cycle.h
25:        typedef struct ngx_shm_zone_s  ngx_shm_zone_t;
26:        …
27:        struct ngx_shm_zone_s {
28:            void                     *data;
29:            ngx_shm_t                 shm;
30:            ngx_shm_zone_init_pt      init;
31:            void                     *tag;
32:        };
这些字段大都容易理解,只有tag字段需要解释一下,因为看上去它和name字段有点重复,而事实上,name字段主要用作共享内存的唯一标识,它能让nginx知道我想使用哪个共享内存,但它没法让nginx区分我到底是想新创建一个共享内存,还是使用那个已存在的旧的共享内存。举个例子,模块A创建了共享内存sa,模块A或另外一个模块B再以同样的名称sa去获取共享内存,那么此时nginx是返回模块A已创建的那个共享内存sa给模块A/模块B,还是直接以共享内存名重复提示模块A/模块B出错呢?不管nginx采用哪种做法都有另外一种情况出错,所以新增一个tag字段做冲突标识,该字段一般也就指向当前模块的ngx_module_t变量即可。这样在上面的例子中,通过tag字段的帮助,如果模块A/模块B再以同样的名称sa去获取模块A已创建的共享内存sa,模块A将获得它之前创建的共享内存的引用(因为模块A前后两次请求的tag相同),而模块B则将获得共享内存已做它用的错误提示(因为模块B请求的tag与之前模块A请求时的tag不同)。
当我们要使用一个共享内存时,总会在配置文件里加上该共享内存的相关配置信息,而nginx在进行配置解析的过程中,根据这些配置信息就会创建对应的共享内存,不过此时的创建仅仅只是代表共享内存的结构体ngx_shm_zone_t变量的创建,这具体实现在函数shared_memory_add()内。另外从这个函数中,我们也可以看到nginx使用的所有共享内存都以list链表的形式组织在全局变量cf->cycle->shared_memory下,在创建新的共享内存之前会先对该链表进行遍历查找以及冲突检测,对于已经存在且不存在冲突的共享内存可直接返回引用。以ngx_http_limit_req_module模块为例,它需要的共享内存在配置文件里以limit_req_zone配置项出现:
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
nginx在进行配置解析时,遇到limit_req_zone配置项则调用其对应的处理函数ngx_http_limit_req_zone(),而在该函数内又将继续调用函数shared_memory_add()创建对应的ngx_shm_zone_t结构体变量并加入到全局链表内:
ngx_http_limit_req_zone() -> ngx_shared_memory_add() -> ngx_list_push()
共享内存的真正创建是在配置文件全部解析完后,所有代表共享内存的结构体ngx_shm_zone_t变量以链表的形式挂接在全局变量cf->cycle->shared_memory下,nginx此时遍历该链表并逐个进行实际创建,即分配内存、管理机制(比如锁、slab)初始化等:
398:        Filename : ngx_cycle.c
399:            /* create shared memory */
400:       
401:            part = &cycle->shared_memory.part;
402:            shm_zone = part->elts;
403:       
404:            for (i = 0; /* void */ ; i++) {
405:        …
467:                if (ngx_shm_alloc(&shm_zone.shm) != NGX_OK) {
468:        …
471:                if (ngx_init_zone_pool(cycle, &shm_zone) != NGX_OK) {
472:        …
475:                if (shm_zone.init(&shm_zone, NULL) != NGX_OK) {
476:        ...
477:            }
其中函数ngx_shm_alloc()是共享内存的实际分配,针对当前系统可提供接口,可以是mmap或shmget等;而ngx_init_zone_pool()函数是共享内存管理机制的初始化,因为共享内存的使用涉及到另外两个主题,第一,既然是共享内存,那么必然是多进程共同使用,所以必须考虑互斥问题;第二,nginx既以性能著称,那么对于共享内存自然也有其独特的使用方式,虽然我们可以不用(在马上要介绍到的init回调函数里做覆盖处理即可),但在这里也默认都会以这种slab的高效访问机制进行初始化。关于这两点,这里暂且略过,待后续再做讨论。
回调函数shm_zone.init()是各个共享内存所特定的,根据使用方的自身需求不同而不同,这也是我们在使用共享内存时需特别注意的函数。继续看实例ngx_http_limit_req_module模块的init函数ngx_http_limit_req_init_zone():
398:        Filename : ngx_http_limit_req_module.c
399:        static ngx_int_t
400:        ngx_http_limit_req_init_zone(ngx_shm_zone_t *shm_zone, void *data)
401:        {
402:            ngx_http_limit_req_ctx_t  *octx = data;
403:        …
398:            if (octx) {
399:        …
608:                ctx->shpool = octx->shpool;
609:        …
608:               return NGX_OK;
609:            }
610:       
611:            ctx->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
612:        …
608:            ctx->sh = ngx_slab_alloc(ctx->shpool, sizeof(ngx_http_limit_req_shctx_t));
609:        …
函数ngx_http_limit_req_init_zone()的第二个参数data表示‘旧’数据,在进行重新加载配置时(即nginx收到SIGHUP信号)该值将不为空,如果旧数据可继续使用,那么可直接返回NGX_OK;否则,需根据自身模块逻辑对共享内存的使用做相关初始化,比如ngx_http_limit_req_module模块,在第634、642行直接使用默认已初始化好的slab机制,进行内存的分配等。当函数ngx_http_limit_req_init_zone()正确执行结束,一个完整的共享内存就已创建并初始完成,接着要做的就是共享内存的使用,这即回到前面提到的两个主题:互斥与slab。
要解决互斥问题,无非就是利用锁机制,强制同一时刻只能有一个进程在访问共享内存,其基本原理就是利用共享的简单资源(比如最简单的原子变量)来代表复杂资源,一个进程在需要操作复杂资源之前先获得对简单资源的使用权限;因为简单资源足够简单,对它的使用权限的获取往往只有一步或几步,所以更容易避免冲突;这个应该是容易理解的,比如一个需要100步的操作肯定比一个只需要3步的操作更容易发生冲突(每一步需要的复杂度相同),因为前一种情况可能会一个进程在进行了99步后却因另外一个进程发出动作而失败,而后一种情况的进程执行完3步后就已经获得完全使用权限了。
要讲清楚nginx互斥锁的实现,如果不结合具体的代码恐怕是不行的,因为都是一些细节上的考量,比如根据各种不同的CPU架构选择不同的汇编指令、使用不同的共享简单资源(原子变量或文件描述符),并没有什么特别难以理解的地方,查CPU手册和系统man手册很容易懂,所以具体实现这里暂且不讲,还好nginx互斥锁的使用非常简单,提供的接口函数以及含义如下:
函数        含义
ngx_shmtx_create()        创建
ngx_shmtx_destory()        销毁
ngx_shmtx_trylock()        尝试加锁(加锁失败则直接返回,不等待)
ngx_shmtx_lock()        加锁(持续等待,直到加锁成功)
ngx_shmtx_unlock()        解锁
ngx_shmtx_force_unlock()        强制解锁(可对其它进程进行解锁)
ngx_shmtx_wakeup()        唤醒等待加锁进程(系统支持信号量的情况下才可用)

slab机制
nginx的slab机制与linux的slab机制在基本原理上并没有什么特别大的不同(当然,相比而言,linux的slab机制要复杂得多),简单来说也就是基于两点:缓存与对齐。缓存意味着预分配,即提前申请好内存并对内存做好划分形成内存池,当我们需要使用一块内存空间时,nginx就直接从已经申请并划分好的内存池里取出一块合适大小的内存即可,而内存的释放也是把内存返还给nginx的内存池,而不是操作系统;对齐则意味着内存的申请与分配总是按2的幂次方进行,即内存大小总是为8、16、32、64等,比如,虽然只申请33个字节的内存,但也将获得实际64字节可用大小的内存,这的确存在一些内存浪费,但对于内存性能的提升是显著的(关于内存对齐对性能的影响,可以参考:http://lenky.info/?p=310),更重要的是把内部碎片也掌握在可控的范围内。
nginx的slab机制主要是和共享内存一起使用,前面提到对于共享内存,nginx在解析完配置文件,把即将使用的共享内存全部以list链表的形式组织在全局变量cf->cycle->shared_memory下之后,就会统一进行实际的内存分配,而nginx的slab机制要做的就是对这些共享内存进行进一步的内部划分与管理,关于这点,从函数ngx_slab_init()的逻辑即可初见端倪,不过在此之前,先看看ngx_init_zone_pool()函数对它的调用:
916:        Filename : ngx_slab.c
917:        static ngx_int_t
918:        ngx_init_zone_pool(ngx_cycle_t *cycle, ngx_shm_zone_t *zn)
919:        {
920:            u_char           *file;
921:            ngx_slab_pool_t  *sp;
922:       
923:            sp = (ngx_slab_pool_t *) zn->shm.addr;
924:        …
937:            sp->end = zn->shm.addr + zn->shm.size;
938:            sp->min_shift = 3;
939:            sp->addr = zn->shm.addr;
940:        …
960:            ngx_slab_init(sp);
961:        …
函数ngx_init_zone_pool()是在共享内存分配好后进行的初始化调用,而该函数内又调用了本节介绍的重点对象slab的初始化函数ngx_slab_init();,此时的情况图示如下:

可以看到此时共享内存的开始部分内存已经被用作结构体ngx_slab_pool_t的存储空间,这相当于是slab机制的额外开销(overhead),后面还会看到其他额外开销,任何一种管理机制都有自己的一些控制信息需要存储,所以这些内存使用是无法避免的。共享内存剩下的部分才是被管理的主体,slab机制对这部分内存进行两级管理,首先是page页,然后是page页内的slab块(通过slot对相等大小的slab块进行管理,为了区分slab机制,下面以slot块来指代这些slab块),也就是说slot块是在page页内存的再一次管理。
在继续对slab机制分析之前,先看看下面这个表格里记录的一些变量以及其对应的值,因为它们可以帮助我们对后面内容的理解。这些变量会根据系统环境的不同而不同,但一旦系统环境确定,那么这些值也就将都是一些常量值,下面表格基于的系统环境在本书最开始有统一介绍,这里不再累述:
变量名        值        描述
ngx_pagesize        4096        系统内存页大小,Linux下一般情况就是4KB。
ngx_pagesize_shift        12        对应ngx_pagesize(4096),即是4096 = 1 << 12;。
ngx_slab_max_size        2048        slots分配和pages分配的分割点,大于等于该值则需从pages里分配。
ngx_slab_exact_size        128        正好能用一个uintptr_t类型的位图变量表示的页划分;比如在4KB内存页、32位系统环境下,一个uintptr_t类型的位图变量最多可以对应表示32个划分块的状态,所以要恰好完整的表示一个4KB内存页的每一个划分块状态,必须把这个4KB内存页划分为32块,即每一块大小为:
ngx_slab_exact_size = 4096 / 32 = 128。
ngx_slab_exact_shift        7        对应ngx_slab_exact_size(128),即是128 = 1 << 7;。
pool->min_shift        3        固定值为3。
pool->min_size        8        固定值为8,最小划分块大小,即是1 << pool->min_shift;。
好,再来看slab机制对page页的管理,初始结构示意图如下:

slab机制对page页的静态管理主要体现在ngx_slab_page_t[K]和page[N] 这两个数组上,需要解释几点:
第一,虽然是一个页管理结构(即ngx_slab_page_t元素)与一个page内存页相对应,但因为有对齐消耗以及slot块管理结构体的占用(图中的ngx_slab_page_t[n]数组),所以实际上页管理结构体数目比page页内存数目要多,即图中的ngx_slab_page_t[N]到ngx_slab_page_t[K-1],这些结构体完全被忽视,我们也不用去管它们,只是需要知道有这些东西的存在。
第二,如何根据页管理结构page获得对应内存页的起始地址p?计算方法如下:
384:        Filename : ngx_slab.c
385:                    p = (page - pool->pages) << ngx_pagesize_shift;
386:                    p += (uintptr_t) pool->start;
对照前面图示来看这很明显,无需过多解释;相反,根据内存页的起始地址p也能计算出其对应的页管理结构page。
第三,对齐是指实际page内存页按ngx_pagesize大小对齐,从图中看就是原本的start是那个虚线箭头所指的位置,对齐后就是实线箭头所指的位置,对齐能提高对内存页的访问速度,但这有一些内存浪费,并且末尾可能因为不够一个page内存页而被浪费掉,所以在ngx_slab_init()函数的最末尾有一次最终可用内存页的准确调整:
75:        Filename : ngx_cycle.c
76:        void
77:        ngx_slab_init(ngx_slab_pool_t *pool)
78:        {
79:        …
130:            m = pages - (pool->end - pool->start) / ngx_pagesize;
131:            if (m > 0) {
132:                pages -= m;
133:                pool->pages->slab = pages;
134:            }
135:        …
第130行计算的m值如果大于0,说明对齐等操作导致实际可用内存页数减少,所以后面的if语句进行判断调整。
page页的静态管理结构基本就是如此了,再来看page页的动态管理,即page页的申请与释放,这就稍微麻烦一点,因为一旦page页被申请或释放,那么就有了相应的状态:使用或空闲。先看空闲页的管理,nginx对空闲page页进行链式管理,链表的头节点pool->free,初始状态下的链表情况如下:

这是一个有点特别的链表,它的节点可以是一个数组,比如上图中的ngx_slab_page_t[N] 数组就是一个链表节点,这个数组通过第0号数组元素,即ngx_slab_page_t[0],接入到这个空闲page页链表内,并且整个数组的元素个数也记录在这个第0号数组元素的slab字段内。
如果经历如下几步内存操作:子进程1从共享内存中申请1页,子进程2接着申请了2页,然后子进程1又释放掉刚申请的1页,那么空闲链表各是一个什么状态呢?逐步来看。
子进程1从共享内存中申请1页:

子进程2接着申请了2页:

然后子进程1又释放掉刚申请的1页:

释放的page页被插入到链表头部,如果子进程2接着释放其拥有的那2页内存,那么空闲链表结构将如下图所示:

可以看到,nginx对空闲page页的链式管理不会进行节点合并,不过关系不大,毕竟page页既不是slab机制的最小管理单元,也不是其主要分配单元。对处于使用状态中的page页,也是采用的链式管理,在介绍其详细之前,需先来看看slab机制的第二级管理机制,即slot块,这样便于前后的连贯理解。
slot块是对每一页page内存的内部管理,它将page页划分成很多小块,各个page页的slot块大小可以不相等,但同一个page页的slot块大小一定相等。page页的状态通过其所在的链表即可辨明,而page页内各个slot块的状态却需要一个额外的标记,在nginx的具体实现里采用的是位图方式,即一个bit位标记一个对应slot块的状态,1为使用,0为空闲。
根据slot块的大小不同,一个page页可划分的slot块数也不同,从而需要的位图大小也不一样。前面提到过,每一个page页对应一个名为ngx_slab_page_t的管理结构,该结构体有一个uintptr_t类型的slab字段。在32位平台上(也就是本书讨论的设定平台),uintptr_t类型占4个字节,即slab字段有32个bit位。如果page页划分的slot块数小于等于32,那么nginx直接利用该字段充当位图,这在nginx内叫exact划分,每个slot块的大小保存在全局变量ngx_slab_exact_size以及ngx_slab_exact_shift内。比如,1个4KB的page页,如果每个slot块大小为128字节,那么恰好可划分成32块。下图是这种划分下的一种可能的中间情况:

如果划分的每个slot块比ngx_slab_exact_size还大,那意味着一个page页划分的slot块数更少,此时当然也是使用ngx_slab_page_t结构体的slab字段作为位图。由于比ngx_slab_exact_size大的划分可以有很多种,所以需要把其具体的大小也记录下来,这个值同样也记录在slab字段里。这样做是可行的,由于划分总是按2次幂增长,所以比ngx_slab_exact_size还大的划分至少要减少一半的slot块数,因此利用slab字段的一半bit位即可完整表示所有slot块的状态。具体点说就是:slab字段的高端bit用作位图,低端bit用于存储slot块大小(仅存其对应的移位数)。代码表现为:
378:        Filename : ngx_slab.c
379:                    page->slab = ((uintptr_t) 1 << NGX_SLAB_MAP_SHIFT) | shift;
如果申请的内存大于等于ngx_slab_max_size,nginx直接返回一个page整页,此时已经不在slot块管理里,所有无需讨论。下面来看小于ngx_slab_exact_size的情况,此时slot块数目已经超出了slab字段可表示的容量。比如假设按8字节划分,那么1个4KB的page页将被划分为512块,表示各个slot块状态的位图也就需要512个bit位,一个slab字段明显是不足够的,所以需要为位图另找存储空间,而slab字段仅用于存储slot块大小(仅存其对应的移位数)。
另找的位图存储空间就落在page页内,具体点说是其划分的前面几个slot块内。接着刚才说的例子,512个bit位的位图,即64个字节,而一个slot块有8个字节,所以就需要占用page页的前8个slot块用作位图。即,一个按8字节划分slot块的page页初始情况如下图所示:

由于前几个slot块一开始就被用作位图空间,所以必须把它们对应的bit位设置为1,表示其状态为使用。
不论哪种情况,都有了slot块的大小以及状态,那对slot块的分配与释放就水到渠成了。下面回到slab机制的最后一个话题,即对处于使用状态中的page页的链式管理。其实很简单,首先,根据每页划分的slot块大小,将各个page页加入到不同的链表内。在我们这里设定的平台上,也就是按8、16、32、64、128、256、512、1024、2048一共9条链表,在ngx_slab_init()函数里有其初始化:
102:        Filename : ngx_slab.c
103:            n = ngx_pagesize_shift - pool->min_shift;
104:       
105:            for (i = 0; i < n; i++) {
106:                slots.slab = 0;
107:                slots.next = &slots;
108:                slots.prev = 0;
109:            }
假设申请一块8字节的内存,那么slab机制将分配一共page页,将它按8字节做slot划分,并且接入到链表slots[0]内,相关示例(表示这只是其中一处实现)代码:
352:        Filename : ngx_slab.c
353:                    page->slab = shift;
354:                    page->next = &slots[slot];
355:                    page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;
356:       
357:                    slots[slot].next = page;
page->prev按4字节对齐,所以末尾两位可以用做它用,这里用于标记当前slot划分类型为NGX_SLAB_SMALL,图示如下:

继续申请8字节的内存不会分配新的page页,除非刚才那页page(暂且称之为页A)被全是使用完,一旦页A被使用完,它会被拆除出链表,相关示例代码:
232:        Filename : ngx_slab.c
233:                prev = (ngx_slab_page_t *)
234:                            (page->prev & ~NGX_SLAB_PAGE_MASK);
235:                prev->next = page->next;
236:                page->next->prev = page->prev;
237:       
238:                page->next = NULL;
239:                page->prev = NGX_SLAB_SMALL;
第234行是过滤掉末尾的标记位,以获得正确的前节点的地址,此时的图示如下:

如果仍然继续申请8字节的内存,那么nginx的slab机制必须分配新的page页(暂且称之为页B),类似于前面介绍的那样,页B会被加入到链表内,此时链表中只有一个节点,但如果此时页A释放了某个slot块,它又会被加入到链表中,终于形成了具有两个节点的链表,相关示例代码(变量page指向页A)以及图示如下:
455:        Filename : ngx_slab.c
456:                page->next = slots[slot].next;
457:                slots[slot].next = page;
458:       
459:                page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;
460:                page->next->prev = (uintptr_t) page | NGX_SLAB_SMALL;


论坛徽章:
0
40 [报告]
发表于 2012-08-16 07:32 |只看该作者
晕,为撒出现了删除线呢?嘛,算了,不管了。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP