이제 [Linux Kernel] 시리즈의 마지막 챕터로, Memory Management에 대해 알아보자.
Memory Management
컴퓨터 시스템에서는 항상 물리적으로 존재하는 것보다 더 많은 양의 메모리를 필요해왔다. 이러한 물리적인 메모리의 한계를 극복하기 위해 여러 기법들이 개발되었는데, 이 중 가상 메모리(virtual memotry) 기법이 가장 성공적인 방법이라고 말할 수 있다. 가상 메모리는 다음과 같은 기능들을 제공한다.
- 넓은 주소공간 : 운영체제는 시스템이 실제로 가진 것(물리)보다 훨씬 많은 양의 메모리를 가지고 있는 것처럼 보이게 한다.
- 따라서 가상 메모리는 물리적 메모리모다 훨씬 더 클 수 있다.
- 보호 : 시스템의 각 프로세스는 각자 독립된 가상 주소공간을 갖는다.
- 프로세스들은 각자 완벽하게 분리되어 있어서, 각자가 다른 것에 영향을 줄 수 없다.
- 메모리 매핑 : 메모리 매핑은 파일을 프로세스 주소공간에 매핑하기 위해 사용된다.
- 파일의 내용은 프로세스 가상 주소 공간에 매핑된다.
- 공유 가상 메모리 : 만약, 프로세스들이 메모리를 공유하는 것이 필요하다면 이를 가능하게 해줄 수도 있다.
- 두 개 이상의 프로세스에게 공통적인 메모리를 줌으로써 프로세스 간 통신(IPC) 메커니즘으로 사용될 수 있음
Process Address space
- 코드를 컴파일하여 실행하게 되면, 메모리에 위 그림과 같이 올라가게 되고, 주소가 주어진다. 이러한 주소 영역들을 Process Address Space(프로세스 주소 공간)라고 부르며, 각 주소 영역을 memory areas(= VMA. virtual memory address)라고 부른다.
- 실제 Address Mapping은 다음과 같이 되어있음.
- 이러한 매핑된 주소의 정보는 PCB의 6가지 리소스 중 mm 구조체에 들어있음
- mm에 포인터가 있고, 이를 따라가보면 실제 메모리 관련 구조들을 찾을 수 있음. 따라서 mm에는 address space에 대한정보도 들어있음.
- 이를 통해 현 프로세스에서 사용중인 VMA 주소들(vm_area_struct)에 접근할 수 있음.
VMA(virtual memory address)
Process Address space 내용을 복습해보면, 현재 실행중인 프로세스에는 각각 PCB(task_struct)가 존재하고, PCB의 mm_struct의 mmap 필드를 따라가다 보면, vm_area_struct가 나온다. 그리고 여기에서 현재 프로세스가 사용중인 가상 메모리의 정보를 알 수 있다.
- PCB’s mm_struct → mmap field → vm_area_struct
- VMA들은 다음과 같은 내용(의 주소)들을 가지고 있다.
- Code : 기계어 (코드를 컴파일하여 기계어로 만들고 디스어셈블을 하여 어셈블리어로 바꾸어 code 영역에 넣는다(instruction))
- data : 전역변수, static 변수의 할당의 위해 존재하는 공간
- heap : 프로그래머의 동적 할당을 위해 존재하는 공간
- stack : 지역 변수가 저장되는 공간
- …
vm_area_struct들은 리스트로 연결되어 있으몀, 각 vm_area_struct는 다음과 같이 구성되어 있다.
- vm_start : 시작 주소
- vm_end : 끝 주소
- vm_ops : read, write 등을 하는 operation
- 이 영역에는 page mapping table과 관련된 정보도 들어있음
- vm_mm : 다시 mm_struct를 가리킴
- vm_next : 다음 vma 리스트를 가리킴
- vm_file : vma에 해당하는 파일(어떤 디스크)을 가리킴
페이징(Paging)
위에서 우리는 실제 컴퓨터 메모리는 한정적이기에, 넓은 주소를 가지는 가상 메모리가 등장하게 되었고, 가상 메모리는 실제 물리적 메모리보다 훨씬 더 클 수 있다고 배웠다. 그렇다면 실제 물리적인 (메인)메모리에 자신보다 큰 가상 메모리가 어떻게 (실제 메모리에) 올라올 수 있는 것일까?
- 먼저, 가상 메모리 시스템에서 가상 주소들은 모두 물리적 주소가 아니라 가상 주소이다. 이 가상 주소들은 운영체제가 관리하는 테이블들에 저장된 정보를 바탕으로 프로세서에 의해 물리적 주소로 변환된다.
- 물리적 주소로 변환되는 작업은 CPU 내 MMU(Memory Management Unit)라고 불리는 하드웨어가 수행한다.
- TLB에는 가장 최근에 변환한 페이지 테이블 엔트리를 정보가 들어있다.
- MMU는 변환하려는 가상주소를 TLB에서 먼저 검색한다. 올라와 있는 엔트리가 존재하면, 바로 물리 메모리로 주소 변한 후 원하는 데이터를 가져올 수 있다. 만약 TLB내에 올라와 있는 엔트리가 없다면 페이지 테이블을 참조해서 변환 과정이 일어난다.
- 이때, 이 변환을 쉽게 하기 위해 가상 메모리와 물리적 메모리는 작은 조각으로 나뉘게 된다.
- 왜 작은 조각으로 나눌까?
→ 만약 a.out의 main()
부터 실행한다고 하면, 전체 조각 중에서 이 main()
이 있는 조각만을 먼저 가져와서 참조할 수 있다. 즉, 현재 실행하려는 조각만 디스크에서 메모리로 가져오는 것이다.
- 그런데 만약 조각들의 크기가 천차만별이라면, 비효율적으로 메모리가 사용되게 되고 다음과 같은 단편화가 발생하게 될 수 있다.
- 내부 단편화 : 메모리 할당 시 프로세스보다 더 큰 공간을 할당받아서, 그 나머지 공간이 낭비되는 것
- 외부 단편화 : 메모리 할당 시 공간들이 연속적으로 붙어있지 않아 빈 공간(낭비)이 생기는 것
- 이러한 단편화 문제를 해결하기 위해 작은 조각들의 크기를 고정분할 방식으로 분할하는 것을 페이징(paging)기법이라고 말한다.
- 페이지(page) : 가상 메모리에서는 하나의 분할된 영역을 페이지라고 함
- 프레임(frame) : 실제 메모리에서는 하나의 분할된 영역을 프레임이라고 함
- 메모리에는 이러한 페이지를 관리하기 위한 table(page table)이 존재한다.
- page table은 다시 말해, 가상 주소를 물리적 주소로 변환시켜주기 위한 정보를 가지고 있는 테이블이다.
요구 페이징
위에서 메모리를 작은 조각(페이지)로 나누는 이유는 실제로 가상 메모리보다 훨씬 적은 물리적 메모리만 있기 때문에 현재 실행하려는 조각만 디스크에서 메모리로 가져와서 실행시키고자 하는 것이라고 이야기했었다. 이렇게 가상 메모리들이 접근되는 경우에만 메모리에 읽어들이는 기법을 요구 페이징(demand paging)이라고 말한다.
- 프로세스가 현재 메모리에 없는 가상 주소를 접근하려고 하면, 프로세서는 참조된 가상 페이지에 대한 페이지 테이블 엔트리를 찾을 수 없을 것이다.
- 이 시점에서 프로세서는 운영체제에게 page fault가 발생했다고 통보한다.
- page fault가 발생하면 운영체제는 해당하는 페이지를 디스크의 이미지로부터 메모리에 가져온다.
- 가져온 페이지는 빈 물리적 페이지 프레임에 기록된다.
- 가상 페이지 프레임 번호를 위한 엔트리가 프로세스의 페이지 테이블에 추가된다.
- 이후 page fault가 실행됐던 시점으로 돌아가서 나머지 일이 진행된다.
스와핑(swapping)
프로세스가 가상 페이지를 물리적 (메인)메모리에 가져와야 하는데, 비어 있는 물리적 페이지가 없다면, 운영체제는 물리적 메모리에서 다른 페이지를 제거하여, 가져올 페이지를 위해 공간을 마련해야 한다.
- 리눅스에서는 시스템에서 제거될 페이지를 공정하게 선택하기 위해 LRU 알고리즘을 사용한다.
- ** LRU(least, recently, uses - 최근최소사용) : 필요한 것만 ram으로 가져오고 오래 안쓰는건 하드디스크에 내려놓는다.
- 만약 물리적 메모리에서 제거될 페이지가 이미지나 데이터 파일에서 온 것이고, 이 페이지에 쓰여진 것이 없다면, 페이지의 내용을 저장할 필요가 없다. 대신 그냥 제거를 하고, 나중에 다시 필요하게 되면 이미지나 데이터 파일로부터 다시 메모리에 읽어들이면 된다.
- 그러나, 페이지가 변경되었다면, 운영체제는 페이지의 내용을 나중에 다시 사용할 수 있도록 보존해야 한다. 이런 페이지를 더티 페이지(dirty page)라고 하며, 이를 메모리에서 제거하기 전에 스왑 파일(swap file)이라는 특별한 파일에 저장한다.
page table
page table에 대해서 좀 더 살펴보자.
- 페이지 사이즈가 4KB라고 가정해보자.
- 32bit 시스템에서는 4KB의 페이지에 접근하기 위해선, (2^12이 4096이므로) 대략 12bit가 필요하다. 그럼 나머지 20bit(32 - 12)는 현재 찾으려는 page가 어디있는지에 사용되게 된다.
- 동일하게, 64bit시스템에서는 52bit(64-12)가 현재 찾으려는 page가 어디있는지에 사용되게 된다.
- 다시 말해, (32bit의 경우) 12bit로는 실제 페이지의 특정 바이트를 가져오고, 20bit로는 페이지 주소를 지칭한다.
- 즉, 32bit 시스템에서는 page table을 위해 20bit(2^20)의 페이지(엔트리)를 거느릴 수 있다.
- 64bit 시스템에서는 page table을 위해 52bit(2^52)의 페이지(엔트리)를 거느릴 수 있다.
- 그러나 이러한 엔트리로 각각의 프로세스를 관리하는 것은 너무 큰 낭비이다.
- 사용되는 부분을 제외하면 나머지는 다 낭비되고 있기 때문
리눅스에서는 위와 같은 문제를 해결하기 위해 다음과 같은 방식을 사용한다.
- 32bit라고 했을 떄, 2^20의 엔트리를 가진다고 했었다.
- 이떄, 20bit로 사용되던 부분을 절반으로 나누고, 나눈 10bit를 특별한 용도로 사용한다.
- 이것이 위 그림에서 Dir_no(10)이다.
- 10bit이므로, 1024 개의 엔트리를 가지는 테이블에 실제 사용되고 있는 엔트리에만 해당 세그먼트의 정보를 담는다.
- 즉, 사용하는 세크먼트들만 2^10개의 엔트리를 할당해주고, 사용하지 않는 엔트리는 생성하지 않아 낭비를 줄이게 된다.
🔥 메모리 관리(흐름) 총 정리
- 프로세스마다 PCB가 존재하고, 그 PCB는 6개로 구성된다. 이중 mm 구조체에는 메모리와 관련된 정보가 들어있다.
- mm구조체에는 여러 필드들이 있는데, 그 중 mmap 필드를 따라가다 보면 vm_area_struct가 나온다.
- 이 vm_area_struct에서는 현재 프로세스가 사용중인 가상 메모리 정보를 알 수 있다.
- vm_area_struct의 필드에는 시작 주소, 끝 주소, 권한, 파일명 등의 정보가 들어있다.
- 또한 mm구조체에는 pgd(page directory. 가상메모리 -> 물리메모리 매핑에 사용되는 페이지 테이블의 주소.)가 있다.
- pgd에는 2^10(32bit 기준) 사이즈의 엔트리를 갖는 디렉토리가 있다.
- pgd의 디렉토리의 비어있지 않은 세그먼트에는 각각 2^10의 엔트리를 갖는 page table 엔트리가 있다.
- 프로세스가 현재 메모리에 없는 가상 주소를 접근하려고 하면, 프로세서는 참조된 가상 페이지에 대한 페이지 테이블 엔트리를 찾을 수 없을 것이다. 이를 page fault가 났다고 한다.
- page fault 시, 물리적 메모리 공간이 있으면, 운영체제는 해당하는 페이지를 디스크의 이미지로부터 메모리로 가져온다.
- 가져온 페이지는 빈 물리적 페이지 프레임에 기록된다.
- 가상 페이지 프레임 번호를 위한 엔트리가 프로세스의 테이블에 추가된다.
- 만약, 물리적 메모리에 공간이 없다면 LRU 알고리즘으로 공간을 마련한다.
- 이떄, 페이지가 변경되어 페이지의 내용을 나중에 다시 사용할 수 있도록 보존해야 한다면, 스왑 파일이라는 특별한 파일로 디스크에 저장한다.
Reference