Julie의 Tech 블로그

리눅스 - (2) 메모리 본문

Tech

리눅스 - (2) 메모리

Julie's tech 2021. 5. 12. 23:35
728x90

프로세스 뿐만 아니라 커널도 메모리를 사용하게 된다.

free라는 명령어를 통해 아래와 같이 시스템의 총 메모리 용량과 사용중인 용량을 확인할 수 있다.

$ free total used free shared buff/cache available Mem: Swap: // total : 시스템 전체 메모리 // free : 이용하지 않는 메모리 // buff/cache : 버퍼 캐시 또는 페이지 캐시가 이용하는 메모리 // available : 실질적으로 사용 가능한 메모리. free가 0이 되면 해제됨 // shared : 프로그램간 공유 가능한 자원 // Swap : 메모리가 OOM이 발생했을 경우 스왑핑 가능한 스왑 영역


메모리 할당 과정과 가상메모리

우선 가상메모리가 없다는 가정 하에 메모리 할당 방식에 대해 설명해보면 아래와 같다.

커널이 프로세스에 메모리를 할당하는 이유는 크게 두 가지이다.

1) 프로세스를 생성

2) 프로세스 생성한 뒤 추가로 메모리 할당시

프로세스가 추가로 메모리가 필요할 때는 시스템 콜을 통해 커널에게 추가 메모리를 요청한다.

이 때 커널이 필요한 사이즈만큼을 메모리의 free 영역에서 추출하여 시작 주소값을 전달한다고 해보자.

이러한 과정을 반복하게 되면 메모리에는 빈 공간이 있게 되지만, 프로세스의 생성과 종료를 반복하면서 이를 효율적으로 사용하지 못하게 된다.

프로세스와 다른 프로세스 사이에 위치한 빈 공간이 필요한 메모리만큼의 크기보다 작을 경우에 말이다.

또한 다른 프로세스에서 특정 프로세스가 사용중인 메모리 주소를 알게 되면 접근이 가능하기 때문에 보안상으로 좋지 않다.

이러한 이유로 가상 메모리가 등장하였다.

가상 메모리란, 가상 주소 공간을 사용하여 물리적인 메모리를 효율적으로 사용할 수 있도록 환경을 제공한다.

즉 우리가 쉘 환경에서 프로그램의 메모리 주소를 출력하게 되면 받는 값들이 전부 가상 주소이다.

이 가상 주소를 실제 물리 메모리의 주소로 매핑한 테이블을 '페이지 테이블'이라고 한다.

물리 메모리에 매핑되어있지 않은 가상 주소공간에 접근하게 되면 페이지 폴트(page fault) 에러가 발생한다.

출처 : https://ko.wikipedia.org/wiki/%EA%B0%80%EC%83%81_%EB%A9%94%EB%AA%A8%EB%A6%AC

그럼 가상메모리로 어떻게 메모리를 할당받을까?

프로그램을 실행할 때 필요한 메모리 사이즈는 코드 영역 사이즈와 데이터 영역 사이즈를 합산한 값이다.

이 크기 만큼을 물리 메모리로부터 할당받아 데이터를 복사하게 된다.

여기서 할당받은 물리 메모리 주소와 가상 주소를 매핑하여 페이지 테이블로 관리한다.

C언어 표준 라이브러리에 있는 'malloc()' 함수가 'mmap()'함수를 호출하여 커널로부터 메모리를 할당받게 된다.

이처럼 가상메모리를 통해 가상메모리상으로는 프로세스가 할당받은 주소 공간이 단일 공간으로 보이지만,

실제 물리 메모리에서는 여러 주소 공간으로 나뉘어있을 수 있다.

이렇게 프로세스마다 가상메모리 주소 영역이 겹치지 않기 때문에 다른 프로세스로의 접근도 불가능하다.

뿐만 아니라 커널 모드만이 커널이 사용하는 메모리의 페이지 엔트리에 접근할 수 있기 때문에 앞서 살펴본 문제점들을 모두 해결할 수 있다.

추가로 페이지 테이블에 사용되는 메모리 양을 줄이기 위해 계층형 페이지 테이블을 사용하게 된다. 페이지별로 하위 페이지테이블을 생성하여 관리하고 있다.

또한 페이지 테이블이 사용하는 메모리 용량이 최대로 달하고 있을 경우 Huge Page라는 방법을 사용하여 (연속된 주소 페이지의 경우 주소를 묶음) 문제를 관리하고 있다.

출처 : https://ko.wikipedia.org/wiki/%ED%8E%98%EC%9D%B4%EC%A7%80_%ED%85%8C%EC%9D%B4%EB%B8%94#

추가로 리눅스는 메모리를 효율적으로 사용하기 위해 디맨드 페이징이라는 방식을 사용하여 메모리를 할당한다.

실제 프로세스가 할당받지 않았거나, 할당받았더라도 물리 메모리에는 할당되지 않은 상태를 표기하는 것을 말한다. 프로그램은 메모리를 할당받은 것으로 알지만, OS는 페이지 폴트를 처리하는 방식으로 프로그램이 인지못하는 사이에 메모리를 할당한다.

