Windows内核编程学习笔记---设备IO

    技术2022-06-12  41

    下面介绍一下设备和它们最常见的用途:

    设备

    最常见的用途

    文件

    任意数据的持久存储

    目录

    属性和文件压缩

    逻辑磁盘

    格式化

    物理磁盘

    分区表访问

    串行口

    通过电话线传输数据

    并行口

    向打印机传送数据

    邮件槽

    一对多的传输数据,通常通过网络向一台Windows机器传输

    命名管道

    一对一的传输数据,通常通过网络向一台Windows机器传输

    无命管道

    在同一台机器上一对一的传输数据(从来不经过网络)

    套接字

    数据传输的块或流,通常通过网络向任一支持套接字的计算机传输(不一定是运行Windows的机器)

    控制台

    文本窗口屏幕缓冲区

    不管你与什么设备通信,Win32的读写设备函数都是一样的。

    16.1打开和关闭设备

    要进行任何I/O操作,必须首先打开所需设备,并得到它的句柄。下表是打开一些设备的方法,

    设备

    打开设备的函数

    文件

    CreateFile(pszName是路径名或UNC路径名)

    目录

    CreateFile(pszName是目录名或UNC目录名,使用FILE_FLAG_BACKUP_SEMANTICS)

    逻辑磁盘

    CreateFile(pszName是“.X:)

    物理磁盘

    CreateFile(pszName“.PHYSICALDRIVEx”)

    串行口

    CreateFile(pszName是“COM?)

    并行口

    CreateFile(pszName是“LPT?)

    邮件槽服务器

    CreateMailslot(pszName是“.mailslotmailslotname);

    邮件槽客户

    CreateFile(pszName是“servernamemailslotmailslotname);

    命名管道服务器

    CreateNamedPipe(pszName是“.pipepipename);

    命名管道客户

    CreateFile(pszName是“servernamepipepipename)

    无命管道客户和服务器

    CreatePipe

    套接字

    SocketacceptAcceptEx

    控制台

    CreateConsoleScreenBufferGetStdHandle

    所有这些函数标识设备的一个32位句柄。

    当你完成设备操作后,必须关闭它。对于大多数设备,可以使用CloseHandle来关闭。如果是套接字要使用int CloseSocket(SOCKET s);来关闭。

    如果你有一个设备的句柄,你可以使用GetFileType来得到设备的类型,

    DWORD GetFileType(HANDLE hFile);它返回如下这些值:

    标志

    说明

    FILE_TYPE_UNKNOWN

    指定的文件类型不清楚

    FILE_TYPE_DISK

    指定文件是一个磁盘文件

    FILE_TYPE_CHAR

    指定文件是一个字符文件,一般是LPT设备或控制台

    FILE_TYPE_PIPE

    指定设备是一个命名或无名管道

    16.1.1  CreateFile函数

    HANDLE CreateFile(LPCTSTR pszName,DWORD dwDesiredAccess,DWORD dwShareMode,LPSECURITY_ATTRIBUTES pSecurityAttributes,DWORD dwCreationDistribution,DWORD DWORD dwFlagsAndAttrs,HANDLE hTemplateFile);

    注:该函数可以打开文件或创建文件,但是它还可以打开很多其它设备。

    参数说明:

    (1).pszName:用来标识设备类型和设备的某个实例。它最大可以为260外字符长,不过可以通过在路径名前加入“?”来使该长度增加到32000个字符。不过该函数不支持相对目录,所以不要在路径名里加“.”或“..”。

    (2).dwDesiredAccess:它指出了你想如何传输数据,如下表所示,你可以指定这些值给它,

    说明

    0

    你不想读写设备数据。当你只想改变设备的配置,如:文件的时间戳时,传递0

    GENERIC_READ

    允许对设备的只读访问

    GENERIC_WRITE

    允许对设备的只写访问。

    GENERIC_READ|GENERIC_WRITE

    允许对设备的读写访问。

    (3).dwShareMode:它指出了设备共享的权限。在网络中一个设备可以被几台计算机同时访问,或被几个进程同时访问。你必须要确定是否要让其它计算机或进程访问设备的数据。

    下表给出了dwShareMode的可能值,

    说明

    0

    你想独占设备的访问权。如果设备已经被打开,CreateFile失败。

    FILE_SHARE_READ

    使用设备只读。如果设备已经被设置成可写,并该设备已经打开,那么CreateFile调用失败。如果打开成功,那么以后以写方式调用CreateFile将会失败。

    FILE_SHARE_WRITE

    让设备为只写。如果设备已经以只读方式打开,那么CreateFile调用失败。如果打开成功,那么将来CreateFile以只读方式打开时,将会失败。

    FILE_SHARE_READ|FILE_SHARE_WRITE

    使设备可读可写。如果设备已经被独占方式打开,那么CreateFile调用失败。如果打开成功,那么以后CreateFile以读写方式都会调用成功。

      

    (4).pSecurityAttributes:该参数允许你设定设备的关联内核对象的安全信息和返回的句柄是否可继承。如果想使用缺省的安全属性和让句柄不可被子进行继承,可以将该参数设置为NULL

    (5).dwCreationDistribution的可能取值如下表所示,

    说明

    CREATE_NEW

    告诉CreateFile创建一个新文件,如果已经有同名文件存在,则失败。

    CREATE_ALWAYS

    告诉CreateFile不管有没有同名文件存在,都创建一个文件。

    OPEN_EXISTING

    告诉CreateFile打开一个已经存在的文件。如果不存在则失败。

    OPEN_ALWAYS

    告诉CreateFile打开一个文件,如果不存在,则创建。

    TRUNCATE_EXISTING

    告诉CreateFile打开一个已经存在的文件,并将其大小设置成0,如果文件不存在,则失败。GENERIC_WRITE必须与该标志一起使用。

    注意:当你使用CreateFile打开的不是文件而是设备时,应当向dwCreationDistribution传递OPEN_EXISTING标志。

    (6).dwFlagsAndAttrs:这个参数有两个作用,一、它允许你微调同设备的通信,二、如果是文件的话,你还可以设置文件的属性。

    (A).下面先说一下在访问设备时需要用到的一些“缓存标志”,

    1.FILE_FLAG_NO_BUFFERING:该标志告诉缓存管理器,不想让缓存管理器缓存任何数据,你自己负责缓存数据。如果不指定这个标志,那么系统就会缓存来往于磁盘的数据,这样,如果你从文件中读了几个字节后,又读了几个字节,文件的数据很可能已经存入内存了,这样只用访问一次磁盘而不是两次,不过,这意味着文件中的数据在内存中保存了两份,也就是说,缓存管理器有一个缓冲区,你调用的某个函数(ReadFile)从缓存管理器中的缓冲区中向你自己的缓冲区中拷贝了一些数据。指定该标志后,系统会将数据写入你提供的缓冲区,你必须遵守以下规则:一、你访问文件时使用偏移量必须是磁盘扇区大小的整数倍。二、你读写的字节数必须是扇区大小的整数倍。三、必须保证进程中的缓冲区开始地址必须是扇区大小的整数倍。

    2.FILE_FLAG_SEQUENTIAL_SCANFILE_FLAG_RONDOM_ACCESS这两个标志只有在你允许系统为你缓存数据时才有用,也就是说,在你没有指定标志FILE_FLAG_NO_BUFFERING时,这两个标志才有用。下面分别说明这两标志,

    FILE_FLAG_SEQUENTIAL_SCAN:它告诉系统你会顺序的访问文件。这时,当你从文件中读数据时,系统实际读的数据要比你要求要读的数据要多。

    FILE_FLAG_RONDOM_ACCESS:该标志告诉系统不要预先读取文件中的数据。

    3.FILE_FLAG_WRITE_THROUGH:该标志使文件写的缓冲区无效,以降低数据丢失的可能性。当这个标志被用于打开网络服务器上的文件时,Win32写文件函数在数据被写到网络服务器的磁盘上之后才返回。

    下面说一下其它一些标志:

    FILE_FLAG_DELETE_ON_CLOSE:该标志告诉系统在关闭文件后删除文件。该标志经常同属性FILE_ATTRIBUTE_TEMPORARY一起使用。当这两个标志一起使用时,你的应用程序可以创建一个临时文件,读、写,而后关闭它,当文件关闭时系统自动删除它。

    FILE_FLAG_BACKUP_SEMANTICS:在对软件进行备份或恢复操作时,可以指定这个标志。使用这个标志的目的:系统为各个文件设置了被访问的权限,在执行备份或恢复操作时,文件的权限会被修改,如果不指定这个标志,那么有可能进程不能访问想要访问的文件。

    FILE_FLAG_POSIX_SEMANTICS:该标志告诉系统使用Posix规则访问文件。Posix使

    用的文件系统区分文件名的大小写。

    FILE_FLAG_OVERLAPPED:该标志告诉系统你想异步访问一个设备。默认情况下,是以同步文件访问一个设备的。在同步访问设备情况下:当从文件中读取数据时,你的应用程序被挂起,直到数据读完,一旦数据读完,你的应用程序就又一次获得控制权,继续执行。异步访问设备情况下:当你调用了一个函数从文件中读取数据后,你不必等到I/O操作结束,你的调用会立即返回,操作系统使用它的线程为你完成读取数据的操作,在读取完数据后,你会得到一个通知。Windows95不支持文件的异步I/O操作。

    (B).下面说明一下“文件属性标志”:

    注意:当你使用CreateFile函数来创建一个文件,并且指定hTemplateFileNULL时,下面这些标志无效。

    标志

    说明

    FILE_ATTRIBUTE_ARCHIVE

    文件是一个存档文件。应用程序使用该标志,来对文件标记为备份或删除。当CreateFile创建一个新文件时,该标志被自动设置。

    FILE_ATTRIBUTE_HIDDEN

    文件是隐藏文件。

    FILE_ATTRIBUTE_NORMAL

    文件没有其它属性集。

    FILE_ATTRIBUTE_READONLY

    文件是只读的。

    FILE_ATTRIBUTE_SYSTEM

    文件是操作系统的一部分,或被操作系统独占使用。

    FILE_ATTRIBUTE_COMPRESSED

    文件或目录是压缩的。

    FILE_ATTRIBUTE_OFFLINE

    文件存在,但是它的数据被移动到离线的存储中。

    FILE_ATTRIBUTE_TEMPRORARY

    文件中的数据只使用很短一段时间。文件系统努力使文件的数据保存在RAM中而不是磁盘中,来使访问的时间尽可能短。

    (7).hTemplateFile:标识一个打开的文件句柄,或是NULL。如果它标识了一个文件句柄,CreateFile将完全忽略在参数dwFlagsAndAttrs参数中设定的属性标志,而是使用同hTemplateFile相关联的文件的属性。由hTemplateFile相关联的文件必须使用GENERIC_READ标志打开,如果CreateFile是打开一个现存的文件而不是创建一个新文件,hTemplateFile参数就被忽略了。

    如果CreateFile成功的创建或打开了一个文件或设备,将返回一个文件或设备的句柄。如果失败将返回INVALID_HANDLE_VALUE

    16.2使用文件设备

    16.2.1定位文件指针:

    CreateFile返回一个文件句柄时,系统给句柄关联了一个文件指针,这个文件指针给出了文件内部一个64位偏移量,下次的同步读写将在此处进行。开始时这个指针被设置成0,所以在你调用CreateFile返回一个文件句柄后,立即调用ReadFile,你将从文件的偏移0处开始读。如果你读取100字节的数据到内存中,系统将会更新与文件句柄关联的指针位置,使得在下一次调用ReadFile时,从文件的第101字节开始读取文件的数据。

    举例说明:

    HANDLE hFile = CreateFile(……);

    ReadFile(hFile,lpBuffer,100,&dwBytesRead,NULL); 是从指针为0处开始读

    WriteFile(hFile,lpBuffer,100,&dwBytesWrite,NULL); 是从指针为100处开始写

    在这段代码中,文件的前100个字节用于执行ReadFile函数,而WriteFile是从文件的第101个字节开始的,这是因为在执行完ReadFile后,与文件关联的指针已经被更新为100了,所以在调用函数WriteFile时,是从文件的第100字节开始写的。

    如果是打开同一文件多次的话,就与上面的情况不一样了,每打开一次文件,与文件句柄关联的指针都会被设置成0,所以如果这样写这段代码,

    HANDLE hFileA = CreateFile(MyFile,);

    HANDLE hFileB = CreateFile(MyFile,);

    ReadFile(hFileA,lpBuffer,100,&dwBytesRead,NULL);是从指针为0处开始读

    WriteFile(hFileB,lpBuffer,100,&dwBytesWrite,NULL);是从指针为0处开始写

    另外介绍一个函数SetFilePointer

    DWORD SetFilePointer(HANDLE hFile,LONG lDistanceToMove,

    PLONG lpDistanceToMoveHigh,DWORD dwMoveMethod);

    参数说明:

    lDistanceToMove告诉系统你想要移动指针多少个字节。注意,你指定的值是增加到指针的当前值,也就是说,你传递给该参数负值的话,等同于将指针向后移。

    LpDistanceToMoveHigh:当你移动指针的范围是正负2GB以内,那么参数lpDistanceToMoveHigh设置为NULL。如果你移动指针的范围是正负180GB,那么你需要在这个值的高32位传递给该参数。实际上,不能直接将这个值的高32位传递给该参数,而是将这个值的高32的地址传递给参数lpDistanceToMoveHigh。这样传递的原因是,SetFilePointer函数返回文件指针以前的位置。如果你感兴趣的是这个指针的低32位,那么SetFilePointer直接返回这个值。如果你对指针的高32位也感兴趣,那么SetFilePointer在返回前填充参数lpDistanceToMoveHigh的值。

    DwMoveMethod:它告诉SetFilePointer如何解释参数lDistanceToMove lpDistanceToMoveHigh,可以传递给该参数三个值,它们是:

    说明

    FILE_BEGIN

    文件指针变成由两个DistanceToMove参数指定的无符号值

    FILE_CURRENT

    文件指针被加上由两个参数DistanceToMove指定的有符号值

    FILE_END

    文件指针变成由文件的字节数加上由两个参数DistanceToMove指定的有符号值

    如果函数SetFilePointer没有改变文件的指针,它将返回0xFFFFFFFFlpDistanceToMoveHigh的缓冲区将包含NULL。最好调用GetError,看GetError是否返回NO_ERROR,来验证SetFilePointer是否成功。

    16.2.2设置文件尾:

    BOOL SetEndOfFile(HANDLE hFile);该函数改变文件长度,例如,你想强制一个文件的长度为1024字节,你可以这样做,

    HANDLE hFile = CreateFile();

    SetFilePointer(hFile,1024,NULL,FILE_BEGIN);

    SetEndOfFile(hFile);

    16.2.3加锁和解锁文件的区域:

    1.文件的加锁和解锁只影响文件的部分区域,使用函数LockFile来锁定一个文件的某一块区域,

    BOOL LockFile(HANDlE hFile,DWORD dwFileOffsetLow,

    DWORD dwFileOffsetHigh,DWORD cbLockLow,DWORD cbLockHigh);

    参数说明:

    hFile是要锁定的文件句柄。

    DwFileOffsetLowdwFileOffsetHigh指定你想开始锁定的文件的64位偏移量。

    DwLockLowdwLockHigh指定你想锁定的字节数。

    举例说明:假如你想锁定数据库中的第一百条记录,

    LockFile(hFile,sizeof(CUSTOMER_RECORD)*(100 - 1),0,

    sizeof(CUSTOMER_RECORD),0);

    如果锁定成功LockFile将返回TRUE。当文件的一个区域被锁时,所有其它想读写该区域的进程都将会失败。

    注意:锁定的区域超过当前的文件尾也是合法的,例如:在你在数据库中增加新记录时。可以锁定超过文件尾的一块区域,之后把新客户数据写到这个区域里。

    还有一点需要注意,你锁定的区域不能包括已经锁定的区域。举例说明:看如下这两个对LockFile函数的调用,第一个会成功调用,第二个会失败,原因是第二LockFile锁定的区域已经包括在第一个LockFile锁定的区域内了,

    LockFile(hFile,sizeof(CUSTOMER_RECORD)*(100 - 1),0,

    sizeof(CUSTOMER_RECORD),0);//锁定第100条记录

    LockFile(hFile,sizeof(CUSTOMER_RECORD)*(100 - 2),0,

    2 * sizeof(CUSTOMER_RECORD),0);//锁定第99条合第100条记录

    很明显,在第二次对LockFile的调用时,也锁定了第100条记录,而在第一调用时,LockFile已经锁定了这条记录,所以第二次对LockFile的调用将失败。

    2.在对锁定的区域操作结束后,需要使用UnLockFile来释放这块锁定的区域,

    BOOL UnLockFile(HANDLE hFile,DWORD dwFileOffsetLow,

    DWORD dwFileOffsetHigh,DWORD dwUnLockLow,DWORD dwUnLockHigh);

    UnLockFile与函数LockFile的参数相同,返回值也一样。当你解锁一个区域时,必须与加锁一个区域时相同。举例说明:

    LockFile(hFile,sizeof(CUSTOMER_RECORD)*(100 - 1),0,

    sizeof(CUSTOMER_RECORD),0);锁定第100条记录

    LockFile(hFile,sizeof(CUSTOMER_RECORD)*(100 - 2),0,

    sizeof(CUSTOMER_RECORD),0);//锁定批99条记录

    下面解锁这两个锁定区域,

    UnLockFile(hFile,sizeof(CUSTOMER_RECORD)*(100 - 1),0,

    sizeof(CUSTOMER_RECORD),0);

    UnLockFile(hFile,sizeof(CUSTOMER_RECORD)*(100 - 1),0,

    sizeof(CUSTOMER_RECORD),0);

    注意:解锁时,必须与加锁时相同,不能用一个UnLockFile把前面两个LockFile加锁的区域一次解锁,加锁与解锁要一一对应。

    还有一点需要注意:在关闭一个文件或终结进程前一定要解锁全部加锁区域。

    3.函数LockFileEx

    LockFileEx比函数LockFile多了两个功能,一、它在锁定文件的某个区域时,可以设定属性以使其它进程可以读该锁定区域,二、可以告诉LockFileEx直接锁定文件成功后返回。第二个功能比起LockFile要好多了,因为当调用LockFile时,如果想锁定的文件区域已经被另一个线程锁定了,那么LockFile会立即返回,但是你可以告诉LockFileEx一直等到获得文件为止才返回。下面列出函数LockFileEx的定义,

    BOOL LockFileEx(HANDLE hFile,DWORD dwFlags,

    DWORD dwReserved,DWORD nNumberOfBytesToLockLow,

    DWORD nNumberOfBytesToLockHigh,

    LPOVERLAPPED lpOverLapped);

    LockFileEx使用OVERLAPPED结构中的成员OffsetOffsetHigh

    下面列出结构OVERLAPPED的定义:

    typedef struct _OVERLAPPED{

    DWORD Internal;

    DWORD InternalHigh;

    DWORD Offset;

    DWORD OffsetHigh;

    HANDLE hEvent; //事件内核对象

    }OVERLAPPED,*LPOVERLAPPED;

    16.3同步I/O

    1.向设备读写数据的函数为:ReadFileWriteFile

    BOOL ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped);

    BOOL WriteFile(HANDLE hFile, CONST VOID * lpBuffer, DWORD nNumberOfBytesToWrite, LPWORD lpNumberOfBytesWrite, LPOVERLAPPED lpOverlapped);

    虽然这两个函数名字中都有File,但是它们都可以被用于任何设备。

    参数说明:

    hFile是你想要访问的文件的句柄。注意:当设备打开时,你不能使用FILE_FLAG_OVERLAPPED标志,否则系统会认为你要异步访问该设备。

    LpBuffer用来指向读入数据的缓冲区或用于向文件中写入数据的缓冲区。

    nNumberOfBytesToReadnNumberOfBytesToWrite分别告诉ReadFileWriteFile从文件中读出和写入多少字节。

    LpNumberOfBytesReadlpNumberOfBytesWrite分别给出一个DWORD的地址,函数将在其中添入成功地向设备传输的字节数。

    lpOverlapped在进行同步I/O时应当为NULL

    注意:只有设备是使用GENERIC_READ标志打开时,才可以使用ReadFile。只有设备是使用GENERIC_WRITE标志打开时,才可以使用WriteFile

    2.向设备强制刷新数据:

    使用函数FlushFileBuffer来强制系统把缓存数据写回设备,

    BOOL FlushFileBuffer(HANDLE hFile);

    该函数强迫同参数hFile标识的设备相关联的缓存数据写回设备。该设备必须使用GENERIC_WRITE打开。

    16.4异步I/O

    Win32有四种技术用于异步I/O。下表简单总结了这几种技术,

    技术

    说明

    使一个设备内核对象变为有信号

    它允许一个线程发出I/O请求,另一个线程进行处理。如果你要对单个设备进行多个同时的I/O请求,这种技术就没有用。

    使一个事件内核对象变为有信号

    允许对单个设备进行多个同时的I/O请求。允许一个线程发出I/O请求,另一个线程进行处理。

    警告I/O

    允许对单个设备进行多个同时的I/O请求。发出I/O请求的线程也必须处理它。

    I/O完成端口

    允许对单个设备进行多个同时的I/O请求。允许一个线程发出I/O请求,另一个线程进行处理。该技术的申缩性和灵活性最好。

    1. 使一个设备内核对象变为有信号

    在异步I/O中要发出请求,还得使用ReadFileWriteFile函数,不过在它们的参数lpOverlapped必须被传递一个被初始化了的OVERLAPPED结构指针。

    Win32使用overlapped(重叠)指出花在执行I/O请求的时间与你的线程做其它事情的时间重叠了。

    当异步I/O时,调用ReadFileWriteFile时,必须分配一个OVERLAPPED结构,并初始化它的OffsetOffsetHighhEvent成员。当设备是文件时,OffsetOffsetHigh才有用,对于其它设备,这两个成员必须被设置成0

    当访问文件时,OffsetOffsetHigh指出了文件中你想要开始I/O操作的64位偏移量,例如:如果你想从一个文件的第123字节开始读取100个字节,可以这样做,

    HANDLE hFile = CreateFile(,FILE_FLAG_OVERLAPPED,);

    BYTE Buffer[100];

    DWORD nNumBytesRead;

    OVERLAPPED Overlapped;

    Overlapped.Offset = 123;

    Overlapped.OffsetHigh = 0;

    Overlapped.hEvent = NULL;

    ReadFile(hFile,bBuffer,sizeof(bBuffer),&nNumBytesRead,&Overlapped);

    ReadFileWriteFile要求在每次异步I/O开始的字节必须在结构OVERLAPPED中指定。这与同步I/O是完全不同的。

    Win32把设备句柄看作是可同步的对象,即它或处于有信号状态,或处于无信号状态。当你调用ReadFileWriteFile时,这些函数首先需要做的事情之一是将设备句柄重设置为无信号状态。当所有数据都被读入或写入设备,系统就将设备句柄置为有信号状态。可以通过调用WaitForSingleObjectWaitForMultpleObjects函数,线程就可以判断异步设备操作何时完成,即设备句柄何时被设置成有信号状态。也可以使用另外一个函数来达到同前两个函数同样的效果,而且这个函数比前两个更好,

    BOOL GetOverlappedResult(HANDLE hFile, LPOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait);

    参数说明:

    前两个参数必须是调用ReadFileWriteFile中的参数值。

    lpcbTransfer是成功传递的字节数。

    FWait表示GetOverlappedResult是否要等待重叠操作完成之后再返回。

    2. 使一个事件内核对象变为有信号

    OVERLAPPED结构中有一个成员hEvent,它是一个事件内核对象。如果你想同时执行多个异步设备I/O请求,你应该为每一个请求创建一个事件。初始化每一个请求的结构OVERLAPPED成员hEvent,再调用ReadFileWriteFile。当你到达了需要与I/O请求的完成同步的那一点时,只要调用WaitForMultipleObjects,传递同每个I/O请求的OVERLAPPED结构相关联的事件句柄。这样,你就可以使用同一设备句柄,而容易可靠地进行多个同时的异步设备I/O操作。

    3.使用警告I/O

    当一个线程创建时,系统还给这个线程分配了一个队列,它叫异步过程调用队列(APCAsynchronous Procedure Call)。线程的异步过程调用队列类似于消息队列,不过消息队列是实现在“用户界面控件”上的消息,而异步过程调用队列是实现在“低层内核”,所以APC队列执行的更有效也更快。

    如何将消息发送到“APC”队列呢?调用ReadFileExWriteFileEx函数来发出I/O请求,这样I/O请求的结果就会被放到APC队列中。还有另一个函数,它允许你人工的向APC队列中添加一项,

    DWORD QueueUserAPC(PAPCFJNC pfnAPC, HANDLE hThread, DWORD dwData);

    参数说明:

    pfnAPC是一个APC函数的指针,该函数如下:VOID WINAPI APCFunc(DWORD dwParam);

    hThread是你想要添加表项的线程的句柄,注意它可以是系统中的任何线程,如果hThread是另一个进程中的线程,那么pfnAPC属于这个线程的进程。

    DwData是传递给回调函数的一个32位值。

    I/O请求结果放入APC队列后,线程并不是马上就能处理这些请求,如果要想这些请求可以被马上处理,就必须使线程处于“警觉状态”,如何让线程处于警觉状态呢?可以使用下面这几个函数实现,

    DWORD SleepEx(DWORD dwTimeOut, BOOL fAlertable);

    DWORD WaitForSingleObjectEx(HANDLE hObject, DWORD dwTimeOut, BOOL fAlertable);

    DWORD WaitForMultipleObjectsEx(DWORD cObjects, LPHANDLE lphObjects, BOOL fWaitAll, DWORD dwTimeOut, BOOL fAlertable);

    DWORD SignalObjectAndWait(HANDLE hObjectToSignal, HANDLE hObjectToWaitOn, DWORD dwMilliseconds, BOOL bAlertable);

    DWORD MsgWaitForMultipleObjectsEx(DWORD nCount, LPHANDLE pHandles, DWORD dwMilliseconds, DWORD dwWakeMask, DWORD dwFlags);

    当调用上面五个函数之一使你的线程进入警觉状态时,系统首先检查线程的APC队列,如果队列中至少有一项,系统就不会让线程休眠,系统会把该项从APC队列中取出来,你的线程调用回调函数,传递给它完成的I/O请求的错误码,传输的字节数和OVERLAPPED结构的地址。当回调函数返回时,系统检查APC队列中是否有更多的项。如果有,它们会被继续处理。如果没有函数调用就会返回。再说一下这五个函数的返回值,如果返回值是WAIT_IO_COMPLETION,那么线程的继续执行就是因为线程的APC队列中至少有一个表项被处理了。如果返回值是别的,就说明线程被唤醒或者是因为睡眠超时了,或者是指定的内核对象变成有信号状态了,或者是一个互斥对象被废弃了。

    4.I/O完成端口

    I/O完成端口使用线程池。

    创建I/O完成端口的函数是CreateIoCompletionPort

    HANDLE CreateIoCompletionPort(HANDLE hFileHandle, HANDLE hExistingCompletionPort, DWORD dwCompletionKey, DWORD dwNumberOfCurrentThreads);

    这个函数创建一个I/O完成端口内核对象,并把这个端口与一个设备关联。要使一个完成端口有用就必须让它与一个设备关联。当使用这个函数创建完一个I/O完成端口时,它同时也创建了五个数据结构(设备列表、I/O完成队列、等待的线程队列、释放线程列表、暂停线程列表)。还有一点,CreateIoCompletionPort是唯一一个创建了内核对象而没有结构SECURITY_ATTRIBUTES的函数。

    参数说明:

    dwNumberOfCurrentThreads告诉I/O完成端口同时能运行的线程数。如果为0,就表示它等于计算机上的CPU数。

    下面是五个数据结构,

    下面分别说明这几个数据结构:

    设备列表:要让一个完成端口有用,你必须把一个设备与完成端口关联。通过调用CreateIoCompletionPort来实现的。

    I/O完成队列:

    当一个设备的异步I/O请求完成时,系统检查该设备是否关联了完成端口。如果是,那么系统就向该设备的完成端口的I/O完成队列加入完成的I/O请求。

    在服务器端的应用程序初始化时,它应该创建I/O完成端口,而后应该创建一个线程池来响应客户端请求。线程池中的这些线程应当使用一个“线程函数”来控制,一般来说,

    这个线程函数在执行完一些初始化工作后就进入了一个循环,在这个循环中线程函数使自己睡眠来等待完成端口的设备I/O请求的完成。那么如何使自己睡眠而等待完成商品的设备I/O请求完成呢?可以使用函数GetQueuedCompletionStatus来完成,

    BOOL GetQueuedCompletionStatus(HANDLE hCompletionPort,

    LPDWORD lpdwNumberOfBytesTransferred,

    LPDWORD lpdwCompletionKey,

    LPOVERLAPPED *lpOverlapped,DWORD dwMilliseconds);

    第一个参数hCompletionPort指出了线程要监视的完成端口。

    简单的说GetQueuedCompletionStatus使调用线程进入睡眠,直到指定的完成端口的I/O完成队列出现了一项或等待超时(同dwMillseconds指定超时时间)。这个函数是以后进先出LIFO的方式被唤醒的。举例说明:假设有4个线程等在线程队列中,如果出现了一个I/O项,最后一个调用GetQueuedCompletionStatus的线程被唤醒来处理这个I/O项,当线程处理完成后,它继续调用GetQueuedCompletionStatus来进入等待线程队列。只要I/O请求完成的够慢,使得一个线程能够处理它们,系统就总是唤醒同一个线程,其他3个线程将继续睡眠,通过使用LIFO算法,不被调度的线程的内存资源就可以被交换到磁盘上。这也就是说,如果你有很多个线程在等,而只有很少的I/O请求完成,那么多余线程的内存资源就可以被系统换出。

    还有一点需要说明的是I/O完成队列是先进先出的FIFO

    再举例说明为什么线程池中的线程数目要多于完成端口的并发线程数目?现在假设我们的机器是双CPU的。我们创建了一个完成端口允许不超过两个线程并发醒来,又创建了4个线程等待完成的I/O请求。如果端口队列中有3个完成的I/O请求,只有两个线程醒来处理这两个请求。现在,如果一个运行着的线程调用了某个函数(Sleep,WaitForSingleObject)使它自己不能运行,I/O完成端口检测到这一点,并立即唤醒第3个线程。最终第1个线程会继续运行,这就使得运行的线程数目大于系统中CPU的数目,不过I/O完成端口会再次检测到这一点,在线程数目少于CPU数目之前,它不会再唤醒其它线程。当线程再次调用GetQueuedCompletionStatus时,线程的数目会降下来。

    4.1模拟完成I/O请求

    BOOL PostQueuedCompletionStatus(HANDLE hCompletionPort, DWORD dwNumberOfBytesTransffered, DWORD dwCompletionKey, LPOVERLAPPED lpOverlapped);

    该函数允许你人工的向一个完成端口的I/O完成队列中加入一个完成I/O请求。

    参数说明:

    hCompletionPort是一个完成端口。其余三个参数是调用GetQueueCompletionStatus的返回值。这个函数可以使你同线程池中的所有线程通信。例如:如果用户想要终止一个服务器端应用程序,你就要让所有的线程安全的干净的退出,但是如果线程都等待在完成端口上,而又没有I/O请求到来,线程就不会醒来。通过对池中的每一个线程调用一次PostQueuedCompletionStatus,线程就会醒来查看GetQueuedCompletionStatus的返回值,线程就会发现应用程序正在终止,这样线程就能正确的清除和结束。

    5.判断I/O请求是否完成

    #define HasOverlappedIoCompleted(lpOverlapped)((lpOverlapped)->Internal != STATUS_PENDING)

    上面的是一个宏,该用于应用程序快速查看一个I/O请求是否完成。完成返回TRUE否则返回FALSE。一旦知道了I/O请求已经完成,就可以调用GetOverlappedResultGetQueuedCompletionStatus来得到操作的具体信息,或者把线程置于警觉状态,这样I/O完成回调函数就有机会被执行了。

    6.取消未完成的I/O请求

    线程取消它对某个设备的所有未完成的I/O请求,使用函数CancelIO来实现,BOOL CancelIO(HANDLE hFile);

    这个函数只取消调用线程所发出的请求。不能取消其它线程发出的请求。如果线程是使用ReadFileExWriteFileEx发出的I/O请求,CancelIO使得线程的APC队列中被加入一项。当线程处于警觉状态时,回调函数被使用错误码ERROR_OPERATION_ABORTED调用。


    最新回复(0)