windows下异步IO一

windows 专栏收录该内容
2 篇文章 0 订阅

介绍

简单讲解下我们程序进行IO的过程,当线程进行一个同步的设备IO请求时,他会被挂起,直到设备完成IO请求,返回给阻塞线程,线程激活继续处理。当进行一个异步的设备IO请求时,该线程可以先去做其他事,等到设备完成IO请求后通知该线程进行处理。本文讨论在windows平台下的异步设备IO。同时在一些示例中会对涉及到的知识进行讲解。

1.异步IO执行

进行异步设备io时我们来做一下下准备工作,首先针对不同的设备(文件,管道,套接字,控制台)的初始化和发出IO不太一样,以简单的文件为例,别的应该都是相通的。

1.1 初始化设备(eg.CreateFile)

首先我们来说下在windows下他的api大多数有后缀为W和A两种情况,W表示以unicode(utf-16)字符编码,
A表示以ANSI字符编码,我们以W为例,然后我们使用CreateFile创建文件设备对象,CreateFile也可以用来创建目录,磁盘驱动器,串口,并口等设备对象。这里我们用最简单的文件为例。

WINBASEAPI
HANDLE
WINAPI
CreateFileW(
    _In_ LPCWSTR lpFileName,
    _In_ DWORD dwDesiredAccess,
    _In_ DWORD dwShareMode,
    _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    _In_ DWORD dwCreationDisposition,
    _In_ DWORD dwFlagsAndAttributes,
    _In_opt_ HANDLE hTemplateFile
    );
  • WINBASEAPI宏表示__declspec(dllimport)是用来导入导出时使用
  • HANDLE类型表示内核对象,比如线程,进程,事件,设备等,操作系统来维护的。
  • WINAPI 宏是__stdcall,VC编译器的指令,可以来设置传参的时入栈的参数顺序,栈内数据清除方式,函数签名等
  • lpFileName文件名
  • dwDesiredAccess访问方式,可读、可写等
  • dwShareMode,其他内核对象使用是的共享方式
  • lpSecurityAttributes 安全属性
  • dwCreationDisposition 打开方式,创建还是打开已有等

  • 我们如果使用CreateFile来进行异步IO,我们需要将dwFlagsAndAttributes设置带有FILE_FLAG_OVERLAPPED属性。OVERLAPPED重叠的意思,表示内核线程和应用线程重叠运行。

1.2 执行(eg.ReadFile,WriteFile)

WINBASEAPI
_Must_inspect_result_
BOOL
WINAPI
ReadFile(
    _In_ HANDLE hFile,
    _Out_writes_bytes_to_opt_(nNumberOfBytesToRead, *lpNumberOfBytesRead) __out_data_source(FILE) LPVOID lpBuffer,
    _In_ DWORD nNumberOfBytesToRead,
    _Out_opt_ LPDWORD lpNumberOfBytesRead,
    _Inout_opt_ LPOVERLAPPED lpOverlapped
    );

WINBASEAPI
BOOL
WINAPI
WriteFile(
    _In_ HANDLE hFile,
    _In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer,
    _In_ DWORD nNumberOfBytesToWrite,
    _Out_opt_ LPDWORD lpNumberOfBytesWritten,
    _Inout_opt_ LPOVERLAPPED lpOverlapped
    );

来看下ReadFile的解释

  • hFile即为上一节的设备对象
  • lpBuffer是文件最后读到的缓冲区,或者要写到设备的缓冲区
  • nNumberOfBytesToRead要读取多少字节,nNumberOfBytesToWrite要写多少字节
  • lpNumberOfBytesRead指向一个DWORD的地址,表示最终读取了多少字节,lpNumberOfBytesWritten最终写了多少字节。

然后就是lpOverlapped了,我们来看下LPOVERLAPPED的结构

typedef struct _OVERLAPPED {
    ULONG_PTR Internal;
    ULONG_PTR InternalHigh;
    union {
        struct {
            DWORD Offset;
            DWORD OffsetHigh;
        } DUMMYSTRUCTNAME;
        PVOID Pointer;
    } DUMMYUNIONNAME;

    HANDLE  hEvent;
} OVERLAPPED, *LPOVERLAPPED;
  • Internal用来保存等到已经处理完IO后的错误码
  • InternalHigh用来保存已传输的字节数
  • Offset和InternalHigh构成一个64位的偏移值,表示访问文件从哪里开始访问
  • Pointer系统保留字
  • hEvent用来接收I/O完成通知时使用,后边会说到

