시프 25- MemConcurrency

강준호·2024년 6월 2일

시스템프로그래밍

목록 보기
17/18

메모리와 동시성 (Memory and Concurrency)

공유 메모리와 동시성 문제

공유 메모리의 개념

  • 공유 상태는 공유 메모리에 존재합니다.
  • 공유 메모리는 동시성 문제를 일으킬 수 있습니다.
  • 메모리가 어떻게 공유되는지 이해하는 것이 중요합니다.

공유 메모리의 유형

  • 동일 프로세스 내의 동일 스레드가 다른 시간에 사용하는 메모리
    • 일반적으로 문제를 일으키지 않음.
  • 동일 프로세스 내의 다른 스레드가 사용하는 메모리
    • 동시성 문제를 일으킬 수 있음.
  • 다른 프로세스가 사용하는 메모리
    • 동시성 문제를 일으킬 수 있음.

공유 메모리 획득

프로세스 내에서 공유 메모리

  • 프로세스 내에서 메모리를 공유하는 것은 특별한 설정이 필요하지 않음.

프로세스 간 공유 메모리

  • 커널의 지원이 필요하며, 다음과 같은 방법들이 있습니다:
    • shm_open(): 이름이 지정된 공유 메모리 영역에 연결.
    • 메모리 매핑된 파일을 사용.
    • 분기(fork) 전에 공유 매핑 생성.

일관성 문제

race condition 레이스 조건 조심

  • 레이스 조건(race condition)은 여러 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제입니다.
  • 동기화 메커니즘을 사용하여 이러한 문제를 방지할 수 있습니다.

일관성 문제 개요

  • 메모리와 동시성의 문제는 주로 일관성 문제와 관련이 있습니다.
  • 전용 컴퓨터 모델에서는 다음과 같은 기대가 있습니다:
    • 메모리 위치에 쓰기 작업이 즉시 반영됨.
    • 메모리 위치에 대한 쓰기는 지속적임.

동시성 흐름에서의 일관성 문제

  • 이러한 기대는 동시성 흐름에서 깨질 수 있습니다.
  • 동기화를 통해 이를 완화할 수 있습니다.
  • 그러나 동기화는 타이밍뿐만 아니라 더 많은 것을 제어해야 합니다.

동기화 메커니즘

  • 완전한 동기화를 위해 하드웨어 및 시스템 수준에서 보장해야 합니다.
  • 캐시 일관성을 유지하기 위해 메모리 배리어(memory barrier)가 필요합니다.
  • 동시 프로그래밍에서는 쓰기의 즉각성과 내구성에 대한 기대가 깨질 수 있으므로 동기화가 필수적입니다.

시간적 동기화 및 공간적 동기화

  • 동기화는 단순히 타이밍에 관한 것이 아니라, 작업이 시스템 전체에 표시되도록 보장하는 것입니다.
  • 시간적 고려 사항: 연산 o1이 o2보다 먼저 실행되고, 인터럽트 없이 유지됨.
  • 공간적 고려 사항: 한 연산이 시스템의 다른 부분에서도 일관되게 보이는 상태를 유지함.

시간적 동기화 (Temporal Synchronization)

시간적 동기화 개념

  • 지금까지 동기화를 시간적 구조로 생각했습니다:
    • 연산 o1이 연산 o2 전에 발생함.
    • 연산 순서가 중단되지 않음.

공간적 동기화 (Spatial Synchronization)

  • 공간적 동기화도 고려해야 합니다:
    • 한 연산이 시스템의 다른 부분에서 볼 수 있는 상태가 됨.

캐싱 (Caching)

캐싱 개요

  • 현대 컴퓨터에는 여러 계층의 캐시가 있습니다.
  • 일부 캐시는 공유되고, 일부는 로컬입니다:
    • 특정 CPU 코어에 로컬
    • 코어의 하위 집합에 로컬
    • 프로세스에 로컬

캐싱의 이유

  • 성능 향상을 위해 캐시가 사용됩니다.
  • 낮은 레벨의 캐시는 훨씬 빠르지만 훨씬 작습니다.
    • L0-L1 캐시는 코어에 로컬, L2-3는 코어 또는 코어의 하위 집합에 로컬.
    • L4는 일반적으로 공유됨.

