산타는 없다

Window via C/C++ 13장 - 윈도우 메모리의 구조 본문

프로그래밍 서적/Window via C++

Window via C/C++ 13장 - 윈도우 메모리의 구조

LEDPEAR 2021. 11. 7. 16:46
반응형
  • 1. 프로세스의 가상 주소 공간
    • 모든 프로세스는 자신만의 가상 주소 공간을 가지고 있다. 32비트 프로세스는 32비트 포인터를 이용하여 0x00000000부터 0xFFFFFFFF까지 표현할 수 있기 때문에 4GB의 주소 공간을 가진다. 64비트 프로세스는 64비트 포인터가 0x0000000000000000부터 0xFFFFFFFFFFFFFFFF까지의 값을 가질수 있으므로 16EB(엑사바이트)의 주소공간을 가진다.
    • 모든 프로세스들은 자신만의 주소 공간을 가지기 때문에 특정 프로세스 내에서 스레드가 수행될 때 해당 스레드는 프로세스가 소유하고 있는 메모리에 대해서만 접근이 가능하다. 다른 프로세스에 의해 소유된 메모리는 숨겨져 있으며 접근이 불가능하다.
    • 서로 다른 프로세스는 동일한 값의 주소 공간을 가질 수 있다(가상이기 때문)
    • 이 주소 공간이 물리적인 저장소가 아닌 가상의 주소 공간이라는 사실을 기억해야 한다. 주소공간이란 단순히 메모리 의 위치를 지정할 수 있는 범위를 나타내는 값이다. 애플리케이션에서 접근 예외를 유발하지 않고 성공적으로 데이터에 접근하기 위해서는 접근하고자 하는 주소 공간에 물리적 저장소가 할당되거나 매핑되어 있어야만 한다.
  • 2. 가상 주소 공간의 분할
    • 각 프로세스의 가상 주소 공간은 분할되어 있으며, 각각의 분할 공간을 파티션이라고한다. 주소 공간의 분할 방식은 운영체제의 구현 방식에 따라 서로 다를 수 있으며, 윈도우 계열에서도 커널이 달라지면 조금씩 그 구조가 달라지곤한다.
    • 파티션 x86 32비트
      윈도우
      x86 32비트 윈도우,
      3GB 유저 모드
      x86 64비트 윈도우 IA-64
      64비트 윈도우
      NULL 포인터
      할당
      0x00000000~
      0x0000FFFF
      0x00000000~
      0x0000FFFF
      0x00000000'00000000
      0x00000000'0000FFFF
      0x00000000'00000000
      0x00000000'0000FFFF
      유저 모드 0x00010000~
      0x7FFEFFFF
      0x00010000~
      0xBFFEFFFF
      0x00000000'00010000
      0x000007FF'FFFEFFFF
      0x00000000'00010000
      0x000006FB'FFFEFFFF
      64KB 접근 금지 0x7FFF0000~
      0x7FFFFFFF
      0xBFFE0000~
      0xBFFFFFFF
      0x000007FF'FFFF0000
      0x000007FF'FFFFFFFF
      0x000006FB'FFFF0000
      0x000006FB'FFFFFFFF
      커널모드 0x80000000~
      0xFFFFFFFF
      0xC0000000~
      0xFFFFFFFF
      0x00000800'00000000
      0xFFFFFFFF'FFFFFFFF
      0x000006FC'00000000
      0xFFFFFFFF'FFFFFFFF
    • 32비트 윈도우 커널과 64비트 윈도우 커널은 매우 유사한 파티션 구조을 가지고 있다. 차이점이 있다면 각 파이션의 크기와 위치가 다를뿐이다.
    • ① 널 포인터 할당 파티션
      • 프로그래머가 NULL 포인터 할당 연산을 수행할 경우를 대비하기 위해 준비된 영역이다. 만일 프로세스의 특정 스레드가 이 파티션에 대해 읽거나 쓰기를 시도하게 되면 접근 위반(access violation)이 발생한다.
      • Win32 애플리케이션 프로그래밍 인터페이스(API)의 함수들은 이 파티션에 대해서 예약 조차도 허용하지 않는다.
    • ② 유저 모드 파티션
      • 프로세스의 주소 공간 내에서 활용될 수 있는 파티션이다. 프로세스는 다른 프로세스가 사용하는 유저 모드 파티션을 가리키는 포인터를 사용할 수 없으며, 이를 통한 읽기, 쓰기도 불가능하다.
      • 이 파티션은 모든 애플리케이션에 대해 프로세스가 유지해야하는 대부분의 데이터가 저장되는 영역이기도 하다. 각각의 프로세스는 데이터 저장을 위한 자신만의 파티션을 가지기 때문에 다른 프로세스의 영향으로 인해 데이터가 소실될 가능성은 거의 없으며, 이러한 특성은 시스템을 좀 더 안정적으로 동작할 수 있도록 해 준다.
      • 시스템은 이 파티션을 커널 코드, 디바이스 드라이버 콛, 장치 I/O 캐시 버퍼, 논페이지 풀 할당, 프로세스 페이지 테이블 등을 저장하기 위해 사용한다.
      • x86 윈도우에서 더 큰 유저 모드 파티션 획득하기
        • 2GB 이상의 유저 모드 파티을 확보하려면 부트 구성 데이터를 변경한 후 시스템을 재시작하면 된다. 최대 3GB로 확장할 수 있다.
        • 하지만 커널 주소공간이 2GB 이하로 줄어들게 돼 스레드의 개수나 스택의 크기 그리고 시스템이 생성할 수 있는 다른 리소스의 개수를 이전보다 줄일 수밖에 없다. 또한 기본 설정에서는 최대 128GB 램을 설치할 수 있는 것에 반해 이 경우 64GB 램밖에 설치할 수 없게 된다.
      • 64비트 윈도우에서 유저 모드 파티션으로 2GB만 사용하기
        • 32비트 애플리케이션을 64비트 환경으로 쉽게 포팅할 수 있는 방법을 많이 제공하고 있지만 단순히 애플리케이션을 다시 빌드하기만 해서는 문제가 해결되지 않고 에러가 발생할 수 있다. 이때, 시스템이 어떤 방식이로든 0x00000000'7FFFFFFF를 초과하는 메모리를 할당하지 않을 것임을 보증해 줄 수만 있다면 애플리케이션은 정상 동작할 수 있을 것이다.
        • 운영체제는 프로세스의 가용 주소 공간을 2GB이하로 제한하는 주소 공간 샌드박스(address space sandbox) 내에서 애플리케이션을 수행하는 방식을 통해 이러한 기능을 제공할 수 있다.
        • 기본적으로 64비트 애플리케이션을 구행하면 시스템은 유저 모드 주소 공간에서 사용되는 주소 값을 0x00000000'80000000 미만으로 하도록 하여 64비트 주소 공간의 하위 2GB에서만 메모리를 할당 받을 수 있도록 한다. 이것이 주소 공간 샌드박스다. 만일 64비트 애플리케이션이 전체 유저 모드 파티션을 사용하고자 한다면 반드시 /LARGEADDRESSAWARE 링커 스위치를 이용하여 링킹되어야 한다.
        • DLL 파일에 설정된 /LARGEADDRESSAWARE 값은 무시되므로 DLL은 전체 4TB의 유저 모드 파티션에서도 정상적으로 동작하도록 작성되어야만 하며, 그렇지 않을 경우 어떤 일이 발생할지에 대해서는 정의된 바 없다.
    • ③ 커널 모드 파티션
      • 운영체제를 구성하는 코드들이 위치하게 된다. 스레드 스케줄링, 메모리 관리, 파일시스템 지원, 네트워크 지원 등을 구현하는 코드와 모든 디바이스 드라이버들이 이 파이션에 로드된다. 이 파티션 영역에 존재하는 내용은 모든 프로세스에 의해 공유된다.
      • 비록 이 파티션이 모든 프로세스의 유저 모드 파티션의 상위에 존재하기는 하지만, 이 파티션 내의 코드와 데이터는 완벽하게 보호된다. 만일 애플리케이션에서 이 파티션에 대해 읽거나 쓰기를 시도하게 되면, 이러한 작업을 시도한 스레드는 접근 위반을 유발한다.
  • 3. 주소 공간 내의 영역
    • 프로세스가 생성되고 그에 따른 고유의 주소 공간이 주어지면, 대부분의 가용 주소 공간은 프리이거나 할당되지 않은 상태가 된다. 이러한 주소 공간을 사용하기 위해서는 먼저 VirtualAlloc 함수를 호출하여 영역(region)을 할당해야 한다. 이러한 작업을 영역을 예약(reserve)한다고 한다.
    • 주소 공간 상에 영역을 예약할 때에는 항상 영역의 시작 주소가 할당 단위(allocation granularity) 경계 상에 위치해야 한다. 할당 단위는 각 CPU별로 서로 상이할 수 있다. 하지만 이 책을 쓸 당시에는 모든 CPU가 64KB 할당 단위를 사용하고 있었다. 이는 각 영역의 시작 주소가 항상 64KB로 나누어 떨어지는 위치로부터 시작되어야 한다는 의미이다.
    • 주소 공간 상에 영역을 예약할 때 영역의 크기는 반드시 시스템의 페이지 크기의 배수로 설정해야한다. 페이지(page)란 운영체제가 메모리를 관리할 때 사용하는 최소 단위를 말한다. 할당 단위와 동일하게 페이지 크기는 CPU별로 서로 상이할 수 있는데, x86과 x64 시스템에서는 4KB의 페이지 크기를 사용하지만, IA-64에서는 8KB의 페이지 크기를 사용한다. 시스템은 자동적으로 사용자가 요청한 영역의 크기를 페이지 크기의 배수가 되도록 올림을 수행한다.
    • 때때로 시스템은 프로세스를 대신하여 주소 공간 상에 특별한 영역을 예약하기도 한다. 프로세스 환경블록(PEB)과 스레드 환경 블록(TEB)이 그렇다
    • 사용자가 새로운 주소 공간을 예약할 때에는 영역의 시작 주소를 할당 단위 경계(현재까지 모든 CPU에서는 64KB)로 맞출 것을 요구하지만, 운영체제 자체는 이와 같은 제한에서 자유롭다. 따라서 프로세스의 PEB나 TEB의 시작 주소는 64KB 경계로부터 시작하지 않을 수 있다. 그러나 영역의 크기는 여전히 CPU의 페이지 크기의 배수여야 하는 제한을 가지고 있다.
  • 4. 물리적 저장소를 영역으로 커밋하기
    • 주소 공간의 예약된 영역을 사용하기 위해서는 반드시 물리적 저장소를 할당해야 하며, 이후 할당된 저장소와 예약된 영역을 매핑해 주어야 한다. 이러한 절차를 물리적 저장소를 커밋(commit)한다고 한다. 물리적 저장소를 예약된 영역에 커밋하기 위해서는 VirtualAlloc 함수를 한 번 더 호출해 주어야 한다.
    • 물리적 저장소를 에약된 영역에 커밋할 때 예약된 영역 전체에 대해 물리적 저장소를 커밋할 필요는 없다.
    • 프로그램이 예약된 영역에 커밋된 물리적 저장소를 더 이상 사용할 필요가 없다면 물리적 저장소를 해제해야 한다. 이러한 절차를 물리적 저장소를 디커밋(decommint)한다고 하며, VirtualFree 함수를 사용하면 된다.
  • 5. 물리적 저장소와 페이징 파일
    • 예전의 운영체제에서는 물리적 자장소가 시스템에 탑재된 램의 크기를 의미하는 말로 사용되었다. 최근의 운영체제는 디스크 공간을 메모리처럼 활용할 수 있는 기능을 가지고 있다. 디스크 상에 존재하는 이러한 파일을 페이징 파일(paging file)이라고 하며, 모든 프로세스가 사용할 수 있는 가상의 메모리로 사용된다.
    • 가상의 메모리가 동작하기 위해서는 CPU 자체의 충분한 지원이 전제되어야만 한다. 스레드가 물리적 저장소에 있는 특정 바이트에 대해 접근을 시도했을 때, CPU는 접근하고자 하는 바이트가 램에 있는지 아니면 디스크에 있는지의 여부를 판단할 수 있어야 한다.
    • 애플리케이션 관점에서 보면 페이징 파일을 사용하면 애플리케이션이 사용할 수 있는 램(혹은 저장소)의 크기가 증가된 것과 같은 효과를 가져온다.
    • 운영체제는 CPU와 협력하여 페이징 파일에 애플리케이션이 필요로 하는 데이터가 있을 때 기존 램의 내용을 페이징 파일로 내보내고, 페이징 파일의 내용을 램으로 읽어 들인다. 페이징 파일을 활용하게 되면 애플리케이션을 사용할 수 있는 램의 공간이 외관상 늘어나는 것으로 보이는 것은 사실이지만, 페이징 파일을 사용할지의 여부는 여전히 사용자가 선택할 수 있다.
    • 프로세스 내의 스레드가 프로세스 주소 공간에 있는 데이터 블록에 접근을 시도하는 과정
      • 1. 스레드가 접근하고자 하는 데이터가 램에 존재하는 경우 수항된다. 이 경우 CPU는 데이터의 가상 메모리 주소를 메모리 내의 물리적 주소로 변경한 후 데이터에 접근하게 된다.
      • 2. 스레드가 접근하려는 데이터가 램에 존재하지 않으며, 페이징 파일의 어딘가에 위치하는 경우에 수행된다. 이 경우 스레드가 이 영역에 접근을 시도하면 페이지 폴트(page fault)를 일으키게 되고 CPU는 운영체제에게 이러한 사실을 전달하게 된다. 이때 운영체제는 램에서 프리 페이지를 찾게 되는데, 만일 프리 페이지가 존재하지 않는 경우 램에 있는 페이지 중 하나를 프리 상태로 변경해야 한다. 수정된적이 없다면 단순히 프리 상태로 변경하지만 수정된 적이 있다면 먼저 선택된 페이지의 내용을 램에서 페이징 파일로 복사한 후 프리 상태로 변경한다. 이러한 작업이 끈타고 나면 앞서 스레드가 접근하고자 했던 데이터를 페이징 파일로부터 프리 페이지로 가져온다. 이제 CPU는 앞서 페이지 폴트를 유발했던 명령을 다시 수행하게 되고, 가상 메모리를 물리적 램의 주소로 변경한 후 데이터에 접근하게 된다.
      • 램에 있는 내용을 페이징 파일에 쓰거나 반대로 페이징 파일의 내용을 램으로 가져오는 작업이 많아지면 많아질수록 하드 디스크 트레쉬가생기게 되고 시스템의 수행 속도는 점점 더 느려질 것이다.(thrashing : 운영체제가 프로그램을 수행하지 못하고 대부분의 시간을 페이지 파일과 램 사이의 스와핑에 소비하는 현상) 따라서 컴퓨터에 추가적으로 램을 설치하게 되면 스레싱 정도가 감소하여 시스템의 성능이 개선된다.
      • ① 페이지 파일 내에 유지되지 않는 물리적 저장소
        • 실제로 애플리케이션을 수행하면 시스템은 애플리케이션을 구성하는 .exe 파일을 열어서 애플리 케이션을 구성하는 코드와 데이터의 크기를 얻어낸다. 이후 프로세스의 주소 공간에 얻어낸 크기만큼 영역을 에약하고, 이 영역에 대한 커밋된 물리적 저장소를 .exe 파일 자체라고 설정한다. 
        • 시스템은 페이징 파일에 공간을 할당하는 대신 프로세스의 주소공간에 예약된 영역을 이처럼 이용함으로써 .exe 파일의 내용이나 이미지 등을 사용할 수 있게 된다. 이렇게 하면 애플리케이션은 좀 더 빠르게 로딩될 수 있고, 페이징 파일의 크기를 증가시키지 않고 그대로 유지할 수 있게 된다.
        • 하드 디스크 상에 존재하는 프로그램 파일(.exe나 DLL 파일)이 주소 공간의 특정 영역에 대한 물리적 저장소로 사용되는 경우, 이러한 파일을 메모리 맵 파일이라고 부른다. .exe나 DLL에 대해 로드를 시도하면 시스템은 자동적으로 프로세스의 주소 공간에 영역을 예약하고 해당 파일을 이 영역에 매핑한다. 추가적으로, 시스템은 주소 공간의 특정 영역에 프로그램 파일이 아닌 데이터 파일을 매핑할 수 있는 방법도 제공하고 있다.
        • 윈도우는 여러 개의 페이징 파일을 사용할 수 있다. 물리적으로 서로 다른 하드 디스크 상에 다수의 페이징 파일을 구성하며 시스템을 좀 더 빨리 구동할 수 있다. 왜냐하면 물리적으로 서로 다른 드라이브에 대해서는 동시에 읽고 쓰는 것이 가능하기 때문이다.
  • 8. 데이터 정렬의 중요성
    • 데이터 정렬의 문제는 운영체제의 메모리 구조와 관련이 있다기보다는 CPU의 구조와 관련되어 있다.
    • CPU는 데이터가 적절하게 정렬되어 있을 때 더욱 효율적으로 접근할 수 있다. 데이터가 저장되어 있는 메모리의 주소 값을 데이터의 크기로 나누었을 때 나머지가 0인 경우 데이터가 정렬되어 있다고 한다.(Alignment)
    • CPU가 메모리 상에 정렬되지 않은 데이터를 읽어오려 하면 두 가지 중 한가지 작업이 수행된다.
      • 첫째, 예외를 유발한다
      • 둘째, 정렬된 위치들을 여러 번 읽어서 정렬되지 않은 데이터를 모두 읽을 때까지 반복한다.
    • CPU가 메모리에 여러 번 접근하게 되면 속도에 영향을 주므로 애플리케이션이 최상의 성능을 발휘하도록 하려면 데이터가 적절히 정렬되도록 코드를 작성해야 한다.
    • 기본적으로 데이터 비정렬 폴트는 하드웨어가 직접 처리한다.
    • 컴파일러가 생성한 추가적인 코드를 통해 비정렬 문제를 해결하는 것이 CPU가 비정렬 데이터 접근 시 예외를 유발하고 그 예외를 운영체제가 처리하는 것에 비하면 훨씬 더 효율적이라 할 수 있다.
반응형
Comments