가상 메모리는 램이 모자라서 나왔지만 남은 이유는 따로 있다
Esc

    가상 메모리는 램이 모자라서 나왔지만 남은 이유는 따로 있다

    –OS메모리

    가상 메모리가 무엇이냐고 물으면 이런 답이 자주 나옵니다.

    램이 모자랄 때 저장 장치를 메모리처럼 빌려 쓰는 것

    이 답은 60년 전에는 맞았습니다. 처음에 그것 때문에 만들어진 것이 맞습니다. 그런데 램이 남아도는 지금도 가상 메모리는 그대로 돕니다. 빌려 쓸 저장 장치를 꺼 두어도 돕니다. 동기는 사라졌는데 장치는 남았습니다. 무엇이 남긴 것인지 보려면 처음 만들어진 자리로 가야 합니다.

    프로그램이 메모리보다 컸다

    1960년대 초입니다. 기계에는 성격이 다른 저장 장치가 둘 있었습니다.

    무엇 성질
    코어 메모리 빠릅니다. 비싸서 조금밖에 못 답니다
    드럼 큽니다. 느립니다

    당시 가장 큰 축에 들던 기계가 코어 16,000워드에 드럼 96,000워드쯤이었습니다. 풀고 싶은 문제는 코어에 안 들어갔습니다. 지금으로 치면 램에 안 들어가는 게임을 돌리는 상황입니다.

    그런데 이 시절에는 그 일을 대신해 줄 것이 아무것도 없었습니다.

    그래서 사람이 조각을 관리했다

    방법은 하나뿐이었습니다. 프로그램을 조각으로 자르고, 지금 필요한 조각만 코어에 올리고, 나머지는 드럼에 두었다가 때가 되면 바꿔 치는 것입니다. 오버레이라고 불렀습니다.

    문제는 이 일을 전부 사람이 했다는 것입니다.

    • 프로그램을 어디서 자를지 정합니다
    • 어느 조각이 어느 조각을 부르는지 따져서 교체표를 만듭니다
    • 교체를 일으키는 호출을 코드 안에 직접 심습니다

    그래서 이런 것들이 따라왔습니다.

    • 논리와 배치가 한 코드에 섞입니다. 무엇을 계산하는지와 무엇이 지금 메모리에 있는지를 같은 자리에서 관리하게 됩니다
    • 기계가 바뀌면 처음부터 다시 짭니다. 조각 크기를 그 기계의 코어 크기에 맞춰 잘라 뒀기 때문입니다
    • 경계를 잘못 자르면 교체만 합니다. 서로를 번갈아 부르는 두 조각을 갈라 놓으면 계속 바꿔 치기만 하고 일이 안 나갑니다

    아틀라스를 만든 사람들은 이 일에 들어가는 시간을 없애면 프로그래머의 생산성이 세 배까지 좋아질 수 있다고 봤습니다. 당시 프로그래밍에서 이 작업이 차지하던 비중이 그 정도였다는 뜻입니다.

    아틀라스가 한 것

    1962년 12월, 맨체스터 대학과 페란티가 만든 아틀라스가 가동합니다. 톰 킬번이 이끈 기계입니다.

    이 기계에는 코어 16,000워드와 드럼 96,000워드가 달려 있었는데, 프로그램은 1,000,000워드짜리 주소를 쓸 수 있었습니다. 없는 크기의 메모리를 있는 것처럼 쓰게 한 것입니다.

    이름이 한 단계 저장소였습니다. 두 단계로 나뉜 저장소를 프로그램에게는 한 단계로 보이게 했다는 뜻입니다.

    방법은 이렇습니다.

    1. 주소 공간을 512워드짜리 블록으로 자릅니다
    2. 프로그램이 주소를 낼 때마다 그 블록이 지금 코어에 있는지 하드웨어가 비교합니다
    3. 있으면 그대로 진행하고, 없으면 인터럽트를 걸어 관리 프로그램이 드럼에서 가져옵니다

    사람이 코드에 적던 교체가 통째로 기계 안으로 들어간 것입니다. 3번이 지금 페이지 폴트라고 부르는 것의 원형입니다.

    바뀐 것은 일의 양이 아니라 누가 그 일을 하느냐입니다. 그러면 얻는 것과 잃는 것이 갈립니다.

    장점
    • 언제 바뀔지 안다. 교체를 코드에 직접 적었으니 어느 줄에서 멈추는지가 눈에 보입니다.
    • 헛것이 안 올라온다. 프로그램의 구조를 아는 사람이 잘랐기 때문입니다.
    단점
    • 논리와 배치가 섞인다. 계산하는 코드와 메모리를 관리하는 코드가 같은 자리에 있습니다.
    • 기계가 바뀌면 다시 짠다. 조각 크기가 그 기계에 묶여 있습니다.
    • 잘못 자르면 교체만 한다. 경계를 잘못 잡으면 일이 안 나갑니다.
    장점
    • 코드에서 교체가 사라진다. 프로그램은 주소를 쓰기만 하고, 없으면 기계가 가져옵니다.
    • 기계가 바뀌어도 돈다. 실제 메모리 크기를 프로그램이 몰라도 됩니다.
    • 딸려 오는 것이 있다. 주소를 번역하는 층이 생기면 다른 것들이 공짜로 따라옵니다.
    단점
    • 언제 멈출지 모른다. 교체가 코드에 안 적혀 있으니 아무 줄에서나 멈출 수 있습니다.
    • 무엇이 올라와 있는지 안 보인다. 느려졌을 때 원인이 코드 안에 없습니다.

    이 둘은 지금도 같이 씁니다. 손으로 하던 일이 통째로 없어진 것이 아니라 덩어리가 커진 채로 위층에 남았습니다.

    무엇을 바꾸나 지금 누가 정하나
    페이지 단위 교체 기계. 커널과 CPU 가 알아서 합니다
    에셋 단위 교체 사람. 게임 엔진의 로드와 언로드가 그것입니다

    그래서 가상 메모리가 무엇인가

    지금까지 나온 것을 한 문장으로 모으면 이렇습니다.

    프로세스마다 가짜 주소 공간을 하나씩 주고, 그 주소를 실제 물리 주소로 바꿔 주는 층

    프로그램이 손에 쥐는 주소는 전부 이 가짜 쪽입니다. 포인터를 찍어 보면 나오는 그 값은 램의 어느 자리도 가리키지 않습니다. CPU 가 번역한 다음에야 메모리에 닿습니다.

    이 구조에 이름이 붙은 것이 여섯입니다.

    용어 뜻
    가상 주소 공간 프로세스가 혼자 다 쓴다고 믿는 주소의 범위
    페이지 가상 주소 공간을 자른 덩어리. 보통 4KB
    프레임 물리 메모리를 같은 크기로 자른 칸
    페이지 테이블 페이지 번호를 넣으면 프레임 번호가 나오는 번역표
    MMU 번역을 실제로 하는 CPU 안의 회로
    페이지 폴트 번역하려는 페이지에 프레임이 안 적혀 있을 때 CPU 가 던지는 예외

    이름만 봐서는 안 잡힙니다. 주소 하나를 실제로 따라가 보겠습니다.

    주소 하나가 값에 닿기까지

    프로그램이 0x2A40 번지를 읽는다고 해 봅시다.

    이 값은 가상 주소입니다. 램의 0x2A40 번지와는 아무 상관이 없습니다. 이 프로세스의 가상 주소 공간 안에서만 뜻이 있는 번호입니다. 실행 코드도, 스택도, 힙도, 열어 둔 파일도 전부 이 주소 공간 위에 놓입니다.

    CPU 가 이 값을 두 토막으로 자릅니다. 4KB 짜리 페이지로 잘라서 관리하니까, 아래 12비트는 페이지 안에서의 자리이고 위쪽은 몇 번째 페이지인지가 됩니다. 16진수로 쓰면 자리가 딱 맞아떨어집니다.

    0x2A40
      └─┬─┘ └┬─┘
        │     └─ 오프셋 0xA40   페이지 안에서 몇 번째 바이트인가
        └─────── 페이지 번호 2   몇 번째 페이지인가

    페이지 번호로 페이지 테이블을 찾습니다. 이 표는 커널이 프로세스마다 하나씩 만들어 둔 것이고, 2번 줄에 “프레임 3” 이라고 적혀 있다면 그 페이지의 내용은 지금 프레임 3 번 칸에 들어 있다는 뜻입니다. 프레임은 물리 메모리를 페이지와 같은 크기로 자른 칸이라, 램 8GB 짜리 기기라면 이 칸이 이백만 개쯤 있습니다.

    이 찾는 일을 하는 것이 MMU 입니다. CPU 안에 들어 있는 회로이고, 프로그램이 주소를 낼 때마다 예외 없이 여기를 지납니다.

    마지막으로 프레임 번호와 오프셋을 다시 붙입니다. 프레임 3 의 시작에서 0xA40 바이트 지점, 그러니까 물리 주소 0x3A40 입니다. CPU 는 그 자리에서 값을 읽습니다.

    정리하면 이렇습니다.

    1. 프로그램이 가상 주소를 냅니다
    2. CPU 가 페이지 번호와 오프셋으로 자릅니다
    3. MMU 가 그 프로세스의 페이지 테이블에서 페이지 번호에 해당하는 줄을 찾아 프레임 번호를 얻습니다
    4. 프레임 번호와 오프셋을 합쳐 물리 주소를 만들고, 그 자리를 직접 읽고 씁니다

    여섯 번째 용어인 페이지 폴트는 이 흐름이 3번에서 걸렸을 때의 이야기입니다. 표의 그 줄에 프레임이 아직 안 적혀 있으면 CPU 가 예외를 던지고, 커널이 프레임을 붙인 뒤 멈췄던 명령부터 다시 실행시킵니다.

    여기서 셋을 짚고 갑니다

    셋 다 반대로 알기 쉬운 것들입니다.

    누가 무엇을 언제
    프로세스 주소를 내밉니다. 자기 표를 보지도 고치지도 못합니다 매 명령
    CPU 표를 읽어 번역하고 직접 읽고 씁니다 메모리에 닿을 때마다
    OS 표를 만들고 고칩니다. 물리 프레임을 배정합니다 할당 요청과 폴트 때만

    하나. 표를 만드는 것은 OS 입니다. 프로세스는 “메모리 좀 주세요” 라고 요청할 뿐이고, 자기 번역표를 건드릴 수 있으면 격리가 무너집니다.

    둘. 번역의 결과는 주소입니다. 크기가 아닙니다. 몇 바이트를 읽을지는 명령어가 이미 정해 뒀습니다. 표에도 크기는 없습니다. 페이지가 전부 같은 크기라서 적을 필요가 없습니다.

    셋. 평소 동작에는 OS 가 없습니다. 표에 제대로 적혀 있는 동안은 CPU 가 번역해서 램에 바로 닿습니다. OS 는 상주하며 심부름하는 것이 아니라 번역이 빗나갔을 때 불려 오는 쪽입니다.

    그리고 메모리를 요청해서 받았다고 그 자리가 물리 메모리에 생긴 것도 아닙니다. OS 가 해 주는 것은 주소 범위를 잡고 표에 줄을 만드는 것까지이고, 그 줄의 프레임 칸은 대개 비어 있습니다. 프레임은 그 페이지를 처음 만질 때 붙습니다.

    번역표가 프로세스마다 따로라는 것

    여기까지 오면 이 구조가 무엇을 막는지가 보입니다. 프로세스 A 와 B 가 똑같이 0x1000 번지를 읽어도, 각자의 표를 거치기 때문에 서로 다른 프레임으로 갑니다.

    프로세스 A 의 주소 공간 프로세스 B 의 주소 공간 A 의 페이지 테이블 B 의 페이지 테이블 물리 메모리 0x1000 을 읽는다 0x1000 을 읽는다 0x1000 → 프레임 3 0x1000 → 프레임 1 프레임 0 프레임 1 프레임 2 프레임 3
    같은 0x1000 인데 번역표가 달라서 서로 다른 자리로 간다.

    A 가 어떤 주소를 찍어도 B 의 자리에는 닿지 않습니다. B 의 프레임을 가리키는 칸이 A 의 번역표에 아예 없기 때문입니다. 남의 메모리를 안 건드리기로 약속한 것이 아니라 가리킬 방법이 없는 것입니다.

    딸려 온 것이 더 컸다

    주소를 번역하는 층을 한 번 넣고 나니, 메모리 용량과 아무 상관 없는 것들이 같이 됐습니다.

    하나. 격리. 방금 본 그림이 그것입니다. 번역표가 프로세스마다 따로라서 남의 자리를 가리킬 방법이 없습니다.

    둘. 재배치. 물리 메모리 어디에 올려도 됩니다. 프로그램은 자기가 몇 번지에 올라갔는지 몰라도 되고, 같은 프로그램을 여러 벌 띄워도 주소가 안 겹칩니다.

    셋. 이어 붙이기. 물리 메모리가 조각조각 흩어져 있어도 주소 공간은 이어져 보입니다. 빈 자리의 총합은 충분한데 이어진 자리가 없어서 못 올리는 일이 없어집니다.

    넷. 공유. 같은 물리 자리를 여러 프로세스의 주소 공간에 동시에 매핑할 수 있습니다. 공용 라이브러리가 메모리에 한 벌만 올라가는 것, 프로세스를 복제할 때 내용을 실제로 복사하지 않고 고칠 때만 복사하는 것이 전부 이 위에서 나옵니다.

    다섯. 보호. 번역표 칸마다 권한을 적어 둘 수 있습니다. 코드가 놓인 페이지는 읽기와 실행만, 데이터 페이지는 쓰기 가능으로 두는 식입니다. 커널 메모리를 사용자 코드가 못 건드리게 막는 것도 여기입니다.

    여러 프로그램을 동시에 올려 두고 돌리는 것이 안전해진 것이 이 다섯 덕분입니다. 용량을 늘리려고 만든 장치가 실제로는 여럿이 한 기계를 나눠 쓰게 만든 장치가 된 것입니다.

    그래서 램이 싸진 뒤에도 남았다

    원래 동기였던 용량 문제는 약해졌습니다. 그런데 위의 다섯은 메모리가 아무리 남아돌아도 필요합니다.

    그래서 지금 이렇습니다.

    • 스왑을 완전히 꺼도 가상 메모리는 돕니다. 반대로 가상 메모리를 끄면 OS 가 아예 못 돕니다
    • 용량 쪽 후손도 형태를 바꿔 남았습니다. 프로그램을 켤 때 실행 파일 전체를 올리지 않고 건드리는 페이지만 올리는 것, 파일을 통째로 주소 공간에 붙여 두고 읽는 것이 전부 아틀라스가 하던 그 일입니다

    처음 질문으로 돌아가면, “램이 모자랄 때 빌려 쓰는 것” 은 가상 메모리가 처음에 팔린 이유였습니다. 지금 안 없어지는 이유는 그 아래에 깔린 주소를 번역하는 층 쪽입니다.

    정리

    • 오버레이는 프로그램을 조각내고 교체 시점을 사람이 코드에 적던 방식입니다. 논리와 메모리 배치가 한자리에 섞이고, 기계가 바뀌면 다시 짜야 했습니다
    • 1962년 아틀라스가 그 교체를 하드웨어와 관리 프로그램에 넘겼습니다. 코어 16,000워드짜리 기계에서 1,000,000워드 주소를 쓰게 한 것이 시작입니다
    • 바뀐 것은 일의 양이 아니라 누가 그 일을 하느냐입니다. 대신 언제 멈출지는 코드에서 안 보이게 됐습니다
    • 그래서 지금의 가상 메모리는 프로세스마다 가짜 주소 공간을 주고 실제 주소로 번역해 주는 층입니다. 가상 주소 공간, 페이지 테이블, MMU 셋으로 굴러가고, 번역표에 없으면 OS 를 부릅니다
    • 요청한다고 프레임이 바로 붙지 않습니다. 표에 줄만 생기고, 그 페이지를 처음 만질 때 커널이 빈 프레임 목록에서 하나 꺼내 붙입니다
    • 주소를 번역하는 층이 생기자 격리, 재배치, 이어 붙이기, 공유, 보호가 딸려 왔습니다. 전부 메모리 용량과 무관한 것들입니다
    • 그래서 램이 싸진 뒤에도 남았습니다. 스왑을 꺼도 가상 메모리는 돌고, 끄면 OS 가 못 돕니다