콘텐츠로 이동

(KO) PostgreSQL 공유 메모리 & IPC — 정적 세그먼트, 동적 공유 메모리, shm_mq 메시지 계층

목차

멀티프로세스 데이터베이스는 모든 워커가 동일한 공유 상태를 볼 수 있어야 한다. 버퍼 풀, 락 테이블, 트랜잭션 상태 배열, 그 밖의 수십 가지 구조체가 모두 여기 해당한다. 전통적인 접근법은 두 가지다.

첫째는 **메시지 패싱(message passing)**이다. 프로세스들은 주소 공간을 공유하지 않고 파이프, 소켓, OS 메시지 큐로 상태를 주고받는다. 격리가 명확하고 앨리어싱 버그가 없다는 장점이 있다. 그러나 공유 상태를 읽을 때마다 커널 진입과 복사가 발생한다.

둘째는 **공유 메모리(shared memory)**다. 모든 프로세스가 같은 가상 페이지를 매핑한다. 읽기와 쓰기는 load/store 명령어로 처리되며 커널 호출이 없다. 초기 매핑 비용만 지불하면 이후 접근은 일반 메모리 연산과 같다. 다만 보호는 공유 영역 안에 내장된 경량 락이나 원자 연산으로 프로세스가 직접 담당해야 한다.

PostgreSQL은 명확히 두 번째 방식을 택한다. postmaster가 fork()하는 모든 백엔드, 보조 프로세스, 병렬 워커는 동일한 공유 메모리 영역에 연결된다. 버퍼 풀, PGPROC 배열, 락 테이블, sinval 링 — 이것들은 프로세스별 복사본이 아니다. 모든 프로세스가 fork()로 상속한(또는 EXEC_BACKEND 경로에서 재연결한) 포인터로 접근하는 단일 객체다.

Database System Concepts(Silberschatz 외, 17장)는 공유 메모리를 공유 디스크 아키텍처에서 버퍼 관리의 자연스러운 기반으로 정의한다. “버퍼 풀은 공유 메모리에 유지되어 모든 프로세스가 직접 접근할 수 있다”는 것이 핵심이다. Database Internals(Petrov, 4장)도 같은 내용을 다룬다. 버퍼 매니저의 디스크립터 배열과 페이지 프레임이 공유 메모리에 있으므로 캐시 히트 시 프로세스 간 복사가 전혀 없다는 점이다.

공유 메모리를 선택한 후에도 두 가지 설계 질문이 남는다.

  1. 세그먼트 크기는 언제, 누가 결정하는가? PostgreSQL처럼 기동 시 한 번 결정하는 방식은 커널이 물리 페이지를 한꺼번에 예약할 수 있고 단편화가 없다. 대신 크기를 변경하려면 재시작해야 한다. 가변 크기 방식은 온라인 조정을 허용하지만, 세그먼트 기반 주소가 바뀌면 해당 영역을 가리키는 모든 포인터를 무효화해야 하는 복잡성이 생긴다.

  2. 영역을 어떻게 나누는가? 디렉터리를 갖춘 범프 할당자(PostgreSQL의 ShmemAlloc + ShmemIndex)는 단순하고 단편화가 없다. 다만 할당은 영구적이다. 객체 클래스별 슬랩·풀 할당자(InnoDB의 buf_pool)는 재활용이 가능한 대신 구조가 복잡해진다.

PostgreSQL은 세 번째 계층도 추가한다. 병렬 쿼리 워커처럼 런타임에만 필요한 구조를 위해 동적 공유 메모리(DSM, dynamic shared memory) 세그먼트를 별도로 운영한다. DSM은 기동 시 고정 크기 제약을 우회하는 역할을 한다. 장수명 전역 상태의 정규 위치는 여전히 정적 세그먼트다.

교과서는 모델을 제시한다. 이 절은 공유 메모리를 IPC 기반으로 선택한 거의 모든 멀티프로세스 DBMS가 채택하는 공학적 관례를 정리한다.

각 서브시스템은 기동 시 크기 산정 함수(sizing function)로 필요한 메모리를 신고한다. 중앙 조정자가 이를 합산하여 세그먼트를 한 번 생성하고, 이후 범프(선형) 할당자로 슬라이스를 나눠준다. Oracle의 SGA, InnoDB의 buf_pool_init, PostgreSQL의 ShmemAlloc 아레나가 모두 이 패턴이다. 범프 포인터는 뒤로 이동하지 않는다는 점이 불변 조건이다. 한 번 할당된 슬라이스는 영원히 해당 서브시스템 소유이고, 포인터가 전진만 하므로 세그먼트는 절대 누수되지 않는다.

정적 세그먼트 안의 평탄한 해시 테이블이 사람이 읽을 수 있는 문자열 이름을 (주소, 크기) 쌍에 대응시킨다. 나중에 세그먼트에 연결하는 프로세스(Windows의 EXEC_BACKEND, 또는 비정상 재시작 후 재초기화하는 백엔드)도 이 디렉터리를 조회하면 재할당 없이 객체를 찾을 수 있다. 오프셋을 하드코딩하는 구형 방식은 필드 하나를 추가하면 모든 하위 오프셋이 바뀌는 취약점이 있다.

수명이 짧은 구조를 위한 동적 세그먼트

섹션 제목: “수명이 짧은 구조를 위한 동적 세그먼트”

장수명 상태는 정적 세그먼트에, 정적 세그먼트는 최악의 경우를 위해 보수적으로 크게 잡는다. 병렬 쿼리 워커 상태, gather 노드와 병렬 스캔 사이의 공유 튜플 큐처럼 일시적인 구조는 정적 세그먼트를 영구적으로 차지해서는 안 된다. 모든 성숙한 멀티프로세스 엔진이 두 번째 계층 동적 할당자를 추가하는 이유가 바로 여기 있다. Oracle의 PGA, PostgreSQL의 DSM, CockroachDB의 goroutine-local 아레나가 그 예다. 핵심 요건은 소유자가 종료하거나 크래시하더라도 backing 페이지가 언매핑되어 OS로 반환되어야 한다는 것이다.

