이 포스팅은 운영체제 정리 시리즈 20 편 중 14 번째 글 입니다.

  • Part 1 - 01: 운영체제란 무엇인가?
  • Part 2 - 02: 운영체제의 역사
  • Part 3 - 03: 이중모드와 보호
  • Part 4 - 04: 운영체제 서비스
  • Part 5 - 05: 프로세스와 관리
  • Part 6 - 06: CPU 스케쥴링
  • Part 7 - 07: 쓰레드(Thread)
  • Part 8 - 08: 프로세스 동기화 Part 1
  • Part 9 - 09: 프로세스 동기화 Part 2
  • Part 10 - 10: 프로세스 동기화 Part 3
  • Part 11 - 11: 프로세스 동기화 Part 4
  • Part 12 - 12: 프로세스 동기화 Part 5
  • Part 13 - 13: 주기억 장치 관리
  • Part 14 - This Post
  • Part 15 - 15: 세그멘테이션 (Segmentation)
  • Part 16 - 16: 가상 메모리 (Virtual Memory)
  • Part 17 - 17: 페이지 교체 알고리즘
  • Part 18 - 18: 프레임 할당 (Allocation of frame)
  • Part 19 - 19: 파일 할당 (Allocation of file)
  • Part 20 - 20: 디스크 스케쥴링 알고리즘
▼ 목록 보기

외부 단편화를 해결하는 방법인 페이징(Paging)에 대해 알아본다.

1. 페이징(Paging)

외부 단편화로 인한 메모리 낭비는 매우 심하다는 것을 살펴보았다. Compaction을 사용하면 외부 단편화는 해결할 수 있지만, 그로 인해 발생하는 오버헤드와 비효율적인 성능으로 사용하기는 어렵다. 그 이후에 연구를 통해 나온 것이 페이징이다. 페이징은 hole을 가지고 해결하려 한 것이 아니라 프로세스를 작은 크기로 나눠서 외부 단편화를 해결하려고 하였다.

페이징은 프로세스를 일정한 작은 크기로 나누는데, 프로세스뿐아니라 hole도 같은 크기로 나눈다. 이러한 작은 조각들의 크기를 맞춰서 메모리에 할당한다. 하지만, 하나의 프로세스는 연속적인 동작을 수행하는데 이를 작은 조각으로 나누어서 여기저기 흩어진다면 프로세스가 정상적으로 동작할까?

메모리상에 여러 곳에 흩어진 프로세스를 수행하기 위해 CPU를 속여야한다. 이전 다중프로그래밍을 살펴봤을 때 MMU를 통해 논리 주소와 물리 주소를 나눠서 사용한다고 했었다. 이 역시 CPU를 속이는 행동이다. 실제 메모리는 전혀 연속적이지 않는데, CPU는 연속적으로 사용하고 있다는 것을 보장받으며 정상적으로 수행한다.

페이징으로 작은 크기로 나눈 것도 위와 같은 방법으로 할 수 있다. 만약 50byte 크기의 프로세스가 있다고 하자. 페이징의 크기는 각 10byte로 나눈다.

image

위 그림과 같이 프로세스 P1은 5개의 페이지로 나눌 수 있다. 이를 메인 메모리 5곳에 나눠서 할당하였다. CPU는 논리 주소로 프로그램이 설정한대로 연속적인 주소값으로 명령을 내리고 이는 메모리로 가기전에 각 페이지의 실제 메모리 주소가 저장되어 있는 테이블에서 물리 주소로 변경되어야 한다.

프로세스를 나눈 조각을 page 라 하고, 메모리를 나눈 조각을 frame 이라 한다. 프로세스는 페이지의 집합이고, 메모리는 프레임의 집합이다. 프로세스를 정상적으로 사용하기 위해 MMU의 재배치 레지스터를 여러개 사용해서 위의 그림과 같이 각 페이지의 실제 주소로 변경해준다. 이러한 여러 개의 재배치 레지스터를 페이지 테이블(Page Table) 이라 한다.

1.1. 주소 변환(Address Translation)

페이징 기법을 사용하기 위해서는 여러 개로 흩어진 페이지에 CPU가 접근하기 위해서 페이지 테이블을 통해 주소를 변환해야 한다.