캐싱의 구조 및 중요성

  • 최신 컴퓨터는 성능 향상을 위해 여러 계층의 캐싱(L0-L4)을 사용합니다.
  • 캐시 레벨이 높아질수록(예: L0 -> L4) 시간이 더 많이 걸립니다.
  • 로컬 캐시에 대한 쓰기는 다른 코어에 즉시 표시되지 않을 수 있습니다.
  • 각 캐시 레벨은 다음 레벨의 블록을 저장합니다.
  • 블록 위치와 크기는 레벨마다 다를 수 있습니다.
  • 읽기는 원하는 데이터를 가진 첫 번째 레벨에서 가져옵니다.
  • 쓰기는 결국 모든 레벨에 전파됩니다.

쓰기 전파 (Write Propagation)

쓰기 전파 문제

  • 쓰기가 모든 레벨에 전파되는 것은 시간이 걸립니다.
  • 로컬 캐시에 쓰인 데이터는 다른 코어에서 보이지 않을 수 있습니다.
  • 예를 들어, 레지스터는 특정 코어에서만 볼 수 있습니다.
  • 쓰기가 모든 캐시 수준에 즉시 전파되지 않아 동시 읽기 시 불일치가 발생할 수 있습니다.
  • 이를 해결하기 위해 메모리 배리어가 필요합니다.

쓰기 전파 시나리오

  1. 코어 C0가 메모리 위치 m에 쓰기 작업을 실행.
  2. 쓰기 작업이 C0의 L1 캐시에 저장됨.
  3. 코어 C1이 메모리 위치 m을 읽기 작업을 실행.
  4. m이 C1의 L1 또는 L2에 없음.
  5. C1이 공유 L3에서 m을 읽음.
  6. C0의 L1이 m을 C0의 L2로 전파.
  7. C0의 L2가 m을 공유 L3로 전파.

메모리 장벽 (Memory Barriers)

메모리 장벽 예시

  • mfence (x86-64)
  • dmb (ARM)
  • 메모리 장벽은 코어 전체에 대한 쓰기 가시성을 보장합니다.

메모리 장벽 개념

  • 메모리 장벽은 메모리의 가시성을 보장하기 위한 하드웨어 기능입니다.
  • 메모리 장벽은 다음을 수행할 수 있습니다:
    • 현재 코어를 블록하여 모든 코어에서 쓰기를 볼 수 있도록 함.
    • 모든 쓰기를 볼 수 있을 때까지 현재 코어를 블록.
    • 모든 코어가 쓰기를 볼 수 있을 때까지 특정 위치에 접근하는 것을 블록.
    • CPU 명령어 재정렬이 이 명령어에 영향을 미치지 않도록 방지.

메모리 장벽을 사용한 쓰기 전파

  1. 코어 C0가 메모리 위치 m에 쓰기 작업을 실행.
  2. 쓰기 작업이 C0의 L1 캐시에 저장됨.
  3. 코어 C1이 m에 대한 모든 쓰기를 위한 장벽을 설정.
  4. 코어 C1이 m에 대한 읽기 작업을 실행.
  5. C1이 m을 읽기 전에 블록됨.
  6. C0의 L1이 m을 C0의 L2로 전파.
  7. C0의 L2가 m을 공유 L3로 전파.
  8. C1이 공유 L3에서 m을 읽음.

동기화와 메모리 장벽

  • 많은 POSIX 동기화 함수(예: fork(), pthread_mutex_lock(), pthread_mutex_unlock())는 올바른 작동을 보장하기 위해 메모리 장벽을 사용합니다.

  • 동기화 원시 기능은 메모리 장벽을 사용합니다.

  • 예를 들어, 다음 함수들은 모두 장벽을 포함합니다:

    • fork()
    • pthread_mutex_lock()
    • pthread_mutex_unlock()
    • pthread_create()
    • pthread_join()
    • 기본적으로 모든 POSIX 동기화 함수.

공유 메모리의 여러 방법

암시적으로 공유되는 메모리

  • 프로세스에는 많은 암시적으로 공유되는 메모리가 있습니다:

    • 공유 라이브러리
    • 실행 파일 이미지
    • 커널 메모리
  • 이 메모리는 명확하게 공유되지 않으며, 읽기 전용이거나 숨겨져 있습니다.

  • 대부분 읽기 전용입니다.

명시적으로 공유되는 메모리

  • 프로세스는 명시적으로 메모리 공유를 요청할 수 있습니다.
  • 이 메모리는 변경 가능하며, 변경 사항은 프로세스 간에 볼 수 있습니다.
  • 커널은 공유 메모리를 설정합니다.
  • POSIX 시스템은 명시적 메모리 공유를 위한 두 가지 기본 시스템 호출과 세 가지 방법을 제공합니다:
    • mmap(): 파일을 메모리에 매핑하고, 파일에 대한 변경 사항을 프로세스 간에 공유.
    • mmap()을 사용하여 부모와 자식 프로세스 간에 익명 공유 매핑 생성.
    • shm_open(): 이름이 지정된 공유 메모리 영역을 엽니다.

mmap() 시스템 호출

mmap() 개요

  • mmap()은 메모리 매핑 도구의 다용도 도구입니다.
  • 커널에 프로세스 가상 메모리 맵을 조작하도록 요청합니다.
  • 파일을 메모리에 매핑하거나, 익명 메모리 매핑을 생성할 수 있습니다.
  • 대응하는 함수는 munmap()입니다.
  • 올바르게 사용하는 것은 매우 복잡할 수 있습니다.

mmap() 사용 예시

#include <sys/mman.h>

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
  • 필수 인수는 flagsfd입니다.
  • flags 인수는 생성할 매핑 유형을 결정합니다:
    • MAP_PRIVATE 또는 MAP_SHARED 중 하나를 포함해야 합니다.
    • MAP_ANONYMOUS는 파일을 매핑하지 않음을 의미합니다.
  • addr 인수는 매핑을 배치할 가상 메모리 맵의 위치를 지정합니다:
    • 종종 0으로 지정하여 커널이 결정하게 합니다.
  • fd 인수는 다음 중 하나여야 합니다:
    • 열린 파일 디스크립터
    • -1
  • len은 파일의 몇 바이트를 매핑할지를 결정합니다.
  • 파일을 매핑하는 경우, offset은 매핑할 파일의 첫 번째 바이트를 결정합니다.

mmap() 예시

#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("example.txt", O_RDONLY);
    if (fd == -1) {
        return 1;
    }

    size_t len = 1024;
    void *mapped = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
    if (mapped == MAP_FAILED) {
        close(fd);
        return 1;
    }

    // Use the mapped memory...

    munmap(mapped, len);
    close(fd);
    return 0;
}

shm_open() 시스템 호출

shm_open

() 개요

  • shm_open()은 이름이 지정된 공유 메모리 영역을 엽니다.
  • 공유 메모리는 프로세스 간에 공유될 수 있으며, 커널에 의해 설정됩니다.

shm_open() 사용 예시

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = shm_open("/my_shared_mem", O_CREAT | O_RDWR, 0666);
    if (fd == -1) {
        return 1;
    }

    size_t len = 1024;
    ftruncate(fd, len); // Set the size of the shared memory

    void *shared_mem = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (shared_mem == MAP_FAILED) {
        close(fd);
        return 1;
    }

    // Use the shared memory...

    munmap(shared_mem, len);
    close(fd);
    shm_unlink("/my_shared_mem"); // Remove the shared memory object
    return 0;
}

요약

  • 캐싱과 CPU 아키텍처는 시간적 동기화 이상의 것이 필요합니다.
  • 메모리 장벽은 데이터 가시성을 보장합니다.
  • 메모리 장벽은 하드웨어 기능입니다.
  • 캐시는 메인 RAM보다 훨씬 빠릅니다.
  • POSIX 동기화 원시 기능은 메모리 장벽을 사용합니다.
  • 공유 메모리는 커널 지원이 필요합니다.
  • 파일은 메모리에 매핑될 수 있습니다.

0개의 댓글