• Nginx源码:内存池的实现



    为什么需要对内存管理?

    • 避免频繁的系统调用带来的开销。
    • 减少了频繁分配和释放小块内存产生的内存碎片。

    解决上述问题,最好的方法就是内存池。内存池就是对堆上的内存进行管理。

    内存池的具体做法是固定大小、提前申请、重复利用。

    • 固定大小。在调用内存分配函数的时候,小块内存每次都分配固定大小的内存块,这样避免了内存碎片产生的可能性。
    • 提前申请一块大的内存,内存不够用时再二次分配,减少了malloc的次数,提高了效率。

    Nginx 使用内存池管理进程内存,当接收到请求时,创建一个内存池。处理请求过程中需要的内存都从这个内存池中申请,请求处理完成后释放内存池。Nginx 将内存池中的内存分为两类:小块内存和大块内存。对于小块内存,用户申请后并不需要释放,而是等待释放内存池时再释放。对于大块内存,用户可以调用相关接口进行释放,也可以等内存池释放时再释放。同时 Nginx 内存池支持增加回调函数,当内存池释放时,自动调用回调函数释放用户申请的资源。回调函数允许增加多个,通过链表进行链接,在内存池释放时被逐一调用。

    源码位置:src/core/ngx_palloc.h, src/core/ngx_palloc.c

    1、数据结构

    在这里插入图片描述

    内存池由内存块链表(内存池节点)组成,每个内存块分为两个两部分,一部分存储该内存块相关信息,另一部分用于小块内存的分配。

    ngx_pool_data_t

    typedef struct {
        u_char               *last;   // 指向该内存块已分配内存的末尾地址,下一个待分配内存的起始地址
        u_char               *end;    // 指向该内存块的末尾地址
        ngx_pool_t           *next;   // 指向下一个内存块
        ngx_uint_t            failed; // 当前内存块分配空间失败的次数
    } ngx_pool_data_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ngx_pool_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;      // 日志信息
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ngx_pool_large_s:大块内存链表节点

    struct ngx_pool_large_s {
        ngx_pool_large_t     *next;     // 指向下一个大块内存节点
        void                 *alloc;    // 指向实际分配的大块内存
    };
    
    • 1
    • 2
    • 3
    • 4

    2、接口函数

    2.1、创建内存池

    申请的内存由两部分组成,一部分用来容纳ngx_pool_t结构体,另一部分内存则是用于满足用户申请。ngx_pool_data_t结构体中的last指针和end指针之间的内存是空闲的,当用户申请小块内存时,如果空闲的内存大小满足用户的需求,则可以分配给用户。

    // 内存对齐,默认16字节对齐
    #define NGX_POOL_ALIGNMENT       16
    
    /**
     * @brief 创建内存池
     * @param size 内存块的大小
     * @param log  log 打印日志
     * @return ngx_pool_t* 返回创建的内存池地址
     */
    ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log) {
        ngx_pool_t  *p;
    
        // 申请内存,默认16字节对齐
        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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2.2、内存分配

    用户可以调用ngx_palloc向内存池申请未初始化的内存。分配内存的时候,根据本次申请空间的大小 size 和内存池设定的pool->max,判断当前要分配的内存是小块内存还是大块内存。

    void *ngx_palloc(ngx_pool_t *pool, size_t size) {
        // 判断申请的内存块是大块还是小块
        // 1、申请小块内存
        if (size <= pool->max) {
            return ngx_palloc_small(pool, size, 1);
        }
        // 2、申请大块内存
        return ngx_palloc_large(pool, size);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    2.2.1、小块内存分配

    若用户申请的是小块内存,则调用ngx_palloc_small遍历内存池的内存块,寻找其中是否有满足需求的内存块

    • 有可分配的内存块,返回待分配空间的首地址
    • 没有可分配的内存块,创建一个新的内存池节点
    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);
            }
    
            // 当前内存块的剩余空间是否足够分配
            // 1、够分配
            if ((size_t) (p->d.end - m) >= size) {
                // 更新 last 指针
                p->d.last = m + size;
                return m;
            }
    
            // 2、不够分配,继续查找下一个内存池节点
            p = p->d.next;
    
        } while (p);
    
        // 重新申请一个内存块,用于小块内存分配
        return ngx_palloc_block(pool, size);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    ngx_palloc_block申请新的内存块,尾插到内存块链表中

    每次调用ngx_palloc_block函数,代表现有内存块的小块内存分配失败,此时,所有内存块的 failed + 1,表示不满足用户的需求增加 1 次。 由于采取的是尾插法,所以内存块链表中内存块的 failed 计数值依次递减。若某个内存块连续 5 次不满足用户需求,则不再使用它,下次遍历时跳过。

    使用ngx_pool_t->current来记录可分配的内存块,下次遍历时,先尝试从可分配内存块的剩余空间分配。

    • 若空间足够,则返回last的地址作为内存分配的起始地址,并更新 last = last + size
    • 若空间不足,则创建一个新的结点,并更新 p->next,返回 last 的地址作为内存分配的起始地址,并更新 last = last + 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移动到可分配内存的开始位置
        m += sizeof(ngx_pool_data_t);
        // 对指针做内存对齐
        m = ngx_align_ptr(m, NGX_ALIGNMENT);
        // 设置新内存块的last
        new->d.last = m + size;
    
        // 将所有内存块的 failed + 1,表示不满足用户的需求 +1 次
        for (p = pool->current; p->d.next; p = p->d.next) {
    		// 若某个内存块若连续 5 次都不满足用户需求,则跳过这个内存块,以后不再遍历它
            if (p->d.failed++ > 4) {
                // 调整 current 指向下一个内存块(该内存块以前的内存块无法分配内存)
                pool->current = p->d.next;
            }
        }
    
        // 将新创建的内存块,尾插到内存块链表
        p->d.next = new;
    
        return m;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    2.2.2、大块内存分配

    对于大块内存,直接申请相应大小的内存,并通过链表将已经申请的大快内存进行链接。值得注意的是,对于大块内存的管理链表节点 ngx_pool_large_t ,从内存池进行申请

    在这里插入图片描述

    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;
            }
    
            // 若连续 4 次都没找到,不找了
            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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    2.3、内存释放

    Nginx 内存池内部只提供大块内存的释放接口,小块内存不需要释放,内存池销毁的时候随之释放。

    2.3.1、大块内存释放
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    2.3.2、内存池释放
    • 查看内存池是否挂载清理函数,若有,则调用链表中的所有回调函数
    • 释放大块内存
    • 释放内存池中的内存块
    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); // free
            }
        }
    
        // 释放内存池内存块
        for (p = pool, n = pool->d.next; ; p = n, n = n->d.next) {
            ngx_free(p); // free
    
            if (n == NULL) {
                break;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    3、slab 共享内存

    3.1、共享内存

    Nginx 各进程间共享数据的主要方式就是使用共享内存,共享内存一般由 master 进程创建,master 进程 fork 出 worker 进程后,所有进程开始使用这块共享内存中的数据。

    Nginx 定义了 ngx_shm_t 结构体,用于描述一块共享内存

    typedef struct {
        u_char      *addr;  // 共享内存起始地址
        size_t       size;  // 共享内存的长度
        ngx_str_t    name;  // 共享内存的名称
        ngx_log_t   *log;   // 记录日志
        ngx_uint_t   exists;// 表示该共享内存是否已经分配过
    } ngx_shm_t;
    
    // 创建共享内存
    ngx_int_t ngx_shm_alloc(ngx_shm_t *shm);
    // 释放共享内存
    void ngx_shm_free(ngx_shm_t *shm);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    共享内存的创建与释放,根据 linux 系统调用区别有不同的实现方式,底层可以通过 mmap 或 shmget 系统调用创建共享内存,通过 munmap 或 shmdt 系统调用释放内存。

    3.2、共享内存池

    共享内存使用的方法有两种

    • 直接调用 ngx_shm_alloc 创建共享内存块,自行管理共享内存空间。
    • 使用 ngx_shared_memory_add 函数创建共享内存块,使用ngx_slab_pool_t 共享内存池管理共享内存

    ngx_shared_memory_add 函数创建 ngx_slab_pool_t 共享内存池,在解析配置文件时调用。

    // 初始化 1 块大小为 size,名称为 name 的 slab 共享内存池(tag 防止重名)
    ngx_shm_zone_t *ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name, size_t size, void *tag);
    
    • 1
    • 2

    其返回值 ngx_shm_zone_t 就是用来获取 ngx_slab_pool_t 对象的,在使用前必须设置初始化回调函数。

    typedef struct ngx_shm_zone_s  ngx_shm_zone_t;
    typedef ngx_int_t (*ngx_shm_zone_init_pt) (ngx_shm_zone_t *zone, void *data);
    
    struct ngx_shm_zone_s {
        ngx_shm_zone_init_pt  init; // 真正创建slab共享内存池后,回调 init 指向的方法
        void	*data; 			   // 回调 init 方法传入的配置文件参数
        ngx_shm_t  shm;			   // 描述共享内存的结构体
        void	*tag;  			   // 对应 ngx_shared_memory_add 的 tag 参数
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ngx_shm_zone_t 结构体中,需要注意两个参数的使用

    • init 成员,必须设置,规定创建 slab 内存池后一定会调用 init 指向的方法。
    • data 成员,若 nginx 首次启动,data 是空指针。若 nginx 重新读取配置文件,之前正处于使用中的共享内存是有可能复用的,因此需要尽可能使用旧共享内存(如果存在的话),此时 data 指向第一次创建共享内存时,ngx_shared_memory_add 返回的 ngx_shm_zone_t 的 data 成员。

    操作 slab 内存池的方法

    // 初始化新创建的共享内存池
    void ngx_slab_init(ngx_slab_pool_t *pool);
    // 加锁保护的内存分配方法
    void *ngx_slab_alloc(ngx_slab_pool_t *pool, size_t size);
    // 不加锁分配的内存分配方法
    void *ngx_slab_alloc_locked(ngx_slab_pool_t *pool, size_t size);
    // 加锁保护的内存释放方法
    void ngx_slab_free(ngx_slab_pool_t *pool, void *p);
    // 不加锁保护的内存释放方法
    void ngx_slab_free_locked(ngx_slab_pool_t *pool, void *p);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    4、参考

    • 聂松松等. Nginx底层设计与源码分析[M]. 北京:机械工业出版社,2021.
  • 相关阅读:
    Windows下PostgreSQL编译调试笔记
    浅谈系统架构中的状态
    【运维】fstab,systemctl与rc.local启动顺序
    【大虾送书第九期】速学Linux:系统应用从入门到精通
    kubernetes 调度
    Redis(七) 主从复制(二)哨兵模式
    【论文笔记】基于预训练模型的持续学习(Continual Learning)(增量学习,Incremental Learning)
    STM32串口通信-简单版
    15.4 Java反射机制的深入应用(血干JAVA系类)
    CentOS修改主机名
  • 原文地址:https://blog.csdn.net/you_fathe/article/details/127926387