土法炼钢兴趣小组的算法知识备份

【PG 内核】进程模型与共享内存:Postmaster 如何管理 100 个 Backend

文章导航

分类入口
databasekernel
标签入口
#postgresql#pg-kernel#postmaster#shared-memory#backend-process#pgproc#lwlock#background-worker#fork#ipc

目录

进程模型与共享内存:Postmaster 如何管理 100 个 Backend

如果你用 ps aux | grep postgres 看过 PG 的进程列表,大概率被一堆叫 postgres 的进程吓到过。每个客户端连接都是一个独立的 OS 进程——这和 MySQL(线程模型)、MongoDB(线程模型)、Redis(单线程)都不一样。

多进程意味着 fork() 的代价、共享内存的复杂协调、信号驱动的父子通信。但 PG 的进程模型不是”老派设计”——它把 OS 的进程隔离和信号机制变成了故障隔离和资源管理的工程优势。

本文从源码路径拆解 PG 的进程骨架:Postmaster 如何孵化和管理子进程、共享内存里塞了哪些关键数据结构、Backend 从 fork() 到退出的完整生命周期。读完你就能对着 ps 输出判断每个进程在干什么、为什么 max_connections 设太高比你想的更危险。


一、PG 的进程全景

进程类型速览

进程类型 进程名示例 职责
Postmaster postgres: postmaster 进程树根节点,监听新连接、fork Backend、处理子进程 crash
Backend postgres: user db 10.0.0.1(54321) SELECT 处理一条客户端连接的所有 SQL
Background Writer postgres: background writer 定期将 shared_buffers 脏页写入磁盘
WAL Writer postgres: walwriter 将 WAL buffer 刷到 WAL 文件
Checkpointer postgres: checkpointer 执行 checkpoint
Autovacuum Launcher postgres: autovacuum launcher 监控需要 VACUUM 的表,fork Autovacuum Worker
Autovacuum Worker postgres: autovacuum worker db 执行具体的 VACUUM / ANALYZE 操作
WAL Sender postgres: walsender user 把 WAL 发送给 standby(流复制)
WAL Receiver postgres: walreceiver standby 上接收 WAL
Logical Replication Worker postgres: logical replication worker 逻辑复制的 apply worker
Background Worker postgres: bgworker: name 用户注册的自定义后台进程(如 pg_cronpg_stat_statements 的 hash 表清理)

这是 PG 的多进程设计哲学:每种职责一个独立进程,进程之间通过共享内存和信号通信。一个 Backend crash 不会影响其他连接,因为 OS 保证了进程的地址空间隔离。

进程树结构

flowchart TD
  PM["postmaster<br/>(PID=pid1)"] --> BG["background writer"]
  PM --> WA["walwriter"]
  PM --> CK["checkpointer"]
  PM --> AL["autovacuum launcher"]
  PM --> BE1["backend 1<br/>(client connection)"]
  PM --> BE2["backend 2<br/>(client connection)"]
  PM --> BE3["backend N<br/>(client connection)"]
  PM --> WS["walsender(s)"]
  AL --> AV1["autovacuum worker 1"]
  AL --> AV2["autovacuum worker 2"]
```text

Autovacuum Launcher 是唯一会自己 fork 子进程的辅助进程(fork Autovacuum Worker);其他所有进程都由 Postmaster 直接创建。

---

## 二、Postmaster:进程树的根

### 入口路径

Postmaster 的启动入口在 `src/backend/postmaster/postmaster.c` 的 `PostmasterMain()` 函数。调用链:

main() // src/backend/main/main.c → PostmasterMain(argc, argv) // src/backend/postmaster/postmaster.c → InitializeGUCOptions() → set up signal handlers // SIGHUP, SIGTERM, SIGINT, SIGQUIT, SIGCHLD → StreamServerPort(AF_UNSPEC) // 对每个 listen_addresses 创建 socket → ServerLoop() // 主循环:accept → fork backend


关键信号处理:

| 信号 | 处理方式 | 效果 |
|------|---------|------|
| `SIGTERM` | `pmdie = SIGTERM` → 等待所有子进程退出 → shutdown | 智能关闭(smart shutdown),等所有连接自然断开 |
| `SIGINT` | `pmdie = SIGINT` → 回滚在线事务 → 断开连接 → shutdown | 快速关闭(fast shutdown) |
| `SIGQUIT` | `pmdie = SIGQUIT` → 立即 SIGKILL 所有子进程 → 退出 | 立即关闭(immediate shutdown),crash recovery 在下次启动时执行 |
| `SIGHUP` | `ProcessConfigFile()` → 所有 Backend 重新加载配置 | reload 配置文件,不重启 |
| `SIGCHLD` | 子进程退出通知 | 回收子进程、决定是否 restart |

```c
// src/backend/postmaster/postmaster.c, PostmasterMain()
// 信号处理器注册(简化)
pqsignal(SIGHUP,  SIGHUP_handler);    // reload config
pqsignal(SIGTERM, pmdie);             // smart shutdown
pqsignal(SIGINT,  pmdie);             // fast shutdown
pqsignal(SIGQUIT, pmdie);             // immediate shutdown
pqsignal(SIGCHLD, reaper);            // child process cleanup

