5장 메모리 관리
카테고리: Linux-architecture
태그: Linux
실습과 그림으로 배우는 리눅스 구조 5장의 내용을 정리한 글입니다.
리눅스는 커널의 메모리 관리 시스템으로 시스템에 탑재된 메모리를 관리합니다.
메모리는 각 프로세스가 사용하는 것은 물론이고 커널 자체도 메모리를 사용합니다.
따라서 메모리의 종류는 크게 세가지로 나눌 수 있습니다.
- 커널 메모리
- 프로세스 메모리
- 비어있는 메모리
메모리 부족
메모리 사용량이 증가하면 사용가능한 메모리가 점점 줄어듭니다.
이러한 상태가 되면 메모리 관리 시스템은 커널 내부의 해제 가능한 메모리 영역을 해제합니다.
이후에도 메모리 사용량이 계속 증가하면 시스템은 메모리가 부족해 동작할 수 없는 메모리 부족 (Out Of Memery, OOM)
상태가 됩니다.
OOM이 발생할 경우 메모리 관리 시스템에는 적절한 프로세스를 선택해 강제 종료(kill)하여 메모리 영역을 해제시키는 OOM Killer
라는 기능이 있습니다.
단순한 메모리 할당
가상 메모리를 사용하지 않은 가장 단순한 메모리 할당에 대해서 알아보겠습니다.
메모리 할당은 두 가지 타이밍에 발생합니다.
- 프로세스를 생성할 때
- 프로세스를 생성한 뒤 추가로 동적 메모리를 할당할 때
지금은 동적 메모리를 할당하는 상황을 가정하겠습니다.
프로세스가 생성된 뒤 추가로 메모리가 필요하면 프로세스는 커널에 메모리 확보용 시스템 콜
을 호출해서 메모리 할당을 요청합니다.
커널은 메모리 확보용 시스템 콜을 호출해서 메모리 필요한 사이즈를 빈 메모리 영역으로부터 잘라내 그 영역의 시작 주소값
을 반환합니다.
이러한 메모리 할당 방법에는 다음과 같은 문제점이 있습니다.
- 메모리 단편화(memory fragmentation)
- 다른 용도의 메모리에 접근 가능
- 여러 프로세스를 다루기 곤란
메모리 단편화
메모리 단편화는 사용가능한 메모리가 충분히 존재하지만 할당(사용)이 불가능
한 상태를 의미합니다.
메모리 단편화는 내부 단편화와 외부 단편화로 구분 가능합니다.
내부 단편화
메모리를 할당할 때 프로세스가 필요한 양보다 더 큰 메모리가 할당되어서
프로세스에서 사용하는 메모리 공간이 낭비되는 상황입니다.
외부 단편화
메모리가 할당되고 해제되는 작업이 반복될 때, 할당 가능한 작은 메모리가 중간중간 존재하게 됩니다.
이때 사용 가능한 메모리 총 메모리의 공간은 충분
하지만, 실제로 할당이 불가능
한 상황입니다.
다른 용도의 메모리에 접근 가능
단순한 메모리 할당 방식으로는 프로세스가 커널이나 다른 프로세스가 사용하고 있는 주소를 직접 지정하면 그 영역에 직접 접근할 수 있습니다.
이러한 방식은 데이터가 오염되거나 파괴될 위험성이 있습니다.
어떤 식으로든 커널의 데이터가 망가지면 시스템은 정삭적으로 동작하지 못합니다.
여러 프로세스를 다루기 곤란
프로그램의 명령과 데이터에서 지정한 메모리 주소는 고정되어 있습니다.
여러개의 프로세스를 동시에 실행할 경우, 메모리의 주소가 겹칠 수 있어 문제가 발생할 수 있습니다.
가상 메모리
단순한 메모리 할당 방식으로 메모리를 할당하면 위와 같은 문제가 발생할 수 있습니다.
이러한 문제를 해결할 수 있는 방식이 가상 메모리 방식입니다.
가상 메모리를 간단하게 설명하면 시스템에 탑재된 메모리를 프로세스가 직접 접근하지 않고, 가상 주소라는 주소를 사용하여 간접적으로 접근하도록 하는 방식입니다.
- 가상 주소 : 프로세스에 보이는 메모리 주소
- 물리 주소 : 시스템에 탑재된 메모리의 실제 주소
주소에 따라서 접근 가능한 범위를 가상 주소 공간
이라고 부릅니다.
프로세스로부터 메모리에 직접 접근하는 방법, 즉 물리주소에 직접적으로 접근하는 방법은 없습니다.
페이지 테이블
가상 주소에서 물리 주소로 변환하는 과정은 커널 내부에 보관되어 있는 페이지 테이블
이라는 표를 사용합니다.
가상 메모리는 전체 메모리를 페이지라는 단위로 나눠서 관리하고 있어서 변환은 페이지 단위
로 이루어집니다.
페이지 테이블
에서 한 페이지에 대한 데이터를 페이지 테이블 엔트리
이라고 부르며, 이 페이지 테이블 엔트리에는 가상 주소와 물리 주소의 대응정보가 들어 있습니다.
페이지 테이블은 프로세스마다 가지고 있으며, 커널 메모리에 저장되어 있습니다.
페이지 폴트
가상 주소와 물리 주소가 맵핑되어 있지 않은 가상 주소에 접근하려 할 때, 페이지 폴트
라는 인터럽트가 발생합니다.
페이지 폴트
가 발생할 경우, 현재 실행 중인 명령이 중단되고 커널 내의 페이지 폴트 핸들러
라는 인터럽트 핸들러가 동작합니다.
커널은 프로세스로부터 메모리 접근이 잘못되었다는 내용을 페이지 폴트 핸들러에 알려줍니다.
그 뒤에 SIGSEGV
시그널을 프로세스에 통지합니다. 그리고 이 시그널을 받은 프로세스는 강제로 종료됩니다.
프로세스에 메모리를 할당할 때
커널이 프로세스를 생성할 때나 추가로 메모리를 요청받을 때, 가상 메모리를 통하여 어떻게 프로세스에 메모리를 할당하고 있는지를 알아보겠습니다.
프로세스를 생성할 때
프로그램을 실행 시켜 메모리 위에 적재할 때, 실행 파일을 읽어 여러 가지 보조 정보를 읽어냅니다.
실행 파일은 다음의 표와 같이 구성되어 있습니다.
- 코드 영역의 파일상 오프셋
- 코드 영역 사이즈
- 코드 영역의 메모리 맵 시작 주소
- 데이터 영역의 파일상 오프셋
- 데이터 영역 사이즈
- 데이터 영역의 메모리 맵 시작주소
- 엔트리 포인트
프로그램을 실행하는데 필요한 메모리 사이즈는 코드 영역 사이즈
+ 데이터 영역 사이즈
입니다.
그리고 이 크기만큼 물리 메모리에 할당해서 필요한 데이터를 복사합니다.
리눅스에서의 실제 물리 메모리 할당은 좀 더 복잡한 디맨드 페이징 방식을 사용합니다.
계속해서 프로세스를 위한 페이지 테이블을 만들고 가상 주소 공간을 물리 주소 공간에 맵핑합니다.
마지막으로 엔트리 포인트의 주소에서 실행을 시작합니다. (엔트리 포인트 주소는 가상 메모리 주소를 가르키고 있습니다.)
추가적인 메모리 할당
프로세스가 새 메모리를 요구하면 커널은 새로운 메모리를 할당하여 대응하는 페이지 테이블을 작성
한 후 할당된 메모리(물리 주소)에 대응하는 가상 주소를 프로세스에 반환합니다.
메모리를 확보하기 위해 mmap()함수를 호출 합니다. mmap()함수는 페이지 단위로 메모리를 확보하기 때문에 메모리 풀을 만들고, malloc()함수 같은 메모리 확보 함수를 호출하여 메모리 풀에서 필요한 양만큼 바이트 단위로 잘라내어 메모리를 확보해야 합니다.
가상 메모리의 응용
가상 메모리를 응용한 다음과 같은 중요한 동작 방식이 있습니다.
- 파일 맵(file map)
- 디맨드 페이징(demand paging)
- Copy on Write 방식의 고속 프로세스 생성
- 스왑(swap)
- 계층형 페이지 테이블
- Huge page
파일 맵
일반적으로 프로세스가 파일에 접근할 때는 파일을 연 뒤에 read()
, write()
, lseek()
등의 시스템 콜을 사용합니다.
뿐만 아니라 파일의 영역을 가상 주소 공간에 메모리 매핑하는 기능이 있습니다.
mmap() 함수를 특정한 방법으로 호출하면 파일의 내용을 메모리에 읽어 들여 그 영역을 가상 주소 공간에
매핑할 수 있습니다.
매핑된 파일은 메모리 접근과 같은 방식으로 접근이 가능합니다.
디멘드 페이징
확보한 메모리 중에는 프로세스가 종료할 땨까지 사용하지 않는 영역이 존재합니다.
- 커다란 프로그램 중 실행에 사용하지 않는 기능을 위한 코드 영역과 데이터 영역
- glibc가 확보한 메모리 맵 중 유저가 malloc() 함수로 확하지 않은 부분
디맨드 페이징을 사용하면 프로세스의 가상 주소 공간 내의 각 페이지에 대응하는 주소는 페이지에 처음 접근할 때 할당됩니다.
프로세스를 생성할 때에는 프로세스의 가상 주소 공간 안에 코드 영역이나 데이터 영역에 대응하는 페이지에프로세스가 이 영역을 얻었음
이라는 정보를 기록합니다. 그러나 물리 메모리는 이 시점에는 할당되지 않았습니다.
이 다음에 프로그램이 엔트리 포인트로부터 실행을 시작할 때에는 엔트리 포인트에 대응하는 페이징용 물리 메모리가 할당됩니다.
처리 흐름은 다음과 같습니다.
- 프로그램이 엔트리 포인트에 접근합니다.
- CPU가 페이지 테이블을 참조해서 엔트리 포인트가 속한 페이지에 대응하는 가상 주소가 물리 주소에 아직 매핍되지 않음을 검출합니다.
- CPU에 페이지 폴트가 발생합니다.
- 커널의 페이지 폴트 핸들러가 1에 의해 접근된 페이지에 물리 메모리를 할당하여 페이지 폴트를 지웁니다.
- 사용자 모드로 돌아와서 프로세스가 실행을 계속합니다.
프로세스의 동적 메모리 확보 또한 비슷하게 동작합니다.
프로세스는 페이지 폴트의 발생여부를 알 수 없습니다.
Copy on Write
프로세스를 생성할 때, fork() 시스템 콜을 사용합니다. 이때 부모 프로세스의 메모리를 자식 프로세스에 전부 복사하지 않고 페이지 테이블만 복사합니다. 그 후, 부모 및 자식 페이지에 쓰기 권한을 무효화(쓰기가 불가능하도록)합니다.
페이지 테이블을 복사한 후, 다음과 같은 흐름으로 부모 및 자식 프로세스의 테이블을 업데이트 합니다.
- 어느 프로세스가 페이지에 쓰기를 시도합니다.
- 페이지에 쓰기는 허용하지 않았기 때문에, 쓰려고 할 때 CPU에 페이지 폴트가 발생합니다.
- CPU가 커널 모드로 변경되어 커널의 페이지 폴트핸들러가 동작합니다.
- 페이지 폴트 핸들러는 접근한 페이지를 다른 물리 메모리에 복사하고, 쓰려고 한 프로세스에 할당한 후 내용을 다시 작성합니다.
- 쓰기를 한 프로세스 쪽에 엔트리는 새롭게 할당된 물리 페이지를 매핍하여 쓰기를 허가합니다.
- 다른 쪽 프로세스의 엔트리에는 쓰기를 허락합니다.
스왑
메모리 부족(OOM) 상태가 되면 리눅스는 스왑을 합니다. 스왑은 저장 장치의 일부를 일시적으로 메모리 대신 사용하는 방식입니다.
구체적으로는 시스템의 물리 메모리가 부족한 상태에서 물리 메모리를 획득을 시도할 때, 기존에 사용하던 물리 메모리의 일부분을 저장 장치에 저장하여 빈 공간을 만드는 방식이 스왑입니다. 이때, 메모리의 내용이 저장된 영역을 스왑 영역이라고 합니다. 스왑 아웃된 메모리 주소는 커널 내의 스왑 영역 관리용 저장소에 기록합니다.
스왑은 디스크 저장소를 메모리처럼 사용하는 것이기 때문에, 스래싱(스왑핑이 많이 발생)되면 성능이 많이 떨어지게 됩니다.
댓글 남기기