第十六章:线程栈
1. 系统对线程占的使用规则是:首先预定1M的空间和调拨两个页面的存储器.然后线程开始执行前,将预定的末尾空间指向线程栈的指针.当有线程需要更多的页面时,就会试图访问防护页面.这样系统就调拨防护页面的下一页面作为新的防护页面.把之前的防护页面作为空间分配使用.
再者,假若线程需要申请栈底前一个页面时,因为此时没有更多的内存可供使用,所以,只是将防护页面当做分配的页面使用.并且不再将防护页面下移.
最后,如果线程再申请空间时,将会抛出异常(EXCEPTION_STACK_OVERFLOW)
另外,之所以线程保留最后的页面(线程栈指针指向)的原因是为了防止栈溢出(下溢).
第十七章:内存映射文件
1. 内存映射文件与虚拟内存最大的区别在于:内存映射文件的物理存储器来自磁盘上已有的文件,而不是来自于系统的页交换文件.
其主要应用范围为:
☞ 系统使用内存映射文件来加载并运行.exe和动态链接库文件.这大量节省了页交换文件的空间以及应用程序启动的时间.
☞ 开发人员可以用内存映射文件来访问磁盘上的数据文件.这使得我们可以避免直接对文件进行I/O操作和对文件内容进行缓存
☞ 通过使用内存映射文件,我们可以在同一台机器的不同进程之间共享数据.
1. 当线程调用CreateProcess的时候,系统会先查找该进程的exe文件,如果没有找到,那么函数会返回失败.否则系统会为进程创建一个私有的地址空间,并预定一个区域,这块区域可以容纳exe文件,然后系统对地址空间区域进行标注,以说明该区域的后备物理存储器来自磁盘的exe文件而非系统的页交换文件.
假若系统在exe访问的过程中使用到了另外一些dll或者exe文件,就像刚才那样预定一块足够大的区域容纳这些文件.此时有两种原因使得预定失败. ①为了节省空间不使用重定位功能,这使得DLL必须被载入指定的基地址②然而,重定位会增加载入DLL所需的时间和占用页交换内存.
接下来就是标注了,这里唯一例外的一种情况是,DLL可能也被部分映射到了页交换文件.
2. 每个DLl或者exe文件都是有许多段组成.
段的属性如下:
属性 |
含义 |
READ |
可以从该段读取数据 |
WRITE |
可以从该段写入数据 |
EXECUTE |
可以执行该段的内容 |
SHARED |
该段的内容为多个实例所共享(这个属性事实上关闭了写时复制机制) |
具体段的组成可以使用DumpBin工具来查看.
常用段及其目的:
段名 |
目的 |
.bss |
未经初始化的数据 |
.CRT |
只读的C运行时的数据 |
.data |
已初始化的数据 |
.debug |
调试信息 |
.didata |
延迟导入的名字表 |
.edata |
导出的名字表 |
.idata |
导入的名字表 |
.rdata |
只读的运行时数据 |
.reloc |
重定位表信息 |
.rsrc |
资源 |
.text |
.exe文件或DLL的代码 |
.textbss |
当启用增量链接选项时,由C++编译器生成 |
.tls |
线程本地存储 |
.xdata |
异常处理表 |
我们也可以使用自定义的段名,格式如下:
#program data_seg(“段名”)
变量定义 //注意,此时需要初始化才行
#program data_seg()
也可以使用allocate来将未初始化的变量放进段里.
同一个exe文件的多个进程就可以开始共享段里面的数据了,但是要主要的一点是,必须在编译器的代码里加上如下命令行:
#pragma comment( linker,"/SECTION:Shared,RWS" )//可读写共享属性
3. 使用内存映射文件需要执行以下三个步骤:
☞ 创建或打开一个文件内核对象,该对象标识了我们想要用作内存映射的那个磁盘文件
☞ 创建一个文件映射内核对象来告知系统需要为其分配多大的区域,以及我们想要以何种方式访问它.
☞ 告诉系统把文件的全部或者部分映射到进程的地址空间中.
相对应的清理操作如下:(采用与上述相反的动作)
☞ 告诉系统从进程地址空间中取消对文件映射内核对象的映射
☞ 关闭文件映射内核对象
☞ 关闭文件内核对象
创建文件映射内核对象:
页面保护属性:
保护属性 |
含义 |
PAGE_READONLY |
完成对文件映射对象的映射时,可以读取文件中的数据.在调用CreateFile时必须传GENERIC_READ |
PAGE_READWRITE |
完成对文件映射对象的映射时,可以读取文件的数据并将数据写入文件.在调用CreateFile时必须传GENERIC_READ|GENERIC_WRITE |
PAGE_WRITECOPY |
完成对文件映射对象的映射时,可以读取文件中的数据,并将数据写入文件.写入操作将导致系统为页面创建一份副本.调用CreateFile时必须传GENERIC_READ或GENERIC_READ|GENERIC_WRITE |
PAGE_EXECUTE_READ |
完成对文件映射的映射时,可以读取文件中的数据,还可以运行其中代码.在调用CreateFile时必须传GENERIC_READ和GENERIC_EXECUTE |
PAGE_EXECUTE_READWRITE |
完成对文件映射对象的映射时,可以读取文件中的数据并将数据写入文件,还可以运行其中的代码.在调用CreateFile时必须传GENERIC_READ,GENERIC_WRITE和GENERIC_EXECUTE |
还可以取或操作的段属性:
☞ SEC_NOCACHE,它告知系统不要对内存映射的页面进行缓存.(这个标志对驱动程序开发的程序员有用)
☞ SEC_IMAGE,它告知系统要映射的文件是一个PE文件映像.当系统把文件映射到进程地址空间时,系统会检查文件的内容并决定应该给各页面指定何种保护属性.
☞ SEC_RESERVE和SEC_COMMIT,他们是互斥的,而且不适合映射到内存的数据文件.
☞ SEC_LARGE_PAGES,告知系统要为内存映射文件使用大页面内存.只有当用于PE映像文件或内存映射文件的时候.这个属性才是有效.需要满足的条件如下:
HANDLE CreateFileMapping(
__in HANDLE hFile, //需要映射到进程地址空间的文件句柄
__in_opt LPSECURITY_ATTRIBUTES lpFileMappingAttributes, //访¤?问¨º属º?性?
__in DWORD flProtect, //页°3面?的Ì?保À¡ê护¡è属º?性?
__in DWORD dwMaximumSizeHigh, //内¨²存ä?映®3射¦?文?件t的Ì?最Á?大䨮大䨮小?,以°?字Á?节¨²为a单Ì£¤位?(高?位?)
__in DWORD dwMaximumSizeLow, //低̨ª位?
__in_opt LPCSTR lpName ); //文?件t映®3像?的Ì?名?称?
◆ 在调用CreateFilemapping时,必须的指定SEC_COMMIT属性来调拨内存.
◆ 映射的大小必须大于GetLargePageMinmum的返回值.
◆ 必须用PAGE_READWRITE保护属性定义映射
◆ 用户必须具有并启用内存中锁定页面用户权限,否则CreateFileMapping函数调用将会失败.
成功时返回文件映射对象句柄,否则返回NULL.
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // handle to file-mapping object
DWORD dwDesiredAccess, // access mode
DWORD dwFileOffsetHigh, // high-order DWORD of offset
DWORD dwFileOffsetLow, // low-order DWORD of offset
SIZE_T dwNumberOfBytesToMap // number of bytes to map
参数: dwDesiredAccess的访问权限如下:
保护属性 |
含义 |
FILE_MAP_WRITE |
可以读取和写入文件.在调用CreateFileMapping时必须传入PAGE_READWRITE保护属性 |
FILE_MAP_READ |
可以读取文件.在调用CreateFileMapping时可以传PAGE_READWRITE和PAGE_READONLY |
FilE_MAP_ALL_ACCESS |
等于FILE_MAP_WRITE|FILE_MAP_READ|FILE_MAP_COPY |
FILE_MAP_COPY |
可以读取和写入文件.写入操作会导致系统该页面创建一份副本.调用CreateFileMapping是必须传入PAGE_WRITECOPY.此时系统会从也交换文件中调拨物理存储器.此时如果不用到读取操作,也就相当于不用到页交换文件 |
FILE_MAP_EXECUTE |
可以讲文件中的数据作为代码来执行.在调用CreateFileMapping时可以传PAGE_EXECUTE_READWRITE或PAGE_EXECUTE_READ属性 |
文件中被映射到进程地址空间中的部分被称作视图.此时需要告诉系统两件事:
● 应该把数据文件的哪个字节映射到视图中的第一个字节.
● 把数据文件中多少映射映射到地址空间中去.也即是最后一个参数的取值.如果为0,系统会试图把文件中从偏移量开始到文件末尾的所有部分都映射到视图中.
撤销对文件数据的映射:
BOOL UnmapViewOfFile(PVOID pvBaseAddress);//参数为MapViewOfFile返回值
函数的特性:如果视图最初是用FILE_MAP_COPY标志映射的,那么对文件数据的任何修改实际上是对数据在也交换文件中的文件数据副本的修改.如果需要保存修改过的数据.必须自己进行额外的操作.
强制刷新文件数据缓存:
BOOL FlushViewOfFile(
PVOID pvAddress, //内存映射文件的视图中第一个字节的地址
SIZE_T dwNumberOfBytesToFlush); //想要刷新的字节总数
4. 系统允许我们把同一个文件中的数据映射到多个视图中.如果其中一个应用程序修改了文件,那么系统会确保各视图的数据是一致的.(原因其实很简单,当我们使用同一个文件的多个视图时,系统的数据文件只有一份,只是这一份数据会映射到多个进程的地址空间而已.)
Windows允许我们以同一个数据文件为后备储存器来创建多个文件映射对象,但是并不保证这些不同的文件映射对象的各个视图是一致的.它只是保证在同一文件映射对象的多个视图间保持一致.
5. 给内存映射文件制定基地址:
PVOID MapViewOfFileEx(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap,
PVOID pvBaseAddress);//给要映射的文件指定一个目标地址
函数调用失败情况如下:
1. 指定的目的地址不是分配粒度(64KB)的整数倍.
2. 系统无法将文件映射到指定的地址.
6. 在进程能够从自己的地址空间中访问内存映射文件的数据之前.windows要求进程先调用MapViewOfFile.如果进程调用了MapViewOfFile,那么系统会在该进程的地址空间中为视图预定一块区域,任何其他进程都无法看到这个视图.如果另一个进程想要访问同一文件映射对象中的数据时,那么第二个进程也必须调用MapViewOfFile,这样系统就在第二个地址空间中为视图预定一个块区域.
7. 有时为了共享数据,而让应用程序在磁盘上创建数据文件并把数据保存在文件中将会到来诸多不便,因此提供了以页交换文件为后备存储器的内存映射文件.
页交换文件的使用:
使用CreateFileMapping,并传INVALID_HANDLE_VALUE作为hFile,一旦创建了文件映射对象,并把一个视图映射到了进程的地址空间中,我们就可以像使用任何内存区域一样使用它.
这里需要特别注意的是:由于当我们使用文件映射对象来处理文件时,CreateFile可能会失败.而关键的一点是:当函数创建失败就会返回INVALID_HANDLE_VALUE,这时再调用CrateFileMapping就会出现使用的是内存页交换文件.这样当系统销毁文件映射对象的时候,整个页交换文件也会一起被销毁.从而导致操作失败.
8. 调用CreateFileMapping传入SEC_RESERVE标志.此时系统不会从页交换文件中调拨物理存储器,它仅返回文件映射的句柄(预定一块区域).然后使用MapViewOfFile来预定一块区域.再通过使用VirtualAlloc来调拨内存(需要注意的是,如果我们使用SEC_RESERVE来预定区域时,无法使用FreeVirtual来释放内存).