SIGCHLD 的处理器 reaper() 是 Postmaster 最关键的函数之一——当 Backend crash 时,Postmaster 通过 waitpid() 回收子进程状态,并决定是否 restart restart_after_crash=on 的所有子进程。

ServerLoop:accept 和 fork

// src/backend/postmaster/postmaster.c, ServerLoop()
// 主循环(简化伪代码)
for (;;) {
    // 1. select() 等待新连接或子进程信号
    selres = WaitEventSetWait(pm_wait_set, ...);

    // 2. 如果是新连接
    if (socket ready) {
        port = ConnCreate(AF_UNSPEC);
        port->sock = accept(listen_socket, ...);

        // 3. fork Backend 进程
        pid = fork_process();
        if (pid == 0) {
            // 子进程:不再需要 Postmaster 的资源
            ClosePostmasterPorts(false);   // 关闭监听 socket
            InitPostmasterChild();         // 注册到 PMChildFlags
            BackendMain(port);             // → 进入 Backend 主循环(永不返回)
        }
        // 父进程:记录子进程信息,回到 select() 等下一个事件
        BackendStartup(port);
    }

    // 4. 如果是 SIGCHLD,reaper() 已经在信号处理器中回收了子进程
}
```text

注意 `fork_process()` 是一个对 `fork()` 的包装——在 fork 之前它先获取 `postmaster_lock` 以保证 fork 的原子性,防止多个 Postmaster 子进程同时 fork 引发竞态。

---

## 三、Backend 进程:从 fork 到查询执行

### 生命周期全景

```mermaid
sequenceDiagram
  participant Client
  participant PM as Postmaster
  participant Backend

  Client->>PM: TCP connect
  PM->>PM: ServerLoop accept()
  PM->>Backend: fork()
  Backend->>Backend: ClosePostmasterPorts()
  Backend->>Backend: InitPostmasterChild()
  Backend->>Backend: InitProcess() — 绑定到 PGPROC slot
  Backend->>Backend: InitPostgres() — 加载 catalog cache, 设置搜索路径
  Backend->>Backend: PostgresMain() 主循环
  loop 每一条 SQL
    Client->>Backend: SQL query
    Backend->>Backend: exec_simple_query()
    Backend->>Client: result
  end
  Client->>Backend: disconnect
  Backend->>Backend: ProcKill() — 释放 PGPROC, 释放锁
  Backend->>Backend: exit(0)
  PM->>PM: SIGCHLD → reaper() 回收

InitPostgres:最关键的三件事

InitPostgres() 在 fork 之后、第一条 SQL 执行之前运行。它做三件关键的事:

1. 绑定 SharedInvalidationCatCache — 订阅共享失效消息。当另一个 Backend 执行 DDL(比如 ALTER TABLE)时,catalog cache 需要被通知失效。这是通过 SI Message Queue(共享内存中的一个环形缓冲区)实现的——发送方写入失效消息,所有订阅方在下一个 AcceptInvalidationMessages() 调用时处理。

2. 初始化 Catalog Cache — PG 的 catalog(pg_classpg_attributepg_proc 等)是 Relation Cache (relcache) 和 Catalog Cache (catcache) 的后端。InitPostgres() 加载 pg_databasepg_authidpg_tablespace 等关键系统表的 entries。

