Maison  >  Article  >  Opération et maintenance  >  Comment implémenter le pool de mémoire nginx

Comment implémenter le pool de mémoire nginx

WBOY
WBOYavant
2023-05-17 13:26:271274parcourir

1. Introduction

La dernière version stable nginx1.20.2.
Afin d'allouer de la mémoire de manière efficace et rapide et de réduire la fragmentation de la mémoire, nginx implémente son propre composant de pool de mémoire de base.
Principaux fichiers d'implémentation ngx_palloc.h, ngx_palloc.cngx_palloc.h, ngx_palloc.c

二、数据结构

2.1 内存池主要结构

typedef struct {
    u_char               *last;
    u_char               *end;
    ngx_pool_t           *next;
    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;
};

内存池中第一个成员是一个结构体:
使用ngx_pool_data_t结构体来表示当前内存池信息。
last :下次开始分配的地址
end: 内存池的结束地址
next: 内存池链表,将多个内存池连接起来

max
整个内存池的最大大小

current
指向从当前内存池开始查找可用内存

chain
buffer使用的,这里不涉及

large
当需要的内存大于内存池最大大小时,需要通过malloc直接分配,然后形成链表进行组织

cleanup
清理工作的回调链表

log
日志句柄

2.2 大内存链

当需要分配的内存比内存池的最大大小都大时,内存池无法满足分配,所以直接从系统中分配,然后构成一个链表进行维护。

typedef struct ngx_pool_large_s  ngx_pool_large_t;

struct ngx_pool_large_s {
    ngx_pool_large_t     *next;
    void                 *alloc;
};

2.3 清理任务链

有一个回调任务的链表,当内存池销毁时,将依次遍历此链表,逐一回调handler进行清理工作。

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;
};

三、内存结构图

3.1 逻辑

Comment implémenter le pool de mémoire nginx

3.2 实际

Comment implémenter le pool de mémoire nginx

可以看出,很多节点都是从内存池中分配的,所以可以把精力都放在实际的数据上而不必在意其他细节上。

四、实现

4.1 创建内存池

/*
 * NGX_MAX_ALLOC_FROM_POOL should be (ngx_pagesize - 1), i.e. 4095 on x86.
 * On Windows NT it decreases a number of locked pages in a kernel.
 */
#define NGX_MAX_ALLOC_FROM_POOL  (ngx_pagesize - 1)

#define NGX_DEFAULT_POOL_SIZE    (16 * 1024)
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
    ngx_pool_t  *p;

    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    if (p == NULL) {
        return NULL;
    }

    p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    p->d.end = (u_char *) p + size;
    p->d.next = NULL;
    p->d.failed = 0;

    size = size - sizeof(ngx_pool_t);
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;
}

从代码中可以看到,内存池最大不超过pagesize的大小

Comment implémenter le pool de mémoire nginx

4.2 从内存池中分配空间

分配函数分了内存对齐和内存不对齐,但这只控制了内存池中分配空间,不控制大内存分配。

(1)分配小空间

  • 内存对齐 ngx_palloc

  • 内存不对齐 ngx_pnalloc

void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)
    if (size <= pool->max) {
        return ngx_palloc_small(pool, size, 1);
    }
#endif

    return ngx_palloc_large(pool, size);
}

当需要分配的空间小于max时,将使用小内存分配方式(即从内存池中分配空间),而ngx_pnalloc和ngx_palloc相比只是调用ngx_palloc_small时的最后一个参数为0。

从pool->current指向的内存池开始遍历,寻找满足分配大小的空间,找到则返回首地址

static ngx_inline void *
ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)
{
    u_char      *m;
    ngx_pool_t  *p;

    p = pool->current;

    do {
        m = p->d.last;

        if (align) {
            m = ngx_align_ptr(m, NGX_ALIGNMENT);
        }

        if ((size_t) (p->d.end - m) >= size) {
            p->d.last = m + size;

            return m;
        }

        p = p->d.next;

    } while (p);

    return ngx_palloc_block(pool, size);
}

