프로세스의 가상 주소 공간

 모든 프로세스는 자신만의 가상 주소공간을 가지는데 32비트 프로세스는 32비트 사이즈의 주소공간을 가지고

 64비트 프로세스는 64비트 사이즈의 주소공간을 가집니다.

프로세스의 주소크기 가상 주소 범위
32비트 0x00000000 ~ 0xFFFFFFFF (4GByte) 
64비트 0x00000000'00000000 ~ 0xFFFFFFFF'FFFFFFFF (16EByte)

 프로세스의 가상 주소 공간은 개별 프로세스의 메모리에 대한 독립성을 제공합니다.

프로세스별 가상주소

 예를 들어 A프로세스가 0x12341234라는 주소에 자신만의 데이터를 저장하고 B프로세스도 0x12341234에 데이터를

 저장 할 수 있습니다. 프로세스들은 서로간의 가상주소공간에 접근할 수 없으며 프로세스의 속한 스레드는 

 자신이 속한 프로세스의 가상주소공간에만 접근할 수 있습니다. 

 실제로 프로세스의 가상주소공간을 접근하여 사용하기 위해서는 물리적 저장소를 할당하고 맵핑 되어 있어야 합니다.

가상 주소 공간의 분할

 각 프로세스의 가상 주소 공간은 파티션이라고 하는 공간으로 분할 되어 있습니다. 

파티션 x86 32비트 윈도우 x64 64비트 윈도우
NULL 포인터 할당 0x00000000 ~ 0x0000FFFF 0x00000000'00000000 ~ 0x00000000'0000FFFF
유저 모드 0x00010000 ~ 0x7FFEFFFF 0x00000000'00010000 ~ 0x000007FF'FFFEFFFF
64KB 접근 금지 0x7FFF0000 ~ 0x7FFFFFFF 0x000007FF'FFFF0000 ~ 0x000007FF'FFFFFFFF
커널 모드 0x80000000 ~ 0xFFFFFFFF 0x00000800'00000000 ~ 0xFFFFFFFF'FFFFFFFF

널 포인터 할당 파티션

 0x00000000 ~ 0x0000FFFF(크기 64KB) 주소공간의 파티션은 프로그래머가 NULL 포인터 할당을 연산을 수행할 경우를

 위해서 준비된 영역입니다.

    DWORD* pdwPointer = nullptr;
    // NULL 포인터 할당 파티션 접근!!
    *pdwPointer = 1000;

 만일 프로세스의 특정 스레드가 이 파티션 주소공간에 대해서 읽거나 쓰기를 시도하면 접근 위반이 발생합니다. 

 우리가 개발하면서 가장 많이 접하는 에러중에 하나입니다.

    DWORD* pdwPointer = new DWORD;
    // 문제없이 동작할 것 같지만 해당 크기의 메모리를 할당하지 못할 경우 NULL을 리턴합니다.
    *pdwPointer = 1000;

유저 모드 파티션

 해당 파티션의 주소공간은 프로세스내에서 사용될 수 있습니다. 

