Microsoft MVP성태의 닷넷 이야기
.NET Framework: 2017. C# - Windows I/O Ring 소개 [링크 복사], [링크+제목 복사],
조회: 16174
글쓴 사람
정성태 (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을 비활성으로 놓고 있습니다.
정성태

... 136  137  138  139  140  141  142  143  144  145  146  147  148  149  [150]  ...
NoWriterDateCnt.TitleFile(s)
1303정성태6/26/201227401개발 환경 구성: 152. sysnet DB를 SQL Azure 데이터베이스로 마이그레이션
1302정성태6/25/201229432개발 환경 구성: 151. Azure 웹 사이트에 사용자 도메인 네임 연결하는 방법
1301정성태6/20/201225764오류 유형: 156. KB2667402 윈도우 업데이트 실패 및 마이크로소프트 Answers 웹 사이트 대응
1300정성태6/20/201231774.NET Framework: 329. C# - Rabin-Miller 소수 생성방법을 이용하여 RSACryptoServiceProvider의 개인키를 직접 채워보자 [1]파일 다운로드2
1299정성태6/18/201232890제니퍼 .NET: 21. 제니퍼 닷넷 - Ninject DI 프레임워크의 성능 분석 [2]파일 다운로드2
1298정성태6/14/201234405VS.NET IDE: 72. Visual Studio에서 pfx 파일로 서명한 경우, 암호는 어디에 저장될까? [2]
1297정성태6/12/201231053VC++: 63. 다른 프로세스에 환경 변수 설정하는 방법파일 다운로드1
1296정성태6/5/201227695.NET Framework: 328. 해당 DLL이 Managed인지 / Unmanaged인지 확인하는 방법 - 두 번째 이야기 [4]파일 다운로드1
1295정성태6/5/201225083.NET Framework: 327. RSAParameters와 System.Numerics.BigInteger 이야기파일 다운로드1
1294정성태5/27/201248530.NET Framework: 326. 유니코드와 한글 - 유니코드와 닷넷을 이용한 한글 처리 [7]파일 다운로드2
1293정성태5/24/201229774.NET Framework: 325. System.Drawing.Bitmap 데이터를 Parallel.For로 처리하는 방법 [2]파일 다운로드1
1292정성태5/24/201223754.NET Framework: 324. First-chance exception에 대해 조건에 따라 디버거가 멈추게 할 수는 없을까? [1]파일 다운로드1
1291정성태5/23/201230276VC++: 62. 배열 초기화를 위한 기계어 코드 확인 [2]
1290정성태5/18/201235081.NET Framework: 323. 관리자 권한이 필요한 작업을 COM+에 대행 [7]파일 다운로드1
1289정성태5/17/201239239.NET Framework: 322. regsvcs.exe로 어셈블리 등록 시 시스템 변경 사항 [5]파일 다운로드2
1288정성태5/17/201226464.NET Framework: 321. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (3) - Type Library파일 다운로드1
1287정성태5/17/201229299.NET Framework: 320. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (2) - .NET 4.0 + .NET 2.0 [2]
1286정성태5/17/201238223.NET Framework: 319. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (1) - .NET 2.0 + x86/x64/AnyCPU [5]
1285정성태5/16/201233265.NET Framework: 318. gacutil.exe로 어셈블리 등록 시 시스템 변경 사항파일 다운로드1
1284정성태5/15/201225697오류 유형: 155. Windows Phone 연결 상태에서 DRIVER POWER STATE FAILURE 블루 스크린 뜨는 현상
1283정성태5/12/201233308.NET Framework: 317. C# 관점에서의 Observer 패턴 구현 [1]파일 다운로드1
1282정성태5/12/201226107Phone: 6. Windows Phone 7 Silverlight에서 Google Map 사용하는 방법 [3]파일 다운로드1
1281정성태5/9/201233193.NET Framework: 316. WPF/Silverlight의 그래픽 단위와 Anti-aliasing 처리를 이해하자 [1]파일 다운로드1
1280정성태5/9/201226157오류 유형: 154. Could not load type 'System.ServiceModel.Activation.HttpModule' from assembly 'System.ServiceModel, ...'.
1279정성태5/9/201224918.NET Framework: 315. 해당 DLL이 Managed인지 / Unmanaged인지 확인하는 방법 [1]파일 다운로드1
1278정성태5/8/201226148오류 유형: 153. Visual Studio 디버깅 - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
... 136  137  138  139  140  141  142  143  144  145  146  147  148  149  [150]  ...