搜索
您的当前位置:首页正文

Nginx源码研究

2024-09-08 来源:步旅网
Nginx源码研究

概貌 ................................................................................................................................................... 3 内存池 ............................................................................................................................................... 5 内存分配相关函数 .............................................................................................................................. 5 内存池结构 .......................................................................................................................................... 5 相关函数 .............................................................................................................................................. 7 小结 ...................................................................................................................................................... 9 ARRAY .............................................................................................................................................. 10 结构 .................................................................................................................................................... 10 相关函数 ............................................................................................................................................ 10 QUEUE ............................................................................................................................................. 11 结构 .................................................................................................................................................... 11 相关函数 ............................................................................................................................................ 12 HASH TABLE ..................................................................................................................................... 12 结构 .................................................................................................................................................... 12 相关函数 ............................................................................................................................................ 14 LIST .................................................................................................................................................. 15 结构 .................................................................................................................................................... 15 相关函数 ............................................................................................................................................ 15 NGINX启动处理 .............................................................................................................................. 16 WORK进程逻辑(NGX_WORKER_PROCESS_CYCLE()函数) ................................................................ 30 1.进程部份 ........................................................................................................................................ 30 2.线程部份 ........................................................................................................................................ 31 3.回到进程 ........................................................................................................................................ 32 CYCLE ............................................................................................................................................... 32 CONNECTION ................................................................................................................................... 33

CONNECTION的内存分布 ..................................................................................................................... 33 CONNECTION的分配与回收 ................................................................................................................. 33

EVENT .............................................................................................................................................. 34 结构 .................................................................................................................................................... 34 相关函数 ............................................................................................................................................ 38 CONNECTION ................................................................................................................................... 38 结构 .................................................................................................................................................... 38 相关函数 ............................................................................................................................................ 42

CONNECTION与EVENT ................................................................................................................... 42 BUFS ................................................................................................................................................ 44 UPSTREAM ....................................................................................................................................... 46

Nginx的源码是0.8.16版本。不是最新版本,但是与网上其他人研究nginx的源码有所修改。阅读时注意参照对比。

概貌

Nginx可以开启多个进程,每个进程拥有最大上限128个子线程以及一定的可用连接数。如果你希望使用线程可以在配置文件中设置worker_threads这个参数,但这个参数在Nginx官方手册上没有。只有通过阅读源代码才看到。最大客户端连接数等于进程数与连接数的乘积,连接是在主进程中初始化的,一开始所有连接处于空闲状态。

每一个客户端请求进来以后会通过事件处理机制,在Linux是Epoll,在FreeBSD下是KQueue放到空闲的连接里。

如果设置了线程数,那么被填充的连接会在子线程中处理,否则会在主线程中依次处理。 nginx由以下几个元素组成:

1. worker(进程) 2. thread(线程) 3. connection(连接) 4. event(事件) 5. module(模块) 6. pool(内存池) 7. cycle(全局设置)

8. log(日志)

整个程序从main()开始算

ngx_max_module = 0;

for (i = 0; ngx_modules[i]; i++) {

ngx_modules[i]->index = ngx_max_module++; }

这几句比较关键,对加载的模块点一下数,看有多少个。ngx_modules并不是在原代码中被赋值的,你先执行一下./configure命令生成用于编译的make环境。在根目录会多出来一个文件夹objs,找到ngx_modules.c文件,默认情况下nginx会加载大约30个模块,的确不少,如果你不需要那个模块尽量还是去掉好一些。

接下来比较重要的函数是 ngx_init_cycle(),这个函数初始化系统的配置以及网络连接等,如果是多进程方式加载的会继续调用ngx_master_process_cycle(),这是main函数中调用的最关键的两个函数。

ngx_init_cycle()实际上是个复杂的初始化函数,首先是加载各子模块的配置信息、并初始化各组成模块。

任何模块都有两个重要接口组成,一个是create_conf,一个是init_conf。分别是创建配置和初始化配置信息。

模块按照先后顺序依次初始化,大概是这样的:

内核模块(ngx_core_module), 错误日志(ngx_errlog_module), 配置模块(ngx_conf_module), 事件模块(ngx_events_module),

事件内核模块(ngx_event_core_module), EPOLL模块(ngx_epoll_module), http模块(ngx_http_module),

http内核模块(ngx_http_core_module), http日志模块(ngx_http_log_module), … …

epoll是比较关键的核心模块之一,nginx兼容多种IO控制模型。

内存池

内存分配相关函数

ngx_alloc.c中包括所有nginx内存申请的相关函数。 ngx_alloc() 包装了malloc(),仅添加了内存分配失败时的,log输出和debug时的log输出。

ngx_calloc() 调用上面的函数,成功分配后,将内存清零。

ngx_memalign() 也是向操作系统申请内存,只不过采用内存对齐方式。估计是为了减少内存碎片。如果操作系统支持posix_memalign()就采用它,如果支持memalign()则用memalign()。在0.8.19版本中,作者不再使用ngx_alloc(),而全部改用ngx_memalign()。注:在nginx的main()函数中,通过将ngx_pagesize 设置为1024来指定内存分配按1024bytes对齐。这是不是意味着你虽指示分配10 bytes的内存,实际上nginx也向操作系统申请至少1024bytes的内存。

内存池结构

Nginx的内存池类型是ngx_pool_t。这个类型定义在ngx_core.h中。 typedef struct ngx_pool_s ngx_pool_t;

由定义可知ngx_pool_t背后实际上是struct ngx_pool_s。这个结构体在ngx_palloc.h中有定义。

据说以前版本nginx中内存池的结构如下:

struct ngx_pool_s {

u_char *last; u_char *end; ngx_pool_t *current; ngx_chain_t *chain; ngx_pool_t *next; ngx_pool_large_t *large; ngx_pool_cleanup_t *cleanup; ngx_log_t *log; };

目前版本中结构则是这样:

内存池管理结点:

typedef struct {

u_char *last; /* 指向所使用内存的最后的地址 */

u_char *end; /* 指向所申请到内存块的最后的地址 */ ngx_pool_t *next; /* 指向下一个ngx_pool_t */ ngx_uint_t failed; /* 这个 */ } ngx_pool_data_t;

内存池管理队列的头结点: struct ngx_pool_s {

ngx_pool_data_t d;

size_t max; /* 内存池块中空闲空间的大小 */ ngx_pool_t *current; /* 指向当前块自身 */ ngx_chain_t *chain;

ngx_pool_large_t *large; /* 用来指向大块内存 */

ngx_pool_cleanup_t *cleanup; /* 释放内存时调用的清除函数队列 */ ngx_log_t *log; /* 指向log的指针 */

};

清除函数的指针如下:

typedef void (*ngx_pool_cleanup_pt)(void *data);

清除函数的队列结构:

typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;

struct ngx_pool_cleanup_s {

ngx_pool_cleanup_pt handler; /* 清除函数 */

void *data; /* 清除函数所用的参数 */ ngx_pool_cleanup_t *next; /* 下一个结点的指针 */ };

指向大块内存的结构:

typedef struct ngx_pool_large_s ngx_pool_large_t;

struct ngx_pool_large_s {

ngx_pool_large_t *next; /* 指向下一个大块内存。*/

/* 大块内存也是队列管理。*/

void *alloc; /* 指向申请的大块数据 */ };

当待分配空间已经超过了池子自身大小,nginx也没有别的好办法,只好按照你需要分配的大小,实际去调用malloc()函数去分配,例如池子的大小是1K,待分配的大小是1M。实际上池子里只存储了ngx_pool_large_t结构,这个结构中的alloc指针,指向被分配的内存,并把这个指针返回给系统使用。

相关函数

ngx_create_pool() 函数用来创建内存池。 第一步,调用ngx_alloc()申请内存;

第二步,设置ngx_pool_t中的成员d中的各个变量; …

p->d.last = (u_char *) p + sizeof(ngx_pool_t); p->d.end = (u_char *) p + size; …

从代码看出,d.end指向内存块的结尾处,而d.last则指向所占用的内存的结尾处。刚申请的内存中占用ngx_pool_t结构作为管理单元。所以,此时d.last指向(u_char *) p + sizeof(ngx_pool_t)处。

第三步,设置其他成员。注意:在计算max时,max中存放的数指所申请内存块中空闲的大小。因此,在计算max之前先减去了管理结点本身的大小。

ngx_destroy_pool() 用来释放内存池,一共分三步: 第一步、在释放前先对业务逻辑进行释放前的处理

