가상 메모리는 왜 등장하게 되었을까?

L-cloud·2023년 11월 5일
0

스터디

목록 보기
5/5
post-thumbnail

본 글은 가상 메모리, 메모리 파편화, 페이징, 스왑 등을 들어본 독자를 대상으로 합니다.
프로세스가 직접 메모리를 조작하지 않고 OS를 거쳐 간접적으로 조작하게 한 이유를 알아봅니다.

메모리 할당은 보통 두 가지 타이밍에서 발생합니다.

  1. 프로세스를 생성할 때
  2. 프로세스를 생성한 뒤 추가로 동적 메모리를 할당할 때!

가상 메모리를 사용하지 않고 단순히 개별 프로세스가 직접 메모리에 접근한다면 문제가 발생할까요?

  1. 다른 용도의 메모리에 접근 가능
    메모리 주소를 통해 직접 접근할 수 있으니 커널이나 다른 프로세스가 사용 중인 곳에 접근이 가능해집니다.

  2. 여러 프로세스를 다루기 곤란
    동일한 프로그램을 1개 더 가동해 메모리에 매핑할 때 어떻게 해야 할까요? 파일 헤더에는 코드와 데이터 영역의 파일 상 오프셋, 사이즈, 메모리 맵 시작 주소 등이 적혀있는데 그럼 동일한 프로그램은 코드 영역이 겹치니 동시에 실행 못 하지 않을까요? 그리고 다른 프로그램을 만들 때 메모리 주소를 직접 정하면 기존에 있는 프로그램의 메모리 주소를 다 피해서 지정해야 하지 않을까요?

  3. 메모리 단편화
    메모리 획득 해제를 반복하면 메모리 파편화 문제가 발생합니다. 남아있는 영역은 300kb인데 100kb씩 나뉘어 있으면 어떻게 해야 할까요? 3개의 영역을 하나로 묶어서 다루면 될까요? 그렇다면 매번 프로세스를 실행할 때마다 몇 개의 영역으로 나뉘어 있는지 확확인해야 않을까요? 상당히 불편하겠죠?

가상 메모리의 등장!

가상 메모리의 핵심 개념은 가상 주소를 가지고 물리 메모리에 간접적으로 접근한다. 입니다. 프로세스가 실제 물리 메모리 영역에 직접 접근할 방법은 없습니다. 그럼 어떻게 간접적으로 접근을 할까요?
바로 OS가 개별 프로세스에 페이지 테이블을 제공하여 OS를 통해서만 물리 메모리에 접근할 수 있게 합니다. 이렇게 하면 프로세스에 허용되지 않은 메모리 접근을 막을 수 있어서 1번 문제를 해결할 수 있겠죠.

직접 메모리에 접근하는 코드로 실험을 해봅시다.

#include <stdio.h>

int main()
{
    int *p = NULL;
    puts("Before invalid access");
    *p = 0;
    puts("After invalid access");
}

root@c89f1455e7c0:/test# ./segv
Before invalid access
Segmentation fault

프로그램이 허용되지 않은 메모리 영역에 접근을 시도하거나, 허용되지 않은 방법으로 메모리 영역에 접근을 시도할 경우 발생하는 Segmentaion fault가 발생하는 것을 확인할 수 있습니다.

또한 단편화된 물리 메모리 주소를 페이지 테이블에서 적절하게 가상 주소로 매핑하여 3번 문제도 해결하고, 개별 프로세스마다 페이지 테이블이 제공되니 2번 문제도 해결이 됩니다.

아래는 참고 내용
처음에 프로세스에 약간의 메모리를 할당하고 추가로 더 할당할 때는 프로세스에 페이지 테이블을 추가로 OS가 작성한 다음 프로세스에 넘겨준다. 그럼 페이지 테이블에 해당 정보가 계속 추가 된다.
c언어 mmap() 은 페이지 단위, malloc()은 바이트 단위로 메모리를 확보한다. 그래서 malloc() 대비하기 위해서 glibc에서 사전에 메모리 풀로 미리 메모리를 확보하고 있다가 malloc() 호출하면 그 풀에서 메모리를 주고.. 그래서 리눅스에서 메모리 사용량 체크하는 거랑 프로세스 내에서 메모리 체크하는 거랑 다를 수 있음! 메모리 풀을 포함하느냐 안 하느냐에 따라.. (이는 가상 메모리 응용을 이해하는 데 도움이 됨)

