ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 공유메모리와 캐시 일관성 문제(쓰기 전파)
    운영체제 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는 각 코어마다 하나씩 갖고있다.

     

    문제 발생 시나리오

    1. 코어 C0가 메모리 위치 m에 쓰기 요청
    2. 이 쓰기는 C0의 L1 캐시에 저장됨 (아직 메인 메모리나 다른 캐시에 반영되지 않음)
    3. 코어 C1이 위치 m을 읽으려 함
    4. C1의 L1, L2 캐시에 m이 없음 (캐시 미스)
    5. 그래서 C1은 공유 캐시인 L3에서 m을 읽음 (아직 C0가 쓴 최신 값이 아님!!)
    6. C0의 L1 캐시가 m을 C0의 L2 캐시로 전파
    7. C0의 L2 캐시가 m을 공유 L3 캐시로 전파

    즉, 아직 코어 C0가 쓴 최신 값이 L3 캐시에 반영되지 않아 C1은 오래된 값을 읽게 된다. → 메모리 일관성 위반

     

    문제 해결법 - 메모리 배리어(memory barrier)

    메모리 배리어란?

    앞에 있는 명령이 끝날 때까지 다음 명령으로 안 넘어가도록 순서를 보장하는 명령어이다.

    현재 코어가 쓰기한 내용이 모든 코어에에 보일때까지 기다린다.

    CPU 명령어의 재배치를 막음으로써 순서를 보장한다.

    메모리 접근을 막는 락(lock) 방식과 달리, 누구나 접근할 수 있지만 반드시 순서를 지키게 강제하는 방식이다.

     

    단계별 설명

    1. 코어 C0가 메모리 위치 m에 쓰기 요청
    2. 쓰기는 C0의 L1 캐시에 저장됨
    3. 코어 C1이 m에 대한 모든 쓰기에 대해 배리어 요청
    4. C0의 L1 캐시가 m을 C0의 L2 캐시로 전파
    5. C0의 L2 캐시가 m을 공유 L3 캐시로 전파
    6. C1은 C0가 m에 대한 쓰기를 완료할 때까지 대기
    7. 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
Designed by Tistory.