2. IO请求完成通知

然后我们来看下,等到IO完成后如何通知到线程中,有四种方式来通知,摘自《windows核心编程》:

方法描述
触发设备内核对象允许一个线程发出IO请求,另一个线程对结果处理,只能同时发出一个IO请求
触发事件内核对象允许一个线程发出IO请求,另一个线程对结果处理 ,能同时发出多个IO请求
可提醒I/O只允许一个线程发出IO请求,须发出请求的线程对结果处理,能同时发出多个IO请求
I/O完成端口循序一个线程发出IO请求,另一个线程对结果处理,能同时发出多个IO请求

2.1 触发设备内核对象

先来看例子:

int main()
{
    HANDLE hFile = CreateFile(L"1.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        std::cout << "open error";
        return -1;
    }

    BYTE bBuffer[1024];
    OVERLAPPED o = { 0 };
    BOOL bReadDone = ReadFile(hFile, bBuffer, 1024, NULL, &o);

    DWORD dwError = GetLastError();
    if (!bReadDone && dwError == ERROR_IO_PENDING) {
        DWORD dw = WaitForSingleObject(hFile, INFINITE);
        bReadDone = TRUE;
    }

    if (bReadDone) {
        std::cout << o.Internal << std::endl;
        std::cout << o.InternalHigh << std::endl;
        bBuffer[o.InternalHigh] = '\0';
        std::cout << bBuffer << std::endl;
    }
    else {
        std::cout << "read error";
        return 0;
    }

    std::cout << "succ";
    return 0;
}

CreateFile用可读可写的权限;用OPEN_ALWAYS的打开方式,表示有文件打开,没有该文件创建文件。
这个例子对一些判断比较完整,我们可以顺便来巩固下基础知识,CreateFile成功返回句柄,失败时返回INVALID_HANDLE_VALUE,而不是像许多windows返回句柄为NULL来表示失败了,但是CreateFile失败返回的是INVALID_HANDLE_VALUE(-1),大家可以注意下。
然后进行初始化,声明的BYTE数组来存放读取到的数据;OVERLAPPED 对象初始化为0,即中的元素值都是0,这里要注意的是Offset为0即为从文件的开头读取数据。
调用ReadFile后,由于是异步的,所以bReadDone 是FALSE,然后获取下错误信息,得知是ERROR_IO_PENDING,表示正在进行IO操作。
最后我们调用WaitForSingleObject(hFile, INFINITE)来等待hFile设备内核对象触发,这里我们大概讲解下关于内核对象触发。

在windows中,内核对象可以用来进行线程同步,内核对象有两个状态:触发和,未触发。比如说线程,进程,他们在创建时是未触发的,运行结束时变为触发状态。在比如Event对象,可以我们写代码来使他的程序变化,后边我们再说。
这里我们说下文件内核对象,ReadFile和WriteFile函数在将IO请求添加到设备的队列之前,会先将状态设为未触发状态,当设备驱动程序完成了所谓请求后,会将对象状态设为触发状态。
再来说WaitForSingleObject函数,就是等待第一个参数(内核对象句柄)状态变成触发,等待时间是第二个参数,等待该时间后或者内核对象状态变成触发该函数返回。

我们先往文件中写入“01234567899876543210”
最后我们打印出来读取结果,依次打印出错误码,读取的字节数,读取内容。另外我们首先在文件中写入了内容。
在这里插入图片描述
这个有一个缺点就是,只能同时处理一个IO请求。

2.2 触发事件内核对象

继续看例子:

static bool readReady = false;
void WaitResultThd(void *param)
{
    HANDLE* hh = (HANDLE*)param;
    DWORD dw = WaitForMultipleObjects(2, hh, TRUE, INFINITE);
    if (dw == WAIT_OBJECT_0) {
        readReady = true;
    }
}

