nginx的物理内存是做什么的??

发布网友 发布时间:2022-02-27 02:43

我来回答

1个回答

热心网友 时间:2022-02-27 04:13

先来看内存池的实现,nginx的内存池实现的非常简单。

这里内存池的一些图表可以看老朱同学的slides :

http://blog.zhuzhaoyuan.com/2009/09/nginx-internals-slides-video/

当内存池初始化的时候(下面会分析到)ngx_poll_s只相当于内存池的一个头,保存了当前内存池的一些必要信息而已。

当从内存池存取数据的时候,nginx是分为两种类型来处理得,一种是小块数据,它是直接从内存池中取得数据,另一方面,当为大块数据时,它是直接malloc一块数据(也就是从内存池外部分配数据),然后保存这个指针到内存池。可以看到很多内存池,比如py的内存池实现,也基本是这个思想。这里的细节,我们将会在下面分析内存池相关函数的时候详细分析。

这里还有一个要注意就是这些子内存池和父内存池是不一样的,我们后面分析函数的时候会详细介绍。

大小块数据得分割线是你创建内存池时传递进来的size和页大小之间的最小值。

下面就是内存池的结构:

Java代码 收藏代码
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;
};

然后我们一个个来看上面的链表。首先是数据区的指针ngx_pool_data_t。

这个结构很简单,它就是包含了我们所需要操作这个内存池的数据的一些指针。

其中last表示当前的数据区的已经使用的数据的结尾。

end表示当前的内存池的结尾。也就是说end-last就是内存池未使用的大小。

我们要知道当我们从一个内存池请求一块内存时,如果此时内存池已经满掉,这是一般都是扩大内存池,而nginx中不是这么做的,它会直接再分配一个内存池,然后链接到data的next指针上。也就是说在nginx中,其实每个内存池都会包含一些子内存池。因此我们请求内存的时候都会遍历这些子内存池。

failed域主要是为了标记我们请求内存(小块内存)由于内存池空间不够,我们需要重新分配一个子内存池的次数。 下面分析函数的时候会再会看到这个域。

Java代码 收藏代码
typedef struct {
u_char *last;
u_char *end;
//指向下一块内存池
ngx_pool_t *next;
///失败标记
ngx_uint_t failed;
} ngx_pool_data_t;

ngx_chain_t这里就先不介绍了,我们现在只需要知道它是与buf相关的。

然后是ngx_pool_large_s,它表示了大块的内存。可以看到这个结构非常简单,就是一个指针指向下一块large,一个alloc指向数据。

Java代码 收藏代码
struct ngx_pool_large_s {
ngx_pool_large_t *next;
void *alloc;
};

接下来是ngx_pool_cleanup_s,这个结构用来表示内存池中的数据的清理handler。

其中handler表示清理函数。
data表示传递给清理函数的数据。
next表示下一个清理handler,也就是说当destroy这个pool的时候会遍历清理handler链表,然后调用handler。

Java代码 收藏代码
struct ngx_pool_cleanup_s {
ngx_pool_cleanup_pt handler;
void *data;
ngx_pool_cleanup_t *next;
}

通过ngx_create_temp_buf创建一个buff,然后通过ngx_alloc_chain_link创建一个chain,然后通过cl->buf = rb->buf;将buff链接到chain中.

下面就是pool的内存图,摘自老朱同学的nginx internal。

ok,接下来我们来通过分析具体的函数,来更好的理解pool的实现。

首先来看ngx_create_pool也就是创建一个pool。

这里我们要知道虽然我们传递进来的大小是size可是我们真正能使用的数据区大小是要减去ngx_pool_t的大小的。

Java代码 收藏代码
///内存池的数据区的最大容量。
#define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize - 1)

ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
ngx_pool_t *p;
///可以看到直接分配size大小,也就是说我们只能使用size-sizeof(ngx_poll_t)大小
p = ngx_alloc(size, log);
if (p == NULL) {
return NULL;
}

///开始初始化数据区。

