Paging (페이징)이란
주소 공간을 Page(페이지)라는 단위로 쪼개서 사용해
가상 메모리(Virtual Memory)를 관리하는 것이다.

페이징이라는 개념을 제대로 이해하기 위해,
페이징이 왜 등장했는지를 설명하겠다.

아래의 프로그램은 정수 배열을 사용하여 연산을 수행하고, 결과값을 출력하는 C 프로그램이다.

#include <stdio.h>

int main() {
    int a[10] = {0, };

    a[0] = 7;
    a[1] = 8;

    a[9] = a[0] * a[1];

    printf("Result: %d\n", a[9]);
    return 0;
}

프로그램을 실행하면 기댓값인 56을 출력함을 확인할 수 있다.

kidz0@kidgr4m:~/Templates$ ./test
Result: 56

해당 프로그램의 배열 연산 부분을 어셈블리어로 보면 다음과 같다.

   0x40113e <+8>:     mov    DWORD PTR [rip+0x2f18],0x7        # 0x404060 <a>
   0x401148 <+18>:    mov    DWORD PTR [rip+0x2f12],0x8        # 0x404064 <a+4>
   0x401152 <+28>:    mov    edx,DWORD PTR [rip+0x2f08]        # 0x404060 <a>
   0x401158 <+34>:    mov    eax,DWORD PTR [rip+0x2f06]        # 0x404064 <a+4>
   0x40115e <+40>:    imul   eax,edx
   0x401161 <+43>:    mov    DWORD PTR [rip+0x2f1d],eax        # 0x404084 <a+36>

어셈블리를 보면 알 수 있듯이, 프로그램은 배열이 연속적인 메모리 공간일 것이라고 기대한다.

허나, 배열에 해당하는 가상 메모리가 연속이 아니라면, 해당 연산에 문제가 발생할 것이다.
비단 배열 연산의 사례뿐만 아니라, 코드 영역의 주소만 봐도 그렇다.

0x40113e를 실행한 후 0x401148이 실행되어야 하는데
메모리가 연속적이지 않을 경우, 프로그램이 엉뚱한 주소의 값을 실행하게 된다.
(혹은 실행 권한이 없는 페이지에 접근해 segmentation fault가 발생할 것이다.)

이러한 문제 때문에, 프로세스 (Process)의 메모리 공간은 연속적이어야 한다.
메모리를 연속 할당하면 해결될 문제라고 생각하면 쉽지만,
물리 메모리를 연속적으로 할당하는 것은 다양한 문제를 야기한다.

이 문제는 크게 3가지로 나눌 수 있다:

  • 단편화 (Fragmention)
  • 재배치 (Relocation)
  • 보호 (Protection)

각 문제들을 살펴보기 위해서
지금부터 “메모리를 연속 할당”하는 방식을 사용한다고 가정해보자.

1. 단편화

연속 할당을 요청받을 때, 정작 컴퓨터가 마주하는 메모리는 다음과 같은 모습이다.

[ Partition ][ Hole ][ Partition ][ Hole ][ Partition ]

간략히 용어 설명을 하자면 다음과 같다:

  • Partition: 프로세스의 메모리 공간
  • Hole: 메모리에서 할당 받지 못한 공간. 이곳에 할당이 가능하다.

메모리는 중간 중간에 할당 가능한 공간이 있는데,
연속 할당을 해야 하므로 할당이 어려워지게 된다.
이렇게 남는 공간이 생기며 메모리를 전부 사용하지 못하는 문제를 단편화라고 한다.

2. 재배치

임의의 프로그램에서 코드 영역의 주소가 0x401000부터 시작한다고 가정해보자.
이걸 운영체제로 옮기면 주소가 새로 배치되어야 하는 상황이 발생할 수 있다.
0x401000에 해당하는 메모리 공간이 사용 중일 수 있기 때문이다.

이렇게 되면 프로그램이 가정하는 코드 영역의 주소가 완전히 깨진다.
그래서 운영체제 입장에선
”이 프로그램을 정상 실행하기 위해선 0x401000 주소가 필요하다”는 제약이 생긴다.

3. 보호

연속적으로 할당하게 되면 보안의 측면에서도 치명적인 문제가 생긴다.
연속 할당으로 메모리가 아래와 같이 할당되었다고 가정해보자.

[ A ][ B ][ C ][ ... ]

운영체제는 해당 메모리에 대해 A와 B의 위치를 말해줄 뿐이다.
따라서 CPU는 해당 주소가 A의 영역에 있는지 B의 영역에 있는지 알지 못한다.

이렇게 되면 다음과 같은 문제가 생긴다.
사용자가 A 프로세스에서 0x1337000 주소에 0x8이라는 값을 쓰는다 가정하면,
CPU는 0x13370000이 A의 메모리에 속하는지, B의 메모리에 속하는지를 판단할 수 없다.
이로 인해 A 프로세스에서 B의 메모리에 0x8을 써버리는 일이 발생할 수 있다.

이 세 문제를 동시에 해결하기 위해 등장한 것이 페이징이다.

페이징의 핵심은 메모리를 연속으로 할당하는 것이 아닌,
페이지의 단위로 쪼개서 할당해 사용하는 것이다.

이때, 페이지의 단위는 통상적으로 4 KB (4096 or 0x1000 Bytes)이다.

다시 말해, 프로세스의 가상 주소 공간을 페이지라는 일정 단위로 자르고,
물리 주소 공간 또한 프레임이라는 페이지와 동일한 크기의 단위로 자른 후,
페이지를 프레임에 할당한다.

이렇게 되면 프로세스가 보는 가상 주소 공간은 연속적으로 유지되며,
해당 연속 공간이 페이지 단위로 프레임에 매핑된다.

각 가상 페이지가 어떤 물리 프레임에 매핑되는지는 다음과 같이 표현할 수 있다.

           Start         End
          0x400000  ~  0x401000  -> 물리 주소: 0x78000
          0x401000  ~  0x402000  -> 물리 주소: 0x9a000

즉, 불연속적인 물리 메모리를 가상 주소 변환을 통해 연속적인 주소 공간으로 추상화하는 것이다.

그렇다면 여기서 문제가 생긴다.
운영 체제는 어떻게 가상 주소 0x400000이 물리 주소 0x78000에 할당되었음을 알 수 있을까?

이때 가상 주소와 물리 주소의 대응 관계를 기록하는 것이 Page Table (1-Level Page Table)이다.