두 프로세스가 영역을 공유하면 링 버퍼 메시지 큐가 자연스럽게 따라온다. 한쪽이 슬롯에 쓰고 쓰기 포인터를 전진시킨다. 선택적으로 래치(latch)로 상대방을 깨운다. 상대방은 읽기 포인터에서 소비하고 전진시킨 뒤 되돌려 신호한다. 커널 복사가 없다. 공유 메모리 링 버퍼 + 래치 깨우기 패턴은 PostgreSQL의 shm_mq, Oracle의 SGA 내부 메시지 큐, Linux 커널 스케줄러의 kfifo 기반 큐에서 모두 볼 수 있다.

개념PostgreSQL 이름
정적 공유 세그먼트PGShmemHeader / ShmemSegHdr (shmem.c)
세그먼트 생성CreateSharedMemoryAndSemaphores (ipci.c)
서브시스템별 크기 산정*ShmemSize() 패밀리, CalculateShmemSize가 합산
범프 할당자ShmemAlloc / ShmemAllocRaw (shmem.c)
shmem 인덱스 / 이름 레지스트리ShmemIndex HTAB + ShmemInitStruct / ShmemInitHash
동적 세그먼트dsm_segment / dsm_create / dsm_attach / dsm_detach (dsm.c)
플랫폼 디스패치 테이블dsm_impl_op (dsm_impl.c)
DSM 위의 링 버퍼 메시지 큐shm_mq / shm_mq_create / shm_mq_attach / shm_mq_send / shm_mq_receive (shm_mq.c)
DSM 위의 가변 크기 슬랩 할당자dsa_area / dsa_create_ext / dsa_allocate_extended / dsa_free (dsa.c)
애드인 요청 훅RequestAddinShmemSpace + shmem_startup_hook

postmaster 기동 시 — 백엔드를 fork하기 전에 — 두 함수가 순서대로 실행된다.

// CalculateShmemSize — src/backend/storage/ipc/ipci.c
Size
CalculateShmemSize(int *num_semaphores)
{
Size size = 100000; /* baseline for small objects */
size = add_size(size, BufferManagerShmemSize());
size = add_size(size, LockManagerShmemSize());
size = add_size(size, ProcGlobalShmemSize());
size = add_size(size, XLOGShmemSize());
size = add_size(size, LWLockShmemSize());
size = add_size(size, ProcArrayShmemSize());
// ... ~35 more ShmemSize() calls condensed ...
size = add_size(size, AioShmemSize()); /* PG18 async I/O */
size = add_size(size, total_addin_request); /* add-ins */
size = add_size(size, 8192 - (size % 8192)); /* page-align */
return size;
}
// CreateSharedMemoryAndSemaphores — src/backend/storage/ipc/ipci.c
void
CreateSharedMemoryAndSemaphores(void)
{
PGShmemHeader *seghdr;
Size size = CalculateShmemSize(&numSemas);
seghdr = PGSharedMemoryCreate(size, &shim); /* SysV / mmap */
InitShmemAccess(seghdr);
PGReserveSemaphores(numSemas);
InitShmemAllocation();
CreateOrAttachShmemStructs(); /* calls every ShmemInit*() */
dsm_postmaster_startup(shim); /* bootstrap DSM control segment */
if (shmem_startup_hook)
shmem_startup_hook(); /* add-in extensions */
}

postmaster 기동 시 크기 산정에서 세그먼트 생성까지는 하나의 직선 파이프라인이다. 필요량을 합산하고, 세그먼트를 하나 생성하고, 범프 할당자를 설정한 뒤, shmem 인덱스를 거쳐 모든 서브시스템의 ShmemInit*를 실행한다.

flowchart TD
    A["CalculateShmemSize<br/>~37개 *ShmemSize() + total_addin_request 합산"] --> B["PGSharedMemoryCreate<br/>SysV shmget / POSIX mmap 단일 세그먼트 생성"]
    B --> C["InitShmemAccess<br/>ShmemBase / ShmemEnd / ShmemSegHdr 설정"]
    C --> D["PGReserveSemaphores"]
    D --> E["InitShmemAllocation<br/>ShmemAllocUnlocked로 ShmemLock 스핀락 할당"]
    E --> F["CreateOrAttachShmemStructs<br/>마스터 초기화 함수"]
    F --> G["InitShmemIndex<br/>아레나 기반에 ShmemIndex HTAB 배치"]
    G --> H["서브시스템별 ShmemInitStruct / ShmemInitHash<br/>BufferManagerShmemInit, LockManagerShmemInit, InitProcGlobal, ..."]
    H --> I["ShmemAllocRaw<br/>CACHELINEALIGN + ShmemLock 보호 하에 freeoffset 전진"]
    F --> J["dsm_postmaster_startup<br/>DSM 제어 세그먼트 부트스트랩"]
    F --> K["shmem_startup_hook<br/>애드인의 ShmemInitStruct 호출"]

그림 1 — postmaster 기동 시 정적 세그먼트 크기 산정과 생성 순서. CalculateShmemSize는 실제로 두 번 호출된다. InitializeShmemGUCs에서 shared_memory_size를 공개할 때 한 번, PGSharedMemoryCreate에 전달하는 권위 있는 크기를 결정할 때 한 번이다. ShmemInitStruct 분기의 모든 리프 노드는 동일한 ShmemAllocRaw 범프 경로를 거친다. 따라서 CreateOrAttachShmemStructs 안에서의 호출 순서가 곧 세그먼트에 슬라이스가 찍히는 순서다.

세그먼트 헤더는 매핑 영역의 시작에 위치한다.

// PGShmemHeader — src/include/storage/pg_shmem.h
typedef struct PGShmemHeader
{
int32 magic; /* identifies a live Postgres segment */
pid_t creatorPID; /* postmaster PID */
Size totalsize; /* total size of the segment */
Size freeoffset; /* bump pointer — next free byte */
dev_t device; /* data-directory device (Unix only) */
ino_t inode; /* data-directory inode (Unix only) */
} PGShmemHeader;

freeoffset이 범프 포인터다. ShmemAlloc이 이를 전진시킨다.

