커널 자체의 메모리를 관리하는 것 외에 커널은 사용자 공간 프로세스의 메모리 관리도 해야한다.
프로세스 주소 공간 : 시스템의 각 사용자 공간 프로세스에 주어진 메모리를 나타내는 영역
리눅스는 가상 메모리 OS이므로 메모리 자원은 시스템 프로세스에 대해 추상화된다. 각 프로세스는 자신이 혼자 시스템의 물리적 메모리 전체를 가지고 있는 것처럼 보게 된다.
메모리 영역에는 다음과 같은 것들이 들어있을 수 있다.
- Text 영역 : 실행 파일 코드가 할당된 메모리
- Data 영역 : 실행 파일의 초기값이 있는 전역 변수가 할당된 메모리
- BSS 영역 : 초기값이 없는 전역 변수가 들어있는 제로 페이지가 할당된 메모리
메모리 서술자
struct mm_struct를 이용해 프로세스의 메모리 주소 공간을 표현한다.
메모리 서술자 할당
current->mm은 현재 프로세스의 메모리 서술자
fork() 함수를 실행하는 동안 copy_mm() 함수가 부모 프로세스의 메모리 서술자를 자식 프로세스로 복하한다.
mm_struct 구조체와 커널 스레드
커널 스레드에는 프로세스 주소 공간이 없기 때문에 메모리 서술자도 없다.
따라서 커널 스레드 프로세스 서술자의 mm 항목은 NULL이 된다.
가상 메모리 영역
리눅스 커널에서 메모리 영역은 가상 메모리 영역(VMA)라고 부르는 경우가 많다.
각 메모리 서술자는 프로세스 주소 공간 내의 고유한 구간을 나타낸다.
vm_start : 해당 구간의 시작(가장 낮은) 주소
vm_end : 해당 구간의 마지막(가장 높은) 주소의 바로 다음 바이트를 가리킴.
vm_mm : VMA(가상메모리영역)에 해당하는 mm_struct(메모리서술자)를 가리킴.
프로세스별 다른 주소 공간을 갖는 이유
VMA 별로 고유한 mm_struct를 가지고 있다. 따라서 두 개의 별도 프로세스가 같은 파일을 각자의 주소 공간에 할당할 경우 각자 별도의 vm_area_struct를 통해 각자의 메모리 공간을 식별한다.
반면, 주소 공간을 공유하는 두 스레드는 모든 vm_area_struct 구조체를 공유한다.
vm_flags
- 메모리 영역의 동작, 메모리 영역이 들어 있는 페이지에 대한 정보, 메모리 영역 전체에 대한 정보 제공
- VM_SHARED : 메모리 영역이 여러 프로세스가 공유하는 할당인지 나타냄. (shared mapping <-> private mapping)
- VM_IO : 보통 장치 드라이버가 입출력 공간에 대해 mmap() 함수를 호출했을 때 설정됨. 메모리 영역이 프로세스의 코어 덤프에 들어가지 않도록 함.
- VM_SEQ_READ : App이 해당 영역에 대해 순차적인(즉 일련의 연속된) 읽기 동작을 수행한다는 사실을 커널에 알려줌. 그러면 커널은 파일 내용을 미리 읽는 정도를 늘리는 선택을 할 수 있음. (Sequential read가 많으면 readahead cache size를 늘리는 것이 성능에 유리한 맥락)
VMA 동작
vm_area_struct 구조체는 모든 유형의 메모리 영역에 사용할 수 있는 범용 객체 역할을 하며, 동작 테이블을 통해 특정 객체 인스턴스에 대한 구체적 동작을 작성한다. -- C를 객체지향언어 처럼 사용.
실제 메모리 영역
/proc 파일시스템이나 pmap(1) 유틸리티를 사용해 살펴볼 수 있음.
/proc/<pid>/maps 파일은 프로세스 주소 공간의 메모리 영역을 출력해준다.
pmap(1) 유틸리티는 이 정보를 더 보기 편하게 바꿔준다. $ pmap <pid>
C라이브러리처럼 읽기 전용 할당 공간은 메모리에 한 벌만 읽어 놓고 모든 프로세스가 함께 사용한다. 따라서 물리 메모리 공간도 한번 읽는 만큼만 사용한다.
do_mmap() : 프로세스의 주소 공간에 주소 범위를 추가하기 위해 사용하는 함수
가능한 경우, 새로 할당된 범위는 인접 메모리 영역과 병합된다. 병합이 불가능하면 vm_area_cachep 슬랩캐시에서 새로운 vm_area_struct 구조체를 할당하고, vma_link() 함수를 이용해 새로운 메모리 영역을 주소 공간의 연결 리스트와 레드블랙 트리에 추가한다.
페이지 테이블
필요성 : 애플리케이션은 물리적 주소가 할당된 가상 메모리를 사용하지만, 프로세서는 직접적인 물리적 주소를 기반으로 동작한다. 따라서 애플리케이션이 가상 메모리 주소에 접근할 때는 프로세서가 요청을 처리하기 전에 가상 메모리 주소를 물리적 주소로 변환해야 한다. 이 변환 작업은 페이지 테이블을 통해 처리된다.
테이블은 다른 테이블을 가리키거나 테이블에 해당하는 물리적 페이지를 가리킨다.
잘게 분산된 주소 공간을 잘 처리하기 위해 세 단계로 구성한다. 일종의 '최대 공약수' 같은 개념이다.
- 최상위 페이지 테이블 : 전역디렉토리 (PGD-Page global directory)로 pgd_t형 배열. PGD의 항목은 두번째 단계 디렉토리인 PMD 항목을 가리킴
- 두 번째 단계 페이지 테이블은 페이지 중간 디렉토리 (PMD-page middle directory) pmd_t형 배열. PTE 항목을 가리킴
- 마지막 단계는 간단히 '페이지 테이블'이라고 부르며 pte_t형으로 구성. 물리적 페이지를 가리킨다.
거의 모든 가상 메모리의 페이지 접근은 그에 해당하는 물리적 주소 변환이 필요하기 때문에 페이지 테이블의 성능은 매우 중요하다. 안타깝게도 메모리의 모든 주소를 빠르게 탐색할 수는 없다.
이 문제를 해결하기 위해 대부분 프로세서에 TLB(변환참조버퍼-translation lookaside buffer)라는 것이 구현되어, 가상-물리 주소 변환 정보를 하드웨어적으로 캐시하는 역할을 한다.
가상 주소에 접근할 때, 프로세서는 먼저 해당 변환 정보가 TLB에 캐시 되어 있는지 확인한다. 캐시되어 있으면 바로 물리적 주소를 변환한다. 캐시 되어 있지 않으면, 해당 물리적 주소를 찾기 위해 페이지 테이블을 참조한다.