3. 初始化 Portal 和 Resource Owner — Portal 是”执行单元”的抽象(一条 SQL 或一个 CURSOR),Resource Owner 跟踪每个事务分配的内存、buffer pin、锁等资源。

// src/backend/utils/init/postinit.c, InitPostgres()
// 简化调用链
void InitPostgres(const char *in_dbname, ...) {
    // 1. 与共享内存中的 PGPROC entry 关联
    InitProcess();                    // → 获取 PGPROC slot, 设置 MyProc

    // 2. 初始化 relcache / catcache / plan cache
    InitPostgres_RelCache();
    InitPostgres_CatCache();

    // 3. 设置搜索路径、时区等 session 参数
    InitializeSession();

    // 4. 启动事务以读取 catalog
    StartTransactionCommand();
    // ... 加载数据库 OID, namespace, 权限信息 ...
    CommitTransactionCommand();
}
```bash

### PostgresMain:查询处理主循环

```c
// src/backend/tcop/postgres.c, PostgresMain()
// 主循环(简化)
for (;;) {
    // 1. 读取客户端消息(SQL 查询)
    firstchar = ReadCommand(&input_message);

    switch (firstchar) {
    case 'Q':   // simple query
        exec_simple_query(input_message.data);
        break;
    case 'P':   // parse (extended query protocol)
        exec_parse_message(...);
        break;
    case 'B':   // bind (extended query protocol)
        exec_bind_message(...);
        break;
    case 'E':   // execute (extended query protocol)
        exec_execute_message(...);
        break;
    case 'X':   // terminate
        goto normal_exit;
    }
    // 2. 处理共享失效消息(DDL 通知)
    AcceptInvalidationMessages();
}

PG 支持两种查询协议:Simple Query(一条 Q 消息发整个 SQL 字符串)和 Extended Query(Parse → Bind → Execute 分三步,支持 prepared statement 和参数化查询)。JDBC、libpq 的 PQexecParams() 使用 Extended 协议;psql 默认使用 Simple 协议。


四、共享内存初始化:CreateSharedMemoryAndSemaphores

CreateSharedMemoryAndSemaphores() 是 PG 启动时最长也最关键的初始化函数之一。它创建各个子系统所需的共享内存段和信号量,并且由 Postmaster 一次性分配,然后 fork 给子进程继承——这就是为什么 Postmaster 必须是所有 Backend 的父进程。

调用时序

PostmasterMain()
  → CreateSharedMemoryAndSemaphores()
    1. 计算总共享内存大小
    2. shmget() / mmap() 分配共享内存段
    3. PGShmemHeader 初始化
    4. 分配信号量集合
    5. 按子系统顺序初始化共享内存数据:
       a. ShmemInitHash("LOCK hash", ...)         // 锁表
       b. ShmemInitHash("Proc Signal hash", ...)  // 进程间信号
       c. ShmemInitHash("Buffer Table", ...)      // Buffer 查找表
       d. InitBufferPool()                        // shared_buffers
       e. InitLockTable()                         // 重量级锁
       f. InitProcGlobal()                        // PGPROC 数组
       g. XLOGShmemInit()                         // WAL buffer
       h. InitLWLocks()                           // LWLock 数组
       i. InitBufferStrategy()                    // Clock sweep 状态
       ...

关键是分配顺序——Postmaster 在 fork 之前一次性分配所有共享内存,子进程通过继承指针直接访问。如果子进程自己做 shmget(),不但重复分配,而且每个 Backend 有自己的独立视图,无法通信。

// src/backend/storage/ipc/ipci.c, CreateSharedMemoryAndSemaphores()
// 共享内存大小的计算(简化)
size = add_size(size, ShmemBackendArraySize());
size = add_size(size, BufferShmemSize());        // shared_buffers
size = add_size(size, LockShmemSize());
size = add_size(size, ProcGlobalShmemSize());
size = add_size(size, XLOGShmemSize());
size = add_size(size, BTreeShmemSize());
size = add_size(size, LWLockShmemSize());
// ... 以及后面每个模块的 request
```bash

### 共享内存的寻址

所有进程通过 `ShmemIndex`(一个 hash table)查找共享内存对象——类似操作系统中的"符号表"。每个子系统在初始化时调用 `ShmemInitStruct(name, size, found)`,第一次调用分配空间,后续调用返回已有空间的指针。

```c
// 示例:ProcGlobal 的分配
ProcGlobal = (PROC_HDR *)
    ShmemInitStruct("Proc Header", ProcGlobalShmemSize(), &found);