// ShmemAllocRaw — src/backend/storage/ipc/shmem.c
static void *
ShmemAllocRaw(Size size, Size *allocated_size)
{
size = CACHELINEALIGN(size); /* align to cache-line boundary */
SpinLockAcquire(ShmemLock);
newStart = ShmemSegHdr->freeoffset;
newFree = newStart + size;
if (newFree <= ShmemSegHdr->totalsize)
{
newSpace = (char *) ShmemBase + newStart;
ShmemSegHdr->freeoffset = newFree;
}
else
newSpace = NULL; /* out of shared memory */
SpinLockRelease(ShmemLock);
return newSpace;
}

세 가지를 짚고 넘어간다. 첫째, CACHELINEALIGN이 각 할당을 캐시 라인 경계에 맞춘다. 인접 객체 간의 거짓 공유(false sharing)를 방지하기 위한 의도적 선택이다. 둘째, ShmemLock(스핀락)은 범프 포인터 갱신 구간만 보호하며, 할당된 영역에 데이터를 쓰는 동안은 잡지 않는다. 셋째, freeoffset은 한 번 전진하면 되돌아오지 않는다. 공유 메모리 할당은 영구적이다.

ShmemAlloc으로 할당된 모든 객체는 세그먼트 내 고정 오프셋에 있는 ShmemIndex(HTAB)에 등록된다. ShmemInitStruct는 이름을 기준으로 객체를 생성·등록하거나 기존 것을 찾아 반환한다.

// ShmemInitStruct — src/backend/storage/ipc/shmem.c
void *
ShmemInitStruct(const char *name, Size size, bool *foundPtr)
{
/* Look up or insert into ShmemIndex */
result = (ShmemIndexEnt *)
hash_search(ShmemIndex, name, HASH_ENTER_NULL, foundPtr);
if (!*foundPtr)
{
structPtr = ShmemAlloc(size);
result->location = structPtr;
result->size = size;
}
else
structPtr = result->location;
return structPtr;
}

이것이 EXEC_BACKEND 재연결 경로를 가능하게 하는 구조다. fork()를 사용할 수 없는 Windows 백엔드는 AttachSharedMemoryStructs()를 호출하여 CreateOrAttachShmemStructs()를 재실행한다. 모든 ShmemInitStruct 호출이 *foundPtr = true를 반환하고 인덱스에서 포인터를 돌려주므로 재할당은 일어나지 않는다.

세그먼트는 ShmemVariableCache(VariableCacheData * 타입)라는 특수 포인터도 노출한다. 클러스터 전역 XID·OID 카운터(nextXid, nextOid, latestCompletedXid)를 담는 구조체다. 인덱스를 거치지 않고 InitShmemAllocation이 고정 오프셋에 직접 배치하며, access/transam/varsup.c가 사용한다.

계층 2: 정적 세그먼트의 거주자들

섹션 제목: “계층 2: 정적 세그먼트의 거주자들”

CreateOrAttachShmemStructs(ipci.c)가 마스터 초기화 함수다. 호출 순서 자체가 정적 세그먼트에 무엇이 사는지 정의한다.

flowchart TD
    A[CreateOrAttachShmemStructs] --> B[CreateLWLocks]
    A --> C[InitShmemIndex]
    A --> D[dsm_shmem_init]
    A --> E[VarsupShmemInit / XLOGShmemInit / XLogRecoveryShmemInit]
    A --> F[CLOGShmemInit / CommitTsShmemInit / SUBTRANSShmemInit]
    A --> G[BufferManagerShmemInit]
    A --> H[LockManagerShmemInit / PredicateLockShmemInit]
    A --> I[InitProcGlobal / ProcArrayShmemInit / BackendStatusShmemInit]
    A --> J[SharedInvalShmemInit]
    A --> K[PMSignalShmemInit / ProcSignalShmemInit / CheckpointerShmemInit]
    A --> L[AutoVacuumShmemInit / ReplicationSlotsShmemInit / WalSndShmemInit]
    A --> M[StatsShmemInit / AioShmemInit]

그림 2 — CreateOrAttachShmemStructs 호출 그래프. 각 리프 노드는 ShmemInitStruct(또는 고유한 범프 할당 경로)를 호출하여 정적 세그먼트의 한 슬라이스를 확보한다. LWLock과 shmem 인덱스가 먼저 와야 한다는 점이 순서를 결정한다.

위에 열거된 모든 객체 — 버퍼 디스크립터, PGPROC 배열, 락 테이블, WAL 버퍼, sinval 링, 누적 통계 영역(PG15+), 비동기 I/O 서브시스템(PG18) — 가 이 하나의 세그먼트 안에 살며, fork된 모든 자식 프로세스가 상속한 포인터로 접근한다.

정적 세그먼트는 postmaster 기동 시 한 번 크기가 정해지고 이후 늘어나지 않는다. 병렬 쿼리 워커 상태, 논리 복제 워커 큐처럼 일시적인 구조에는 동적 공유 메모리가 필요하다. 런타임에 생성·소멸되는 별도 세그먼트다.

// dsm_segment (backend-local handle) — src/backend/storage/ipc/dsm.c
struct dsm_segment
{
dlist_node node; /* link in backend's segment list */
ResourceOwner resowner; /* tracks ownership for cleanup */
dsm_handle handle; /* shared name (integer key) */
uint32 control_slot; /* slot in the DSM control segment */
void *impl_private; /* platform-specific private data */
void *mapped_address; /* where this backend mapped the segment */
Size mapped_size; /* size of this backend's mapping */
slist_head on_detach; /* callbacks fired on dsm_detach() */
};

공개 API는 세 함수다.

// dsm_create / dsm_attach / dsm_detach — src/backend/storage/ipc/dsm.c
dsm_segment *dsm_create(Size size, int flags); /* line 516 */
dsm_segment *dsm_attach(dsm_handle h); /* line 665 */
void dsm_detach(dsm_segment *seg); /* line 803 */

dsm_createdsm_segment 핸들을 반환하고, DSM 제어 세그먼트(dsm_postmaster_startup이 postmaster 기동 시 고정시킨 특수 세그먼트)에 슬롯을 할당하며, 호출자의 주소 공간에 새 세그먼트를 매핑한다. dsm_attach는 핸들로 제어 세그먼트를 조회하여 매핑한다. dsm_detach는 매핑을 해제하고 참조 카운트가 0이 되면 플랫폼 백엔드로 세그먼트를 파괴한다.

