Microsoft MVP성태의 닷넷 이야기
.NET Framework: 2017. C# - Windows I/O Ring 소개 [링크 복사], [링크+제목 복사],
조회: 20724
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# - Windows I/O Ring 소개

이번엔 아래의 글에 대한,

Create and Initialize an I/O Ring
; https://windows-internals.com/i-o-rings-when-one-i-o-operation-is-not-enough/

정리입니다.




I/O ring 관련한 함수는 kernelbase.dll에 있지만 Import library가 없어서 동적 로딩으로 대충 이런 식으로 처리하면 됩니다.

extern "C"
{
    typedef HRESULT(*CreateIoRingFunc)(IORING_VERSION ioringVersion, IORING_CREATE_FLAGS flags, UINT32 submissionQueueSize,
        UINT32 completionQueueSize, _Out_ HIORING* h);
    typedef HRESULT(*BuildIoRingReadFileFunc)(_In_ HIORING ioRing, IORING_HANDLE_REF fileRef, IORING_BUFFER_REF dataRef, UINT32 numberOfBytesToRead, UINT64 fileOffset, UINT_PTR userData, IORING_SQE_FLAGS flags);
    typedef HRESULT(*SubmitIoRingFunc)(_In_ HIORING ioRing, UINT32 waitOperations, UINT32 milliseconds, _Out_opt_ UINT32* submittedEntries);
    typedef HRESULT(*CloseIoRingFunc)(_In_ _Post_ptr_invalid_ HIORING ioRing);
    typedef HRESULT(*GetIoRingInfoFunc)(HIORING ioRing, IORING_INFO* info);
}

CreateIoRingFunc CreateIoRingImpl;
SubmitIoRingFunc SubmitIoRingImpl;
CloseIoRingFunc CloseIoRingImpl;
BuildIoRingReadFileFunc BuildIoRingReadFileImpl;
GetIoRingInfoFunc GetIoRingInfoImpl;

BOOL InitFunctions()
{
    HMODULE hModule = ::LoadLibrary(L"kernelbase.dll");
    if (hModule == nullptr)
    {
        return FALSE;
    }

    CreateIoRingImpl = (CreateIoRingFunc)GetProcAddress(hModule, "CreateIoRing");
    SubmitIoRingImpl = (SubmitIoRingFunc)GetProcAddress(hModule, "SubmitIoRing");
    CloseIoRingImpl = (CloseIoRingFunc)GetProcAddress(hModule, "CloseIoRing");
    BuildIoRingReadFileImpl = (BuildIoRingReadFileFunc)GetProcAddress(hModule, "BuildIoRingReadFile");
    GetIoRingInfoImpl = (GetIoRingInfoFunc)GetProcAddress(hModule, "GetIoRingInfo");

    return TRUE;
}

이후 사용법은 "Create and Initialize an I/O Ring" 글에 실려 있는데 아래와 같이 정리할 수 있습니다.

// 소스 코드 출처: https://windows-internals.com/i-o-rings-when-one-i-o-operation-is-not-enough/