if (!found) {
    // Postmaster 初始化路径:清零并设置
    ProcGlobal->FreeProcs = ...;
}
// Backend 路径:found == true,直接使用

五、关键共享内存结构

PGPROC 与 ProcArray

PGPROC 是每个 Backend 在共享内存中的”名片”。ProcArray 是所有 PGPROC 的数组:

// src/include/storage/proc.h
struct PGPROC {
    // 基础标识
    int             pgprocno;           // 在 ProcArray 中的索引
    BackendId       backendId;          // 在 PGPROC 数组之外的唯一 ID

    // 事务信息
    TransactionId   xid;                // 当前事务 ID(只读事务为 InvalidTransactionId)
    TransactionId   xmin;               // 快照的最小活跃事务
    LocalTransactionId lxid;            // 本 Backend 的本地事务 ID

    // 锁等待
    LOCK           *waitLock;           // 正在等的锁
    PGPROC         *lockGroupLeader;    // 并行查询中 leader 的 PGPROC
    int             waitStatus;         // STATUS_OK / STATUS_WAITING

    // LWLock 等待
    LWLock         *lwWaiting;          // 正在等的 LWLock
    int             lwWaitMode;         // LW_SHARED / LW_EXCLUSIVE

    // 快照
    XidCache        subxids;            // 缓存的子事务 ID 列表

    // 信号
    sem_t          *sem;                // 本 Backend 的信号量(用于锁等待唤醒)
};
```text

`ProcArray` 是所有活跃 Backend 的 `PGPROC` 的数组,定义在 `src/backend/storage/ipc/procarray.c`。它被快照构建(`GetSnapshotData()`)频繁扫描——这是 PG 的一个可扩展性热点。在 PG 14 之前,`GetSnapshotData()` 需要用 `ProcArrayLock` 的共享锁保护整个扫描;在有大量连接的场景下(如 1000+ 连接),`ProcArrayLock` 成为瓶颈。PG 14 引入了 `xid`/`xmin` 的 per-backend 进度原子更新,大幅减少了需要持锁的代码路径。

### PGXACT

`PGXACT` 是 `PGPROC` 的轻量级伴随结构,把 `GetSnapshotData()` 扫描需要读的字段从 `PGPROC` 中分离出来,减少 CPU cache miss:

```c
// src/include/storage/proc.h
struct PGXACT {
    TransactionId  xid;      // 当前事务 ID
    TransactionId  xmin;     // 最小活跃事务
    uint8          nxids;    // 缓存的子事务数
    uint8          vacuumFlags;  // PROC_IN_VACUUM / PROC_IS_AUTOVACUUM
};

LWLock 数组

LWLock 是 PG 内部使用最频繁的轻量级锁。所有 LWLock 在共享内存中作为 LWLockPadded 数组分配:

// src/include/storage/lwlock.h
typedef union LWLockPadded {
    LWLock  lock;
    char    pad[LWLOCK_PADDED_SIZE];  // CPU cache line padding(避免 false sharing)
} LWLockPadded;

// 在共享内存中:
LWLockPadded *MainLWLockArray;  // 全局 LWLock 数组,每个子系统的锁有固定索引
```text

PG 17 中 LWLock 的实现使用原子操作(`pg_atomic_*`)而非信号量——对短临界区来说,自旋等待比 `semop()` 快得多。只有当自旋超过一定次数后,等待者才会 `semop()` 进入内核等待。这个设计在"锁持时间极短"的常见场景中很高效,但如果某处代码持 LWLock 做了重 I/O 或函数调用链过深,就会触发 LWLock 争用风暴。

---

## 六、Background Worker:可编程的后台进程

PG 9.3 引入了 Background Worker 框架,允许扩展注册自定义后台进程。`pg_stat_statements`(定期合并统计信息)、`pg_cron`(定时任务调度器)、`Citus` 的分布式执行器都基于这个框架。

### 注册与调度

Worker 在 `shared_preload_libraries` 加载时通过 `RegisterBackgroundWorker()` 注册到共享内存中的 `BackgroundWorkerArray`。Postmaster 在 `ServerLoop` 中检查是否有 worker 需要启动,并 fork:

```c
// src/backend/postmaster/bgworker.c
void maybe_start_bgworkers(void) {
    for (slot = 0; slot < max_worker_processes; slot++) {
        if (worker 注册了且还没启动) {
            pid = fork_process();
            if (pid == 0) {
                // 子进程
                InitPostmasterChild();
                StartBackgroundWorker();
            }
        }
    }
}