当现有内存池中都无法满足分配条件时,创建新的内存池

static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;

    psize = (size_t) (pool->d.end - (u_char *) pool);

    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) {
        return NULL;
    }

    new = (ngx_pool_t *) m;

    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;

    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    new->d.last = m + size;

    for (p = pool->current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;
        }
    }

    p->d.next = new;

    return m;
}

其中,创建好新的内存池后,又做了一次遍历,将failed计数加一,当大于4时,将跳过此内存池,下次就不从它开始查找。
即认为超过4次你都不能满足分配,以后都不能满足分配,不再用你了,减少遍历个数,加快成功分配效率

(2)分配大空间

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;

    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }

        if (n++ > 3) {
            break;
        }
    }

    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}

可以看出,为了避免分配空间,遍历large链查找可重用的节点,但是如果链表过大又可能太慢,所以只查找前三个,如果三个都没有找到,则直接分配(而且节点也是从内存池中分配的,所以后续清理时,不需要管节点,只需要释放申请的大内存本身)

内存对齐

void *
ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
{
    void              *p;
    ngx_pool_large_t  *large;

    p = ngx_memalign(alignment, size, pool->log);
    if (p == NULL) {
        return NULL;
    }

    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}

4.3 注册清理任务

ngx_pool_cleanup_t *
ngx_pool_cleanup_add(ngx_pool_t *p, size_t size)
{
    ngx_pool_cleanup_t  *c;

    c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t));
    if (c == NULL) {
        return NULL;
    }

    if (size) {
        c->data = ngx_palloc(p, size);
        if (c->data == NULL) {
            return NULL;
        }

    } else {
        c->data = NULL;
    }

    c->handler = NULL;
    c->next = p->cleanup;

    p->cleanup = c;

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "add cleanup: %p", c);

    return c;
}

可以看出,这里只是分配了一个节点,并没有设置handler以及data数据,所以还得看具体的调用方进行设置,因为这里返回了分配的节点。

比如在函数ngx_create_temp_file

ngx_int_t
ngx_create_temp_file(ngx_file_t *file, ngx_path_t *path, ngx_pool_t *pool,
    ngx_uint_t persistent, ngx_uint_t clean, ngx_uint_t access)
{
    ...

    cln = ngx_pool_cleanup_add(pool, sizeof(ngx_pool_cleanup_file_t));
    if (cln == NULL) {
        return NGX_ERROR;
    }

       ...
        file->fd = ngx_open_tempfile(file->name.data, persistent, access);
				...
        if (file->fd != NGX_INVALID_FILE) {

            cln->handler = clean ? ngx_pool_delete_file : ngx_pool_cleanup_file;
            clnf = cln->data;

            clnf->fd = file->fd;
            clnf->name = file->name.data;
            clnf->log = pool->log;

            return NGX_OK;
        }
			...
}

生成临时文件,将fd以及文件名注册到清理任务中,后续文件不使用了则不需要特殊处理,内存内存池释放时将统一清理。

4.4 重置内存池

  • 释放大内存

  • 重置内存中last

  • 重置failed计数

void
ngx_reset_pool(ngx_pool_t *pool)
{
    ngx_pool_t        *p;
    ngx_pool_large_t  *l;

    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }

    for (p = pool; p; p = p->d.next) {
        p->d.last = (u_char *) p + sizeof(ngx_pool_t);
        p->d.failed = 0;
    }

    pool->current = pool;
    pool->chain = NULL;
    pool->large = NULL;
}

这里有个现象:
在内存池中空间不足时,将调用ngx_palloc_block创建一个新的内存池,而last指向的是m += sizeof(ngx_pool_data_t);, 因此当前新分配的内存池将比第一个内存池可用大小多了(max,current,chain,large,cleanup,log)这几个字段大小(可能没有那么多,因为要对齐,可能对齐后就完全一样了),而现在重置时,p->d.last = (u_char *) p + sizeof(ngx_pool_t);

