Mr Dk.'s BlogMr Dk.'s Blog
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🗜️ Understanding Nginx
    • Part 1 - Nginx 能帮我们做什么

      • Chapter 1 - 研究 Nginx 前的准备工作
      • Chapter 2 - Nginx 的配置
    • Part 2 - 如何编写 HTTP 模块

      • Chapter 7 - Nginx 提供的高级数据结构
    • Part 3 - 深入 Nginx

      • Chapter 8.1-8.2 - Nginx 基础架构
      • Chapter 8.3-8.6 - Nginx 框架核心结构体
      • Chapter 8.7 - Nginx 内存池
      • Chapter 9.1-9.3 - 事件处理框架
      • Chapter 9.4-9.6 - 事件驱动模块与 EPOLL
      • Chapter 9.7-9.8 - 定时器事件与事件驱动框架处理流程
      • Chapter 9.9-9.10 - 文件的异步 I/O 与 TCP
      • Chapter 10.1-10.2 - HTTP 框架的配置解析与合并
      • Chapter 10.3-10.7 - HTTP 阶段划分与框架初始化
      • Chapter 11 - HTTP 框架的执行流程
      • Chapter 12.1-12.4 - Upstream 与上游服务器通信
      • Chapter 12.5 - 接收上游服务器响应并处理
      • Chapter 12.6 - 12.9 - 转发响应并结束请求
      • Chapter 13.1-13.5 - 邮件代理模块 - 认证服务器
      • Chapter 13.6-13.7 - 邮件代理模块 - 上游
      • Chapter 14 - 进程间通信机制
      • Chapter 16 - slab 共享内存

Chapter 16 - slab 共享内存

Created by : Mr Dk.

2020 / 08 / 08 21:18

Nanjing, Jiangsu, China


16.1 操作 slab 共享内存的方法

  • void ngx_slab_init(ngx_slab_pool_t *pool); - 初始化共享内存 (由 Nginx 框架自动调用)
  • 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); 不加锁保护的内存释放函数

由于 slab 大部分时间都要跨进程通信,因此不加锁的分配、释放函数较少使用。

Slab 内存池在解析配置文件时被初始化。当执行 -s reload 时,Nginx 会重新加载配置文件,再次初始化 slab 共享内存池。

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 {
    void                     *data; // 回调函数的参数
    ngx_shm_t                 shm; // 描述共享内存的结构体
    ngx_shm_zone_init_pt      init; // slab 内存池创建成功后回调
    void                     *tag;
    void                     *sync;
    ngx_uint_t                noreuse;  /* unsigned  noreuse:1; */
};

16.3 slab 内存管理的实现原理

动态管理内存面对的主要问题:

  • 时间上,使用者随时可能分配内存、释放内存
  • 空间上,每次申请分配内存的大小随机

这两个问题使得内存中会产生碎片,从而导致内存的浪费。常见内存分配算法的两种设计方向:

  • First-fit - 首次适应,从头遍历空闲内存块链表,找到第一个符合需求的内存块 (快,但可能产生碎片)
  • Best-fit - 找到与所需分配的内存最为匹配的内存块 (遍历慢些,但是产生碎片的概率小)

Nginx 基于 best-fit 的思路分配内存,但是需要以更快的速度找到合适大小的内存。Nginx 假定所有需要使用 slab 内存的模块请求分配的内存都不超过 4KB。基于这个假设,Nginx 中的内存块分配方法如下:

  1. 把整块内存按 4KB 分页,每页存放 固定大小 的内存块,页首存放 bitmap (通过 bitmap 遍历空闲内存块)
  2. Slab 内存分配器把请求分配的内存大小简化为有限几种 (2 字节的倍数)
  3. 让有限的几种大小相同的页面构成链表,按内存块大小排序保存在一个数组中
  4. 页面中分为空闲页、半满页、全满页,全满页脱离链表,分配内存时不再访问
  5. 对于大于 4KB 页的内存分配需求,寻找地址连续的几个空闲页来分配

16.3.1 内存结构布局

每一个 slab 内存池对应着一块共享内存。内存首地址开始的地方保存的是 slab 管理结构,之后才是分配给使用者的内存:

typedef struct {
    ngx_shmtx_sh_t    lock; // 使用信号量进行进程同步时会用到

    size_t            min_size; // 最小内存块长度
    size_t            min_shift; // 最小内存块长度的偏移 (1 << ?)

    ngx_slab_page_t  *pages; // 所有的内存页
    ngx_slab_page_t  *last;
    ngx_slab_page_t   free; // 所有的空闲页链表

    ngx_slab_stat_t  *stats;
    ngx_uint_t        pfree;

    u_char           *start; // 所有的实际页面的首地址
    u_char           *end; // 共享内存尾部

    ngx_shmtx_t       mutex; // 互斥锁

    u_char           *log_ctx; // slab 操作失败时用于记录日志
    u_char            zero; // '\0'

    unsigned          log_nomem:1;

    void             *data; // 由使用 slab 的模块自由使用
    void             *addr; // 执行所属 ngx_shm_t 中的 addr 成员
} ngx_slab_pool_t;

为了描述每一个内存页,都要使用一个 ngx_slab_page_t 结构体:

typedef struct ngx_slab_page_s  ngx_slab_page_t;

