테마
가상 메모리와 페이징
가상 메모리(virtual memory)는 프로세스(process)마다 "메모리 전체를 혼자 쓰는 것처럼" 보이는 주소 공간(address space)을 주고, 그 주소를 실제 메모리 위치로 몰래 바꿔 주는 운영체제의 기술이다. 프로그램이 쓰는 주소를 가상 주소(virtual address), 실제 메모리 칩의 주소를 물리 주소(physical address)라고 한다. 오늘날 운영체제는 이 변환을 메모리를 같은 크기 조각으로 나누는 페이징(paging)으로 하고, 조각마다 어디에 있는지를 페이지 테이블(page table)에 적어 둔다. 덕분에 프로세스끼리 서로의 메모리를 볼 수 없고, 연속된 빈 공간이 없어도 흩어진 빈칸을 모아 쓸 수 있다.
먼저 연속 할당과 외부 단편화
가장 단순한 방법은 프로세스 하나에 물리 메모리의 연속된 한 덩어리를 통째로 주는 연속 메모리 할당(contiguous memory allocation)이다. 프로세스가 끝나면 그 자리는 빈 구멍이 되고, 새 프로세스는 자기 크기가 들어가는 구멍을 골라 들어간다. 구멍을 고르는 방법은 세 가지가 있다.
- 최초 적합(first fit)은 앞에서부터 찾다가 처음 만난, 충분히 큰 구멍에 넣는다. 찾는 시간이 짧다.
- 최적 적합(best fit)은 충분히 큰 구멍 가운데 가장 작은 곳에 넣는다. 남는 조각이 작지만, 그 조각이 너무 작아 쓸모없어지기 쉽다.
- 최악 적합(worst fit)은 가장 큰 구멍에 넣는다. 남는 조각이 커야 나중에 다른 프로세스가 그 조각을 쓸 수 있다고 보기 때문이다.
어느 방법이든 시간이 지나면 작은 구멍이 여기저기 흩어진다. 빈 공간을 모두 합치면 충분한데 연속된 한 덩어리가 없어 새 프로세스를 못 올리는 상태를 외부 단편화(external fragmentation)라고 한다. 흩어진 프로세스를 한쪽으로 몰아 빈 공간을 합치는 압축(compaction)으로 풀 수는 있지만, 메모리를 통째로 옮기는 동안 프로세스를 멈춰야 해서 비싸다.
페이징: 같은 크기로 잘라 흩어 놓기
페이징은 연속으로 줘야 한다는 조건 자체를 없앤다. 가상 주소 공간을 같은 크기의 조각인 페이지(page)로 자르고, 물리 메모리도 같은 크기의 칸인 페이지 프레임(page frame)으로 자른다. 페이지 하나는 아무 빈 프레임에나 들어갈 수 있으므로 연속된 큰 구멍이 필요 없고, 외부 단편화가 생기지 않는다. 흔한 페이지 크기는 4KB이며 x86-64 리눅스의 기본값도 4KB다.
대신 마지막 페이지는 꽉 차지 않을 수 있다. 5KB가 필요한 프로세스는 4KB 페이지 두 개를 받고, 두 번째 페이지의 3KB는 아무도 못 쓰고 버려진다. 이렇게 할당 단위 안에서 남는 낭비를 내부 단편화(internal fragmentation)라고 한다. 페이지가 클수록 이 낭비가 커진다. 반대로 너무 작게 만들면 페이지 수가 늘어 페이지 테이블이 커진다.
주소 변환: 페이지 번호와 오프셋
변환은 CPU 안의 하드웨어인 MMU(Memory Management Unit, 메모리 관리 장치)가 메모리에 접근할 때마다 한다. 가상 주소는 두 부분으로 나뉜다. 앞쪽은 몇 번째 페이지인지를 가리키는 페이지 번호(page number)이고, 뒤쪽은 그 페이지 안에서 몇 번째 바이트인지를 가리키는 오프셋(offset)이다. 페이지가 4KB(2의 12제곱 바이트)면 오프셋은 주소를 2진수(binary number)로 썼을 때 오른쪽 끝 12자리, 즉 하위 12비트다.
예를 들어 가상 주소 0x3ABC는 16진수(hexadecimal)로 쓴 값이다. 16진수 한 자리는 2진수 네 자리(4비트)이므로, 하위 12비트는 16진수 끝 세 자리인 0xABC이고 이것이 오프셋이다. 나머지 0x3은 페이지 번호다. 페이지 테이블에서 3번 페이지가 7번 프레임에 있다고 나오면, 물리 주소는 프레임 번호 7 뒤에 오프셋을 그대로 붙인 0x7ABC가 된다. 오프셋은 바뀌지 않고 페이지 번호만 프레임 번호로 바뀐다는 점이 핵심이다.
다이어그램 로딩…
페이지 테이블은 프로세스마다 하나씩 있고, 페이지 번호를 칸 번호로 쓰는 배열(array)처럼 생겼다. 각 칸을 PTE(Page Table Entry, 페이지 테이블 항목)라고 하며, 프레임 번호와 함께 몇 가지 상태 비트를 담는다. 가상 메모리에서는 모든 페이지가 늘 물리 메모리에 있지는 않다. 당장 쓰지 않는 페이지는 디스크에 내보내 두었다가 필요할 때 다시 올리므로, 이런 상태를 비트로 표시해 둔다.
| 비트 | 뜻 | 쓰이는 곳 |
|---|---|---|
| 유효(valid) | 이 페이지가 지금 물리 메모리에 있는가 | 꺼져 있으면 운영체제가 개입 |
| 보호(protection) | 읽기·쓰기·실행 허용 여부 | 코드 영역 쓰기 금지 |
| 참조(reference) | 최근에 읽거나 쓴 적이 있는가 | 교체할 페이지 고르기 |
| 수정(dirty) | 메모리에 올라온 뒤 내용이 바뀌었는가 | 내보낼 때 디스크에 다시 쓸지 결정 |
유효 비트(valid bit)가 꺼진 페이지에 접근하면 MMU는 변환을 멈추고 커널(kernel)에 알린다. 이것이 페이지 폴트(page fault)이며, 처리 과정은 요구 페이징과 페이지 교체에서 다룬다. 보호 비트를 어기는 접근도 커널이 잡는다. 대개는 그 프로세스를 강제 종료하지만, 쓰기 시 복사(copy-on-write)처럼 커널이 일부러 쓰기를 막아 둔 경우에는 그때 복사본을 만들어 준다.
이어지는 내용은 가상 메모리와 페이징 2에서 다룬다.
면접 포인트
- "가상 메모리를 왜 쓰나요?" → 프로세스 간 격리, 연속 메모리 불필요, 물리 메모리보다 큰 주소 공간 제공.
- "외부 단편화와 내부 단편화의 차이는?" → 외부는 빈 공간이 흩어져 합치면 충분해도 못 쓰는 것, 내부는 할당 단위 안에서 남는 낭비. 연속 할당은 외부 단편화, 페이징은 내부 단편화가 생긴다.
- "주소 변환 과정은?" → 가상 주소를 페이지 번호와 오프셋으로 나누고, 페이지 번호만 페이지 테이블로 프레임 번호로 바꾼다. 오프셋은 그대로 붙인다.
- "PTE에는 무엇이 들어 있나요?" → 프레임 번호와 상태 비트(유효·보호·참조·수정). 유효 비트가 꺼져 있으면 페이지 폴트.
- 페이징이 모든 단편화를 없앤다고 말하면 틀린다. 내부 단편화는 남는다.