II. Structure des données

2.1 Structure principale du pool de mémoire
    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);
            }
        }
    
    
        for (l = pool->large; l; l = l->next) {
            if (l->alloc) {
                ngx_free(l->alloc);
            }
        }
    
        for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
            ngx_free(p);
    
            if (n == NULL) {
                break;
            }
        }
    }
  • Le premier membre du pool de mémoire est une structure :

    Utilisez le Corps de structure ngx_pool_data_t pour représenter les informations actuelles du pool de mémoire.

    last : l'adresse à allouer la prochaine fois
  • end : l'adresse de fin du pool de mémoire
  • next : une liste chaînée de pools de mémoire qui connecte plusieurs pools de mémoire

    max
  • La taille maximale de l'ensemble du pool de mémoire
  • current

    points à partir du pool de mémoire actuel Commencez à rechercher la mémoire disponible 🎜🎜chain🎜buffer est utilisé, ce qui n'est pas impliqué ici🎜🎜large🎜Lorsque la mémoire requise est supérieure à la taille maximale du pool de mémoire, elle doit être allouée directement via malloc , puis organisé en une liste chaînée🎜🎜cleanup🎜Liste chaînée de rappel pour le travail de nettoyage 🎜🎜log🎜Log handle🎜🎜2.2 Grande chaîne de mémoire🎜🎜Lorsque la mémoire qui doit être allouée est supérieure à la taille maximale du pool de mémoire , le pool de mémoire ne peut pas satisfaire l'allocation, il est donc alloué directement à partir du système et forme ensuite une liste chaînée pour la maintenance. 🎜
    ngx_int_t
    ngx_pfree(ngx_pool_t *pool, void *p)
    {
        ngx_pool_large_t  *l;
    
        for (l = pool->large; l; l = l->next) {
            if (p == l->alloc) {
                ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                               "free: %p", l->alloc);
                ngx_free(l->alloc);
                l->alloc = NULL;
    
                return NGX_OK;
            }
        }
    
        return NGX_DECLINED;
    }
    🎜2.3 Chaîne de tâches de nettoyage🎜🎜Il existe une liste chaînée de tâches de rappel Lorsque le pool de mémoire est détruit, cette liste chaînée sera parcourue en séquence et les gestionnaires seront rappelés un par un pour effectuer le travail de nettoyage. 🎜
    void *
    ngx_pcalloc(ngx_pool_t *pool, size_t size)
    {
        void *p;
    
        p = ngx_palloc(pool, size);
        if (p) {
            ngx_memzero(p, size);
        }
    
        return p;
    }
    🎜3. Diagramme de structure de la mémoire🎜🎜3.1 Logique🎜🎜Que diriez-vous de la mémoire nginx pool Implementation🎜🎜3.2 Actual🎜🎜Comment implémenter le pool de mémoire nginx 🎜🎜On peut voir que de nombreux nœuds sont alloués à partir du pool de mémoire, vous pouvez donc vous concentrer sur les données réelles sans vous soucier d'autres détails. 🎜🎜4. Implémentation🎜🎜4.1 Créer un pool de mémoire🎜
    void
    ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd)
    {
        ngx_pool_cleanup_t       *c;
        ngx_pool_cleanup_file_t  *cf;
    
        for (c = p->cleanup; c; c = c->next) {
            if (c->handler == ngx_pool_cleanup_file) {
    
                cf = c->data;
    
                if (cf->fd == fd) {
                    c->handler(cf);
                    c->handler = NULL;
                    return;
                }
            }
        }
    }
    void
    ngx_pool_cleanup_file(void *data)
    {
        ngx_pool_cleanup_file_t  *c = data;
    
        ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",
                       c->fd);
    
        if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
            ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                          ngx_close_file_n " \"%s\" failed", c->name);
        }
    }
    🎜Comme vous pouvez le voir dans le code, le pool de mémoire maximum ne dépasse pas la taille de la taille de la page🎜🎜Comment implémenter le pool de mémoire nginx🎜🎜4.2 Allouer de l'espace à partir du pool de mémoire🎜🎜La fonction d'allocation est divisée en alignement de la mémoire et désalignement de la mémoire, mais il contrôle uniquement le pool de mémoire alloue de l'espace dans et ne contrôle pas les allocations de mémoire importantes. 🎜🎜(1) Allouer un petit espace 🎜🎜🎜🎜Alignement de la mémoire ngx_palloc🎜🎜🎜🎜Désalignement de la mémoire ngx_pnalloc🎜🎜