for (c = pool->cleanup; c; c = c->next) { if (c->handler) {

ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, \"run cleanup: %p\ c->handler(c->data); }

}

第二步、释放large占用的内存 for (l = pool->large; l; l = l->next) {

ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, \"free: %p\

if (l->alloc) {

ngx_free(l->alloc); } }

第三步、释放所有的池子

for (p = pool, n = pool->next; /* void */; p = n, n = n->next) { ngx_free(p);

if (n == NULL) { break; } }

ngx_palloc_large() 函数专用来申请大块内存。 第一步,申请的大块内存,在ngx_pool_t中大块他队列中寻找空闲的ngx_pool_larger结点。如果找到,将大块内存挂在该结点上。

ngx_pool_larger队列中查找空闲结点数不会超过三次。超过三个结点没找到空闲结点就放弃。

创建一个新的结点,将申请到地大块内存挂在这个新结点上。将这个结点插入队列头部。

ngx_palloc() 函数用来申请内存块。首先要说明的是内存池中可能是由多块内存块组成的队列。其中每块内存都有一个ngx_pool_t管理结点用来连成管理队列。 1. 如果申请的内存大小超过了当前的内存池结点中空闲空间,nginx直接采用

ngx_palloc_large()函数进行大块内存申请;

2. 在内存池管理队列中寻找能够满足申请大小的管理结点。如果找到了,先按32

位对齐方式计算申请内存的指针,再将last指向申请内存块的尾部。 3. 找不到合适的内存池,用ngx_palloc_block()函数。

ngx_pnalloc() 函数与ngx_palloc()函数唯一不同之处,就是在计算申请内存的指针的方式未按32位对齐方式计算。

ngx_palloc_block() 函数用来分配新的内存池块,形成一个队列。

这个函数中申请到新的内存池块后,在该块中分配完ngx_pool_data_t结点后,将这个结点挂在内存池队列的结尾处。 这个函数中有两个要注意的地方:

1. 在内存池块中保留ngx_pool_data_t时,不仅是按ngx_pool_data_t大小计算而

且是按32位对齐。

2. ngx_pool_data_t结构中的failed的妙用。单从字面上不是太好理角这个成员变

量的作用。实际上是用来计数用的。

for (p = current; p->d.next; p = p->d.next) { if (p->d.failed++ > 4) { current = p->d.next; } } 从上面这段代码是寻找内存池队列的尾部。当队列较长,由于内存池管理队列

是单向队列所以每次从头到尾搜索是很费时的。每次搜寻失败的结点(非尾部结点)的failed加1。failed指出了该结点经历多少次查寻,目前版本中超过4次时,将内存池的current指针指向其后续的结点。这样,下次再做类似查询时,可以跳过若干不必要的结点加快查询速度。

ngx_pmemalign() 函数采用内存对齐的方式申请大内存块。 ngx_pfree() 函数用来释放大内存块。成功返回NGX_OK,失败返回NGX_DECLINED。

ngx_pcalloc() 函数使用ngx_palloc()函数申请内存后,将申请的内存清零。

ngx_pool_cleanup_add() 函数只是用来添加内存池的cleanup队。

由ngx_pool_cleanup_t类型的结点组成了内存池的清除函数处理队列。后加入队列的函数先调用。Nginx中预定义了两个cleanup函数。 void ngx_pool_cleanup_file(void *data) 用来关闭打开的文件。

void ngx_pool_delete_file(void *data) 用来删除文件并且试图关闭文件。

小结

下面是nginx内存池的概貌图。

big data blockbig data blocklastallocnextallocnextendnextfailedngx_pool_data_tpoollargercleanuphandlerdatanext...handlerdatanextcurrent......ngx_pool_t

Array

结构

struct ngx_array_s {

void *elts; ngx_uint_t nelts; size_t size; ngx_uint_t nalloc; ngx_pool_t *pool; };

/* 指向数组元素的起始地址 */ /* 现使用的元素的数目 */ /* 元素的大小 */

/* 分配数组中元素的数目 */ /* 指向所在的内存池的指针 */

注:nelts应该小于等于nalloc。例:分配了5个元素,使用了3个元素;则nalloc = 5, nelts = 3。

相关函数

ngx_array_init() 函数,在指定的array对象上分配n个元素,元素大小为size。

ngx_array_create()函数,函数参数中没有array对象的指针。这个函数在内存池中

创建一个array对象,并且分配n个元素,元素大小为size。

ngx_array_push()函数,将array对象当作堆栈,作压栈处理。如果当前内存池没有空闲空间可用,就会申请新的内存池并且创建一个是原来array对象两倍大小的新array,原array对象中的元素复制到新array中。

ngx_array_push_n()函数,与ngx_array_push()函数功能类似。ngx_array_push_n()是压n个元素,ngx_array_push()压入一个元素。

Queue

结构

typedef struct ngx_queue_s ngx_queue_t;

struct ngx_queue_s { ngx_queue_t *prev; ngx_queue_t *next; };

注意nginx的队列操作和结构只进行指针的操作,不负责节点内容空间的分配和保存,所以在定义自己的队列节点的时候,需要自己定义数据结构以及分配空间, 并包含一个ngx_queue_t类型的成员, 需要获得原始的数据节点的时候需要使用ngx_queue_data宏

#define ngx_queue_data(q, type, link) (type *) ((u_char *) q - offsetof(type, link)) 另外,整个queue结构中包含一个 sentinel(哨兵) 节点, 他指向队列的头和尾

相关函数

ngx_queue_init(()函数,初始化队列;

ngx_queue_insert_head()函数,在队列头部插入一个元素; ngx_queue_sentinel()函数,取得队列的哨兵; ngx_queue_prev()函数,取得前一个元素; ngx_queue_last()函数,取得队列最后一个元素; ngx_queue_sort()函数,对队列中的元素进行排序; ngx_destroy_pool()函数,销毁队列。

Hash table

结构

这个在模块配置解析里经常被用到。该 hash 结构是只读的,即仅在初始创建时可以给出保存在其中的 key-val 对,其后就只能查询而不能进行增删改操作了。

typedef struct {

void *value; u_char len;

u_char name[1]; } ngx_hash_elt_t;

typedef struct {

ngx_hash_elt_t **buckets; ngx_uint_t size; } ngx_hash_t;

typedef struct {

ngx_hash_t hash; void *value; } ngx_hash_wildcard_t;

typedef struct {

ngx_str_t key;

ngx_uint_t key_hash; void *value; } ngx_hash_key_t;

typedef ngx_uint_t (*ngx_hash_key_pt) (u_char *data, size_t len);

typedef struct {

ngx_hash_t hash; ngx_hash_wildcard_t *wc_head; ngx_hash_wildcard_t *wc_tail; } ngx_hash_combined_t;

typedef struct {

ngx_hash_t *hash; ngx_hash_key_pt key;

ngx_uint_t max_size; ngx_uint_t bucket_size;

char *name; ngx_pool_t *pool;

ngx_pool_t *temp_pool; } ngx_hash_init_t;

相关函数

虽然代码理解起来比较混乱,但是使用还是比较简单的,常用的有创建 hash 和在 hash 中进行查找两个操作,对于创建hash的操作,过程一般为:

a) 构造一个 ngx_hash_key_t 为成员的数组, 包含 key, value 和 使用key计算出的一

个hash值

b) 构建一个 ngx_hash_init_t结构体的变量, 其中包含了ngx_hash_t 的成员, 为hash

的结构体, 还包括一些其他初始设置,如bucket的大小,内存池等

c) 调用 ngx_hash_init 传入 ngx_hash_init_t 结构, ngx_hash_key_t 的数组,和数组

的长度, 进行初始化,这样 ngx_hash_init_t的hash成员就是我们要的hash结构 查找的过程很简单 a) 计算 key 的hash值;

b) 使用ngx_hash_find进行查找,需要同时传入hash值和key,返回的就是value的指针。

List

结构

struct ngx_list_part_s {

void *elts; /* list中元素 */

ngx_uint_t nelts; /* 使用data多少字节,初值为0 */ ngx_list_part_t *next; /* 指向下一个list part结点 */ };

typedef struct {

ngx_list_part_t *last; /* 指向最后一个list part结点 */ ngx_list_part_t part; /* list的头结点中list part部份 */ size_t size; /* 元素大小 */

ngx_uint_t nalloc; /* list part结点中申请的元素的容量大小 */ ngx_pool_t *pool; /* 指向内存池 */ } ngx_list_t;

lasteltspartneltsnextsizenallocpooleltsneltsnexteltsneltsnextdatadatadata

相关函数

ngx_list_init() 函数创建一个新的list对象设置ngx_list_t指针,内存池中申请的头结点大小为n * size。

1. ngx_list_create() 函数中没有指定list指针,创建一个新的list对象,头结点大小为

n*size。

2. ngx_list_push() 函数,将list当作堆栈将元素压栈,如果当前的list结点中没有空间,

则在内存池中申请一个新的list结点,且作为堆栈的顶部。

3. 遍历一个list的方法

part = &list.part; data = part->elts;

for (i = 0 ;; i++) { if (i >= part->nelts) { if (part->next == NULL) { break; }

part = part->next; data = part->elts; i = 0; }

... data[i] ... }

这段示例代码中,part->nelts表示占用的数据区中的字节数,每到一个节点将i设为0;所以i>=part->nelts满足时表示当前list节点中数据处理完了,于是跳到下一个节点,在对下一个节点的数据进行处理之前,再将i设为0。

