콘텐츠로 이동

(KO) PostgreSQL 래치, 대기 이벤트 셋, 프로세스 간 시그널

다중 프로세스 데이터베이스 서버는 본질적으로 서로 협력하는 프로세스들의 집합이다. 이 프로세스들은 벽시계 기준으로 대부분의 시간을 잠든 채 보내며, 다음 작업 단위가 올 때까지 대기한다. 서버 내부 구조 전반을 관통하는 설계 질문은 겉으로 보기엔 단순하다. 잠든 프로세스는 어떻게 깨어나야 한다는 것을 알게 되며, 다른 프로세스(또는 같은 프로세스 안의 시그널 핸들러)는 어떻게 그것을 알리는가? 이 공간은 세 가지 하위 문제로 정의된다.

  1. 깨우기 경쟁 손실(lost-wakeup race). 단순한 패턴은 공유 플래그와 sleep의 조합이다. 워커는 while (!flag) sleep()을 반복하고, 생산자는 flag = true를 설정한다. 이 패턴은 선점형 시스템에서 틀렸다. 생산자가 플래그를 설정하고 시그널을 보내는 시점이 워커의 플래그 검사와 sleep() 호출 사이에 끼어들면, 웨이크업이 소실되고 워커는 영원히 잠든다. 올바른 기본 연산은 “조건 검사”와 “잠들기 시작”을 웨이크업에 원자적으로 묶어야 한다. pselect/ppoll과 조건 변수가 제공하는 보장이 바로 그것이며, 평범한 poll + 시그널 핸들러는 이 보장을 제공하지 않는다.

  2. 이기종 이벤트 동시 대기. 백엔드가 하나의 이벤트만 기다리는 경우는 드물다. 클라이언트 소켓 가독성(새 쿼리), 래치(다른 백엔드의 통지), 포스트마스터 사망(부모 프로세스 충돌로 자식이 종료해야 함), 그리고 가능하다면 타임아웃(statement_timeout)을 동시에 기다린다. OS 준비 상태 기본 연산인 epoll, kqueue, poll은 파일 디스크립터를 네이티브로 다중화하지만, 래치는 파일 디스크립터가 아니고 포스트마스터 사망도 그것이 아니다. 추상화 계층이 이것들을 통합해야 한다.

  3. 비동기 시그널을 동기적·안전한 동작으로 전환. Unix 시그널은 malloc 수행 중, 스핀락 보유 중, 또는 프로토콜 메시지 구성 중간 등 어떤 명령어 경계에서도 도착할 수 있다. 시그널 핸들러에서 안전하게 수행할 수 있는 작업은 극히 적다(비동기 시그널 안전 함수 목록은 매우 짧다). 그래서 보편적 패턴은 핸들러가 절대적 최소만 수행하는 것이다. volatile sig_atomic_t 플래그를 설정하고 웨이크업을 찌르는 것이 전부다. 실제 작업은 프로세스가 자발적으로 플래그를 검사하는, 잘 정의된 안전 지점까지 미뤄진다.

Operating Systems: Three Easy Pieces(Arpaci-Dusseau)는 (1)과 (3)을 “동시성”과 “시그널 핸들러가 발행할 수 있는 제한된 지시”라는 맥락에서 다룬다. 깨우기 손실 문제는 조건 변수가 관련 뮤텍스를 필요로 하는 이유와 동일한 위험이다. Database Internals(Petrov)는 노드 로컬 동시성과 프로세스 모델 설명에서, 데이터베이스의 프로세스/스레드 스케줄러가 바로 이 OS 기본 연산 위에 구축되며 웨이크업의 비용(시스템 콜, 컨텍스트 스위치, 캐시 라인 바운스)이 OLTP에서 1차 성능 관심사임을 지적한다. Architecture of a Database System(Hellerstein et al., §“Process Models”)은 PostgreSQL이 사용하는 프로세스-per-연결 모델을 정리하며, 이 모델이 사용자 공간 스케줄러를 구축하는 대신 OS IPC와 시그널링에 크게 의존한다고 관찰한다.

