【读书笔记】Windows核心编程 - 进程,线程相关

何为内核对象、句柄、内核对象句柄表?


如何关闭内核对象?

  • CloseHandle 函数

将内核对象计数减1,如果计数变为0,则内核对象被销毁
函数返回前,会清除进程句柄表中的对应记录项。无论内核对象是否被销毁,此过程都会发生。

  • 进程退出

当进程终止运行时,系统会扫描该进程的句柄表。如果表中有有效的记录项,则操作系统会关闭此句柄,对应内核对象计数减1。


如何跨进程共享内核对象?

  1. 继承
  2. 命名
  3. 复制
继承

当父进程创建一个内核对象时,父进程必须指出此句柄是否可以继承。

创建子进程时,遍历父进程的句柄表,将可继承的句柄复制到子进程句柄表相同的位置。这意味着,父进程和子进程对一个内核对象进行标识的句柄值是完全一样的。处了,复制句柄表达的记录项,系统还会递增内核对象的计数。

子进程不知道自己继承了什么句柄,需要父进程将句柄值作为命令行参数传给子进程。

句柄继承之所以能够实现,唯一的原因是,共享的内核对象的句柄值在父进程和子进程中完全一样。

由于句柄值实际是作为进程句柄表的索引来使用的,所以这些句柄是与当前进程相关的,无法供其他进程使用,如果在其他进程中使用它,就会实际引用到那个进程的句柄表中同一位置的内核对象——只是索引值相同而已,不知道会指向什么对象。

命名

调用CreateMutex()函数,系统会首先查看是否存在一个命名相同的内核对象,如果存在,检查对象的类型。

如果相同,且权限运行时,系统会在进程的句柄表中查找一个空白的记录项,将其初始化为指向现有的内核对象,内核对象计数递增1。

如果类型不匹配,或者权限不允许,则函数调用返回失败。

赋值对象句柄

需要三个进程,调用DuplicateHandle()函数。


如何实现程序只允许一个实例?

使用命名内核对象

程序启动时,Create*()创建命名对象,然后GetLastError(),判断是不是ERROR_ALREADY_EXISTS。即内核对象是否已经存在。

如果已经存在,则终止程序。


应用程序的入口函数是什么?

main winmain dllmain

MainCRTStartUp、WinMainCRTStartUp、DllMainCRTStartUp


创建新进程的流程是什么?

创建新进程:

  • 操作系统创建内核对象,初始计数为1
  • 系统为新进程创建虚拟地址空间,加载可执行文件exe,dll
  • 操作系统为进程创建一个线程内核对象,初始计数为1
  • 主线程开始执行C/C++运行时库的入口函数:MainCRTStartUp()或WinMainCRTStartup()。

创建一个进程,会创建一个进程内核对象和一个线程内核对象 。CreateProcess函数内部会打开这些对象,计数变为2

想释放进程内核对象,必须:进程终止(计数减1)、父进程关闭句柄(计数减1)
想释放线程内核对象,必须:线程终止(计数减1)、进程关闭句柄(计数减1)

程序装载、启动过程


终止进程的方式有哪些?

四种方式:

  • 主线程从入口点函数返回
  • 任何一个线程调用ExitProcess()函数
  • 另一个进程调用TerminateProcess()函数
  • 进程中所线程自然死亡

进程终止运行时有哪些步骤?

进程终止时,操作系统会:

  • 终止所有线程
  • 释放所有内核对象
  • 进程退出码从STILL_ACTIVE变为传给ExitrProcess或TerminateProcess函数的代码
  • 进程内核对象变为已触发态
  • 进程内核对象计数递减1

如何判断子进程终止了的?

利用进程内核对象:创建子进程后,调用WaitForSingleObject()函数,传入子进程的句柄,当子进程结束时,其内核对象会变为触发状态,函数才返回。


进程意外终止,会不会造成资源泄漏?

操作系统会在进程终止后彻底清理,进程终止不会泄露任何东西


ExitProcess和ExiteThread作用效果?/

