← Posts

OS · Virtual Memory · Isolation

왜 내 메모리를 남이 못 건드리나 - 가상 메모리

두 프로그램이 똑같은 메모리 주소를 써도 서로 안 부딪힌다. 각 프로세스가 보는 주소는 가짜고, OS가 진짜 물리 메모리로 몰래 바꿔치기하기 때문이다. 이 한 겹이 격리를 공짜로 만든다.

목차
  1. 두 프로그램이 같은 주소를 써도 안 부딪힌다
  2. 가짜 주소를 진짜로 바꿔친다
  3. 그래서 격리가 공짜로 온다
  4. 메모리를 페이지로 자른다
  5. 물리 메모리보다 많이 쓴다
  6. 바꿔치기에도 값이 있다
  7. 컨테이너 격리의 밑에도 이게 있다
  8. 실무에서: 메모리 부족은 이렇게 나타난다
  9. 정리

앞 글에서 프로세스마다 “자기만의 메모리”를 받는다고 했다. 그런데 컴퓨터의 물리 메모리는 하나뿐이다. 어떻게 저마다 자기 것을 갖는가. 그 마술이 가상 메모리다.

두 프로그램이 같은 주소를 써도 안 부딪힌다

프로그램 A가 주소 0x1000에 값을 쓰고, 프로그램 B도 0x1000에 값을 쓴다. 물리 메모리가 하나라면 둘은 같은 칸을 덮어써 서로를 망가뜨려야 한다. 그런데 안 그런다. 둘 다 0x1000을 쓰는데 서로 아무 영향이 없다.

비밀은 이거다 - 프로세스가 보는 0x1000진짜 주소가 아니다.

가짜 주소를 진짜로 바꿔친다

프로세스가 다루는 주소는 전부 가상 주소(virtual address) 다. 실제 물리 메모리의 주소가 아니라, “나는 이 주소를 쓴다”고 믿는 가짜 번호다. 프로세스가 메모리에 접근할 때마다, 하드웨어와 OS가 이 가상 주소를 진짜 물리 주소로 몰래 바꿔친다.

diagramdiagram

A의 0x1000은 물리 칸 47번으로, B의 0x1000은 88번으로 이어진다. 같은 가상 주소지만 다른 물리 칸을 가리키니, 둘은 절대 안 부딪힌다. 이 “가상 → 물리” 대응표를 프로세스마다 따로 두고, OS가 그 표를 관리한다.

그래서 격리가 공짜로 온다

여기서 중요한 결과가 나온다. 프로세스 A는 자기 대응표에 적힌 물리 칸에만 닿을 수 있다. B의 물리 칸으로 가는 길이 A의 표엔 아예 없다. 그러니 A가 아무리 이상한 주소를 써도 B의 메모리엔 손댈 수 없다.

앞 글에서 말한 “프로세스는 서로의 메모리를 못 본다”가 바로 이것이다. 격리를 위해 뭔가를 따로 하는 게 아니라, 주소를 바꿔치는 구조 자체가 격리를 만든다. 남의 메모리는 내 지도에 없어서 갈 수가 없다.

메모리를 페이지로 자른다

주소 하나하나마다 대응을 적으면 표가 천문학적으로 커진다. 그래서 메모리를 페이지(page) 라는 일정한 크기(흔히 4KB) 덩어리로 잘라, 페이지 단위로 대응을 관리한다.

plaintext
가상 페이지 0  →  물리 프레임 47
가상 페이지 1  →  물리 프레임 88
가상 페이지 2  →  물리 프레임 12

주소 하나가 아니라 페이지 한 덩어리씩 매핑하니 표가 감당할 크기가 된다. “물리 프레임”은 물리 메모리를 같은 크기로 자른 칸이다. 가상 페이지와 물리 프레임을 짝지어 두는 게 대응표의 정체다.

물리 메모리보다 많이 쓴다

가상이라는 한 겹이 또 하나를 가능케 한다. 모든 페이지가 물리 메모리에 항상 올라와 있을 필요가 없다. 지금 안 쓰는 페이지는 디스크로 밀어두고, 필요해질 때 물리 메모리로 불러온다.

그래서 프로세스는 물리 메모리보다 주소공간을 가진 것처럼 굴 수 있다. 대신 디스크로 밀린 페이지를 건드리면 그걸 다시 불러오느라 느려진다 - 물리 메모리가 부족해 이 왕복이 잦아지면 시스템이 급격히 굼떠진다. “메모리가 모자라면 느려진다”의 밑바닥이 여기다.

바꿔치기에도 값이 있다

가상 주소를 물리로 바꾸는 일은 메모리 접근마다 일어난다. 매번 대응표를 뒤지면 느리니, 하드웨어가 최근에 바꾼 대응을 작은 캐시에 넣어 둔다. 대부분은 이 캐시에서 즉시 해결돼 비용이 거의 없다.

여기선 “공짜는 아니지만 대개 아주 싸다”만 알면 된다. 가상 메모리는 격리와 유연함을 주면서, 그 대가인 변환 비용은 하드웨어가 거의 흡수한다.

컨테이너 격리의 밑에도 이게 있다

컨테이너가 프로세스를 격리한다지만, 메모리 격리만큼은 컨테이너가 새로 하는 게 아니다. 그건 가상 메모리가 이미 프로세스마다 공짜로 준다. 컨테이너 안 프로세스든 밖 프로세스든, 서로의 메모리엔 원래 못 닿는다.

컨테이너가 더하는 건 앞 글에서 본 그 나머지 - 프로세스 목록·파일·네트워크처럼 가상 메모리가 안 가려주는 부분이다. 밑에서 메모리는 프로세스가, 그 위 나머지는 컨테이너가 격리한다. 층이 다르다.

실무에서: 메모리 부족은 이렇게 나타난다

  • OOM(메모리 부족) - 프로세스가 요구하는 페이지를 물리 메모리로 다 못 올리면, OS가 프로세스를 강제로 죽이기도 한다. “잘 돌던 앱이 갑자기 죽었다”의 흔한 원인.
  • 메모리 지표는 물리 기준으로 본다 - 프로세스가 실제로 물리 메모리에 올려 쓰는 양이 압박의 척도다. 가상 주소공간 크기가 아니라 이 물리 사용량을 본다.
  • 컨테이너엔 메모리 상한을 건다 - 한 컨테이너가 물리 메모리를 독차지해 옆을 굶기지 않도록, 프로세스가 올릴 수 있는 물리 메모리에 상한을 준다. 넘으면 그 프로세스가 죽는다.

정리

  • 프로세스가 보는 주소는 전부 가상 주소 - 하드웨어가 매 접근마다 진짜 물리 주소로 바꿔친다(OS는 그 대응표를 놓는다).
  • 프로세스마다 대응표가 따로라, 같은 가상 주소도 다른 물리 칸을 가리킨다 - 바꿔치는 구조 자체가 격리를 만든다(남의 메모리는 내 지도에 없다).
  • 메모리는 페이지로 잘라 관리하고, 안 쓰는 페이지는 디스크로 밀어 물리보다 큰 주소공간을 흉내 낸다(대신 왕복이 잦으면 느려진다).
  • 메모리 격리는 가상 메모리가 공짜로 주고, 컨테이너는 그 위의 나머지를 가린다.

다음 글은 그 주소공간 안에서 지역변수와 new가 각각 어디에 사는지 - 스택과 힙으로 간다.