쓰기 시 복사: 복제를 미루면 언제 이득이 생길까
같은 데이터를 여러 작업이 읽기만 한다면 처음부터 별도 복사본을 만들 필요가 없을 수 있다. 쓰기 시 복사, Copy-on-Write는 데이터를 공유하다가 실제 변경이 필요해진 순간 복사본을 만드는 방법이다. 복사 비용을 지연하고, 끝까지 변경되지 않은 영역은 한 번의 저장으로 유지한다. 운영체제의 페이지 관리와 프로그램의 자료구조 양쪽에서 만날 수 있다.
변경 전에는 공유하고 변경이 필요해지면 독립적인 복사본으로 분리한다.
같은 페이지를 함께 읽는다
프로세스 복제를 예로 들면 부모와 자식의 가상 주소 공간을 따로 만들되, 일부 페이지는 같은 물리 프레임에 연결할 수 있다. 공유하는 동안 쓰기를 통제하도록 매핑을 설정한다. 어느 한쪽이 쓰려 하면 운영체제가 그 상황을 감지하고 새 프레임을 준비한다. 가상 주소는 같아 보여도 변경 뒤에는 서로 다른 물리 저장 위치를 가리키게 된다.
Linux의 fork 문서는 프로세스 복제에 쓰기 시 복사 페이지를 사용한다고 설명한다. 따라서 자식 생성 직후 모든 사용자 메모리를 즉시 복사한다고 생각하면 비용 구조를 잘못 이해한다. 다만 프로세스 관리 정보와 페이지 테이블을 준비하는 비용까지 없어지는 것은 아니다.
첫 쓰기만 따라가 보기
부모와 자식이 정수 배열이 든 페이지 P를 공유한다고 하자. 자식이 한 원소를 변경하려 하면 쓰기가 통제된 페이지에서 예외가 발생한다. 운영체제는 자식이 합법적으로 변경할 수 있는지 확인하고, 필요한 경우 P의 내용을 새 페이지 Q에 복사한다. 자식의 매핑을 Q로 바꾼 뒤 명령을 다시 실행하면 부모는 P의 기존 값을, 자식은 Q의 변경된 값을 본다.
복사는 보통 관리 단위 전체에 대해 일어난다. 배열 원소 하나를 바꿨다고 그 바이트만 별도로 복사하는 것은 아니다. 페이지 단위라면 해당 페이지 전체의 내용이 준비될 수 있다. 공유자가 더 없어진 페이지라면 복사 없이 쓰기를 허용할 여지도 있다. 구체적인 판단은 구현과 참조 상태에 따라 달라진다.
읽는 복제와 쓰는 복제의 비용은 다르다
자식이 대부분 읽기만 하거나 곧 다른 프로그램으로 실행 내용을 교체한다면 복제를 생략한 이점이 크다. 반대로 모든 페이지를 빠르게 변경하면 나중에 결국 많은 복사를 하며 첫 쓰기 지연도 발생한다. 복사 시점을 미뤘다는 사실은 총비용이 언제나 줄었다는 보장이 아니다. 초기 응답과 변경 단계의 지연을 따로 살펴야 한다.
자료구조에서도 비슷한 원리를 사용할 수 있다. 여러 독자가 하나의 내용을 공유하다가 수정 요청이 생길 때 독립적인 버전을 만드는 방식이다. 그러나 객체 내부에 가변 참조가 있다면 겉 객체만 복사해도 내부 데이터가 계속 공유될 수 있다. 얕은 복사와 깊은 복사, 실제 변경 단위를 구분해야 독립성의 약속을 지킨다.
쓰기 시 복사는 명시적인 공유 메모리의 의미와도 다르다. 공유 메모리는 한쪽 변경을 다른 쪽에서도 볼 수 있도록 구성할 수 있지만, 쓰기 시 복사는 변경 시 분리해 독립적인 상태를 유지하려는 경우가 많다. 같은 프레임을 공유한다는 초기 상태만 보고 최종 변경 의미까지 같다고 생각하면 안 된다.
이 기법을 평가할 때는 공유한 크기, 실제 변경한 비율, 복사 단위, 첫 쓰기의 지연을 측정한다. 메모리 표시에서도 여러 프로세스의 공유 영역을 단순 합산하면 실제 물리 사용량을 과대평가할 수 있다. 쓰기 시 복사는 복제를 없애는 마법이 아니라, 필요가 확인될 때까지 비용과 저장 공간을 공유하는 설계다.