void
ngx_pool_delete_file(void *data)
{
    ngx_pool_cleanup_file_t  *c = data;

    ngx_err_t  err;

    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d %s",
                   c->fd, c->name);

    if (ngx_delete_file(c->name) == NGX_FILE_ERROR) {
        err = ngx_errno;

        if (err != NGX_ENOENT) {
            ngx_log_error(NGX_LOG_CRIT, c->log, err,
                          ngx_delete_file_n " \"%s\" failed", c->name);
        }
    }

    if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                      ngx_close_file_n " \"%s\" failed", c->name);
    }
}
🎜Lorsque l'espace alloué est nécessaire Quand il est inférieur au maximum, la petite méthode d'allocation de mémoire (c'est-à-dire l'allocation d'espace à partir du pool de mémoire) sera utilisée. Par rapport à ngx_palloc, ngx_pnalloc n'a que le dernier paramètre de 0 lors de l'appel de ngx_palloc_small. 🎜🎜Commencez à parcourir le pool de mémoire pointé par pool->current, à la recherche d'un espace qui correspond à la taille d'allocation, et s'il est trouvé, renvoyez la première adresse🎜rrreee🎜Lorsque les conditions d'allocation ne peuvent pas être remplies dans le pool de mémoire existant, créez un nouveau pool de mémoire🎜rrreee 🎜Parmi eux, après avoir créé un nouveau pool de mémoire, un autre parcours est effectué pour augmenter le nombre d'échecs de un. Lorsqu'il est supérieur à 4, ce pool de mémoire sera ignoré et la recherche ne démarrera pas à partir de celui-ci. la prochaine fois. 🎜Autrement dit, on considère que vous ne pouvez pas respecter l'allocation plus de 4 fois et que vous ne pourrez pas respecter l'allocation à l'avenir. Vous ne serez plus utilisé. Réduisez le nombre de traversées et accélérez l'efficacité de. allocation réussie🎜🎜(2) Allouer un grand espace🎜rrreee🎜On peut voir que pour éviter d'allouer de l'espace et parcourir la grande chaîne pour trouver des nœuds réutilisables. Cependant, si la liste chaînée est trop grande, elle peut être trop lente, Ainsi, seuls les trois premiers sont recherchés. Si aucun des trois n'est trouvé, ils sont alloués directement (et les nœuds sont également alloués à partir du pool de mémoire. Par conséquent, lors du nettoyage ultérieur, vous n'avez pas besoin de gérer le nœud, il vous suffit de le faire. pour libérer la grande mémoire appliquée elle-même)🎜🎜Alignement de la mémoire🎜rrreee🎜4.3 Enregistrement de la tâche de nettoyage🎜rrreee🎜On peut voir qu'un seul nœud est alloué ici, et aucun gestionnaire ni donnée n'est défini, cela dépend donc du. appelant spécifique pour les paramètres, car le nœud alloué est renvoyé ici. 🎜🎜Par exemple, dans la fonction ngx_create_temp_file, 🎜rrreee🎜 génère un fichier temporaire, enregistre le fd et le nom du fichier dans la tâche de nettoyage, et aucun traitement spécial n'est requis si les fichiers suivants ne sont pas utilisés. le pool de mémoire sera unifié lors de sa sortie. Nettoyer. 🎜🎜4.4 Réinitialiser le pool de mémoire🎜🎜🎜🎜Libérer une grande mémoire🎜🎜🎜🎜Réinitialiser la dernière mémoire en mémoire🎜🎜🎜🎜Réinitialiser le nombre d'échecs🎜🎜rrreee🎜Il y a un phénomène ici : 🎜Il y a de l'espace dans le pool de mémoire Lorsqu'il est insuffisant, ngx_palloc_block sera appelé pour créer un nouveau pool de mémoire, et le dernier pointe vers m += sizeof(ngx_pool_data_t);, donc le pool de mémoire actuellement nouvellement alloué sera plus grand que La taille disponible du premier pool de mémoire est beaucoup plus grande (max, current, chain, large, cleanup, log) et la taille de ces champs (peut-être pas tant que ça car il doit être aligné, peut-être que ce sera le cas). exactement la même chose après l'alignement), et maintenant il est réinitialisé. Quand, p->d.last = (u_char *) p + sizeof(ngx_pool_t); la taille disponible de chaque pool de mémoire devient la même encore. 🎜🎜4.5 Détruire le pool de mémoire🎜🎜🎜🎜Tâche de nettoyage de rappel🎜🎜🎜🎜Libérer une grande mémoire🎜🎜🎜🎜Libérer le pool de mémoire lui-même🎜
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);
        }
    }


    for (l = pool->large; l; l = l->next) {
        if (l->alloc) {
            ngx_free(l->alloc);
        }
    }

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

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