主线程结束,进程会不会结束?

从操作系统的角度来说:一个进程在其所有线程都终止之后才会终止。

但是,C/C++运行库为应用程序采取了一个不同的策略:不管进程中是否有其他线程运行,只要主线程从它的入口点函数返回,C/C++运行库就会调用ExitProcess()来终止进程。

不过,如果在入口点函数中调用ExitThread(),而不是调用ExitProcess()或者直接返回,应用程序的主线程将停止运行,而进程中其他的线程继续运行。

C/C++应用程序应当避免使用ExitProcess()和ExitThread()函数,因为会导致进程或者线程直接终止运行,不会返回当前函数调用,C/C++运行库不能执行正确清理工作。

ExitThread()的情况下,进程会继续运行,但可能泄露内存或其他资源。


作业的概念是什么?如何获取作业的通知?

操作系统提供了作业内核对象,允许将进程组合到一起,并创建一个沙箱限制进程能做什么。作业可以理解为进程容器。


线程的组成部分有哪些?

  • 一个线程内核对象
  • 一个线程栈

线程内核对象中保存有CPU寄存器的状态:CONTEXT结构,用于上下文的切换。

线程栈的预订大小要么由链接器指定,要么由CreateThread的参数指定。取其中的较大值。预定的地址空间的容量设定了栈空间的上限,不仅可以防止应用程序耗尽内存,还可以捕获无穷递归的bug。


线程函数使用局部变量和全局变量会出现什么问题?如何解决?

互斥问题
同步问题

使用线程间互斥、同步机制:临界区,互斥锁,信号量,事件,消息等


线程栈空间是如何管理的?

系统创建线程时,会为线程栈预订一块地址空间区域(每个线程都有自己的栈)。
在预订了地址空间后,系统会为区域的最高两个页面调拨物理存储器。
系统把栈指针指向第一个页面的末尾,这个页面就是线程开始使用栈的地方。设置第二个页面为防护页面

当线程试图访问防护页面的内存时,系统会得到通知,并先给防护页面下面的页面调拨存储器,然后将新的页面设为防护页面,去除原来的防护页面防护属性。

随着线程调用函数增多,栈不断生长。但是系统不会给区域底部的页面调拨存储器。系统给倒数第二个页面调拨物理存储器时,同时还会抛出EXCEPTION_STACK_OVERFLOW异常,系统发出这一通知时,程序应当做出响应,从而使得程序能从异常中恢复。

如果线程不响应异常,继续使用栈,并尝试访问最后一个页面内存时,系统会抛出访问违规异常,收回控制权,结束整个进程

系统不给栈地址空间区域最底层的页面调拨物理存储器。目的是保护进程使用的其他数据。


终止线程的方法有几种?

  • 线程函数返回
  • 调用ExitThread()函数
  • 其他线程调用TerminateThread()
  • 进程终止

ExitThread、TerminateThread、_endthreadex区别

  • TerminateThread()

将立即结束线程,操作系统不会回收线程使用的堆栈,C++运行库也收不到线程终止的通知。C/C++的资源(如C++类对象)不会被销毁

这是为了让其他线程可以继续使用此线程堆栈上的值。

  • ExitThread()

将立即结束线程,操作系统会回收线程使用的操作系统资源,但是C++运行库收不到线程终止的通知。故C/C++的资源(如C++类对象)不会被销毁

  • _endthreadex()

将立即结束线程,操作系统会回收线程使用的操作系统资源,C++运行库会收不到线程终止的通知。C/C++的资源(如_tiddata块)会被销毁。


CreateThread和_beginthreadex 、 ExitThread和_endthreadex的区别?

参考

CreateThread()和_beginthreadex()有什么不同?

_beginthreadex是对CreateThread的包装,它在调用createthread之前申请了一个叫tiddata的结构。这个结构被保存到TLS数组,然后调用用户的线程入口真正开始线程。(每个线程都有线程环境块(TEB,Thread Environment Block)。这个结构保存了线程堆栈地址,线程ID等,其中有一个域是TLS数组地址。)

