Windows程序设计6(内存、线程、进程)
一、 Windows 内存管理
- 地址空间:32位操作系统空间0~ 2^32-1(4G),地址空间越大程序越易于编写。
- 地址空间的划分:
2.1 用户地址空间 0 - 2G(0x7FFFFFFF )存放用户的程序和数据。用户空间的代码是不能访问内核空间的数据和代码。
2.1.1 空指针区(NULL区,0-64K)系统将地址小于64K指针,都认为是空指针。
2.1.2 用户区 64K~ 0x7FFEFFFF,存放用户程序的代码和数据
2.1.3 64K禁入区(0x7FFEFFFF - 0x7FFFFFFF )
2.2 内核地址空间 2G - 4G
存放内核的代码和数据,例如系统驱动。
内核空间代码是可以访问用户空间。
- 区域:连续的一块内存。区域的大小一般为64K或者64K倍数。每个区域都有自己的状态
1)空闲:没有被使用
2)私有:被预定的区域
3)映像:存放代码
4)映射:存放数据
- 物理内存(半导体,内存条):系统可以使用的实际内存。CPU可以直接访问的内存。
- 虚拟内存(磁盘交换文件):将硬盘文件虚拟成内存使用。(pagefile.sys 文件)CPU如果要访问虚拟内存数据,必须将虚拟内存数据放到物理内存。经常使用的数据存放在物理内存中,不经常使用的数据存放在虚拟内存中。
- 内存页:系统管理内存的最小单位。内存页大小为4K,每个内存页有自己的权限。
- 页目表:
- 指针地址
31 22 21 12 11 0
|------------------|------------------|-------------------|
10位 10位 12位
2^10=1024 1024 4K
页目 页表
包含1K个页表 包含1K个页 包含4K个字节
页目中包含1K项,每项对应一个页表,页表包含1K个项,每项对应一个页,页中包含4k字节 ---- 1K*1K*4K=4 G
- 内存获取数据,访问过程
a) CPU根据地址在物理内存中查找相应的位置。如果找到物理内存,取回数据。如果未找到,执行b)。
b) 根据地址去虚拟内存中查找相应的位置。如果未找到,那么该地址没有内存空间,返回错误(野指针)。如果找到,执行c)。
c) 将该地址所在内存页,置换到物理内存中,同时将原物理内存数据,存入到虚拟内存中。
d) 将物理内存中的数据返回给使用者。
- 内存分配
a) 虚拟内存分配-适合大内存分配,一般是1M之上的内存。
b) 堆内存分配-适合小内存分配,一般是1M以下的内存。malloc/new
c) 栈内存分配-适合小内存分配,一般是1M以下的内存。
d)
- 虚拟内存:
a) 虚拟内存分配:速度快,大内存效率高。将内存和地址分配分别执行,可以先分配内存地址,在需要的时候再将地址绑定(提交)到内存。常用字大型电子表格等处理。
b) 虚拟内存的分配:
LPVOID VirtualAlloc(
LPVOID lpAddress,// NULL或提交地址
SIZE_T dwSize, //分配的大小
DWORD flAllocationType, //分配方式
DWORD flProtect //内存访问方式
); //分配成功返回虚拟内存地址,失败返回NULL。
flAllocationType分配方式:
MEM_COMMIT - 提交内存分配之后返回地址和内存空间.(内存和地址同时分配)
MEM_RESERVE- 保留地址,分配之后只返回地址,内存空间不生成(不绑定到内存)。要使用内存必须再次提交执行,即再次执行VirtualAlloc(第一次返回的提交地址, ** , MEM_COMMIT , ** )。
不足一页分配或是跨页地址分配,则操作系统会将从跨页的低位地址开始分配的。即按照页边界对齐的原则。内存绑定(提交)以页(4096字节)为单位。
获取内存状态:
VOID GlobalMemoryStatus(
LPMEMORYSTATUS lpBuffer // 内存状态结构
);
参数说明:内存状态结构:
typedef struct _MEMORYSTATUS { // mst
DWORD dwLength; // 结构体字节数
DWORD dwMemoryLoad; // 内存使用率,百分之
DWORD dwTotalPhys; // 物理内存总字节数
DWORD dwAvailPhys; // 空闲物理内存字节数
DWORD dwTotalPageFile; // 分页文件总字节数
DWORD dwAvailPageFile; // 空闲分页文件字节数
DWORD dwTotalVirtual; // 虚拟内存总字节数
DWORD dwAvailVirtual; // 空闲虚拟内存字节数
} MEMORYSTATUS, *LPMEMORYSTATUS;
c) 虚拟内存的释放:
BOOL VirtualFree(
LPVOID lpAddress,//释放地址
SIZE_T dwSize, //释放的大小,字节数
DWORD dwFreeType //释放方式
); //成功返回true,失败返回false。
dwSize : 0, 表示全部释放
释放方式:
MEM_DECOMMIT - 只释放内存,不释放地址。
MEM_RELEASE - 地址和内存都释放。
- 堆内存:
a) 堆内存分配:适合分配小内存,一般是小于1M的内存。一般每个程序都有自己的堆,默认大小为1M,会根据使用情况需要进行动态调整。
b) 堆的使用
- 堆的信息:
- 获取调用进程的首个堆:
HANDLE GetProcessHeap (void);
//成功返回调用进程首个堆的句柄,失败返回NULL。
- 获取调用进程的所有堆
DWORD GetProcessHeaps(
DWORD NumberOfHeaps, //堆句柄数组的容量
PHANDLE ProcessHeaps // 保存返回堆句柄数组
);//成功返回进程堆数目,失败返回0。
- 创建堆:
HANDLE HeapCreate(
DWORD flOptions,//创建选项
SIZE_T dwInitialSize, //初始字节数,以后可调整
SIZE_T dwMaximumSize //最大字节数,0表示无穷大
); 成功返回堆句柄,失败返回NULL。
参数说明:
flOptions:
HEAP_GENERATE_EXCEPTIONS 堆内存分配失败则引发异常
HEAP_NO_SERIALIZE 支持不连续存取
- 从堆中分配内存
LPVOID HeapAlloc(
HANDLE hHeap, //堆句柄
DWORD dwFlags, //分配方式
SIZE_T dwBytes //分配内存大小
); 成功返回地址,失败返回NULL。
参数说明:
HEAP_GENERATE_EXCEPTIONS 堆内存分配失败则引发异常
HEAP_NO_SERIALIZE 支持不连续存取,
(若在创建堆的时候已经指定此参数,则此参数将忽略.)。
HEAP_ZERO_MEMORY 初始化清零
- 释放堆内存:
BOOL HeapFree(
HANDLE hHeap, // 堆句柄
DWORD dwFlags, // 释放方式,只能取HEAP_NO_SERIALIZE
LPVOID lpMem // 释放堆内存地址
); //成功返回true,失败返回false。
- 销毁堆:
BOOL HeapDestroy(
HANDLE hHeap //堆句柄
); //成功返回true ,失败返回false
当堆被销毁后,其中该堆分配的内存均将释放。
c) Win32 的malloc / new 实现实际是调用了上述堆函数。即:
VirtualAlloc/HeapAlloc/malloc/new在Windows平台上,函数调用关系:
new/malloc -> HeapAlloc ->VirtualAlloc
- 栈内存
a) 栈内存-每个线程都具有自己的栈,默认大小1M,一般是系统维护栈。
b) Windows提供了 _alloca 函数, 用于在栈上分配内存。
c) 操作系统负责自动维护栈内存的分配与释放。
- 内存映射文件:
a) 本质:将文件映射成内存来使用。当使用内存时,就是在使用文件。
常用于实现进程间通信,比直接通过文件I/O效率更高。
b) 内存映射的使用:
- 创建/ 打开文件CreateFile。设置可读可写属性.
- 创建映射:
HANDLE CreateFileMapping(
HANDLE hFile, //文件句柄
LPSECURITY_ATTRIBUTES lpAttributes, //安全属性设为NULL
DWORD flProtect,//访问方式可读写,PAGE_READWRITE
DWORD dwMaximumSizeHigh,//内存映射文件大小的高32位
DWORD dwMaximumSizeLow, //内存映射文件大小的低32位
LPCTSTR lpName //映射名,NULL表示匿名映射,其他进程不可访问
); // 创建成功返回映射文件句柄,失败返回NULL。
- 加载映射文件:
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,//内存映射文件句柄
DWORD dwDesiredAccess,//访问模式,FILE_MAP_ALL_ACCESS
DWORD dwFileOffsetHigh, //偏移量的高32位
DWORD dwFileOffsetLow, //偏移量的低32位
SIZE_T dwNumberOfBytesToMap //映射的字节数量
); 成功返回地址,失败返回NULL。
参数说明:
dwFileOffsetHigh和dwFileOffsetLow合成的偏移量,必须是区域粒度的整数倍(64K的整数倍)
使用映射,即对该返回地址的操作即可。
- 使用映射(内存):以内存的方式使用该映射
- 卸载内存映射:
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress //映射的地址
); .//成功返回true,失败返回false。
- 关闭(销毁)映射:
BOOL CloseHandle(
HANDLE hObject // 句柄
);
- 关闭文件:
CloseHandle,同上
- 对于已建好的映射,打开映射:
HANDLE OpenFileMapping(
DWORD dwDesiredAccess, // 访问方式,FILE_MAP_ALL_ACCESS
BOOL bInheritHandle, // 子进程是否继承此函数所返回的句柄
LPCTSTR lpName //映射名
);// 成功返回映射句柄,失败返回NULL。
c) 基于内存映射文件的进程间的通信
- 写进程:创建文件- 创建映射- 加载映射- 写入映射- 卸载映射 –
销毁映射- 关闭文件
- 读进程:打开映射- 加载映射- 读取映射- 卸载映射- 销毁映射
- 关于句柄:
句柄就是内存对象地址在句柄表中索引。通过句柄不能直接访问内存,但是可以通过APIs函数操作其标识的对象。
二、 Windows 进程
1. 基本概念
1) 进程是一个容器,包含程序执行所需要的代码,数据。资源、等信息。
Windows 是一个多任务操作系统,可以同时执行多个进程。
2. 进程特点
1)每个进程都有自己的ID号
2)每个进程都有自己的地址空间,进程之间无法访问对方的地址空间。
3)每个进程都有自己的安全属性
4)每个进程当中至少包含一个线程
3. 进程环境信息
获取和释放环境信息
获取
LPVOID GetEnvironmentStrings(VOID);//返回当前进程环境变量地址
释放
BOOL FreeEnvironmentStrings(
LPTSTR lpszEnvironmentBlock // 进程环境块指针
);//成功返回true,失败返回false。
4. 获取和设置环境变量
设置环境变量:
BOOL SetEnvironmentVariable(
LPCTSTR lpName, //变量名
LPCTSTR lpValue // 变量值
);//成功返回true,失败返回false。
获取环境变量:
DWORD GetEnvironmentVariable(
LPCTSTR lpName, // 变量名
LPTSTR Buffer, //变量值缓冲区
DWORD Size // 变量值缓冲区大小(字符为单位含尾空字符)
);//返回存在变量缓冲区的字符数,不含尾空字符
若没有找到缓冲区变量名,则返回0。
5. 进程信息:
1) 获取进程ID:
DWORD GetCurrentProcessId(VOID);//返回调用进程的ID。
另外,int _getpid( void );也可以返回进程的ID,
2) 获取进程句柄:
HANDLE GetCurrentProcess(VOID);//返回调用进程的伪句柄(-1),可以使用该句柄访问该进程的所用操作。但其子进程不继承该句柄
6. 进程的使用:
1)创建进程:
WinExec - Win6 遗留,现在基本不用。
ShellExecute - Shell 操作 ,速度慢。
Createprocess 目前使用最多
BOOL CreateProcess(
LPCTSTR lpApplicationName,//应用程序名称路径
LPTSTR lpCommandLine, //命令行参数
LPSECURITY_ATTRIBUTES lpProcessAttributes, //进程安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes,
//线程安全属性,NULL为缺省属性,同上。
BOOL bInheritHandles, //子进程是否可以继承父进程的句柄
DWORD dwCreationFlags, //创建方式,0表示立即启动
LPVOID lpEnvironment, //子进程环境,NULL表示继承父进程环境
如:”book=c++\0pen=color\00”其中的\0,表示环境变量分隔符,\0\0表示结束分号。
LPCTSTR lpCurrentDirectory,//子进程工作目录,NULL则继承父进程工作目录
LPSTARTUPINFO lpStartupInfo, //启动信息
LPPROCESS_INFORMATION lpProcessInformation
//进程信息,返回进程和线程的句柄ID等。
);//成功返回true,失败返回false。
参数说明:
LPPROCESS_INFORMATION:
typedef struct _PROCESS_INFORMATION { // pi
HANDLE hProcess; ///子进程句柄
HANDLE hThread; ///子进程的主线程句柄
DWORD dwProcessId; ///子进程ID
DWORD dwThreadId; ////子进程的主线程ID
} PROCESS_INFORMATION;
7. 等候进程
等候函数:
等候单个:WaitForSingleObject
DWORD WaitForSingleObject(
HANDLE hHandle, //等待的进程、线程句柄
DWORD dwMilliseconds // 等候时间(毫秒),INFINITE永远等候
);// 成功返回引起该函数返回的事件码,失败返回WAIT_FAILED(-1).
引起该函数返回的事件码:
WAIT_OBJECT_0 句柄有信号,进线程程已结束
WAIT_ WAIT_TIMEOUT 等候线、进程超时,进线程结束
阻塞函数,等候句柄的信号,只在句柄有信号或超出等候时间,才会结束等候。
等候多个:WaitForMultipleObjects -
DWORD WaitForMultipleObjects(
DWORD nCount, //句柄数量
CONST HANDLE *lpHandles, //句柄BUFF的首地址
BOOL bWaitAll,//等候方式
DWORD dwMilliseconds // 等候时间
);
bWaitAll - 等候方式
TRUE - 表示所有句柄都有信号,才结束等候
FASLE- 表示句柄中只要有1个有信号,就结束等候。
8. 退出进程
VOID ExitProcess(
UINT uExitCode // 退出码
);
终止进程(终止指定进程及其线程):
BOOL TerminateProcess(
HANDLE hProcess, //进程句柄
UINT uExitCode // 退出码
);//成功返回true,失败返回false
9. 通过进程ID获取句柄
HANDLE OpenProcess(
DWORD dwDesiredAccess,// 访问权限,PROCESS_ALL_ACCESS
BOOL bInheritHandle, // 子进程是否继承父进程句柄
DWORD dwProcessId // 进程ID
); 返回打开进程句柄,失败返回NULL
10. 关闭进程句柄
CloseHandle(HANDLE handle);
三、 Windows 线程
1. 线程概念:Windows线程是可以执行的代码的实例。系统是以线程为单位调度程序。一个程序当中可以有多个线程,实现多任务的处理。
Windows 线程特点:
1)线程都具有1个唯一标识 —— TID
2)线程具有自己的安全属性
3)每个线程都具有自己的内存栈
4)每个线程都具有自己的寄存器信息,(用以保存自己的存储状态、现场等)
进程多任务和线程多任务:
进程多任务是每个进程都使用私有地址空间,相互独立,彼此交换数据困难。
线程多任务是进程内的多个线程使用同一个地址空间即共享。彼此交换数据方便,容易产生冲突。
线程的调度:
将CPU的执行时间划分成若干时间片,分给不同的线程,依次根据时间片轮流执行不同的线程。
线程轮询:线程A -> 线程B -> 线程A......
2. 线程过程函数
DWORD WINAPI ThreadProc(
LPVOID lpParameter //创建线程时,传递给线程的参数
);
//返回值代表线程执行的成功或失败,可由GetExitCodeThread函数获取。
3. 创建线程
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//安全属性,NULL
SIZE_T dwStackSize, //线程栈的初始大小,0表示与调用线程相同
LPTHREAD_START_ROUTINE lpStartAddress, //线程处理函数的函数地址
LPVOID lpParameter, //传递给线程处理函数的参数
DWORD dwCreationFlags, //线程的创建方式,
LPDWORD lpThreadId //创建成功,返回线程的ID
); 创建成功,返回线程句柄,失败返回NULL。
参数说明:dwCreationFlags,
0 立即运行
CREATE_SUSPENDED 创建后先挂起,直到调用ResumeThread函数再运行。
4. 结束进程
终止指定线程
BOOL TerminateThread(
HANDLE hThread, // 线程句柄
DWORD dwExitCode // 退出码
);//成功返回true,失败返回false
结束函数所在的线程
VOID ExitThread(
DWORD dwExitCode // 线程退出码
);
5. 关闭线程句柄
CloseHandle( HANDLE hadle);
6. 线程的挂起和执行
挂起:
DWORD SuspendThread(
HANDLE hThread // 线程句柄
);//成功返回线程此前被挂起的次数,失败返回-1.
执行:
DWORD ResumeThread(
HANDLE hThread //线程句柄
);//返回线程此前被恢复的次数,失败返回-1.
7. 线程信息
获取当前线程的ID:
DWORD GetCurrentThreadId(VOID);
获取当前线程的句柄:
HANDLE GetCurrentThread(VOID);
8. 打开指定ID的线程,获取其句柄
HANDLE OpenThread(
DWORD dwDesiredAccess, // 访问方式
BOOL bInheritHandle, // 子进程是否继承父进程句柄
DWORD dwThreadId //线程ID
);//成功返回线程句柄,失败返回NULL。
9. 线程问题:
线程A -> 线程B -> 线程A 。。。。。
当线程A执行printf输出时,如果线程A的执行时间结束,系统会将线程A的相关信息(栈、寄存器)压栈保护,同时将线程B相关信息恢复,然后执行线程B,线程B继续输出字符。由于线程A正输出字符,线程B会继续输出,画面字符会产生混乱。
四、 线程同步技术
同步机制:原子锁、临界区(段)、事件、互斥、信号量
1. 原子锁:
问题描述:
多个线程对同一个数据进行原子操作,会产生结果丢失。比如执行++运算时,当线程A执行g_nValue1++时,如果线程切换时间正好是在线程A将值保存到g_nValue1之前,线程B继续执行g_nValue1++,那么当线程A再次被切换回来之后,会将原来线程A保存的值保存到g_nValue1上,线程B进行的加法操作被覆盖。
S1: g_nValue1 à寄存器
S2:寄存器的值+1
S3:寄存器的值—>g_nValue1
多个线程分别执行此三个过程,使得值发生覆盖。
原子锁的使用:
对单条指令操作的API:
LONG InterlockedIncrement(LPLONG lpAddend);//自增变量的共享指针
//返回自增后的结果。
LONG InterlockedDecrement(LPLONG lpAddend);//自减变量的共享指针
//返回自减后的结果。
LONG InterlockedCompareExchange(LPLONG lpAddend);
LONG InterlockedExchange(LPLONG lpAddend);
LONG InterlockedExchangeAdd(LPLONG lpAddend);
原子锁的实现:
直接对数据所在的内存操作,并且在任何一个瞬间只能有一个线程访问。
2. 临界区
问题描述:
printf输出混乱,多线程情况下同时使用一段代码。临界区可以锁定一段代码,防止多个线程同时使用该段代码。
临界区结构体:CRITICAL_SECTION
1) 初始化一个临界区
VOID InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //临界区结构体变量
);
2)进入临界区:
添加到被锁定的代码之前
VOID EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection // 临界区结构体变量
);
阻塞函数,直到调用线程获取对指定临界区对象的所有权才返回,任何时候只有一个线程拥有临界区资源。
3) 离开临界区:
添加到被锁定的代码之后
VOID LeaveCriticalSection(
LPCRITICAL_SECTION pCriticalSection // 临界区结构体变量
);
4) 删除临界区
VOID DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //临界区结构体变量
);
原子锁和临界区:
原子锁 - 单条指令。
临界区 - 单条或多行代码。
3. 互斥体(Mutex)
问题描述:多线程下代码或资源的共享使用。
互斥体的使用:
1) 创建互斥
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性
BOOL bInitialOwner,//调用线程是否初始拥有该互斥体
LPCTSTR lpName //互斥体名字
); 创建成功返回互斥句柄,失败返回NULL。
2)等待互斥体
添加到资源之前
WaitForSingleObject (hMutex , INFINITE);
若其他线程拥有该互斥体,则hMutex无信号,函数阻塞,直到调用线程获得对互斥体的所有权,此时hMetex有信号,函数返回。
3) 释放互斥体
添加到资源之后
BOOL ReleaseMutex(
HANDLE hMutex // 互斥体句柄
);//成功返回true,失败返回false。
4)关闭互斥体
CloseHandle(hMutex);
互斥体和临界区的区别:
临界区 - 用户态,执行效率高,只能在同一个进程中使用。
互斥体 - 内核态,执行效率低,可以通过命名的方式跨进程使用。
4. 事件
问题描述:程序之间的通知的问题。
事件的使用:
1)创建事件
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性
BOOL bManualReset,
//事件重置方式,TRUE手动,FALSE自动
BOOL bInitialState, //事件初始状态,TRUE有信号
LPCTSTR lpName //事件命名
); 创建成功返回 事件句柄,失败返回NULL。
2)等待事件
WaitForSingleObject/
WaitForMultipleObjects
3)触发事件
将事件设置成有信号状态
BOOL SetEvent(
HANDLE hEvent // 事件句柄
);
将事件设置成无信号状态
BOOL ResetEvent(
HANDLE hEvent // 事件句柄
);
4)关闭事件
CloseHandle (HANDLE handle);
注意:防止事件造成死锁。
5. 信号量
问题描述:类似于事件,解决通知的相关问题。但是可以提供一个计数器,可以设置次数。解决多个进程共享有限的资源的问题。
信号量的使用:
1) 创建信号量
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
//安全属性
LONG lInitialCount, //初始化信号量资源数量
LONG lMaximumCount, //信号量资源的最大值
LPCTSTR lpName //信号量命名
); 创建成功返回信号量句柄,失败返回NULL。
2)等待信号量
添加到资源之前。
WaitForSingleObject (hSemaphore, INFINITE);
每等候通过一次,信号量的信号资源数减1,直到为0阻塞。
若资源计数为0,则hSemaphore无信号,函数阻塞,直到资源计数大于0.此时有信号,函数返回同时资源数减1;。
3)释放信号量
添加到资源之后。
BOOL ReleaseSemaphore(
HANDLE hSemaphore, //信号量句柄
LONG lReleaseCount, //释放资源数量
LPLONG lpPreviousCount
//释放前信号量的数量,可以为NULL
);//成功返回true,失败返回false。
4)关闭句柄
CloseHandle( HANDLE handle);
6. 线程局部存储
局部与线程的全局变量
1) 分配线程局部存储
DWORD TlsAlloc(VOID);
//成功返回线程局部存储索引,失败返回-1;
2) 保存数据到线程局部存储
BOOL TlsSetValue(
DWORD dwTlsIndex , //线程局部存储索引
LPVOID lpTlsValue //数据
);//成功返回true,失败返回false
3) 线程局部储存中获取数据
LPVOID TlsGetValue(
DWORD dwTlsIndex //线程局部存储索引
);成功返回储存索引,失败返回NULL
4) 释放线程局部储存索引
BOOL TlsFree(
DWORD dwTlsIndex // 释放的线程局部存储索引
);//成功返回true,失败返回false。
5) 静态线程局部存储
--declspec(thread) int g_cn =0;
--declspec(thread) int static g_cn =0;