Nginx启动处理

1. 从main()函数开始,进行了一系列的初始化处理工作。下面将分别介绍,对于不是

很重要或是很好理解力的部分可能不作详细说明。 a) 首先是从命令行获取参数,打印参数的用法说明。

b) ngx_time_init()函数,获取当前系统的日期和时间。Nginx中定义了三个全局

ngx_str_t变量,ngx_cached_err_log_time、ngx_cached_http_time、

ngx_cached_http_log_time分别用来表示error.log中的时间格式,http协议中的时间格式,和accesse.log中的时间格式。另外,nginx中还分别定义了ngx_str_t 类型cached_err_log_time[NGX_TIME_SLOTS],

cached_http_time[NGX_TIME_SLOTS],cached_http_log_time[NGX_TIME_SLOTS]用来缓存系统的时间。还有static ngx_time_t cached_time[NGX_TIME_SLOTS]。 这些数组的长度为64。这此缓存数极组有什么用处,还不知道。 在函数最后,调用ngx_time_update()。

c) ngx_time_update()函数,通过ngx_gettimeofday()取得当前的毫秒数,其后逻辑

比较简单,只是根据获取的毫秒数通过一些系统调用获取年,月,日,时,分,秒以及按不同时区进行的转换。

d) 注意,在ngx_time_update()函数后部使用了一个ngx_memory_barrier()。这是个

宏。对不同的编译器不同的环境有不同的定义。关于x86和gcc的定义如下:

/*

* on x86 the write operations go in a program order, so we need only

* to disable the gcc reorder optimizations */

#define ngx_memory_barrier() __asm__ volatile (\"\" ::: \"memory\")

2. 用ngx_getpid()获取nginx的pid。

3. 如果宏NGX_OPENSSL定义了,在ngx_ssl_init()函数中载入open ssl的库。 4. 随后在全局的ngx_cycle变量中,创建内存池,大小为1024。 5. ngx_log_init()函数,先确定NGX_ERROR_LOG_PATH宏是否定义了errlog文件的路径,

如果没有定义,nginx会以stderr作为log输出文件,然后函数返回。

如果定义了errlog文件的路径,接着检查这个路径是不是绝对路径,Window系统中查看盘符,其他系统则盾根目录’/’;如果是相对路径,则会加上设定的或是NGX_PREFIX宏定义的前辍路径;最后以append模式打开路径中指定的log文件。如果打开log文件失败,则会再次将stderr设为log输出。(ngx_log全局变量中保存着nginx中log的相关信息,包括打开的log文件指针,log级别,处理函数等。)

struct ngx_log_s {

ngx_uint_t log_level; ngx_open_file_t *file;

ngx_atomic_uint_t connection;

ngx_log_handler_pt handler; void *data;

/*

* we declare \"action\" as \"char *\" because the actions are usually * the static strings and in the \"u_char *\" case we have to override * their types all the time */

char *action; };

6. 下面代码比较重要:

ngx_memzero(&init_cycle, sizeof(ngx_cycle_t)); init_cycle.log = log; ngx_cycle = &init_cycle; 后面对init_cycle的操作,实际上都是对ngx_cycle起作用。

7. ngx_save_argv()函数则是将从main()函数传入的参数表保存到nginx的全局变量

ngx_argc,ngx_argv[],ngx_os_argv,ngx_os_environ中。如果是freebsd系统,直接保存即可,否则nginx会创建新的参数数组保存命令行参数;

ngx_argcngx_argv[]NULLargvngx_os_argcNULL

8. ngx_process_options()函数,做下面工作:

a) 确定前缀路径,如果当前nginx系统已经取得了前缀路径,则将其保存在cycle变量(即init_cycle)中,同时也将该前缀作为conf文件的前缀路径;

b) 若还没有取得前缀路径,就立即设置前缀路径。这时如果NGX_PREFIX宏没有定义,就以当前工作路径作为前缀路径,同时也检查NGX_CONF_PREFIX宏,有定义则conf文件的前缀采用NGX_CONF_PREFIX宏中的定义。

c) 接着用ngx_conf_full_name()函数检查conf文件的路径是不是全路径

(ngx_conf_full_name()函数会调用ngx_conf_test_full_name()函数测试文件名的全称,如果不是全称则会试着用cycle中的conf_prefix来组合全称路径)。若不是全路径则函数返回错误;

d) 配置文件名是全路径时,从文件名字符串尾寻找第一个文件分隔符(‘/’或’\\’)。找到后,用ngx_cycle中保存的配置文件名替换当前cycle(即init_cycle)中的

conf_prefix,然后跳出循环;(由于init_cycle和ngx_cycle此时不过是别名,所以实质上是用ngx_cycle中的conf_file的名字放到conf_prefix中作为conf文件的前缀了。) e) ngx_conf_params存在时,将其保存到当前的cycle(即init_cycle)中; f) ngx_test_config有效时,当前cycle(即init_cycle)中log的log_level设为NGX_LOG_INFO。

9. ngx_os_init()函数,在NGX_HAVE_OS_SPECIFIC_INIT宏有效时,调用

ngx_os_specific_init()函数进行特定操作系统环境下的初进时化工作。在linux中,主要是获取当前操作系统发行版的全称,内核名称,类型,及操作最信号的限制等; 调用ngx_init_setproctitle()函数的逻辑如下:

a) 计算environ[]的大小,然后在内存池中分配足够大的空间; b) 将ngx_os_argv_last定位到ngx_os_argv[]尾部(即)enivorn[]起始处); c) 循环地将enivorn[]中环境变量复制到内存池已分配的空间中; d) ngx_os_argv_last最后定位到最后的环境变量。

调用getpagesize()函数取得pagesize大小;用NGX_CPU_CACHE_LINE常量设定ngx_cacheline_size。(NGX_CPU_CACHE_LINE未定义,可能不是宏,可能是通过编译前由configure脚本程序确定。)

下面这行代码的作用不明白: for (n = ngx_pagesize; n >>= 1; ngx_pagesize_shift++) { /* void */ }

ngx_ncpu为0,则设为1;接着调用ngx_cpuinfo()函数,取得cpu类型,制造商,所支持的cacheline的大小。

调用getrlimit(RLIMIT_NOFILE, &rlmt)函数,取得进程所能打开最大文件数。相关代码如下:

if (getrlimit(RLIMIT_NOFILE, &rlmt) == -1) {

ngx_log_error(NGX_LOG_ALERT, log, errno,

\"getrlimit(RLIMIT_NOFILE) failed)\"); return NGX_ERROR; }

ngx_max_sockets = (ngx_int_t) rlmt.rlim_cur;

接下来这段代码用来继承非阻塞属性: #if (NGX_HAVE_INHERITED_NONBLOCK) ngx_inherited_nonblocking = 1;

#else ngx_inherited_nonblocking = 0;

#endif 初始化随机种子(srandom(ngx_time()))。 10. ngx_crc32_table_init()函数中初始化用于crc32循环冗余校验的数据表。 11. ngx_add_inherited_sockets()函数中,继承父进程中的socket。继承方法是通过

取得”NGINX”这个环境变量的值,该值是个由”:”或”;”作为分隔符的列表,列表是表示socket的文件描述符。Nginx将继承的socket压入一个堆栈中,然后置变量ngx_inherited为1,表示已经取得要继承的socket。

最后调用ngx_set_inherited_sockets()函数。ngx_set_inherited_sockets()函数,由于是在ngx_add_inherited_sockets()函数最后调用,实际上对继承来的socket进行操作。那些socket,则保存在init_cycle的listening数组中。Listening结构如下:

struct ngx_listening_s { ngx_socket_t fd; /* socket所用的文件描述符 */

struct sockaddr *sockaddr; /* 指向sockaddr地址 */ socklen_t socklen; /* size of sockaddr */

size_t addr_text_max_len; /* 地址的文本表示的最大长度 */ ngx_str_t addr_text; /* 地址的文本表示 */

int type; /* */

int backlog; /* listen()中使用的参数,默认511。 */ int rcvbuf; /* 接受缓冲区的大小 */

int sndbuf; /* 发送缓冲区的大小 */

/* handler of accepted connection */ ngx_connection_handler_pt handler;

void *servers;/* array of ngx_http_in_addr_t, for example */

ngx_log_t log; ngx_log_t *logp;

size_t pool_size;

/* should be here because of the AcceptEx() preread */ size_t post_accept_buffer_size; /* should be here because of the deferred accept */ ngx_msec_t post_accept_timeout;

ngx_listening_t *previous; ngx_connection_t *connection;

unsigned open:1; /* socket已经打开 */ unsigned remain:1; /* */ unsigned ignore:1; /* */

unsigned bound:1; /* already bound */

unsigned inherited:1; /* inherited from previous process */ unsigned nonblocking_accept:1; unsigned listen:1;

unsigned nonblocking:1;

unsigned shared:1; /* shared between threads or processes */ unsigned addr_ntop:1;

#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY) unsigned ipv6only:2; #endif

#if (NGX_HAVE_DEFERRED_ACCEPT)

unsigned deferred_accept:1; unsigned delete_deferred:1; unsigned add_deferred:1; #ifdef SO_ACCEPTFILTER

char *accept_filter; #endif #endif };

ngx_set_inherited_sockets()函数中只有一个循环,在循环中对init_cycle->listening数组中继承而来的socket加工处理。

a) 在内存池中预分配保存地址结构的空间;

b) 调用getsockname()函数获取与socket相关连的地址,如果出错设定ignore为1; c) 根据socket地址的类型,设置地址文本格式的最大长度;如果不是IPv4和IPv6