플랫폼 디스패치는 dsm_impl_op가 담당한다.

// dsm_impl_op — src/backend/storage/ipc/dsm_impl.c
bool
dsm_impl_op(dsm_op op, dsm_handle handle, Size request_size,
void **impl_private, void **mapped_address,
Size *mapped_size, int elevel)
{
switch (dynamic_shared_memory_type)
{
case DSM_IMPL_POSIX: return dsm_impl_posix(...); /* shm_open */
case DSM_IMPL_SYSV: return dsm_impl_sysv(...); /* shmget */
case DSM_IMPL_WINDOWS: return dsm_impl_windows(...);
case DSM_IMPL_MMAP: return dsm_impl_mmap(...); /* mmap file */
}
}

dynamic_shared_memory_type GUC(Linux/macOS 기본값 posix, Windows는 windows, 범용 폴백은 mmap)가 런타임에 구현을 선택한다. DSM 스택의 나머지 코드는 dsm_segment 핸들만 다루며 어떤 백엔드가 사용됐는지 알지 못한다.

DSM 생명주기를 정리하면 다음과 같다.

flowchart TD
    Start([start]) -->|dsm_create| Created[Created refcnt=1]
    Created -->|dsm_attach in other backend| Attached[Attached refcnt&gt;1]
    Created -->|dsm_pin_segment| Pinned[Pinned survives creator exit]
    Pinned -->|dsm_unpin_segment| Attached
    Attached -->|dsm_detach with refcnt&gt;1 — unmap only| Attached
    Attached -->|dsm_detach drops refcnt to 0 — destroy| Detached[Detached]
    Created -->|dsm_detach drops refcnt to 0 — destroy| Detached
    Detached -->|OS resources freed| Done([end])

그림 3 — DSM 세그먼트 생명주기. dsm_pin_segment는 참조 카운트를 올려 생성자가 종료해도 세그먼트가 파괴되지 않게 한다. DSA 영역은 이 메커니즘으로 backing 세그먼트를 생성자 종료 후에도 유지한다.

계층 4: shm_mq — 무락(lock-free) 링 버퍼 메시지 큐

섹션 제목: “계층 4: shm_mq — 무락(lock-free) 링 버퍼 메시지 큐”

shm_mq는 DSM 세그먼트 내에 할당되는 단일 생산자/단일 소비자(single-producer/single-consumer) 링 버퍼다. 병렬 쿼리에서 병렬 워커가 Gather 노드로 튜플 스트림을 전달할 때, 그리고 백그라운드 워커가 결과를 돌려줄 때 사용하는 전송 계층이다.

// shm_mq — src/backend/storage/ipc/shm_mq.c
struct shm_mq
{
slock_t mq_mutex; /* protects mq_receiver/sender */
PGPROC *mq_receiver; /* set once, then read-only */
PGPROC *mq_sender; /* set once, then read-only */
pg_atomic_uint64 mq_bytes_read; /* consumer position */
pg_atomic_uint64 mq_bytes_written; /* producer position */
Size mq_ring_size; /* ring buffer capacity */
bool mq_detached; /* either side has gone away */
uint8 mq_ring_offset; /* offset of ring within struct */
char mq_ring[FLEXIBLE_ARRAY_MEMBER];
};

프로토콜은 단순하다. shm_mq_create로 링 크기를 결정한 뒤, 송신자가 shm_mq_set_sender를, 수신자가 shm_mq_set_receiver를 호출하여 자신의 PGPROC 포인터를 등록한다. 이후 송수신은 락 없이 진행된다. 송신자는 mq_ring[mq_bytes_written % mq_ring_size]에 쓰고 mq_bytes_written을 전진시킨다. 수신자는 mq_ring[mq_bytes_read % mq_ring_size]에서 읽고 mq_bytes_read를 전진시킨다. 링이 가득 차거나(송신자) 비었을 때(수신자)만 상대방 PGPROC 래치에 SetLatch / WaitLatch를 호출한다.

sequenceDiagram
    participant W as Worker (sender)
    participant MQ as shm_mq ring
    participant G as Gather (receiver)

    W->>MQ: shm_mq_send(data, nbytes)
    note over MQ: write to ring[written % size]\nadvance mq_bytes_written
    MQ-->>G: (reader polls or is woken)
    G->>MQ: shm_mq_receive()
    note over MQ: read from ring[read % size]\nadvance mq_bytes_read
    G-->>W: (ring has space — sender unblocks)
    W->>MQ: shm_mq_detach() on exit
    note over MQ: mq_detached = true\nwake counterparty latch

그림 4 — shm_mq 송수신 프로토콜. 링 버퍼 데이터 경로에는 락이 없다. mq_mutexmq_receiver / mq_sender의 초기 설정만 보호한다. 생산자-소비자 간 메모리 순서는 원자적 mq_bytes_written / mq_bytes_read와 적절한 배리어로 보장된다.

계층 5: DSA — DSM 위의 가변 크기 슬랩 할당자

섹션 제목: “계층 5: DSA — DSM 위의 가변 크기 슬랩 할당자”

shm_mq는 고정 크기 메시지 프레이밍을 처리한다. 해시 테이블, 트리, 여러 프로세스가 봐야 하는 힙 할당 객체처럼 가변 크기 공유 데이터 구조를 위해 PostgreSQL은 **동적 공유 메모리 영역(DSA, Dynamic Shared-memory Area)**을 제공한다. DSA는 DSM 세그먼트에서 dsa_area 제어 객체를 잘라내고, 이후 추가 DSM 세그먼트들을 풀로 관리하며 할당을 제공한다.

// dsa_pointer — src/include/utils/dsa.h
typedef uint64 dsa_pointer;
/*
* Encoded as: (segment_number << DSA_OFFSET_WIDTH) | offset_within_segment
* DSA_OFFSET_WIDTH = 40 bits on 64-bit, giving 1 TB per segment.
* Segment number identifies which DSM segment holds the data.
*/
#define DSA_POINTER_FORMAT "%016" PRIx64
// dsa_create_ext — src/backend/utils/mmgr/dsa.c (line 421)
dsa_area *dsa_create_ext(int tranche_id,
size_t init_segment_size,
size_t max_segment_size);
// dsa_allocate_extended — src/backend/utils/mmgr/dsa.c (line 671)
dsa_pointer dsa_allocate_extended(dsa_area *area, size_t size, int flags);
// dsa_free — src/backend/utils/mmgr/dsa.c (line 826)
void dsa_free(dsa_area *area, dsa_pointer dp);
// dsa_get_address — src/backend/utils/mmgr/dsa.c (line 942)
void *dsa_get_address(dsa_area *area, dsa_pointer dp);