///由于一开始数据区为空,因此last指向数据区的开始。
p->d.last = (u_char *) p + sizeof(ngx_pool_t);
///end也就是数据区的结束位置
p->d.end = (u_char *) p + size;
p->d.next = NULL;
p->d.failed = 0;

///这里才是我们真正能使用的大小。
size = size - sizeof(ngx_pool_t);

///然后设置max。内存池的最大值也就是size和最大容量之间的最小值。
p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

///current表示当前的内存池。
p->current = p;

///其他的域置NULL。
p->chain = NULL;
p->large = NULL;
p->cleanup = NULL;
p->log = log;
///返回指针。
return p;
}

接下来我们来看如何从内存池中分配一块内存来使用。在nginx中有3个函数可以使用,分别是ngx_palloc,ngx_calloc,ngx_pnalloc。这三个函数的区别就是第一个函数分配的内存会对齐。第二个函数用来分配一块清0的内存,第三个函数分配的内存不会对齐。

由于这三个函数差不多,因此我们就只分析一个就够了。我们就来看ngx_palloc.

Java代码 收藏代码
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
u_char *m;
ngx_pool_t *p;

///首先判断当前申请的大小是否超过max,如果超过则说明是大块,此时进入large
if (size <= pool->max) {

///得到当前的内存池指针。
p = pool->current;

///开始遍历内存池,
do {
///首先对齐last指针。
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);

///然后得到当前内存池中的可用大小。如果大于请求大小,则直接返回当前的last,也就是数据的指针。
if ((size_t) (p->d.end - m) >= size) {
///更新last,然后返回前面保存的last。
p->d.last = m + size;

return m;
}
///否则继续遍历
p = p->d.next;

} while (p);
///到达这里说明内存池已经满掉,因此我们需要重新分配一个内存池然后链接到当前的data的next上。(紧接着我们会分析这个函数)
return ngx_palloc_block(pool, size);
}

///申请大块。
return ngx_palloc_large(pool, size);
}

接下来就来看ngx_palloc_block的实现,这个函数主要就是重新分配一块内存池,然后链接到当前内存池的数据区指针。

然后要注意这里重新new的这个内存池大小是和它的父内存池一样大的。

并且新得内存池只保存了ngx_pool_data_t这个结构,也就是说数据区直接跟在ngx_pool_data_t下面。

Java代码 收藏代码
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
u_char *m;
size_t psize;
ngx_pool_t *p, *new, *current;

///计算当前的内存池的大小。
psize = (size_t) (pool->d.end - (u_char *) pool);

///再分配一个同样大小的内存池
m = ngx_alloc(psize, pool->log);
if (m == NULL) {
return NULL;
}

new = (ngx_pool_t *) m;

///接下来和我们create一个内存池做的操作一样。就是更新一些指针
new->d.end = m + psize;
new->d.next = NULL;
new->d.failed = 0;

///这里要注意了,可以看到last指针是指向ngx_pool_data_t的大小再加上要分配的size大小,也就是现在的内存池只包含了ngx_pool_data_t和数据。
m += sizeof(ngx_pool_data_t);
m = ngx_align_ptr(m, NGX_ALIGNMENT);
new->d.last = m + size;

///设置current。
current = pool->current;

///这里遍历所有的子内存池,这里主要是通过failed来标记重新分配子内存池的次数,然后找出最后一个大于4的,标记它的下一个子内存池为current。
for (p = current; p->d.next; p = p->d.next) {
if (p->d.failed++ > 4) {
current = p->d.next;
}
}

///链接到最后一个内存池后面
p->d.next = new;

///如果current为空,则current就为new。
pool->current = current ? current : new;

return m;
}

这里解释一下为什么这样设置current,这里的主要原因是我们在ngx_palloc中分配内存是从current开始的,而这里也就是设置current为比较新分配的内存。而当failed大于4说明我们至少请求了4次内存分配,都不能满足我们的请求,此时我们就假设老的内存都已经没有空间了,因此我们就从比较新的内存块开始。

接下来是ngx_palloc_large,这个函数也是很简单就是malloc一块ngx_poll_large_t,然后链接到主的内存池上。

Java代码 收藏代码
static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
void *p;
ngx_uint_t n;
ngx_pool_large_t *large;