类型设定ignore为1;

d) 地址文本格式的最大长度加上端口的文本格式的最大长度;

e) 使用ngx_sock_ntop()函数,将socket绑定的地址转换为文本格式(IPv4和IPv6

的不相同);

f) 设置每个监听的socket的backlog(NGX_LISTEN_BACKLOG定义为511);

g) 获取SO_RCVBUF和SO_SNDBUF选项的大小,保存与当前socket对应的

ngx_listening_t结构中; h) 支持accept filter时,通过SO_ACCEPTFILTER选项取得socket的accept_filter表,

保存在对应项的accept_filter中;

SO_ACCEPTFILTER places an accept_filter on the socket, which will filter incoming connections on a listening stream socket before being presented for accept. Once more, listen must be called on the socket before trying to install the filter on it, or else the setsockopt system call will fail.

struct accept_filter_arg { char af_name[16]; char af_arg[256-16]; };

i)

如果当前所在操作系统TCP层支持TCP DEFER ACCEPT功能,则试图获取TCP_DEFER_ACCEPT的timeout值。Timeout大于0时,则将socket对应deferred_accept标志设为1。

TCP_DEFER_ACCEPT

我 们首先考虑的第1个选项是TCP_DEFER_ACCEPT(这是Linux系统上的叫法,其他一些操作系统上也有同样的选项但使用不同的名字)。为了理 解TCP_DEFER_ACCEPT选项的具体思想,我们有必要大致阐述一下典型的HTTP客户/服务器交互过程。请回想下TCP是如何与传输数据的目标 建立连接的。在网络上,在分离的单元之间传输的信息称为IP包(或IP 数据报)。一个包总有一个携带服务信息的包头,包头用于内部协议的处理,并且它也可以携带数据负载。服务信息的典型例子就是一套所谓的标志,它把包标记代 表TCP/IP协议栈内的特殊含义,例如收到包的成功确认等等。通常,在经过“标记”的包里携带负载是完全可能的,但有时,内部逻辑迫使TCP/IP协议 栈发出只有包头的IP包。这些包经常会引发讨厌的网络延迟而且还增加了系统的负载,结果导致网络性能在整体上降低。

现在服务器创建了一个套接 字同时等待连接。TCP/IP式的连接过程就是所谓“3次握手”。首先,客户程序发送一个设臵SYN标志而且不带数据负载的TCP包(一个SYN包)。服 务

器则以发出带SYN/ACK标志的数据包(一个SYN/ACK包)作为刚才收到包的确认响应。客户随后发送一个ACK包确认收到了第2个包从而结束连接 过程。在收到客户发来的这个SYN/ACK包之后,服务器会唤醒一个接收进程等待数据到达。当3次握手完成后,客户程序即开始把“有用的”的数据发送给服 务器。通常,一个HTTP请求的量是很小的而且完全可以装到一个包里。但是,在以上的情况下,至少有4个包将用来进行双向传输,这样就增加了可观的延迟时 间。此外,你还得注意到,在“有用的”数据被发送之前,接收方已经开始在等待信息了。

为了减轻这些问题所带来的影响,Linux(以及其他的 一些操作系统)在其TCP实现中包括了TCP_DEFER_ACCEPT选项。它们设臵在侦听套接字的服务器方,该选项命令内核不等待最后的ACK包而且 在第1个真正有数据的包到达才初始化侦听进程。在发送SYN/ACK包之后,服务器就会等待客户程序发送含数据的IP包。现在,只需要在网络上传送3个包 了,而且还显著降低了连接建立的延迟,对HTTP通信而言尤其如此。

对于那些支持deffered accept的操作系统,nginx会设臵这个参数来增强功能,设臵了这个参数,在 accept的时候,只有当实际收到了数据,才唤醒在accept等待的进程,可以减少一些无聊的上下文切换,如下: val = 5;

setsockopt(socket_fd, SOL_TCP, TCP_DEFER_ACCEPT, &val, sizeof(val)); kernel 在 val 秒之内还没有收到数据,不会继续唤醒进程,而是直接丢弃连接,如果connect之后立刻收到数据,kernel才创建数据套接口并唤醒在accept上等待的进程。

12. ngx_init_cycle()函数,这是个比较重要的函数,被main,

ngx_master_process_cycle,ngx_single_process_cycle 调用, 其中后两者是在reconfigure的时候被调用的。它主要工作是,初始化cycle是基于旧有的cycle进行的,比如这里的 init_cycle,会继承old cycle的很多属性, 比如log等, 但是同时会对很多资源重新分配,比如pool, shared mem, file handler, listening socket 等,同时清除旧有的cycle的资源。

注:ngx_master/single_process_cycle里面会对init_process进行调用,并且循环调

用 ngx_process_events_and_timers,其中里面会调用ngx_process_events(cycle, timer, flags); 对事件循环进行polling 时间一般默认为500 ms。

下面是ngx_init_cycle()函数的主要工作: a) 获取系统的当前时间,以及与时间相的操作;

b) 用NGX_CYCLE_POOL_SIZE(16384)定义的大小新建一个内存池,并在这个内存

池中新建一个cycle对象; c) 执行下面操作:

cycle->pool = pool; cycle->log = log;

cycle->new_log.log_level = NGX_LOG_ERR; cycle->old_cycle = old_cycle;

d) 从old_cycle中将conf_prefix复制到新cycle的conf_prefix中; e) 从old_cycle中将prefix复制到新cycle的prefix中; f) 从old_cycle中将conf_file复制到新cycle的conf_file中; g) 从old_cycle中将conf_param复制到新cycle的conf_param中; h) 在新cycle中创建pathes对象;

i) 计算old_cycle中open_files队列中打开文件的数目,若old_cycle中open_files

是空的,则假定打开文件数目为20;然后在新cycle中新建open_files队列; j) 在新cycle中创建shared_memory对象的方式与上面类似; k) 接着是在新cycle中创建listening对象; l) 接着创建conf_ctx对象;

m) 取得本机的主机名,保存在新cycle的hostname中;

n) 遍历加载的模块,仅调用核心模块中的create_conf钩子函数(参看后面的核心模块

结构),并将返回的非空结果保存在conf_ctx数组中对应位置(以ngx_modules[i]->index来定位);

o) 创建一个conf对象,包括其中的args数组,temp_pool,还有下面相关属性设置:

conf.ctx = cycle->conf_ctx; conf.cycle = cycle; conf.pool = pool; conf.log = log;

conf.module_type = NGX_CORE_MODULE; conf.cmd_type = NGX_MAIN_CONF;

p) 调用ngx_conf_param()函数,把命令行中的指令(-g directives)转换为配置

结构并把指针加入到cycle.conf_ctx中;

q) 接着调用ngx_conf_parse()函数把配置文件中的指令转换为配置结构并把指

针加入到cycle.conf_ctx中;

配置的更新主要是在ngx_conf_parse中进行的,这个函数中有一个for循环,每次循环调用ngx_conf_read_token取得一个配置指令(以;结尾),然后调用ngx_conf_handler处理这条指令,ngx_conf_handler每次都会遍历所有模块的指令集,查找这条配置指令并分析其合法性,如果指令正确,则会创建配置结构并把指针加入到 cycle.conf_ctx中,配置结构的赋值是调用该指令的钩子set完成的。

r) 南再次遍历加载的模块,仅调用核心模块中的init_conf钩子函数;

s) 如果ngx_process是NGX_PROCESS_SIGNALLER模式,函数返加新cycle; t) 接下来的代码,理解有点费力,从表面看是在调用核心配置文件函数,创建pid文

件;(注:pid文件中只记录后来管理进程的pid,不是启动最初那个进程。) u) ngx_test_lockfile函数,检测lockfile是否存在,不存在则失败跳出;如果存在

该函数会删除这个文件。(lock_file在ngx_core_module_init_conf()函数中获取,默认的宏是NGX_LOCK_PATH)

v) ngx_create_pathes()函数,主要用来创建目录,cycle中的pathes建成对应目录,

设置用户读、写、执行权限;

w) ngx_conf_open_file()函数,先在打开的文件名表中找error log文件,找到了则

返回打开的error log文件结构;如果还没有打开error log文件,将error log文件名添加到打开的文件列表中,若error log文件名不存在,则将stderr作为error log文件,否则打开文件的描述符为-1;最后error log文件的缓冲设为NULL; x) 遍历打开文件列表,打开列表中的文件,如果有失败则跳到fail:处,非Windows

