리눅스는 커널의 메모리 관리 시스템으로 시스템에 탑재된 메모리를 관리한다. 각 프로세스는 물론이고 커널 자체도 메모리를 사용한다.
메모리 통계정보
메모리 통계정보는 free
명령어로 확인할 수 있다.
(base) [user@server ~]$ free
total used free shared buff/cache available
Mem: 65387028 16650384 7639664 233184 41096980 48005532
Swap: 32767996 6065624 26702372
(base) [user@server ~]$
(base) [user@server ~]$ free -h
total used free shared buff/cache available
Mem: 62G 15G 7.3G 227M 39G 45G
Swap: 31G 5.8G 25G
- total: 탑재된 전체 메모리
- free: 표기상 이용하지 않는 메모리
- available: 실질적으로 사용 가능한 메모리. free 값이 부족하면 해제되는 커널 내의 메모리 영역 사이즈를 더한 값.
메모리 부족
메모리 사용량이 증가하면 free 영역이 줄어든다. 그러면 메모리 관리 시스템은 커널 내부의 해제가능한 메모리 영역을 차례차례 해제한다.
이후에도 메모리 사용량이 증가하면 OOM(Out of Memory)상태가 된다. 이러한 경우, 메모리 관리 시스템에는 OOM Killer라는 기능이 있는데 적절한 프로세스를 선택해 강제 종료하는 기능이다. 문제는 그 적절한 프로세스를 죽였을 때 문제가 있을지 없을지 메모리 관리 시스템이 모른다는 점이다.
단순한 메모리 할당
가상 메모리라는 개념이 없는 경우를 가정하며 가상메모리가 없을 때 어떤 문제가 발생하는지 알아본다. 커널이 프로세스에 메모리를 할당하는 일은 크게 두 가지 타이밍에 이루어진다.
- 프로세스를 생성할 때
- 프로세스를 생성한 뒤 추가로 동적 메모리를 할당할 때
2에 관하여
프로세스는 메모리가 더 필요할 때 메모리 확보용 시스템콜을 호출하여 메모리 할당을 요청하고, 커널은 메모리 할당 요청을 받으면 필요한 사이즈를 빈 메모리 영역에서 잘라내어 그 영역의 시작 주소값을 반환한다. 이와 같은 방식에는 세 가지 문제가 발생한다.
- 메모리 단편화
- 다른 용도의 메모리에 접근 가능
- 여러 프로세스 다루기 곤란
메모리 단편화
메모리의 빈 공간이 총 합이 300이 있더라도, 100 100 100 이렇게 나누어져있다면 100보다 큰 메모리 공간을 확보할 수 없다. 100으로 나누어진 세 공간을 하나의 공간으로 인식하려고 한다면 할 수야 있다. 하지만 추가로 메모리를 요청하여 얻은 뒤 얻은 메모리가 몇 개의 영역에 나누어져 있는지 매번 확인해야 하며, 100보다 큰 하나의 데이터(ex. 200만큼의 배열)를 만드는 용도로는 사용할 수 없다.
다른 용도의 메모리에 접근 가능
지금 방식으로는 프로세스가 커널이나 다른 프로세스가 사용중인 메모리 영역에 직접 접근 가능하다. 데이터가 오염될 가능성이 있다.
여러 프로세스 다루기 곤란
-> 이해 안됨. 펭귄책 p124
가상 메모리
위의 문제들을 해결하기 위해 가상메모리의 개념이 등장했다. 시스템에 탑재된 메모리를 프로세스가 직접 접근하지 않고 가상 주소라는 주소를 사용하여 간접적으로 접근한다. 프로세스에 보이는 메모리 주소를 가상 주소, 시스템에 탑재된 메모리의 실제 주소를 물리 주소라고 한다. 또한 주소에 따라 접근 가능한 범위를 가상 주소 공간 이라고 한다.
위 그림에서 프로세스가 주소 100번지에 접근하는 것은 실제 메모리에서 500번지에 접근하는 것과 같다. 프로세스가 물리 주소에 직접 접근하는 방법은 없다.
페이지 테이블
가상 주소에서 물리 주소로 변환하는 과정은 커널 내부에 있는 페이지 테이블이라는 표를 사용한다. 페이지 테이블에서 한 페이지에 데한 데이터를 페이지 테이블 엔트리라고 하고, 이 페이지 테이블 엔트리에 가상 주소와 물리 주소의 대응 정보가 들어있다. x86_64 아티켁처의 페이지 사이즈는 4kb이다.
프로세스가 300번지에 접근하면 PCU는 커널의 물리주소 변환을 통해 자동으로 테이지 테이블 내용을 참조하여 대응된 물리주소로 접근하도록 변환한다.
300번지에만 물리 메모리가 할당된 상황에서 프로세스가 301~500번지에 접근하면 CPU에는 Page fault라는 인터럽이 발생한다. Page fault라는 인터럽트에 의해 실행중인 명령이 중단되고 커널 내의 Page fault handler 라는 인터럽트 핸들러가 동작한다. 커널은 프로세스로부터 메모리 접근이 잘못되었다는 내용을 핸들러에게 알려주고 'SIGSEGV'시그널을 프로세스에 통지하고 이 시그널을 받은 프로세스는 강제 종료된다.
프로세스에 메모리를 할당할 때
프로세스를 생성할 때나 추가 메모리 할당 요청을 받은 커널이 어떻게 프로세스에 메모리에 할당하는지 알아본다.
프로세스를 생성할 때
실행 파일은 아래와 같이 구성되어있다.
코드 영역의 파일상 오프셋: 100, 코드 영역 사이즈:100, 코드 영역의 메모리 맵 시작 주소:0, 데이터 영역의 파일상 오프셋:200, 데이터 영역 사이즈:200, 데이터 영역의 메모리 맵 시작주소: 100, 엔트리 포인트:0
프로그램을 실행할 때 필요한 메모리 사이즈는 코드 영역 사이즈 + 데이터 영역 사이즈이므로 아래 그림과 같이 이 영역을 물리 메모리에 할당해 필요한 데이터를 복사한다.(실제로 물리 메모리 할당은 디맨드 페이징이라는 방식을 사용한다. 후술됨) 계속해서 프로세스를 위한 페이지 테이블을 만들고 가상 주소 공간을 물리 주소 공간에 매핑ㄹ한다. 마지막으로 엔트리 포인트의 주소에서 실행을 시작하면 된다.
추가 메모리 할당
프로세스가 새 메모리를 요구하면 커널은 새로운 메모리를 할당하여 대응하는 페이지 테이블을 작성한 후 할당된 메모리에 대응하는 가상 주소를 프로세스에 반환한다.
고수준 레벨에서의 메모리 할당
C에서의 메모리 획득
C언어의 표준 라이브러리에 있는 malloc() 함수가 메모리 확보 함수다. 리눅스에서는 내부족으로 malloc() 함수에서 mmap() 함수를 호출하도록 구현되어있다.
mmap() 함수는 페이지 단위로 메모리를 확보하지만 malloc() 함수는 바이트 단위로 메모리를 확보한다. glibc는 mmap() 시스템 콜을 이용해 커다란 메모리 공간을 미리 확보해 메모리 풀을 만든다. 이후 프로세스에서 malloc()을 호출하면 필요한 양만큼의 바이트를 잘라내어 반환하는 처리를 한다. 풀로 만들어 둔 메모리가 더 이상 남아있지 않다면 다시 mmap()을 호출하여 새로운 메모리 풀을 확보한다.
Python에서의 메모리 획득
직접 메모리 관리를 하지 않는 파이썬과 같은 스트립트 언어에서 객체를 생성할 때 최종적으로는 C언어의 malloc()을 사용한다.
가상 메모리로 아래 문제를 해결하는 방식
메모리 단편화
프로세스의 페이지 테이블을 잘 설정하면 물레 메모리의 단편화된 영역을 프로세스의 가상 주소 공간에서는 하나의 큰 영역처럼 보이게 할 수 있다.
다른 용도의 메모리에 접근 가능
가상 주소 공간은 프로세스별로 만들어지고, 페이지 테이블도 프로세스별로 만들어진다. 서로의 메모리가 보이지 않는다.
여러 프로세스 다루기 곤란
나중에 다시 작성하기
가상 메모리의 응용
파일 맵(File map)
프로세스가 파일에 접근할 때 mmap() 함수를 이용해 파일의 영역을 프로세스의 가상 주소 공간에 메모리 매핑을 할 수 있다. 매핑된 파일은 메모리에 접근하는 방식과 동일한 방식으로 접근 가능하다.
디맨드 페이징
프로세스 생성될 때, mmap() 시스템 콜로 프로세스에 메모리를 추가적으로 할당할 때 아래와 같은 순서로 진행된다.
- 커널이 필요한 영역을 메모리에 확보한다.
- 커널이 페이지 테이블을 설정하여 가상 주소 공간에 물리 주소 공간을 매핑한다.
이 방법에는 아래의 두 가지 경우와 같이 메모리를 확보한 때부터 혹은 프로세스가 종료할 때 까지 사용하지 않는 메모리 영역이 있기 때문에 메모리를 낭비한다는 단점이 있다.
- 커다란 프로그램 중 실행에 사용하지 않는 기능을 위한 코드 영역과 데이터 영역
- glibc가 확보한 메모리 풀 중 유저가 malloc() 함수로 확보하지 않은 부분
이러한 문제를 해결하기 위해 디맨드 페이징 방식을 이용하여 프로세스에 메모리를 할당한다.
디맨드 페이징을 사용하면 프로세스의 가상 주소 공간 내의 각 페이지에 해당하는 주소는 페이지에 처음 접근할 때 할당된다.
각 페이지에는 (1)프로세스에는 할당되지 않음, (2)프로세스에 할당되었으며 물리 메모리에도 할당되었음과 더불어 (3)프로세스에는 할당되었지만 물리 메모리에는 할당되지 않음 이라는 새로운 상태가 추가된다.
프로세스가 처음 시작하면 물리 메모리에는 할당되지 않은 상태가 된다. 이 다음, 프로세스가 엔트리포인트부터 실행을 시작하면 엔트리 포인트에 대응하는 페이지용 물리 메모리가 할당된다. 이 때의 처리 흐름은 다음과 같다.
- 프로그램이 엔트리포인트에 접근한다.
- CPU가 페이지 테이블을 참조해서 엔트리 포인트가 속한 페이지에 대응하는 가상 주소가 물리 주소에 아직 매핑되지 않음을 인지한다.
- CPU에 페이지 폴트가 발생한다.
- 커널의 페이티 폴트 핸들러가 1에 의해 접근된 페이지에 물리 메모리를 할당하고, 페이지 폴트를 지운다.
- 사용자 모드로 돌아와 프로세스가 실행을 계속한다.
이후에도 아직 페이지용 물리 메모리가 할당되지 않은 다른 영역에 접근하면 위와 같은 5단계가 반복되며 실제 물리 메로리가 할당된다. mmap()을 이용하여 메모리 풀을 확보할 때는 가상 메모리주소만 확보하며 물리 메모리는 아직 확보하지 않는다. 이 이후에 프로세스가 mmap()으로 추가로 확보한 가상 주소 영역에 접근하면 이 때 가상 주소 영역에 물리 메모리를 할당한다. mmap()을 이용하여 메모리를 확보하는 것을 '가상 메모리를 확보했을'이라고 표현하며, 확보한 가상 메모리에 접근하여 물리 메모리를 확보하고 매핑하는 것을 '물리 메모리를 확보했음'이라고 표현한다
가상 메모리 부족과 물리 메모리 부족
가상 메모리 부족이란 프로세스가 가상 주소 공간의 범위가 꽉 차도록 가상 메모리를 전부 사용한 뒤에도 가상 메모리를 더 요청했을 때 발생한다. 가상 메모리 부족은 물리 메모리가 얼마나 남아있든 상관없이 발생할 수 있다.
물리 메모리 부족은 시스템에 탑재된 물리 메모리를 전부 사용하면 발생한다.
Copy on Write (CoW)
쓸 때 복사한다.
fork()시스템 콜을 이용하여 자식 프로세스를 만들었을 때, 우선 부모 프로세스의 페이지 테이블을 그대로 복사한다(같은 물리 페이지에 접근한다). 이 때, 페이지 테이블 엔트리 안의 쓰기 권한을 나타내는 필드에 부모, 자삭 모두 전체 페이지에 대한 쓰기 권한을 무효화(못 쓰도록)한다. 이후에 페이지를 읽기만 한다면 부모와 자식 프로세스가 계속 같은 물리 메모리에 접근한다. 그러다가 누구 하나가 물리 메모리(페이지)에 쓰기를 진행하면 아래와 같은 단계가 진행된다.
- 메모리 쓰기 권한이 없기 때문에 CPU에 페이지 폴트가 발생한다.
- CPU가 커널모드로 변경되어 커널의 페이지 폴트 핸들러가 동작한다.
- 페이지 폴트 핸들러는 접근한 페이지를 다른 장소에 복사하고, 쓰려고 한 프로세스에 할당한 후 내용을 다시 작성한다. (쓰려고 할 때 복사한다 = Copy on Write)
- 부모, 자식 프로세스가 각각 공유가 해제된 페이지에대응하는 페이지 테이블 엔트리를 업데이트 한다.
- 쓰기를 한 프로세스 엔트리는 새롭게 할당된 물리 메모리를 매핑하고 쓰기를 허가한다.
- 다른 쪽 프로세스 엔트리에는 쓰기를 허가한다.
자식 프로세스를 만드는 fork()시스템 콜을 호출했을 때 물리 메모리를 복사하는 것이 아니라, 읽기가 계속 될 때는 물리 메모리를 공유하다가 쓰기가 발생했을 때 물리 메모리 복사가 발생하므로 Copy on Write 라고 한다.
스왑(Swap)
물리 메모리가 부족하게되면 메모리부족 상태(OOM)가 된다. 이에 대응하기 위해 가상 메모리를 응용한 스왑을 이용한다. 스왑은 저장장치의 일부를 일시적으로 메모리 대신 사용하는 방식이다. 물리 메모리가 부족한 상태가 되어 프로세스가 물리 메모리를 획득할 때 기존 사용하던 물리 메모리의 일부분을 저장장치에 저장하여 빈 공간을 만들어낸다. 이때 메모리의 내용이 저장된 영역을 스왑 영역이라고 부른다.
물리 메모리의 일부를 스왑 영역에 임시보관 하는 것을 스왑 아웃이라고 하며 스왑 영역에 임시 보관했던 데이터를 물리 메모리에 되돌리는 것을 스왑 인이라고 한다. 이 둘을 합쳐 스와핑이라고 하며 리눅스에는 스왑의 단위가 페이지 단위이므로 페이지 아웃, 페이지 인, 이를 합쳐서 페이징이라고도 부른다. 스왑의 동작 방식은 시스템에서 사용할 수 있는 메모리의 양이 실제 탑제된 메모리 + 스왑 영역처럼 보이게 한다.
하지만 저장 장치에 접근하는 속도가 메모리에 접근하는 속도에 비해 매우 느리다. 스왑이 자주 된다는 것은 그만큼 연산이 느려진다는 의미이다. 시스템 메모리 부족이 일시적인 현상이 아니라 만성적 현상이라면 스왑 인, 아웃이 반복되는데 이를 스래싱(thrashing)이라고 한다.
계층형 페이지 테이블
페이지 테이블은 1차원적인 구조로 구현되어 있지 않고 계층 구조로 만들어져 페이지 테이블 자체에 필요한 메모리의 양을 절약한다. x86_64 아키텍처는 4단 구조로 되어있다. 가상메모리를 대량으로 사용하는 프로세스는 Huger Page로 처리 가능하다.
Huge Page
'Computer Science > Computer Architecture' 카테고리의 다른 글
System Bus (0) | 2022.02.13 |
---|---|
PIO(Programmed I/O)와 DMA(Direct Memory Access) (0) | 2022.02.13 |
컴퓨터 시스템의 기본 (0) | 2022.02.08 |
메모리계층 (0) | 2022.02.06 |