如果使用CreateThread创建线程,线程使用_tidata时,若发现没有_tidata结构时会马上申请这个结构。

但是ExitThread()不会释放_tidata结构内存,而_endthreadex()则会释放。

但是,很多时候即使使用CreateThread()/ExitThread(),也不会造成内存泄漏,为什么?

原因在于DLLMain函数,每次进程/线程开始退出的时候,每个DLL的DllMain都会被调用一次,于是动态链接版的CRT 就有机会在DllMain中释放线程的_tiddata。

可是只有当CRT是动态链接版的时候才起作用,静态链接CRT没有DllMain,这就造成了使用CreateThread()会造成内存泄漏


线程终止过程

  • 线程释放使用的内核对象句柄
  • 线程退出代码从STILL_ACTIVE变为传给ExitThread或TerminateThread的代码
  • 线程内核对象变为触发态
  • 如果线程是进程最后一个线程,终止进程
  • 线程内核对象计数减1

_beginthreadex 和 _begingthread的区别?

_begingthread():不能创建具有安全属性的线程,不能让线程创建时挂起,不能获得程序ID值。

_endthread():不能指定退出码


线程挂起和恢复的机制是怎样的?

每个线程有一个上下文(CONTEXT),后者保存在线程的内核对象中。操作系统每个20ms会查看当前所有的内核对象,windows在可调度的线程中选择一个,将上下文载入CPU寄存器。即上下文切换。

又过20ms,windows将cpu寄存器存回线程的上下文,线程不再运行。系统选择一个可调度线程的内核对象,重复上述操作。

线程内核对象有一个值表示线程的挂起计数。调用CreateProcess或者CreateThread时,系统将创建线程内核对象,并把挂起计数初始化为1。这样就不会给线程调度CPU了,因为线程初始化需要时间,不能在线程准备好之前就开始执行它。线程初始化完之后,CreateProcess或者CreateThread函数将查看是否有CREATE_SUSPEND标志传入,如果有,函数返回并让新线程处于挂起态。如果没有,则将线程挂起计数递减为0。线程挂起计数为0时,线程就是可调度的了


如何挂起进程(如何将进程中所有线程挂起),这样做会有什么问题?

通过SuspendThread函数可以增加线程挂起计数,通过ResumeThread可以递减计数。两函数均返回线程的前一个挂起计数

不知道线程在做什么就挂起线程,如果线程在分配堆内存被挂起,则线程将锁定堆,其他线程要访问堆的时候,它们的执行会被中止,直到第一个线程恢复。


Sleep(0)的作用,及其与SwitchToThread()的区别?

Sleep(0)表示线程放弃时间片的剩余时间,它强制系统选择调度线程,但是系统可能重新调度刚刚Sleep(0)的线程。如果没有相同或者优先级高的可调度线程时,就会发生这种情况。

SwitchToThread()调用后,如果有其他可调度的线程,系统会让此线程运行,即使该线程的优先级比较低


何为线程调度饥饿?系统如何处理饥饿?

高优先级的线程占用CPU的时间,低优先级的线程无法运行,称为饥饿。

windows使用动态提升饥饿线程的优先级方式。


何为优先级逆转?会有什么问题?如何避免?

定义:

优先级反转是指一个低优先级的任务持有一个被高优先级任务所需要的共享资源。高优先任务由于因资源缺乏而处于受阻状态,一直等到低优先级任务释放资源为止。

问题:

而低优先级获得的CPU时间少,如果此时有优先级处于两者之间的任务,并且不需要那个共享资源,则该中优先级的任务反而超过这两个任务而获得CPU时间。

如果高优先级等待资源时不是阻塞等待,而是忙循环,则可能永远无法获得资源,因为此时低优先级进程无法与高优先级进程争夺CPU时间,从而无法执行,进而无法释放资源,造成的后果就是高优先级任务无法获得资源而继续推进。

解决方案:

当线/进程使用资源时,提升其优先级别,不允许优先级倒置。

