-
MemoryContext介绍
MemoryContext 以功能为单位组织起来的树形数据结构,不同的阶段使用不同MemoryContext,它的存在是为了更清晰的管理内存。
-
合理管理碎片小内存。频繁的向 OS 申请和释放内存效率是很差的。MemoryContext 会以 trunk 为单位向 OS 申请成块的内存,并管理起来。当程序需求小内存时从 trunk 中分配,用完后归还给对应的 MemoryContext ,并不归还给 OS。
-
内存上下文借鉴的操作系统的概念,操作系统为每个进程分配了进程执行环境,进程环境之间不互相影响,由操作系统来对环境进行切换,进程可以在其进程环绕中调用一些库函数(如 malloc free realloc等)来执行内存操作 类似的,一 个内存上下文实际上就相当于一个进程环境, PostgreSQL 以类似的方式提供了在内存上下文进行内存操作的函数:palloc,pfree,repalloc
PostgresSQL 的每一个子进程都拥有多个私有的内存上下文,每个子进程的内存上下文组成树形结构 ,其根节点为 TopMemoryContext 。在根节点之下有多个子节点,每个子节 点都用于不同的功能模块,例如 CacheMemoryContext 用于管理Cache,ErrorMemoryContext 用于错误 处理,每个子节点又可以有自己的子节点。
-
内存上下文相关的数据类型
2.1 MemoryContext
MemoryContext,见名知义,即所谓的“内存上下文”。它是一组内存块的控制结构,类似于面向对象设计语言(比如C++)中设计抽象类。它指明了该组内存的一般用途,并且在该数据类型内部定义了操作内存的一组接口,具体内存管理和操作需要根据不同的实际场景与用途在子类中去实现。截止到当前PostgreSQL 13.2,只实现了一种
AllocSetContext
(即AllocSet
),这里的AllocSetContext你可以认为等同于继承自MemoryContext抽象类的子类。MemoryContext的结构如下所示(src/include/nodes/memnodes.h),从内存上下文的结构可以看到,每节点只能有1个父节点,但是可以有多个子节点。typedef struct MemoryContextData
{
NodeTag type; /* identifies exact kind of context */
/* these two fields are placed here to minimize alignment wastage: */
bool isReset; /* T = no space alloced since last reset */
bool allowInCritSection; /* allow palloc in critical section */
MemoryContextMethods *methods; /* virtual function table */
MemoryContext parent; /* NULL if no parent (toplevel context) */
MemoryContext firstchild; /* head of linked list of children */
MemoryContext prevchild; /* previous child of same parent */
MemoryContext nextchild; /* next child of same parent */
char *name; /* context name (just for debugging) */
MemoryContextCallback *reset_cbs; /* list of reset/delete callbacks */
} MemoryContextData;
2.2 AllocSetContex
AllocSetContext是MemoryContext的标准实现。其结构如下:
typedef struct AllocSetContext
{
MemoryContextData header; /* Standard memory-context fields */
/* Info about storage allocated in this context: */
AllocBlock blocks; /* head of list of blocks in this set */
AllocChunk freelist[ALLOCSET_NUM_FREELISTS]; /* free chunk lists */
/* Allocation parameters for this context: */
Size initBlockSize; /* initial block size */
Size maxBlockSize; /* maximum block size */
Size nextBlockSize; /* next block size to allocate */
Size allocChunkLimit; /* effective chunk size limit */
AllocBlock keeper; /* if not NULL, keep this block over resets */
} AllocSetContext;
-
header标准的内存上下文区域,即抽象类型MemoryContext(可理解为子类对象中的基类对象的内存区域。),通过这个字段可以区分各内存上下文的层次关系。
-
blocks内存块链表,记录内存上下文向操作系统申请的连续大块内存,指向AllocBlockData。它是实际分配内存的地方,AllocSetContext中内存的分配是以Block(块)为单位向OS申请的,多个Block之间以单链表的方式连接起来。此blocks是单链表的头节点,在清除内存上下文时从它开始遍历整个链表,是否所有的Block中的内存。
-
freelist组织该内存上下文里所有内存块中已释放的内存片的链表结构。它是一个数组成员,其数组大小是
ALLOCSET_NUM_FREELISTS
(该值是11
)。freelist数组中各成员分别表示不同大小的AllocChunkData
。在AllocSetContext的实现中,对于小的内存块(8 ~ 8192Byte
)来说,当释放的时候不会归还给OS,而是将其缓存到freelist
中。 -
initBlockSizeBlock的初始化大小,默认8KB。
-
maxBlockSizeBlock的最大大小。
-
nextBlockSize下一个Block的大小。
-
allocChunkLimit内存片的阈值,申请的内存超过此阀值直接分配新的block。
-
keeper为防止一些需要频繁重置的小的内存上下文重复的进行malloc,重置时保留第一次申请的内存块。
-
freeListIndex在
context_freelists
全局数组中的顺序,0表示默认freelist,1表示小内存的freelist,-1表示不需要进入freelist(比如超过allocChunkLimit的Block)。context_freelists的的数据类型是AllocSetFreeList
AllocSet 所管理的内存区域被分成若干个内存块(block),内存块用AllocBlockData 结构(下面数据结构3.11)表示。每个内存块内又被分成多个称为内存片(chunk)的单元。
2.3 AllocBlockData
PostgreSQL向OS申请内存分配的基本单位是Block(块),一个Block可能被拆分为若干个Chunk,也可能只包含一个Chunk(比如较大块内存)。在chunk释放时候会放入freelist链表中,以便于下次分配,最后由Block统一释放归还给OS。
/*
* AllocBlock
* An AllocBlock is the unit of memory that is obtained by aset.c
* from malloc(). It contains one or more AllocChunks, which are
* the units requested by palloc() and freed by pfree(). AllocChunks
* cannot be returned to malloc() individually, instead they are put
* on freelists by pfree() and re-used by the next palloc() that has
* a matching request size.
*
* AllocBlockData is the header data for a block --- the usable space
* within the block begins at the next alignment boundary.
*/
typedef struct AllocBlockData
{
AllocSet aset; /* aset that owns this block */
AllocBlock prev; /* prev block in aset's blocks list, if any */
AllocBlock next; /* next block in aset's blocks list, if any */
char *freeptr; /* start of free space in this block */
char *endptr; /* end of space in this block */
} AllocBlockData;
AllocBlockData 记录在一块内存区域的起始地址处,这块内存区域通过标准库函数 malloc 进行分配,称为一个内存块。在每个内存块中进行内存分配时产生的内存片段称之为内存片,每个内存片包括一个头部信息和数据区域,其中头部信息包括该内存片所属的内存上下文以及该内存区的其他相关信息,数据区则存储实际数据。
2.4 AllocChunkData
内存片的头部信息由数据结构 AllocChunkData 描述(下面数据结构3.12),内存片的数据区域则紧跟在其头部信息之后分配。通过 PostgreSOL 中定义的 palloc 函数和 pfree 函数,我们可以自由地在内存上下文中申请和释放内存片,被释放的内存片将被加人到FreeList 中以备复使用。
AllocChunkData是AllocBlockData中每段内存的头部信息。
/*
* AllocChunk
* The prefix of each piece of memory in an AllocBlock
*/
typedef struct AllocChunkData
{
/* size is always the size of the usable space in the chunk */
Size size;
#ifdef MEMORY_CONTEXT_CHECKING
/* when debugging memory usage, also store actual requested size */
/* this is zero in a free chunk */
Size requested_size;
#if MAXIMUM_ALIGNOF > 4 && SIZEOF_VOID_P == 4
Size padding;
#endif
#endif /* MEMORY_CONTEXT_CHECKING */
/* aset is the owning aset if allocated, or the freelist link if free */
void *aset;
/* there must not be any padding to reach a MAXALIGN boundary here! */
} AllocChunkData;
2.5 MemoryContextMethods
MemoryContext 中的 methods 字段是一个 MemoryContextMethods 类型(数据结构3.9) 。它是由一系列函数指针组成的集合,其中包含了对内存上下文进行操作的函数。对于不同的 MemoryContext 实现,可以设置不同的方法集合。
/*
* MemoryContext
* A logical context in which memory allocations occur.
*
* MemoryContext itself is an abstract type that can have multiple
* implementations, though for now we have only AllocSetContext.
* The function pointers in MemoryContextMethods define one specific
* implementation of MemoryContext --- they are a virtual function table
* in C++ terms.
*
* Node types that are actual implementations of memory contexts must
* begin with the same fields as MemoryContext.
*
* Note: for largely historical reasons, typedef MemoryContext is a pointer
* to the context struct rather than the struct type itself.
*/
typedef struct MemoryContextMethods
{
void *(*alloc) (MemoryContext context, Size size);
/* call this free_p in case someone #define's free() */
void (*free_p) (MemoryContext context, void *pointer);
void *(*realloc) (MemoryContext context, void *pointer, Size size);
void (*init) (MemoryContext context);
void (*reset) (MemoryContext context);
void (*delete_context) (MemoryContext context);
Size (*get_chunk_space) (MemoryContext context, void *pointer);
bool (*is_empty) (MemoryContext context);
void (*stats) (MemoryContext context, int level, bool print,
MemoryContextCounters *totals);
#ifdef MEMORY_CONTEXT_CHECKING
void (*check) (MemoryContext context);
#endif
} MemoryContextMethods;
全局变量AllocSetMethods 中指定了 AllocSetContext 实现的操作函数,它们一一对应 MemoryContextMethods 中的操作函数:AllocSetAlloc、 AllocSetFree、 AllocSetRealloc、 AllocSetInit 、AllocSetReset 、AllocSetDelete、AllocSetGetChunkSpace、AllocSetIsEmpty、AllocSetStats 和AllocSetCheck 。后面我们将看到,对于内存上下文的操作都是通过这些函数来实现的。
在任何时候,都有一个"当前"的 MemoryContext ,记录在全局变量 CurrentMemoryContext 里, 进程就在这个内符上下文中调用palloc函数来分配内存。在需要变换内存上下文时,可以使用 MemoryContextSwitchTo 函数将 CurrentMemoryContext 指向其他的内存上下文。
例如:
oldcontext = MemoryContextSwitchTo(TopMemoryContext );
...
fctx = palloc(sizeof(directory_fctx));
...
MemoryContextSwitchTo(oldcontext);
//MemoryContextSwitchTo函数实现
MemoryContextSwitchTo(MemoryContext context)
{
MemoryContext old = CurrentMemoryContext;
CurrentMemoryContext = context;
return old;
}
-
内存上下文的初始化和创建
3.1 初始化内存上下文
当使用pg_ctl或postmaster(postgres)启动PostgreSQL数据库服务时候,在main()函数中会先对名为TopMemoryContext、CurrentMemoryContext和ErrorContext的内存上下文全局变量进行初始化操作,由函数MemoryContextInit()负责完成。其中TopMemoryContext是所有其他内存上下文的父节点。
在初始化时首先创建所有内存 下文的根节点TopMemoryContext,然后在该节点下创建子节点ErrorContext用于错误恢复处理:
• TopMemoryContext: 该节点在分配后将一直存在,直到系统退出时候才释放。在该节点下面分配其他内存上下文节点本身所占用的空间
• ErrorContext: 该节点是 TopMemoryContext 的第一个子节点,是错误恢复处理的永久性内存上下文 ,恢复完毕就会进行重置。
void MemoryContextInit(void)
{
//封装的assert()断言函数的功能, 即AssertState括号中的判别式需要为true
AssertState(TopMemoryContext == NULL);
/* 首先, 初始化TopMemoryContext,它是所有其他内存上下文的父节点. */
TopMemoryContext = AllocSetContextCreate((MemoryContext) NULL,
"TopMemoryContext",
ALLOCSET_DEFAULT_SIZES);
/* 没有任何其他位置指向 CurrentMemoryContext,请将其指向TopMemoryContext. 调用者应尽快更改此选项! */
CurrentMemoryContext = TopMemoryContext;
/*
将 ErrorContext 初始化为一个增长速度较慢的 AllocSetContext ——我们并不真正期望在其中分配太多内存.
更重要的是,要求它在任何时候至少8K. 只有在这种情况下,上下文中的保留内存才是必不可少的——
我们要确保ErrorContext仍然有一些内存,即使我们在其他地方已经用完了! 此外,
允许在关键部分的ErrorContext中进行分配.否则,在打印出失败的真正原因之前,
死机将导致错误报告代码中的断言失败.
这应该是此函数的最后一步,因为elog.c假设一旦ErrorContext为非空,内存管理器就可以工作.
*/
ErrorContext = AllocSetContextCreate(TopMemoryContext,
"ErrorContext",
8 * 1024,
8 * 1024,
8 * 1024);
MemoryContextAllowInCriticalSection(ErrorContext, true);
}
3.2 内存上下文节点的创建
当初始化完毕,建立根节点和错误恢复子节点后, PostgreSQL 进程就可以开始创建其他的内存上下文。内存上下文的创建由 AllocSetContextCreate 函数来实现,主要有两个工作:创建内存上下文节点以及分配内存块。
例如:我们想自己创建一个内存上下文,在定义和声明一个内存上下文之后,可以用AllocSetContextCreate来创建:
if (!MyTopMemoryContext){
MyTopMemoryContext = AllocSetContextCreate(TopMemoryContext,
"MyTopMemoryContext",
ALLOCSET_DEFAULT_SIZES);
}
3.2 内存上下文中内存的分配、重分配和释放
Postgres内存的分配、重分配和释放都是在内存上下文中进行,因此不再使用的标准库函数malloc、 realloc、 free 来操作, PostgreSQL 实现了 palloc、repalloc、pfree 来分别实现内存上下文中对于内存的分配、重分配和释放。
palloc(内存分配)
palloc 是一个宏定义, 它会被转换为在"当前"内存上下文中对 MemoryContextAlloc 函数的调用,而 MemoryContextAlloc 函数实际上是调用了"当前"内存上下文的 methods 段中所指定的alloc 函数指针。 目前postgresSql的实现来说,用palloc 实际就是调用 alloc 指向的AllocSetAlloc 函数。
void *
palloc(Size size)
{
/* duplicates MemoryContextAlloc to avoid increased overhead */
void *ret;
AssertArg(MemoryContextIsValid(CurrentMemoryContext));
AssertNotInCriticalSection(CurrentMemoryContext);
if (!AllocSizeIsValid(size))
elog(ERROR, "invalid memory alloc request size %zu", size);
CurrentMemoryContext->isReset = false;
//调用palloc 实际就是调用 alloc 指向的AllocSetAlloc 函数
ret = (*CurrentMemoryContext->methods->alloc) (CurrentMemoryContext, size);
if (ret == NULL)
{
MemoryContextStats(TopMemoryContext);
ereport(ERROR,
(errcode(ERRCODE_OUT_OF_MEMORY),
errmsg("out of memory"),
errdetail("Failed on request of size %zu.", size)));
}
VALGRIND_MEMPOOL_ALLOC(CurrentMemoryContext, ret, size);
return ret;
}
AllocSetAlloc(内存分配)
-
函数AllocSetAlloc负责处理具体的内存分配工作,该函数的参数为一个内存上下文节点以及需要申请的内存大小。
-
AllocSetAlloc为内存片分配空间时并不是严格按照申请的大小来分配的,而是将申请的大小向上对齐为2的幂,然后按照对齐的大小来分配空间。例如,我们要申请一块大小 30 字节的空间,则 AllocSetAlloc实际会为我们分配1块大小为 2^5=32 节的内存片。 该内存片对应的AlloChunkData 中的 size 段设置为 32 ,而 requested_size 设置为 30。
repalloc(内存重分配)
内存重分配由AllocSetRealloc函数实现, AllocSetRealloc将在指定的内存上下文中对参数 pointer 指向的内存空间进行重新分配,新分配的内存大小由参数 size 指定。
释放内存上下文
释放内 文中的内存, 要有以下3种方式:
-
释放1个内存上下文中指定的内存片
当释放一个内存上下文中指定的内存片时,调用函数AllocSetFree 。该函数的执行方式如下:
1) 如果指定要释放的内存片是内存块中唯一的1个内存片,则将该内存块直接释放。
2) 否则,将指定的内存片加入到 Feelist 链表中以便下次分配。
-
重置内存上下文
重置内存上下文的工作由函数AllocSetReset完成 。在进行重置时,内存上下文中除了在 keeper字段中指定要保留的内存块外,其他内存块全部释放,包括空闲链表中的内存。 keeper 中指定保留的内存块将被清空内容,它使得内存上下文重置之后就立刻有一块内存可供使用。
-
释放当前内存上下文中的全部内存块
这个工作由 AllocSetDelete 函数完成,该函数释放当前内存上下文中的所有内存块,包括 keeper指定的内存块在内。但内存上下文节点并不释放,因为内存上下文节点是在 TopMemoryContext 中申请的内存,将在进程运行结束时统一释放。