PostgreSQL의 답은 세 계층을 조합한다. Latch(경쟁 없는 1비트 웨이크업, 문제 #1 해결), WaitEventSet(OS 준비 상태 기본 연산 위의 이식 가능한 다중화기, 문제 #2 해결 및 래치를 동일한 대기에 내장), 인터럽트 기구—ProcSignal(프로세스 간 메시징)과 InterruptPending/CHECK_FOR_INTERRUPTS()(지연 처리)—(문제 #3 해결)이다.

이 섹션은 다중 프로세스 서버(데이터베이스를 포함한 일반 서버)에서 반복적으로 나타나는 공학 패턴을 정리한다. 이 패턴들을 알면 PostgreSQL의 구체적 선택이 공유된 설계 공간 안에서의 선택으로 읽힌다.

이벤트/래치 객체: 불리언 + 웨이크업

섹션 제목: “이벤트/래치 객체: 불리언 + 웨이크업”

프로세스-per-연결 또는 프로세스-per-워커 모델을 사용하는 거의 모든 서버는 “잠든 프로세스를 찌르는” 기본 연산이 필요하다. 형태는 항상 같다. (a) “작업이 있는가?” 불리언 플래그와 (b) 소유자에게 OS 수준 웨이크업을 전달하기에 충분한 정보(pid, 이벤트 핸들, 파이프 fd)를 담은 작은 공유 객체다. 두 절반은 메모리 배리어로 순서를 보장해야 한다. 설정자는 플래그를 공개한 누군가 잠자고 있는지 확인하고, 대기자는 플래그를 지운 조건을 다시 검사한다. 이렇게 해야 어느 쪽도 낡은 메모리에 근거해 “아무것도 없다 / 깨울 사람이 없다”고 결론 짓지 않는다. 이것은 크로스 프로세스 공유 메모리에 적응된, 조건 변수를 둘러싼 교과서적 이중 확인 패턴이다.

OS 리액터를 통한 준비 상태 다중화

섹션 제목: “OS 리액터를 통한 준비 상태 다중화”

많은 fd와 내부 이벤트를 동시에 기다리기 위해, 서버들은 OS가 제공하는 “리액터” 위에 얇은 이식성 래퍼를 구축한다. Linux의 epoll, BSD와 macOS의 kqueue, 최소 공통 분모로서의 poll/select, 그리고 Windows의 I/O 완료 포트나 이벤트 객체가 그것이다. 이 래퍼는 관심 명세 집합을 한 번 등록한 뒤 블로킹 기본 연산을 반복적으로 호출하며, 커널의 “이것들이 준비됐다”는 응답을 서버 자신의 이벤트 어휘로 변환한다. nginx, redis, libuv, PostgreSQL 모두 정확히 이 형태의 모듈을 가진다.

경쟁 없는 블로킹: ppoll/pselect 또는 self-pipe 기법

섹션 제목: “경쟁 없는 블로킹: ppoll/pselect 또는 self-pipe 기법”

평범한 poll()은 “시그널 차단 해제와 잠들기”를 원자적으로 수행할 수 없다. poll()에 진입하기 직전에 도착한 시그널은 그것을 중단시키지 못한다. 두 가지 표준 해결책이 있다. (a) 시그널 마스크를 받는 p-변형 시스템 콜(ppoll/pselect)을 사용하거나, (b) self-pipe 기법(Bernstein)을 사용한다. self-pipe에서는 시그널 핸들러가 fd 집합에 포함된 파이프의 쓰기 끝에 한 바이트를 쓴다. 그러면 대기 중인 시그널이 읽기 가능한 fd로 나타나므로 poll()이 놓칠 수 없다. Linux는 세 번째 옵션인 signalfd도 제공한다. signalfd는 시그널 전달 자체를 읽기 가능한 fd로 만들어 프로세스가 시그널을 차단된 상태로 유지하면서 동기적으로 소비하게 한다.

하나의 시그널 번호를 여러 논리 메시지로 다중화

섹션 제목: “하나의 시그널 번호를 여러 논리 메시지로 다중화”

Unix는 사용자 정의 가능한 시그널(SIGUSR1, SIGUSR2)을 소수만 제공한다. 수십 가지 서로 다른 프로세스 간 알림이 필요한 서버는 메시지 유형당 하나의 시그널을 감당할 수 없다. 표준 기법은 단일 시그널을 범용 “메일 있음” 도어벨로 지정하고, 이유를 공유 메모리에 대역 외로 전달하는 것이다. 발신자는 메시지를 식별하는 수신자별 플래그를 설정한 뒤, 그 하나의 시그널을 보낸다. 수신자의 핸들러는 플래그들을 스캔해 실제로 무슨 일이 있었는지 파악한다. 이렇게 하면 (희소한) 시그널 네임스페이스와 (무제한인) 메시지 네임스페이스가 분리된다.

안전 지점에서의 지연 인터럽트 처리

섹션 제목: “안전 지점에서의 지연 인터럽트 처리”

마지막 공통 패턴은 다음과 같다. 시그널 핸들러는 절대 실제 작업을 수행하지 않는다. InterruptPending 스타일의 플래그를 설정하고 반환한다. 실제 작업은 메인 루프가 안전 지점에 도달해 검사 매크로를 호출할 때 수행된다. 이렇게 하면 비동기적이고 위험한 컨텍스트가 동기적이고 통제된 컨텍스트로 변환된다. 또한 서버가 “대기 해제” 카운터를 증가시키는 것만으로 임계 섹션을 보호할 수 있으며, 검사 매크로는 그 카운터를 존중한다.

flowchart TD
  A["생산자 프로세스<br/>또는 시그널 핸들러"] -->|"SetLatch / kill(SIGUSR1)"| B["공유 상태:<br/>is_set 플래그,<br/>ProcSignal 이유 비트"]
  B --> C["OS 웨이크업:<br/>self-pipe 바이트 /<br/>signalfd / SIGURG"]
  C --> D["WaitEventSetWait()에서<br/>잠든 프로세스"]
  D -->|"준비 상태 반환"| E["메인 루프가<br/>안전 지점에 도달"]
  E -->|"CHECK_FOR_INTERRUPTS()"| F["ProcessInterrupts():<br/>취소 / 종료 / 배리어"]

PostgreSQL은 세 계층을 세 개의 소스 모듈과 postgres.c의 인터럽트 접착 코드로 구현한다. 계층화는 엄격하다. latch.cwaiteventset.c의 얇은 파사드이며, waiteventset.c는 OS 리액터에 접촉하는 유일한 모듈이다. procsignal.c는 래치 위에 SIGUSR1 다중화기를 구축하고, postgres.c는 지연 핸들러가 실제로 수행하는 작업을 정의한다.

Latch: 1비트, 경쟁 없는 웨이크업

섹션 제목: “Latch: 1비트, 경쟁 없는 웨이크업”

Latch는 세 개의 중요한 필드—is_set, maybe_sleeping, owner_pid—와 is_shared 플래그로 구성된다. 프로세스 로컬 래치(InitLatch)는 같은 프로세스 내부(보통 시그널 핸들러)에서만 설정할 수 있다. 공유 래치(InitSharedLatch + OwnLatch)는 공유 메모리에 상주하며 어느 백엔드에서도 설정 가능하다. MyLatch는 모든 프로세스의 표준 웨이크업 객체로, “무언가 일어났다, 상태를 다시 확인하라”는 신호로 광범위하게 사용된다.

SetLatch는 경쟁 없음의 핵심이다. 두 메모리 배리어로 공개-후-확인 시퀀스를 감싼다.

// SetLatch — src/backend/storage/ipc/latch.c
pg_memory_barrier();
/* Quick exit if already set */
if (latch->is_set)
return;
latch->is_set = true;
pg_memory_barrier();
if (!latch->maybe_sleeping)
return;
/* ... figure out owner_pid and deliver a wakeup ... */
owner_pid = latch->owner_pid;
if (owner_pid == 0)
return;
else if (owner_pid == MyProcPid)
WakeupMyProc(); /* in-process: self-pipe or SIGURG to self */
else
WakeupOtherProc(owner_pid);/* cross-process: kill(pid, SIGURG) */

대기자 측은 WaitEventSetWait 내부에서 거울상 동작을 수행한다. maybe_sleeping = true를 설정하고 배리어를 발행한 뒤, 실제로 잠들기 전에 is_set을 다시 확인한다. SetLatch가 그 창에서 실행됐다면, 대기자가 is_set을 보고 잠들기를 건너뛰거나, 설정자가 maybe_sleeping을 보고 웨이크업을 전달한다. 둘 다 놓치는 경우는 없다. ResetLatchis_set을 후행 배리어와 함께 지워, 이후의 플래그 읽기가 지우기 이전으로 재정렬되지 않도록 한다.

flowchart TD
  subgraph Setter["SetLatch (어느 프로세스 / 핸들러)"]
    S1["barrier; is_set이면 반환"] --> S2["is_set = true"]
    S2 --> S3["barrier; maybe_sleeping이 아니면 반환"]
    S3 --> S4["owner==me? WakeupMyProc<br/>else WakeupOtherProc(pid)"]
  end
  subgraph Waiter["WaitEventSetWait (소유자)"]
    W1["maybe_sleeping = true"] --> W2["barrier"]
    W2 --> W3["is_set이면: WL_LATCH_SET 보고,<br/>잠들기 건너뜀"]
    W3 --> W4["아니면 epoll/poll에서 블록<br/>self-pipe/signalfd 가독까지"]
    W4 --> W5["drain(); is_set 재확인"]
  end
  S4 -.->|"self-pipe 바이트 / SIGURG"| W4

WaitEventSet: 래치 + 소켓 + PM 사망 + 타임아웃을 하나의 대기로

섹션 제목: “WaitEventSet: 래치 + 소켓 + PM 사망 + 타임아웃을 하나의 대기로”

WaitLatch는 이제 단순한 편의 래퍼다. 실제 기구는 WaitEventSet이다. 이것은 WaitEvent 관심 레코드의 등록된 배열과 플랫폼별 커널 객체(epoll_fd, kqueue_fd, 또는 pollfd 배열)를 보유한다. CreateWaitEventSet으로 생성하고, AddWaitEventToSet으로 이벤트를 등록하며(나중에 ModifyWaitEvent할 수 있는 위치를 반환), WaitEventSetWait에서 블로킹한다. 이벤트 종류는 비트마스크다. WL_LATCH_SET, WL_SOCKET_READABLE/WRITEABLE/CLOSED/..., WL_POSTMASTER_DEATH(또는 WL_EXIT_ON_PM_DEATH), WL_TIMEOUT이 있다.

핵심 설계 선택은 소켓이 아닌 세 이벤트를 모두 파일 디스크립터에 매핑해 OS 리액터가 균일하게 대기할 수 있도록 한다는 점이다.

  • 래치는 self-pipe(poll 빌드)의 읽기 끝 또는 signalfd(epoll 빌드)가 된다.
  • 포스트마스터 사망은 postmaster_alive_fds[POSTMASTER_FD_WATCH]가 된다. 이 파이프를 포스트마스터가 열어두고, 포스트마스터가 죽으면 커널이 닫아(EPOLLHUP) 그 사실을 알린다.
  • 타임아웃은 리액터 호출 자체의 타임아웃 인자다.
// AddWaitEventToSet — src/backend/storage/ipc/waiteventset.c
if (events == WL_LATCH_SET)
{
set->latch = latch;
set->latch_pos = event->pos;
#if defined(WAIT_USE_SELF_PIPE)
event->fd = selfpipe_readfd;
#elif defined(WAIT_USE_SIGNALFD)
event->fd = signal_fd;
#else
event->fd = PGINVALID_SOCKET;
#ifdef WAIT_USE_EPOLL
return event->pos;
#endif
#endif
}
else if (events == WL_POSTMASTER_DEATH)
{
#ifndef WIN32
event->fd = postmaster_alive_fds[POSTMASTER_FD_WATCH];
#endif
}

WaitLatchInitializeLatchWaitSet이 한 번 구축하는 수명이 긴 LatchWaitSet을 활용한다. 슬롯 두 개—위치 0의 래치와 위치 1의 PM 사망—만 있으며, 각 호출에서 ModifyWaitEvent만 수행한다. 이렇게 하면 매우 일반적인 단일 래치 대기에서 epoll fd를 생성하고 해제하는 비용을 피할 수 있다. 반면 WaitLatchOrSocket은 소켓이 변하기 때문에 호출마다 새로운 세 슬롯 셋을 구축한다.

PostgreSQL은 컴파일 시간에 리액터에 따라 웨이크업 메커니즘을 선택한다. waiteventset.c의 헤더 주석이 행렬을 정확히 제시한다. poll()은 self-pipe를 사용하고, epoll()은 signalfd를 사용하며 SIGURG를 차단 상태로 유지한다. kqueue()는 SIGURG용 EVFILT_SIGNAL을 등록하고, Windows는 상속된 이벤트 객체를 사용한다. 래치 웨이크업을 전달하는 시그널은 SIGURG다. SIGUSR1(ProcSignal 메시지를 전달)과 의도적으로 구별해 두 관심사가 간섭하지 않도록 한다.

// latch_sigurg_handler / sendSelfPipeByte — src/backend/storage/ipc/waiteventset.c
static void
latch_sigurg_handler(SIGNAL_ARGS)
{
if (waiting)
sendSelfPipeByte(); /* turn the signal into a readable fd */
}

epoll 경로에서는 핸들러가 없다. InitializeWaitEventSupport가 SIGURG를 차단하고 signalfd를 열어, SIGURG 전달이 epoll이 직접 감시하는 읽기 가능한 디스크립터가 되도록 한다. 어느 쪽이든, WaitEventSetWaitBlock이 래치 fd의 가독성을 보고하면 drain()을 호출해 파이프/signalfd를 비운다(비블로킹 읽기를 EAGAIN까지 반복). 이렇게 해야 누적된 바이트가 바쁜 루프를 유발하지 않는다.

ProcSignal: SIGUSR1을 여러 이유로 다중화

섹션 제목: “ProcSignal: SIGUSR1을 여러 이유로 다중화”

웨이크업만이 아닌 크로스 백엔드 메시지는 SIGUSR1을 탄다. procsignal.cProcNumber당 하나씩(보조 프로세스당 하나 추가) ProcSignalSlot의 공유 메모리 배열을 유지한다. 각 슬롯에는 pss_signalFlags[NUM_PROCSIGNALS] 불리언 배열이 있다. 백엔드 N에 이유 R로 시그널을 보내려면, SendProcSignal이 슬롯의 스핀락 아래 slot[N].pss_signalFlags[R] = true를 설정한 뒤 kill(pid, SIGUSR1)을 보낸다. 수신자의 procsignal_sigusr1_handlerCheckProcSignal로 모든 이유를 스캔하고, 설정된 각 플래그마다 Handle...Interrupt를 디스패치하며, 마지막에 SetLatch(MyLatch)를 호출한다.

// procsignal_sigusr1_handler (excerpt) — src/backend/storage/ipc/procsignal.c
if (CheckProcSignal(PROCSIG_NOTIFY_INTERRUPT))
HandleNotifyInterrupt();
if (CheckProcSignal(PROCSIG_PARALLEL_MESSAGE))
HandleParallelMessageInterrupt();
if (CheckProcSignal(PROCSIG_BARRIER))
HandleProcSignalBarrierInterrupt();
/* ... recovery-conflict reasons ... */
SetLatch(MyLatch);

계층화에 주목해야 한다. 모든 Handle...Interrupt는 플래그 설정만 수행하고(InterruptPending = true; SomethingPending = true;), 마지막 SetLatch가 백엔드를 어떤 대기에서도 깨워 다음 CHECK_FOR_INTERRUPTS()가 실제 핸들러를 실행하도록 한다. ProcSignal은 전역 배리어(EmitProcSignalBarrier / WaitForProcSignalBarrier)도 전달한다. 이것은 상태 변경이 진행되기 전에 모든 백엔드가 확인해야 할 때(예: SMGR 해제) 사용하는 세대 카운터 프로토콜이다.

인터럽트 지연: CHECK_FOR_INTERRUPTS와 ProcessInterrupts

섹션 제목: “인터럽트 지연: CHECK_FOR_INTERRUPTS와 ProcessInterrupts”

지연 계약은 miscadmin.hpostgres.c에 있다. 핸들러는 InterruptPending을 설정한다. 메인 루프는 안전 지점에 CHECK_FOR_INTERRUPTS()를 산재시킨다. 그 매크로는 인터럽트가 대기 중일 때만 ProcessInterrupts()를 호출하며, 이 함수는 ProcDiePending, QueryCancelPending, 타임아웃, 복구 충돌, ProcSignal 배리어, 병렬 메시지를 서비스하고 적절히 ERROR/FATAL을 던진다.

// CHECK_FOR_INTERRUPTS / INTERRUPTS_CAN_BE_PROCESSED — src/include/miscadmin.h
#define CHECK_FOR_INTERRUPTS() \
do { \
if (INTERRUPTS_PENDING_CONDITION()) \
ProcessInterrupts(); \
} while(0)
#define INTERRUPTS_CAN_BE_PROCESSED() \
(InterruptHoldoffCount == 0 && CritSectionCount == 0 && \
QueryCancelHoldoffCount == 0)

HOLD_INTERRUPTS()/RESUME_INTERRUPTS()InterruptHoldoffCount를 증감하며, ProcessInterrupts는 그 카운터나 CritSectionCount가 0이 아니면 즉시 반환한다. 임계 섹션이 안전할 때까지 취소를 미루는 방법이 바로 이것이다. 쿼리 취소 경로는 QueryCancelHoldoffCount도 존중하므로, 백엔드가 프로토콜 메시지를 읽는 중에는 취소가 발동하지 않아 FE/BE 스트림 동기화가 깨지지 않는다.

flowchart TD
  K1["kill(pid, SIGINT)<br/>StatementCancelHandler"] --> K2["QueryCancelPending = true<br/>InterruptPending = true<br/>SetLatch(MyLatch)"]
  K2 --> K3["백엔드가<br/>WaitEventSetWait에서 깨어남"]
  K3 --> K4["다음 CHECK_FOR_INTERRUPTS()"]
  K4 -->|"holdoff/crit-section?"| K5["InterruptPending 재설정,<br/>지연"]
  K4 -->|"안전"| K6["ProcessInterrupts():<br/>ereport(ERROR,<br/>'canceling statement<br/>due to user request')"]

이 섹션은 실제 코드를 호출 흐름 순으로, 서브시스템별로 묶어 살펴본다. 심볼은 안정적 기준점이며, 줄 번호는 끝의 위치 힌트 표에서만 다룬다.

래치 수명 주기와 SetLatch (latch.c)

섹션 제목: “래치 수명 주기와 SetLatch (latch.c)”

래치는 InitLatch(프로세스 로컬) 또는 InitSharedLatch + OwnLatch(공유)로 초기화된다. OwnLatch는 공유 래치가 현재 프로세스와 연관되는 순간으로, owner_pid = MyProcPid를 기록한다. DisownLatch는 이것을 되돌린다. OwnLatch의 온전성 검사는 래치에 이미 소유자가 있으면 PANIC을 발생시킨다. 잠금이 없으므로, 두 프로세스가 하나의 래치를 소유하려 경쟁할 수 있다면 호출자가 외부에서 인터락해야 한다.

// OwnLatch — src/backend/storage/ipc/latch.c
owner_pid = latch->owner_pid;
if (owner_pid != 0)
elog(PANIC, "latch already owned by PID %d", owner_pid);
latch->owner_pid = MyProcPid;

SetLatch(앞 섹션에서 인용)는 유일한 설정자다. 주요 특성은 다음과 같다. 시그널 핸들러와 임계 섹션에서 안전하게 호출 가능하다(절대 던지지 않는다). 래치가 이미 설정된 경우 비용이 저렴하다(첫 번째 배리어+테스트에서 단락된다). owner_pidMyProcPid와 비교해 웨이크업 전달을 결정한다. 프로세스 내부 설정(방금 인터럽트 플래그를 설정한 핸들러의 일반적 경우)은 WakeupMyProc으로, 크로스 프로세스 설정은 WakeupOtherProc으로 라우팅된다. ResetLatchis_set을 지우며 소유자만 호출할 수 있다. 표준 관용구는 대기, 재설정, 처리, 맨 아래에서 반복으로, 처리 중에 도착한 설정이 소실되지 않도록 한다.

WaitEventSet 구축과 수정 (waiteventset.c)

섹션 제목: “WaitEventSet 구축과 수정 (waiteventset.c)”

InitializeWaitEventSupport는 프로세스 시작 시 한 번 실행된다. poll 빌드에서는 self-pipe를 생성하고(양 끝을 논블로킹·close-on-exec으로), SIGURG용 latch_sigurg_handler를 설치하며, fd.c로 외부 fd 두 개를 예약한다. epoll 빌드에서는 SIGURG를 차단하고 signalfd를 연다.

// InitializeWaitEventSupport (signalfd branch) — src/backend/storage/ipc/waiteventset.c
sigaddset(&UnBlockSig, SIGURG); /* keep SIGURG blocked */
sigemptyset(&signalfd_mask);
sigaddset(&signalfd_mask, SIGURG);
signal_fd = signalfd(-1, &signalfd_mask, SFD_NONBLOCK | SFD_CLOEXEC);
if (signal_fd < 0)
elog(FATAL, "signalfd() failed");

CreateWaitEventSet은 MAXALIGN 패딩된 하나의 연속 블록을 할당한다. 이 블록은 WaitEventSet, WaitEvent 배열, 플랫폼의 반환 버퍼(epoll_ret_events / kqueue_ret_events / pollfds)를 담는다. 그런 다음 커널 객체(epoll_create1(EPOLL_CLOEXEC) / kqueue())를 연다. 셋은 선택적으로 ResourceOwner로 추적해 오류 시 해제할 수 있다.

AddWaitEventToSet은 요청을 검증하고(래치 이벤트는 이 프로세스가 소유한 래치를 필요로 하고, 소켓 이벤트는 실제 fd를 필요로 한다), WaitEvent를 저장하고, 내부 이벤트를 fd에 매핑하며(앞서 표시), 플랫폼별 조정 루틴을 호출한다. epoll의 경우 WaitEventAdjustEpoll로, WL_* 마스크를 EPOLLIN/EPOLLOUT/EPOLLRDHUP으로 변환하고 epoll_ctl을 호출한다.

// WaitEventAdjustEpoll — src/backend/storage/ipc/waiteventset.c
epoll_ev.data.ptr = event; /* so epoll_wait hands back our WaitEvent */
epoll_ev.events = EPOLLERR | EPOLLHUP; /* always watch for errors */
if (event->events == WL_LATCH_SET)
epoll_ev.events |= EPOLLIN;
else if (event->events == WL_POSTMASTER_DEATH)
epoll_ev.events |= EPOLLIN;
else
{
if (event->events & WL_SOCKET_READABLE) epoll_ev.events |= EPOLLIN;
if (event->events & WL_SOCKET_WRITEABLE) epoll_ev.events |= EPOLLOUT;
if (event->events & WL_SOCKET_CLOSED) epoll_ev.events |= EPOLLRDHUP;
}
rc = epoll_ctl(set->epoll_fd, action, event->fd, &epoll_ev);

ModifyWaitEventWaitLatch가 사용하는 빠른 경로다. 마스크와 래치가 모두 변경되지 않았다면 즉시 반환한다. Unix에서 래치 수정은 커널 호출이 필요 없다. 기저 파이프/signalfd가 모든 래치에 걸쳐 공유되므로, MyLatch의 교체는 사실상 무료다.

대기 루프: WaitEventSetWait와 WaitEventSetWaitBlock

섹션 제목: “대기 루프: WaitEventSetWait와 WaitEventSetWaitBlock”

WaitEventSetWait는 이식 가능한 외부 루프다. waiting 플래그를 설정하고(SIGURG 핸들러가 self-pipe에 써야 한다는 것을 알 수 있도록), 이벤트가 최소 하나 반환될 때까지 반복한다. 래치 빠른 경로는 경쟁 없는 프로토콜의 구현이다. maybe_sleeping을 설정하고, 배리어를 발행하고, is_set을 재확인하며, 여전히 설정되지 않은 경우에만 블로킹한다.

// WaitEventSetWait (latch fast path) — src/backend/storage/ipc/waiteventset.c
if (set->latch && !set->latch->is_set)
{
set->latch->maybe_sleeping = true; /* tell SetLatch we're about to sleep */
pg_memory_barrier();
/* and recheck */
}
if (set->latch && set->latch->is_set)
{
occurred_events->events = WL_LATCH_SET;
/* ... fill event, return without blocking ... */
set->latch->maybe_sleeping = false;
}

WaitEventSetWaitBlock은 플랫폼별 내부 호출이다. epoll 버전은 epoll_wait를 호출하고, EINTR은 “재시도”로, rc == 0은 타임아웃으로 처리한다. 반환된 epoll_event 배열을 순회하며, 래치 fd가 발동하면 signalfd를 drain()하고 보고 전에 set->latch->is_set && maybe_sleeping을 재확인한다. 이것은 허위 깨우기를 방어한다. PM 사망 fd가 발동하면 PostmasterIsAliveInternal()로 재확인하고(허위 사망 보고는 재앙적이므로), exit_on_postmaster_death이면 proc_exit(1)을 직접 호출한다.

// WaitEventSetWaitBlock (epoll, latch + PM-death) — src/backend/storage/ipc/waiteventset.c
if (cur_event->events == WL_LATCH_SET &&
cur_epoll_event->events & (EPOLLIN | EPOLLERR | EPOLLHUP))
{
drain(); /* empty signalfd / self-pipe */
if (set->latch && set->latch->maybe_sleeping && set->latch->is_set)
{
occurred_events->events = WL_LATCH_SET;
returned_events++;
}
}
else if (cur_event->events == WL_POSTMASTER_DEATH && ...)
{
if (!PostmasterIsAliveInternal())
{
if (set->exit_on_postmaster_death)
proc_exit(1);
occurred_events->events = WL_POSTMASTER_DEATH;
}
}

웨이크업 전달과 drain (waiteventset.c)

섹션 제목: “웨이크업 전달과 drain (waiteventset.c)”

WakeupMyProc은 소유자가 현재 프로세스일 때 SetLatch가 호출하는 함수다. 시그널 핸들러 내부에서 전형적으로 사용된다. self-pipe 빌드에서는 바이트를 쓰고, signalfd 빌드에서는 kill(MyProcPid, SIGURG)를 보낸다. WakeupOtherProc은 항상 kill(pid, SIGURG)를 보낸다. sendSelfPipeByte는 한 바이트를 논블로킹으로 쓰며, EAGAIN/EWOULDBLOCK을 성공으로 처리한다(꽉 찬 파이프는 이미 웨이크업을 전달한다). 핸들러에서 실행될 수 있으므로 다른 오류는 조용히 무시한다. drain은 디스크립터가 빌 때까지 읽는다.

// WakeupMyProc / WakeupOtherProc — src/backend/storage/ipc/waiteventset.c
void
WakeupMyProc(void)
{
#if defined(WAIT_USE_SELF_PIPE)
if (waiting)
sendSelfPipeByte();
#else
if (waiting)
kill(MyProcPid, SIGURG);
#endif
}
void
WakeupOtherProc(int pid)
{
kill(pid, SIGURG);
}

ProcSignal: 슬롯, 전송, SIGUSR1 핸들러 (procsignal.c)

섹션 제목: “ProcSignal: 슬롯, 전송, SIGUSR1 핸들러 (procsignal.c)”

ProcSignalShmemInitProcSignalHeader(전역 배리어 세대 + NumProcSignalSlots 슬롯의 유연한 배열)를 배치한다. ProcSignalInitMyProcNumber의 슬롯을 확보하고, 쓰기 메모리 배리어로 pss_pid를 공개한다. 이렇게 해야 EmitProcSignalBarrier가 절반만 초기화된 슬롯을 건너뛰지 않는다. on_shmem_exitCleanupProcSignalState를 등록한다.

SendProcSignal이 발신자다. ProcNumber가 알려진 경우 슬롯으로 바로 간다. 그렇지 않으면 뒤에서 앞으로 스캔한다(보조 프로세스는 끝 부근에 있다). 스핀락 아래 이유 플래그를 설정하고 하나의 SIGUSR1을 보낸다.

// SendProcSignal (fast path) — src/backend/storage/ipc/procsignal.c
SpinLockAcquire(&slot->pss_mutex);
if (pg_atomic_read_u32(&slot->pss_pid) == pid)
{
slot->pss_signalFlags[reason] = true; /* which reason */
SpinLockRelease(&slot->pss_mutex);
return kill(pid, SIGUSR1); /* the doorbell */
}
SpinLockRelease(&slot->pss_mutex);

CheckProcSignal은 하나의 이유 플래그를 읽고 지운다(플래그는 volatile sig_atomic_t이므로 잠금 없이 핸들러에서 읽을 수 있다). procsignal_sigusr1_handler(앞서 인용)는 모든 이유를 순회하고 SetLatch(MyLatch)로 끝낸다. 디스패치하는 각 Handle...Interrupt는 다른 곳(async.c, parallel.c, postgres.c)에 있으며 대기 중 플래그만 설정한다.

EmitProcSignalBarrier(type)은 모든 슬롯의 pss_barrierCheckMask에 타입 비트를 OR 하고, 전역 psh_barrierGeneration을 원자적으로 증가시키며, PROCSIG_BARRIER 이유로 모든 살아있는 프로세스에 SIGUSR1을 보낸다. ProcessProcSignalBarrier(CHECK_FOR_INTERRUPTS에서 실행)는 프로세스의 로컬 세대와 공유 세대를 비교하고, 검사 마스크를 교환하며, PG_TRY 내부에서 요청된 각 배리어를 디스패치한다(예: ProcessBarrierSmgrRelease). ERROR가 발생하면 비트를 재설정한다. 성공 시 pss_barrierGeneration을 진행시키고 슬롯의 조건 변수에 브로드캐스트한다. WaitForProcSignalBarrier(gen)은 모든 슬롯의 세대가 gen에 도달할 때까지 각 슬롯의 CV에서 잠든다.

// EmitProcSignalBarrier — src/backend/storage/ipc/procsignal.c
for (int i = 0; i < NumProcSignalSlots; i++)
pg_atomic_fetch_or_u32(&ProcSignal->psh_slot[i].pss_barrierCheckMask, flagbit);
generation = pg_atomic_add_fetch_u64(&ProcSignal->psh_barrierGeneration, 1);
/* ... then SIGUSR1 every slot with pid != 0, reason PROCSIG_BARRIER ... */
return generation;

인터럽트 핸들러와 ProcessInterrupts (postgres.c)

섹션 제목: “인터럽트 핸들러와 ProcessInterrupts (postgres.c)”

터미널 핸들러들은 작다. die(SIGTERM)는 ProcDiePending/InterruptPending을 설정하고 SetLatch(MyLatch)를 호출한다. StatementCancelHandler(SIGINT)는 QueryCancelPending을 마찬가지로 설정한다. 취소 시그널은 SIGINT다. SendCancelRequest가 취소 키 일치 후 이것을 보낸다. ProcSignal 메시지는 SIGUSR1이고, 래치 웨이크업은 SIGURG다. 세 개의 서로 다른 시그널이 세 개의 서로 다른 목적을 수행한다.

// StatementCancelHandler — src/backend/tcop/postgres.c
if (!proc_exit_inprogress)
{
InterruptPending = true;
QueryCancelPending = true;
}
SetLatch(MyLatch); /* waken anything waiting */

ProcessInterruptsCHECK_FOR_INTERRUPTS의 외부 바디다. InterruptHoldoffCount 또는 CritSectionCount가 0이 아니면 즉시 반환한다. 그런 다음 InterruptPending을 지우고 각 조건을 우선순위 순으로 서비스한다. ProcDiePending이 먼저다(FATAL, autovacuum/bgworker/walreceiver 등에 대한 역할별 메시지와 함께). 그 다음 클라이언트 연결 손실, QueryCancelPending(QueryCancelHoldoffCount != 0이면 프로토콜 읽기를 보호하기 위해 다시 지연), 타임아웃 패밀리, 복구 충돌, ProcSignal 배리어, 병렬 메시지 순이다.

// ProcessInterrupts (cancel-during-read guard) — src/backend/tcop/postgres.c
if (QueryCancelPending && QueryCancelHoldoffCount != 0)
{
/* Re-arm so we process the cancel once we're done reading the message. */
InterruptPending = true;
}
else if (QueryCancelPending)
{
QueryCancelPending = false;
/* ... distinguish lock_timeout / statement_timeout / user request,
LockErrorCleanup(), then ereport(ERROR, "canceling statement ...") */
}

ProcessClientReadInterruptProcessClientWriteInterrupt는 저수준 소켓 읽기/쓰기를 감싼다. 유휴 상태(DoingCommandRead)에서 읽기는 인터럽트를 확인하고 NOTIFY/catchup을 서비스한다. 종료 중(ProcDiePending)에는 블로킹 I/O가 돌아와 응답하지 않는 클라이언트에서 매달리지 않고 즉시 종료하도록 래치가 설정됐는지 확인한다.

위치 힌트 (2026-06-05 기준, REL_18 273fe94)

섹션 제목: “위치 힌트 (2026-06-05 기준, REL_18 273fe94)”
심볼파일
InitializeLatchWaitSetsrc/backend/storage/ipc/latch.c35
InitLatchsrc/backend/storage/ipc/latch.c63
InitSharedLatchsrc/backend/storage/ipc/latch.c93
OwnLatchsrc/backend/storage/ipc/latch.c126
DisownLatchsrc/backend/storage/ipc/latch.c144
WaitLatchsrc/backend/storage/ipc/latch.c172
WaitLatchOrSocketsrc/backend/storage/ipc/latch.c223
SetLatchsrc/backend/storage/ipc/latch.c290
ResetLatchsrc/backend/storage/ipc/latch.c374
InitializeWaitEventSupportsrc/backend/storage/ipc/waiteventset.c241
CreateWaitEventSetsrc/backend/storage/ipc/waiteventset.c364
FreeWaitEventSetsrc/backend/storage/ipc/waiteventset.c481
AddWaitEventToSetsrc/backend/storage/ipc/waiteventset.c570
ModifyWaitEventsrc/backend/storage/ipc/waiteventset.c656
WaitEventAdjustEpollsrc/backend/storage/ipc/waiteventset.c738
WaitEventSetWaitsrc/backend/storage/ipc/waiteventset.c1038
WaitEventSetWaitBlock (epoll)src/backend/storage/ipc/waiteventset.c1182
latch_sigurg_handlersrc/backend/storage/ipc/waiteventset.c1896
sendSelfPipeBytesrc/backend/storage/ipc/waiteventset.c1904
drainsrc/backend/storage/ipc/waiteventset.c1945
WakeupMyProcsrc/backend/storage/ipc/waiteventset.c2020
WakeupOtherProcsrc/backend/storage/ipc/waiteventset.c2033
ProcSignalSlot (struct)src/backend/storage/ipc/procsignal.c64
ProcSignalShmemInitsrc/backend/storage/ipc/procsignal.c132
ProcSignalInitsrc/backend/storage/ipc/procsignal.c167
SendProcSignalsrc/backend/storage/ipc/procsignal.c293
EmitProcSignalBarriersrc/backend/storage/ipc/procsignal.c365
WaitForProcSignalBarriersrc/backend/storage/ipc/procsignal.c433
ProcessProcSignalBarriersrc/backend/storage/ipc/procsignal.c508
CheckProcSignalsrc/backend/storage/ipc/procsignal.c658
procsignal_sigusr1_handlersrc/backend/storage/ipc/procsignal.c683
SendCancelRequestsrc/backend/storage/ipc/procsignal.c741
ProcessClientReadInterruptsrc/backend/tcop/postgres.c502
ProcessClientWriteInterruptsrc/backend/tcop/postgres.c548
diesrc/backend/tcop/postgres.c3027
StatementCancelHandlersrc/backend/tcop/postgres.c3057
HandleRecoveryConflictInterruptsrc/backend/tcop/postgres.c3090
ProcessInterruptssrc/backend/tcop/postgres.c3299
CHECK_FOR_INTERRUPTS (macro)src/include/miscadmin.h123
INTERRUPTS_CAN_BE_PROCESSED (macro)src/include/miscadmin.h130

아래의 모든 사실은 /data/hgryoo/references/postgres의 REL_18_STABLE 트리, 커밋 273fe94 기준으로 검증됐다.

검증 완료:

  • latch.cwaiteventset.c의 얇은 래퍼다. WaitLatch는 공유된 두 슬롯 LatchWaitSet을 사용하고, WaitLatchOrSocket은 호출마다 세 슬롯 셋을 구축한다(CreateWaitEventSet(CurrentResourceOwner, 3)). WaitLatch/WaitLatchOrSocket에서 확인됨.
  • SetLatch는 공개/검사를 두 pg_memory_barrier() 호출로 감싸고 WakeupMyProc/WakeupOtherProc으로 디스패치한다. WaitEventSet 측은 WaitEventSetWait에서 일치하는 배리어와 함께 maybe_sleeping을 설정한다. 확인됨.
  • 래치 웨이크업 시그널은 SIGURG, ProcSignal은 SIGUSR1, 쿼리 취소는 SIGINT다. WakeupOtherProc(kill(pid, SIGURG)), SendProcSignal(kill(pid, SIGUSR1)), SendCancelRequest(kill(-backendPID, SIGINT) (HAVE_SETSID 아래))에서 확인됨.
  • 컴파일 시간 행렬은 다음과 같다. epoll→signalfd(SIGURG 차단, 핸들러 없음), poll→self-pipe(latch_sigurg_handler 설치), kqueue→SIGURG용 EVFILT_SIGNAL, win32→이벤트 객체. WAIT_USE_* #if 사다리와 InitializeWaitEventSupport에서 확인됨.
  • 포스트마스터 사망은 postmaster_alive_fds[POSTMASTER_FD_WATCH]의 읽기 끝으로 전달되며, 보고 전에 PostmasterIsAliveInternal()로 재확인된다. WL_EXIT_ON_PM_DEATHWaitEventSetWaitBlock 내부에서 proc_exit(1)을 호출한다. 확인됨.
  • ProcSignal 슬롯은 ProcNumber로 인덱싱된다. NumProcSignalSlots = MaxBackends + NUM_AUXILIARY_PROCS다. pss_signalFlagsvolatile sig_atomic_t[NUM_PROCSIGNALS]다. 확인됨.
  • 배리어 프로토콜은 64비트 세대 카운터, 슬롯당 pss_barrierCheckMask, 슬롯당 ConditionVariable pss_barrierCV를 사용한다. ProcessProcSignalBarrier는 처리 전에 pg_atomic_exchange_u32로 마스크를 지우고, PG_TRY 내부에서 실패 시 재설정한다. 확인됨.
  • ProcessInterruptsInterruptHoldoffCount || CritSectionCount에서 일찍 반환하며, QueryCancelPending && QueryCancelHoldoffCount != 0일 때 InterruptPending을 재설정한다. 확인됨.
  • REL_18에서 ProcessProcSignalBarrier의 switch에서 처리되는 유일한 배리어 타입은 PROCSIGNAL_BARRIER_SMGRRELEASE(→ ProcessBarrierSmgrRelease)다. switch (type) 블록에서 확인됨.

범위 참고 / 비단언 사항:

  • kqueue와 Win32 WaitEventSetWaitBlock 변형이 존재하며 읽었지만 인용하지 않았다. 이 문서는 epoll 경로의 줄 수준 세부 사항만 단언한다. poll 경로의 self-pipe 핸들러는 인용됐다.
  • signalfd/epoll은 Linux 전용이다. latch_sigurg_handler/sendSelfPipeByte의 위치 힌트 줄은 #if defined(WAIT_USE_SELF_PIPE) 내부에 있으며 poll 빌드에서만 컴파일된다. 줄 번호는 소스 위치로서 정확하다.
  • contrib/는 범위 밖이다. 여기서 contrib 심볼은 단언하지 않는다.

PostgreSQL 너머 — 비교 설계와 연구 동향

섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 동향”

래치/대기 셋/인터럽트 스택은 모든 동시성 시스템이 직면하는 문제에 대한 하나의 구체적 답이다. 다른 설계와 나란히 놓으면 PostgreSQL이 무엇을 선택했고 왜 그랬는지가 더 선명해진다.

PostgreSQL의 Latch는 도덕적으로 단일 비트 상태로 제한된 크로스 프로세스 조건 변수다. 하지만 웨이크업에 Linux futex(퓨텍스)와 같은 커널 블로킹 기본 연산을 의도적으로 사용하지 않는다. 이유는 이식성과 하나의 시스템 콜에서 웨이크업을 다른 대기 소스와 합성해야 하는 필요 때문이다. futex는 futex 단어에서만 대기할 수 있지만, 백엔드는 래치와 소켓과 포스트마스터 사망을 동시에 기다려야 한다. 래치를 파일 디스크립터(self-pipe/signalfd)로 라우팅하면 리액터의 관심 집합에서 또 다른 fd가 된다. 비용은 웨이크업당 추가 시스템 콜 하나(파이프 쓰기 또는 kill)와 drain 읽기다. PostgreSQL에는 진정한 futex 스타일 기본 연산도 있다. 배리어 CV가 사용하는 ConditionVariable과 LWLock은 모두 프로세스의 세마포어에서 잠든다. 하지만 이것들은 소켓과 다중화할 수 없으므로 래치가 별도 메커니즘으로 존재하는 이유가 바로 그것이다. 세마포어 기반 잠금은 postgres-lwlock-spinlock.md에서 다룬다.

Self-pipe 대 signalfd 대 EVFILT_SIGNAL 대 eventfd

섹션 제목: “Self-pipe 대 signalfd 대 EVFILT_SIGNAL 대 eventfd”

“시그널을 읽기 가능한 fd처럼 보이게 만드는” 문제는 OS별 답의 작은 동물원을 낳았으며, PostgreSQL은 그중 네 가지를 지원한다. 고전적 self-pipe 기법(Bernstein, 1990년대경; Stevens와 qmail 코드베이스로 대중화)이 이식 가능한 기준선이다. Linux의 signalfd(2.6.22)와 eventfd(2.6.22)는 self-pipe를 대체하기 위해 추가됐다. PostgreSQL은 epoll 빌드에서 signalfd를 사용하지만 래치에 eventfd를 사용하지 않는다. 기존 SIGURG 기반 크로스 프로세스 웨이크업과 signalfd가 깔끔하게 합성되기 때문이다. BSD kqueue EVFILT_SIGNAL 필터는 보조 fd 없이 시그널 대기를 리액터에 직접 접는다. 가장 깔끔한 설계로, kqueue 경로가 self-pipe도 signalfd도 필요 없는 이유다. 연구 최전선 질문은 io_uringIORING_OP_*와 eventfd/futex 대기의 네이티브 지원이 언젠가 WaitEventSet을 완전히 대체할 수 있는가이다. PostgreSQL 18의 새 비동기 I/O 서브시스템(postgres-aio.md)이 트리에서 io_uring의 첫 발판이지만, 래치 경로는 아직 그것에 닿지 않는다.

ProcSignal은 하나의 시그널 번호를 공유 메모리 플래그로 ~20가지 이유로 다중화한다. 대안 설계—많은 액터 모델과 마이크로커널 시스템이 사용하는—는 각 메시지가 타입 있는 페이로드를 전달하는 진정한 프로세스별 메시지 큐다. PostgreSQL에는 실제로 둘 다 있다. shm_mq 공유 메모리 큐(postgres-shared-memory-ipc.md)는 병렬 워커와 리더 간에 데이터를 전달하고, ProcSignal은 알림만 전달한다(“큐에 메시지가 있다, 확인하라”). 이 분리는 의도적이다. 시그널은 희소하고 손실 가능하다(동일한 이유의 병합은 멱등 알림에서 버그가 아니라 기능이다). 반면 shm_mq는 페이로드를 손실 없이 순서대로 전달한다. 일반 DBMS 교훈인 “도어벨과 우편함을 분리하라”는 많은 시스템에서 반복된다.

CHECK_FOR_INTERRUPTS()는 PostgreSQL의 취소를 협력적으로 만든다. 백엔드는 폴링하는 안전 지점에서만 취소될 수 있다. 인터럽트 검사가 없는 장기 실행 C 함수는 사실상 취소 불가다. 확장 기능의 타이트한 루프나 특정 정규식 연산처럼 실제로 가끔 고통스러운 제한이다. 대안인 선점형 취소(강제로 스레드를 풀기)는 관리형 런타임의 스레드-per-연결 엔진(예: JVM 기반 시스템)이 시도할 수 있지만, 잠금과 할당자 상태 주변에서 악명 높게 안전하지 않다. 시그널 핸들러에서 실제 작업을 금지하는 것과 같은 위험이다. PostgreSQL의 선택은 취소-대기-시간을 희생해 취소가 절대 공유 상태를 손상하지 않는다는 보장을 얻는다. InterruptHoldoffCount / CritSectionCount / QueryCancelHoldoffCount 카운터는 안전 지점 창을 넓히거나 좁히는 명시적 조절 장치다. 일반 경우에 브랜치 비용을 치르지 않고 핫 경로에 더 많은 검사를 뿌려 최악의 취소 지연을 줄이는 것은 현재 진행 중인 연구/공학 과제다. INTERRUPTS_PENDING_CONDITIONunlikely() 힌트가 현재의 완화책이다.

모든 대기를 WaitEventSetWait로 집중시키는 부수적 이득은 관찰 가능성이다. 각 호출은 pgstat_report_wait_start/_end가 기록하는 wait_event_info를 전달하며, 이것이 pg_stat_activity.wait_eventwait_event_type을 채운다. PostgreSQL에서 가장 많이 사용하는 운영 진단 중 하나이며, 모든 잠들기가 하나의 모듈에 집중되어 있기 때문에 사실상 무료로 존재한다. 코드베이스 전반에 블로킹 호출을 분산시킨 엔진들은 이런 균일한 대기 계산을 사후에 추가하기 어렵다. 수집 측은 postgres-cumulative-stats.md에서 다룬다.

PostgreSQL 소스 (/data/hgryoo/references/postgres 아래, REL_18 273fe94)

섹션 제목: “PostgreSQL 소스 (/data/hgryoo/references/postgres 아래, REL_18 273fe94)”
  • src/backend/storage/ipc/latch.cInitLatch, InitSharedLatch, OwnLatch, DisownLatch, WaitLatch, WaitLatchOrSocket, SetLatch, ResetLatch, InitializeLatchWaitSet.
  • src/backend/storage/ipc/waiteventset.cInitializeWaitEventSupport, CreateWaitEventSet, AddWaitEventToSet, ModifyWaitEvent, WaitEventAdjustEpoll, WaitEventSetWait, WaitEventSetWaitBlock, latch_sigurg_handler, sendSelfPipeByte, drain, WakeupMyProc, WakeupOtherProc, 그리고 WAIT_USE_* 선택 사다리.
  • src/backend/storage/ipc/procsignal.cProcSignalSlot, ProcSignalShmemInit, ProcSignalInit, SendProcSignal, EmitProcSignalBarrier, WaitForProcSignalBarrier, ProcessProcSignalBarrier, CheckProcSignal, procsignal_sigusr1_handler, SendCancelRequest.
  • src/backend/tcop/postgres.cdie, StatementCancelHandler, HandleRecoveryConflictInterrupt, ProcessInterrupts, ProcessClientReadInterrupt, ProcessClientWriteInterrupt.
  • src/include/miscadmin.hCHECK_FOR_INTERRUPTS, INTERRUPTS_PENDING_CONDITION, INTERRUPTS_CAN_BE_PROCESSED, HOLD_INTERRUPTS/RESUME_INTERRUPTS, InterruptPending.
  • src/include/storage/procsignal.hProcSignalReason 열거형, NUM_PROCSIGNALS, ProcSignalBarrierType.
  • src/include/storage/latch.h, src/include/storage/waiteventset.hWL_* 이벤트 마스크 상수와 공개 API 프로토타입.

교재 챕터 (knowledge/research/dbms-general/ 아래)

섹션 제목: “교재 챕터 (knowledge/research/dbms-general/ 아래)”
  • Architecture of a Database System(Hellerstein et al.), §“Process Models” — 프로세스-per-연결 모델과 사용자 공간 스케줄러 대신 OS IPC/시그널링에 의존하는 방식.
  • Database Internals(Petrov) — 노드 로컬 동시성과 프로세스 모델; OLTP에서 웨이크업과 컨텍스트 스위치의 비용.
  • Operating Systems: Three Easy Pieces(Arpaci-Dusseau) — 조건 변수와 깨우기 손실 위험; 시그널 핸들러가 안전하게 발행할 수 있는 제한된 지시.
  • postgres-shared-memory-ipc.mdProcSignalshm_mq를 담는 공유 메모리 세그먼트; 슬롯 할당 방법; ProcSignal의 알림 전용 역할과 쌍을 이루는 페이로드 전달 메시지 큐.
  • postgres-backend-lifecycle.md — 백엔드가 InitializeWaitEventSupport, ProcSignalInit을 호출하고 CHECK_FOR_INTERRUPTS()를 뿌리는 PostgresMain 루프에 도달하는 위치.
  • postgres-aux-processes.md — ProcSignal 배열 끝 부근의 슬롯을 소유하고 래치를 주 대기로 사용하는 보조 프로세스들.
  • postgres-lwlock-spinlock.md — 세마포어 기반 잠금과 ConditionVariable, 래치의 다중화 불가능한 친척들.
  • postgres-wire-protocol.mdProcessClientReadInterrupt/ProcessClientWriteInterrupt로 감싸인 FE/BE 읽기/쓰기 지점, 그리고 SendCancelRequest를 트리거하는 취소 요청.
  • postgres-cumulative-stats.mdWaitEventSetWait 내부의 pgstat_report_wait_start/_end가 채우는 대기 이벤트 계산.
  • postgres-aio.md — PG18 io_uring 비동기 I/O, 트리에서 io_uring의 첫 사용자이자 대기 기구의 가능한 미래 방향.