环境下,调用fcntl()将每个打开的文件设为FD_CLOEXEC; y) 设定新log文件;

cycle->log = &cycle->new_log; pool->log = &cycle->new_log;

z) 遍历共享内存块列表,将old_cycle中共享内存块列表中数据迁移到新cycle中的

共享内存列表的对应内存块对象中,old_cycle中的共享内存块关闭; aa) 关闭没用的listening socket; bb) 关闭没用的文件;

cc) 销毁配置对象中和temp_pool;

dd) 进程是NGX_PROCESS_MASTER模式时且ngx_is_init_cycle(old_cycle)检测为真,

销毁old_cycle中的内存池,返回新cycle;

ee) ngx_cleaner_event不存在则重新创建,并reset ngx_old_cycles,

ngx_cleaner_event对象;

ff) ngx_cleaner_event对象没有设定定时器时,设置一个定时器;

gg) 最后是failed:分支,关闭打开的文件、监听的socket;销毁conf对象中的内存

池和临时内存池。

13. ngx_os_specific_status()函数中,仅仅是在特殊操作系统情况下记录操作系统名

称以及相关信息。 14. 计算nginx的模块数,同时设置每个模块的index值。 15. 如果执行nginx的命令行参数中设置了signal值,则进行ngx_signal_process()

处理。

ngx_signal_process()函数先取得自己的pid。(可能是从/proc目录下nginx的pid文件中读取的pid值。仅是猜测,目前还不清楚是不是这样。)

最后调用ngx_os_signal_process() 函数,来向pid代表的进程发送信号。 发送的信号都定义在一个全局信号表中:

ngx_signal_t signals[] = {

{ ngx_signal_value(NGX_RECONFIGURE_SIGNAL), \"SIG\" ngx_value(NGX_RECONFIGURE_SIGNAL), \"reload\

ngx_signal_handler },

{ ngx_signal_value(NGX_REOPEN_SIGNAL), \"SIG\" ngx_value(NGX_REOPEN_SIGNAL), \"reopen\

ngx_signal_handler },

{ ngx_signal_value(NGX_NOACCEPT_SIGNAL),

\"SIG\" ngx_value(NGX_NOACCEPT_SIGNAL), \"\

ngx_signal_handler },

{ ngx_signal_value(NGX_TERMINATE_SIGNAL), \"SIG\" ngx_value(NGX_TERMINATE_SIGNAL), \"stop\

ngx_signal_handler },

{ ngx_signal_value(NGX_SHUTDOWN_SIGNAL), \"SIG\" ngx_value(NGX_SHUTDOWN_SIGNAL), \"quit\

ngx_signal_handler },

{ ngx_signal_value(NGX_CHANGEBIN_SIGNAL), \"SIG\" ngx_value(NGX_CHANGEBIN_SIGNAL), \"\

ngx_signal_handler },

{ SIGALRM, \"SIGALRM\

{ SIGINT, \"SIGINT\

{ SIGIO, \"SIGIO\

{ SIGCHLD, \"SIGCHLD\

{ SIGSYS, \"SIGSYS, SIG_IGN\

{ SIGPIPE, \"SIGPIPE, SIG_IGN\

{ 0, NULL, \"\

};

其中元素的结构如下: typedef struct {

int signo; char *signame; char *name;

void (*handler)(int signo); } ngx_signal_t; 16. ngx_os_status()函数则是对应不同的操作系统,将不操作系统的内核版本,发

布版本等写出到log文件中。不同系统输出内容不一样。

17.

ngx_cycle = cycle;

ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);

if (ccf->master && ngx_process == NGX_PROCESS_SINGLE) { ngx_process = NGX_PROCESS_MASTER; } 18. 随后ngx_init_signals()函数中,针对singals表中的信号安装信号处理函数。

Nginx中的信号处理函数是ngx_signal_handler()。

在ngx_signal_handler()函数中,会检查收到的信号是不是nginx有效的信号,取得当前系统的时间。

接着根据不同的进程模式——NGX_PROCESS_MASTER,NGX_PROCESS_SINGLE,NGX_PROCESS_WORKER,对应不同信号进行相应处理。

NGX_PROCESS_MASTER与NGX_PROCESS_SINGLE模式下信号处理方式一致。其中NGX_CHANGEBIN_SIGNAL信号需要特别在意。

case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):

if (getppid() > 1 || ngx_new_binary > 0) {

/*

* Ignore the signal in the new binary if its parent is * not the init process, i.e. the old binary's process

* is still running. Or ignore the signal in the old binary's * process if the new binary's process is already running. */

action = \ ignore = 1; break; }

ngx_change_binary = 1; action = \ break; …

收到这个信号表示要进程切换,ngx_change_binary置为1。如果收到该信号的进程是下列两种情况,就忽略信号。 a) 新启动的进程,其父进程不是init;

b) 老的进程(即将终了的进程),其派生的子进程已经在运行了。 NGX_CHANGEBIN_SIGNAL信号用来restart起动nginx。Nginx是采用一种平滑进程切换的方式来重启。这种方式保证了不间断地处理请求。上面两种情况其实是指由将要退出的老进程生成新的进程。新生的进程对NGX_CHANGEBIN_SIGNAL信号不作处理。 19. 如果配置文件中设置了daemon运行模式且ngx_inherited为0。

调用ngx_daemon()将进程置为daemon运行模式。ngx_daemon()函数使用fork()和

setsid()系统调用将进程变成后台进程。 20. ngx_create_pidfile()函数将pid写入文件。 21. 将标准错误输出重定向到当前打开的log文件。 22. ngx_use_stderr = 0; 23. 根据进程模式是NGX_PROCESS_SINGLE还是NGX_PROCESS_MASTER分别调用

ngx_single_process_cycle(),ngx_master_process_cycle()。 24. ngx_single_process_cycle() 函数,其逻辑相对比较简单。

a) 遍历所有模块的,如果模块有init钩子函数,调用之;

b) 接下来进入一个循环,先调用ngx_process_events_and_timers(),猜测是进行

nginx事件处理,后面再分析。

c) 随后,如果是终结或是退出状态,先调用各模块中的exit_process钩子函数,接

着调用ngx_master_process_exit()函数退出程序;

d) ngx_reconfigure标志为1,重新创建并初始化ngx_cycle对象; e) ngx_reopen标志为1,重新打开log文件。

ngx_master_process_exit() 函数先删除保存pid的文件,再调用各个模块中exit_master钩子函数,最后销毁内存池对象。

25. ngx_master_process_cycle() 函数,这个函数会启动工作进程干活,并且会

处理信号量,处理的过程中会杀死或者创建新的进程。 a) 阻塞所有nginx关心的信号;

b) 设置进程的title(有点类似命令行形式的一个字符串,应该不是很重要。); c) 启动一个work process进程; d) 启动一个缓冲管理进程;

e) 初始化几个标志:ngx_new_binary = 0; delay = 0; live = 1;

f) 后面一个循环对不同的状态进行不同处理,而那些状态多数是进程收到的不同

信号。下面是各个处理的详解: a) delay不为0,如果收到SIGALRM信号ngx_sigalrm设为1,将delay时间

乘以2;最后设置一个实时类型的计时器;

b) 挂起当前进程,等到有信号,就会从挂起状态退出,继续执行; c) 退出挂起状态后,根据操作系统时间重新更新当前时间;

d) ngx_reap为1(收到SIGCHLD信号,有worker退出(ngx_reap==1)),调用

ngx_reap_children()回收子进程;

e) 如果子进程都退出了(!live)且当前进程收到

ngx_signal_value(NGX_SHUTDOWN_SIGNAL)或

ngx_signal_value(NGX_TERMINATE_SIGNAL)信号,本进程进行退出处理(ngx_master_process_exit());退出处理先删除pid文件,然后将调用所有模块的进程退出钩子,销毁内存池对象;

f) 如果ngx_terminate为1,delay为0,就设成50;如果delay>1000,向

work进程发送SIGKILL信号,否则向work进程发送ngx_signal_value(NGX_TERMINATE_SIGNAL)信号; g) 如果ngx_quit为1,向work进程发送

ngx_signal_value(NGX_SHUTDOWN_SIGNAL)信号,然后将所有全局listening中的socket全关闭;continue;

h) 如果ngx_reconfigure为1(ngx_signal_value(NGX_RECONFIGURE_SIGNAL)

信号对应),就重新读取config文件;重新创建并初始化ngx_cycle对象,启动work进程,启动缓冲管理进程,将live设为1,调用ngx_signal_worker_processes()发送

ngx_signal_value(NGX_SHUTDOWN_SIGNAL)信号;

i) ngx_new_binary为1(表示是新启动的一个进程),启动work进程,启动

缓冲管理进程,然后将ngx_noaccepting设为0;continue;

