← System Programming DUJINLABS.COM

Signal / Event / IPC · Linux userspace / kernel ABI

POSIX shared memory와 process-shared semaphore

shm_open으로 이름을 inode-like object에 연결하고 MAP_SHARED memory 안의 semaphore로 data publish 순서를 맞춥니다.

Series
25 / 37
Build
cc -std=c17 -Wall -Wextra -O2 -pthread shared_sem.c -o shared_sem
Run
./shared_sem
Kernel
Linux 6.18.37 LTS

같은 physical page를 mapping하면 synchronization 없이 구조체도 안전하게 공유되는가?

shared memory는 byte를 같은 backing page에서 보이게 할 뿐, 여러 CPU의 read/write 순서와 object consistency를 자동으로 정하지 않는다. writer가 payload를 완성하기 전에 ready flag가 보이면 reader는 절반만 갱신된 구조를 읽을 수 있다.

process-shared sem_t, pthread mutex/condvar, futex, atomic protocol 중 하나로 publish와 consume 순서를 만든다. synchronization object 자체도 MAP_SHARED range 안에 있고 pshared 속성으로 초기화돼야 한다.

구조 그림

그림 1. 서로 다른 VA가 같은 shared page를 가리키는 구조

Process A

  • VA 0x7f10…
  • message write
  • sem_post release

Shared pages

  • offset 0: sem_t
  • offset 64: message
  • tmpfs/memfd backing

Process B

  • VA 0x6a20…
  • sem_wait acquire
  • message read

Naming / lifetime

  • shm_open name
  • fd references
  • shm_unlink 후 mapping 유지

process마다 mapping 주소는 달라도 backing page는 같다. raw pointer 대신 offset을 저장하고 semaphore로 publish 순서를 맞춘다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
shm_open 이름으로 object open
ftruncate 공유 크기 확정
MAP_SHARED 각 process mapping
sem_post/wait publish/acquire
unlink 이름 제거

shared data 수명, 이름 수명, fd 수명, mapping 수명, synchronization object 수명을 각각 누가 끝내는지 정한다.

그림 3. 커널 내부에서 지나가는 주요 지점
tmpfs inode /dev/shm object
mmap page cache mapping
sem fast path user atomic
futex wait/wake contention 때 kernel
munmap/unlink 참조 정리

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
mm/shmem.c shmem_file_setup(), shmem_mmap() tmpfs/shared memory backing과 page fault
kernel/futex/waitwake.c futex_wait(), futex_wake() semaphore contention 시 sleep/wakeup 기반
mm/mmap.c mmap_region() 같은 file을 MAP_SHARED VMA로 연결

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 -pthread shared_sem.c -o shared_sem
01#define _DEFAULT_SOURCE
02#include <errno.h>
03#include <fcntl.h>
04#include <semaphore.h>
05#include <stdio.h>
06#include <string.h>
07#include <sys/mman.h>
08#include <sys/wait.h>
09#include <unistd.h>
10
11struct shared_data {
12    sem_t ready;
13    char message[128];
14};
15
16int main(void)
17{
18    struct shared_data *data = mmap(NULL, sizeof(*data), PROT_READ | PROT_WRITE,
19                                    MAP_SHARED | MAP_ANONYMOUS, -1, 0);
20    if (data == MAP_FAILED || sem_init(&data->ready, 1, 0) < 0)
21        return 1;
22    pid_t pid = fork();
23    if (pid == 0) {
24        while (sem_wait(&data->ready) < 0)
25            if (errno != EINTR)
26                _exit(2);
27        dprintf(STDOUT_FILENO, "child read: %s\n", data->message);
28        _exit(0);
29    }
30
31    strcpy(data->message, "published through shared memory");
32    sem_post(&data->ready);
33    waitpid(pid, NULL, 0);
34    sem_destroy(&data->ready);
35    return munmap(data, sizeof(*data)) != 0;
36}

코드 조각별 설명

실제 코드 11행struct shared_data

synchronization object와 보호할 payload를 같은 shared mapping에 둔다. 서로 다른 process의 일반 heap pointer를 구조체에 저장하면 의미가 없다.

실제 코드 19행MAP_SHARED | MAP_ANONYMOUS

fork 전 만든 anonymous shared mapping이라 parent와 child가 같은 backing page를 본다. unrelated process라면 shm_open/memfd를 사용한다.

실제 코드 20행sem_init(&data->ready, 1, 0)

pshared=1로 process 사이에서 쓰는 semaphore를 초기값 0으로 만든다.

실제 코드 31행strcpy(data->message

payload를 먼저 완성한 뒤 sem_post로 publish한다. semaphore operation이 필요한 memory ordering을 제공한다.

실제 코드 34행sem_destroy

다른 process/thread가 더 이상 wait/post하지 않는다는 합의 뒤에 파괴한다. wait 중인 semaphore를 destroy하면 정의되지 않은 동작이다.

세부 동작

01

이름 제거와 object 제거는 다르다

shm_unlink는 새 shm_open이 찾는 이름을 제거하지만 이미 열린 fd와 mapping은 계속 유효하다. 일반 file unlink와 비슷하다.

creator가 초기화 완료 전에 이름을 공개하면 opener가 미완성 header를 볼 수 있으므로 version/magic/ready protocol을 둔다.

02

shared pointer는 address가 다를 수 있다

unrelated process는 같은 shared object를 서로 다른 virtual address에 mapping할 수 있다. mapping 안에 raw pointer를 저장하면 다른 process에서 유효하지 않다.

base-relative offset, index, fixed-width handle을 사용하고 range validation을 한다.

03

owner crash를 protocol에 포함한다

semaphore를 감소한 process가 작업 중 죽으면 자동으로 credit이 복구되지 않는다. mutex라면 robust process-shared mutex와 EOWNERDEAD 복구를 검토한다.

shared header에 generation, state, checksum을 두고 recovery owner를 한 명 정한다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
shared backing objectshm_open/memfd 또는 anonymous fork mapping에서 생기고 마지막 참조 후 해제된다size, seals, name/link
shared mapping각 process mmap에서 별도로 생기고 munmap/exit에서 사라진다base address, protection
sem_tshared page에서 sem_init되고 합의된 마지막 사용 뒤 sem_destroy된다pshared, count, waiter

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
reader가 깨진 구조체를 봄publish synchronization 누락writer order와 acquire primitive
다른 process pointer crashraw virtual address 공유offset/index encoding 확인
영원히 sem_waitproducer crash 또는 credit protocol 오류timeout, owner heartbeat, recovery

직접 확인

  1. sem_post를 message write 앞으로 옮겨 잘못된 publish 순서를 stress loop에서 관찰한 뒤 원복한다.
  2. shm_open 버전으로 바꿔 unrelated process 두 개가 서로 다른 mapping base에서도 offset으로 data를 찾게 만든다.
  3. PTHREAD_PROCESS_SHARED와 PTHREAD_MUTEX_ROBUST mutex를 사용해 owner crash 뒤 EOWNERDEAD 복구를 구현한다.
실행./shared_sem
추적strace -f -e trace=openat,ftruncate,mmap,futex,wait4,munmap,unlink ./shared_sem

원문