///分配一块内存。
p = ngx_alloc(size, pool->log);
if (p == NULL) {
return NULL;
}

n = 0;
///开始遍历large链表,如果有alloc(也就是内存区指针)为空,则直接指针赋值然后返回。一般第一次请求大块内存都会直接进入这里。并且大块内存是可以被我们手动释放的。
for (large = pool->large; large; large = large->next) {
if (large->alloc == NULL) {
large->alloc = p;
return p;
}
///这里不太懂什么意思。
if (n++ > 3) {
break;
}
}

///malloc一块ngx_pool_large_t。
large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
if (large == NULL) {
ngx_free(p);
return NULL;
}

///然后链接数据区指针p到large。这里可以看到直接插入到large链表的头的。
large->alloc = p;
large->next = pool->large;
pool->large = large;

return p;
}

ok,分配看完了,我们来看释放。这里要知道在nginx中,只有大块内存提供了free接口,可以供我们收工释放,而小块内存是没有提供这个接口的。也就是说小块内存只有当整个内存池被desrtoy掉时,才会被释放。

这里一些简单的函数就不分析了。
比如ngx_pfree(ngx_pool_t *pool, void *p),这个函数就是从pool的large链表中找到p,然后free掉它。

ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)

这个函数也就是添加一个ngx_pool_cleanup_t到当前的pool上,然后返回,我们此时就能通过返回的结构来给对应的handler赋值。

而ngx_pool_cleanup_t这个主要是当内存池destroy的时候我们可能需要做一些清理工作,此时我们就能add这些清理handler到pool中,然后当内存池要释放的时候就会自动调用。

ok,现在来看pool 被free的实现。

这个函数主要是遍历large,遍历current,然后一一释放。

Java代码 收藏代码
void
ngx_destroy_pool(ngx_pool_t *pool)
{
ngx_pool_t *p, *n;
ngx_pool_large_t *l;
ngx_pool_cleanup_t *c;

///先做清理工作。
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);
c->handler(c->data);
}
}

///free大块内存
for (l = pool->large; l; l = l->next) {

ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0, "free: %p", l->alloc);

if (l->alloc) {
ngx_free(l->alloc);
}
}

///遍历小块内存池。
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
///直接free掉。
ngx_free(p);

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

通过上面我们可以看到在nginx中内存池中的小块数据是从来不释放的,这样就简化了内存池的操作。

接下来我们来看buf的实现。

buf分为两种类型,一种是file,一种是memory.因此这里会有文件的一些操作域。

可以看到buf相对于pool多了一个pos域(file_pos).这里我们要知道我们发送往套接字异或者其他的设备,我们这里会现将数据放到buf中,然后当设备或者套接字准备好了,我们就会从buf中读取,因此这里pos指针就是放到buf中的已经被执行的数据(也就是已经送往套接字)的位置。

Java代码 收藏代码
struct ngx_buf_s {
///pos表示已经执行的数据的位置。
u_char *pos;
///last和上面内存池中last一样,也就是使用的内存的最后一个字节的指针
u_char *last;
///文件指针
off_t file_pos;
off_t file_last;
///buf的开始指针
u_char *start; /* start of buffer */
u_char *end; /* end of buffer */

///这里表示这个buf从属于那个模块。
ngx_buf_tag_t tag;
ngx_file_t *file;
ngx_buf_t *shadow;

///一些标记
/* the buf's content could be changed */
unsigned temporary:1;

///在内存中是不能改变的。
unsigned memory:1;

///是否是mmap的内存
unsigned mmap:1;

unsigned recycled:1;

///是否文件。
unsigned in_file:1;
unsigned flush:1;
unsigned sync:1;
unsigned last_buf:1;
unsigned last_in_chain:1;

unsigned last_shadow:1;
unsigned temp_file:1;

/* STUB */ int num;
};

ok,接下来我们来看如何创建一个buf.在nginx中一般都是调用ngx_create_temp_buf来创建一个buf。函数很简单,就是从pool中分配内存然后初始化相关域。
声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。
E-MAIL:11247931@qq.com