과정을 들어 설명하면, 프로그램이 실행되면서 엔트리 포인트에 접근하게 되면, 그에 대응하는 가상 주소가 아직 매핑되지 않았기에 페이지 폴트가 발생한다. 페이지폴트 핸들러가 물리 주소를 매핑해주게 되고, 프로세스는 이 폴트가 발생한 사실을 모른채 실행을 계속한다.

동적으로 메모리를 할당받을 때에도 동적으로 가상 메모리를 획득하였지만, 이에 매핑되는 물리 주소가 없을 경우 페이지 폴트 핸들러가 물리 주소로 매핑해주게 된다.

이러한 디맨드 페이징 방식을 통해 가상 메모리를 할당하면서 물리 메모리가 더 이상 충분하지 않은 경우 물리 메모리 부족 현상이 발생한다.

즉 할당받은 물리 메모리에 실제로 접근하지 않는 이상 물리 메모리 사용량 값은 변화하지 않는다.

* 페이지의 경우 가상메모리를 모든 같은 크기의 블록으로 나누어 운영하는 방법, 일정한 크기를 가진 블록을 부르는 단위

* 디맨드 페이징의 경우 남은 공간을 찾기 위한 탐색에 소요되는 시간이 있어, 고속의 프로그램을 만들기 위해서는 프로그램 자체적으로 메모리를 아예 할당받고, 프로그램 안에서 메모리 매니지먼트를 하게 된다. (ex. Java의 가비지 컬렉터 등)


스왑

OOM(Out of Memory)가 발생하게 되면 저장 장치를 메모리를 대신하여 사용하게 되는 '스왑핑' 이 발생한다.

커널은 사용 중인 물리 메모리의 일부를 스왑 영역에 임시로 보관하게 된다. 이 과정을 '스왑 아웃'이라고 한다.

이 때 어느 영역을 보관하게 될 것인지는 특정 알고리즘을 통해 가장 사용하지 않을 것 같은 영역을 발라내게 된다.

이렇게 스왑아웃한 영역을 메모리를 필요로하는 프로세스에 할당하게 되고, 시간이 흘러 스왑아웃된 영역을 프로세스가 접근하게 되면 페이지 폴트가 발생하여 커널이 다시 해당 영역을 메모리의 빈 공간에 '스왑인'하게 된다.

디맨드페이징과 마찬가지로 메모리를 효율적으로 사용하기 위한 방식이다.


캐시 메모리

메모리 장치로는 저장장치, 메모리, 캐시메모리, 레지스터가 있다.

저장장치 > 메모리 > 캐시메모리 > 레지스터 순으로 크기가 크고, 가격이 저렴하며 접근속도가 느리다.

컴퓨터는 명령어를 수행 시 레지스터에서 데이터를 읽어 메모리로 읽어 계산하고, 계산한 값을 메모리에 쓰게 된다.

레지스터와 메모리간의 접근 레이턴시를 늦추기 위해 '캐시 메모리'라는 것이 등장하였다.

메모리 내용을 캐시메모리에 복사해두어 CPU가 메모리까지 접근하지 않더라도 캐시 메모리를 통해 바로 읽을 수 있다.

레지스터에서 값을 덮어쓴 경우 캐시 메모리에서 더티 플래그를 표기하여 관리한다.

최근에는 캐시 메모리 역시 계층형으로 구성되어 있다.

L1, L2, L3가 있는데, 뒤로갈수록 용량이 크고 접근 속도가 느리다.

CPU에서 저장 장치에 접근하는 속도가 굉장히 느린데, 이를 줄이기 위한 방안 중 하나로 페이지 캐시 기능이 있다.

캐시 메모리와 유사한데, 저장장치 내의 파일을 메모리에 캐싱하는 것이다.

프로세스가 파일의 데이터를 읽게 되면, 메모리에 올라가있는 프로세스의 영역에 바로 복사하는 것이 아니라, 페이지 캐시 영역에 복사한 뒤 프로세스 메모리로 다시 복사하게 된다.

이 과정을 마친 뒤 페이지캐시에는 파일 이름과 파일 오프셋(영역 크기), 메모리 주소에 대한 정보를 저장한다.

이후 동일한 데이터에 접근할 때는 페이지 캐시 영역에서 탐색하여 바로 반환하게 된다.

프로세스가 데이터를 write하는 과정은 반대로 일어나게 된다. 여기서 페이지 캐시에 더티 플래그를 세우게 되고, 스토리지 내의 파일에 새로운 데이터를 반영하게 되면 플래그는 지워지게 된다.

(추가: 위에서 확인한 것 처럼 CPU는 대부분 메모리 또는 캐시 메모리로부터 데이터를 기다리는 데 시간을 소요하고 있다. 이러한 CPU의 성능을 효율적으로 쓰기 위해 하이퍼스레드라는 기능이 있는데, CPU 코어 안의 일부 자원들을(레지스터 등) 복제하여 시스템 입장에서는 각각의 논리 CPU로 인식하게끔 하는 것이다. 따라서 물리적으로는 코어 개수가 변하지 않는데, 논리적으로 여러 개로 인식되게끔 하여 프로그램 처리 속도를 증가하도록 하는 것이다.)

다음 편에서는 파일 시스템에 대해 알아보도록 할 것이다.


참고 도서 : 실습과 그림으로 배우는 리눅스 구조, 다케우치 사토루

https://book.naver.com/bookdb/book_detail.nhn?bid=14524977

 

반응형