Worker 类型有:BgWorkerStart_PostmasterStart(Postmaster 启动后就启动)、BgWorkerStart_RecoveryFinished(crash recovery 完成后启动)、BgWorkerStart_ConsistentState(达到一致状态后启动)。

Worker 的限制


七、为什么是进程而不是线程

这是 PG 最常被问的问题。MySQL 和 InnoDB 用线程处理连接,PG 用进程。为什么?

维度 进程模型 (PG) 线程模型 (MySQL)
内存隔离 OS 保证——crash 不污染其他连接 需自行保证——野指针可能破坏整个进程
fork 开销 ~5-10MB 私有内存/连接,fork 本身 ~0.5ms 创建线程 ~0.05ms,共享地址空间
1000 连接时的内存 5-10GB(只有私有内存) 100-500MB(共享代码段和 buffer pool)
共享缓存 shared_buffers 在 fork 之前分配,被所有 Backend 继承 InnoDB buffer pool 在所有线程间可见
信号处理 每个进程独立信号 mask,SIGCHLD 通知父进程 线程间信号混乱,通常不用信号做 IPC
调试 gdb attach 单个 Backend 即可 需要在多线程中导航

PG 的进程模型在一个关键点上很聪明:所有共享状态在 fork 之前分配在共享内存中,fork 之后的私有内存浪费只来自 Backend 自己的栈、堆、catalog cacheshared_buffers(通常是内存大户)在所有进程间共享,不计入每个 Backend 的私有内存。

真正的风险不是 fork() 本身(0.5ms),而是 max_connections 设太高导致的工作内存膨胀——每个 Backend 可能同时使用多个 work_mem 配额(参考 配置陷阱)。


八、实验:观察 PG 的进程树和共享内存

进程树

# 观察 Postmaster 及其直接子进程
ps -eo pid,ppid,comm,args | grep postgres | grep -v grep

# 示例输出(PID 列 → PPID 列 → 进程名 → 参数):
# 1024     1  postgres  /usr/local/pgsql/bin/postgres -D /data/pgdata
# 1026  1024  postgres  postgres: checkpointer
# 1027  1024  postgres  postgres: background writer
# 1028  1024  postgres  postgres: walwriter
# 1029  1024  postgres  postgres: autovacuum launcher
# 1030  1024  postgres  postgres: user db 10.0.0.1(54321) SELECT
```text

注意所有子进程的 PPID 都是 Postmaster 的 PID(1024)。

### 共享内存段

```bash
# 查看 PG 分配的共享内存段
ipcs -m | grep postgres

# 每个 PG 集群通常有 1-3 个共享内存段(取决于大小和平台)
# 0x0052e2c1 32769     postgres 600  134217728  1
# 13GB shared_buffers + 其他共享结构

用 GDB 观察 PGPROC

# attach 到一个 Backend
gdb -p $(pgrep -f "postgres: user")

# 打印当前 Backend 的 PGPROC 结构
(gdb) p *MyProc
# 输出:xid, xmin, waitLock, pgprocno 等字段

# 查看 ProcArray 中的活跃事务数
(gdb) p ProcGlobal->allPgXact
```bash

### 连接开销的量化

```bash
# 用 pgbench 测试不同连接数下的内存和 fork 开销
pgbench -i -s 100 postgres

# 测量 10 个连接
pgbench -c 10 -T 30 postgres &
sleep 2
ps -eo pid,rss,comm | grep "postgres: user" | awk '{sum+=$2} END {print "Total RSS (KB):", sum}'