CPU 아키텍처 주소 범위 사용가능한 유저모드 크기
x86 0x00010000 ~ 0x7FFEFFFF ~ 2GB
x86 w/3GB 0x00010000 ~ 0xBFFEFFFF ~ 3GB
x64 0x00000000'00010000 ~ 0x000007FF'FFFEFFFF ~ 8192GB
IA-64 0x00000000'00010000 ~ 0x000006FB'FFFEFFFF ~ 7152GB

 CPU의 설계 구조에 따라서 유저모드의 크기가 달라지는데 x86은 32비트를 지원하는 CPU입니다.

 x64와 IA-64는 64비트의 머신을 지원하는 CPU인데 차이점은 x64는 x86을 기반으로 64비트를 지원하도록

 확장된 버전이고 IA-64은 64비트 구조를 지원하기 위해서 새로 만들어진 설계 구조입니다. 

 이러한 이유로 x86을 이어받은 x64는 가정용과 기업용으로 도입 될 수 있었고 IA-64는 기업용 서버/워크스테이션으로

 제공되고 있습니다. 

 프로세스가 사용하는 모든 메모리는 이곳에 적재되어 사용되어 집니다. 이렇게 프로세스만의 주소공간이 할당되는

 덕분에 다른 프로세스의 영향으로 메모리 변경되는 상황이 발생되지 않아서 프로세스가 안정적으로 동작할 수

 있도록 지원합니다. 모든 .exe와 DLL 모듈도 이 파티션에 로드 됩니다. 

 x86의 윈도우에서는 2GB보다 더 큰 유저 모드 파티션을 획득 할 수 있는데 최대 3GB 크기까지 할당 받을 수 있습니다.

 관리자 권한으로 명령 프롬프트를 실행한뒤에 아래와 같이 실행하면 됩니다.

// 유저모드 파티션 크기 설정
bcdedit /set IncreaseUserVa 3072

// 초기 값으로 원복하기
bcdedit /deletevalue IncreaseUserVa 

// 셋팅된 환경변수 확인하기
bcdedit /enum

추가적으로 어플리케이션의 링크 스위치를 지정해야 확장된 유저 모드 파티션으로 동작합니다.

https://docs.microsoft.com/ko-kr/cpp/build/reference/largeaddressaware-handle-large-addresses?view=vs-2019

 (추가되는 주소공간은 커널모드 파티션에서 가져오게 되어 커널이 사용할 수 있는 리소스가 줄어듭니다.)

커널 모드 파티션

 해당 파티션의 주소 공간은 운영체제를 구성하는 코드들이 위치 하게 됩니다. 

 스레드 스케줄링, 메모리 관리, 파일시스템 지원, 네트워크 지원들을 구현하는 모든 코드와 모든 디바이스 드라이버들이

 커널 모드 파티션에 로드됩니다. 이 파티션의 코드와 데이터는 완벽하게 보호됩니다.

 어플리케이션에서 이 주소공간에 읽거나 쓰기를 시도하게 되면 접근 위반을 발생시킵니다. 

프로세스 주소 공간와 물리적 저장소 

 프로세스가 생성되고 프로세스의 주소 공간이 주어지면 대부분의 가용 주소 공간은 프리(free)이거나 

 할당되지 않는 상태 입니다. 이러한 주소공간을 사용하기 위해서는 VirtualAlloc함수를 사용해서 예약하거나

 커밋할 수 잇습니다. 

물리적 저장소를 예약 작업

 프로세스의 주소공간 상에 예약을 할 때에는 영역의 크기는 시스템의 페이지 크기의 배수로 설정해야합니다.

 페이지(Page)란 운영체제가 메모리를 관리하기 위한 최소 단위를 말합니다. 

 페이지 크기는 CPU별로 다른데 x86이나 x64에서는 4KB의 페이지 크기를 사용하고 IA-64에서는 8KB를 사용합니다.

 예를 들어서 주소 공간에 17KB 크기의 영역을 예약하려고 한다면 x86이나 x64 시스템에서는 20KB(5페이지)의 영역을

 예약할 것이고 IA-64 시스템이라면 24KB(3페이지)로 예약할 것입니다. 

물리적 저장소의 커밋 작업

 예약된 주소 공간을 사용하기 위해서는 반드시 물리적 저장소를 할당하고 할당된 저장소와 예약된 영역을 매핑

 시켜줘야하는데 이러한 작업을 물리적 저장소의 커밋(Commit)이라고 합니다.

 물리적인 저장소를 예약된 영역에 커밋 할 때에는 영역 전체에 대해서 커밋 할 필요는 없습니다.

 예를 들어서 x64머신에서 20KB의 예약된 영역이 있다고 하면 5개의 페이지로 구성됩니다. 

 커밋 작업은 페이지 단위로 진행할 수 있는데 1번, 3번 페이지만 커밋을 진행 할 수 있습니다. 

 소스코드를 통한 물리적 저장소 예약 및 커밋