struct ngx_slab_page_s {
    uintptr_t         slab; // (多用途)
    ngx_slab_page_t  *next; // 下一页
    uintptr_t         prev; // (多用途),同时指向双向链表的前一页
};

所有内存页可被分为全满页、半满页、空闲页。全满页不在任何链表中,因此其中的 next 和 prev 没有任何链表的功能。

所有的空闲页则构成一个双向链表,由 free 指针指向这个链表 - 但 并不是每个空闲页都是链表中的元素。首页面的 slab 成员大于 1 时表示其后有相邻的页面,这些相邻页面作为一个链表元素而存在。由于内存的分配与回收时机不同,可能出现相邻的内存块各占一个链表元素的情况。当 slab 内存池刚刚初始化完毕时,free 链表中只有一个元素 - 就是第一个页面,slab 的值为总页数。

对于半满页,存放相同大小内存块的页面会构成双向链表,存放在 slot 数组的相应位置上。

16.3.2 分配内存流程

首先需要判断用户申请的内存是否大于 ngx_slab_max_size - 如果大于这个值,说明要按照页面来分配内存;否则只用一个页面中的内存块就可以实现分配。如果要按页来分配内存,那么首先需要计算分配多少个页面才能存放用户申请的内存字节数 (不足一页时按一页计算)。

ngx_uint_t pages = (size >> ngx_pagesize_shift) + ((size % ngx_pagesize) ? 1 : 0);

在计算出页数后,需要遍历 free 空闲页链表,找出能够容纳用户申请的字节数的 连续内存:

static ngx_slab_page_t *
ngx_slab_alloc_pages(ngx_slab_pool_t *pool, ngx_uint_t pages)
{
    ngx_slab_page_t  *page, *p;

    // 遍历 free 链表
    for (page = pool->free.next; page != &pool->free; page = page->next) {

        // 链表元素的 slab 数量已经达到所需要分配的页数
        if (page->slab >= pages) {

            // slab 在分配后还会有空闲页
            if (page->slab > pages) {
                page[page->slab - 1].prev = (uintptr_t) &page[pages];

                // slab 中第一个不会被分配的空闲页
                // 修改其指针,使其作为链表元素
                page[pages].slab = page->slab - pages;
                page[pages].next = page->next;
                page[pages].prev = page->prev;

                // 修改链表前后结点的指针
                p = (ngx_slab_page_t *) page->prev;
                p->next = &page[pages];
                page->next->prev = (uintptr_t) &page[pages];

            } else {
                // slab 中的页被完全分配了
                // 直接将这个元素移出链表
                p = (ngx_slab_page_t *) page->prev;
                p->next = page->next;
                page->next->prev = page->prev;
            }

            // 设置被分配内存的属性
            page->slab = pages | NGX_SLAB_PAGE_START;
            page->next = NULL;
            page->prev = NGX_SLAB_PAGE;

            // 内存池中的空闲页数减少
            pool->pfree -= pages;

            // 如果只分配了一个页,那么可以直接返回了
            if (--pages == 0) {
                return page;
            }

            // 如果不止分配了一个页,那么设置后续内存页的属性
            for (p = page + 1; pages; pages--) {
                p->slab = NGX_SLAB_PAGE_BUSY;
                p->next = NULL;
                p->prev = NGX_SLAB_PAGE;
                p++;
            }

            return page;
        }
    }

    // 没有找到符合要求的内存页面
    if (pool->log_nomem) {
        ngx_slab_error(pool, NGX_LOG_CRIT,
                       "ngx_slab_alloc() failed: no memory");
    }

    return NULL;
}

如果被分配的内存大小在一页以内,那么就要计算符合这个内存大小的最小内存块了。Slab 页面上允许存放的内存块以 8 字节起步,以 2 为倍数递增。此时先要根据 best-fit 原则找到最适合的内存块大小 (比如 64B)。然后取出半满页的链表,取得相应块大小的内存页链表 (全是 64B 内存块的半满页)。遍历内存页链表,如果没有找到半满页,那么分配一个新的页用于存放这个大小的内存块。

在每个半满页中,根据页大小和内存块大小,可以计算出 bitmap 的长度。然后找到 bitmap 中的第一个空闲块;如果没有空闲块,那这就不是一个半满页了,因此需要把它从链表中移走,然后遍历下一个半满页。如果找了到空闲块,在记录该块被使用以后,还要判断这是不是当前页的最后一个空闲页,如果是,那么也要将其移出链表。

如果分配了新页用于存放内存块,那么还要计算出 bitmap 后,将 bitmap 本身占用的内存块标记为已使用。新页面由空闲页变为半满页,因此会被插入到半满页链表相应内存块大小的链表的首部。

16.3.3 释放内存流程

首先判断内存块地址是否合法,然后得到内存块所属页的页描述结构体,判断出页面是整页内存还是内存块页面。如果是内存块页面,那么还要计算出 bitmap 的长度。将 bitmap 中相应的位置为空闲,如果该页是一个全满页,在释放后将变为半满页,那么还要将其插入半满页链表中。另外,还要检测 bitmap 中是否还有 0 的位,如果没有,那么说明变为空闲页,还要插入回 free 链表中。


在内存分配和释放的过程中,Nginx 大量使用了位操作来提高性能。

Edit this page on GitHub
Prev
Chapter 14 - 进程间通信机制