dsa_pointer는 (세그먼트 번호, 오프셋) 쌍을 인코딩한 64비트 정수다. **위치 독립적(position-independent)**이라는 점이 핵심이다. 같은 dsa_pointer 값을 프로세스 간에 전달하면 각자가 dsa_get_address로 자신의 DSM 세그먼트 매핑에서 해석한다. 첫 번째 DSM 세그먼트 시작에 내장된 dsa_area_control 구조체가 풀 프리 리스트와 세그먼트 디렉터리를 유지한다. dsa_attach를 호출하는 모든 백엔드는 같은 제어 객체를 가리키는 자신만의 dsa_area 셸을 얻는다.

세 가지 동적 계층은 access/transam/parallel.c에서 하나로 합쳐진다. 리더의 InitializeParallelDSM은 하나의 DSM 세그먼트를 크기 산정 후 생성하고, 그 위에 shm_toc 목차를 얹은 뒤, 워커당 shm_mq 오류/튜플 큐를 잘라낸다. 병렬 노드가 가변 크기 공유 스크래치 메모리(예: 병렬 해시 조인)를 필요로 하면 dsa_create_in_place로 동일 세그먼트 안에 DSA 영역을 배치할 수 있다. 각 워커는 핸들로 다시 연결하고 자신을 shm_mq 송신자로 등록한다.

flowchart TD
    subgraph Leader
        L1["GetSessionDsmHandle"] --> L2["dsm_create(segsize, DSM_CREATE_NULL_IF_MAXSEGMENTS)"]
        L2 --> L3["shm_toc_create(PARALLEL_MAGIC, seg base)"]
        L3 --> L4["shm_mq_create(start, PARALLEL_ERROR_QUEUE_SIZE)<br/>per worker"]
        L4 --> L5["shm_mq_set_receiver(mq, MyProc)"]
        L5 --> L6["shm_mq_attach(mq, seg, NULL) -> error_mqh"]
        L3 -.optional.-> LD["dsa_create_in_place(place, size, tranche, seg)<br/>variable-size shared scratch"]
        L6 --> L7["LaunchParallelWorkers -> RegisterDynamicBackgroundWorker<br/>handle = dsm_segment_handle(seg)"]
    end
    subgraph Worker
        W1["ParallelWorkerMain(main_arg = dsm_handle)"] --> W2["dsm_attach(DatumGetUInt32(main_arg))"]
        W2 --> W3["shm_toc_attach(PARALLEL_MAGIC, seg base)"]
        W3 --> W4["shm_mq_set_sender(mq, MyProc)"]
        W4 --> W5["shm_mq_attach(mq, seg, NULL) -> mqh"]
        W3 -.optional.-> WD["dsa_attach_in_place(place, seg)"]
    end
    L7 ==>|fork/exec inherits handle| W1
    W5 ==>|tuples + errors via ring| L6

그림 5 — 병렬 쿼리 하나의 동적 계층 구성. 단일 dsm_create 세그먼트가 공유 기반이고, shm_toc는 워커가 자신의 매핑에서 모든 하위 객체(큐, DSA 제어, 플랜 상태)를 재 위치시키는 오프셋 디렉터리다. shm_mq는 오류/튜플 스트림을 운반한다. 선택적 인플레이스(in-place) dsa_area가 가변 크기 할당을 제공한다. 워커는 리더의 원시 포인터를 직접 받지 않는다. 정수 dsm_handle과 TOC 키만 프로세스 경계를 넘는다.

확장 포인트: RequestAddinShmemSpaceshmem_startup_hook

섹션 제목: “확장 포인트: RequestAddinShmemSpace와 shmem_startup_hook”

shared_preload_libraries로 로드된 확장은 세그먼트 생성 전에 정적 세그먼트를 키울 수 있다.

// RequestAddinShmemSpace — src/backend/storage/ipc/ipci.c
void
RequestAddinShmemSpace(Size size)
{
if (!process_shmem_requests_in_progress)
elog(FATAL, "cannot request additional shared memory "
"outside shmem_request_hook");
total_addin_request = add_size(total_addin_request, size);
}

이 훅은 CalculateShmemSize 내부에서 실행된다. 두 번째 훅인 shmem_startup_hookCreateOrAttachShmemStructs 이후에 실행되어 확장이 자신의 객체에 ShmemInitStruct를 호출할 기회를 준다. 두 훅 합쳐서 영구 공유 메모리 구조를 위한 완전한 확장 API가 된다.

정적 세그먼트 크기 산정 및 초기화 (ipci.c)

섹션 제목: “정적 세그먼트 크기 산정 및 초기화 (ipci.c)”
  • CalculateShmemSize — 모든 서브시스템의 *ShmemSize() 함수와 total_addin_request를 합산한다. CreateSharedMemoryAndSemaphoresInitializeShmemGUCs에서 호출된다.
  • CreateSharedMemoryAndSemaphores — postmaster 전용 진입점. PGSharedMemoryCreate, InitShmemAccess, InitShmemAllocation, CreateOrAttachShmemStructs, dsm_postmaster_startup을 순서대로 호출한다.
  • CreateOrAttachShmemStructs — 마스터 초기화 함수. 의존성 순서에 따라 모든 서브시스템의 ShmemInit* 함수를 호출한다.
  • AttachSharedMemoryStructs — EXEC_BACKEND 재연결 경로(Windows). CreateOrAttachShmemStructs를 재실행하여 로컬 포인터를 재구축한다.
  • RequestAddinShmemSpace — 생성 전 크기 예약을 위한 확장 API.
  • InitializeShmemGUCsshared_memory_sizeshared_memory_size_in_huge_pages GUC 값을 계산한다.

범프 할당자 및 shmem 인덱스 (shmem.c)

