-
공유메모리와 캐시 일관성 문제(쓰기 전파)운영체제 2025. 6. 1. 10:20
공유 메모리를 사용하는 형태는 두가지가 있다.
1. 같은 프로세스 내의 여러 스레드 간 공유 메모리
ex) 스레드 간 데이터 공유
2. 서로 다른 프로세스 간 공유 메모리
ex) 공유 메모리 IPC
이중 두번째 방법에 대해 알아보자.
프로세스 간의 공유 메모리 생성 방법
공유 메모리 생성 방법으로는 3가지가 있다.
방법1) 익명 메모리 생성 (부모-자식 간)
- 부모-자식 프로세스 간의 메모리 공유가 필요할때 사용한다. 단, 다른 프로세스에서 접근이 불가능하다.
방법2) 이름 있는(shared) 메모리 생성
- /dev/shm 아래에 파일처럼 생성된다. 단, 파일 시스템이 아닌 커널 내부에 존재한다.
- 주로 IPC 구현 용도로 사용된다.
방법3) 일반 파일 사용
- 일반 파일을 공유메모리로 사용하는 방식이다. 디스크에 저장되어 영구적인 메모리 공유가 가능하다.
구현하기
방법1) 익명 메모리 생성 (부모-자식 간)
mmap(MAP_SHARED | MAP_ANONYMOUS)로 메모리 영역을 만들고, fork()를 호출하는 방법이다.
부모와 자식은 가상주소만 다르고 물리주소는 같은 상태가 된다.
#include <sys/mman.h> #include <unistd.h> #include <stdio.h> int main() { int* shared = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); *shared = 0; if (fork() == 0) { // 자식 프로세스 (*shared) += 1; printf("Child: %d\n", *shared); } else { printf("Parent: %d\n", *shared); } return 0; }
방법2) 이름 있는(shared) 메모리 생성.
shm_open()으로 이름있는 메모리를 생성하고, mmap()으로 매핑하는 방법이다.
#include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #include <stdio.h> #include <string.h> int main() { int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666); ftruncate(fd, sizeof(int)); //mmap() 전에 int크기만큼 버퍼 할당 int* shared = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); *shared = 42; //공유 메모리에 값 저장 printf("Shared memory value: %d\n", *shared); shm_unlink("/myshm"); // 메모리 정리 return 0; }
/myshm이라는 임시 공유 파일을 만들고 fd를 리턴받아서 mmap에 넘긴다.
그 후 mmap에서 리턴한 포인터에 값을 저장한다.
ftruncate()는 파일 크기를 지정한 길이로 자르거나 늘리는 함수이다. mmap하기 전에 fd 파일크기를 int크기만큼 버퍼 할당해준다.
방법3) 일반 파일 사용
일반 파일을 open()하고 mmap(MAP_SHARED)으로 매핑하는 방법이다.
#include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #include <stdio.h> int main() { int fd = open("mapped_file.txt", O_RDWR | O_CREAT, 0666); ftruncate(fd, sizeof(int)); int* mapped = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); *mapped = 100; printf("File-mapped value: %d\n", *mapped); close(fd); return 0; }
공유메모리 일관성 문제 - 캐시 일관성 문제
공유메모리는 동시 접근이 가능하다. 따라서 한쪽이 데이터를 쓰는 중에 다른쪽이 읽기 시도하면 문제가 발생한다.
이 경우 한쪽에 lock을 걸어 접근을 못하게 막으면 된다.
하지만 lock으로 해결되지 않는 경우가 있다. 이는 캐시와 관련된다.
한 프로세스에서 값을 캐시에만 저장하고 메모리에 아직 안쓴 상태면 다른 프로세스는 값이 변경되었음을 알지 못한다.
이를 쓰기 전파(write propagation) 문제라고 한다. 더 자세히 알아보자.
아래 그림은 캐시 L1, L2, L3 를 나타낸다.
이 캐시는 아래와 같은 원칙을 지켜야 한다.
- CPU 안에 여러 코어가 존재한다.
- 각 코어는 L1을 통해서만 메모리에 접근 가능하다.
- 캐시는 계층적으로 동작한다. L1 -> L2 -> L3 순으로 접근 가능하다.
(즉, L1은 L2만 접근 가능하고, L2는 L3만 접근 가능하다.)
- L3은 여러 코어가 공유하는 공유캐시이다. L1, L2는 각 코어마다 하나씩 갖고있다.
문제 발생 시나리오
- 코어 C0가 메모리 위치 m에 쓰기 요청
- 이 쓰기는 C0의 L1 캐시에 저장됨 (아직 메인 메모리나 다른 캐시에 반영되지 않음)
- 코어 C1이 위치 m을 읽으려 함
- C1의 L1, L2 캐시에 m이 없음 (캐시 미스)
- 그래서 C1은 공유 캐시인 L3에서 m을 읽음 (아직 C0가 쓴 최신 값이 아님!!)
- C0의 L1 캐시가 m을 C0의 L2 캐시로 전파
- C0의 L2 캐시가 m을 공유 L3 캐시로 전파
즉, 아직 코어 C0가 쓴 최신 값이 L3 캐시에 반영되지 않아 C1은 오래된 값을 읽게 된다. → 메모리 일관성 위반
문제 해결법 - 메모리 배리어(memory barrier)
메모리 배리어란?
앞에 있는 명령이 끝날 때까지 다음 명령으로 안 넘어가도록 순서를 보장하는 명령어이다.
현재 코어가 쓰기한 내용이 모든 코어에에 보일때까지 기다린다.
CPU 명령어의 재배치를 막음으로써 순서를 보장한다.
메모리 접근을 막는 락(lock) 방식과 달리, 누구나 접근할 수 있지만 반드시 순서를 지키게 강제하는 방식이다.
단계별 설명
- 코어 C0가 메모리 위치 m에 쓰기 요청
- 쓰기는 C0의 L1 캐시에 저장됨
- 코어 C1이 m에 대한 모든 쓰기에 대해 배리어 요청
- C0의 L1 캐시가 m을 C0의 L2 캐시로 전파
- C0의 L2 캐시가 m을 공유 L3 캐시로 전파
- C1은 C0가 m에 대한 쓰기를 완료할 때까지 대기
- C1이 m을 읽을 때, 공유 캐시 L3에서 최신 값 읽음
즉, 배리어를 요청함으로써 C0이 L3 캐시에 반영될때까지 대기하게 된다.
이때 "대기"는 C1이 자기 순서가 올때까지 기다리느라 생긴 상황일뿐, 강제로 대기시키는 것은 아니다.
구현 방법
x86-64의 경우 mfence라는 어셈블리어 명령어를 사용한다.
mov 0xfff111, eax ;0xfff111에 데이터 쓰기 mfence ;위 코드가 작동완료되어야 아래 코드 작동하도록 제한 mov 0xfff222, ebx ;0xfff222에 데이터 쓰기
mfence를 사용하는 POSIX 함수로는 fork(), pthread_mutex_lock(), pthread_mutex_unlock(), pthread_create(), pthread_join() 등이 있다.
'운영체제' 카테고리의 다른 글
동기화 - Race condition, Critical Sections, mutex(lock), deadlock (1) 2025.06.07 IPC - signal(), kill() (0) 2025.05.24 가상 메모리와, 공유메모리 / COW와 mmap() (0) 2025.05.10 file descriptor (fd) (0) 2025.05.03 IPC - pipe (0) 2025.04.26