第四章 进程
一、进程定义:分为两个部分
1、进程内核对象,一个内存块,一个数据结构,维护该内核对象的某些属性 。地址所在:我猜在内存中
2、进程地址空间,包含可执行文件或者dll模块的代码和数据。还包含动态内存分配。比如线程堆栈和堆的分配。地址所在:我猜在磁盘中
二、进程启动
启动流程:嵌入启动函数->入口点函数->入口点函数返回->嵌入启动函数返回
三、CreateProcess函数
1、略
四、终止进程
1、ExitProcess:该函数执行后,函数后面代码不会执行,c/c++运行库不会正常执行清理工作
2、TerminnateProcess:同ExitProcess的缺点,不同的是该函数是异步,且可以销毁其他进程,需要用到waitforobject函数
3、进程中所有线程停止结束,没有活动线程
明确一点:虽然1和2的调用方式没有正确执行清理工作,导致暂时内存泄露。但是进程结束以后,会清理一切内存,不会内存泄露。
第五章 作业
相当于进程manager,管理进程,进程容器
可以统一管理限制进程,改变进程属性
第六章 线程基础
1、windows是抢占式操作系统
2、cpu20ms查询一次可调用的线程内核对象
3、线程定义
一、线程内核对象,操作系统内核可以操纵这个对象,管理线程;他是一个内存块,数据结构,存放线程信息,上下文context,context包含:1、指针指令寄存器 2、堆栈指针寄存器
二、线程栈,用户储存线程运行时所需要的所有函数参数和局部变量
4、终止线程
1、ExitThread
2、TerminnateThread
基本和进程终止方式类似,都不推荐,调用后不会之后后续代码,不会执行入口函数返回,不会执行清理工作
5、线程内幕
1、初始化信息:引用计数:2 , 退出代码:active ,信号标志:未激活 ,中断计数 :1
2、context存在于线程内核对象中,包含指令指针寄存器:存放RtlUserThreadStart;推展指针寄存器:存放pfnStartAddr
在调用CreateThread后,会先调用RtlUserThreadStart,起始函数
函数模型
ExitProcess后不会返回到RtlUserThreadStart
6、c/c++运行库注意事项
一、_beginthreadex代替CreateThread
二、理由:c/c++运行库初始设计为单线程,很多全局变量和全局函数不是线程安全的,比如_error
所以需要每个线程创建自己独立的数据块_ptidddata,存放属于线程自己的变量和函数,_ptidddata是在c/c++运行库的堆里申请的
在线程调用c/c++运行库函数_error的时候,其实c/c++库会拿到主调线程绑定的_ptidddata,然后调用线程绑定的专属自己_ptidddata中的_error函数,这样就实现了线程安全
三、_beginthreadex实现逻辑:_beginthreadex内部封装了CreateThread,申请_ptidddata内存,绑定主调线程id和_ptiddata,
执行线程函数pfnStartAddr
函数顺序逻辑:_beginthreadex->CreateThread->RtlUserThreadStart->_threadstartex(绑定主调线程id和_ptiddata)->_callthreadex(异常优化:包括运行异常等还有signal异常)->pfnStartAddr->_endthreadex
_endthreadex的工作是释放_ptidddata的内存还有结束退出线程,ExitThread
PS:如果使用CreateThread后果:
1、假如线程使用需要_ptidddata结构的c/c++运行库函数或者变量,那么运行库会自动生成_ptidddata并和线程绑定
那么在线程结束后,这个_ptidddata内存不会释放,造成内存泄漏(大多数c/c++运行库函数都是线程安全的,不用这个_ptidddata结构,那就不会有这个问题,要注意的是少部分需要_ptidddata的函数)
2、创建线程后立刻使用signal信号函数,那么会崩溃
四、不要使用老版本的_beginthread和_endthread
7、注意自己的身份
GetCurrrentProcess返回的是-1或者-2,是伪句柄,指的是当前进程,
当前进程使用正常,传给需要句柄的函数的时候,会被翻译成当前进程句柄值
但是其他进程使用这个句柄,其他进程的函数就会默认翻译成其他进程的句柄
但是CloseHandle的时候,只是会返回调用失败
利用DuplicateHandle可以将伪句柄变成实际句柄值
第七章 线程调度优先级关联性
上下文切换概念:系统大每隔20ms检查当前存在的可调度的线程内核对象,调用该内核对象,加载context。进行调度。
一、线程挂起和恢复
函数:resumethread,suspendthread,存在挂起计数概念,挂起计数>1,是挂起,挂起计数为0,恢复
挂起时候为不可调度状态,可以在创建挂起线程,在代码运行之前,改变他的环境(优先级)
挂起和恢复函数可以被重复挂起和恢复,但是挂起次数要等于恢复次数,两个函数返回值是函数操作前的挂起计数
注意事项:suspendthread是异步的,在恢复之前,是不能执行用户模式的代码。
挂起线程要注意线程在做什么,比如线程正在分配堆内存,其他线程将该线程挂起,这样,如果其他线程再去申请分配内存
,就会等待,等待挂起的那个线程恢复,死锁问题出现。要明确线程在做什么,避免死锁发生,才能用suspendthread
二、进程的挂起和恢复
进程的挂起和恢复,其实是用的线程的挂起和恢复,通过利用toolhelp拿到进程的所有线程,遍历所有线程,执行挂起或者恢复
resumethread和suspendthread
注意事项:用toolhelp拿到所有线程的时候,如果创建和销毁线程,那么对拿到的线程进行挂起和恢复就会出现问题
三、睡眠
sleep。线程告诉系设定一个时间范围,可以不调度自己
传入infinit说明是需要永远不调用自己,不要给自己留时间片段
传入0说明,告诉系统,主调线程放弃剩余时间片。切换到其他同等或者高于此线程的其他线程。如果没有这样的其他线程,那么会再次调用此线程
四、切换到另一个线程
switchtothread
如果存在其他可调度线程,那么可以切换线程
sleep和switchtothread的区别:switchtothread允许低优先级的线程切换
五、线程的执行时间
执行时间=用户模式执行时间(也就是应用代码部分)
不包含内核模式执行时间(系统内核)
方法一:使用gettickcount64->dowork->gettickcount64
比较两次获得的时间戳,缺点:包含内核模式执行时间,因为切换线程也要时间,内核代码也要时间。不准确、
方法二:getthreadtime
可以分别获得内核时间和用户时间,使用getprocesstimes可以获得所有线程累加的内核和用户时间
缺点:这是vista版本之前的方法,目前cpu时间分配改变,并不好用了
方法三:vista版本后,获得时钟周期数,queryperformancefrequency+queryperformancecounter
分别是获取频率和时钟周期数
用户模式线程执行时间 = queryperformancecounter/queryperformancefrequency,周期数/频率 = 时间,这里针对短生命代码,排除内核内码时间
缺点:长生命周期内核代码时间怎么办,对不对
方法四:querythreadcycletime/获取cpu频率函数
推荐:获取线程所用的时钟周期数后,再去获取cpu主频,然后相除
cpu主频获得方式:通过方式三,计算睡眠一秒,得出的时钟周期数/频率,更精确,因为这个是实时的
这里的querythreadcycletime应该除去了内核时间,应该为纯线程用户时间
六、context上下文
context包含了好几种寄存器
控制寄存器:指令指针,栈指针,标志函数返回等,
整形寄存器,浮点型寄存器,调试扩展和标识寄存器
getthreadcontext获得上下文,只能获取用户模式的上下文,获取之前要suspendthread,不然线程上下文改变,获取过来的信息不是旧的。
setthreadcontext设置上下文,也是要先挂起线程,如果设置不好,会把线程异常
增加这些函数的目的是帮助调试器和其他工具实现 设置下一个语句
七、线程优先级
优先级0-31,高优先级会抢占低优先级线程,只有一个为0优先级的页面清零线程
八、抽象角度看线程优先级
线程基本优先级 = 进程优先级+线程相对优先级
用户应用程序优先级范围:1-16
内核模式驱动程序:17-31
realtime线程优先级的最终优先级不能低于16,非reatime的不能高于15
设计优先级原则:
高优先级的线程应该是不可调度的,占用cpu资源少的,等待调度,但是如果需要的话,要及时响应
低优先级的线程应该是处理大量任务的,占用cpu资源
九、优先级编程
1、setthreadpriority,getthreadpriority
setpriorityclass,getpriortyclass
注意事项:改变优先级的时候,要挂起线程或者进程,然后在恢复线程或者进程
创建子进程的进程会选择子进程运行环境的优先级(也就是父进程优先级)
2、动态提升优先级
如果要动态提升的优先级,可以通过
setthreadpriortyboost,
setprocesspriortyboost
动态提升只提升1-15优先级的线程,而且提升后的线程也不会超过15
动态提升优先级情况有两种:
io事件:磁盘读取,窗口事件,维持两个时间片,每个时间片依次减一
饥饿线程:如果系统检测存饥饿线程3,4秒,会将饥饿线程提升至15的基本优先级,维持两个时间片
3、为前台程序微调调度程序
程序分为前台程序和后台程序
系统可以让用户微调提升前台优先级,因为窗口是展示给用户看,理所应当优先处理
前台进程是normal才能提升
4、调度io优先级
这里有个后台优先级设置,通过setthreadpriority,设置后台优先级,这样即可以降低io处理优先级,优先后台程序处理
注意事项:文件io后台处理接口也可以设置,setfileinfomationbuhandle,这会覆盖setthreadpriority
另一个注意事项是小心死锁:比如两个线程a,b,a线程在等待b线程释放锁,由于b线程设置了后台优先级,可能导致不io,b线程可能不释放锁,导致a线程死锁
十、关联性
1、通过函数设置关联性
提高性能,因为程序运行在不同组cpu,会导致性能下降
可以指定进程或者线程在哪些cpu运行
setprocessaffinymask设置进程的线程在哪些cpu里
getsysteminfo获得cpu信息
getprocessaffinymask获得线程在哪些cpu
setthreadaffinymask设置线程在哪些cpu里
setthreadidealprocessor设置理想cpu作用:可以在理想目标cpu被占用或者无法使用的时候,使用其他cpu,这里的参数就是设置理想cpu的id。当通过setprocessaffinymask或者setthreadaffinymask设置目标cpu后,当这些cpu被高优先级线程占用或者其他原因无法使用的时候,我们这个理想cpu就会登场
2、程序未开始时候,通过程序头部设置处理器关联性利用imagehlp.h的一些函数
3、任务管理器设置关联性。如果程序在x86的环境,可以设置bcd限制使用cpu数量
第八章:用户模式下的线程同步
用户模式下的线程同步有interlocked,关键段,读写段,条件变量
其中条件变量可以和关键段或者读写段搭配使用
这几种都是原子方式工作:可以保证一个线程访问修改资源的时候,其他线程不会进入,书上说是通过总线什么的 拉起
性能比较:interlocked系列函数>读写段>关键段
关键段和读写段内部都是interlocked系列函数编写的
如果要编写高性能,优先考虑用户模式的线程同步
interlocked系列:
优点:最快
缺点:interlocked系列函数多,不好记,只能修改单个资源
关键段:
优点:可以对相同逻辑的一些资源进行保护,可以重复锁定资源,和释放资源
缺点:当我们用关键段,出现死锁,互相等待的时候,那么可以设置挨饿时间=最大等待时间。
关键段内部有interlock系列函数和一个事件内核,如果有其他线程访问,会用这个事件内核让线程等待
关键段+旋转锁更配性能更高:因为用户模式切换到内核模式需要很多时间,可能在切完后,资源就可以访问了,所以配上旋转锁。
在旋转锁等待一段时间后,如果资源还是不能访问,再进入内核模式等待。
一些函数整理:
关键段:initializecriticalsection,deletecriticalsection,entercriticalsection,leavecriticalsection,tryentercriticalsection
旋转锁:initializecriticalsectionandspincout,setcriticalsectionandspincout,初始化关键段和旋转锁
书上说保护进程堆的旋转锁次数为4000为宜,其他的需要自己试验
关键段异常出错:
1、initializecriticalsection和initializecriticalsectionandspincout分配内存的时候失败。解决办法:使用异常捕获或者返回返回值发现,等到内存正常或者排查出问题后,再去初始化关键段。
2、entercriticalsection的时候,创建事件内核对象失败,因为内存不够。解决办法:xp系统后,会自动使用有键事件内核代替和处理
这个内核可以多个线程通用,看样子省内存。
读写段srwlock:
优点:读写逻辑分离,支持读的时候不进行原子方式隔离同步,所有线程可以同一时间访问资源;写的时候再原子方式。
缺点:和关键段相比,没有try函数,不能递归重复锁定保护资源
疑问:读写锁是不是跟关键段一样,有内核等待事件?看书上好像没有诶,没有看到读写锁搭配旋转锁的使用方法
条件变量:
优点:搭配关键段或者读写段来用的,用来做触发条件
缺点:没啥缺点
其他:volatile作用:确保每次读取是从内存中读取
如果不加的话,如果编译器设置优化的话,变量会被优化成从cpu寄存器或者其他缓冲区读取
高速缓冲区:
内存->高速缓冲区->cpu
优点:提高性能,不需要每次读取数据要去内存那拿
缺点:多处理器中,当cpu修改数据的时候,每个cpu相关联的高速缓冲区要作废,重新从内存读数据写数据到缓冲区,这个时候影响性能
提高高速缓冲区性能办法:
1、读和写,分开写,要修改的数据放在一块,不修改的数据放在一块
2、应用程序的数据要和缓冲区进行字节对其,getlogiccalprocessorinformation获得高速缓冲区的大小
字节对其后,就可以有高速缓冲区功能了
对其,也就是设置高速缓冲区函数:_declspec(algin)