섹션 제목: “범프 할당자 및 shmem 인덱스 (shmem.c)”
  • InitShmemAccessPGShmemHeader 포인터로 모듈 레벨의 ShmemBase, ShmemEnd, ShmemSegHdr를 설정한다.
  • InitShmemAllocationShmemLock(스핀락)을 할당·초기화하고 ShmemIndex를 null로 설정한다.
  • ShmemAllocShmemAllocRaw를 호출하며 공간 부족 시 에러를 낸다.
  • ShmemAllocNoError — 같은 함수지만 공간 부족 시 NULL을 반환한다.
  • ShmemAllocUnlockedShmemLock 자체를 할당하기 전 부트스트랩 단계에서 사용하는 락 없는 변종이다.
  • ShmemAllocRaw — 내부 구현. CACHELINEALIGN 후 스핀락 보호 하에 범프 포인터를 전진시킨다.
  • InitShmemIndex — 아레나 기반에 ShmemIndex HTAB를 ShmemAllocUnlocked로 생성한다.
  • ShmemInitStructShmemIndex를 통한 이름 기반 객체 생성·재연결.
  • ShmemInitHashShmemInitStruct와 같지만 HTAB 파라미터를 HASHCTL로 받는다.

동적 공유 메모리 (dsm.c + dsm_impl.c)

섹션 제목: “동적 공유 메모리 (dsm.c + dsm_impl.c)”
  • dsm_postmaster_startup — postmaster 기동 시 DSM 제어 세그먼트를 생성하고 핸들을 PGShmemHeader에 기록한다.
  • dsm_shmem_initCreateOrAttachShmemStructs에서 호출되어 각 백엔드가 제어 세그먼트를 매핑한다.
  • dsm_create — 새 DSM 세그먼트를 할당하고 dsm_control_item 슬롯을 예약하며 dsm_segment 핸들을 반환한다.
  • dsm_attach — 핸들로 기존 DSM 세그먼트를 매핑하고 dsm_control_item.refcnt를 증가시킨다.
  • dsm_detach — 세그먼트를 언매핑하고 on_detach 콜백을 실행한다. refcnt가 0이 되면 OS 객체를 파괴한다.
  • on_dsm_detach — 세그먼트 분리 전에 실행할 콜백을 등록한다. DSA가 영역 메모리 해제에 사용한다.
  • dsm_impl_op — 플랫폼 디스패치. dynamic_shared_memory_type GUC에 따라 dsm_impl_posix, dsm_impl_sysv, dsm_impl_windows, dsm_impl_mmap 중 하나를 선택한다.
  • dsm_control_item — 세그먼트별 공유 메타데이터. handle, refcnt, first_page, npages, pinned.
  • shm_mq_create — DSM 세그먼트 내 호출자 제공 주소에 shm_mq를 초기화하고 mq_ring_size를 설정한다.
  • shm_mq_set_receiver / shm_mq_set_sender — 송신·수신 PGPROC을 등록한다. 각각 한 번만 호출 가능하다.
  • shm_mq_attach — 기존 shm_mq에 대한 백엔드 로컬 shm_mq_handle을 생성한다. 선택적으로 BackgroundWorkerHandle을 등록한다.
  • shm_mq_send — 메시지를 전송한다. 링이 가득 차면 블록하거나 SHM_MQ_WOULD_BLOCK을 반환한다.
  • shm_mq_receive — 다음 메시지를 수신한다. 링이 비어 있으면 블록하거나 SHM_MQ_WOULD_BLOCK을 반환한다.
  • shm_mq_detachmq_detached = true로 설정하고 상대방 래치를 깨운다.

DSM 위의 가변 크기 할당자 (dsa.c)

섹션 제목: “DSM 위의 가변 크기 할당자 (dsa.c)”
  • dsa_create_ext — 새 DSM 세그먼트를 기반으로 새 dsa_area를 생성한다. dsa_area_control을 세그먼트 시작에 배치한다.
  • dsa_attachdsa_handle(dsm_handle과 동일)로 기존 DSA 영역을 매핑한다.
  • dsa_allocate_extendedsize 바이트를 할당하고 위치 독립적인 dsa_pointer를 반환한다.
  • dsa_free — 메모리를 영역 프리 리스트로 반환한다.
  • dsa_get_addressdsa_pointer를 백엔드 로컬 가상 주소로 변환한다.
  • dsa_pin / dsa_unpin — 비고정 백엔드가 모두 분리될 때 영역이 파괴되는 것을 방지하거나 허용한다.

위치 힌트 표 (2026-06-05 기준, 커밋 273fe94, REL_18_STABLE)

