来源:哔哩哔哩
2023-02-08 02:08:27
第十二:互斥体
#内核级临界资源
【资料图】
---临界资源:一次只允许一个线程使用的共享资源
---临界区:访问临界资源的代码(程序)
---上一节的临界资源是全局变量(堆中),如果这个临界资源是一个内核级的临界资源,不同的进程的线程进行访问(不可能放在应用层中,放在A应用层B无法访问;放在B应用层A无法访问)
---互斥体:能够放在内核中的一种令牌
---创建互斥体CreateMutex:
---对于CreateMutex的第二个参数:BOOL bInitialOwner
---如希望立即占有该互斥量,则设为TRUE。 操作系统记录线程ID,将递归计数器设置为1,互斥量处于未触发/无信号/未通知状态。一个互斥体同时只能由一个线程拥有 。
--- 为FALSE,则线程ID为NULL,操作系统将递归计数器设置为0,互斥量处于触发/有信号/已通知状态,不为任何线程所占用。
---在VC6里面开2个界面,执行2段代码(两个printf代表2个进程的线程)
---相同名字的互斥体的只能创建一个
---第一次创建成功,返回句柄,并且GetLastError()返回NULL
---如果第一次互斥体就创建失败,就会返回NULL(GetLastError()返回失败原因)
---第二个B进程创建xyz的互斥体成功,其实是返回的之前A进程创建的xyz互斥体句柄,并且GetLastError()返回ERROR_ALREADY_EXISTS错误
---第二个B进程创建失败,返回NULL,并且GetLastError()返回其它原因
---发现一个现象:只有当A进程x线程全部执行完之后
---B进程的y线程才开始执行
#互斥体和线程锁的区别
---1.线程锁只能用于单个进程间的线程的控制
---2.互斥体可以设置等待超时,但是线程锁不能
---3.线程意外终结时,互斥体可以避免无线等待
---4.互斥体没有线程锁(避免在内核中创建对象)的效率高
#进程获取互斥体,还没有释放令牌,就已经结束
---i=7的时候直接退出,没有执行释放互斥体的代码ReleaseMutex(g_hMutex);
---A进程直接终止,意味着1个进程没有机会等到令牌
----猜测B进程无法执行
---但是发现B进程任然可以执行(具体原因不知道)
#防止程序的多开(游戏应用)
---这里使用CloseHandle(g_hMutex);的原因:不同进程(通过私有句柄表)访问内核对象(g_hMutex)建立了连接,关闭进程的同时,需要关闭进程对于内核对象的连接即私有句柄表(我猜测是这样)
---点击发现,只能打开一个exe文件,其它的exe文件根本打不开
第十三:事件
#通知类型:CreateEvent()
---设置事件,可以控制线程的执行
---无信号和有信号的区别
---设置事件为:无信号的通知类型
---例如,将SetEvent注释
---本质是在内核存在一个结构体,结构体的状态为0/1
---发现2个线程都没有执行
---原因是2个现在都卡在WaitForSingleObject(g_hEvent,INFINITE);无法执行
---将SetEvent(g_hEvent);的注释取消,发现2个线程都执行了
---和线程锁的区别:线程锁只能是互斥的(一个执行完了,另外一个才能执行),而通知事件可以:同步的控制2个线程的执行
---通知事件:即使WaitForSingleObject获取到,也不会改变信号的状态
#互斥类型
---将BOOL bManualReset设置为FALSE(无信号的互斥类型)
---发现2个线程只有1个线程执行
---原因是:WaitForSingleObject捕获,使得信号的状态改变
---如:A获取,内核的有信号改为无信号,那么B线程就没有办法执行
#总结
#线程互斥:
---对于进程共享的系统资源,在单个线程访问的时候具有排他性。
---即:在进程内,若干线程共有一个资源的,任何一个时刻最多允许一个线程使用
---其它线程需要等待,直到占有资源者释放资源
#线程同步
---线程之间所具有的一种制约关系
---一个线程的执行,依赖于另一个线程的消息
---当线程没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒
#线程同步的前提条件:互斥(只能由一个线程访问临界资源)
---互斥特点:者一次是左边的线程,下一次未必是右边的线程
#线程同步的特点(有序访问)
---平均访问,右边访问一次,左边访问一次
---经典的案例:消费者/生产者问题:生成一个,然后消费一个,达到容器的均衡
---通过互斥体实现生产者/消费者问题
---生产者/消费者线程都执行了10次,但是不是理想状态生产>消费>生产>消费(emmm但是我这里是平均的,不知道乍回事)
---但是视频里面是就不是同步的
---解决方法1:通过判断全局变量容器的值来限制同步
---结果是有序的
---但是程序的消耗大,建立在浪费CPU资源的前提下
---使用事件来控制互斥体的有序
---发现互斥且有序
第十四:窗口的本质
---Win32Api:在C:\Windows\System32路径下所有的dll
---Win32Api的重要dll归属于2个最重要的模块(ntoskrnl.exe模块和Win32k.sys模块)
---进程创建过程
---对于的堆栈的存储空间
---每一个进程,都有自己的私有句柄表(在内核对象:EPROCESS中)
---在Win32k.sys中也存在和私有句柄表(HANDLE)的类似的:全局句柄表(HWND)
---全局句柄表(HWND):整个操作系统只有一张表,记录所有进程的的句柄
---总结:在Win32k.sys存在一个关于画图窗口的全局句柄表,这个表是指向不同进程(也有可能是指向进程的线程的,这里我不是很清楚)
---后面我去查了下:一个线程可以用多个窗口,但每个窗口只能属于一个线程。而一个进程会有多个线程,所以这里的全局句柄表HWND的内核地址对象我认为应该是指向ETHREAD结构体(sorry这个图有点问题)
---总结:一个进程存在一个私有的句柄表(Handle),指向了不同的线程。而在Win32k.sys模块中存在一个全局句柄表(HWND)存放每一个窗口的地址编号,而窗口对象的成员变量指向了窗口对应的线程
#GDI(graphic device interface)图形设备接口:
---学习目的:了解图形窗口(消息机制)的本质,而不是画图
---DebugView下载:https://learn.microsoft.com/en-us/sysinternals/downloads/debugview
---点击exe文件之后会生成一个窗口(点击exe >创建进程 > 生成窗口)
---获取窗口的句柄SPY++(在VC6里面自带:D:\VC6.0\Common\Tools)
---将SPY++的光盘拖拽到DebugView的窗口,获取窗口句柄:000508DA(十六进制)
---获取设备的上下文对象(先画在内存上,再渲染到屏幕上,所以在内存中会有一个对象)
---窗口(设备对象)是屏幕的对象,而设备上下文(是屏幕对应内存的对象)
---成功返回一个HDC的句柄,失败返回NULL
---给设备画线(先画在内存上)LineTo(),这个函数由GDI32.dll模块提供
---注意:这个API是采用的默认的画笔(Pen)、画刷(Brushes)、字体(Fonts)、位图(Bitmaps),如果想要自定义画线的颜色风格等,需要设置图形对象
---左上方的左边为(0,0)
---x轴y轴的范围是根据屏幕的分辨率决定
---但是我们想要画的线是我们自己定义的线条
---这就引出了图形对象,例如:自定义线条的颜色,粗细,虚实等
---注意:图形对象是继承设备上下文对象
---通过GDI32.dll绘图,创建画笔(Pen)对象,来设置线条属性
---对应画笔的颜色,指向的的是RGB结构体的指针
---RGB色彩模式是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的
---RGB的颜色空间
---由于是自定义的画笔对象,需要和设备上下文对象进行关联,替换原来默认的画笔
---当然,设备上下文对象也存在默认的:画笔(Pen)、画刷(Brushes)、字体(Fonts)、位图(Bitmaps)
---对应图形对象的参数选择
---在DebugView的左上角到右下角画一条红色的实线
---但是点击缩小窗口,红色的实线就会消失
---这是由于窗口其实是在不停的画,而我们刚刚只画了一次
#在桌面画一个黑框(设备对象为NULL)
---获取操作系统默认的画刷
---例如,存在一个灰色 / 黑色的画刷 / 以及自定义颜色的画刷等
---当然,也可以设置画笔、字体等
---返回一个画刷对象(这里需要强转)
---其中:DC_BRUSH可以根据SetDCBrushColor()自定义画刷的颜色
---由于这里传参是hdc,所以需要先将画笔和画刷与设备上下文关联之后,直接设置HDC
---画一个矩形的函数(和LineTo()函数类似)
---由于我这里SetDCBrushColor和DC_BRUSH不识别
---所以把自定义画刷改成了黑色画刷
---注意,要在Rectangle和getchar下2个断点才能看见(不然是一闪而过)
---相当于是用红色的画笔画一个矩形,然后用黑色画刷来填充这个矩形
#总结
---1.桌面窗口是画出来的2.窗口是不停画出来的(动态)
第十五:消息队列
---上节课内容回顾
---MoveToEx是函数,功能是将当前绘图位置移动到某个具体的点,同时也可获得之前位置的坐标。
---注意:MoveToEx和LIneTo函数(用当前画笔画一条线,从当前位置连到一个指定的点。这个函数调用完毕,当前位置变成x,y)配合使用
---lpPoint指针指向的结构体
---不停的画窗口
---发现窗口的颜色随着键盘输入的值而改变
---但是我不知道为什么会弹出2个printf,奇怪
---缺点1:.只能接受命令行消息,不能接受鼠标消息
---缺点2:没有地方存储接受的消息
#什么是消息
---当我们点击鼠标或者敲击键盘的时候,操作系统会捕获这些动作(注意不是进程先得到,而是OS先得到)
---然后将这些动作存储到一个结构体中,这个结构体就是消息
#消息队列
---一个线程可以用多个窗口,但每个窗口只能属于一个线程
---每一个线程都有一个消息队列
---点击窗口的× > OS捕获操作,生成消息对象 > OS根据鼠标的坐标找到对应的窗口(OS在底层变量所有的HWND) > 根据窗口找到对应的线程(根据成员变量指针)> 将消息对象传输到对应线程的消息对象 > 线程根据消息队列进行对应的处理
---从这里也可以看出:win32k.sys模块中,全局句柄表的分别代表HWND和线程
#总结
---1.每一个进程都存在多个线程
---2.每一个线程在内核都存在结构体
---3.每个线程都存在1个消息队列
---4.每个线程,可能存在多个窗口设备对象
---5.点击鼠标 > OS捕获生成消息(结构体) > 消息对象放入线程的内核结构体
第十六:第一个window(窗口)程序
#控制台和窗口程序的区别
---控制台程序:为了兼容DOS程序而设立的,没有自己的界面,只是通过字符串来显示或者监控程序。常被应用在测试、监控等用途,用户往往只关心数据,不在乎界面。
---窗口程序:是在用户计算机上运行的客户端应用程序,可显示信息、请求用户输入以及通过网络与远程计算机进行通信。对数据库处理提供全面支持,可用于设计窗体和可视控件。
---创建Win32Application项目
---生成一个入口
---发现生成的头文件已经将<windows.h>包含
---并且生成了一个和int main(int argc,char* argv[] )不同的入口函数
---nCmdShow:指明窗口如何显示。该参数可以是下列值之一:
#句柄总结
---什么是句柄:句柄是一种索引,真正的对象放在内核(大小:DWORD (4Byte))
---分类:全局句柄表和私有句柄表
---作用:相当于一层防火墙,防止用户层直接调用内核层
#调试信息的输出
---不能直接使用控制台程序的printf,而需要使用Win32Api
---例如:我们获取exe模块的在进程空间的内存地址
---注意:1.需要在头文件#include<stdio.h>,2.需要对头文件进行编译(build)3.不能点击BuildeExcute,而需要点击go(调试)
---执行结果如下
#利用Win32Api创建窗口
---1.定义你的窗口长成什么样的
---需要用WNDCLASS类(有很多成员,但是只有四个成员是有用的,其它可以不管)
---窗口函数的格式(参考之前创建线程CreateThread()的参数)
---而接受到的消息是如何处理的:这里是调用默认的消息处理函数,例如拖到窗口、最大化、最小化、点击关闭等
---注册窗口类RegisterClass:通过填充WNDCLASS结构将窗口过程与窗口类相关联。 我知道这是处理来自操作系统的消息的函数
---我的理解:1.鼠标点击窗口 > 2.OS捕获(坐标、操作)生成消息 > 3.传递给线程的消息队列 > 4.线程根据消息队列中消息的类型给OS,OS来进行窗口操作
---其中,第四步就是通过DefWindowProc()来默认处理消息(这个是处理的方法)
---那么,OS在通过DefWindowProc()来对窗口操作进行反映的时候,需要知道窗口的地址(在内存中),就需要通过RegisterClass来将窗口类(结构体)和OS关联,告诉OS窗口类的地址
---创建窗口:CreateWindow()
---注意:在注册完窗口类后就需要进行窗口的创建即CreateWindow
---而这个函数是基于窗口类的,所以还需要指定几个参数来制定特定窗口(自定义窗口类)。
---窗口的显示:根据不同的参数显示窗口
---例如:SW_SHOW:在窗口原来的位置以原来的尺寸激活和显示窗口。nCmdShow=5。
---创建并显示窗口
---窗口效果如下:
---注意:要在ShowWindow下断点调试(单步步入),不然是窗口只是一闪而过
#总结窗口的创建
---注意区分:模块、进程、线程的区别
---模块:一段可执行的程序(包括EXE和DLL),其程序代码、数据、资源被加载到内存中
---由系统静置的数据结构来管理它,就是一个块,这里所有的数据结构,名为Module/'mɑdʒul/ (模块、组件) Database (MDB)
---其实就是PE格式中的PE表头,你可以从WINNT.H文件中找到一个IMAGE_NT_HEADER结构。
---对于线程里面的消息队列,我们需要从里面获取消息(线程自己获取)
---采用GetMessage(),存在消息返回真,没有消息返回FALSE
---由于一个线程存在多个窗口,而窗口的操作是OS底层进行操作,而不是线程本身自己操作
---所以线程需要将消息取出(GetMessage),交给OS然后OS根据窗口的句柄找到对应的窗口位置(DispatchMessage),然后根据特定的窗口程序(WindowProc),根据映射规则(DefWindowProc)对窗口进行调整
---这个将获取的消息加给OS,然后OS将消息分发个窗口的函数就是:DispatchMessage
---如果想要打印键盘的输入,就需要在分发消息之前,对消息进行转换翻译:TranslateMessage
---将消息的虚拟码(类似于ASCII),转换为字符
---整个消息的流程
----相应的代码如下
---效果如下:窗口可以放大、缩小、拖拽、关闭等待
---而且不是一闪而过,而是永久存在
#总结
第十七:消息类型
#消息产生与处理流程
---查看GetMessage取出的消息的定义
---可以看出窗口程序(由OS调动)的四个参数就是我们通过GetMessage得到的
---我们对于之前的程序,对于点击窗口的×进行自定义处理
---并且对于键盘的输入进行打印
---发现拖动窗口,会有消息输出
---注意:在没有鼠标、键盘的输入的时候,也会由消息的输入
---这是由于内核在运行的时候已经有消息了
---例如:如果消息是1,代表的就是窗口的创建
---如果是2的话,就是窗口销毁
---相关代码
---发现可以打印键盘的输入(必须要经过TranslateMessage才行)
---要用调试模式才行
第十八:子窗口
#子窗口控件
---1.Windows提供了几个预定义的窗口类方便我们使用,一般称他们为子窗口控制,简称控件
---2.控件会自己处理消息,并且在自己状态改变时,通知父窗口
---3.预定义的控件有:按钮、复选框、编辑框、静态字符串标签、滚动条等
#在上一节的基础上,实现子窗口(左边写字、右边按钮)
---在窗口函数的时候,根据不同的消息创建子窗口
---子窗口在父窗口创建的同时创建
--可以看到创建了2个子窗口按钮,而且文本框可以写字
---我们希望子窗口有一些自己的属性
---例如:想要文本子窗口可以换行:搜索edit styles
#如果我们想点击按钮
---测试点击按钮,然后打印uMsg
---有210、111、135等,视频是说111
---搜索111发现是WM_COMMAND
---那么,又是如何来判别2和子窗口的呢
---查看接受WM_COMMAND的窗口函数:
---其中WPARAM wParam是32位,分为高16位和低16位
---低16位用来表明子窗口的身份,用LOWORD()获取wParam低16位的值
---定义宏来区分子窗口
---对于文本框,存在2个函数
---设置文本框的内容(写)
---获取文本框的内容(读取)
----对于文本框通过2个按钮进行读写操作
---点击设置(这里换行符没有读取)
---点击获取
第十九:物理内存和虚拟内存
#虚拟内存
---OS给每个进程都分配了4GB的内存空间
---1GB=1024MB,1MB=1024KB,1KB=1024Byte
----在进程的看似分配虚拟内存,实际上是物理内存(OS为了便于管理,将物理内存进行分页存储:4KB为1页)
---进程其实只有2GB的虚拟内存
---因此,线性地址都有4GB,但是未必都能访问(所以需要计算,那些地方分配了)
---注意:物理内存和内存条之间,其实还存在者一层映射
#物理内存
---可供使用的物理内存
MmNumberOfPhysicalPages *4=物理内存
---能够识别的物理内存:由于OS限制,32位系统最多识别64GB的物理内存(由于系统限制,XP只能识别4G)
---在虚拟机设置内存为1GB
---打开任务管理器,能识别的接近1024MB
---转化为4KB的物理页=1024*256=262144个物理页,转化为16进制:4 0000
---例如:查看DebugView.exe在内核中的EPROCESS结构体
---在结构体里面的0x11c的位置,变量这个成员变量
---这里记录:当前进程,哪些虚拟内存空间被使用了?(以页为单位)
---例如4KB的10,对于的十六进程:10 000(1KB=1024Byte,4KB=4096Byte,而4096对应的十进制是1000)
---20对应的十六进制:20 000,第三个30 000-12f 000
---如果物理页<程序需要:OS会把硬盘当作内存(设置一种叫做:虚拟内存)
---自定义虚拟内存780MB:当物理内存不够用时,在硬盘划区780MB当作内存条使用
---这个780MB也可以在C盘根目录查看(我的没看到):
---如果(A进程)的物理页使用不高,OS为了节省空间,将物理页数据放入虚拟内存(pagefile.sys)中
---当A进程再次访问虚拟内存(4GB)时,OS分配一个新的物理页,然后将数据从pagefile.sys中拿出,放入物理页
第二十:虚拟内存的申请释放
#运行Test.exe查看内核调试器那些虚拟内存被占用
---start:虚拟地址的开始(4KB),end:虚拟地址的结束
---私有的:Private物理页只能自己使用
---映射的:Mapping物理页和其它进程公用
#申请内存的2种方式
---1.通过VirtualAlloc(给自己进程申请) / VirtualAllocEx(可以指定PID为其它进制)申请
---2.通过CreateFileMapping映射的:Mapped Memory
#内存申请与释放
---分配内存的类型DWORD flAllocationType
#内存释放VirtualFree
---释放类型分为2种
---在程序实现
---发现多了390、391两个虚拟地址块,属性是private,就是这个地址是当前进程独有
---Commit=2,代表2个物理页
---如果是MEM_RESERVE就是0,代表没有物理页
---而且是可读可写属性
---VirtualAlloc和malloc的区别
---malloc的堆和栈都是申请号的内存进行分配,都在用户模式区
---malloc的汇编:可以看出只是拿分配好的虚拟内存来用
---堆(程序员申请)和栈(自动分配)的区别
---查看汇编也可以看出
---总结:无论是堆、栈,都是进程启动时,Win32API的VirtualAlloc申请的虚拟内存
第二十一:共享内存的申请和释放
#申请内存的2种方式
---1.通过VirtualAlloc(给自己进程申请) / VirtualAllocEx(可以指定PID为其它进制)申请
---2.通过CreateFileMapping映射的:Mapped Memory
---CreateFileMapping的本质:创建一个内核对象,为我们提供一块用来映射的物理内存(可以和文件关联,将文件映射到物理页上面)
---如果只想要物理内存,不映射文件,hFile=NULL
---和CreateProcess一样,第二次调用任然返回之前创建的物理页的句柄,并且在GetLastERROR返回ERROR_ALREADY_EXISTS
---物理页创建好了,但是并没有和进程A、B的虚拟内存进行关联
---将物理页和线性地址映射:MapViewOfFile
---关闭物理页和虚拟内存的映射:UnmapViewOfFile
---进程A和物理内存关联
---物理页关联到00630000这个虚拟地址
---物理页多了390,而且是Mapped(映射),读写权限
---commit=0是应为:OS会在你使用的时候给你挂上物理页,不用担心
---重开一个VC6,代表进程B
---注意:要在关闭物理页之前加getchar(),不然会报错
---发现读取成功