# 测量 100 个连接
pgbench -c 100 -T 30 postgres &
sleep 2
ps -eo pid,rss,comm | grep "postgres: user" | awk '{sum+=$2} END {print "Total RSS (KB):", sum}'

你会观察到每个 Backend 的 RSS 初始在 5-10MB,但会随着 catalog cache 的增长而上升。


九、关键要点

  1. PG 是多进程架构——Postmaster 是根节点,每个连接是一个独立的 OS 进程。进程隔离是故障隔离的基础。
  2. 共享内存在 fork 之前一次性分配——Postmaster 在 CreateSharedMemoryAndSemaphores() 中分配所有共享内存段,子进程通过继承指针直接访问。不需要每个 Backend 做 shmget()
  3. PGPROC 和 ProcArray 是核心枢纽——GetSnapshotData() 扫描 ProcArray 获取活跃事务列表,这是快照隔离的基础。大量连接时这是性能热点。
  4. Background Worker 框架让扩展可以注册自定义后台进程——pg_cronpg_stat_statements 等扩展都基于此。
  5. 进程模型的代价是每个连接 ~5-10MB 私有内存——max_connections 设太高真正的风险不在 fork,而在 work_mem × 连接数 × 操作数 的内存爆炸。

系列索引:PostgreSQL 内核机制深度拆解 下一章:页面布局与元组格式,拆解 PG 的 8KB 页面物理布局、Heap Tuple 的 header 结构(xmin/xmax/ctid/t_infomask)、TOAST 外存机制——理解数据如何在磁盘和内存中表示。


参考资料

源码(PG 17)

官方文档

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-16 · database / kernel

【PG 内核】锁管理器:从 SpinLock 到死锁检测的三层体系

拆解 PostgreSQL 锁管理器的完整架构:SpinLock 自旋锁的硬件原语与使用边界、LWLock 从 PG 9.4 前到 PG 16 LWLockWaitListLock 的三代演进、Heavyweight Lock 的 LockAcquire() 完整路径和锁表结构、死锁检测 DeadLockCheck() 的等待图 DFS 算法、行级锁 FOR UPDATE/FOR SHARE/FOR KEY SHARE 的 t_infomask 实现,以及用 pg_locks 和 pg_blocking_pids() 追踪生产锁等待链的诊断方法。

2026-06-16 · database / kernel

【PG 内核】性能异常调查方法论:从现象到内核根因的五层调查链

不是工具箱罗列,而是一条按顺序推进的调查链:从 pg_stat_statements 定位可疑 queryid,到 EXPLAIN (ANALYZE, BUFFERS) 解剖执行计划,到 pg_stat_activity + wait_event 诊断等待类型,到 pg_locks + pg_blocking_pids() 追踪锁等待树,最后用 OS 层工具(iostat/perf/bpftrace)确认物理瓶颈。覆盖三个特殊场景:计划缓存的快慢切换、CPU 100% 无慢查询的 LWLock 自旋根因、命中率 99% 但 IO 打满的统计骗局。

2026-06-16 · database / kernel

【PG 内核】页面布局与元组格式:PG 如何把一行数据塞进 8KB

拆解 PostgreSQL 的物理存储层:Page 的 8KB 布局(PageHeaderData、ItemId 数组、special space)、HeapTupleHeaderData 的字段语义(xmin/xmax/ctid/t_infomask/t_infomask2)、TOAST 外存机制的压缩阈值与四种策略(PLAIN/EXTENDED/EXTERNAL/MAIN),以及用 pageinspect 扩展直接观察页面字节。理解页面格式是理解 VACUUM、Index Scan、MVCC 可见性判断的共同前提。

2026-06-16 · database / kernel

【PG 内核】MVCC 实现:CLOG、hint bit 与快照可扩展性

在已有 MVCC 文章基础上深入 PG 并发控制的三个基础设施:CLOG 的 SLRU 结构(事务状态位、页面格式、SLRU 淘汰)、hint bit 的写入时机和竞争问题(何时写、谁写、写坏了怎么办)、PG 14 snapshot scalability 优化的具体机制(ProcArrayLock 为什么是瓶颈、xid/xmin 的原子更新如何减少持锁路径),以及事务 ID 回卷(wraparound)的威胁模型。最后与 InnoDB undo log 方案做系统性对比。


By .