섹션 제목: “위치 힌트 표 (2026-06-05 기준, 커밋 273fe94, REL_18_STABLE)”
심볼파일
CalculateShmemSizestorage/ipc/ipci.c89
CreateSharedMemoryAndSemaphoresstorage/ipc/ipci.c200
CreateOrAttachShmemStructsstorage/ipc/ipci.c268
AttachSharedMemoryStructsstorage/ipc/ipci.c173
RequestAddinShmemSpacestorage/ipc/ipci.c74
InitializeShmemGUCsstorage/ipc/ipci.c357
PGShmemHeaderinclude/storage/pg_shmem.h29
InitShmemAccessstorage/ipc/shmem.c102
InitShmemAllocationstorage/ipc/shmem.c115
ShmemAllocstorage/ipc/shmem.c152
ShmemAllocNoErrorstorage/ipc/shmem.c172
ShmemAllocUnlockedstorage/ipc/shmem.c238
ShmemAllocRaw (static)storage/ipc/shmem.c183 (추정)
InitShmemIndexstorage/ipc/shmem.c283
ShmemInitHashstorage/ipc/shmem.c332
ShmemInitStructstorage/ipc/shmem.c387
dsm_postmaster_startupstorage/ipc/dsm.c177
dsm_shmem_initstorage/ipc/dsm.c479
dsm_createstorage/ipc/dsm.c516
dsm_attachstorage/ipc/dsm.c665
dsm_detachstorage/ipc/dsm.c803
on_dsm_detachstorage/ipc/dsm.c1132
dsm_segment (구조체)storage/ipc/dsm.c66
dsm_control_item (구조체)storage/ipc/dsm.c79
dsm_impl_opstorage/ipc/dsm_impl.c159
shm_mq (구조체)storage/ipc/shm_mq.c71
shm_mq_createstorage/ipc/shm_mq.c177
shm_mq_set_receiverstorage/ipc/shm_mq.c206
shm_mq_set_senderstorage/ipc/shm_mq.c224
shm_mq_attachstorage/ipc/shm_mq.c290
shm_mq_sendstorage/ipc/shm_mq.c329
shm_mq_receivestorage/ipc/shm_mq.c572
shm_mq_detachstorage/ipc/shm_mq.c843
dsa_create_extutils/mmgr/dsa.c421
dsa_attachutils/mmgr/dsa.c510
dsa_allocate_extendedutils/mmgr/dsa.c671
dsa_freeutils/mmgr/dsa.c826
dsa_get_addressutils/mmgr/dsa.c942
dsa_pinutils/mmgr/dsa.c975
dsa_unpinutils/mmgr/dsa.c994
dsa_create_in_place_extutils/mmgr/dsa.c471
dsa_attach_in_placeutils/mmgr/dsa.c545
shm_toc_createstorage/ipc/shm_toc.c40
shm_toc_attachstorage/ipc/shm_toc.c64
GetSessionDsmHandleaccess/common/session.c70
InitializeParallelDSMaccess/transam/parallel.c211
LaunchParallelWorkersaccess/transam/parallel.c580
ParallelWorkerMainaccess/transam/parallel.c1299
  • CalculateShmemSize는 서브시스템별 크기 산정 함수의 단순 합산이며 동적 조정이 없다. ipci.c 89~170번 줄로 확인. 약 37개의 *ShmemSize() 헬퍼와 total_addin_request를 더한다. PG18 추가분은 AioShmemSize()(비동기 I/O)와 SlotSyncShmemSize()(슬롯 동기화 워커)다. 런타임 피드백 루프는 없다. 세그먼트가 너무 작게 생성되면 ShmemAlloc이 “out of shared memory” 에러를 낸다.

  • ShmemAllocRawMAXALIGN이 아닌 CACHELINEALIGN으로 정렬한다. shmem.c 약 205번 줄에서 확인. 주석은 “경험상 현대 시스템에서 MAXALIGN만으로는 부족하다…캐시 라인 경계에 맞추려 시도한다”고 명시한다. PG14 이전 동작에서 의도적으로 변경한 내용이다.

  • DSM 플랫폼은 컴파일 시점이 아닌 런타임에 dynamic_shared_memory_type GUC로 선택된다. dsm_impl_op(dsm_impl.c 159번 줄)에서 확인. switch 문이 GUC 값으로 디스패치하며, 네 개의 백엔드(posix/sysv/windows/mmap)가 모두 컴파일될 수 있다. 기본값은 Linux/macOS에서 posix다.

  • shm_mq의 데이터 경로는 뮤텍스를 사용하지 않는다. mq_receiver/mq_sender 초기 설정만 뮤텍스로 보호된다. shm_mq.c 30~67번 줄의 구조체 주석으로 확인. mq_bytes_readmq_bytes_written은 메모리 순서가 문서화된 pg_atomic_uint64이며, mq_ring은 락 없이 읽고 쓴다.

  • dsa_pointer는 (세그먼트 번호, 오프셋) 쌍을 인코딩한다. dsa.h 81103번 줄과 dsa.c 7898번 줄에서 확인. 64비트 시스템에서 DSA_OFFSET_WIDTH = 40이며 세그먼트당 최대 1 TB, 영역당 최대 1024개 세그먼트를 허용한다. 32비트 폴백은 DSA_OFFSET_WIDTH = 27(세그먼트 32개, 각 128 MB)을 사용한다.

  • RequestAddinShmemSpaceshmem_request_hook 실행 중에만 호출할 수 있도록 게이트가 걸려 있다. ipci.c 74~77번 줄에서 확인. 훅 바깥에서 호출하면 FATAL이 발생한다. process_shmem_requests_in_progress 플래그가 postmaster의 pre-fork 훅 루프에서 설정되고 CreateSharedMemoryAndSemaphores 반환 전에 해제된다.

  • PG18은 CreateOrAttachShmemStructsAioShmemInit()SlotSyncShmemInit()을 추가했다. ipci.c 268~355번 줄 grep으로 확인. AioShmemInit은 비동기 I/O 서브시스템(storage/aio/)을 초기화하고, SlotSyncShmemInit은 PG17에 추가된 슬롯 동기화 워커를 지원한다. REL_16 이하에는 없다.

  1. 위치 힌트 표의 ShmemAllocRaw 줄 번호는 추정값이다. ShmemAllocRawstatic 함수여서 grep으로 안정적인 진입점을 찾기 어렵다. 183번 줄 추정은 InitShmemAllocation(115번 줄)에서 보이는 함수 본문 수를 더해 계산했다. 확인 경로: grep -n 'ShmemAllocRaw' shmem.c.

  2. NUMA 인식 shmem 할당. shmem.cpg_numa.hfirstNumaTouch 플래그(96번 줄)를 참조한다. PG18이 정적 세그먼트를 배치할 때 실제로 NUMA 토폴로지를 활용하는지, 아니면 pg_numa_available() SQL 함수만 노출하는지는 불명확하다. 확인 경로: ShmemAlloc에서 firstNumaTouch를 추적하고 새 pg_numa_available 내장 함수와 연계해 검증한다.

  3. dsa_createdsa_create_ext. (해결됨) PG18에서 dsa_createdsa.h(117번 줄)의 매크로로, dsa_create_ext(tranche_id, DSA_DEFAULT_INIT_SEGMENT_SIZE, DSA_MAX_SEGMENT_SIZE)로 확장된다. 마찬가지로 dsa_create_in_place(122번 줄)는 dsa_create_in_place_ext를 감싼다. _ext 형태가 실제 함수이고, 매크로는 기본 세그먼트 크기 한계를 적용하는 편의 래퍼다. 이름 변경이 아니라 계층 분리다.