가상 메모리의 응용

  • 파일 맵
  • 디맨드 페이징
  • Copy on Write 방식의 고속 프로세스 생성
  • 스왑
  • 계층형 페이지 테이블
  • Huge page

파일 맵(MMF)

전통적인 파일 입출력은 데이터를 읽거나 쓸 때 시스템 콜 사용합니다. 그리고 데이터는 버퍼에 일시적으로 저장되며 여기에서 읽기, 쓰기 등이 이루어집니다. 하지만 메모리 맵 파일은 파일 전체나 일부를 가상 메모리의 주소 공간에 직접 매핑함. 메모리에 있는 일반 변수를 읽고 쓰는 것과 유사하게 동작함.
따라서 write와 같은 시스템 콜 호출 없이 메모리 영역대로 내용을 복사해서 실제 파일에 내용을 저장할 수 있습니다. 예시 코드

디맨드 페이징

프로세스의 모든 영역이 메모리에 올라올 필요는 없습니다. 즉 현재 필요한 페이지만 메모리에 올리면 됩니다.
그럼 아직 할당이 안 된 영역에 접근하면 어떻게 될까요? 페이지 폴트가 발생한 다음 커널 모드에서 메모리를 할당해 줍니다. 이렇게 동적으로 할당하면 훨씬 메모리를 아낄 수 있겠죠?

실제로 mmap() 함수는 메모리 영역 확보는 일단 가상 메모리를 확보했음을 의미하고 실제 물리 메모리 확보를 하지는 않습니다. 실제 그 메모리에 접근할 때 물리 메모리에 할당이 됩니다. 이 코드와 함께 페이지 폴트 여부와 메모리 상황을 모니터링 해보면 해당 내용을 눈으로 확인할 수 있습니다.

Copy on Write

fork() 시스템 콜을 사용하면 부모 프로세스의 메모리를 자식 프로세스에 전부 복사하는 것이 아니라 그냥 페이지 테이블만 복사하기에 상당히 빠릅니다. 그럼 데이터의 write는 어떻게 할까요? 일단 물리 메모리 영역을 공유하고 있기 때문에 쓰기 호출이 들어오는 경우

  1. 페이지에 쓰기는 허용하지 않기 때문에 일단 페이지 폴트 발생
  2. CPU가 커널 모드로 변경되어서 페이지 폴트 핸들러 동작
  3. 페이지 폴트 핸들러는 write 하려고 하는 페이지를 다른 장소에 복사하고, write 요청을 보낸 프로세스에 해당 영역을 할당한 후 내용을 작성함
  4. 부모, 자식 프로세스 모두 각각 공유가 해제된 페이지에 대응하는 페이지 테이블 엔트리를 업데이트!!

그림으로 나타내면 아래와 같습니다.

스왑

저장 장치의 일부를 일시적으로 메모리 대신 사용하는 방식입니다.

스왑 아웃과 스왑 인을 합쳐서 스와핑이라고 합니다.

메모리가 부족해서 메모리에 접근할 때마다 스와프 인, 스왑 아웃 발생하면 스래싱 상태가 됩니다.

sar -W 1 명령어로 스와핑 발생 유무도 확인할 수 있습니다.

Major Fault → 저장 장치에 대한 접근이 발생하는 페이지 폴트. (SSD, HDD 등)

페이지 테이블 크기 문제 해결 방법

계층형 페이지 테이블

x86_64 아키텍쳐의 가상 주소는 128테라 바이트입니다. 1페이지의 크기는 4kb, 테이지 테이블 엔트리 사이즈는 8byte. 그럼 프로세스 1개당 (8 바이트 * 128 테라 바이트 / 4kb)의 용량이 필요할까요? NO!!

계층 구조로 이것을 표현해서 용량을 줄이거나 해시 페이지 테이블, 역 페이지 테이블 등을 사용합니다.
자세한 내용

Huge Page

프로세스의 가상 메모리 사용 사이즈가 증가하면 페이지 테이블에 사용하는 물리 메모리양도 증가합니다.

이를 해결하기 위해 Huge Page를 사용해서 페이지 테이블에 필요한 메모리양을 줄입니다.


출처

COW
소스코드

profile
내가 배운 것 정리

0개의 댓글