(1)优先级置顶

给资源设定一个比使用该资源的具有高优先级的用户的优先级还要高一级的优先级,使用资源的线/进程都将动态获得这个高优先级,其他试图进入临界区的进程的优先级都低于这个高优先级,那么优先级反转就不会发生。

(2)优先级继承

当一个高优先级进程等待一个低优先级进程持有的资源时,低优先级进程将暂时获得高优先级进程的优先级别,在释放共享资源后,低优先级进程回到原来的优先级别。嵌入式系统VxWorks就是采用这种策略。

(3)第三种方法就是使用中断禁止,通过禁止中断来保护临界区,采用此种策略的系统只有两种优先级:可抢占优先级和中断禁止优先级。前者为一般进程运行时的优先级,后者为运行于临界区的优先级。火星探路者正是由于在临界区中运行的气象任务被中断发生的通信任务所抢占才导致故障,如果有临界区的禁止中断保护,此一问题也不会发生。


何为进程、线程的关联性?

windows在分配线程任务时,使用软关联。

如果其他因素一样,系统将使线程在上一次运行的处理器上运行,让线程始终在同一个处理器上运行有助于重用高速缓存。


介绍自旋锁的原理,使用自旋锁会有哪些问题?

自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。原子操作+自循环。即通常说的用户构造模式

由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。

自旋锁会耗费CPU时间,且在单CPU且不可抢占的内核机器上使用自旋锁没有意义。


什么情况下适合使用自旋锁?

资源只会被占用一小段时间


volatile限定符的作用是什么?

告诉编译器不要对这个变量做任何优化,始终保持变量在内存中的位置读取变量的值。


临界区和互斥锁的区别?

1、互斥锁是内核对象,临界区是非内核对象。

临界区内部使用数据结构计数和事件内核对象实现线程等待。

2、故互斥锁可以实现进程间互斥,临界区不可以实现进程间互斥。

3、而且,内核对象使用时需要陷入内核态,速度慢。临界区不需要陷入内核态。只在用户态进行锁操作,速度快。


临界区的性能缺点是什么?如何解决?

临界区使用速度快,但是如果已经有线程进入了临界区,则线程需要进入等待状态。还是需要从用户态转到内核态。开销大。

为了提高关键段的性能,把自旋锁合并到关键段中,进入临界区时,用一个自旋锁不断循环,尝试在一段时间内获得资源访问权。尝试失败后,切换到内核模式并进入等待状态。


条件变量如何使用?

pthread_cond使用

pthread_cond_wait总和一个互斥锁结合使用。在调用pthread_cond_wait前要先获取锁。pthread_cond_wait函数执行时先自动释放指定的锁,然后等待条件变量的变化。在函数调用返回之前,自动将指定的互斥量重新锁住。

pthread_cond_broadcast()与pthread_cond_signal()有什么区别?
signal 是唤醒N多 race 中的一个,具体那个被唤醒,依赖于操作系统。
broadcast 是唤醒N多reace 的全部。
注:broadcast后的非空检查:把资源放入时候后signal或者broadcast,这个时候如果是broadcast,那么可能会唤起很多线程对mutex进行锁竞争,但是必须注意一点,竞争到锁后,对拿到的资源需要进行非空检查,因为别的线程可能已经拿到锁,然后处理完资源把资源清空了。


手动重置事件与自动重置事件的区别?

当一个手动重置事件被触发的时候,事件需要手动重置。正在等待的所有线程都会变成可调度的。

当一个自动重置事件被触发的时候,只有一个正在等待的线程会被变成可调度态。事件自动重置。


互斥锁遗弃问题,遗弃会造成其他等待线程永久挂起吗?

互斥锁和临界区的区别?

等待链遍历?不记录哪些内核对象?

异步I/O请求一定会以异步方式执行吗?

获取异步I/O完成通知有哪几种方法?

可提醒I/O是如何工作的?回调函数在什么时候被调用执行?

可提醒I/O的缺点在哪里?

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值