int main()
{
    HANDLE hFile = CreateFile(L"1.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, 
    						  OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        std::cout << "open error";
        return -1;
    }

    BYTE bBuffer1[11] = {0};
    OVERLAPPED o1 = { 0 };
    o1.hEvent = CreateEvent(NULL, FALSE, FALSE, L"");
    o1.Offset = 0;

    ReadFile(hFile, bBuffer1, 10, NULL, &o1);

    BYTE bBuffer2[11] = { 0 };
    OVERLAPPED o2 = { 0 };
    o2.hEvent = CreateEvent(NULL, FALSE, FALSE, L"");
    o2.Offset = 10;

    ReadFile(hFile, bBuffer2, 10, NULL, &o2);

    HANDLE h[2];
    h[0] = o1.hEvent;
    h[1] = o2.hEvent;
    _beginthread(WaitResultThd, 0, h);

    while (1)
    {
        /* do somthing*/
        Sleep(500);

        if (readReady) {
            std::cout << bBuffer1 << std::endl;
            std::cout << bBuffer2 << std::endl;
            break;
        }
    }

    return 0;
}

我们看下这个和上一个的区别是用OVERLAPPED的hEvent变量来实现IO完成的通知,首先CreateEvent为每个OVERLAPPED的变量创建事件内核对象,看下CreateEvent:

WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateEventW(
    _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
    _In_ BOOL bManualReset,
    _In_ BOOL bInitialState,
    _In_opt_ LPCWSTR lpName
    );
  • lpEventAttributes设置的安全属性
  • bManualReset,意为是否为手动重置对象,为TRUE表示手动重置,事件触发时正在等待改事件的所有线程将都变成可调度状态。为FALSE为自动重置,事件触发时只有一个线程变成可调度状态。
  • bInitialState初始状态,TRUE是触发状态,FALSE为未触发状态
  • lpName是可以用次来共享该事件对象

当我们创建成功了时间内核对象时,可以使用SetEvent将其设置为触发状态,可以使用ResetEvent将其设置为未触发状态

我们继续,当异步IO请求完成后,设备驱动程序会检查OVERLAPPED的hEvent是不是为空,如果不是为空,调用SetEvent来触发该对象。
为了演示可以多线程来进行操作,我们开启另一个线程来等待事件完成,使用WaitForMultipleObjects来等待多个事件触发,我们再来看下WaitForMultipleObjects

WINBASEAPI
DWORD
WINAPI
WaitForMultipleObjects(
    _In_ DWORD nCount,
    _In_reads_(nCount) CONST HANDLE* lpHandles,
    _In_ BOOL bWaitAll,
    _In_ DWORD dwMilliseconds
    );
  • nCount表示等待几个对象
  • lpHandles,等待的对象句柄数组
  • bWaitAll,表示是等待所有对象都变成触发状态再返回(TRUE),还是只要有一个对象触发就返回(FALSE)
  • dwMilliseconds 表示等待的时间
    如果bWaitAll为TRUE,返回值为WAIT_OBJECT_0表示全部触发
    如果bWaitAll为FALSE,返回值为WAIT_OBJECT_0表示lpHandles[0]对象触发,WAIT_OBJECT_0 + 1表示lpHandles[1]触发,以此类推。

再继续,我们设置的两次IO读取请求是从文件的不同偏移开始读的,我们来看下读取结果:
在这里插入图片描述

2.3 可提醒的I/O

可提醒IO是使用回调函数来实现,同时执行IO请求的函数有点变化,这里我们介绍RadFileEx和WriteFileEx,我们看下函数原型:

WINBASEAPI
_Must_inspect_result_
BOOL
WINAPI
ReadFileEx(
    _In_ HANDLE hFile,
    _Out_writes_bytes_opt_(nNumberOfBytesToRead) __out_data_source(FILE) LPVOID lpBuffer,
    _In_ DWORD nNumberOfBytesToRead,
    _Inout_ LPOVERLAPPED lpOverlapped,
    _In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );

WINBASEAPI
BOOL
WINAPI
WriteFileEx(
    _In_ HANDLE hFile,
    _In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer,
    _In_ DWORD nNumberOfBytesToWrite,
    _Inout_ LPOVERLAPPED lpOverlapped,
    _In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );

和ReadFile及WriteFile,有几点不一样。

  • 这两个函数没有指向DWORD地址的指针表示已传输多少字节,毕竟在异步中不能立即拿到,该信息在回调函数才能得到
  • lpCompletionRoutine增加了这个参数,即回调函数的函数指针,看下类型:
typedef
VOID
(WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(
    _In_    DWORD dwErrorCode,
    _In_    DWORD dwNumberOfBytesTransfered,
    _Inout_ LPOVERLAPPED lpOverlapped
    );

错误码,传输的字节数,及LPOVERLAPPED 结构。
然后我们来看下例子,通过此来讲解下。

static bool readReady = false;
static BYTE bBuffer1[11] = { 0 };
static BYTE bBuffer2[11] = { 0 };

VOID WINAPI ReadyFunction(ULONG_PTR param)
{
    static int times = 0;
    times++;
    if (times == 2) {
        readReady = true;
    }
}

VOID WINAPI DoWorkRountine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, OVERLAPPED* lpOverlapped)
{
    if (lpOverlapped->Offset == 0) {
        std::cout << bBuffer1 << std::endl;
    }
    else {
        std::cout << bBuffer2 << std::endl;
    }

    QueueUserAPC(ReadyFunction, GetCurrentThread(), NULL);
}

void DoWorkThd(void *param)
{
    HANDLE hFile = CreateFile(L"1.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        std::cout << "open error";
        return;
    }

    OVERLAPPED o1 = { 0 };
    o1.Offset = 0;
    ReadFileEx(hFile, bBuffer1, 10, &o1, DoWorkRountine);

    OVERLAPPED o2 = { 0 };
    o2.Offset = 10;
    ReadFileEx(hFile, bBuffer2, 10, &o2, DoWorkRountine);

    while (1) {
        if (readReady) {
            break;
        }

        SleepEx(500, TRUE);
    }
}

int main()
{
    HANDLE tHandle = (HANDLE)_beginthread(DoWorkThd, 0, NULL);
    WaitForSingleObject(tHandle, INFINITE);
    return 0;
}

我们将DoWorkRountine作为IO完成的回调函数传入,其读出来的数据我们用两个全局变量来缓冲,我们注意到了发起IO请求的线程使用了SleepEx函数进去睡眠,我们看下这个函数:

WINBASEAPI
DWORD
WINAPI
SleepEx(
    _In_ DWORD dwMilliseconds,
    _In_ BOOL bAlertable
    );

和sleep相似,多了一个bAlertable参数,表示是否是可提醒的,如果是可提醒的,那么完成了IO请求完成后就会唤醒线程去执行回调函数。

  • 当系统创建一个线程,会创建一个与线程相关的待执行队列,这个队列被称为异步队列,在此当IO请求完成后,设备驱动程序就会在调用线程的异步队列中添加一项。当线程是可提醒的状态就会被激活去执行相关任务。且如果队列中至少有一项,那么系统就不会让线程进入到睡眠状态,当回调函数返回时,系统判断队列中是否有任务,如果有就会继续取出任务去执行,如果没有其他项,SleepEx等可提醒的函数返回,返回值是WAIT_IO_COMPLETION
  • Sleep函数内部也是调用了SleepEx,只是将bAlertable置为FALSE。其他可以将线程置为可提醒状态的还有WaitForSingleObjectEx,WaitForMultipleObjectEx,SingleObjectAndWaitEx,GetQueuedCompletionStatusEx,MsgWaitForMutipleObjectEx。

QueueUserAPC是允许我们手动往编程里添加任务。原型是:

WINBASEAPI
DWORD
WINAPI
QueueUserAPC(
    _In_ PAPCFUNC pfnAPC,
    _In_ HANDLE hThread,
    _In_ ULONG_PTR dwData
    );
  • pfnAPC是待执行的函数
  • hThread要添加的线程
  • dwData回调函数的自定义参数
    可提醒IO的确定很明显,回调函数没有足够地方存放上下文信息,需要一些全局变量,如我们例子中的bBuffer;第二个就是只能一个线程来完成IO请求和完成通知,不能用上多线程,可能对资源利用率不足。
    最后我们看下运行结果:
    在这里插入图片描述

2.4 注意事项

由于篇幅限制,我们下一篇再讲述完成端口,剩下这里我们说下关于进行异步IO的时候注意事项

  • 当我们发起IO多个请求时,设备驱动程序并不会按照我们请求的顺序去执行(顺序是不一定的),所以大家尽量避免依靠顺序编码。
  • 当我们进行IO请求时,可能会同步返回,这是有可能系统之前有了这一部分的数据就会直接返回,所以大家需要在ReadFile等要判断返回值。
  • 我们在完成IO请求完成之前,一定要保证数据缓存和OVERLAPPED结构的存活,这些是在我们发起IO请求时只会传入地址,完成后会填充改地址的值。所以一定要保证他的存活性。

好了,就到这里了,参考自《windows核心编程》,欢迎交流

  • 5
    点赞
  • 0
    评论
  • 2
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:1024 设计师:我叫白小胖 返回首页

打赏作者

leapmotion

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值