j) 如果ngx_restart为1(当ngx_noaccepting=1的时候会把ngx_restart

设为1,重启worker),启动work进程,启动缓冲管理进程,live设为1; k) 如果ngx_reopen为1(ngx_signal_value(NGX_REOPEN_SIGNAL)信号对

应),则重新找开log文件,调用ngx_signal_worker_processes()发送ngx_signal_value(NGX_REOPEN_SIGNAL)信号;

l) 如果ngx_change_binary为1(ngx_signal_value(NGX_CHANGEBIN_SIGNAL)

信号对应),调用ngx_exec_new_binary()执行新进程;

m) 如果ngx_noaccept为1(ngx_signal_value(NGX_NOACCEPT_SIGNAL)对

应),设ngx_noaccepting为1,调用ngx_signal_worker_processes()发送ngx_signal_value(NGX_SHUTDOWN_SIGNAL)信号。

26. ngx_start_worker_processes()函数,这个函数按指定数目n,以

ngx_worker_process_cycle()函数为参数调用ngx_spawn_process()创建work进程并初始化相关资源和属性;执行子进程的执行函数ngx_worker_process_cycle;向之前已经创建的所有worker广播当前创建的worker进程的信息;每个进程打开一个通道(ngx_pass_open_channel())。

ngx_start_worker_processes()函数的主要逻辑如下:

a) 首先要说明的是参数respawn有两种含义:type 即创建新进程式的方式,如

NGX_PROCESS_RESPAWN, NGX_PROCESS_JUST_RESPAWN„,是负值;另一种表示进程信息表的下标,此时是非负;

b) 查找ngx_processes[]进程信息表中一个空项;

c) 若不是分离的子进程(respawn != NGX_PROCESS_DETACHED),

i) 创建一对已经连接的无名socket;

ii) 设置socket(channel[0]和channel[1]))为非阻塞模式; iii) 开启channel[0]的消息驱动IO; iv) 设置channel[0]的属主,控制channel[0]的SIGIO信号只发给这个进程; v) 设置channel[0]和channel[1]的FD_CLOEXEC属性(进程执行了exec后关闭socket);

vi) 取得用于监听可读事件的socket(即ngx_channel = ngx_processes[s].channel[1];);

d) 分离的子进程时,ngx_processes[s].channel[0] = -1;

ngx_processes[s].channel[1] = -1;

e) 设置当前子进程的进程表项索引;ngx_process_slot = s; f) 创建子进程;

g) 子进程式中,设置当前子进程的进程id,运行执行函数; h) respawn >= 0时,直接返回pid; i) 设置其他的进程表项字段;

ngx_processes[s].proc = proc; ngx_processes[s].data = data; ngx_processes[s].name = name; ngx_processes[s].exiting = 0;

j) 根据respawn表示的类型,按不同方式设置表项。

创建socketpair用于进程间通信,master进程为每个worker创建一对socket, master进程空间打开所有socketpair的channel[0],channel[1]两端句柄。 当创建一个worker的时候,这个worker会继承master当前已经创建并打开的所有socketpair,这个worker初始化的时候(调用ngx_worker_process_init)会关闭掉本进程对应socketpair的channel[0]和其他worker对应的channel[1],保持打开本进程对应socketpair的channel[1]和其他worker对应的channel[0],并监听本进程对应socketpair的channel[1]的可读事件。这样,每个worker就拥有了其他worker的channel[0],可以sendmsg(channel[0], ...)向其他worker发送消息。 先于当前worker创建的worker通过继承得到了其channel[0],但是之后创建的进程的channel[0]该如何获得呢,答案在上面(ngx_start_worker_processes) master在创建并启动完成一个worker之后,会调用ngx_pass_open_channel 把这个worker的channel[0]和进程id、在进程表中的偏移slot广播(ngx_write_channel())给所有其他已经创建的worker,这样,创建完所有进程之后,每个worker就获得了所有其他worker的channel[0]了。

ngx_worker_process_cycle()函数是每个进程的实际工作内容。这个函数中首先调用ngx_create_thread()初始化各线程。我们知道每个线程都有一个启动处理函数,nginx的线程处理函数为ngx_worker_thread_cycle(),内部过程中最重要的是对ngx_event_thread_process_posted()函数的调用,用于实际处理每一次请求。 初始化线程结束后,首先调用ngx_process_events_and_timers()函数,该函数继续调用ngx_process_events接口监听事件,一般情况下对应的函数是ngx_epoll_process_events(),如果使用的是其它种 类的IO模型,则应该实现相应的实际函数。这个接口负责把事件投递到ngx_posted_events事件队列里,并在 ngx_event_thread_process_posted()函数中进行处理。

27. ngx_start_cache_manager_processes()函数,这个函数在ngx_cycle全局对

象的path数组中,检查是否有manager和loader函数。有manage函数,创建缓冲管理进程,打开相应的通道,负责管理工作;有loader函数,创建缓冲管理进程,打开相应通道,负责载入工作。 28. ngx_pass_open_channel()函数,这个函数在全局的ngx_processes数组中所

有pid不为-1,channel[0]不为-1的进程打开通道。也就是为创建的每个work进程打开一个通道,发送消息(ngx_write_channel())给工作进程。 29. ngx_signal_worker_processes()函数,这个函数主要是向各个work进程发送

一个信号。如果是ngx_signal_value(NGX_REOPEN_SIGNAL)信号,设ngx_processes中的标志exiting为1。 30. ngx_reap_children()函数,这个函数清理要退出的进程。ngx_processes数

组中每个项保存对应一个进程的信号,有pid,status,channel等。如果有子进程还没有退出exiting或! detached,则live设为1。 31. ngx_master_process_exit()函数,这个函数删除pid文件,接着调用模块中

的exit_master钩子函数,销毁内存池。

managerwork...work

Work进程逻辑(ngx_worker_process_cycle()函数)

Nginx的工作进程中嵌入了线程逻辑。下面三个部份按照代码的逻辑来说明的。如果不支持线程,线程部份代码编译时不会生成。

1.进程部份

一切从ngx_worker_process_init()函数开始:

1.先调用ngx_set_environment()函数为本进程设定环境变量,那些环境变量都是从cycle中继承过来的;

2. 有进程执行优先权,则调用setpriority(PRIO_PROCESS, 0, ccf->priority)函数设置优先权;

3. 设置进程可打开最大文件数(setrlimit(RLIMIT_NOFILE, &rlmt)); 4. 设置内核转存文件最大长度(setrlimit(RLIMIT_CORE, &rlmt)); 5. 支持RLIMIT_SIGPENDING的情况下,设置

RLIMIT_SIGPENDING(setrlimit(RLIMIT_SIGPENDING, &rlmt)); 6. 支持PR_SET_DUMPABLE时,设置PR_SET_DUMPABLE(prctl(PR_SET_DUMPABLE,1,0,0,0)); 7. 换到当前工作的目录下; 8. 清空所有的信号;

9. 清掉监听socket上以前的事件;

10. 调用所的模块的init_process钩子函数; 11. 将其他进程的channel[1]关闭,自己的除外; 12. 将自己的channel[0]关闭;

13. 调用ngx_add_channel_event()函数,给ngx_channel(在

ngx_start_worker_processes()函数中,ngx_channel = ngx_processes[s].channel[1];,所以ngx_channel就是进程自身的channel[1],用来读取的socket。)注册一个读事件处理

函数;

ngx_add_channel_event()函数的主要工作: 1. 取得一个空闲的connection资源;

2. 创建了一个读事件和一个写事件对象,并设置了channel为1;

3. 检查注册的事件是读还是写,分别选用不同的事件对象,安装事件处理函数;

4. epoll事件模式并且ngx_event_actions.add_conn存在时,调用add_conn添加一个connection资源;否则用ngx_event_actions.add添加一个事件对象;

ngx_channel_handler()函数,这个函数作为事件驱动: 1. 事件对象中的timedout清空;

2. 从事件对象中取得connection对象;

3. 进入循环,调用ngx_read_channel()函数,读取channel对象(还记得什么地方用ngx_write_channel()函数写向子进程写入一个channel对象吗?); 4. NGX_USE_EVENTPORT_EVENT事件标志时,添加读事件;

5. 接下来是检查取得的channel对象中的command,不同的命令不同的处理: a) NGX_CMD_QUIT: ngx_quit = 1; b) NGX_CMD_TERMINATE: ngx_terminate = 1; c) NGX_CMD_REOPEN: ngx_reopen = 1; d) NGX_CMD_OPEN_CHANNEL: 在ngx_processes[]表中指定项中设置pid和写文件描述符; e) NGX_CMD_CLOSE_CHANNEL: 关闭ngx_processes[]表中指定表项中的写文件描述符。

2.线程部份

如果支持线程会执行下面的工作(当然线程数不能为0): 1.调用ngx_init_threads()函数,设定线程所用堆栈大小; 2.用ngx_thread_key_create()函数,创建线程所用的key; 3.创建ngx_create_thread()函数,创建线程;

上面这组关于线程的函数,是对不同API的包装,如freebsd下,和posix的。相对简单明了,不作详解了。

ngx_worker_thread_cycle()函数是线程的工作函数。