PostgreSQL 너머 — 비교 설계와 연구 프런티어

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프런티어”
  • Oracle SGA(System Global Area). Oracle의 정적 공유 세그먼트는 개념적으로 PostgreSQL과 같다. 기동 시 크기를 결정하고, 범프 할당하며, 버퍼 캐시·락 테이블·redo 버퍼를 담는다. Oracle은 SGA 컴포넌트 간에 메모리를 런타임에 재분배할 수 있는 자동 메모리 관리(AMM) 계층을 추가했다. PostgreSQL의 CalculateShmemSize 합산은 고정이다. 동적 재분배가 복잡성 대비 얼마나 이득을 가져오는지 정량적으로 비교하면 의미 있는 설계 분석이 된다.

  • Linux io_uring 공유 링과 PG18 storage/aio/ 서브시스템. PG18의 비동기 I/O 계층(AioShmemInit)은 I/O 워커 상태를 정적 세그먼트에 두고 io_uring 제출/완료 큐를 사용한다. 이 큐 자체가 커널이 설정한 공유 메모리 링 버퍼다. 사용자 공간의 shm_mq 링 버퍼 패턴과 커널 공간의 io_uring 패턴은 구조적으로 동일하다(원자 카운터를 가진 생산자/소비자). 두 링의 메모리 순서 보장이 어떻게 다른지 병렬 비교하면 PostgreSQL 튜플 전송 링과 커널 I/O 링의 차이가 드러날 것이다.

  • CockroachDB / 분산 공유 불가(shared-nothing) 아키텍처. PostgreSQL의 설계는 모든 프로세스가 물리 메모리를 공유한다고 가정한다. 공유 불가 분산 엔진에는 공유 세그먼트가 없고, 모든 상태는 노드 로컬이거나 명시적 RPC 계층으로 교환된다. scalable-lock-manager.md 논문(Johnson 외, VLDB 2010)은 공유 메모리 엔진에서도 중앙화된 락 테이블 접근이 확장성 병목이 됨을 지적하며 CPU별 파티셔닝을 제안한다. PostgreSQL은 버퍼 매핑 테이블에는 이 패턴을 쓰지만 전체 락 테이블에는 아직 적용하지 않았다.

  • shm_mq와 LMAX Disruptor / 무락 링 패턴. shm_mq의 락 없는 데이터 경로는 전형적인 단일 생산자/단일 소비자 링 버퍼다. LMAX Disruptor(Thompson 외, 2011)는 시퀀스 배리어로 이를 다중 생산자/다중 소비자로 확장한다. PostgreSQL의 병렬 실행기는 shm_mq 인스턴스당 송신자 하나로 제한된다. 미래의 다중 병렬 워커-Gather 전송 경로에서는 다중 생산자 변형이 유익할 수 있다. pg_atomic_* 프리미티브로 공유 메모리 맥락에서 Disruptor 패턴을 이식하는 것은 구체적인 연구 방향이다.

  • DSA 대 직접 제작 슬랩 할당자. dsa는 범용 가변 크기 할당자다. 일부 PostgreSQL 서브시스템(누적 통계, pg_wait_sampling)은 DSA 없이 DSM 위에 특화 데이터 구조를 직접 구현했다. DSA의 오버헤드(크기 클래스 버킷팅, 세그먼트당 페이지맵)를 감수할 가치가 있는 경우와 직접 구현이 나은 경우를 구분하면 새 서브시스템에서 DSA 채택 여부를 판단하는 기준이 생긴다.

(없음 — 소스 트리를 직접 분석하여 작성)

  • Database System Concepts (Silberschatz, Korth, Sudarshan), 7판, 17장 §“Shared Memory and Semaphores” — 멀티프로세스 엔진에서 버퍼 풀 기반으로서의 공유 메모리.
  • Database Internals (Petrov, 2019), 4장 §“Buffer Management” — 공유 구조체로서의 버퍼 풀과 캐시 키로서의 페이지 ID.

소스 코드 (REL_18_STABLE, 커밋 273fe94)

섹션 제목: “소스 코드 (REL_18_STABLE, 커밋 273fe94)”
  • src/backend/storage/ipc/ipci.c — 세그먼트 크기 산정 및 마스터 초기화.
  • src/backend/storage/ipc/shmem.c — 범프 할당자 및 shmem 인덱스.
  • src/backend/storage/ipc/dsm.c — 동적 공유 메모리 세그먼트.
  • src/backend/storage/ipc/dsm_impl.c — 플랫폼 백엔드(posix/sysv/windows/mmap).
  • src/backend/storage/ipc/shm_mq.c — 링 버퍼 메시지 큐.
  • src/backend/utils/mmgr/dsa.c — 가변 크기 DSM 할당자.
  • src/backend/storage/ipc/shm_toc.c — DSM 세그먼트 위의 목차(key → offset 디렉터리, 병렬 워커가 사용).
  • src/backend/access/transam/parallel.c — 리더/워커 DSM + shm_mq 설정(InitializeParallelDSM, LaunchParallelWorkers, ParallelWorkerMain).
  • src/backend/access/common/session.cGetSessionDsmHandle.
  • src/include/storage/pg_shmem.hPGShmemHeader.
  • src/include/storage/dsm.hdsm_handle, dsm_segment 공개 API.
  • src/include/storage/shm_mq.hshm_mq 공개 API.
  • src/include/utils/dsa.hdsa_pointer, dsa_area 공개 API.
  • src/include/access/transam.hVariableCacheData / ShmemVariableCache XID·OID 카운터 레이아웃.
  • postgres-architecture-overview.md — 축 2(공유 메모리 기반); postmaster fork 모델.
  • postgres-lock-manager.md — 정적 세그먼트의 LockManagerShmemInit 거주자.
  • postgres-lwlock-spinlock.mdLWLockShmemSize / CreateLWLocks. LWLock은 정적 세그먼트에 살지만 내부 동작은 해당 문서에서 다룬다.
  • postgres-buffer-manager.mdBufferManagerShmemInit. 버퍼 디스크립터와 페이지 프레임은 정적 세그먼트의 가장 큰 거주자다.
  • postgres-parallel-query.md — DSM + shm_mq 소비자. nodeGather.c가 튜플 전송 링을 구동하는 방식.
  • knowledge/research/dbms-papers/scalable-lock-manager.md — Johnson 외 VLDB 2010. CPU별 락 테이블 파티셔닝의 확장성 동기.