POSIX shared memory의 핵심은 shm_open() 호출 하나가 아닙니다. 새 object의 크기를 정하고, process-shared semaphore를 초기화하고, client가 사용할 준비가 됐다는 사실을 알린 뒤, 마지막 참조를 내려놓는 순서까지 필요합니다. 이 글에서는 client가 21을 보내고 server가 42로 답하는 작은 IPC를 독립 process 두 개로 실행하겠습니다.
아래는 책의 listing을 옮긴 코드가 아닌 독립 Linux C 예제입니다. 실습 묶음의 목표는 Linux 4.17 이상과 glibc 2.27 이상이며, 같은 architecture·libc ABI로 build한 신뢰하는 server/client 한 쌍을 전제합니다. Linux man-pages 6.19와 kernel의 tmpfs 문서를 기술 근거로 사용했습니다.
이름이 존재하는 것과 초기화가 끝난 것은 다릅니다
새 POSIX shared memory object의 길이는 처음에 0입니다. Server는 shm_open(O_CREAT | O_EXCL | O_RDWR, 0600)으로 자신이 새 object의 owner임을 확인하고, ftruncate()로 structure 크기를 설정한 뒤 MAP_SHARED로 mapping합니다. O_EXCL 없이 기존 object를 재사용해 sem_init()을 다시 호출하면 다른 process가 사용하던 상태를 훼손할 수 있습니다. shm_open(3)
Client는 이름이 보인다는 이유로 바로 attach하지 않습니다. Server가 크기 설정과 semaphore 초기화를 끝내고 control socket으로 이름을 전달한 뒤에만 shm_open()합니다. Client에는 O_CREAT와 ftruncate()가 없습니다. Client의 역할은 기존 object를 검증하고 사용하는 것이지, 준비가 덜 된 object를 임의로 완성하는 것이 아니기 때문입니다.
Linux/glibc에서는 통상 /dev/shm의 tmpfs를 사용합니다. 이는 다른 POSIX system에서도 같은 pathname을 직접 열라는 뜻이 아닙니다. 여기서는 API에 /이름 형식을 전달하며 directory 경로를 직접 조합하지 않습니다. tmpfs는 필요에 따라 swap을 사용할 수도 있으므로 shared memory를 "반드시 물리 RAM에 고정된 buffer"라고 설명하지 않겠습니다. Linux kernel tmpfs 문서
Shared structure 안의 semaphore가 순서를 만듭니다
exchange.h의 structure에는 request, reply, done semaphore와 정수 value가 있습니다. 초기화의 pshared는 1이며, 그 structure 자체가 MAP_SHARED 영역에 있습니다. Header는 아래 전체 코드만으로 완결됩니다.
#ifndef LAB_EXCHANGE_H
#define LAB_EXCHANGE_H
#include <errno.h>
#include <semaphore.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>
struct exchange {
sem_t request;
sem_t reply;
sem_t done;
uint32_t value;
};
static inline int exchange_init(struct exchange *box)
{
box->value = 0;
if (sem_init(&box->request, 1, 0) == -1)
return -1;
if (sem_init(&box->reply, 1, 0) == -1) {
int saved = errno;
sem_destroy(&box->request);
errno = saved;
return -1;
}
if (sem_init(&box->done, 1, 0) == -1) {
int saved = errno;
sem_destroy(&box->reply);
sem_destroy(&box->request);
errno = saved;
return -1;
}
return 0;
}
static inline int exchange_wait(sem_t *sem)
{
struct timespec deadline;
if (clock_gettime(CLOCK_REALTIME, &deadline) == -1)
return -1;
deadline.tv_sec += 15;
int result;
do {
result = sem_timedwait(sem, &deadline);
} while (result == -1 && errno == EINTR);
return result;
}
static inline int exchange_server(struct exchange *box)
{
if (exchange_wait(&box->request) == -1)
return -1;
if (box->value != 21) {
errno = EPROTO;
return -1;
}
box->value *= 2;
if (sem_post(&box->reply) == -1)
return -1;
return exchange_wait(&box->done);
}
static inline int exchange_client(struct exchange *box)
{
box->value = 21;
if (sem_post(&box->request) == -1 ||
exchange_wait(&box->reply) == -1)
return -1;
if (box->value != 42) {
errno = EPROTO;
return -1;
}
puts("reply=42");
return sem_post(&box->done);
}
static inline int exchange_destroy(struct exchange *box)
{
int result = 0;
if (sem_destroy(&box->done) == -1)
result = -1;
if (sem_destroy(&box->reply) == -1)
result = -1;
if (sem_destroy(&box->request) == -1)
result = -1;
return result;
}
#endif
한 번의 교환에서 value를 쓰는 owner는 차례대로 바뀝니다. Client는 21을 쓴 다음 request를 post합니다. Server는 request wait가 성공해야 읽고, 42로 바꾼 다음 reply를 post합니다. Client는 reply wait가 성공한 뒤에만 42를 읽고 검사합니다. 여기에는 동시에 value를 읽고 쓰는 polling loop가 없습니다. volatile이나 sleep()도 data 전달 protocol에 사용하지 않습니다.
sem_timedwait()는 각 wait에 15초의 absolute deadline을 둡니다. EINTR 재시도에서도 deadline은 유지되지만 CLOCK_REALTIME 변경까지 막는 장치는 아닙니다. Process-shared semaphore를 지원하지 않아 sem_init()이 실패하면 그 오류를 보고하고 초기화를 중단합니다. sem_init(3), sem_wait(3)
Client가 done을 post한 것만으로 정리가 끝난 것은 아닙니다. Client는 이어서 mapping과 descriptor를 해제하고, 마지막에 control connection을 닫습니다. Server는 done과 socket EOF를 모두 확인한 정상 경로에서만 semaphore를 destroy합니다.
Control socket은 준비 상태와 종료 상태를 전달합니다
다음 channel.h는 한 client를 위한 UNIX domain SOCK_SEQPACKET 연결입니다. 고정 길이 이름 record를 주고받고, 대기를 제한하며, 정상 종료 시 EOF를 확인합니다. 실제 계산값은 이 socket으로 전송하지 않습니다.
#ifndef LAB_CHANNEL_H
#define LAB_CHANNEL_H
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
static inline int channel_wait(int fd)
{
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) == -1)
return -1;
long long end = (long long)now.tv_sec * 1000 + now.tv_nsec / 1000000 + 15000;
for (;;) {
if (clock_gettime(CLOCK_MONOTONIC, &now) == -1)
return -1;
long long left = end - ((long long)now.tv_sec * 1000 + now.tv_nsec / 1000000);
if (left <= 0) {
errno = ETIMEDOUT;
return -1;
}
struct pollfd event = {.fd = fd, .events = POLLIN};
int result = poll(&event, 1, (int)left);
if (result > 0)
return 0;
if (result == -1 && errno != EINTR)
return -1;
}
}
static inline int channel_open(const char *path, int server)
{
struct sockaddr_un address = {.sun_family = AF_UNIX};
if (strlen(path) >= sizeof(address.sun_path)) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(address.sun_path, path, strlen(path) + 1);
int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0);
if (fd == -1)
return -1;
int bound = 0;
if (server) {
if (bind(fd, (struct sockaddr *)&address, sizeof(address)) == -1)
goto fail;
bound = 1;
if (listen(fd, 1) == -1)
goto fail;
printf("READY %s\n", path);
fflush(stdout);
} else if (connect(fd, (struct sockaddr *)&address, sizeof(address)) == -1) {
goto fail;
}
return fd;
fail:;
int saved = errno;
close(fd);
if (bound)
unlink(path);
errno = saved;
return -1;
}
static inline int channel_accept(int listener)
{
if (channel_wait(listener) == -1)
return -1;
return accept4(listener, NULL, NULL, SOCK_CLOEXEC);
}
static inline int channel_send(int fd, const void *data, size_t length)
{
ssize_t count;
do {
count = send(fd, data, length, MSG_NOSIGNAL);
} while (count == -1 && errno == EINTR);
if (count == (ssize_t)length)
return 0;
if (count >= 0)
errno = EPROTO;
return -1;
}
static inline int channel_receive(int fd, void *data, size_t length)
{
if (channel_wait(fd) == -1)
return -1;
ssize_t count = recv(fd, data, length, MSG_TRUNC | MSG_DONTWAIT);
if (count == (ssize_t)length)
return 0;
if (count >= 0)
errno = EPROTO;
return -1;
}
static inline int channel_finished(int fd)
{
if (channel_wait(fd) == -1)
return -1;
char byte;
ssize_t count = recv(fd, &byte, 1, MSG_DONTWAIT);
if (count == 0)
return 0;
if (count > 0)
errno = EPROTO;
return -1;
}
static inline int channel_remove(int listener, const char *path)
{
int result = 0;
if (close(listener) == -1)
result = -1;
if (unlink(path) == -1)
result = -1;
return result;
}
#endif
Socket을 둘 directory는 mktemp -d로 자신만 접근하는 곳을 만듭니다. 실행 중인 server의 socket 이름과 충돌해도 기존 socket을 지우지 않습니다. 같은 user의 악의적인 process나 잘못된 protocol까지 격리하는 인증 체계는 없으므로, 이 실습은 협력하는 local process 한 쌍의 범위입니다.
Server와 client의 책임을 나눕니다
posix_server.c는 shared memory 이름을 command-line argument로 받습니다. 이름 record는 64 byte로 제한해 완전하게 초기화한 뒤 전송합니다. Object를 실제로 생성한 경우에만 shm_unlink()의 책임을 갖습니다. O_EXCL 충돌로 실패한 process가 다른 owner의 object를 지우지 않도록 descriptor 획득 여부를 구분합니다.
#define _GNU_SOURCE
#include "channel.h"
#include "exchange.h"
#include <fcntl.h>
#include <stdlib.h>
#include <sys/mman.h>
int main(int argc, char **argv)
{
if (argc != 3 || strlen(argv[2]) >= 64) {
fprintf(stderr, "usage: %s SOCKET /SHM_NAME (under 64 bytes)\n", argv[0]);
return EXIT_FAILURE;
}
char name[64] = {0};
memcpy(name, argv[2], strlen(argv[2]));
int result = EXIT_FAILURE, listener = -1, peer = -1;
int initialized = 0, published = 0;
int fd = shm_open(name, O_CREAT | O_EXCL | O_RDWR, 0600);
struct exchange *box = MAP_FAILED;
if (fd == -1 || ftruncate(fd, (off_t)sizeof(*box)) == -1)
goto out;
box = mmap(NULL, sizeof(*box), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (box == MAP_FAILED || exchange_init(box) == -1)
goto out;
initialized = 1;
listener = channel_open(argv[1], 1);
if (listener == -1)
goto out;
peer = channel_accept(listener);
if (peer == -1)
goto out;
published = 1;
if (channel_send(peer, name, sizeof(name)) == -1 ||
exchange_server(box) == -1 || channel_finished(peer) == -1)
goto out;
puts("exchange complete");
result = EXIT_SUCCESS;
out:
if (result != EXIT_SUCCESS)
perror("POSIX server");
if (initialized && (!published || result == EXIT_SUCCESS) &&
exchange_destroy(box) == -1) {
perror("sem_destroy");
result = EXIT_FAILURE;
}
if (box != MAP_FAILED && munmap(box, sizeof(*box)) == -1) {
perror("munmap");
result = EXIT_FAILURE;
}
if (fd != -1) {
if (close(fd) == -1) {
perror("close");
result = EXIT_FAILURE;
}
if (shm_unlink(name) == -1) {
perror("shm_unlink");
result = EXIT_FAILURE;
}
}
if (peer != -1 && close(peer) == -1) {
perror("peer close");
result = EXIT_FAILURE;
}
if (listener != -1) {
if (channel_remove(listener, argv[1]) == -1) {
perror("listener cleanup");
result = EXIT_FAILURE;
}
}
return result;
}
posix_client.c는 이름 record의 길이와 NUL 종료를 확인합니다. fstat()으로 size와 owner를 검사한 뒤 mapping하며, cleanup에서는 자신의 mapping과 descriptor만 해제합니다. Shared structure의 raw pointer를 socket으로 보내지 않으므로 mapping 주소가 서로 달라도 됩니다.
#define _GNU_SOURCE
#include "channel.h"
#include "exchange.h"
#include <fcntl.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
int main(int argc, char **argv)
{
if (argc != 2) {
fprintf(stderr, "usage: %s SOCKET\n", argv[0]);
return EXIT_FAILURE;
}
int result = EXIT_FAILURE, fd = -1;
int peer = channel_open(argv[1], 0);
struct exchange *box = MAP_FAILED;
char name[64];
struct stat info;
if (peer == -1 || channel_receive(peer, name, sizeof(name)) == -1)
goto out;
if (name[0] != '/' || memchr(name, '\0', sizeof(name)) == NULL) {
errno = EPROTO;
goto out;
}
fd = shm_open(name, O_RDWR, 0);
if (fd == -1 || fstat(fd, &info) == -1)
goto out;
if (info.st_size != (off_t)sizeof(*box) || info.st_uid != geteuid()) {
errno = EPROTO;
goto out;
}
box = mmap(NULL, sizeof(*box), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (box == MAP_FAILED || exchange_client(box) == -1)
goto out;
result = EXIT_SUCCESS;
out:
if (result != EXIT_SUCCESS)
perror("POSIX client");
if (box != MAP_FAILED && munmap(box, sizeof(*box)) == -1) {
perror("munmap");
result = EXIT_FAILURE;
}
if (fd != -1 && close(fd) == -1) {
perror("close");
result = EXIT_FAILURE;
}
if (peer != -1 && close(peer) == -1) {
perror("peer close");
result = EXIT_FAILURE;
}
return result;
}
File descriptor를 얻었다고 initialization이 완료됐다고 보장되지는 않습니다. 이 예제의 보장은 같은 server가 초기화를 마친 후 record를 보낸다는 application protocol에서 나옵니다. Size 검사 하나만으로 untrusted peer의 모든 race를 막는다고 해석해서는 안 됩니다.
세 가지 lifetime을 구분해 보겠습니다
| Operation | 해제하는 것 | 이미 존재하는 다른 참조 |
|---|---|---|
close(fd) |
그 descriptor | 기존 mapping은 별도로 남을 수 있습니다 |
munmap() |
그 process의 mapping | 다른 process의 mapping은 별도로 남습니다 |
shm_unlink() |
object를 다시 찾는 이름 | 이미 열린 descriptor와 mapping은 즉시 무효화되지 않습니다 |
정상 완료에서는 client가 자기 참조를 해제하고 EOF를 보낸 뒤, server가 semaphore를 destroy하고 자기 mapping·descriptor·이름을 해제합니다. Server에서 shm_unlink()했다고 client의 pointer가 곧바로 유효하지 않게 되는 모델은 아닙니다. shm_open(3)
오류가 공개 이전에 발생했다면 server 혼자 가진 semaphore를 정리합니다. 공개 후 protocol이 실패하면 client가 wait 중일 가능성이 있으므로 destroy를 건너뛰고 자기 참조와 object 이름을 정리합니다. Client가 이미 mapping했다면 그 참조는 client가 timeout 후 놓을 때까지 남을 수 있습니다. 이 abort 경로는 Linux/glibc를 대상으로 한 object 폐기이며 다른 POSIX implementation에 그대로 이식하는 cleanup 정책은 아닙니다. Waiter가 존재하는 semaphore를 destroy하면 undefined behavior가 됩니다. sem_destroy(3)
직접 실행해 보겠습니다
exchange.h, channel.h, posix_server.c, posix_client.c가 같은 labs directory에 있는 상태에서 시작합니다. -lrt는 shm_open()의 library 연결을 위해 명시했습니다.
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -O2 -pthread posix_server.c -o posix_server -lrt
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -O2 -pthread posix_client.c -o posix_client -lrt
run_dir=$(mktemp -d ./posix-run-XXXXXX)
shm_name="/codex-posix-$(basename "$run_dir")"
./posix_server "$run_dir/control.sock" "$shm_name" &
server=$!
READY가 보인 뒤 같은 shell에서 이어서 실행합니다. 각 실행은 client 한 개만 받으며, server의 연결 대기는 15초입니다.
./posix_client "$run_dir/control.sock"
wait "$server"
rmdir "$run_dir"
정상 교환의 출력은 client의 reply=42와 server의 exchange complete입니다. Client를 먼저 실행하면 연결 실패가 나옵니다. Client 없이 server만 두면 timeout으로 실패하면서 자신의 object 이름과 socket을 정리합니다. 같은 shared memory 이름으로 두 번째 server를 시작하면 기존 object는 그대로 남고 새 server가 EEXIST로 실패해야 합니다.
이름을 제거하기 전 server가 SIGKILL을 받으면 object 이름과 socket pathname이 남을 수 있습니다. 해당 실행의 process가 모두 종료됐고 owner와 정확한 이름이 일치하는지 확인한 뒤 그 자원만 제거해야 합니다. 운영 환경이라면 이러한 restart·stale object 처리를 별도의 정책으로 설계해야 합니다. 이 코드에는 signal handler나 자동 복구 service가 없습니다.
확인한 범위
2026-11-03, x86-64 WSL2 Linux 6.18.33.2-microsoft-standard-WSL2, GCC 15.2.0, glibc 2.43에서 compile하고 실행했습니다. 정상 왕복 여덟 번, client 선실행, client 부재 timeout, shared memory 이름 충돌, socket 충돌, 잘못된 길이의 이름 record 거절을 확인했습니다. 충돌 시 기존 owner의 object/socket은 유지됐고, 정상·timeout·bind 실패 뒤 자신이 만든 자원은 제거됐습니다.
서로 다른 libc·architecture 조합, 다중 client, crash recovery, /dev/shm 용량 고갈과 모든 system call 실패 주입은 미검증입니다. 낮은 version의 Linux·glibc도 목표 API 범위일 뿐 실제 실행 검증을 마친 version 범위는 아닙니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access edition, 2025, §14.7.2. 위 코드는 독립 예제입니다.
- Linux man-pages 6.19,
shm_open(3),sem_init(3),sem_wait(3),sem_destroy(3). 2026-11-03 열람. - Linux kernel documentation, Tmpfs. Version 고정 URL이 아닌 live documentation이며 2026-11-03 열람했습니다.