#1 ============== windows文件系统 和 io ==============
windows下共有四种文件系统,最常用且通用的文件系统为 NT文件系统(NTFS),其他的都或多或少已经弃用
(API)文件创建:CreateFile
(API)文件关闭:CloseHandle
(API)读文件:ReadFile
(API)写文件:WriteFile
+++ 中文乱码问题可通过如下编程规范解决 +++
如何编写UNICODE编码的程序:
1)使用TCHAR 代替 char;使用 LPTSTR 代替 char* ; 使用 LPCTSTR 代替 const char *
2)增加预定义宏 #define UNICODE 和 #define _UNICODE。否则 TCHAR == CHAR == char。同时这个宏应当在#include之前,最好是在工程配置中
3)使用tchar.h中的字符串处理函数。比如使用_stprintf代替sprintf
4)使用专用的宏来修饰常量字符串
注:TCHAR 不是一种类型,而是一个二选一的模式,如果定义了UNICODE,则TCHAR == WCHAR ,否则 TCHAR == CHAR。
即包含T的都是一个二选一类型,可选的为ANSI的8字节类型和16字节的宽字符。
T可以理解为 “通用” ,具体是什么需要根据是否定义了 UNICODE 宏来确定
CreateFile函数起始也是一个通用函数,他会根据是否定义了UNICODE宏而被替换为CreateFileA或者CreateFileW 之一。
如果没定义 UNICODE 宏,那么可以使用 char 和通用库 ,比如pritf strcmp。
如果定义了 UNICODE 宏,那么可以使用 TCHAT 和 tchar.h 对应的库
该如何选择?
如果涉及到中文相关的内容,请定义UNICODE,然后使用TCAHR LPTSTR LPCTSTR 来进行字符串操作。
如果仅仅是做数据存储,那么可以直接使用char,另外即便定义了UNICODE,char也是可以正常工作的,不用担心。获取标准设备对应的HANDLE,windows下不适用文件描述符,而使用HANDLE,可通过如下函数获取标准输入、输出、出错对应的HANDLE
(API)HANDLE GetStdHandle(DWORD nStdHandle) // 入参可选值为 STD_INPUT_HANDLE/STD_OUTPUT_HANDLE/STD_ERROR_HANDLE
(API)删除文件:DeleteFile
(API)赋值文件:CopyFile
(API)创建硬连接:CreateHardLink
(API)创建符号连接:CreateSymbolicLink
注:几种连接的区别https://www.cnblogs.com/liuzhaoyzz/p/9877094.html
(API)移动文件:MoveFile / MoveFileEx
(API)创建目录:CreateDirectory
(API)删除目录:RemoveDirectory
(API)设置“进程”的工作目录:SetCurrentDirectory
注:书中写的是线程的工作目录,是不对的,官方文档是 current process,以下代码可以验证:
#include
#include
#include "ProcessEnv.h"
#include
unsigned int __stdcall Thread(void* p) {
SetCurrentDirectoryA("../");
CreateFileA("1.txt", GENERIC_READ | GENERIC_WRITE, 0,NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,NULL);
return 0;
}
int main()
{
//当前路径下创建
CreateFileA("0.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
//切换到上级路径,创建1.txt
_beginthreadex(NULL,0,Thread,NULL,0,NULL);
Sleep(2000);
//如果设置工作路径是线程级别,那么2.txt应该和0.txt同级,如果是进程级别,那么2.txt应该和1.txt同级
CreateFileA("2.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
}
ps: linux下的chdir也是进程级别的
#2 ============== 高级文件、目录处理和注册表 ==============本节主要介绍文件管理相关的内容,比如权限和密码等等,一般编程不大会用到,主要了解下注册表
Windows对于文件偏移量的控制和unix一样,在unix下叫做偏移量,在windows下文件指针,二者在创建文件时都会被置为0,后续
每次读写文件都会偏移到相应的位置以供下次访问能够接上。(API)设置文件偏移量:SetFilePointerEx (类比unix下的lseek 和 fseek)
注:ReadFile和WriteFile这两个函数都接受一个OVERKAOOED参数,可通过此参数指定文件指针(偏移量)的位置。
(API)获取文件尺寸:GetFileSizeEx
+++ 文件搜索相关 +++
(API)获取一个搜索动作句柄: FindFistFile
(API)搜索下一个符合条件的文件:FindNextFile
(API)结束本次搜索动作:FindClose
注:FindClose会关闭有 FindFirstFile打开的搜索动作句柄,因此不需要使用CloseHandle来关闭。(API)获取文件时间戳:GetFileTime
(API)获取文件全路径:GetFullPathName
(API)获取文件属性:GetFileAttributes
(API)文件加锁:LockFileEx
注:1)windows可以完全或者部分锁住文件。
2)这个锁是进程级别的,区别于Mutex这样的线程间同步锁,进程锁可以有效地控制进程间的race condition
(API)文件解锁:UnlockFileEx
注:解锁解锁和Mutex一样,同样可以设计自释放锁来保证锁的安全性
+++ 注册表相关 +++
注册表汇中会保存应用程序和操作系统的配置信息,其中也包括了各个后缀名的文件的默认打开应用程序。
ps:windwos的注册表对应着unix的/etc目录,都是存放应用程序的配置信息。
相关API:RegOpenKeyEx
RegEnumKeyEx
RegCreateKeyEx
RegEnumValue
RegSetValueEx#3 ============== 异常处理 ==============
c++环境下使用 try-catch语句进行异常捕获和处理
c环境下使用 __try-__except 语句进行异常捕获c编译器使用
__try{}
__except(filter_expression){}
filter_expression的可选值为:
EXCEPTION_EXECUTE_HANDLER -- 跳到异常处理代码中执行
EXCEPTION_CONTINUE_SEARCH -- 不使用当前__except中的代码块,递归向上寻找EXCEPTION_EXECUTE_HANDLER或EXCEPTION_CONTINUE_EXECUTION的代码块
EXCEPTION_CONTINUE_EXECUTION -- 忽略所有异常,继续执行代码(API)获取异常代码:GetExceptionCode
注:可以编写一些处理此函数返回值的swich函数,根据不同的异常码返回上述三个可选值之一,从而达到针对异常进行
不同处理方式的目的。
例子:
__try{
}
__except(MyExceptHandler(GetExceptionCode()))
{
LOG(INFO)<<"进入了异常处理流程";
}DWORD MyExceptHandler(DWORD exceptcode) //入参为异常编号,具体参照相应表格, 返回值为filter_expression三个值中的一个
{
switch(exceptcode){
case EXCEPTION_INT_DIVCIDE_BY_ZERO: //如果是被0除的异常
return EXCEPTION_EXECUTE_HANDLER; //进入异常处理函数
break;
case EXCEPTION_ACCESS_VIOLATION: //如果是非法内存访问的异常
return EXCEPTION_CONTINUE_SEARCH; //本except代码块不处理EXCEPTION_ACCESS_VIOLATION异常,程序会递归地向上寻找处理EXCEPTION_ACCESS_VIOLATION异常的try-except语句
break;
case EXCEPTION_DATATYPE_MISALIGNMENT: //如果是类型对其错误
return EXCEPTION_CONTINUE_EXECUTION;//直接忽略此异常,进行制定try中的后续代码
break;
....
}
}注:这里需要注意下EXCEPTION_CONTINUE_SEARCH的行为,这个场景下不是说不处理异常,而是说本try-except块不处理异常,会拿着
此异常在代码中寻找能处理此异常的try-except代码块,注意此时程序的执行流程依旧是被卡主的,同时查找的方向是回溯调用栈
的查找,不能在还没执行的代码中查找,因为此时代码是被冻结住了。
(API)自行触发异常:RaiseException //也可以自行定义新的异常,然后用此函数触发,并捕获UNIX的信号 和 WINDOWS的异常处理 的异同:
同:1)二者都可以终端当前的代码执行流程;
2)二者都在事件发生时都可以选择进入处理流程/本次忽略/继续传递等等策略;
3)单个进程中,二者都可以主动触发事件, unix的 raise 和 windows的RaiseException
4)可以给其他进程发送事件,unix的 kill 和 windows 的 GenerateConsoleCtrlEvent
异:1)windows下可以通过TerminateProcess 和 TerminateThread 让一个进程/线程杀死另一个进程/线程
2)unix下可以在c++中使用异常处理try-catch,但是不能使用c的异常处理__try-__except,windows环境下无法使用c的信号
(!!!)__finally 可以保证 __try在任何场景下都会执行其中的语句。即便是在__try中使用了return,__fina1也会被执行。
__finally会在下述场景后进入:
1)try执行完毕后,会进入__finally;
2)在try中执行下述任何一条语句:return、break、goto、longjmp、continue、__leave,这其中__leave是微软C特有的,也是推荐使用的
3)try中的代码执行时产生异常__leave的动作等于 :直接跳到try语句的结束为止。
上述三种进入__finally的方法中,仅1)和__leave这两种方法被认为是正常进入 __finally,其他都认为是非正常进入。(API)判断是正常还是异常进入__finally:AbnormalTermination
注:此函数返回true则表示是正常进入__finally,FALSE表示非正常。即便是在try的最后一条语句使用return,也被认为是非正常。__try 后要么跟 一个 __finally ,要么跟一个 __except,注意!!!!不能既有 __finally又有__except,同时有,会导致编译出错。
异常和非正常终止将会导致全局堆栈解开(global stack unwind)来搜索处理程序。
(!!!)如果进程/线程终止,那么__finally中的程序不会被执行。如果是进程/线程的自行退出,则__finnally会被执行,但是如果是被其他
进程/线程杀死(TerminateThread/TerminateProcess),则__finally不会被执行。
+++ SEH 和 c++的异常处理 +++
c++中的异常处理程序为 try-catch,同时使用throw进行异常抛出,这和SEH(__try-__except)的工作机制是类似的,其实c++的异常处理
就是用SEH实现的,但是不建议混合使用 __try-__except 和 try-catch,理由为:
1)SEH 和 c++异常处理可能在捕获异常时出现交织,导致SEH抛出的异常被c++异常处理捕获,或者c++异常处理抛出的异常被SEH捕获,
这会导致异常可能不会进入期望的异常处理流程,比如__try-__except如果设置了忽略异常,则可能会导致异常丢失。
!!2)如果在__try语句中分配了局部类,那么在__except和__finally中不会调用这个局部类实例的析构函数,如果这个类在构造是分配
和堆内存,则无法通过析构函数释放这些内存,这将导致内存泄漏(但是属于这类的栈内存会被释放)。不过,我们可以通过/Eha
编译选项来告知编译器在编译时需要处理这一个问题
(!!!)小结:微软推荐在 c++ 程序中完全使用 c++异常处理,仅在 纯C程序中才使用 SEH
(API)注册控制台“信号”处理程序:SetConsoleCtrlHandler //参数1是信号处理函数指针,但是注册的函数要符合函数类型要求
注:1)控制台的Ctrl-C、Ctrl-break、Ctrl-Z等其他信号均会进入由此函数设置的函数中处理(这个函数接收一个函数地址)
2)这个函数实际上会操作一个函数指针列表,因此可以追加多个信号处理函数指针,通过在第二个参数设置为true来实现
信号 和 异常的区别:信号是进程级别的,异常是线程级别的。当某个线程产生异常时,我们如果不设置捕捉函数,那么程序将崩溃,如果我们设置了全局异常捕捉处理函数,那么会进入
相应的处理函数,此时会进入函数中执行,然后在函数中决定是该终止程序还是该继续运行。
这里需要注意的一点,异常是线程级别的,因此进入异常处理函数时依旧处于发生异常的线程中,同时会被进程的其他线程
全部阻塞住,因为这个线程此时设置了独享cpu。由于异常时线程级别的,那么当我们在一个无所谓的线程中捕获了一个无所谓
的异常时,我们可以选择只杀死这一个线程然后告知其他线程来重启此异常线程,这样的话我们的程序便不会崩溃了。因此
当发生了 非法内存访问这种异常,有时候也可以根据事发线程的重要程度来决定是否要直接dump整个程序并生成dump文件,
完全可以选择只杀死线程。
题外话:产生异常说明程序有问题,理所应当要告知开发人员,但是直接dump整个程序在某些重要场合下显得有些粗暴,我们本
可以更加优雅一点地记录问题,然后悄悄地告知开发人员。
SetConsoleCtrlHandler函数指定的信号处理程序有一个入参,这入参有系统填入,因此我们只需要使用这个参数即可,参数的
可用值如下:
1)CTRL_C_EVENT表示键盘输入了Ctrl-C
2)CTRL_CLOSE_EVENT表示控制台正在被关闭
3)CTRL_BREAK_EVENT表示Ctrl-break信号
4)CTRL_LOGOFF_EVENT表示用户正在注销
5)CTRL_SHUTDOWN_EVENT表示Windows正在关闭。
信号处理函数返回TRUE表示函数已经处理了此信号,返回FALSE表示信号被传递给下一个信号处理函数处理了。
(API)注册向量化异常处理:AddVectorExceptionHandler
注:可以理解为,如果对某个指定异常注册了向量处理函数,那么当指定异常发生时,第一步就是进入此函数执行,
第二部才是进入__except异常捕获流程中。也就是说如果异常发生在__try语句块中,那么这个异常会被捕获
并处理两次,第一次在向量化异常处理函数中,第二次在__except语句块中。
VEH = vector exception handle
#4 ============== 内存管理、内存映射文件和DLL ==============
#4.1 内存管理
Win32让进城至少可以使用虚拟内存一般的内存空间,比如32位程序可使用2GB,另外的空间用来存放 共享数据、代码、系统代码、驱动程序 等等关于虚拟内存的一些描述:
1)虚拟内存和物理内存是独立的概念,虚拟内存的大小由应用程序的位数决定;
2)各个进程都会占用自己的虚拟内存空间
3)虚拟地址到物理地址的映射有操作系统负责
4)大多数虚拟内存内容都不在物理内存汇总,应用程序的使用的内存会被划分为虚拟内存页(小片段),操作系统的内存管理
模块会建立起对这些页的索引,这些页或被放在物理内存中,或被放在磁盘中,甚至是文件中,当cpu执行到相应的指令
需要访问被存在磁盘和文件中的内存页时,cpu会触发缺页错误(说明自己想访问的页不在物理内存里),此错误会引发中断
,通知内存管理系统拿着相应的索引去磁盘或者文件中找对应的页,找到以后将这些页的内存导入物理内存中。
5)缺页错误过多会导致程序的执行效率变低。
(API)获取系统参数:GetSystemInfo
(API)获取本进程的堆的句柄:GetProcessHeap
注:每个进程都有自己的堆,在windows下,堆是windwos对象,但不是windows内核对象,它是有HANDLE的,同时,一个
进程可以有多个堆,不过我们往往让一个进程只使用一个堆,如果有多个堆的话,可以通过HANDLE来进行操作各个堆
+++ 使用属于线程的堆的 优劣 +++
在一个进程中使用多个堆的好处:
1)安全性:如果给一个线程只使用某一个堆,那么这个线程的内存泄漏不会传播到整个进程中,这可以保证其他线程和整个
进程的堆安全。
2)多线程高效:如果每个线程都拥有自己的堆,那么线程间分配堆内存时争夺堆资源的情况将变少,这有助于提高性能。
3)分配高效:在小的堆内存中分配堆比在大堆中分配堆要快。
4)释放高效:windows提供整个释放堆的接口,当某个线程终止时,可以整个释放这个线程的堆内存,同时一些在本线程对应
堆中内存泄漏的堆也会被释放。
!!! 这也是线程对象的存在的一个重要依托,我们可以在线程对象的析构函数中释放属于自己的堆!!!
5)缺页错误会变少:使用小堆可以有效减少缺页错误,同时对页的索引遍历也会变快,因为小堆的索引比较少,遍历起来快
弊端:堆本身作为线程间共享资源,有些时候会扮演者线程间通讯媒介的角色,如果让线程间互相隔离,则这一编程手段将丧失,
不过可以合理让相关的线程使用同一个堆,这也可以提高程序的安全性,同时也减少了由race condition造成bug的风险
注:目前只有windows有线程堆,linux下面只有进程堆(API)创建堆:HeapCreate
SIZE_T 类型是一个二选一类型,如果预定义 _WIN32 则是32位unsigned int ,过是 _WIN64 则是64位unsigned long。
SIZE_T主要用在需要兼容32位和64位的引用程序中。
SSIZE_T 类型是有符号版本。
(API)销毁堆:HeapDestory
注:只能销毁由HeapCreate创建的堆,不能销毁进程堆。(ps:进程堆由GetProcessHeap)
(!!!)重要:如果在线程堆里分配了数据结构(包括类),则在堆销毁是会自动释放这些数据结构,但是!!!如果
这些数据结构中有类实例,那么只会释放这个类实例的内存,但是不会调用这类实例的析构函数!!!
也就是说,如果一个类中有指针,这个指针指向了别的线程堆或者进程堆,然后释放动作是在析构函数中
进行,那么销毁当前线程堆只会把这个指针的内存空间释放掉,而不会触发析构函数中的释放指向其他线程堆/进程堆的流程。
ps:一般我们也不会这么用,线程堆内部指针一般都指向本堆,那么即便不再析构函数中释放,线程堆
也会自动释放其所有内存,这其中就包括被指针指向的那部分堆内存。
+++ (???)windos内核对象都有安全属性,那么安全属性有哪些?为什么要引入安全属性? +++
(API)堆分配:HeapAlloc //既可以在线程堆分配,也可以在进程堆分配,只要把堆的HANDLE传给函数即可
注:返回LPVOID,这个是32位指针还是64位指针,由编译选项是 _WIN32 还是 _WIN64 确定。
(API)堆释放:HeapFree
(API)重新分配:HeapReAlloc //重新分配很多时候是用来调整堆尺寸的。此函数提供标志位,可以保持原来堆内存中的数据不变,也可以
//将原来的数据初始化掉(API)获取堆内某个内存块的大小:HeapSize
注:这个函数不能获取线程堆的大小,只能获取通过HeapAlloc和HeapReAlloc分配的内存块的大小,因此这个函数其实应该叫
HeapGetBlockSize,这里需要注意。
(!!!)需要说明一点:线程堆之间是互通的,即我在线程A中可以通过使用线程B的线程堆句柄来在线程B中分配和释放内存,虽然
这样做不符合编程和设计思路,但是确实是允许的。
(!!!) 所谓的线程堆不是说某个堆是属于某个线程,而是通过某种编程手段/规范让堆句柄只提供给指定线程使用。
(API)获取最大可用内存块:HeapCompact
(API)检查堆崩溃:HeapValidate
(API)枚举堆中的内存块:HeapWalk
(API)获取进程中的所有子堆句柄:GetPorcessHeaps //这里不再使用线程堆的字样,因为不准确,转而使用子堆来代替线程堆
(API)进程对堆访问做控制:HeapLock HeapUnlock
(API)其他:GlobalAlloc 、 LocalAlloc(!!!!!!!)小结 : 1)c库的malloc 和 free可以用来操作堆,上面介绍的一系列API是对这两个函数的扩充,但是!不要将他们混在一起使用
2)windows场景下,如果不想创建子堆和堆异常捕获,那么可以继续使用malloc 和 free
3)堆是 windows 资源,因此有HANDLE,因此需要使用 HeapDestroy来回收HANDLE,不然HANDLE会泄漏
4)HeapAlloc 和 HeapReAlloc 对应 malloc , HeapFree 对应 free , HeapSize 在 C库中 没有配对的函数
#4.2 文件的内存映射
文件的内存映射有如下好处:
1)无需使用文件I/O来操作文件,因此不需要考虑缓冲区问题
2)可以将程序中的数据结构直接写入文件中,这样其他进程便可以通过打开这个文件来访问这些数据结构,这也是一种 IPC 机制。
3)可以用内存算法提高需要用算法处理的文件内容
4)无需分页文件空间,节省系统资源
(API)创建映射文件对象:CreateFileMapping
注:1)此函数返回的HANDLE是一个windows内核对象,因此需要设置一些安全属性
2)如果只是为了做进程间内存共享,那么可以不创建文件,那么入参指定文件句柄是可以使用INVALID_HANDLE_VALUE。
!!!3)最后一个参数指明映射文件对象的名称。其他进程可以通过使用系统接口查询此名称来获取映射文件对象的HANDLE。
4)函数返回映射文件对象的HANDLE,供本进程使用
(API)打开已有的文件映射对象:OpenFileMapping
注:此函数接收一个字符串值,这个值就是映射文件对象的名称,即CreateFileMapping的最后一个参数
(API)将映射对象映射到当前进程的虚拟内存地址空间:MapViewOfFile
注:创建映射对象是只会指定一个文件句柄,具体与当前进程虚拟内存的关联,需要通过此函数进行。
(!!!)重要:每个进程创建的用来与映射对象关联的虚拟内存地址是不一样的,这些进程是通过 文件HANDLE 作中间媒介,以方便
内核的内存管理系统做 各个进程的虚拟内存关联,其实这些虚拟内存都关联到同一块物理内存 或者 磁盘上。
(API)关闭映射句柄:UnmapViewOfFile
注:映射的创建是一个动作,完成以后就已经把文件和内存关联起来了,后续只需要操作内存即可。因此如果没有特殊需求,
句柄是不会在后续中使用,因此很多时候,在MapViewOfFile后可直接UnmapViewOfFile来提前释放句柄。不会有影响映射关系。(API)将脏页(更改过的)写入磁盘:FlushViewOfFile
注:不同进程在访问同一个内存映射区时,数据可能会不同步,因为这些进程都有自己的虚拟内存,因此需要通过FlushViewOfFile
将数据从虚拟内存刷到磁盘上,这才能保证不同进程访问的数据能够得到同步。
此外,如果一个进程通过映射内存访问文件,另一个进程通过文件I/O访问文件,那么这两个进程无论如何都无法同步数据,
即便在进行文件I/O时设置无缓冲也无法解决不同步问题。ps:可以理解为内存映射是针对物理内存的操作,因此两个进程都是用内存映射的话,他们操作的是同一个物理内存/磁盘,因此同步
问题可以保障,但是文件I/O是针对磁盘进行的,如果一个进程时内存映射,另一个是文件I/O,那么无法保证两个不同存储
设备的数据能够同步(物理内存和磁盘)WIN32场景下的虚拟内存只有4g,其中用户态最多能访问3g,因此做内存映射的时候最大限度也只有不到3g,所以win32场景下的内存
映射不可以操作超过3g的文件。
基指针:????
#4.3 动态链接库
几点说明:
1)动态库中的全局变量在所有调用它的应用程序中都有自己的副本,不会共享,因此“不要”试图通过DLL进行进程间IPC。
2)DLL可以使用调用者的所有资源,比如句柄/调用栈等等。
3)动态库的内部实现应当是线程安全的。(API)动态库搜索路径设置:SetDllDirectory
far/FAR类型 是16位系统相关函数以及为了兼容16位系统的函数具有的标志性参数类型/返回类型。(API)加载可执行二进制文件:LoadLibrary LoadLibraryEx //既可以用来加载动态库,也可以用来加载和直接执行exe文件
(API)获取相应符号的入口地址:GetProcAddress
(API)释放可执行二进制文件:FreeLibrary
由于DLL是进程共享的,所以操作系统会为每一个DLL维护一个引用计数。LoadLibrary和LoadLibraryEx会增加这个计数。FreeLoadLibrary
会减少这个计数。启动时链接也会增加计数。如果DLL依赖其他DLL,那么在LoadLibrary时操作系统弄会去检测相应的DLL是否存在,如果找不到,LoadLibrary会失败。
(!!!)前面提到了虚拟内存会留一部分空间给内核态,这部分空间中就包括动态库,如果win32中一个动态库的大小超过了2g,那么这个
dll将无法使用,因为无法win32中内核态最多只有2g。
(!!!)这里有一点需要说明,如果一个DLL被多个进程调用,只要有一个进程没有退出,那么其他进程都将无法将这个DLL从自己的
虚拟内存中移除,因为他的引用计数还没有变为0。这点很变态。除非某个进程直接退出了。
(???)这个观点需要自行实验验证下!!!windows下DLL有动态库进入点程序,这个程序在创建DLL工程时会一并创建,我们也可以不使用DLL进入点。
DLL进入点在加载动态库时(静态/动态),会被默认执行,相当于DLL的初始化函数。具体的操作后续在说明。
BOOL DllMain(
HINSTANCE hDll,
DWORD Reason,
LPVOID lpReserved
)