void IoRingKernelBase()
{
    HRESULT result;
    HIORING handle;
    IORING_CREATE_FLAGS flags;
    UINT32 submittedEntries;
    HANDLE hFile = NULL;
    ULONG sizeToRead = 0x200;
    PVOID* buffer = NULL;
    ULONG64 endOfBuffer;

    flags.Required = IORING_CREATE_REQUIRED_FLAGS_NONE;
    flags.Advisory = IORING_CREATE_ADVISORY_FLAGS_NONE;
    result = CreateIoRingImpl(IORING_VERSION_1, flags, 1, 1, &handle);

    if (!SUCCEEDED(result))
    {
        printf("Failed creating IO ring handle: 0x%x\n", result);
        return;
    }

    do {

        hFile = CreateFile(L"C:\\Windows\\System32\\notepad.exe",
            GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
        if (hFile == INVALID_HANDLE_VALUE)
        {
            printf("Failed opening file handle: 0x%x\n", GetLastError());
            break;
        }

        IORING_HANDLE_REF requestDataFile(hFile);

        int size_IORING_HANDLE_REF = sizeof(IORING_HANDLE_REF);
        int size_IORING_BUFFER_REF = sizeof(IORING_BUFFER_REF);

        requestDataFile.Kind = IORING_REF_RAW;

        buffer = (PVOID*)VirtualAlloc(NULL, sizeToRead, MEM_COMMIT, PAGE_READWRITE);
        if (buffer == NULL)
        {
            printf("Failed to allocate memory\n");
            break;
        }

        IORING_BUFFER_REF requestDataBuffer(buffer);
        requestDataBuffer.Kind = IORING_REF_RAW;

        result = BuildIoRingReadFileImpl(handle, requestDataFile, requestDataBuffer,
            sizeToRead, 0, NULL, IOSQE_FLAGS_NONE);
        if (!SUCCEEDED(result))
        {
            printf("Failed building IO ring read file structure: 0x%x\n", result);
            break;
        }

        result = SubmitIoRingImpl(handle, 1, 10000, &submittedEntries);
        if (!SUCCEEDED(result))
        {
            printf("Failed submitting IO ring: 0x%x\n", result);
            break;
        }

        IORING_INFO ringInfo;
        result = GetIoRingInfoImpl(handle, &ringInfo);
        if (SUCCEEDED(result))
        {
            printf("Status: SubmissionQueueSize %d, CompletionQueueSize %d\n", ringInfo.SubmissionQueueSize, ringInfo.CompletionQueueSize);
        }

        printf("Data from file:\n");
        endOfBuffer = (ULONG64)buffer + sizeToRead;
        for (; (ULONG64)buffer < endOfBuffer; buffer++)
        {
            printf("%p ", *buffer);
        }

        printf("\n");

    } while (false);

    if (handle != 0)
    {
        CloseIoRingImpl(handle);
    }
    if (hFile)
    {
        CloseHandle(hFile);
    }
    if (buffer)
    {
        VirtualFree(buffer, NULL, MEM_RELEASE);
    }
}

/* 출력 결과
Status: SubmissionQueueSize 8, CompletionQueueSize 16
Data from file:
0000000300905A4D 0000FFFF00000004 ...[생략]... 0000000000000000 000000747865742E
*/

출력 결과에 보면 "SubmissionQueueSize 8, CompletionQueueSize 16"이 나오는데요, 이에 대해 "IoRing vs. io_uring: a comparison of Windows and Linux implementations/" 글을 보면 이런 설명이 나옵니다.

...
at least 8 entries will always be allocated.
...
the completion queue will have twice the number of entries as the submission queue.

그 외 출력 및 전체 소스 코드를 보면, 사용법이 딱히 어렵진 않습니다. CreateIoRing으로 I/O Ring을 생성한 후, 파일을 일반적인 방법으로 열어 VirtualAlloc으로 할당한 메모리 주소와 함께 BuildIoRingReadFile로 submission queue에 Read 연산을 적재합니다.

따라서, 필요한 만큼 여러 개의 I/O 작업을 BuildIoRingReadFile을 이용해 I/O Ring 큐에 집어넣는 것입니다. 그다음, 이러한 작업을 한꺼번에 커널 쪽에 SubmitIoRing을 통해 전달합니다.

어떤 의미인지 아시겠죠?

I/O의 Read/Write 작업은 필수적으로 커널 모드로의 전환이 필요한데요, I/O ring을 사용하면 그 횟수를 줄일 수 있습니다. 가령 10번의 Read를 한다면, 이것을 단순히 I/O ring에 Read 신호를 적재만 한 후, SubmitIoRing 호출로 일괄 전달함으로써 단 한 번의 커널 전환만 하기 때문에 CPU 사용을 보다 더 효율적으로 할 수 있는 것입니다.

의미는 좋은데, 사실 사용 시나리오가 많지는 않을 것 같습니다. 예를 들어, 단일 파일에 대한 I/O를 가정해봤을 때, 오히려 Read 연산을 여러 번에 나눌 것도 없이 기존에도 그냥 한 번에 처리하는 것이 가능했습니다. 단순히 4GB 파일을 읽는다고 가정했을 때 한 번의 ReadFile로 파일을 로드하는 것이 가능하지만, 그렇게 되면 4GB 메모리가 소비되기 때문에 어쩔 수 없이 I/O를 나눠 작업하는 식으로 처리해왔던 것입니다. 그런 시나리오에서 I/O Ring을 사용하는 것은, 어차피 VirtualAlloc으로 committed 상태의 메모리를 전달해야 하기 때문에 메모리의 순간적인 소비는 피할 수 없습니다.

반면, 여러 개의 파일을 동시에 다루는 것이라면 그나마 효율적일 것도 같은데... 현업에서 특수한 사례에 속하는 시나리오가 될 것입니다. "I/O Rings – When One I/O Operation is Not Enough" 글을 쓴 Yarden Shafir도 마지막 즈음에, 윈도우에서조차도 안 쓰는 듯하다가 21H2 버전 이후로 I/O가 많은 윈도우 응용 프로그램에 적용되기 시작했다고 언급하고 있습니다.




Yarden Shafir가 IoRing에 관해 쓴 다른 글도 읽어보시면 좋을 듯합니다. ^^

IoRing vs. io_uring: a comparison of Windows and Linux implementations
; https://windows-internals.com/ioring-vs-io_uring-a-comparison-of-windows-and-linux-implementations/

제목만 봐도 아시겠지만, 리눅스에는 이미 io_uring이라는 이름으로 있었고, 윈도우의 IoRing은 그것과 아주 유사하다고 합니다.

또 하나 글을 볼까요?

One Year to I/O Ring: What Changed?
; https://windows-internals.com/one-year-to-i-o-ring-what-changed/

Yarden Shafir가 쓴 가장 최근(2022-04-29)의 I/O Ring 소식인데요, 위의 글에 보면 쓰기 연산(BuildIoRingWriteFile)까지 추가된 것을 볼 수 있고 아직 Preview 버전의 윈도우에만 적용된 듯합니다.

아울러, 개별 요청에 대한 이벤트 알림을 받는 것도 추가되었고, 이와 함께 I/O Ring 요청의 비동기 사용법을 예제 코드와 함께 자세하게 설명하고 있습니다.

마지막으로 I/O Ring 버전이 3까지 진행되었다고 하니, Preview 버전이 Windows 11 22H2로 반영되고 나면 IORING_VERSION_3으로 해서 신규 기능을 테스트하면 될 듯합니다.




마지막으로, 아래의 코드는 C#으로 구현해 본 것입니다. ^^

using Microsoft.Win32.SafeHandles;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;

public enum IORING_VERSION
{
    INVALID,
    VRESION_1,
}

public enum IORING_CREATE_REQUIRED_FLAGS
{
    NONE
}

public enum IORING_CREATE_ADVISORY_FLAGS
{
    NONE
}

public enum IORING_REF_KIND
{
    IORING_REF_RAW,
    IORING_REF_REGISTERED,
}

[StructLayout(LayoutKind.Explicit)]
public struct IORING_HANDLE_REF
{
    [FieldOffset(0)]
    public IORING_REF_KIND Kind;

    [FieldOffset(8)]
    public IntPtr Handle;

    [FieldOffset(8)]
    public uint Index;

    public IORING_HANDLE_REF(SafeFileHandle handle)
    {
        this.Index = 0;
        this.Kind = IORING_REF_KIND.IORING_REF_RAW;
        this.Handle = handle.DangerousGetHandle();
    }
}

public struct IORING_REGISTERED_BUFFER
{
    // Index of pre-registered buffer
    public uint BufferIndex;

    // Offset into the pre-registered buffer
    public uint Offset;
}

[StructLayout(LayoutKind.Explicit)]
public struct IORING_BUFFER_REF
{
    [FieldOffset(0)]
    public IORING_REF_KIND Kind;

    [FieldOffset(8)]
    public IntPtr Address;

    [FieldOffset(8)]
    public IORING_REGISTERED_BUFFER IndexAndOffset;

    public IORING_BUFFER_REF(IntPtr address)
    {
        this.IndexAndOffset = new IORING_REGISTERED_BUFFER();
        this.Kind = IORING_REF_KIND.IORING_REF_RAW;
        this.Address = address;
    }
}

public struct IORING_CREATE_FLAGS
{
    public IORING_CREATE_REQUIRED_FLAGS Required;
    public IORING_CREATE_ADVISORY_FLAGS Advisory;
}

public struct IORING_INFO
{
    public IORING_VERSION IoRingVersion;
    public IORING_CREATE_FLAGS Flags;
    public uint SubmissionQueueSize;
    public uint CompletionQueueSize;
}

[StructLayout(LayoutKind.Sequential)]
public struct HIORING
{
    public IntPtr handle;

    public override int GetHashCode()
    {
        return handle.GetHashCode();
    }

    public override bool Equals([NotNullWhen(true)] object? obj)
    {
        switch (obj)
        {
            case IntPtr targetPtr:
                return this.handle == targetPtr;

            case HIORING ioRing:
                return this.handle == ioRing.handle;
        }

        return false;
    }

    public static bool operator ==(HIORING arg1, IntPtr arg2)
    {
        return arg1.handle == arg2;
    }

    public static bool operator !=(HIORING arg1, IntPtr arg2)
    {
        return arg1.handle != arg2;
    }
}

public enum IORING_SQE_FLAGS
{
    IOSQE_FLAGS_NONE = 0,
}

internal class Program
{
    [DllImport("KernelBase.dll")]
    public static extern int CreateIoRing(IORING_VERSION IoRingVersion,
        IORING_CREATE_FLAGS Flags,
        uint SubmissionQueueSize,
        uint CompletionQueueSize,
        out HIORING Handle);

    [DllImport("KernelBase.dll")]
    public static unsafe extern int BuildIoRingReadFile(HIORING ioRing, IORING_HANDLE_REF fileRef, IORING_BUFFER_REF dataRef, uint numberOfBytesToRead, ulong fileOffset, uint* userData, IORING_SQE_FLAGS flags);

    // [DllImport("KernelBase.dll")]
    // public static unsafe extern int BuildIoRingWriteFile(HIORING ioRing, IORING_HANDLE_REF fileRef, IORING_BUFFER_REF dataRef, uint numberOfBytesToWrite, ulong fileOffset, FILE_WRITE_FLAGS writeFlags, uint* userData, IORING_SQE_FLAGS flags);

    [DllImport("KernelBase.dll")]
    public static unsafe extern int SubmitIoRing(HIORING ioRing, uint waitOperations, uint milliseconds, uint* submittedEntries);

    [DllImport("KernelBase.dll")]
    public static extern int CloseIoRing(HIORING ioRing);

    [Flags()]
    private enum AllocationType : uint
    {
        COMMIT = 0x1000,
        RESERVE = 0x2000,
        RESET = 0x80000,
        LARGE_PAGES = 0x20000000,
        PHYSICAL = 0x400000,
        TOP_DOWN = 0x100000,
        WRITE_WATCH = 0x200000
    }

    [Flags()]
    public enum MemoryProtection : uint
    {
        EXECUTE = 0x10,
        EXECUTE_READ = 0x20,
        EXECUTE_READWRITE = 0x40,
        EXECUTE_WRITECOPY = 0x80,
        NOACCESS = 0x01,
        READONLY = 0x02,
        READWRITE = 0x04,
        WRITECOPY = 0x08,
        GUARD_Modifierflag = 0x100,
        NOCACHE_Modifierflag = 0x200,
        WRITECOMBINE_Modifierflag = 0x400
    }

    public const uint MEM_RELEASE = 0x00008000;

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

    [DllImport("kernel32")]
    private static extern bool VirtualFree(IntPtr lpAddress, ulong dwSize, UInt32 dwFreeType);

    static unsafe void Main(string[] args)
    {
        if (IntPtr.Size != 8)
        {
            Console.WriteLine("Only supported in x64");
            return;
        }

        IORING_CREATE_FLAGS flags = new IORING_CREATE_FLAGS();
        flags.Advisory = IORING_CREATE_ADVISORY_FLAGS.NONE;
        flags.Required = IORING_CREATE_REQUIRED_FLAGS.NONE;
        HIORING handle;
        IntPtr buffer;

        int result = CreateIoRing(IORING_VERSION.VRESION_1, flags, 1, 1, out handle);
        if (Marshal.GetExceptionForHR(result) != null)
        {
            Console.WriteLine($"Failed creating IO ring handle: 0x{result:x}");
            return;
        }

        using FileStream fs = File.OpenRead(@"C:\temp\Program.cs");

        do
        {
            IORING_HANDLE_REF requestDataFile = new IORING_HANDLE_REF(fs.SafeFileHandle);
            Debug.Assert(16 == Marshal.SizeOf(requestDataFile));

            uint sizeToRead = 0x200;

            buffer = VirtualAlloc(IntPtr.Zero, sizeToRead, AllocationType.COMMIT, MemoryProtection.READWRITE);
            IORING_BUFFER_REF requestDataBuffer = new IORING_BUFFER_REF(buffer);
            Debug.Assert(16 == Marshal.SizeOf(requestDataFile));

            result = BuildIoRingReadFile(handle, requestDataFile, requestDataBuffer, sizeToRead, 0, null, IORING_SQE_FLAGS.IOSQE_FLAGS_NONE);
            if (Marshal.GetExceptionForHR(result) != null)
            {
                Console.WriteLine($"Failed building IO ring read file structure: 0x{result:x}");
                break;
            }

            uint submittedEntries;
            result = SubmitIoRing(handle, 1, 10000, &submittedEntries);

            if (Marshal.GetExceptionForHR(result) != null)
            {
                Console.WriteLine($"Failed submitting IO ring: 0x{result:x}");
                break;
            }

            Console.WriteLine("Data from file:");
            Span<byte> readBuffer = new Span<byte>(buffer.ToPointer(), (int)sizeToRead);

            Console.WriteLine(Encoding.UTF8.GetString(readBuffer));

        } while (false);

        if (handle != IntPtr.Zero)
        {
            CloseIoRing(handle);
        }

        if (buffer != IntPtr.Zero)
        {
            VirtualFree(buffer, 0, MEM_RELEASE);
        }
    }
}

(첨부 파일은 이 글에서 다룬 C++, C# 예제 코드를 포함합니다.)




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







[최초 등록일: ]
[최종 수정일: 5/31/2023]

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

비밀번호

댓글 작성자
 



2022-09-16 05시16분
[익명] io_uring의 경우 네트워크 통신을 위해 만들어졌다고 알고 있고, IORing은 io_uring을 윈도우즈 기반에서 구현을 한 것같은데... 왜 Registered IO는 오히려 정보가 없네요.
[guest]
2023-05-31 11시43분
What is IO_uring? (Inside IO_uring)
; https://smileostrich.tistory.com/entry/What-is-IOuring-Inside-IOuring

리눅스의 경우, 보안 문제로 인해,

Learnings from kCTF VRP's 42 Linux kernel exploits submissions
; https://security.googleblog.com/2023/06/learnings-from-kctf-vrps-42-linux.html

io_uring을 비활성으로 놓고 있습니다.
정성태

... 61  62  63  64  65  66  67  68  69  70  71  72  73  74  [75]  ...
NoWriterDateCnt.TitleFile(s)
12153정성태2/23/202024468.NET Framework: 898. Trampoline을 이용한 후킹의 한계파일 다운로드1
12152정성태2/23/202021454.NET Framework: 897. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 세 번째 이야기(Trampoline 후킹)파일 다운로드1
12151정성태2/22/202024086.NET Framework: 896. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 - 두 번째 이야기 (원본 함수 호출)파일 다운로드1
12150정성태2/21/202024206.NET Framework: 895. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 [1]파일 다운로드1
12149정성태2/20/202021099.NET Framework: 894. eBEST C# XingAPI 래퍼 - 연속 조회 처리 방법 [1]
12148정성태2/19/202025793디버깅 기술: 163. x64 환경에서 구현하는 다양한 Trampoline 기법 [1]
12147정성태2/19/202021073디버깅 기술: 162. x86/x64의 기계어 코드 최대 길이
12146정성태2/18/202022291.NET Framework: 893. eBEST C# XingAPI 래퍼 - 로그인 처리파일 다운로드1
12145정성태2/18/202023886.NET Framework: 892. eBEST C# XingAPI 래퍼 - Sqlite 지원 추가파일 다운로드1
12144정성태2/13/202024091.NET Framework: 891. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 두 번째 이야기파일 다운로드1
12143정성태2/13/202018510.NET Framework: 890. 상황별 GetFunctionPointer 반환값 정리 - x64파일 다운로드1
12142정성태2/12/202022474.NET Framework: 889. C# 코드로 접근하는 MethodDesc, MethodTable파일 다운로드1
12141정성태2/10/202021439.NET Framework: 888. C# - ASP.NET Core 웹 응용 프로그램의 출력 가로채기 [2]파일 다운로드1
12140정성태2/10/202022753.NET Framework: 887. C# - ASP.NET 웹 응용 프로그램의 출력 가로채기파일 다운로드1
12139정성태2/9/202022447.NET Framework: 886. C# - Console 응용 프로그램에서 UI 스레드 구현 방법
12138정성태2/9/202028651.NET Framework: 885. C# - 닷넷 응용 프로그램에서 SQLite 사용 [6]파일 다운로드1
12137정성태2/9/202020322오류 유형: 592. [AhnLab] 경고 - 디버거 실행을 탐지했습니다.
12136정성태2/6/202022000Windows: 168. Windows + S(또는 Q)로 뜨는 작업 표시줄의 검색 바가 동작하지 않는 경우
12135정성태2/6/202027786개발 환경 구성: 468. Nuget 패키지의 로컬 보관 폴더를 옮기는 방법 [2]
12134정성태2/5/202025061.NET Framework: 884. eBEST XingAPI의 C# 래퍼 버전 - XingAPINet Nuget 패키지 [5]파일 다운로드1
12133정성태2/5/202022815디버깅 기술: 161. Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 - 두 번째 이야기
12132정성태1/28/202025914.NET Framework: 883. C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기) [1]파일 다운로드1
12131정성태1/27/202024536개발 환경 구성: 467. LocaleEmulator를 이용해 유니코드를 지원하지 않는(한글이 깨지는) 프로그램을 실행하는 방법 [1]
12130정성태1/26/202022091VS.NET IDE: 142. Visual Studio에서 windbg의 "Open Executable..."처럼 EXE를 직접 열어 디버깅을 시작하는 방법
12129정성태1/26/202029096.NET Framework: 882. C# - 키움 Open API+ 사용 시 Registry 등록 없이 KHOpenAPI.ocx 사용하는 방법 [3]
12128정성태1/26/202023270오류 유형: 591. The code execution cannot proceed because mfc100.dll was not found. Reinstalling the program may fix this problem.
... 61  62  63  64  65  66  67  68  69  70  71  72  73  74  [75]  ...