Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

C# - Windows 운영체제의 2MB Large 페이지 크기 할당 방법

다음의 글이 있군요. ^^

What are the page sizes used by Windows on various processors?
; https://devblogs.microsoft.com/oldnewthing/20210510-00/?p=105200

Page 크기는 CPU 종류에 의해 결정된다고 합니다.

Processor Page size Reasonable choices
Normal Large
x86-32 without PAE 4KB 4MB 4KB only
x86-32 with PAE 4KB 2MB 4KB only
x86-64 4KB 2MB 4KB only
SH-4 4KB 1KB, 4KB
MIPS 4KB 1KB, 4KB
PowerPC 4KB 4KB only
Alpha AXP 8KB 8KB, 16KB, 32KB
Alpha AXP 64 8KB 8KB, 16KB, 32KB
Itanium 8KB 4KB, 8KB
ARM (AArch32) 4KB N/A 1KB, 4KB
ARM64 (AArch64) 4KB 2MB 4KB only

위의 표에 보면 Normal 페이지의 크기를 선택할 수 있는 CPU(SH-4, MIPS, Alpha AXP/64, Itanium, ARM)들이 있는데 현재는 윈도우에서 지원하고 있지 않는 CPU들입니다. 또한 그중에서도 MIPS와 ARM(AArch32)의 경우 1KB를 지원하는 방식이 원래의 4KB 페이지를 분할하는 1KB subpages 방식으로 지원했다고 합니다. ARM의 경우 이후 ARM64(AArch64)에 와서는 그런 지원을 없앴습니다.

또한, Large 페이지에 "—" 표시가 있는 것들은 해당 CPU들이 Large Page를 지원하긴 하지만 당시 윈도우에 그 기능을 넣기도 전에 지원이 중단되었다고 합니다. 반면, ARM(AArch32)의 경우 "Lage Page" 자체를 CPU 차원에서 미지원하는 경우였고.

우리가 흔히 아는 x86/x64 프로세서 모두 Normal 4KB, Larget 2MB입니다. (근래의 윈도우 운영체제는 모두 DEP를 위해 PAE가 적용되었습니다.)

현재 현실적으로 사용할 수 있는 윈도우 운영체제가 x86/x64/ARM64를 지원하기 때문에 그냥 모든 페이지가 Normal 4KB, Large 2MB라고 간단하게 기억해 두시면 되겠습니다. 그런데, 여기까지 읽었는데도 혹시 윈도우에서 페이지 크기를 바꾸는 것이 가능할지 궁금한 분이 계실까요? ^^ 처음에도 말했지만, Raymond Chen은 다음의 글에서도 답을 하고 있습니다.

Is there a way to change the minimum size for large pages?
; https://devblogs.microsoft.com/oldnewthing/20160614-00/?p=93665

The large page minimum size isn’t chosen by Windows. It’s chosen by the processor.
...
So if you want to have custom sizes for large pages, you need to start by asking the processor manufacturers.





윈도우 운영체제의 페이지 크기는 다음과 같은 코드로 구할 수 있습니다.

using System.Runtime.InteropServices;

internal class Program
{
    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern void GetSystemInfo(ref SYSTEM_INFO Info);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern UInt32 GetLargePageMinimum();

    static void Main(string[] args)
    {
        SYSTEM_INFO si = new SYSTEM_INFO();
        GetSystemInfo(ref si);

        uint largePageSize = GetLargePageMinimum();

        Console.WriteLine($"normal page granularity: 0x{si.dwPageSize:x8}");
        Console.WriteLine($"large page granularity: 0x{largePageSize:x8}");
    }
}

[StructLayout(LayoutKind.Sequential)]
internal struct SYSTEM_INFO
{
    internal ushort wProcessorArchitecture;
    internal ushort wReserved;
    internal uint dwPageSize;
    internal IntPtr lpMinimumApplicationAddress;
    internal IntPtr lpMaximumApplicationAddress;
    internal IntPtr dwActiveProcessorMask;
    internal uint dwNumberOfProcessors;
    internal uint dwProcessorType;
    internal uint dwAllocationGranularity;
    internal ushort wProcessorLevel;
    internal ushort wProcessorRevision;
}

실행해 보면,

normal page granularity: 0x00001000 // == 4KB
large page granularity: 0x00200000 // == 2MB

예상했던 결과를 구할 수 있습니다. 해본 김에, large page 할당도 호출해 볼까요? ^^ 이를 위해 간단하게 다음과 같이 코딩을 할 수 있는데요,

using System.Runtime.InteropServices;

internal class Program
{
    // ...[생략]...

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr VirtualAlloc(IntPtr lpAddress, UIntPtr dwSize, AllocationType flAllocationType,
          MemoryProtection flProtect);

    [Flags()]
    private enum AllocationType : uint
    {
        COMMIT = 0x1000,
        RESERVE = 0x2000,
        LARGE_PAGES = 0x20000000,
    }

    [Flags()]
    public enum MemoryProtection : uint
    {
        EXECUTE_READWRITE = 0x40,
    }

    static void Main(string[] args)
    {
        // ...[생략]...

        uint largePageSize = GetLargePageMinimum();

        var ptr = VirtualAlloc(IntPtr.Zero, largePageSize,
               AllocationType.COMMIT | AllocationType.RESERVE | AllocationType.LARGE_PAGES,
               MemoryProtection.EXECUTE_READWRITE);
        if (ptr == IntPtr.Zero)
        {
            Console.WriteLine($"Error: {Marshal.GetLastWin32Error()}");
            return;
        }
    }

    // ...[생략]...
}

실행해 보면, 1314 오류 코드(A required privilege is not held by the client.)가 나옵니다. 왜냐하면 이에 대해서는 특권이 필요한데요, 관련해서 아래의 문서에 잘 나와 있습니다.

Large-Page Support
; https://learn.microsoft.com/en-us/windows/win32/memory/large-page-support

Creating a File Mapping Using Large Pages
; https://learn.microsoft.com/en-us/windows/win32/memory/creating-a-file-mapping-using-large-pages

그러니까, 우선 SeLockMemoryPrivilege 특권을 AdjustTokenPrivileges API를 이용해 활성화해야 하므로, 이전에 구현했던 C# 코드를 이용해 다음과 같이 코드를 추가해 줍니다.

using System;
using System.Runtime.InteropServices;

internal class Program
{
    // ...[생략]...

    [DllImport("advapi32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, [MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges,
        ref TOKEN_PRIVILEGES NewState, UInt32 Zero, IntPtr Null1, IntPtr Null2);

    [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool LookupPrivilegeValue(string? lpSystemName, string lpName, out LUID lpLuid);

    [DllImport("advapi32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool OpenProcessToken(IntPtr ProcessHandle, UInt32 DesiredAccess, out IntPtr TokenHandle);
    
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GetCurrentProcess();

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool CloseHandle(IntPtr hObject);

    public const string SE_LOCK_MEMORY_PRIVILEGE = "SeLockMemoryPrivilege";
    public const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002;

    [StructLayout(LayoutKind.Sequential)]
    public struct LUID
    {
        public UInt32 LowPart;
        public Int32 HighPart;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct TOKEN_PRIVILEGES
    {
        public UInt32 PrivilegeCount;
        public LUID Luid;
        public UInt32 Attributes;
    }

    // ...[생략]...

    private static uint TOKEN_QUERY = 0x0008;
    private static uint TOKEN_ADJUST_PRIVILEGES = 0x0020;

    static void Main(string[] args)
    {
        // ...[생략]...

        if (SetLargePagePrivileges() == false)
        {
            return;
        }

        var ptr = VirtualAlloc(IntPtr.Zero, largePageSize,
               AllocationType.COMMIT | AllocationType.RESERVE | AllocationType.LARGE_PAGES,
               MemoryProtection.EXECUTE_READWRITE);
        if (ptr == IntPtr.Zero)
        {
            Console.WriteLine($"Error: {Marshal.GetLastWin32Error()}");
            return;
        }
    }

    // ...[생략]...
    // Some remarks on VirtualAlloc and MEM_LARGE_PAGES
    // ; https://devblogs.microsoft.com/oldnewthing/20110128-00/?p=11643
    private static bool SetLargePagePrivileges()
    {
        IntPtr hToken;
        LUID luidSeLockMemoryPrivilege;
        TOKEN_PRIVILEGES tkpPrivileges;

        if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hToken))
        {
            Console.WriteLine("OpenProcessToken() failed, error = {0} . SeLockMemoryPrivilege is not available", Marshal.GetLastWin32Error());
            return false;
        }

        if (!LookupPrivilegeValue(null, SE_LOCK_MEMORY_PRIVILEGE, out luidSeLockMemoryPrivilege))
        {
            Console.WriteLine("LookupPrivilegeValue() failed, error = {0} .SeLockMemoryPrivilege is not available", Marshal.GetLastWin32Error());
            CloseHandle(hToken);
            return false;
        }

        tkpPrivileges.PrivilegeCount = 1;
        tkpPrivileges.Luid = luidSeLockMemoryPrivilege;
        tkpPrivileges.Attributes = SE_PRIVILEGE_ENABLED;

        if (!AdjustTokenPrivileges(hToken, false, ref tkpPrivileges, 0, IntPtr.Zero, IntPtr.Zero))
        {
            Console.WriteLine("AdjustTokenPrivileges() failed, error = {0} .SeLockMemoryPrivilege is not available", Marshal.GetLastWin32Error());
            return false;
        }

        int result = Marshal.GetLastWin32Error();
        if (result != 0)
        {
            Console.WriteLine("AdjustTokenPrivileges() failed, error = {0} .SeLockMemoryPrivilege is not available", result);
            return false;
        }

        CloseHandle(hToken);

        return true;
    }
}

// ...[생략]...

하지만 이렇게 해도 여전히 AdjustTokenPrivileges API 호출 시 GetLastError 값이 1300(ERROR_NOT_ALL_ASSIGNED - Not all privileges or groups referenced are assigned to the caller.)이 나오며 실패하게 됩니다.

왜냐하면, 애당초 사용자에게는 SeLockMemoryPrivilege 특권을 활성화조차 할 수 없는 상태이기 때문인데 이에 대해서는 이미 이전 글에서 설명했습니다.

C# - gpedit.msc의 "User Rights Assignment" 특권을 코드로 설정/해제하는 방법
; https://www.sysnet.pe.kr/2/0/13207

따라서 관리자 권한을 이용해 이전 글에 실은 예제 코드를 실행하거나, gpedit.msc를 이용해 "Local Computer Policy" / "Computer Configuration" / "Windows Settings" / "Security Settigns" / "Local Policies" / "User Rights Assignment" 패널에서 "Lock pages in memory" 특권에 (2MB 페이지 할당을 실행할 사용자의) 계정 또는 그룹을 추가해야 합니다. 또한, 반드시 변경된 사용자 특권이 적용되도록 재로그인 하는 것도 잊지 마시고.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




이에 대해 검색해 보면 다음의 Q&A 글이 나오는데요,

Enable large pages in Windows programmatically
; https://stackoverflow.com/questions/42354504/enable-large-pages-in-windows-programmatically

위의 답변에 실린 코드를 보면, 특권에 사용자 계정을 추가(LsaAddAccountRights)하고, 그 특권을 허용(AdjustTokenPrivileges)한 다음 2MB 페이지 메모리 할당(VirtualAlloc)까지 하나의 소스 코드에서 하고 있는데요, (저 당시에는 가능했는지 모르지만) 현재는 저렇게 모든 작업을 동시에 수행할 수 없습니다.

이전 글에 언급한 대로, "SeLockMemoryPrivilege" 특권에 사용자를 포함하는 LsaAddAccountRights 작업은 "관리자 권한"으로 수행한 후 반드시 재로그인을 필요로 합니다. 즉, 로그오프한 다음 다시 로그인을 해야 반영되는 것입니다.

일단, 특권에 사용자가 추가됐으면 이후 AdjustTokenPrivileges를 수행해 특권을 허용하는 작업은 "관리자 권한" 없이 가능합니다. 또한, 그렇게 특권 허용을 했으면 Large 페이지 할당도 관리자 권한 없이 가능하고.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 10/26/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13329정성태4/24/20233707Windows: 246. Win32 C/C++ - 직접 띄운 대화창 템플릿을 위한 Modal 메시지 루프 생성파일 다운로드1
13328정성태4/19/20233379VS.NET IDE: 184. Visual Studio - Fine Code Coverage에서 동작하지 않는 Fake/Shim 테스트
13327정성태4/19/20233794VS.NET IDE: 183. C# - .NET Core/5+ 환경에서 Fakes를 이용한 단위 테스트 방법
13326정성태4/18/20235205.NET Framework: 2109. C# - 닷넷 응용 프로그램에서 SQLite 사용 (System.Data.SQLite) [1]파일 다운로드1
13325정성태4/18/20234505스크립트: 48. 파이썬 - PostgreSQL의 with 문을 사용한 경우 연결 개체 누수
13324정성태4/17/20234319.NET Framework: 2108. C# - Octave의 "save -binary ..."로 생성한 바이너리 파일 분석파일 다운로드1
13323정성태4/16/20234260개발 환경 구성: 677. Octave에서 Excel read/write를 위한 io 패키지 설치
13322정성태4/15/20235038VS.NET IDE: 182. Visual Studio - 32비트로만 빌드된 ActiveX와 작업해야 한다면?
13321정성태4/14/20233827개발 환경 구성: 676. WSL/Linux Octave - Python 스크립트 연동
13320정성태4/13/20233806개발 환경 구성: 675. Windows Octave 8.1.0 - Python 스크립트 연동
13319정성태4/12/20234291개발 환경 구성: 674. WSL 2 환경에서 GNU Octave 설치
13318정성태4/11/20234114개발 환경 구성: 673. JetBrains IDE에서 "Squash Commits..." 메뉴가 비활성화된 경우
13317정성태4/11/20234228오류 유형: 855. WSL 2 Ubuntu 20.04 - error: cannot communicate with server: Post http://localhost/v2/snaps/...
13316정성태4/10/20233553오류 유형: 854. docker-compose 시 "json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)" 오류 발생
13315정성태4/10/20233750Windows: 245. Win32 - 시간 만료를 갖는 컨텍스트 메뉴와 윈도우 메시지의 영역별 정의파일 다운로드1
13314정성태4/9/20233828개발 환경 구성: 672. DosBox를 이용한 Turbo C, Windows 3.1 설치
13313정성태4/9/20233911개발 환경 구성: 671. Hyper-V VM에 Turbo C 2.0 설치 [2]
13312정성태4/8/20233933Windows: 244. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (개선된 버전)파일 다운로드1
13311정성태4/7/20234442C/C++: 163. Visual Studio 2022 - DirectShow 예제 컴파일(WAV Dest)
13310정성태4/6/20234021C/C++: 162. Visual Studio - /NODEFAULTLIB 옵션 설정 후 수동으로 추가해야 할 library
13309정성태4/5/20234206.NET Framework: 2107. .NET 6+ FileStream의 구조 변화
13308정성태4/4/20234098스크립트: 47. 파이썬의 time.time() 실숫값을 GoLang / C#에서 사용하는 방법
13307정성태4/4/20233868.NET Framework: 2106. C# - .NET Core/5+ 환경의 Windows Forms 응용 프로그램에서 HINSTANCE 구하는 방법
13306정성태4/3/20233668Windows: 243. Win32 - 윈도우(cbWndExtra) 및 윈도우 클래스(cbClsExtra) 저장소 사용 방법
13305정성태4/1/20234032Windows: 242. Win32 - 시간 만료를 갖는 MessageBox 대화창 구현 (쉬운 버전)파일 다운로드1
13304정성태3/31/20234387VS.NET IDE: 181. Visual Studio - C/C++ 프로젝트에 application manifest 적용하는 방법
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...