서로 다른 process가 같은 physical memory를 볼 수 있으면 data를 복사하는 비용을 줄일 수 있습니다. 하지만 한쪽의 write가 언제 끝났는지, 다른 쪽이 어느 순서로 읽어야 하는지까지 mapping이 해결해 주지는 않습니다.
Linux에서 System V, POSIX shared memory, shared anonymous mapping과 memfd를 비교해 보겠습니다. 공통점은 shared backing을 여러 process의 주소 공간에 연결한다는 것이고, 차이는 그 backing을 찾고 전달하며 수명을 관리하는 방식에 있습니다.
같은 backing을 알아야 같은 data를 봅니다
두 process가 각각 MAP_SHARED를 지정했다고 해서 자동으로 같은 memory를 얻는 것은 아닙니다. 같은 file object 등 공유할 backing에 접근해야 합니다. 각 process에 반환되는 virtual address 값은 서로 달라도 됩니다.
따라서 shared 영역 안에 다른 process의 raw pointer를 그대로 넣는 방식은 일반적으로 적합하지 않습니다. 동일한 mapping 위치를 보장하는 별도 설계가 없다면, 기준 주소에 대한 offset이나 index와 같이 각 process가 해석할 수 있는 표현이 필요합니다.
동기화도 별도입니다. Process-shared mutex나 semaphore, 적절한 atomic protocol 등으로 data의 소유권과 순서를 정해야 합니다. 단순히 sleep()으로 시간을 벌거나 volatile을 붙이는 것만으로 IPC의 동시 접근 규칙이 만들어지지는 않습니다.
System V는 segment ID로 attach합니다
shmget()은 key와 크기, 권한 및 creation flag를 사용해 shared memory segment를 찾거나 만듭니다. IPC_CREAT는 없으면 생성하라는 의미이며, 이미 존재하는 segment와의 충돌을 다루려면 IPC_EXCL 등 원하는 규약을 명시해야 합니다.
shmat()은 segment를 process의 주소 공간에 attach하고 그 주소를 반환합니다. NULL 주소 요청은 kernel의 위치 선택을 허용하는 일반적인 방식입니다. 다른 process도 같은 segment에 attach해야 같은 data를 봅니다.
수명 관리에서는 detach와 segment 제거를 구분해야 합니다. shmdt()는 해당 process의 attachment를 해제하고, shmctl()의 IPC_RMID는 segment의 제거 절차와 연결됩니다. 한 process가 종료된다는 사실만으로 application이 의도한 자원 정리가 모두 끝나는 설계인지 확인해야 합니다.
Linux 구현에서는 shmem backing이 이 동작과 연결됩니다. 그러나 userspace API에서 System V key와 segment ID를 일반 file descriptor와 같은 값으로 취급해서는 안 됩니다.
Linux v6.12의 일반적인 non-HugeTLB 생성 경로에서는 shmget()의 새 segment 처리가 newseg()로 이어지고 shmem_kernel_file_setup()으로 이름 없는 shmem file을 준비합니다. Directory에 pathname이 없다는 사실과 backing file object가 없다는 뜻은 다릅니다. SHM_HUGETLB를 사용하는 별도 경로까지 같은 tmpfs 생성으로 일반화하지 않습니다. Linux v6.12 ipc/shm.c
POSIX shared memory는 descriptor로 mapping합니다
shm_open()으로 이름 있는 object를 열면 file descriptor를 얻습니다. 새 object를 만든 뒤 ftruncate()로 크기를 정하고 MAP_SHARED로 mapping하는 흐름이 일반적입니다. Linux에서는 보통 /dev/shm의 tmpfs를 통해 구현되지만 그 경로를 모든 POSIX 환경의 고정 규칙으로 볼 수는 없습니다.
이름을 제거하는 shm_unlink(), descriptor를 닫는 close(), mapping을 제거하는 munmap()은 서로 다른 수명 단계를 다룹니다. 이름이 없어져도 이미 참조 중인 object가 즉시 모든 process에서 사라지는 것은 아닙니다.
기존 object를 여는 client는 creation 여부, 크기와 초기화 완료 상태를 확인할 규약이 필요합니다. Descriptor를 얻었다는 사실만으로 다른 process가 initialization을 모두 끝냈다고 가정하면 race가 생깁니다.
Parent와 child는 shared anonymous mapping을 상속할 수 있습니다
fork()에서 private mapping은 CoW로 처리될 수 있지만, 이미 shared인 mapping의 sharing 성격까지 사라지는 것은 아닙니다. Parent가 MAP_SHARED | MAP_ANONYMOUS로 만든 mapping은 child가 상속해서 사용할 수 있습니다. Linux에서는 이 경로도 shmem backing과 연결됩니다.
아래 전체 예제는 mapping을 만든 뒤 child가 data를 쓰고, parent가 waitpid()로 child의 종료를 기다린 다음 읽는 간단한 순서입니다. 동시에 서로 data를 갱신하는 일반적인 IPC protocol은 아닙니다. 별도의 동시 읽기·쓰기 동기화가 필요 없는 단계로 제한한 예입니다.
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
long page_size = sysconf(_SC_PAGESIZE);
if (page_size < 64) {
fputs("could not obtain page size\n", stderr);
return EXIT_FAILURE;
}
char *shared = mmap(NULL, (size_t)page_size, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (shared == MAP_FAILED) {
perror("mmap");
return EXIT_FAILURE;
}
pid_t child = fork();
if (child == -1) {
perror("fork");
munmap(shared, (size_t)page_size);
return EXIT_FAILURE;
}
if (child == 0) {
const char message[] = "written by the child";
memcpy(shared, message, sizeof(message));
_exit(EXIT_SUCCESS);
}
int status;
pid_t result;
do {
result = waitpid(child, &status, 0);
} while (result == -1 && errno == EINTR);
if (result == -1 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) {
fputs("child did not finish successfully\n", stderr);
munmap(shared, (size_t)page_size);
return EXIT_FAILURE;
}
puts(shared);
if (munmap(shared, (size_t)page_size) == -1) {
perror("munmap");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
이 예제는 page 하나만 사용하며 global VM 설정을 변경하지 않습니다. Child가 쓰는 동안 parent는 data를 읽지 않고, 성공적인 종료를 확인한 뒤 문자열을 출력합니다. 반복적인 양방향 통신으로 확장하려면 준비 상태, 종료와 오류의 전달 및 data 동기화를 새로 설계해야 합니다.
memfd는 이름 대신 descriptor를 전달합니다
memfd_create()는 anonymous file을 가리키는 descriptor를 만듭니다. 크기를 ftruncate()로 정한 뒤 shared mapping으로 사용할 수 있습니다. 표시용 이름이 같다는 이유만으로 다른 memfd가 같은 object가 되는 것은 아닙니다.
Process 간에는 UNIX domain socket의 descriptor 전달 등으로 같은 object에 대한 참조를 전달할 수 있습니다. /proc/PID/fd/N 경로를 열어 접근하는 방법에는 권한과 process 수명, descriptor 재사용 등의 문제가 있습니다. 경로 문자열이 descriptor 자체와 동일한 전달 수단은 아닙니다.
MFD_ALLOW_SEALING을 사용하면 seal로 특정 변경을 제한할 수 있습니다. 예를 들어 F_SEAL_GROW는 크기 증가를 제한하고 F_SEAL_SEAL은 이후 seal 추가를 막습니다. 이 둘만으로 기존 범위의 write나 shrink까지 모두 금지되는 것은 아닙니다. 각각 어떤 동작을 제한하는지 따로 선택해야 합니다.

Sealing은 공유 object의 변경 가능성을 통제하지만 data 접근 순서에 관한 동기화를 대신하지는 않습니다. 누가 object를 만들고, 누가 초기화하고, 누가 어떤 권한으로 참조를 전달하며, 마지막 참조를 언제 놓는지까지 정해야 shared memory가 application의 IPC로 완성됩니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access edition, 2025, §14.7.
- Linux man-pages
shmget(2) - Linux man-pages
shm_open(3) - Linux man-pages
memfd_create(2) - Linux man-pages
mmap(2)