目录
看一下大名鼎鼎的nginx,学学别人是怎么设计一个服务器的
可以看看这位博主的文章,从nginx的数据结构开始剖析,讲得很好https://blog.csdn.net/chen19870707
Nginx设计时重视的几个关键点:
- 性能:包括网络性能,单词请求的延迟性,网络效率;(名词就不解释了~)
- 可伸缩性:可通过添加组件来提升服务,或者允许组件之间具有交互功能;
- 简单性:组件的简单程度,便于理解和实现;
- 可修改性:包括可进化性,可扩展性,可定制性,可配置性,可重用性;
- 可见性:可监控关键组件的运行情况;
- 可移植性:跨平台运行;
- 可靠性:在服务出现故障时,一个架构容易受到系统层面故障影响的程度;
架构:
1.1. 主进程
Nginx 启动时,会生成两种类型的进程,一个是 主进程 ( master ), 一个 ( windows版本的目前只有一个)或 多个工作进程 ( worker )。 主进程 并不处理网络请求,主要负责 调度工作进程 ,也就是图示的 3 项: 加载配置 、 启动工作进程 及 非停升级 。所以, Nginx 启动以后,查看操作系统的进程列表,我们就能看到 至少有两个 Nginx 进程。
1.2. 工作进程
服务器实际 处理网络请求 及 响应 的是 工作进程 ( worker ),在类 unix 系统上, Nginx可以配置 多个 worker ,而每个 worker 进程 都可以同时处理 数以千计 的 网络请求 。
1.3. 模块化设计
Nginx 的 worker 进程,包括核心和功能性模块 ,核心模块负责维持一个运行循环 ( run-loop ),执行网络请求处理的不同阶段的模块功能,比如: 网络读写 、 存储读写 、 内容传输 、 外出过滤 ,以及将请求发往上游服务器 等。而其代码的模块化设计 ,也使得我们可以根据需要对功能模块进行适当的选择和修改 ,编译成具有特定功能的服务器。
1.4. 事件驱动模型
基于异步及非阻塞的事件驱动模型 ,可以说是 Nginx 得以获得高并发 、 高性能的关键因素,同时也得益于对 Linux 、 Solaris 及类 BSD 等操作系统内核中事件通知 及 I/O 性能增强功能的采用,如 kqueue 、 epoll 及 event ports 。
1.5. 代理(proxy)设计
代理设计,可以说是 Nginx 深入骨髓的设计,无论是对于 HTTP ,还是对于 FastCGI 、 Memcache 、 Redis 等的网络请求或响应,本质上都采用了代理机制 。所以, Nginx 天生就是高性能的代理服务器 。
Nginx进程处理模型
Nginx 服务器使用 master/worker 多进程模式 。多线程启动和执行的流程如下:
- 主程序 Master process 启动后,通过一个 for 循环来接收和处理外部信号 ;
- 主进程通过 fork() 函数产生 worker 子进程 ,每个 子进程 执行一个 for 循环来实现 Nginx 服务器对事件的接收和处理 。
一般推荐 worker 进程数与 CPU 核心数 一致,这样一来不存在 大量的子进程生成和管理任务,避免了进程之间竞争 CPU 资源和进程切换的开销。而且 Nginx 为了更好的利用多核特性 ,CPU亲和:将进程/线程与cpu绑定,最直观的好处就是提高了cpu cache的命中率,从而减少内存访问损耗,提高程序的速度。
对于每个请求,有且只有一个工作进程对其处理。首先,每个 worker 进程都是从 master进程 fork 过来。在 master 进程里面,先建立好需要 listen 的 socket(listenfd) 之后,然后再 fork 出多个 worker 进程。
所有 worker 进程的 listenfd 会在新连接 到来时变得可读 ,为保证只有一个进程处理该连接,所有 worker 进程在注册 listenfd 读事件 前抢占 accept_mutex ,抢到互斥锁的那个进程 注册 listenfd 读事件 ,在读事件里调用 accept 接受该连接。
当一个 worker 进程在 accept 这个连接之后,就开始读取请求 , 解析请求 , 处理请求 ,产生数据后,再返回给客户端 ,最后才断开连接 ,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由worker进程来处理,而且只在一个 worker 进程中处理。
在 Nginx 服务器的运行过程中, 主进程和工作进程需要进程交互。交互依赖于 Socket 实现的管道来实现。
Master-Worker 交互
这条管道与普通的管道不同,它是由主进程指向工作进程的单向管道 ,包含主进程向工作进程发出的指令 , 工作进程 ID 等;同时主进程与外界通过信号通信 ;每个子进程具备接收信号 ,并处理相应的事件的能力。
worker-worker 交互
这种交互是和主进程-工作进程交互是基本一致的,但是会通过主进程间接完成。 工作进程之间是相互隔离的,所以当工作进程 W1 需要向工作进程 W2 发指令时,首先找到 W2 的 进程ID ,然后将正确的指令写入指向 W2 的 通道 。 W2 收到信号采取相应的措施。
锁的争用与负载均衡(HTTP服务情景)
- 所谓的惊群
简单举例来说
TCP服务端socket的建立,一般经过socket、bind、listen初始化后,调用accept等待客户端的连接。服务器一般做法会在listen后,fork多个子进程同时accept客户端的连接。子进程在调用accept后会堵塞睡眠,当第一个客户端连接到来后,所有子进程都会唤醒,但只会有一个子进程accept调用成功,其他子进程返回失败,代码中的处理往往在accept返回失败后,继续调用accept。虽然这在功能上没有什么问题,但在性能上很是浪费。
- 惊群的解决
Linux内核2.6已经解决了accept时的惊群问题,多个子进程accept堵塞睡眠时,连接到来,只有一个进程的accept会被唤醒返回。但现在子进程的实现方式不是直接accept,而是将初始化好的fd加入到epoll 的事件队列中,epoll返回后再调用accept。Linux无法解决多个子进程epoll返回的情况。这需要子进程自己处理。
- Nginx的处理
Nginx中处理epoll时惊群问题的思路很简单,多个子进程有一个锁,谁拿到锁,谁才将accept的fd加入到epoll队列中,其他的子进程拿不到锁,也就不会将fd加入到epoll中,连接到来也就不会导致所有子进程的epoll被唤醒返回。
- 惊群处理的代码
ngx_process_events_and_timers()处理事件,这个函数中的以下代码,处理惊群问题,顺带实现了负载均衡,因为处理惊群问题和负载均衡问题的代码在一起,下面一起分析一下。
if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}
ngx_use_accept_mutex配置nginx是否利用ngx_accept_mutex锁的方式解决惊群问题(nginx只有这种方式,如果不使用,惊群问题就会存在),默认配置是打开的。
ngx_accept_disable用于处理负载均衡。
这里可以看出使用ngx_accept_mutex锁的情况下,一个进程要处理accept事件,必须满足两个条件:
- 满足负载均衡条件(负载压力低,稍后介绍nginx怎么判断压力高低)
- 获取ngx_accept_mutex锁
- 负载均衡条件
Ngx_accept_disable会在每次accept事件正确处理后,更新其值
ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
connection_n是配置文件中配置的该进程处理的最大连接数,free_connection_n初始值为connection_n,新建一个连接--,关闭一个连接++。
当free_connection_n 大于connection_n的1 / 8(连接数少于总数的7 / 8)时,ngx_accept_disable是负的,此时进程会参与锁的争夺,如果获得锁,该进程处理accept事件,其他进程,不处理accept事件。
当该进程处理accept事件增多,处理的连接也就增多,当free_connection_n小于connection_n的1 / 8(连接数大于总数的7 / 8)时,ngx_accept_disabled是正的,此时该进程会简单的将ngx_accept_disabled--,并退出锁的争夺,把机会让给其他进程。就这样平衡了负载,防止一个进程负载过高。
进程的连接越多,放弃争夺的次数也就越多,而ngx_accept_disabled--,避免了进程一直退出锁的争夺,防止不再接受新连接。
当每个进程的连接数都比较少时,谁抢到了ngx_accept_disable,谁处理连接;当一个进程抢的比较多,连接数率先达到了 7/ 8,那么就会退出锁的争夺,把机会让给其他进程,并不时ngx_accept_disabled--,保证一段时间后继续参加锁的争夺;当所有进程的连接数都大于 7 / 8时,这时再有新连接到来,处理延迟应该会比较大,因为所有进程都放弃了争夺,直到ngx_accept_disable—到小于0,再次争夺。
- 获取ngx_accept_mutex锁
当满足了负载均衡条件,进程就会参与ngx_accept_mutex锁的争夺 ngx_trylock_accept_mutex(cycle)
Ngx_trylock_accept_mutex出现错误,直接return了
没有错误的情况,ngx_accept_mutex_held=1表示拿到了锁
拿到锁会设置flags |= NGX_POST_EVENTS; 设置了这个标志,epoll返回的所有事件不会立即处理,而是将事件放到post队列中,然后释放锁后在把队列中的事件一一拿出处理,这样做的原因是防止处理事件导致锁长时间得不到释放,新的accept连接事件得不到(其他进程的)及时处理。
ngx_accept_mutex_held=0表示没有拿到锁
拿不到锁的进程会修改epoll超时时间,让epoll尽快返回,早早的参与到下一次锁的争夺上来,也就能快速的处理新的accept连接事件。(写到这里不得不佩服作者真是各种情况都考虑到了,牛啊)
接下来看看关于锁的战争
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
if (ngx_accept_mutex_held
&& ngx_accept_events == 0
&& !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
{
return NGX_OK;
}
if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
ngx_shmtx_unlock(&ngx_accept_mutex);
return NGX_ERROR;
}
ngx_accept_events = 0;
ngx_accept_mutex_held = 1;
return NGX_OK;
}
if (ngx_accept_mutex_held) {
if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
return NGX_ERROR;
}
ngx_accept_mutex_held = 0;
}
return NGX_OK;
}
line4 试图获取锁
line20 没有获取到锁,但ngx_accept_mutex_held却是1,是个异常,禁止当前进程处理accept事件(将accept fd将epoll队列中移除),重置ngx_accept_mutex_held=0
line5-10 成功获取锁,发现自己早就获得了该锁,只是没有accept事件发生,又循环了一次,继续获得了该锁。(第一次没有事件发生,应该释放锁,再进入第二次的争夺呀??)
line11-16 成功获得锁后,将accept fd加入到epoll队列中,准备监听accept事件,并置ngx_accept_mutex_head = 1表示获取到了锁(ngx_accept_events具体什么时候会用到)。
负载均衡(反向代理情景)
负载均衡也是Nginx常用的一个功能,负载均衡其意思就是分摊到多个操作单元上进行执行,例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等,从而共同完成工作任务。简单而言就是当有2台或以上服务器时,根据规则随机的将请求分发到指定的服务器上处理,负载均衡配置一般都需要同时配置反向代理,通过反向代理跳转到负载均衡。而Nginx目前支持自带3种负载均衡策略,还有2种常用的第三方策略。
RR
按照轮询(默认)方式进行负载,每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。虽然这种方式简便、成本低廉。但缺点是:可靠性低和负载分配不均衡。
权重
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
upstream test{
server localhost:8080 weight=9;
server localhost:8081 weight=1;
}
此时8080和8081分别占90%和10%。
ip_hash
上面的2种方式都有一个问题,那就是下一个请求来的时候请求可能分发到另外一个服务器,当我们的程序不是无状态的时候(采用了session保存数据),这时候就有一个很大的很问题了,比如把登录信息保存到了session中,那么跳转到另外一台服务器的时候就需要重新登录了,所以很多时候我们需要一个客户只访问一个服务器,那么就需要用iphash了,iphash的每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
upstream test {
ip_hash;
server localhost:8080;
server localhost:8081;
}
fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。
upstream backend {
fair;
server localhost:8080;
server localhost:8081;
}
url_hash(第三方)
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。 在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法。
upstream backend {
hash $request_uri;
hash_method crc32;
server localhost:8080;
server localhost:8081;
}
平滑升级
https://www.cnblogs.com/liuqiang0/p/8528009.html
一、解释nginx的平滑升级
随着nginx越来越流行使用,并且nginx的优势也越来越明显,nginx的版本迭代也开始了加速模式,1.9.0版本的nginx更新了许多新功能,例如stream四层代理功能。伴随着nginx的广泛应用,版本升级必然是越来越快的,线上业务不能停,此时nginx的升级就是运维的重要工作了,下面就带大家一起来理解下nginx平滑升级。
二、nginx平滑升级原理
多进程模式下的请求分配方式
Nginx默认工作在多进程模式下,即主进程(master process)启动后完成配置加载和端口绑定等动作,fork
出指定数量的工作进程(worker process),这些子进程会持有监听端口的文件描述符(fd),并通过在该描述符上添加监听事件来接受连接(accept)。
信号的接收和处理
Nginx主进程在启动完成后会进入等待状态,负责响应各类系统消息,如SIGCHLD、SIGHUP、SIGUSR2等。
Nginx信号简介
主进程支持的信号
TERM
,INT
: 立刻退出QUIT
: 等待工作进程结束后再退出KILL
: 强制终止进程HUP
: 重新加载配置文件,使用新的配置启动工作进程,并逐步关闭旧进程。USR1
: 重新打开日志文件USR2
: 启动新的主进程,实现热升级WINCH
: 逐步关闭工作进程
工作进程支持的信号
TERM
,INT
: 立刻退出QUIT
: 等待请求处理结束后再退出USR1
: 重新打开日志文件
三、nginx平滑升级实战
3.1 备份原Nginx二进制文件
备份二进制文件和nginx的配置文件
[root@iZ28t900vpcZ ~]#cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx_$(date +%F)
[root@iZ28t900vpcZ ~]#cp /usr/local/nginx/conf/nginx.conf /usr/local/nginx/conf/nginx.conf_$(date +%F)
3.2 编译新的nginx源码包
编译新Nginx源码,安装路径需与旧版一致
[root@iZ28t900vpcZ nginx-1.9.12]#./configure --prefix=/usr/local/nginx-1.9.12 --user=www --group=www --with-http_ssl_module --with-openssl=/path/to/openssl_src
[root@iZ28t900vpcZ nginx-1.9.12]#make&make install
3.3 发送USR2信号
向主进程发送USR2信号,Nginx会启动一个新版本的master进程和工作进程,和旧版一起处理请求
[root@iZ28t900vpcZ ~]# ps -ef|grep nginx|grep -v grep
root 900 1 0 Mar07 ? 00:00:00 nginx: master process /usr/local/openresty/nginx/sbin/nginx
nginx 28475 900 0 11:32 ? 00:00:00 nginx: worker process
[root@iZ28t900vpcZ ~]#kill -USR2 900
3.4 发送WITCH信号
向原Nginx主进程发送WINCH信号,它会逐步关闭旗下的工作进程(主进程不退出),这时所有请求都会由新版Nginx处理
[root@iZ28t900vpcZ ~]#kill -WITCH 900
3.5 发送HUP信号
如果这时需要回退,可向原Nginx主进程发送HUP信号,它会重新启动工作进程, 仍使用旧版配置文件 。然后可以将新版Nginx进程杀死(使用QUIT、TERM、或者KILL)
[root@iZ28t900vpcZ ~]#kill -HUP 900
注:此步骤只需在回滚的时候执行即可
3.6 升级完毕
如果不需要回滚,可以将原Nginx主进程杀死(使用QUIT、TERM、或者KILL),至此完成热升级。
nginx内存池源码解析
Author:Echo Chen(陈斌)
Email:chenb19870707@gmail.com
Blog:Blog.csdn.net/chen19870707
Date:Nov 11th, 2014
- 1.源代码位置
头文件:http://trac.nginx.org/nginx/browser/nginx/src/core/ngx_palloc.h
源文件:http://trac.nginx.org/nginx/browser/nginx/src/core/ngx_palloc.c
- 2.数据结构定义
先来学习一下nginx内存池的几个主要数据结构:
ngx_pool_data_t(内存池数据块结构)
1: typedef struct {
2: u_char *last;
3: u_char *end;
4: ngx_pool_t *next;
5: ngx_uint_t failed;
6: } ngx_pool_data_t;
last:是一个unsigned char 类型的指针,保存的是/当前内存池分配到末位地址,即下一次分配从此处开始。
end:内存池结束位置;
next:内存池里面有很多块内存,这些内存块就是通过该指针连成链表的,next指向下一块内存。
failed:内存池分配失败次数。
ngx_pool_s(内存池头部结构)
1: struct ngx_pool_s {
2: ngx_pool_data_t d;
3: size_t max;
4: ngx_pool_t *current;
5: ngx_chain_t *chain;
6: ngx_pool_large_t *large;
7: ngx_pool_cleanup_t *cleanup;
8: ngx_log_t *log;
9: };
d:内存池的数据块;
max:内存池数据块的最大值;
current:指向当前内存池;
chain:该指针挂接一个ngx_chain_t结构;
large:大块内存链表,即分配空间超过max的情况使用;
cleanup:释放内存池的callback
log:日志信息
由ngx_pool_data_t和ngx_pool_t组成的nginx内存池结构如下图所示:
- 3.相关函数介绍
在分析内存池方法前,需要对几个主要的内存相关函数作一下介绍:
ngx_alloc:(只是对malloc进行了简单的封装)
1: void *
2: ngx_alloc(size_t size, ngx_log_t *log)
3: {
4: void *p;
5:
6: p = malloc(size);
7: if (p == NULL) {
8: ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
9: "malloc(%uz) failed", size);
10: }
11:
12: ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, log, 0, "malloc: %p:%uz", p, size);
13:
14: return p;
15: }
ngx_calloc:(调用malloc并初始化为0)
1: void *
2: ngx_calloc(size_t size, ngx_log_t *log)
3: {
4: void *p;
5:
6: p = ngx_alloc(size, log);
7:
8: if (p) {
9: ngx_memzero(p, size);
10: }
11:
12: return p;
13: }
ngx_memzero:
1: #define ngx_memzero(buf, n) (void) memset(buf, 0, n)
ngx_free :
1: #define ngx_free free
ngx_memalign:
1: void *
2: ngx_memalign(size_t alignment, size_t size, ngx_log_t *log)
3: {
4: void *p;
5: int err;
6:
7: err = posix_memalign(&p, alignment, size);
8:
9: if (err) {
10: ngx_log_error(NGX_LOG_EMERG, log, err,
11: "posix_memalign(%uz, %uz) failed", alignment, size);
12: p = NULL;
13: }
14:
15: ngx_log_debug3(NGX_LOG_DEBUG_ALLOC, log, 0,
16: "posix_memalign: %p:%uz @%uz", p, size, alignment);
17:
18: return p;
19: }
这里alignment主要是针对部分unix平台需要动态的对齐,对POSIX 1003.1d提供的posix_memalign( )进行封装,在大多数情况下,编译器和C库透明地帮你处理对齐问题。nginx中通过宏NGX_HAVE_POSIX_MEMALIGN来控制;调用posix_memalign( )成功时会返回size字节的动态内存,并且这块内存的地址是alignment的倍数。参数alignment必须是2的幂,还是void指针的大小的倍数。返回的内存块的地址放在了memptr里面,函数返回值是0.
- 4.内存池基本操作
内存池对外的主要方法有:
创建内存池ngx_create_pool
ngx_create_pool用于创建一个内存池,我们创建时,传入我们的需要的初始大小:
1: ngx_pool_t *
2: ngx_create_pool(size_t size, ngx_log_t *log)
3: {
4: ngx_pool_t *p;
5:
6: //以16(NGX_POOL_ALIGNMENT)字节对齐分配size内存
7: p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
8: if (p == NULL) {
9: return NULL;
10: }
11:
12: //初始状态:last指向ngx_pool_t结构体之后数据取起始位置
13: p->d.last = (u_char *) p + sizeof(ngx_pool_t);
14: //end指向分配的整个size大小的内存的末尾
15: p->d.end = (u_char *) p + size;
16:
17: p->d.next = NULL;
18: p->d.failed = 0;
19:
20: size = size - sizeof(ngx_pool_t);
21: //#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1),内存池最大不超过4095,x86中页的大小为4K
22: p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;
23:
24: p->current = p;
25: p->chain = NULL;
26: p->large = NULL;
27: p->cleanup = NULL;
28: p->log = log;
29:
30: return p;
31: }
nginx对内存的管理分为大内存与小内存,当某一个申请的内存大于某一个值时,就需要从大内存中分配空间,否则从小内存中分配空间。
nginx中的内存池是在创建的时候就设定好了大小,在以后分配小块内存的时候,如果内存不够,则是重新创建一块内存串到内存池中,而不是将原有的内存池进行扩张。当要分配大块内存是,则是在内存池外面再分配空间进行管理的,称为大块内存池。
内存申请 ngx_palloc
1: void *
2: ngx_palloc(ngx_pool_t *pool, size_t size)
3: {
4: u_char *m;
5: ngx_pool_t *p;
6:
7: //如果申请的内存大小小于内存池的max值
8: if (size <= pool->max) {
9:
10: p = pool->current;
11:
12: do {
13: //对内存地址进行对齐处理
14: m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);
15:
16: //如果当前内存块够分配内存,则直接分配
17: if ((size_t) (p->d.end - m) >= size)
18: {
19: p->d.last = m + size;
20:
21: return m;
22: }
23:
24: //如果当前内存块有效容量不够分配,则移动到下一个内存块进行分配
25: p = p->d.next;
26:
27: } while (p);
28:
29: //当前所有内存块都没有空闲了,开辟一块新的内存,如下2详细解释
30: return ngx_palloc_block(pool, size);
31: }
32:
33: //分配大块内存
34: return ngx_palloc_large(pool, size);
35: }
需要说明的几点:
1、ngx_align_ptr,这是一个用来内存地址取整的宏,非常精巧,一句话就搞定了。作用不言而喻,取整可以降低CPU读取内存的次数,提高性能。因为这里并没有真正意义调用malloc等函数申请内存,而是移动指针标记而已,所以内存对齐的活,C编译器帮不了你了,得自己动手。
1: #define ngx_align_ptr(p, a) \
2: (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
2、开辟一个新的内存块 ngx_palloc_block(ngx_pool_t *pool, size_t size)
这个函数是用来分配新的内存块,为pool内存池开辟一个新的内存块,并申请使用size大小的内存;
1: static void *
2: ngx_palloc_block(ngx_pool_t *pool, size_t size)
3: {
4: u_char *m;
5: size_t psize;
6: ngx_pool_t *p, *new;
7:
8: //计算内存池第一个内存块的大小
9: psize = (size_t) (pool->d.end - (u_char *) pool);
10:
11: //分配和第一个内存块同样大小的内存块
12: m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
13: if (m == NULL) {
14: return NULL;
15: }
16:
17: new = (ngx_pool_t *) m;
18:
19: //设置新内存块的end
20: new->d.end = m + psize;
21: new->d.next = NULL;
22: new->d.failed = 0;
23:
24: //将指针m移动到d后面的一个位置,作为起始位置
25: m += sizeof(ngx_pool_data_t);
26: //对m指针按4字节对齐处理
27: m = ngx_align_ptr(m, NGX_ALIGNMENT);
28: //设置新内存块的last,即申请使用size大小的内存
29: new->d.last = m + size;
30:
31: //这里的循环用来找最后一个链表节点,这里failed用来控制循环的长度,如果分配失败次数达到5次,就忽略,不需要每次都从头找起
32: for (p = pool->current; p->d.next; p = p->d.next) {
33: if (p->d.failed++ > 4) {
34: pool->current = p->d.next;
35: }
36: }
37:
38: p->d.next = new;
39:
40: return m;
41: }
3、分配大块内存 ngx_palloc_large(ngx_pool_t *pool, size_t size)
在ngx_palloc中首先会判断申请的内存大小是否超过内存块的最大限值,如果超过,则直接调用ngx_palloc_large,进入大内存块的分配流程;
1: static void *
2: ngx_palloc_large(ngx_pool_t *pool, size_t size)
3: {
4: void *p;
5: ngx_uint_t n;
6: ngx_pool_large_t *large;
7:
8: // 直接在系统堆中分配一块大小为size的空间
9: p = ngx_alloc(size, pool->log);
10: if (p == NULL) {
11: return NULL;
12: }
13:
14: n = 0;
15:
16: // 查找到一个空的large区,如果有,则将刚才分配的空间交由它管理
17: for (large = pool->large; large; large = large->next) {
18: if (large->alloc == NULL) {
19: large->alloc = p;
20: return p;
21: }
22: //为了提高效率, 如果在三次内没有找到空的large结构体,则创建一个
23: if (n++ > 3) {
24: break;
25: }
26: }
27:
28:
29: large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
30: if (large == NULL) {
31: ngx_free(p);
32: return NULL;
33: }
34:
35: //将large链接到内存池
36: large->alloc = p;
37: large->next = pool->large;
38: pool->large = large;
39:
40: return p;
41: }
整个内存池分配如下图:
内存池重置 ngx_reset_pool
1: void
2: ngx_reset_pool(ngx_pool_t *pool)
3: {
4: ngx_pool_t *p;
5: ngx_pool_large_t *l;
6:
7: //释放大块内存
8: for (l = pool->large; l; l = l->next) {
9: if (l->alloc) {
10: ngx_free(l->alloc);
11: }
12: }
13:
14: // 重置所有小块内存区
15: for (p = pool; p; p = p->d.next) {
16: p->d.last = (u_char *) p + sizeof(ngx_pool_t);
17: p->d.failed = 0;
18: }
19:
20: pool->current = pool;
21: pool->chain = NULL;
22: pool->large = NULL;
23: }
内存池释放 ngx_pfree
1: ngx_int_t
2: ngx_pfree(ngx_pool_t *pool, void *p)
3: {
4: ngx_pool_large_t *l;
5:
6: //只检查是否是大内存块,如果是大内存块则释放
7: for (l = pool->large; l; l = l->next) {
8: if (p == l->alloc) {
9: ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
10: "free: %p", l->alloc);
11: ngx_free(l->alloc);
12: l->alloc = NULL;
13:
14: return NGX_OK;
15: }
16: }
17:
18: return NGX_DECLINED;
19: }
所以说Nginx内存池中大内存块和小内存块的分配与释放是不一样的。我们在使用内存池时,可以使用ngx_palloc进行分配,使用ngx_pfree释放。而对于大内存,这样做是没有问题的,而对于小内存就不一样了,分配的小内存,不会进行释放。因为大内存块的分配只对前3个内存块进行检查,否则就直接分配内存,所以大内存块的释放必须及时。
外部资源的清理
Nginx内存池支持通过回调函数,对外部资源的清理。ngx_pool_cleanup_t是回调函数结构体,它在内存池中以链表形式保存,在内存池进行销毁时,循环调用这些回调函数对数据进行清理。
1: typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
2:
3: struct ngx_pool_cleanup_s {
4: ngx_pool_cleanup_pt handler;
5: void *data;
6: ngx_pool_cleanup_t *next;
7: };
handler:是回调函数指针;
data:回调时,将此数据传入回调函数;
next:指向下一个回调函数结构体;
如果我们需要添加自己的回调函数,则需要调用ngx_pool_cleanup_add来得到一个ngx_pool_cleanup_t,然后设置handler为我们的清理函数,并设置data为我们要清理的数据。这样在ngx_destroy_pool中会循环调用handler清理数据;
比如:我们可以将一个开打的文件描述符作为资源挂载到内存池上,同时提供一个关闭文件描述的函数注册到handler上,那么内存池在释放的时候,就会调用我们提供的关闭文件函数来处理文件描述符资源了。
1: ngx_pool_cleanup_t *
2: ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
3: {
4: ngx_pool_cleanup_t *c;
5:
6: //分配ngx_pool_cleanup_t
7: c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
8: if (c == NULL) {
9: return NULL;
10: }
11:
12: //给data分配内存
13: if (size) {
14: c->data = ngx_palloc(p, size);
15: if (c->data == NULL) {
16: return NULL;
17: }
18:
19: } else {
20: c->data = NULL;
21: }
22:
23: //将回掉函数链入内存池
24: c->handler = NULL;
25: c->next = p->cleanup;
26:
27: p->cleanup = c;
28:
29: ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);
30:
31: return c;
32: }
内存池销毁 ngx_destroy_pool
1: void
2: ngx_destroy_pool(ngx_pool_t *pool)
3: {
4: ngx_pool_t *p, *n;
5: ngx_pool_large_t *l;
6: ngx_pool_cleanup_t *c;
7:
8: //依次调用外部析构回调函数
9: for (c = pool->cleanup; c; c = c->next) {
10: if (c->handler) {
11: ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
12: "run cleanup: %p", c);
13: c->handler(c->data);
14: }
15: }
16:
17: //释放大块内存
18: for (l = pool->large; l; l = l->next) {
19:
20: ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);
21:
22: if (l->alloc) {
23: ngx_free(l->alloc);
24: }
25: }
26: //释放小块内存
27: for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
28: ngx_free(p);
29:
30: if (n == NULL) {
31: break;
32: }
33: }
34: }
安装与使用
安装见https://www.cnblogs.com/dongye95/p/11196118.html
使用:
因为我是安装在/usr/local/nginx目录下,在/usr/local/nginx/conf/nginx.conf文件中新增一个自己的server
82 server {
83 listen 8011;
84 server_name localhost;
85 charset utf_8;
86 location / {
87 alias /usr/local/nginx/;
88
89 }
90 }
在/usr/local/nginx目录新建一个文件
记得修改访问权限 chmod 755 index.html
访问方式:
先启动nginx 在 /usr/local/nginx/sbin 里面 sudo ./nginx 可执行文件即可
1.命令行
curl localhost:8011
返回 hello world
2.浏览器
在目录下有日志文件,可方便查看每次访问的请求内容和错误记录等信息