모든 이야기

쓰기 시 복사: 복제를 미루면 언제 이득이 생길까

같은 데이터를 여러 작업이 읽기만 한다면 처음부터 별도 복사본을 만들 필요가 없을 수 있다. 쓰기 시 복사, Copy-on-Write는 데이터를 공유하다가 실제 변경이 필요해진 순간 복사본을 만드는 방법이다. 복사 비용을 지연하고, 끝까지 변경되지 않은 영역은 한 번의 저장으로 유지한다. 운영체제의 페이지 관리와 프로그램의 자료구조 양쪽에서 만날 수 있다.

부모와 자식이 페이지 P를 공유하다가 자식의 첫 쓰기 후 자식만 복사본 Q를 가리키는 전후 그림

변경 전에는 공유하고 변경이 필요해지면 독립적인 복사본으로 분리한다.

같은 페이지를 함께 읽는다

프로세스 복제를 예로 들면 부모와 자식의 가상 주소 공간을 따로 만들되, 일부 페이지는 같은 물리 프레임에 연결할 수 있다. 공유하는 동안 쓰기를 통제하도록 매핑을 설정한다. 어느 한쪽이 쓰려 하면 운영체제가 그 상황을 감지하고 새 프레임을 준비한다. 가상 주소는 같아 보여도 변경 뒤에는 서로 다른 물리 저장 위치를 가리키게 된다.

Linux의 fork 문서는 프로세스 복제에 쓰기 시 복사 페이지를 사용한다고 설명한다. 따라서 자식 생성 직후 모든 사용자 메모리를 즉시 복사한다고 생각하면 비용 구조를 잘못 이해한다. 다만 프로세스 관리 정보와 페이지 테이블을 준비하는 비용까지 없어지는 것은 아니다.

첫 쓰기만 따라가 보기

부모와 자식이 정수 배열이 든 페이지 P를 공유한다고 하자. 자식이 한 원소를 변경하려 하면 쓰기가 통제된 페이지에서 예외가 발생한다. 운영체제는 자식이 합법적으로 변경할 수 있는지 확인하고, 필요한 경우 P의 내용을 새 페이지 Q에 복사한다. 자식의 매핑을 Q로 바꾼 뒤 명령을 다시 실행하면 부모는 P의 기존 값을, 자식은 Q의 변경된 값을 본다.

복사는 보통 관리 단위 전체에 대해 일어난다. 배열 원소 하나를 바꿨다고 그 바이트만 별도로 복사하는 것은 아니다. 페이지 단위라면 해당 페이지 전체의 내용이 준비될 수 있다. 공유자가 더 없어진 페이지라면 복사 없이 쓰기를 허용할 여지도 있다. 구체적인 판단은 구현과 참조 상태에 따라 달라진다.

읽는 복제와 쓰는 복제의 비용은 다르다

자식이 대부분 읽기만 하거나 곧 다른 프로그램으로 실행 내용을 교체한다면 복제를 생략한 이점이 크다. 반대로 모든 페이지를 빠르게 변경하면 나중에 결국 많은 복사를 하며 첫 쓰기 지연도 발생한다. 복사 시점을 미뤘다는 사실은 총비용이 언제나 줄었다는 보장이 아니다. 초기 응답과 변경 단계의 지연을 따로 살펴야 한다.

자료구조에서도 비슷한 원리를 사용할 수 있다. 여러 독자가 하나의 내용을 공유하다가 수정 요청이 생길 때 독립적인 버전을 만드는 방식이다. 그러나 객체 내부에 가변 참조가 있다면 겉 객체만 복사해도 내부 데이터가 계속 공유될 수 있다. 얕은 복사와 깊은 복사, 실제 변경 단위를 구분해야 독립성의 약속을 지킨다.

쓰기 시 복사는 명시적인 공유 메모리의 의미와도 다르다. 공유 메모리는 한쪽 변경을 다른 쪽에서도 볼 수 있도록 구성할 수 있지만, 쓰기 시 복사는 변경 시 분리해 독립적인 상태를 유지하려는 경우가 많다. 같은 프레임을 공유한다는 초기 상태만 보고 최종 변경 의미까지 같다고 생각하면 안 된다.

이 기법을 평가할 때는 공유한 크기, 실제 변경한 비율, 복사 단위, 첫 쓰기의 지연을 측정한다. 메모리 표시에서도 여러 프로세스의 공유 영역을 단순 합산하면 실제 물리 사용량을 과대평가할 수 있다. 쓰기 시 복사는 복제를 없애는 마법이 아니라, 필요가 확인될 때까지 비용과 저장 공간을 공유하는 설계다.

참고 자료