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

비밀번호

댓글 작성자
 




... 106  [107]  108  109  110  111  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11249정성태7/12/201718550오류 유형: 410. LoadLibrary("[...].dll") failed - The specified procedure could not be found.
11248정성태7/12/201725040오류 유형: 409. pip install pefile - 'cp949' codec can't decode byte 0xe2 in position 208687: illegal multibyte sequence
11247정성태7/12/201719385오류 유형: 408. SqlConnection 객체 생성 시 무한 대기 문제파일 다운로드1
11246정성태7/11/201718150VS.NET IDE: 118. Visual Studio - 다중 폴더에 포함된 파일들에 대한 "Copy to Output Directory"를 한 번에 설정하는 방법
11245정성태7/10/201723768개발 환경 구성: 321. Visual Studio Emulator for Android 소개 [2]
11244정성태7/10/201723324오류 유형: 407. Visual Studio에서 ASP.NET Core 실행할 때 dotnet.exe 프로세스의 -532462766 오류 발생 [1]
11243정성태7/10/201720003.NET Framework: 666. dotnet.exe - 윈도우 운영체제에서의 .NET Core 버전 찾기 규칙
11242정성태7/8/201720276제니퍼 .NET: 27. 제니퍼 닷넷 적용 사례 (7) - 노후된 스토리지 장비로 인한 웹 서비스 Hang (멈춤) 현상
11241정성태7/8/201719024오류 유형: 406. Xamarin 빌드 에러 XA5209, APT0000
11240정성태7/7/201721957.NET Framework: 665. ClickOnce를 웹 브라우저를 이용하지 않고 쿼리 문자열을 전달하면서 실행하는 방법 [3]파일 다운로드1
11239정성태7/6/201723431.NET Framework: 664. Protocol Handler - 웹 브라우저에서 데스크톱 응용 프로그램을 실행하는 방법 [5]파일 다운로드1
11238정성태7/6/201720970오류 유형: 405. NT 서비스 시작 시 "Error 1067: The process terminated unexpectedly." 오류 발생 [2]
11237정성태7/5/201722603.NET Framework: 663. C# - PDB 파일 경로를 PE 파일로부터 얻는 방법파일 다운로드1
11236정성태7/4/201725862.NET Framework: 662. C# - VHD/VHDX 가상 디스크를 마운트하지 않고 파일을 복사하는 방법파일 다운로드1
11235정성태6/29/201719981Math: 20. Matlab/Octave로 Gram-Schmidt 정규 직교 집합 구하는 방법
11234정성태6/29/201717318오류 유형: 404. SharePoint 2013 설치 과정에서 "The username is invalid The account must be a valid domain account" 오류 발생
11233정성태6/28/201717250오류 유형: 403. SharePoint Server 2013을 Windows Server 2016에 설치할 때 .NET 4.5 설치 오류 발생
11232정성태6/28/201718233Windows: 144. Windows Server 2016에 Windows Identity Extensions을 설치하는 방법
11231정성태6/28/201718847디버깅 기술: 86. windbg의 mscordacwks DLL 로드 문제 - 세 번째 이야기 [1]
11230정성태6/28/201718022제니퍼 .NET: 26. 제니퍼 닷넷 적용 사례 (6) - 잦은 Recycle 문제
11229정성태6/27/201719261오류 유형: 402. Windows Server Backup 관리 콘솔이 없어진 경우
11228정성태6/26/201716720개발 환경 구성: 320. Visual Basic .NET 프로젝트에서 내장 Manifest 자원을 EXE 파일로부터 제거하는 방법파일 다운로드1
11227정성태6/19/201724490개발 환경 구성: 319. windbg에서 python 스크립트 실행하는 방법 - pykd [6]
11226정성태6/19/201716329오류 유형: 401. Microsoft Edge를 실행했는데 입력 반응이 없는 경우
11225정성태6/19/201715652오류 유형: 400. Outlook - The required file ExSec32.dll cannot be found in your path. Install Microsoft Outlook again.
11224정성태6/13/201718141.NET Framework: 661. Json.NET의 DeserializeObject 수행 시 속성 이름을 동적으로 바꾸는 방법파일 다운로드1
... 106  [107]  108  109  110  111  112  113  114  115  116  117  118  119  120  ...