4.6 大内存释放

通过遍历找到要释放的节点,将内存释放,并且将alloc设置成NULL,则有了节点重用的情况。

ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)
{
    ngx_pool_large_t  *l;

    for (l = pool->large; l; l = l->next) {
        if (p == l->alloc) {
            ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,
                           "free: %p", l->alloc);
            ngx_free(l->alloc);
            l->alloc = NULL;

            return NGX_OK;
        }
    }

    return NGX_DECLINED;
}

4.7 分配并清空数据

void *
ngx_pcalloc(ngx_pool_t *pool, size_t size)
{
    void *p;

    p = ngx_palloc(pool, size);
    if (p) {
        ngx_memzero(p, size);
    }

    return p;
}

正常分配的空间中都是垃圾数据,所以当前函数在分配空间后,将分配的空间清零。

4.8 回调文件清理

(1) 手动关闭指定fd

遍历清理任务,找到ngx_pool_cleanup_file的handler,如果是要关闭的fd,则回调

void
ngx_pool_run_cleanup_file(ngx_pool_t *p, ngx_fd_t fd)
{
    ngx_pool_cleanup_t       *c;
    ngx_pool_cleanup_file_t  *cf;

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

            cf = c->data;

            if (cf->fd == fd) {
                c->handler(cf);
                c->handler = NULL;
                return;
            }
        }
    }
}

(2) 关闭fd

void
ngx_pool_cleanup_file(void *data)
{
    ngx_pool_cleanup_file_t  *c = data;

    ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d",
                   c->fd);

    if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                      ngx_close_file_n " \"%s\" failed", c->name);
    }
}

(3) 删除文件并关闭fd

void
ngx_pool_delete_file(void *data)
{
    ngx_pool_cleanup_file_t  *c = data;

    ngx_err_t  err;

    ngx_log_debug2(NGX_LOG_DEBUG_ALLOC, c->log, 0, "file cleanup: fd:%d %s",
                   c->fd, c->name);

    if (ngx_delete_file(c->name) == NGX_FILE_ERROR) {
        err = ngx_errno;

        if (err != NGX_ENOENT) {
            ngx_log_error(NGX_LOG_CRIT, c->log, err,
                          ngx_delete_file_n " \"%s\" failed", c->name);
        }
    }

    if (ngx_close_file(c->fd) == NGX_FILE_ERROR) {
        ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
                      ngx_close_file_n " \"%s\" failed", c->name);
    }
}

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer