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)
13244정성태2/5/20233974VS.NET IDE: 179. Visual Studio - External Tools에 Shell 내장 명령어 등록
13243정성태2/5/20234837디버깅 기술: 190. windbg - Win32 API 호출 시점에 BP 거는 방법 [1]
13242정성태2/4/20234278디버깅 기술: 189. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.UnauthorizedAccessException
13241정성태2/3/20233814디버깅 기술: 188. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.IO.FileNotFoundException
13240정성태2/1/20233980디버깅 기술: 187. ASP.NET Web Application (.NET Framework) 프로젝트의 숨겨진 예외 - System.Web.HttpException
13239정성태2/1/20233623디버깅 기술: 186. C# - CacheDependency의 숨겨진 예외 - System.Web.HttpException
13238정성태1/31/20235615.NET Framework: 2092. IIS 웹 사이트를 TLS 1.2 또는 TLS 1.3 프로토콜로만 운영하는 방법
13237정성태1/30/20235280.NET Framework: 2091. C# - 웹 사이트가 어떤 버전의 TLS/SSL을 지원하는지 확인하는 방법
13236정성태1/29/20234950개발 환경 구성: 663. openssl을 이용해 인트라넷 IIS 사이트의 SSL 인증서 생성
13235정성태1/29/20234479개발 환경 구성: 662. openssl - 윈도우 환경의 명령행에서 SAN 적용하는 방법
13234정성태1/28/20235515개발 환경 구성: 661. dnSpy를 이용해 소스 코드가 없는 .NET 어셈블리의 코드를 변경하는 방법 [1]
13233정성태1/28/20236868오류 유형: 840. C# - WebClient로 https 호출 시 "The request was aborted: Could not create SSL/TLS secure channel" 예외 발생
13232정성태1/27/20234690스크립트: 43. uwsgi의 --processes와 --threads 옵션
13231정성태1/27/20233603오류 유형: 839. python - TypeError: '...' object is not callable
13230정성태1/26/20234018개발 환경 구성: 660. WSL 2 내부로부터 호스트 측의 네트워크로 UDP 데이터가 1개의 패킷으로만 제한되는 문제
13229정성태1/25/20234959.NET Framework: 2090. C# - UDP Datagram의 최대 크기
13228정성태1/24/20235075.NET Framework: 2089. C# - WMI 논리 디스크가 속한 물리 디스크의 정보를 얻는 방법 [2]파일 다운로드1
13227정성태1/23/20234794개발 환경 구성: 659. Windows - IP MTU 값을 바꿀 수 있을까요? [1]
13226정성태1/23/20234465.NET Framework: 2088. .NET 5부터 지원하는 GetRawSocketOption 사용 시 주의할 점
13225정성태1/21/20233738개발 환경 구성: 658. Windows에서 실행 중인 소켓 서버를 다른 PC 또는 WSL에서 접속할 수 없는 경우
13224정성태1/21/20234066Windows: 221. Windows - Private/Public/Domain이 아닌 네트워크 어댑터 단위로 방화벽을 on/off하는 방법
13223정성태1/20/20234263오류 유형: 838. RDP 연결 오류 - The two computers couldn't connect in the amount of time allotted
13222정성태1/20/20233926개발 환경 구성: 657. WSL - DockerDesktop.vhdx 파일 위치를 옮기는 방법
13221정성태1/19/20234137Linux: 57. C# - 리눅스 프로세스 메모리 정보파일 다운로드1
13220정성태1/19/20234280오류 유형: 837. NETSDK1045 The current .NET SDK does not support targeting .NET ...
13219정성태1/18/20233853Windows: 220. 네트워크의 인터넷 접속 가능 여부에 대한 판단 기준
1  2  3  4  5  6  7  8  9  10  11  12  13  14  [15]  ...