Copy-on-Write (CoW)와 fork()의 메모리 최적화

1,589 단어·4 분·원문(.md)

fork()의 딜레마 #

리눅스 유닉스 환경에서 새로운 프로세스를 생성할 때 사용하는 시스템콜이 fork()이다.

원칙적으로 fork가 호출되면 부모 프로세스의 모든 메모리 공간(코드, 데이터, 힙, 스택)을 자식 프로세스에게 그대로 복사해야한다.

과거에 fork()는 해당 명령어를 실행하면 부모 프로세스의 모든 메모리와 똑같은 양의 물리 메모리를 할당받고 모든 데이터를 복사하는 방식으로 구현되었다.

만약 부모 프로세스가 2GB의 메모리를 사용한다면 자식 프로세스를 생성하는 순간에 물리 메모리 2gb를 추가로 할당받아 모든 데이터를 복사해야한다. 이는 막대한 메모리 낭비와 컨텍스트 스위칭 지연을 발생시킨다.

Copy-on-Write CoW #

os는 위의 문제를 해결하기 위하여 Write 작업이 발생할 때만 Copy를 수행한다는 CoW 기법을 도입했다.

초기 공유 (공간 절약): fork가 호출되면 os는 물리 메모리를 복사하지 않고 대신 자식 프로세스의 페이지 테이블 page table만 생성하고 부모와 자식이 동일한 물리적 메모리 페이지를 가리키게 만든다.

권한 변경: 이때 공유되는 모든 메모리 페이지의 접근 권한을 read-only으로 변경한다. 양쪽 프로세스가 데이터를 읽기만 한다면, 이 상태로 물리 메모리를 계속 공유해야한다.

페이지 폴트와 복사 (쓰기 발생 시): 부모나 자식 중 한 프로세스가 특정 메모리 영역에 데이터를 write하려고 시도한다.

  • 페이지가 읽기 전용으로 설정되어 있으므로 cpu(mmu)에서 protection fault가 발생하여 os 커널 모드로 진입한다.
  • OS는 이 폴트가 CoW로 인한것을 확인하고 해당 특정 페이지(일반적으로 4KB)에 대해서만 새로운 물리 메모리를 할당하여 데이터를 복사한다.
  • 이후 쓰기를 시도한 프로세스의 페이지 테이블을 새 물리 페이지로 매핑하고, 쓰기 권한을 부여한 뒤 프로세스를 재개한다.

exec() 시스템 콜과 시너지 #

대부분의 경우 프로세스는 fork() 직후 exec() 시스템 콜을 호출하여 자신의 메모리 공간을 완전히 새로운 프로그램 코드로 덮어씌운다.

만약 cow가 없었다면 fork() 시점에 2GB를 전부 복사해놔야했는데, 1밀리초 뒤에 exec()가 호출되어 기껏 복사한 2GB를 전부 버리고 새 데이터를 로드해야하는 극심한 자원 낭비가 발생한다.

CoW 덕분에 실질적으로 변경되는 아주 적은 양의 페이지(주로 스택 변수)만 복사하여 프로세스를 초고속으로 생성할 수 있다.

exec() 호출하면 2GB가 왜 버려지지 #

exec()의 본질을 이해하면 왜 2gb가 버려지는지 cow가 필요한지 명확하게 알 수 있을거같아서 더 적어본다.

exec()프로세스 뇌내망상 덮어쓰기이다.

해당 시스템콜의 역할은 단 하나, **현재 프로세스의 기존 메모리 공간(코드, 데이터, 힙, 스택)**을 싹 다 비우고 전혀 새로운 프로그램의 소스 코드를 디스크에서 읽어와 그 자리에 덮어 쓰는것이다.

프로세스 껍대기 PID등은 유지하되 알맹이 (프로그램)를 완전히 교체해 버리는 것이다.

CoW 가 없을때 fork() -> exec() #

부모 프로세스가 2GB의 물리 메모리를 사용중이고 자식 프로세스를 만들어 ls 명령어 (새 프로그램)을 실행하려는 상황을 가정해보겠다.

  1. fork()로 무식한 복사: os는 자식 프로세스를 위해 물리 메모리 2gb를 추가로 할당하고, 부모의 메모리를 1비트도 빠짐없이 자식에게 복사한다. 이 과정에서 엄청난 cpu 리소스가 낭비되고 메모리 대역폭이 소모된다.
  2. 새 프로그램 로드 (exec): 자식 프로세스가 ls 명령어를 실행하기 위해 exec("ls")를 호출한다.
  3. 2GB 폐기: exec는 방금 전 힘겹게 복사해온 2GB 데이터를 어차피 안 쓸 데이터라고 간주해서 모조리 메모리 해제 처리해버린다 그리고 고작 mb남짓한 ls 프로그램 코드를 메모리에 새로 올린다.

결국 1밀리초 뒤에 지워버릴 2gb를 복사하느라 시스템 전체에 랙이 걸려버리는 낭비가 발생한것이다.

그래서 이 fork()가 그대로 복사해버리는 동작이라서 그런건데, 초창기에는 fork()가 이런식으로 동작했고 지금은 읽기할때는 계속 공유하고 write를 시도하려하는 순간 CoW로 서로 독립적인 공간으로 만들기 위해 페이지 한칸만 물리 메모리로 떼어내서 복사해준다.

Operating-System/cow.md