1.1.1 논리 주소(Logical address)

image

CPU가 내는 주소는 2진수로 표현된다. 이 주소가 m비트로 표현된다고 가정하자. 여기서 하위 n비트는 오프셋(offset) 또는 변위(displacement)라고 한다. 그리고 상위 m-n 비트는 페이지의 번호에 해당한다.(n = d, m-n = p)

논리주소를 물리주소(physical address)로 변환하기 위해서 페이지 번호(p)는 페이지 테이블의 인덱스 값이고, p에 해당되는 테이블 내용은 메모리의 프레임 번호이다. 변위(d)는 변하지 않는 값이다. d는 페이지 크기에 따라 달라진다. 만약 현재 페이지 크기를 16byte이라고 한다면, 이는 2^4이므로 d = 4 이다.

만약 논리주소가 50번째로 주어진다면, 50=110010 이고, page size가 16byte라 주어졌을 때, 4자리를 제외한 p=11이고 d=0010 이다.

1.1.2 동작 예시

image

위 그림을 보면, 어떻게 동작하는 지 한눈에 확인 할 수 있다. 우리가 해야할 일은, 논리 주고가 들어왔을 때, 이것을 page, displacement로 나누고, page 변수를 frame 변수로 바꿔주는 page table을 통과하여 나온 값을 가지고 물리 주소를 찾으면 된다.

image

image

위 그림으로 부터 따라해 볼 수 있다. p와 d만 잘 생각하면 쉬운 아이디어임을 알 수 있다. 이 페이징으로부터 연속 메모리 할당을 하면서 외부 단편화가 발생하는 문제는 해결했다. 하지만..

1.2 내부단편화(Internal Fragment)

페이징은 외부 단편화가 아닌 내부 단편화가 발생한다. 내부단편화는 프로세스 크기가 페이지 크기의 배수가 아닐 경우, 마지막 페이지는 한 프레임을 다 채울 수 없다. 이로 인해 발생하는 공간은 결국 메모리 낭비로 이어진다.

예를 들어, 15bytes 크기의 프로세스 P가 있다. 페이지 크기(프레임 크기)를 4bytes라 하면, P를 페이지로 나눈 결과인 4, 4, 4, 3 의 크기로 총 4개의 페이지가 만들어진다. 여기서 마지막 3bytes 페이지는 프레임 크기보다 1byte작으므로, 이 만큼 메모리 공간이 비게 된다. 이렇게 비어진 공간은 프로세스 P에서도 쓰지 않고, 다른 프로세스에서도 쓰지 못하는 낭비되는 공간이 된다.

내부단편화는 해결할 방법이 없다. 하지만 내부단편화는 외부단편화에 비해 낭비되는 메모리 공간은 매우 적다. 내부단편화의 최대 낭비되는 크기는 page size - 1 이 된다.(외부 단편화는 최대 전체 메모리의 1/3이 낭비된다고 이전에 살펴봤다.) 이는 무시할 정도로 작은 크기이다.

1.3 페이지 테이블 만들기

페이지 테이블을 만드는 방법은 여러 가지가 있다. 먼저, CPU 내부에 페이지 테이블을 만들 수 있다. CPU 내부의 기억장치는 레지스터로, 여러 개의 레지스터로 페이지 테이블을 만드는 것이다. CPU 내부에 페이지 테이블을 만들면, 장점은 주소 변환 속도가 빠르다. 하지만 단점은 CPU 내부에 사용할 수 있는 레지스터는 한정되어 있으므로 페이지 테이블의 크기가 매우 제한된다.

  • CPU
    • 장점 : 주소 변환 속도가 빠르다.
    • 단점 : CPU 내부에 사용할 수 있는 레지스터는 한정되어 테이블의 크기가 제한된다.

반대로, 페이지 테이블을 메모리 내부에서 만들 수도 있다. 메모리 내부에 만드는 것의 장단점은 CPU와 정 반대이다. 즉, 장점은 페이지 테이블의 크기에 제한이 없는 것이고, 단점은 주소 변환 속도가 느리다는 것이다. CPU는 프로세스의 주소에 접근하기 위해서 메모리에 위치한 페이지 테이블에 한 번, 실제 주소로 접근하는데 한 번해서 메모리에 총 2번 접근해야하므로 속도 역시 2배로 느려진다.

  • Memory
    • 장점 : 페이지 테이블의 크기에 제한이 없다.
    • 단점 : 주소 변환 속도가 느리다.