// 2KB의 메모리를 예약합니다.
LPVOID lpvResult = VirtualAlloc(NULL, 1024 * 2, MEM_RESERVE, PAGE_READWRITE);

// 2KB의 메모리를 물리적 저장소와 커밋합니다.
if(lpvResult)
	lpvResult = VirtualAlloc(lpvResult, 1024 * 2, MEM_COMMIT, PAGE_READWRITE);

 VirtualAlloc함수로 예약 작업이 수행 되었을 당시의 lpvResult 메모리를 확인해보면 어플리케이션에서 아직 메모리가 

 할당되지 않는 것을 확인 할 수 있습니다.

VirtualAlloc(NULL, 1024 * 2, MEM_RESERVE, PAGE_READWRITE) 수행후 메모리

 VirtualAlloc함수로 Commit을 수행을 하게 되면 할당 사이즈를 페이지 단위로 할당받을 수 있습니다.

 예제에서는 2KB를 요청 하였지만 실제로는 페이지 크기인 4KB를 할당 받았습니다.

 커밋까지 완료된 메모리를 접근이 완료되면 어플리케이션에서 메모리를 사용할 수 있습니다.

VirtualAlloc(lpvResult, 1024 * 2, MEM_COMMIT, PAGE_READWRITE) 수행 후 메모리

 메모리를 더이상 사용하지 않게 되면 VirtualFree 함수를 호출해서 메모리를 해제 해야 합니다.

// MEM_RELEASE은 디 커밋 및 영역을 해제 합니다. 
if (!VirtualFree(lpvResult, 0, MEM_RELEASE))
{
    std::cout << "Fail VirtualFree " << GetLastError() << std::endl;
}

물리적 저장소와 페이징 파일

 일반적으로 물리적 저장소라고 하면 Ram 메모리만 생각하게 되는데 운영체제는 디스크의 공간을 메모리처럼

 활용할 수 있습니다. 디스크상에 이러한 용도로 사용되는 파일을 페이징 파일이라고 하며 모든 프로세스가 

 사용 할 수 있는 가상의 메모리로 사용 됩니다.

 예를 들어서 컴퓨터에 8GB의 램이 있고 하드 디스크에 8GB의 페이징 파일이 있다면 수행 중인 어플리케이션은

 컴퓨터에 16GB에 램이 있는 것으로 판단 합니다. 실제로 16GB이 존재하는건 아니고 운영체제가 CPU와 협력해서 

 페이징 파일에 어플리케이션이 필요한 데이터가 있을 때 기존의 램의 내용을 페이징 파일로 내보내고 그 공간에

 페이징 파일의 내용을 읽어 들이도록 동작합니다. 

 프로세스내의 스레드가 프로세스의 주소 공간에 접근 할 때 발생되는 내용을 간단히 단순화해서 그림으로 만들어

 보았습니다.

가상 주소를 물리적 주소로 변경하는 작업

 위의 내용에서 보듯이 가용할 수 있는 램의 메모리가 부족한 상황이 오게 된다면 램에 있는 내용을 페이징 파일로

 쓰거나(Swap out) 페이징 파일의 내용을 램으로 가져오는 작업(Swap in)이 많아지고 하드 디스크의 트레쉬가

 발생하게 되고 시스템의 수행속도는 느려지게 됩니다. 

 이럴 때에는 램을 추가해서 성능상 개선을 이룰 수 있습니다. 

페이징 파일 크기 설정하는 방법

 페이징 파일을 셋팅하는 방법은 내 PC를 우클릭해서 "속성" -> "고급 시스템 설정" -> "고급" 을 이동해서

 위 그림과 같이 셋팅할 수 있습니다

+ Recent posts