1.阻塞NGX_RECONFIGURE_SIGNAL、NGX_REOPEN_SIGNAL、NGX_CHANGEBIN_SIGNAL三个信号; 2.设置线程的title——“worker thread”; 3.创建线程的私有存贮空间;

4.接下来主要是循环处理事件,先假定是线程是空闲的,一旦取得一个事件调用ngx_event_thread_process_posted()函数处理(这儿有疑问,不知道为什么要连着调用两次ngx_event_thread_process_posted()函数),这时线程是忙碌状态,最后

ngx_event_actions.process_change()(目前不清楚是什么作用)存在,则调用之。 如果发现ngx_terminate为真,线程退出。

ngx_event_thread_process_posted()函数,是一个事件驱动处理器。主逻辑比较好理解,但是细节部份没有注解,加上有些宏或函数与芯片架构有关,所以细节不好理解。

其主要工作,是从事件队列(ngx_posted_events)中取一个事件对象,调用事件对象中的handler()处理这个事件。由于是多线程模式,所以取事件时采用排他锁。这里要注意,排他锁的相关函数,是针对不同环境,自己写代码实现的,没有用api。可能是为了提高效率。

3.回到进程

由于在ngx_channel_handler()函数中对应收到的命令,设置了

ngx_quit,ngx_terminate,ngx_reopen这些变量,于是在这里进入处理循环,分别处理之。 1. ngx_exiting: 试着关掉所在打着的connection对象。如果其中还有数据没读,试着读取完; 事件没有了(ngx_event_timer_rbtree.root == ngx_event_timer_rbtree.sentinel),调用ngx_worker_process_exit()退出;

2.调用ngx_process_events_and_timers()函数,

3. ngx_terminate: 调用ngx_worker_process_exit()退出;

ngx_worker_process_exit()函数调用各个模块中的exit_process钩子函数;ngx_exiting状态下会在log中记录那些打开的等待读的不正常的connection;销毁内存池;退出。

4. ngx_quit:ngx_setproctitle(\"worker process is shutting down\"); if (!ngx_exiting) {

ngx_close_listening_sockets(cycle); /*关闭监听中的socket*/ ngx_exiting = 1; }

5. ngx_reopen:调用ngx_reopen_files()函数。

Cycle

1.

2.

Connection

connection的内存分布

connection的分配与回收

nginx依据worker_connections的设置,一次性在pool当中初始化一定数量的ngx_connection_t结构,如1024个。这个过程是在ngx_event_process_init()函数中完成的。初始化的时候令所有的c->data指向下一个connection直到最后一个指向NULL。

当我们需要获取一个连接的时候会调用ngx_get_connection()函数获取一个未被使用的ngx_connection_t结构,实际上就是调用一个特别的指针ngx_cylcle->free_connections所指向的位置。

第一次申请的时候被指向位置0,这样位置0被取出使用。连续3次使用

ngx_get_connection()方法3个connction被使用ngx_cycle->free_connections指向下一个connection。

我们假设1位置的connection首先调用ngx_free_connection()获得释放,这时候位置1的data指针跳过位置2指向位置3。自己成为free_connections。如果这时候需要继续分配connection,会分配1,再分配3,后面的顺序如果还没有被打乱过。那么继续分配。

如果继续释放,那么data指针会根据释放的顺序,重新分配指针,但总保证这是一个完全连接着的单向链表结构。

ngx_get_connection()函数实际上是根据,free_connections来获取连接,因此并不需要实际释放这些内存,而通过指针的方式重复利用这些结构体。

Event

结构