1.3.1 TLB(Translation Look-aside Buffer)

페이지 테이블을 CPU에 만들 때나 메모리에 만들 때 둘 다 장단점이 확실하기 때문에, 이를 해결하기 위해 페이지 테이블도 캐시로 만들어 해결하였다. 페이지 테이블을 별도의 칩(SRAM)으로 만들어서 CPU와 메모리 사이에 위치시키는 것이다. 이러한 테이블을 TLB(Translation Look-aside Buffer) 라고 부른다. 이는 CPU보다 변환 속도는 느리고 메모리보다 테이블 크기는 작지만, CPU보다 테이블 크기가 크고 메모리보다 변환 속도가 빠르다.

TLB는 캐시와 역할이 동일하므로, 실제 전체 페이지 테이블은 메모리에 위치해 있고 테이블의 일부를 TLB에 가져와서 사용한다. 그러므로 TLB에 유효한 페이지가 있을 때와 없을 때의 속도 차이가 발생한다.

1.3.2 TLB의 효율

그렇다면, TLB의 효율을 알아보기 위해 Effective Memory Access Time을 계산해보자.

용어 정의
Tm 메모리를 읽는 시간 100ns
Tb TLB를 읽는 시간 20ns
hit ratio TLB에 유효한 페이지 엔트리가 있을 확률 80%

먼저, EMAT의 정형화된 식을 보자. 가중 평균의 식이다. h는 hit ratio이다.

\[EMAT = h(Tb + Tm) + (1 - h)(Tb + Tm + Tm)\]

실제 유효한 메모리에 접근하는 시간은 위와 같다. 없을 경우 2번 읽어야 하여 Tm을 두번 더해주었다. TLB에 유효한 페이지가 있다면 TLB를 읽는 시간과 실제 메모리를 읽는 시간만 있으면 된다. 하지만, TLB에 유효한 페이지가 없다면 이를 다시 메모리에서 가져와야 하므로 메모리를 총 2번 읽어야 한다.

예제를 계산해보면, 0.8 * (20 + 100) + 0.2 * (20 + 100 + 100) = 140ns 이다. hit ratio는 실제로 평균 95%이상이므로 충분히 효율적으로 동작한다고 볼 수 있다.

1.4 보호(Protection)

image

모든 주소는 페이지 테이블을 경유하므로, 테이블을 이용해서 보호 기능을 수행할 수 있다. 접근이 유효한지 그렇지 않은지를 구분하는 bit를 추가하여 켜져있을 때 수행하도록 한다. 대표적으로 페이지 테이블마다 r(read), w(write), x(execute) 비트를 두어, 해당 비트가 켜져있을 때 그 수행이 가능하도록한다.

image

위 그림은 페이지 테이블에 r,w,x 비트를 추가한 모습이다. 만약, 1번 페이지 엔트리처럼 쓰기 비트가 꺼져있는 페이지에 쓰기 작업을 시도하면 CPU에 인터럽트가 발생하여 ISR에서 강제로 해당 프로세스를 종료시킨다.

1.5 공유(Sharing)

공유는 메모리 낭비를 방지하기 위함이다. 같은 프로그램을 쓰는 복수 개의 프로세스가 있다면, 프로세스의 메모리는 code + data + stack 영역으로 나뉘는데 프로그램이 같다면 code 영역은 같을 것이다. 그러므로 하나의 code 영역을 복수 개의 프로세스가 공유하여 메모리 낭비를 줄이는 것이다. 단, code가 공유되려면 code가 변하지 않는 프로그램이어야 한다. 이를 non-self-modifying code = reentrant code(재진입가능 코드) = pure code 라고 한다.

Reference

KOCW 양희재 교수님 - 운영체제 양희재 교수님 블로그(시험 기출 문제)
codemcd 님의 정리글
세마포 사진
Operating System Concepts, 9th Edition - Abraham Silberschatz