Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)
(시리즈 글이 3개 있습니다.)
.NET Framework: 2086. C# - Windows 운영체제의 2MB Large 페이지 크기 할당 방법
; https://www.sysnet.pe.kr/2/0/13208

Windows: 218. 왜 윈도우에서 가상 메모리 공간은 64KB 정렬이 된 걸까요?
; https://www.sysnet.pe.kr/2/0/13209

Windows: 219. 윈도우 x64의 경우 0x00000000`7ffe0000 아래의 주소는 왜 사용하지 않을까요?
; https://www.sysnet.pe.kr/2/0/13210




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, Large 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

// 참고로, 페이지 파일을 사용하지 않는 파일 기반의 CreateFileMapping은 지원하지 않는다고 합니다.
Why can’t I use SEC_LARGE_PAGES with a file-based file mapping?
; https://devblogs.microsoft.com/oldnewthing/20250409-00/?p=111061

// 일례로 다음의 코드는 ERROR_INVALID_PARAMETER 오류를 반환
hMapFile = CreateFileMapping(
         hFile,                   // use a specific file
         NULL,                    // default security
         PAGE_READWRITE | SEC_COMMIT | SEC_LARGE_PAGES,
         0,                       // max. object size
         size,                    // buffer size
         szName);                 // name of mapping object

그러니까, 우선 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 페이지 할당도 관리자 권한 없이 가능하고.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/10/2025]

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)
13843정성태12/13/20244381오류 유형: 938. Docker container 내에서 빌드 시 error MSB3021: Unable to copy file "..." to "...". Access to the path '...' is denied.
13842정성태12/12/20244522디버깅 기술: 205. Windbg - KPCR, KPRCB
13841정성태12/11/20244855오류 유형: 937. error MSB4044: The "ValidateValidArchitecture" task was not given a value for the required parameter "RemoteTarget"
13840정성태12/11/20244428오류 유형: 936. msbuild - Your project file doesn't list 'win' as a "RuntimeIdentifier"
13839정성태12/11/20244857오류 유형: 936. msbuild - error CS1617: Invalid option '12.0' for /langversion. Use '/langversion:?' to list supported values.
13838정성태12/4/20244595오류 유형: 935. Windbg - Breakpoint 0's offset expression evaluation failed.
13837정성태12/3/20245051디버깅 기술: 204. Windbg - 윈도우 핸들 테이블 (3) - Windows 10 이상인 경우
13836정성태12/3/20244610디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
13835정성태12/2/20245055오류 유형: 934. Azure - rm: cannot remove '...': Directory not empty
13834정성태11/29/20245284Windows: 275. C# - CUI 애플리케이션과 Console 윈도우 (Windows 10 미만의 Classic Console 모드인 경우) [1]파일 다운로드1
13833정성태11/29/20244965개발 환경 구성: 737. Azure Web App에서 Scale-out으로 늘어난 리눅스 인스턴스에 SSH 접속하는 방법
13832정성태11/27/20244900Windows: 274. Windows 7부터 도입한 conhost.exe
13831정성태11/27/20244381Linux: 111. eBPF - BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_RINGBUF에 대한 다양한 용어들
13830정성태11/25/20245181개발 환경 구성: 736. 파이썬 웹 앱을 Azure App Service에 배포하기
13829정성태11/25/20245140스크립트: 67. 파이썬 - Windows 버전에서 함께 설치되는 py.exe
13828정성태11/25/20244439개발 환경 구성: 735. Azure - 압축 파일을 이용한 web app 배포 시 디렉터리 구분이 안 되는 문제파일 다운로드1
13827정성태11/25/20245091Windows: 273. Windows 환경의 파일 압축 방법 (tar, Compress-Archive)
13826정성태11/21/20245320닷넷: 2313. C# - (비밀번호 등의) Console로부터 입력받을 때 문자열 출력 숨기기(echo 끄기)파일 다운로드1
13825정성태11/21/20245652Linux: 110. eBPF / bpf2go - BPF_RINGBUF_OUTPUT / BPF_MAP_TYPE_RINGBUF 사용법
13824정성태11/20/20244742Linux: 109. eBPF / bpf2go - BPF_PERF_OUTPUT / BPF_MAP_TYPE_PERF_EVENT_ARRAY 사용법
13823정성태11/20/20245287개발 환경 구성: 734. Ubuntu에 docker, kubernetes (k3s) 설치
13822정성태11/20/20245151개발 환경 구성: 733. Windbg - VirtualBox VM의 커널 디버거 연결 시 COM 포트가 없는 경우
13821정성태11/18/20245079Linux: 108. Linux와 Windows의 프로세스/스레드 ID 관리 방식
13820정성태11/18/20245235VS.NET IDE: 195. Visual C++ - C# 프로젝트처럼 CopyToOutputDirectory 항목을 추가하는 방법
13819정성태11/15/20244477Linux: 107. eBPF - libbpf CO-RE의 CONFIG_DEBUG_INFO_BTF 빌드 여부에 대한 의존성
13818정성태11/15/20245264Windows: 272. Windows 11 24H2 - sudo 추가
1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...