struct ngx_event_s {

void *data;

unsigned write:1;

unsigned accept:1;

/* used to detect the stale events in kqueue, rtsig, and epoll */ unsigned instance:1;

/*

* the event was passed or would be passed to a kernel; * in aio mode - operation was posted. */

unsigned active:1;

unsigned disabled:1;

/* the ready event; in aio mode 0 means that no operation can be posted */ unsigned ready:1;

unsigned oneshot:1;

/* aio operation is complete */ unsigned complete:1;

unsigned eof:1; unsigned error:1;

unsigned timedout:1; unsigned timer_set:1;

unsigned delayed:1;

unsigned read_discarded:1;

unsigned unexpected_eof:1;

unsigned deferred_accept:1;

/* the pending eof reported by kqueue or in aio chain operation */ unsigned pending_eof:1;

#if !(NGX_THREADS)

unsigned posted_ready:1; #endif

#if (NGX_WIN32)

/* setsockopt(SO_UPDATE_ACCEPT_CONTEXT) was succesfull */ unsigned accept_context_updated:1; #endif

#if (NGX_HAVE_KQUEUE)

unsigned kq_vnode:1;

/* the pending errno reported by kqueue */ int kq_errno; #endif

/*

* kqueue only:

* accept: number of sockets that wait to be accepted * read: bytes to read when event is ready

* or lowat when event is set with NGX_LOWAT_EVENT flag * write: available space in buffer when event is ready

* or lowat when event is set with NGX_LOWAT_EVENT flag *

* iocp: TODO *

* otherwise:

* accept: 1 if accept many, 0 otherwise */

#if (NGX_HAVE_KQUEUE) || (NGX_HAVE_IOCP) int available; #else

unsigned available:1; #endif

ngx_event_handler_pt handler;

#if (NGX_HAVE_AIO)

#if (NGX_HAVE_IOCP)

ngx_event_ovlp_t ovlp; #else

struct aiocb aiocb; #endif

#endif

ngx_uint_t index;

ngx_log_t *log;

ngx_rbtree_node_t timer;

unsigned closed:1;

/* to test on worker exit */ unsigned channel:1; unsigned resolver:1;

#if (NGX_THREADS)

unsigned locked:1;

unsigned posted_ready:1; unsigned posted_timedout:1; unsigned posted_eof:1;

#if (NGX_HAVE_KQUEUE)

/* the pending errno reported by kqueue */ int posted_errno; #endif

#if (NGX_HAVE_KQUEUE) || (NGX_HAVE_IOCP) int posted_available; #else

unsigned posted_available:1; #endif

ngx_atomic_t *lock;

ngx_atomic_t *own_lock;

#endif

/* the links of the posted queue */ ngx_event_t *next; ngx_event_t **prev; #if 0

/* the threads support */

/*

* the event thread context, we store it here

* if $(CC) does not understand __thread declaration * and pthread_getspecific() is too costly */

void *thr_ctx;

#if (NGX_EVENT_T_PADDING)

/* event should not cross cache line in SMP */

uint32_t padding[NGX_EVENT_T_PADDING]; #endif #endif };

typedef struct {

ngx_uint_t lock;

ngx_event_t *events; ngx_event_t *last; } ngx_event_mutex_t;

typedef struct {

ngx_int_t (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags); ngx_int_t (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

ngx_int_t (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags); ngx_int_t (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

ngx_int_t (*add_conn)(ngx_connection_t *c);

ngx_int_t (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);

ngx_int_t (*process_changes)(ngx_cycle_t *cycle, ngx_uint_t nowait); ngx_int_t (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags);

ngx_int_t (*init)(ngx_cycle_t *cycle, ngx_msec_t timer); void (*done)(ngx_cycle_t *cycle); } ngx_event_actions_t;

相关函数

Connection

结构

typedef struct ngx_listening_s ngx_listening_t;

struct ngx_listening_s {

ngx_socket_t fd;

struct sockaddr *sockaddr;

socklen_t socklen; /* size of sockaddr */ size_t addr_text_max_len;

ngx_str_t addr_text;

int type;

int backlog; int rcvbuf; int sndbuf;

/* handler of accepted connection */ ngx_connection_handler_pt handler;

void *servers; /* array of ngx_http_in_addr_t, for example

*/

ngx_log_t log; ngx_log_t *logp;

size_t pool_size;

/* should be here because of the AcceptEx() preread */ size_t post_accept_buffer_size; /* should be here because of the deferred accept */ ngx_msec_t post_accept_timeout;

ngx_listening_t *previous; ngx_connection_t *connection;

unsigned open:1; unsigned remain:1; unsigned ignore:1;

unsigned bound:1; /* already bound */

unsigned inherited:1; /* inherited from previous process */ unsigned nonblocking_accept:1; unsigned listen:1;

unsigned nonblocking:1;

unsigned shared:1; /* shared between threads or processes

*/

unsigned addr_ntop:1;

#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY) unsigned ipv6only:2; #endif

#if (NGX_HAVE_DEFERRED_ACCEPT)

unsigned deferred_accept:1; unsigned delete_deferred:1; unsigned add_deferred:1; #ifdef SO_ACCEPTFILTER

char *accept_filter; #endif #endif };

typedef enum {

NGX_ERROR_ALERT = 0, NGX_ERROR_ERR, NGX_ERROR_INFO,

NGX_ERROR_IGNORE_ECONNRESET, NGX_ERROR_IGNORE_EINVAL } ngx_connection_log_error_e;

typedef enum {

NGX_TCP_NODELAY_UNSET = 0, NGX_TCP_NODELAY_SET,

NGX_TCP_NODELAY_DISABLED } ngx_connection_tcp_nodelay_e;

typedef enum {

NGX_TCP_NOPUSH_UNSET = 0, NGX_TCP_NOPUSH_SET,

NGX_TCP_NOPUSH_DISABLED } ngx_connection_tcp_nopush_e;

#define NGX_LOWLEVEL_BUFFERED 0x0f #define NGX_SSL_BUFFERED 0x01

struct ngx_connection_s {

void *data; ngx_event_t *read; ngx_event_t *write;

ngx_socket_t fd;

ngx_recv_pt recv; ngx_send_pt send;

ngx_recv_chain_pt recv_chain; ngx_send_chain_pt send_chain;

ngx_listening_t *listening;

off_t sent;

ngx_log_t *log;

ngx_pool_t *pool;

struct sockaddr *sockaddr; socklen_t socklen; ngx_str_t addr_text;

#if (NGX_SSL)

ngx_ssl_connection_t *ssl; #endif

struct sockaddr *local_sockaddr; socklen_t local_socklen;

ngx_buf_t *buffer;

ngx_atomic_uint_t number;

ngx_uint_t requests;

unsigned buffered:8;

unsigned log_error:3; /* ngx_connection_log_error_e */

unsigned single_connection:1; unsigned unexpected_eof:1; unsigned timedout:1; unsigned error:1; unsigned destroyed:1;

unsigned idle:1; unsigned close:1;

unsigned sendfile:1; unsigned sndlowat:1;

unsigned tcp_nodelay:2; /* ngx_connection_tcp_nodelay_e

*/

unsigned tcp_nopush:2; /* ngx_connection_tcp_nopush_e */

#if (NGX_HAVE_IOCP)

unsigned accept_context_updated:1; #endif

#if (NGX_HAVE_AIO_SENDFILE)

unsigned aio_sendfile:1; ngx_buf_t *busy_sendfile; #endif

#if (NGX_THREADS)

ngx_atomic_t lock; #endif };

相关函数

ngx_create_listening()函数,这个函数创建监听的地址。 ngx_set_inherited_sockets()函数,函数中取得socket中的SO_RCVBUF和SO_SNDBUF对应参数,支持accept filter时,取得SO_ACCEPTFILTER选项参数,支持TCP_DEFER_ACCEPT时,取得相应参数。 ngx_open_listening_sockets()函数,创建一个socket,将其设为重用(SO_REUSEADDR),如果操作系统中不支持异步IO,将这个socket设为非阻塞模式,将socket绑定到地址,最后监听该socket。

ngx_configure_listening_sockets()函数,这个函数设置障接受缓冲区与发送缓冲区大小,如果listen为1,再次监听socket;如果系统支持accept filter,设置accept filter参数,如果支持TCP_DEFER_ACCEPT,设置相应参数。

Connection与Event

nginx的connection和event是按照链表的方式进行存放的,区别在于connection是单向链表而event是双向链表。

connection链表是提前分配的,定义在ngx_event_process_init()函数内,具体代码如下:

...

cycle->connections = ngx_alloc(sizeof(ngx_connection_t) * ecf->connections,

cycle->log);

...

i = cycle->connection_n; next = NULL;

do { i--;

c[i].data = next;

c[i].read = &cycle->read_events[i]; c[i].write = &cycle->write_events[i]; c[i].fd = (ngx_socket_t) -1;

next = &c[i];

#if (NGX_THREADS)

c[i].lock = 0; #endif

} while (i);

cycle->free_connections = next;

cycle->free_connection_n = ecf->connections;

在内存池里为所有connection分配空间,并依次初始化各连接的链表关系,也就是在data上存下一个connection的指针。

在具体应用中通过ngx_get_connection()函数取出空闲的connection并使用。

至于event是一个双向链表,链表的入队和出队有下面的定义:

#define ngx_locked_post_event(ev, queue) \\

if (ev->prev == NULL) { \\

ev->next = (ngx_event_t *) *queue; \\ ev->prev = (ngx_event_t **) queue; \\ *queue = ev; \\ \\ if (ev->next) { \\

ev->next->prev = &ev->next; \\ } \\

\\

ngx_log_debug1(NGX_LOG_DEBUG_CORE, ev->log, 0, \"post event %p\ev); \\ } else { \\

ngx_log_debug1(NGX_LOG_DEBUG_CORE, ev->log, 0, \"update posted event %p\

ev); \\ }

#define ngx_post_event(ev, queue) \\

ngx_mutex_lock(ngx_posted_events_mutex); \\ ngx_locked_post_event(ev, queue); \\

ngx_mutex_unlock(ngx_posted_events_mutex);

#define ngx_delete_posted_event(ev) \\ *(ev->prev) = ev->next; \\ \\ if (ev->next) { \\

ev->next->prev = ev->prev; \\ } \\ \\

ev->prev = NULL; \\

ngx_log_debug1(NGX_LOG_DEBUG_CORE, ev->log, 0, \"delete posted event %p\ev);

简单说 ngx_post_event 用于插入事件、ngx_delete_posted_event 用于删除事件。这两个是宏定义,会比函数定义用起来节省。

整个程序就围绕这event和connection进行。不断的投入、拿出。

Bufs

和缓冲区有关的最基本的是三个结构:

ngx_buf_t、ngx_chain_t、ngx_bufs_t

分别代表 缓冲区、链表、缓冲区尺寸。

一块缓存区可以存放多段数据,因此在nginx中缓存结构被这样定义:

struct ngx_buf_s {

u_char *start; //缓冲区首部 u_char *end; //缓冲区尾部

u_char *pos; //当前指针所在位置 u_char *last; //有效数据的尾部 ... }

按照这种道理end永远不小于last,例如缓存区一共申请了10个单元,而目前只填充了6个单元,那么last为6,end为10。

为了区分是那种类型的缓冲,在这个结构中还有一些标识,例如:

ngx_file_t *file; //文件结构指针 unsigned memory:1; //是否在内存中 unsigned mmap:1; //内存中的文件映射 unsigned recycled:1; //被回收 unsigned in_file:1; //文件缓冲 unsigned flush:1; //被清除 unsigned sync:1; //异步

unsigned last_in_chain:1; //链表的尾部

unsigned temp_file:1; //是否是临时文件中的缓冲

通过一个8位存储了一个缓冲区的8种状态,在C语言里这种方式比较经济节省。在高级语言里,是尝不到这种便宜的。

链表结构ngx_chain_t是单向链表结构。

ngx_http_upstream_s是nginx一个核心数据结构,用于负载均衡的实现。 在这个结构中使用了若干缓存有关的子结构,例如:

ngx_buf_t、ngx_chain_t、ngx_output_chain_ctx_t等,具体的结构有

ngx_chain_t *out_bufs; ngx_chain_t *busy_bufs; ngx_chain_t *free_bufs; ngx_chain_t *request_bufs;

因此一般的缓存会以单向链表的形式出现。

对于特定的缓存也会出现对应的缓存结构,例如:

typedef struct {

ngx_buf_t *buf; ngx_chain_t *in;

ngx_chain_t *free; ngx_chain_t *busy;

unsigned sendfile;

unsigned need_in_memory; unsigned need_in_temp;

ngx_pool_t *pool;

ngx_int_t allocated; ngx_bufs_t bufs; ngx_buf_tag_t tag;

ngx_output_chain_filter_pt output_filter; void *filter_ctx; } ngx_output_chain_ctx_t;

Upstream

目前 nginx只是五种路由方式:

1. 轮询(默认) 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务

器down掉,能自动剔除。

2. Weight 指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情

况。

3. ip_hash 每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服

务器,可以解决session的问题。

一般做负载均衡,都需要后端多台web服务器之间实现session共享,否则用户登录可能就有问题了。nginx可以根据客户端IP进行负载均衡,在upstream里设置ip_hash,就可以针对同一个C类地址段中的客户端选择同一个后端服务器,除非那个后端服务器宕了才会换一个。

nginx文档中的原文如下: The key for the hash is the class-C network address of the client. This method guarantees that the client request will always be forwarded to the same server. But if this server is considered inoperative, then the request of this client will be transferred to another server. This gives a high probability clients will always connect to the same server.

也就是说我可以在两台服务器上跑两个论坛,但共享一个后台数据库,而不用去关心session共享的问题,前面启用ip_hash,正常情况下,客户端上网 获得IP,登录浏览发帖等都会被转到固定的后端服务器上,这样就不会出问题了。应该说来访IP分布越广,负载均衡就越平均。嘿嘿,如果都是同一个C来的用 户,那就没

用了。

4. fair(第三方)按后端服务器的响应时间来分配请求,响应时间短的优先分配。 5. url_hash(第三方)按访问url的hash结果来分配请求,使每个url定向到同一个后

端服务器,后端服务器为缓存时比较有效。

因篇幅问题不能全部显示,请点此查看更多更全内容

Top