进程与线程复习知识点
1. 进程
进程就是一个正在运行当中的程序
我们理解进程可以把它当做一个容器来理解,容器里面包含的有线程,其本身并没有执行代码的能力。因此我们在进程的创建之初都会创建一个主线程用于执行代码,如果此主线程结束,系统就会销毁这个进程内核对象
进程简单的分为两种:系统进程和用户进程
系统进程就是用于完成操作系统的各种功能的进程
用户进程就是由用户启动的进程
进程的创建:
我们使用CreateProcess()函数来创建一个进程,其代码如下
int _tmain(int argc, _TCHAR* argv[])
{
STARTUPINFO si = {}; //新进程窗口的特性
PROCESS_INFORMATION pi = {}; //新进程信息结构体
CreateProcess(
L"E:\\BaiduNetdisk\\BaiduNetdisk.exe", //可执行的文件名,路径
NULL, //命令行
NULL,
NULL,
FALSE, //句柄继承
NULL,
NULL,
NULL,
&si, //子进程创建配置结构体,此结构体可以详细控制子进程的各种创建状态
&pi //返回进程创建的详细信息
);
WaitForSingleObject(pi.hProcess, INFINITE);
return 0;
}
2. 线程
一个线程就是操作系统的一个内核对象,至与什么是内核对象,我们后面详细去分析
在Windows操作系统内核中没有进程的概念,只有线程的概念,进程只不过是在逻辑上对一组线程及其相关的资源进行的一种封装
线程的创建
我们使用CreateThread去创建线程
我们创建线程的时候,也必须像主线程的初始函数一样,所以Windows对线程函数做了限定,原型如下:(我们写线程函数模式就应该按照下面模式写!)
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
........
return 0 ;
}
实例:
DWORD WINAPI ThreadPro(LPVOID lpParam){
int i = 0;
while (1)
{
printf("%d\n", i++);
Sleep(100);
}
return 0;
}
//我们可以规定程序的主线程在哪里执行,默认的是main函数
//#pragma comment(linker, "/entry:\"ThreadPro\"")
int _tmain(int argc, _TCHAR* argv[])
{
DWORD dwThreadID = 0;
CreateThread(
NULL, //默认安全属性
0, //默认堆栈大小
ThreadPro, //线程函数
NULL, //参数
0, //默认创建标志
&dwThreadID); //返回TID,也就是新创建线程的ID
while (1)
{
printf("我是主线程\n");
Sleep(100);
}
return 0;
}
上面代码有句,//#pragma comment(linker, "/entry:\"ThreadPro\"")
我们可以规定主函数的入口是谁,这句代码后面写要替代的函数名
3. 内核对象
在解释什么是内核对象之前,先来解释下windows操作系统中对象都有哪些,windows操作系统是一个面向对象的操作系统,之前说的窗口,设备环境,进程,画笔,画刷都是对象,他们都是一个个的结构体,我们去访问他们只能靠得到他们的句柄再去调用相应的API操作对象,所以我们就可以说句柄就是对象的索引,根据使用情况我们可以分为三种对象,user对象,GDI对象,内核对象
常见的内核对象有:
进程 | 线程 | 访问令牌 | 文件 | 文件映射 |
I/O完成端口 | 邮槽 | 管道 | 互斥体 | 信号量 |
事件 | 计时器 | 线程池 |
这里分析下内核对象的公共特点:
内核对象的使用规则也是有套路可寻
- 第一步:创建对象
创建一个内核对象,一般都是使用CreateXXX的方式:
CreateProcess | 创建进程 | CreateThread | 创建线程 |
CreateFile | 创建文件 | CreateEvent | 创建事件对象 |
- 第二步:打开对象,得到句柄
- 第三步:通过API访问对象
- 第四步:关闭句柄
- 第五步:句柄全部关闭,对象自动销毁
内核对象都属于操作系统内核,可以在不同的进程间访问到,俗称:内核对象是跨进程的
很多时候我们可以在不同的进程中访问同一个内核对象(进程间共享数据)
每一个内核对象都有一个引用计数,当有一个进程创建或者打开了此内核对象,那么内核对象的引用数自增1,进程终止,或者关闭了句柄,引用书-1,当引用数为0的时候,内核对象自动销毁,这里也印证了上面我们所说的内核对象的使用规则第五步,为什么是句柄全部关闭,对象才自动销毁
举例:一个内核对象M在进程A创建,另一个进程B也使用了此内核对象,进程A退出后,M并不销毁,因为这时候引用计数不为0,它还在进程B中使用,只有B中也关闭,内核对象才会被销毁
内核对象的句柄
在Windows操作系统,操作对象只能通过句柄来操作,对于内核对象来说,内核对象的句柄和进程是相关的,对于同一个对象来说,不同的进程有不同的句柄,这点和GDI对象不同,GDI的句柄是全局有效的,在不同的进程中,可以使用同一句柄值访问同一个GDI对象,由此可见,不同的类型的对象,其管理方式也不同
在每一个进程对象中,都有一个句柄表。用于记录本进程所打开的所有内核对象,可以简单理解为句柄表是一个一维数组,句柄的值可以看做句柄表中的索引,内核对象的句柄值,只对本进程有效(我们每次开起一个程序,创建一个进程,它的句柄表都会发生变化。因为他是内核对象)
内核对象的跨进程访问
其他的进程有三种形式跨进程访问内核对象
1.由父进程继承子进程,当父进程创建子进程时,若指定了继承句柄的属性,则子进程能够将父进程所有可以继承的句柄全部继承到自己的句柄表中,但是
即使继承了句柄,子进程却不知道自己继承了谁,句柄值是什么,这只能由父进程通过进程间通讯的方式告诉他。
2.在进程A中创建内核对象的时候,给内核对象命名,在进程B中,通过名字打开内核对象,假如内核对象不能命名或者没有一个标识,也就无法使用这种方式
3.使用DuplicateHandle() 函数,将一个句柄从一个进程传递给另一个进程
上面说的是,如果在两个进程中进行跨进程访问,跨进程访问必须通过句柄访问,下面说的是,如果在一个进程的句柄表中添加句柄
所以综上所述,一个进程的句柄表中添加句柄的方式有四种:
1.创建对象时就被打开 如CreateFile,在创建一个新文件,就同时打开了那个文件对象
2.子进程继承父进程句柄表中的句柄
3.显示打开,OpenFile , OpenMutex , OpenProcess显示打开某个对象
4.DuplicateHandle这个API间接打开对象,获得句柄
以上四条,第一条就是创建一个对象,这个进程肯定多一个句柄,第二条,子进程继承父进程,就增加了父进程中的句柄表,第三条,我们可以根据名字打开对象,从而多出一个句柄,第四条,使用DuplicateHandle函数拷贝一个句柄过来
句柄的属性有重要的一条,是否可以继承给子进程
4. 在论进程
我们这里在来深入讨论什么是进程,进程包含什么,怎么控制它......
我们喜欢把一个exe文件认为是一个进程,但是进程所拥有的东西远远多于这个exe文件,他至少包含了:
- 一个虚拟的内存空间
- 在内存空间中,有映射进来的.exe文件,以及所有和程序运行相关的.dll文件,映射在内存中的exe与dll我们称之为模块
- 进程内核对象,操作系统使用此对象来管理进程,内核对象中至少包含了,进程的内核对象句柄表,进程的权限,进程的全局唯一ID值
- 至少一个运行着的线程
这里总结下,进程中有什么:一个虚拟的内存空间,这个内存空间中保存着exe文件和dll文件 (我们将exe与dll成为模块), 还有进程内核对象(也就是一个结构体)
操作系统就是通过这个对象来操纵管理进程,内核对象中肯定包含进程内核对象的句柄表,进程的权限,和进程的ID。当然进程肯定至少有一个线程
进程控制
进程的控制主要是通过一组进程,线程API来实现。这里不一一详述 P14页
5. 进程与模块的遍历
我们要知道某一个时刻的进程有哪些,我们必须将某一时刻记录下来,否则进程随时都在变化,我们不能准确的得到所有的进程都有哪些,所以就有了快照这一机制
int _tmain(int argc, _TCHAR* argv[])
{
//1.创建快照,得到一个快照句柄
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
HANDLE hSnapshot = CreateToolhelp32Snapshot(
TH32CS_SNAPPROCESS,//我们要建立的是进程快照
0 //只有当建立模块快照的时候,这个参数才有用
//建立哪一个进程的模块快照
);
//2.循环遍历,找到所有进程
Process32First(hSnapshot, &pe);
do
{
printf("父进程ID:%d ", pe.th32ParentProcessID);
printf("进程ID: %d ", pe.th32ProcessID);
printf("进程名:%S \n", pe.szExeFile);
} while (Process32Next(hSnapshot, &pe));
system("pause");
return 0;
}
遍历模块
bool GetModuleList(DWORD dwPId) {
HANDLE hModuleSnap = INVALID_HANDLE_VALUE;
MODULEENTRY32 me32 = { sizeof(MODULEENTRY32) };
// 1. 创建一个模块相关的快照句柄
hModuleSnap = CreateToolhelp32Snapshot(
TH32CS_SNAPMODULE, // 指定快照的类型
dwPId); // 指定进程
if (hModuleSnap == INVALID_HANDLE_VALUE)
return false;
// 2. 通过模块快照句柄获取第一个模块信息
if (!Module32First(hModuleSnap, &me32)) {
CloseHandle(hModuleSnap);
return false;
}
// 3. 循环获取模块信息
do {
printf("模块名称:%S ", me32.szModule);
printf("模块句柄:%x\n", me32.hModule);;
//me32.th32ProcessID;
//...
} while (Module32Next(hModuleSnap, &me32));
// 4. 关闭句柄并退出函数
CloseHandle(hModuleSnap);
return true;
}
int _tmain(int argc, _TCHAR* argv[])
{
GetModuleList(24076);
system("pause");
return 0;
}
5. 进程间的通讯
一个进程中有多个线程是可以共享进程资源,有时我们需要在进程间传递信息,进程间通讯方式有很多,例如:
- WM_COPYDATA
它与其他消息不同,其他消息携带两个固定参数的消息,而WM_COPYDATA则可以发送一个大体积的消息参数
在发送WM_COPYDATA时候,wParam应该保存有发送此消息的窗口句柄,lParam则应该指向一个名为COPYDATASTRUCT的结构体
来看看COPYDATASTRUCT结构体有什么
typedef struct tagCOPYDATASTRUCT {
ULONG_PTR dwData; //任意一个32位的值
DWORD cbData; //发送数据的大小
PVOID lpData; //待发送数据块指针
} COPYDATASTRUCT , *PCOPYDATASTRUCT;
这个结构体中lpData是一个指针,看到指针我们不难理解,它指向的空间就是我们附加的消息,这样我们收到COPYDATASTRUCT 后,就可以从lpData中提取数据,但是,别忘了这个结构体是由lParam发送过去的,我们在发送消息的时候应该应该在lparam中发送这个结构体的地址
Pis:需要注意一点,WM_COPYDATA的数据会被发送到目标进程的栈空间,所以发送消息的数据量不宜过大
下面是简单的示例
发送数据
COPYDATASTRUCT stcCDS = {0X12345678 , 16, L"15PB"}; //这里我们设置这个结构体,传入简单的数据,并没有传数据块指针
HWND hWnd = : :FindWindow(NULL,L"远程目标窗口名");
: :SendMessage(hWnd ,WM_COPYDATA ,(WPARAM) m_hWnd ,(LPARAM)&stcCDS); //第三个参数是发送此消息的窗口句柄,第四个参数是保存数 据块的结构体,也就是附加消息,这里传结构体地址
接受数据
先在主窗口内添加WM_COPYDATA消息相应函数
MessageBox( (LPCWSTR)pCopyDataStruct->lpData );
- 邮槽
邮槽是进程间的通信方式,邮槽的通讯是单向的,只有服务端才能从邮槽中读取信息,客户端只能写入消息 (消息的保存以队列形式)
我们了解下它的API函数
API | 说明 |
CreateMailslot | 创建一个邮槽 |
GetMailslotInfo | 获取邮槽信息 |
CreateFile | 打开文件(打开邮槽) |
WriteFile | 写入文件 |
ReadFile | 读取文件 |
6. 线程回顾
CreateThread | 创建线程 |
OpenThread | 打开线程 |
ExitThread | 退出本线程 |
TerminateThread | 结束其他线程 |
SuspendThread | 暂停线程 |
ResumeThread | 恢复线程 |
<wiz_code_mirror>
//FindWindow 找到的是窗口句柄(User对象)
//我们要得到的进程对象的句柄(Kernel对象)
HANDLE hProcess =
OpenProcess(PROCESS_ALL_ACCESS, FALSE, 10904);
TerminateProcess(hProcess,0 );
上面我们看到,我们要关闭一个进程的窗口,我们必须知道他的句柄,如果用FindWindow是错误的,这里找到的是窗口句柄,我们要找的是进程句柄,也就是内核对象
线程遍历
之前有了进程遍历,现在去进行线程遍历就变得简单,当然我们还要用到创建快照的方式
int a = 1;
VOID ListProcessThreads(DWORD dwPID){
HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
THREADENTRY32 te32;
// 创建快照
hThreadSnap =
CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
// 设置输入参数,结构的大小
te32.dwSize = sizeof(THREADENTRY32);
// 开始获取信息
Thread32First(hThreadSnap, &te32);
do {
if (te32.th32OwnerProcessID == dwPID)
{
printf("线程ID:%d", te32.th32ThreadID);
printf("\n");
}
a++;
} while (Thread32Next(hThreadSnap, &te32));
CloseHandle(hThreadSnap);
}
int _tmain(int argc, _TCHAR* argv[])
{
ListProcessThreads(2952);
return 0;
}
线程内核对象
线程内核对象这个数据结构中存放了一些重要的消息
- 线程上下文
我们可以理解为设备环境
对于CPU而言,系统中有一大堆的线程等着被执行,CPU是根据线程优先级去决定执行哪一个线程,在执行线程之前,CPU为线程分配一个时间片,当线程执行起来,耗尽了时间片,就会执行另一个线程,如此往返,使得线程被轮流执行,但是我们这里想想,一个线程在退出CPU的时候,是不是应该去保存一下他当前的状态,以便下一次执行的时候,我们可以按照之前继续执行,所以就有了线程环境这一概念,在线程的内核对象中,有这样一个结构体来保存线程环境,当然我们可以通过API获取线程环境,设置线程环境
- 暂停次数
一个线程可以被暂停的,但是一个线程能够被暂停多少次就是不那么好理解,在线程的内核对象中有记录的,暂停一次暂停次数+1,恢复暂停,暂停次数就-1,直到0就可以运行起来
7. 信号
信号也就是本身的一个状态,我们前面用到的WaitForSingleObject(),就是来检查这个状态,如果状态为FALSE,也就是没有信号,称为非激发态。非激发态等待函数就会等下去,
状态如果为TRUE,反之我们称为激发态,等待函数就返回,当线程结束的时候,就会由没有信号变为由信号,WaitForSingleObject()函数就能够等到线程结束时在返回
总结:有信号就不等,没有信号就等待线程结束
8. 线程调度
线程优先级,简单的说,每一个线程都有其本身的线程优先级,优先级就是windows系统先执行哪个后执行哪个的一个参考。优先级的范围(0-31)
而windows系统不允许我们直接去操作线程优先级,而是通过进程优先级与线程优先级的方式,由操作系统自己的出线程优先级的数值,(线程优先级是被进程优先级所控制的)。进程优先级会让这个进程中的线程由一个主基调。
优先级和优先级值是不一样的,我们只能说,在一个进程中修改了它的优先级,它的线程优先级不会变,但是优先级值会变化
9. 线程同步
我们知道了有了线程这个概念就可以在windows系统同一时间内做不同的事情,但是做不同的事情直接,如果这些事情由一定的关系,那么如何去维护这些关系就成了一个问题
P26页代码就暴漏了这个问题
书上以及对线程同步的问题进行了解释,因此我们就提出了原子操作这一概念
原子操作:指的是一个线程对于某一个资源做操作的时候,能够保证没有其他的线程能够对此资源进行访问
但是原子操作都是一些基本的算术运算操作
临界区
我们用原子操作解决了书上想要解决的问题,但是我们的希望是解决对一段代码的维护,而不单单是对算术的简单操作
而对一段代码的原子操作,使用临界区就可以解决我们的要求
//使用临界区,必须先初始化一个临界区对象
CRITICAL_SECTION cs;
//多个线程进入临界区,同一时刻只能有一个线程进入,其他线程都等待
//只有当进入临界区的线程,离开临界区,其他线程才能够开始抢占进入,但始终只有一个能进入。
//保护的代码并非越多越好,因为保护的代码太多,就会造成其他线程长时间等待,
//就失去了多线程的高效性。
int g_n;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter){
for (int i = 0; i < 100000; i++)
{
EnterCriticalSection(&cs);
g_n++;
//有100行代码
LeaveCriticalSection(&cs);
}
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter){
for (int i = 0; i < 100000; i++)
{
EnterCriticalSection(&cs);
g_n++;
LeaveCriticalSection(&cs);
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[]){
HANDLE hThread1 = 0, hThread2;
InitializeCriticalSection(&cs);
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
WaitForSingleObject(hThread1, -1);
WaitForSingleObject(hThread2, -1);
printf("%d", g_n);
return 0;
}
等待函数
。。。
互斥体
互斥体是解决临界区的问题而生的,那么临界区的问题有哪些?临界区是在一个进程内有效,无法在多线程中同步,但是当一个进程刚进入临界区,就被某些原因崩溃了,那么临界区就无法被释放,那么其他的进程也就无法进入临界区,会被卡主。
int g_n;
HANDLE hMutex = INVALID_HANDLE_VALUE;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter){
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(hMutex, -1); //等待互斥体
g_n++;
printf("线程1:%d\n", g_n);
Sleep(100);
ReleaseMutex(hMutex); //互斥体解锁
}
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter){
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(hMutex, -1);
g_n++;
printf("线程2:%d\n", g_n);
//Sleep(100);
ReleaseMutex(hMutex);
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[]){
HANDLE hThread1 = 0, hThread2;
hMutex = CreateMutex(NULL, FALSE, NULL); //创建互斥体
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
WaitForSingleObject(hThread1, -1);
WaitForSingleObject(hThread2, -1);
printf("%d", g_n);
return 0;
}
互斥体是一个内核对象,所以在多进程间可以同步,当锁住互斥体的线程意外崩溃,没有调用ReleaseMutex,互斥体就会自动设置为不被任何线程拥有,被处于激发态(直接返回)
信号量(内核对象)
他是互斥体的进化版,互斥体只有一个锁孔,信号量有很多锁孔,他只有所有锁孔都被锁住,才不允许其他线程访问信号量锁住的区域
int g_n;
HANDLE g_hSemaphore = 0;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter){
for (int i = 0; i < 100000; i++)
{
LONG nCount = 0; //原来信号数
WaitForSingleObject(g_hSemaphore, -1); //等待信号量
g_n++;
ReleaseSemaphore(g_hSemaphore, 1, &nCount); //解锁信号量
}
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter){
for (int i = 0; i < 100000; i++)
{
LONG nCount = 0;
WaitForSingleObject(g_hSemaphore, -1);
g_n++;
ReleaseSemaphore(g_hSemaphore, 1, &nCount);
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[]){
HANDLE hThread1 = 0, hThread2;
g_hSemaphore = CreateSemaphore(NULL,1,1,NULL); //创建信号量
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
WaitForSingleObject(hThread1, -1);
WaitForSingleObject(hThread2, -1);
printf("%d", g_n);
return 0;
}
信号量有一个当前信号数,只要当前信号数不为0,则信号量就为激发态
这里解释下为什么,当前信号数不为0,信号量就为激发态当当前信号数不为0,那么信号量的锁孔并没有全部堵上,意为这时候可以有线程进入信号量,那么肯定有WaitForSingleObject返回,返回了即为有信号才返回,只有返回其他的线程才能进入,所以说信号量不为0的时候,信号量为激发态
当信号量为0的时候,信号量的锁孔全部被锁住,那么WaitForSingleObject函数不会直接返回,所以这时候为未激发态,没有信号
事件
int g_n;
HANDLE g_hEvent;
DWORD WINAPI ThreadPro1(LPVOID lpThreadParameter){
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(g_hEvent, -1);
g_n++;
SetEvent(g_hEvent);
}
return 0;
}
DWORD WINAPI ThreadPro2(LPVOID lpThreadParameter){
for (int i = 0; i < 100000; i++)
{
WaitForSingleObject(g_hEvent, -1);
g_n++;
SetEvent(g_hEvent);
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[]){
HANDLE hThread1 = 0, hThread2;
g_hEvent = CreateEvent(
NULL,
FALSE,//FALSE:自动设置,TRUE:手动设置
TRUE,//FALSE:初始为非激发态 TRUE:初始为激发态
NULL //名字
);
hThread1 = CreateThread(NULL, NULL, ThreadPro1, NULL, NULL, NULL);
hThread2 = CreateThread(NULL, NULL, ThreadPro2, NULL, NULL, NULL);
WaitForSingleObject(hThread1, -1);
WaitForSingleObject(hThread2, -1);
printf("%d", g_n);
return 0;
}
对于自动设置时间对象,等待函数返回的时候,会自动将其状态设置为非激发态,从而阻塞其他线程
对于互斥体,信号量以及事件的原理分析:
其实三者的代码流程套路都是一样的,先声明一个HANDLE句柄,
在主函数里面去创建这个内核对象,CreateXXX,然后创建线程,调用回调函数,在回调函数里面就有了固定的流程套路
在要独立运行的代码前加 WaitForSingleObject(g_hEvent, -1); 然后写入保护代码,之后再调用相关函数,
信号量写解锁信号量 ReleaseSemaphore(g_hSemaphore, 1, &nCount);
互斥体写解锁互斥体 ReleaseMutex(hMutex);
事件写 SetEvent(g_hEvent);
在回调函数里面的WaitForSingleObject()函数作用就是将激发态设置为非激发态,如果是非激发态就等待
总结
原子操作:只能对4个字节的数据进行运算操作
临界区:对一段代码的保护操作,只能在一个进程中的不同线程进行使用,无法检测由于线程崩溃的临界区无法释放问题,有线程拥有者的概念
互斥体:是一个内核对象,可以在不同进程的线程中实现一段代码的维护,但是只能对一个线程进行保护,也有线程拥有者概念
信号量:是一个内核对象,没有拥有者的概念,可以控制多个线程同时访问被保护的代码,并且线程数量有一个上限
事件: 是一个内核对象,没有拥有者的概念,自主性非常强
10. 文件I/O:
重叠IO
文件IO想要表达的是同一时间我们即可以正常运行我们的程序,又可以使我们程序中的读取文件操作异步进行
程序在读取文件的时候,读取文件函数会马上返回,这样程序就会继续运行,但是读取文件的操作并未如愿完成,重叠IO的机制就是,读取文件函数返回后,我们让系统底层去处理这些文件的读取操作,我们程序正常运行,但是我们如何知道文件有没有读取完呢?这时候重叠IO的核心机制就会帮我们解决这个问题
在实现重叠IO功能之前,先来了解文件操作的基础函数
创建一个文件 返回值 文件句柄
CreateFile(
LPCTSTR lpFileName, //文件名
DWORD dwDesiredAccess, //创建方式 //这里有个创建方式 我们在创建文件的时候就需要声明,他是一个支持异步IO功能的文件,即写ILE_FLAG_OVERLAPPED
DWORD dwShareMode, //共享方式
LPSECURITY _ATTRIBUTES //安全属性
)
读取文件
ReadFile(
HANDEL hFile, //文件句柄
LPVOID lpBuffer, //缓冲区
DWORD nNumberOfBytesToRead //要读取的字节数
LPDWORD lpNumberOfBytesRead //实际读取的字节数
LPOVERLAPPED lpOverlapped //重叠结构 这里要输入一个重叠结构体,这个结构体也就是重叠IO的核心机制
);
写入文件
WriteFile(
HANDLE hFile //文件句柄
LPCVOID lpBuffer //缓冲区
DWORD nNumberOfBytesToWrite //要读取的字节数
LPDWORD lpNumberOfBytesWritten //实际读取的字节数
LPOVERLAPPED lpOverlapped //重叠结构 作用和读文件作用一样,我们要实现异步IO这一步必不可少
)
我们在读取文件和写入文件的时候,都是以文件句柄为条件去打开文件,如果一个句柄是以重叠IO的方式打开,那么他就具有两个特性:
1.句柄变为可等待的对象,具有了激发态和非激发态
2.文件指针这时候就失效了(原因可能是现在文件读取操作权归系统所有,我们无法去干预它,也就无法用指针),我们需要用OVERLAPPED结构体中的Offect表示读取或者写入的位置
而这里我们就不能使用SetFilePointer这样类似函数
我们如何获取重叠IO是否完成,有三种方法
直接等待句柄
等待Overlapped中hEvent句柄
使用异步调用方式获得重叠IO方式,APC调用(调用回调函数)
·
直接等待句柄
使用自己的文件句柄作为等待对象,当IO操作结束后,句柄处于激发态
WaitForSingleObject(hFile,-1);
GetOverlappedResult(); 使用它可以得知IO是否完成
等待事件对象
OVERLAPPED结构体的hEvent参数是NULL,IO完成后,句柄就变为激发态,但是创建一个事件对象,赋值给hEvent,等IO完成后,事件就变为激发态
int _tmain(int argc, _TCHAR* argv[])
{
DWORD dwRealSize = 0;
HANDLE hFile = CreateFile(
L"E:\\8_安装文件\\Win7系统\\cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408.iso",
FILE_GENERIC_READ,
NULL,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,//这就创建了一个支持异步IO的句柄
NULL
);
char *buf = new char[1024 * 1024 * 1024];
OVERLAPPED ol = {};
ol.hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); // 激发态 自动
ReadFile(
hFile,//文件句柄
buf,
1024 * 1024 * 1024,
&dwRealSize,
&ol
);
printf("%d\n", buf[1024 * 1024 * 500]);
WaitForSingleObject(ol.hEvent, -1);
printf("%d\n", buf[1024 * 1024 * 500]);
return 0;
}
上面是通过等待句柄,和等待事件对象的方式来检测重叠IO是否完成
我们还可以使用异步调用的方式来检测重叠IO是否完成
<wiz_code_mirror>
struct MYOVERLAPPED
{
OVERLAPPED ol;
//下面的内容自己随便定义
char * buf;
};
VOID WINAPI ReadProc(
_In_ DWORD dwErrorCode,
_In_ DWORD dwNumberOfBytesTransfered,//获取到传输了多少字节
_Inout_ LPOVERLAPPED lpOverlapped //获取到的是调用的时候传的OVERLAPPED结构体
)
{
MYOVERLAPPED * poi = (MYOVERLAPPED*)lpOverlapped;
printf("%d", poi->buf[0]);
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD dwRealSize = 0;
HANDLE hFile = CreateFile(
L"E:\\8_安装文件\\Win7系统\\cn_windows_7_ultimate_with_sp1_x64_dvd_u_677408.iso",
FILE_GENERIC_READ,
NULL,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,//这就创建了一个支持异步IO的句柄
NULL
);
char *buf = new char[1024 * 1024 * 1024];
MYOVERLAPPED* pol = new MYOVERLAPPED;
ZeroMemory(pol, sizeof(MYOVERLAPPED));
pol->buf = buf;
ReadFileEx(
hFile,//文件句柄
buf,
1024 * 1024 * 1024,
(LPOVERLAPPED)pol,
ReadProc
);
SleepEx(1, TRUE);//这种模型比之前的模型要科学
//我们在想要处理读取数据的地方,直接调用一下SleepEx就可以去处理了
//基本模型还是需要长时间的等待。
return 0;
}
这里我们在OVERLAPPED中的事件对象填与不填都没有意义(因为我们现在使用的异步调用的方式去检测重叠IO是否完成,说简单点就是我们的重叠IO如果完成,他就会去调用回调函数)
调用回调函数的线程依旧是发起重叠IO的线程!我们明明是要去异步的做这件事,但是做了半天发现还是我们这个线程在处理事情,这里系统会帮我们去平衡他们之间的关系,这里我们不用去深入思考,但是我们需要去在线程的函数下面调用SleepEx(1,TRUE),去等待一下回调函数的时间,使得得去数据更加准确