Postgres 内核:从入门到“入土” (一) —— MemoryContext:数据库里的“分赃”艺术

今天我们要聊的,是数据库内核的“命根子”——内存管理

1. 为什么我们不用 malloc

写 C 语言的同学都知道,mallocfree 就像是婚姻:申请(结婚)容易,释放(离婚)难。在复杂的数据库内核里,一个查询请求发过来,中间可能经过解析、优化、执行,涉及成千上万个小的内存分配。

TopMemoryContext: 老祖宗

PostmasterContext: 守护进程

CacheMemoryContext: 缓存

ErrorContext: 报错专用

MessageContext: 连接请求

ExecutorState: 执行状态

ExprContext: 表达式计算

Postgres 的先贤们(伯克利的聪明脑袋们)想出了一个**“餐巾纸策略”**:

别管什么 free 了!我给每个查询请求开一桌菜,铺上一张特制的“桌布”。等这桌客人吃完走人,我直接拎起桌布四角,管你是剩菜残羹还是满桌油腻,连桌布带餐具全部“咔嚓”一下扔进垃圾桶!

这就是 MemoryContext(内存上下文) 的精髓。

2. 深入源码:那张“桌布”长啥样?

在 PG 源码中,所有的内存上下文都抽象成了一个结构体(没错,我们用 C 写出了 OOP 的感觉)。

阵地就在:src/include/utils/memutils.hsrc/backend/utils/mmgr/mcxt.c

// 简化后的 MemoryContextData
typedef struct MemoryContextData
{
    NodeTag     type;           /* 它是哪种上下文?通常是 AllocSetContext */
    MemoryContextMethods *methods; /* 它是怎么分配内存的?(类似于 C++ 的虚函数表) */
    MemoryContext parent;       /* 谁是它的亲爹? */
    MemoryContext firstchild;    /* 它的第一个孩子 */
    MemoryContext nextchild;     /* 它的兄弟 */
    char       *name;           /* 它的名字,方便我们调试(比如 "PortalContext") */
} MemoryContextData;

重点:这是一个树状结构。
当你销毁一个父上下文时,它会自动递归销毁所有子上下文。比如,销毁整个“查询上下文”(QueryContext),那么这个查询过程中产生的所有临时计算内存、排序缓冲区,全都会瞬间灰飞烟灭。

3. 核心机制:AllocSet(内存分配集)

在 PG 里,最常用的上下文实现叫做 AllocSet(代码在 src/backend/utils/mmgr/aset.c)。它并不是每次调用 palloc 就去求内核给内存,而是先找内核要一大块(Block),然后再从小块里切(Chunk)给你。

AllocSet_Context

AllocBlock 8KB

Chunk 32B

Chunk 64B

Free Space

Next AllocBlock

这里的逻辑极其抠门(为了性能):

  • AllocBlock:就像是批发商的一箱货(通常是 8KB 甚至更大)。
  • AllocChunk:就像是你从箱子里拿出来的一瓶可乐。

当你调用 palloc(size) 时,PG 会做以下几件事:

  1. 查查小本子(Freelists):如果你要分配的大小刚好是某些固定尺寸(比如 8, 16, 32… 字节),PG 会先去它的“二手回收站”(Freelist)看看有没有之前用剩的。
  2. 切大块(CurrentBlock):如果没有现成的,就从当前的大块里切。
  3. 批发(Malloc):如果当前大块都填满了,就找系统 malloc 一个新大块。

4. 那个叫 palloc 的家伙

在 PG 源码里,你几乎看不到 malloc,取而代之的是 palloc

void *palloc(Size size);

它和 malloc 的最大区别是:它不需要你传上下文指针!它会自动把内存分在**当前活动的上下文(CurrentMemoryContext)**里。

这就像是你去食堂吃饭,只要你坐下了(设置了 CurrentMemoryContext),你喊一声“上菜!”,菜就会自动端到你这张桌子上,不用你每次都交待你是哪桌的。

5. 高级黑:为什么会有 OOM?

虽然 MemoryContext 这么牛逼,但如果你在一个大循环里拼命 palloc 却从不清理,或者忘记了切换上下文,你依然会把服务器内存撑爆。

作为资深开发者,我给你的忠告是:永远要注意 CurrentMemoryContext 的位置。

// 危险操作:在全局上下文中循环分配
MemoryContextSwitchTo(TopMemoryContext); // 切换到了“皇宫级”上下文,永不回收
for (int i = 0; i < 1000000; i++) {
    char *p = palloc(1024); // 你完了,这 1G 内存直到数据库重启才会还给系统
}

6. 源码深潜:指针背后的秘密

既然咱们都要“入土”了,那就不能只停留在 API 层面。当你拿到一个 palloc 返回的 void * 指针时,它前面到底藏了什么?

6.1 隐形的头部:AllocChunk

aset.c 里,每一块被切出来的内存,前面都有一个“头”:

typedef struct AllocChunkData
{
    /* 这里的两个字段非常关键! */
    Size        size;           /* 这块内存到底多大?free 的时候我就不问你了 */
    void       *context;        /* 它是属于哪个 Context 的?(其实这里为了对齐做了很多黑魔法) */
    
    /* 后面才是你拿到的数据指针 */
} AllocChunkData;

所以,当你调用 pfree(ptr) 时,PG 会悄悄地做一步 ptr - sizeof(AllocChunkData),把指针往回倒一点,找到这个头部,从而知道该释放多少内存,以及归还给哪个 Context。

这也是解释了为什么如果你发生了缓冲区溢出(Buffer Overflow),往往会把下一个 Chunk 的头部踩坏,导致莫名其妙的 Crash。

6.2 C 语言的虚函数表:MemoryContextMethods

Postgres 是如何实现多态的?看看 src/include/utils/memutils.h

typedef struct MemoryContextMethods
{
    void       *(*alloc) (MemoryContext context, Size size);
    /* ... */
    void        (*free_p) (MemoryContext context, void *pointer);
    void       *(*realloc) (MemoryContext context, void *pointer, Size size);
    void        (*reset) (MemoryContext context);
    void        (*delete_context) (MemoryContext context);
    /* ... */
} MemoryContextMethods;

每一个 Context 实例(比如 AllocSet 或 Slab)都带这么一张表。当你调用 palloc 时,实际上是在执行:
CurrentMemoryContext->methods->alloc(...)

这绝对是 C 语言实现面向对象编程的教科书级案例。

总结:

Postgres 的内存管理就像是一个等级森严的家族企业:

  • TopMemoryContext:老祖宗,死掉之前决不散伙。
  • PostmasterContext:家里的二把手。
  • PortalContext/ExecutorState:勤劳的打工人,干完活就消失。

这种设计的核心理念就是:批量管理优于碎片管理,自动化回收优于手动清理。


下回预告
下一回,我们将聊聊那个“整天蹲在大门口的保镖”——Postmaster。看看它是如何从一个老父亲的角度,打理整个数据库进程帝国的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值