精华内容
下载资源
问答
  • 线程切换怎么样一个流程?
    千次阅读
    2021-07-07 00:17:03

    点击上方“朱小厮的博客”,选择“设为星标”

    后台回复"书",获取

    后台回复“k8s”,可领取k8s资料

    进程切换分两步:

    1.切换页目录以使用新的地址空间

    2.切换内核栈和硬件上下文

    对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。

    切换的性能消耗:

    1、线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

    2、另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor's Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

    系统调用:处于进程上下文

    系统调用是在进程上下文中,并没有tasklet之类的延迟运行,系统调用本身可以休眠,这些可以参见内核代码

    虽然系统调用实与其他中断实现有点类似,通过IDT表查找入口处理函数,但是系统调用与其他中断最大的不同是,系统调用是代表当前进程执行的,所以current宏/task_struct是有意义的,这个休眠可以被唤醒
     
    系统调用,异常,中断(其中中断是异步时钟,异常时同步时钟),也可以把系统调用成为异常

    中断上下文:在中断中执行时依赖的环境,就是中断上下文(不包括系统调用,是硬件中断)

    进程上下文:当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文

    1、首先,这两个上下文都处于内核空间。

    2、其次,两者的区别在于,进程上下文与当前执行进程密切相关,而中断上下文在逻辑上与进程没有关系。

    进程上下文主要是异常处理程序和内核线程。内核之所以进入进程上下文是因为进程自身的一些工作需要在内核中做。例如,系统调用是为当前进程服务的,异常通常是处理进程导致的错误状态等。所以在进程上下文中引用current是有意义的。

    内核进入中断上下文是因为中断信号而导致的中断处理或软中断。而中断信号的发生是随机的,中断处理程序及软中断并不能事先预测发生中断时当前运行的是哪个进程,所以在中断上下文中引用current是可以的,但没有意义。事实上,对于A进程希望等待的中断信号,可能在B进程执行期间发生。例如,A进程启动写磁盘操作,A进程睡眠后现在时B进程在运行,当磁盘写完后磁盘中断信号打断的是B进程,在中断处理时会唤醒A进程。

    上下文这个词会让人想到进程的CPU寄存器状态,但好像进入进程上下文(异常处理系统调用)和进入中断上下文(中断处理),内核所做的工作没有太大区别。所以,这两个上下文的主要区别,我认为在于是否与进程相关。

    运行于进程上下文的内核代码是可抢占的,但中断上下文则会一直运行至结束,不会被抢占。因此,内核会限制中断上下文的工作,不允许其执行如下操作:

    (1) 进入睡眠状态或主动放弃CPU;

    由于中断上下文不属于任何进程,它与current没有任何关系(尽管此时current指向被中断的进程),所以中断上下文一旦睡眠或者放弃CPU,将无法被唤醒。所以也叫原子上下文(atomic context)。

    (2) 占用互斥体;

    为了保护中断句柄临界区资源,不能使用mutexes。如果获得不到信号量,代码就会睡眠,会产生和上面相同的情况,如果必须使用锁,则使用spinlock。

    (3) 执行耗时的任务;

    中断处理应该尽可能快,因为内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。在中断处理例程中执行耗时任务时,应该交由中断处理例程底半部来处理。

    (4) 访问用户空间虚拟内存。

    因为中断上下文是和特定进程无关的,它是内核代表硬件运行在内核空间,所以在中断上下文无法访问用户空间的虚拟地址

    (5) 中断处理例程不应该设置成reentrant(可被并行或递归调用的例程)。

    因为中断发生时,preempt和irq都被disable,直到中断返回。所以中断上下文和进程上下文不一样,中断处理例程的不同实例,是不允许在SMP上并发运行的。

    (6)中断处理例程可以被更高级别的IRQ中断。(不能嵌套中断)使用软中断,上部分关中断,也就是禁止嵌套,下半部分使用软中断

    如果想禁止这种中断,可以将中断处理例程定义成快速处理例程,相当于告诉CPU,该例程运行时,禁止本地CPU上所有中断请求。这直接导致的结果是,由于其他中断被延迟响应,系统性能下降。

    软中断是一种延时机制,代码执行的优先级比进程要高,比硬中断要低。相比于硬件中断,软中段是在开中断的环境中执行的(长时间关中断对系统的开销太大), 代码是执行在中断/线程上下文的,是不能睡眠的,虽然每个cpu都有一个对应的ksoftirqd/n线程来执行软中断,但是do_softirq这个函数也还会在中断退出时调用到,因此不能睡眠(中断上下文不能睡眠的原因是由于调度系统是以进程为基本单位的,调度时会把当前进程的上下文保存在task_struct这个数据结构中,当进程被调度重新执行时会找到执行的断点,但是中断上下文是没有特定task_struct结构体的,当然现在有所谓的线程话中断,可以满足在中断处理函数执行阻塞操作,但是实时性可能会有问题。还有就是中断代表当前进程执行的概念,个人感觉有点扯淡,毕竟整个内核空间是由所有进程共享的,不存在代表的概念)

    上面我们介绍的可延迟函数运行在中断上下文中(软中断的一个检查点就是do_IRQ退出的时候),于是导致了一些问题:软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。

    想知道更多?描下面的二维码关注我

    后台回复"技术",加入技术群

    后台回复“k8s”,可领取k8s资料

    【精彩推荐】

    更多相关内容
  • 对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。 切换的性能消耗: 1、线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存...
  • 我们知道,多线程是Android开发中必现的场景,很多原生API和开源项目都有多线程的内容,这里简单总结和探讨一下常见的多线程切换方式。 我们先回顾一下Java多线程的几个基础内容,然后再分析总结一些经典代码中对于...
  • 主要介绍了RxJava的消息发送和线程切换实现原理,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 曾经使用过AndroidAnnotations,对里面的线程切换方式很是喜欢,使用非常方便,可是由于各种原因,已经不再想使用AndroidAnnotations了, 但又舍不得这种便捷的方式,于是就实现了自己的线程切换
  • MagicThread - 安卓端、纯注解使用的线程切换框架
  • 线程切换工具类

    2018-02-26 15:56:16
    方便简单的线程切换工具类,调用方便简洁,安卓开发时主线程子线程切换非常方便
  • 文章目录主动切换(调用API)KiSwapContext函数分析哪些API调用了SwapContext函数总结时钟中断切换如何中断一个正在执行的程序系统时钟时钟中断的...之前我们已经学习了模拟Windows线程切换的代码,里面用于线程切换的...

    主动切换(调用API)

    之前我们已经学习了模拟Windows线程切换的代码,里面用于线程切换的函数就是SwitchContext。只要调用这个函数就会导致线程切换,Windows也有类似的函数:KiSwapContext

    KiSwapContext函数分析

    在这里插入图片描述

    用IDA打开ntkrlpa.exe,找到KiSwapContext函数

    .text:004699B4                 sub     esp, 10h
    .text:004699B7                 mov     [esp+10h+var_4], ebx ; ------------------------
    .text:004699BB                 mov     [esp+10h+var_8], esi
    .text:004699BF                 mov     [esp+10h+var_C], edi ; 保存当前线程的寄存器现场
    .text:004699C3                 mov     [esp+10h+var_10], ebp ; ---------------------------
    

    首先,KiSwapContext保存当前线程的寄存器现场

    .text:004699C6                 mov     ebx, ds:0FFDFF01Ch ; 取出KPCR存到ebx
    

    接着取出KPCR存到ebx

    .text:004699CC                 mov     esi, ecx        ; ecx是上一层调用的函数传进来的 是要切换线程的KTHREAD
    

    这个ecx来自上一层函数的传参

    在这里插入图片描述

    Ctrl+X找到上一层调用

    在这里插入图片描述

    ecx来自于eax,而eax是KiFindReadyThread函数的返回值,该函数会返回一个就绪线程的KTHREAD

    .text:004699C6                 mov     ebx, ds:0FFDFF01Ch ; 取出KPCR存到ebx
    .text:004699CC                 mov     esi, ecx        ; 要切换线程的KTHREAD
    .text:004699CE                 mov     edi, [ebx+124h] ; KPCR+0x124是当前线程的KTHREAD结构体
    

    回到KiSwapContext函数,此时esi存储的是要切换线程的KTHREAD,edi是当前线程的KTHREAD。

    .text:004699D4                 mov     [ebx+124h], esi ; 修改KPCR里的当前线程为目标线程
    

    接着修改KPCR里的当前线程为目标线程

    .text:004699DA                 mov     cl, [edi+58h]
    .text:004699DD                 call    SwapContext     ; 进行线程切换
    

    接着调用SwapContext函数进行线程切换,跟进SwapContext函数。这个函数的代码比较复杂,先来看几个关键代码

    在这里插入图片描述

    这行代码将当前的ESP存储到KernelStack里,继续往下找到另外一行关键代码

    在这里插入图片描述

    这行代码将目标线程的KernelStack存到ESP里。真正的线程切换从这里开始,从这行代码往后已经不再是当前线程了,而是目标线程的堆栈。

    哪些API调用了SwapContext函数

    现在我们知道了只要调用了SwapContext就会导致线程切换,那么现在我们可以看一下到底有多少个API调用了这个函数

    在这里插入图片描述

    先找到KiSwapContext的函数KiSwapThread

    在这里插入图片描述

    打开交叉引用,可以看到有7个函数都调用了KiSwapThread。那就意味着只要我们调用了这里面的任何一个函数都会导致线程切换。

    再来查看一下其中一个父函数KeWaitForSingleObject,看看这个函数被多少个函数调用

    在这里插入图片描述

    总共270个函数调用了父函数KeWaitForSingleObject,还有6个父函数我们没有查看。这270个函数如果再被其他函数调用也会导致线程切换

    这样我们可以得出一个结论,绝大多数的内核函数都会调用SwapContext,导致线程切换

    总结

    1. Windows中绝大部分的API都会调用SwapContext函数,也就是说,当线程调用了API就会导致线程切换

    时钟中断切换

    那么如果当前的线程不去调用系统API,操作系统是不是就无法实现线程切换了呢?实际上并不是这样?我们先要来分析一下如何中断一个正在执行的程序

    如何中断一个正在执行的程序

    1. 异常 比如缺页或者INT N指令
    2. 中断 比如时钟中断

    系统时钟

    IDT表中断号IRQ说明
    0x30IRQ0时钟中断

    Windows系列的操作系统每隔10-20毫秒会触发一次时钟中断。如果要获取当前系统的时钟中断间隔,可使用W32 API:GetSystemTimeAdjustment

    时钟中断的执行流程

    接下来分析时钟中断的执行流程
    在这里插入图片描述

    用IDA打开ntkrnlpa.exe,搜索_IDT

    在这里插入图片描述

    找到中断号为0x30的中断处理函数,只要分析这个函数,就能知道系统的执行流程

    在这里插入图片描述

    这里调用了一个非当前模块的函数

    在这里插入图片描述

    在导入表中我们可以看到这个函数来自于HAL

    在这里插入图片描述

    然后又调用了一个HAL模块中的函数。

    在这里插入图片描述

    继续跟进,用IDA打开hal.dll,找到HalBeginSystemInterrupt函数。这个函数并没有回到ntkrnlpa.exe

    在这里插入图片描述

    再往下找到HalEnableSystemInterrupt函数

    在这里插入图片描述

    这个函数在内部又调用了KiDispatchInterrupt

    在这里插入图片描述

    接着搜索导入表,可以看到这个函数来自于ntoskrl内核文件

    在这里插入图片描述

    继续在ntoskrl跟进KiDispatchInterrupt函数

    在这里插入图片描述

    这个函数里面也调用了SwapContext。到这里,大致的流程也就分析完成了。这说明当时钟中断发生的时候,也会触发线程切换

    时钟中断的执行流程:

    1. KiStartUnexpectedRange
    2. KiEndUnexpectedRange
    3. KiUnexpectedInterruptTail
    4. HalEndSystemInterrupt
    5. KiDispatchInterrupt
    6. SwapContext

    总结

    线程切换的几种情况

    1. 主动调用API函数
    2. 时钟中断
    3. 异常处理

    如果一个线程不调用API,在代码中屏蔽中断,并且不会出现异常,那么当前线程将永久占有CPU。单核占有率100%,2核就是50%

    时间片管理

    我们已经知道时钟中断会导致线程进行切换,但并不是说只要有时钟中断就一定会切换线程,时钟中断时,两种情况会导致线程切换

    1. 当前线程的时间片到期
    2. 存在备用线程(KPCR.PrcbData.NextThread)

    下面分别解释这两种情况

    1.时间片到期

    什么是时间片?

    当一个新的线程开始执行的时候,初始化程序会在_KTHREAD.Quantum赋初始值,该值的大小由_KPROCESS.ThreadQuantum决定

    我们在winbdg中随便查看一个进程结构体

    kd> dt _KPROCESS 889e0288
    ntdll!_KPROCESS
       +0x000 Header           : _DISPATCHER_HEADER
       +0x010 ProfileListHead  : _LIST_ENTRY [ 0x889e0298 - 0x889e0298 ]
       +0x018 DirectoryTableBase : 0x7ea1d520
       +0x01c LdtDescriptor    : _KGDTENTRY
       +0x024 Int21Descriptor  : _KIDTENTRY
       +0x02c ThreadListHead   : _LIST_ENTRY [ 0x889e1960 - 0x88846e58 ]
       +0x034 ProcessLock      : 0
       +0x038 Affinity         : _KAFFINITY_EX
       +0x044 ReadyListHead    : _LIST_ENTRY [ 0x889e02cc - 0x889e02cc ]
       +0x04c SwapListEntry    : _SINGLE_LIST_ENTRY
       +0x050 ActiveProcessors : _KAFFINITY_EX
       +0x05c AutoAlignment    : 0y0
       +0x05c DisableBoost     : 0y0
       +0x05c DisableQuantum   : 0y0
       +0x05c ActiveGroupsMask : 0y1
       +0x05c ReservedFlags    : 0y0000000000000000000000000000 (0)
       +0x05c ProcessFlags     : 0n8
       +0x060 BasePriority     : 8 ''
       +0x061 QuantumReset     : 6 ''
    

    其中0x061这个位置的QuantumReset值为6。这就意味着当进程里面的线程开始执行的时候,初始化的程序就会将QuantumReset的值拿出来存到当前线程结构体的Quantum里。这个值就是当前线程时间片的大小

    时间片什么时候发生改变?

    每次时钟中断会调用KeUpdateRunTime函数,该函数每次将当前线程Quantum减少3个单位,如果减到0,则将KPCR.PrcbData.QuantumEnd的值设置为非0

    在这里插入图片描述

    在IDA中找到KeUpdateRunTime函数

    在这里插入图片描述

    每一次时钟中断,都会把当前线程的CPU时间片减少3,

    接着会判断这个值是否为0,如果为零,就会把QuantumEnd的值设置为非0,这个值是一个标志,标志着当前CPU的时间片有没有用完。

    没有用完的时候这个值为0,如果用完了,会存储一个非0的值。

    CPU时间片到期了如何处理?

    KiDispatchInterrupt会判断时间片是否到期。

    在这里插入图片描述

    这个函数是每一次系统时钟中断最后要执行的函数

    在这里插入图片描述

    这行代码会判断当前的CPU时间片是否到期,当系统时间片到期后会发生跳转

    在这里插入图片描述

    如果时间片到期会将QuantumEnd修改为0,然后调用KiQuantumEnd函数,跟进这个函数

    在这里插入图片描述

    这个函数主要做的事情就是将CPU的时间片重新设置为ThreadQuantum,也就是最开始看的6

    在这里插入图片描述

    设置完成之后会调用KiFindReadyThread,通过这个函数找到下一个要运行的线程。找到以后函数返回。

    在这里插入图片描述

    也就是说KiQuantumEnd函数的作用是重设CPU时间片 找到下一个要运行的线程,接着跳转

    在这里插入图片描述

    跳转以后,先调用KiReadyThread将当前线程挂到就绪链表里,然后调用SwapContext切换线程

    CPU时间片总结

    1. 当一个新的线程开始执行时,初始化程序会在_KTHREAD.Quantum赋初始值,该值的大小由_KPROCESS.ThreadQuantum决定(观察ThreadQuantum大小)
    2. ​ 每次时钟中断会调用KeUpdateRunTime函数,该函数每次将当前线程Quantum减少3个单位,如果减到0,则将KPCR.PrcbData.QuantumEnd的值设置为非0
    3. KiDispatchInterrupt判断时间片到期后,调用KiQuantumEnd(重新设置时间片、找到要运行的线程)

    2.存在备用线程

    在这里插入图片描述

    接着回到KiDispatchInterrupt函数,这里首先会判断CPU时间片是否到期,接着判断备用线程是否为0,如果在不为0有备用线程的前提下,继续往下执行

    在这里插入图片描述

    同样会调用SwapContext函数进行线程切换

    在这里插入图片描述

    如果以上两个条件都不满足,代码会进行跳转,函数直接retn返回,此时不会发生线程切换

    总结

    线程切换的三种情况

    1. 当前线程主动调用API:API函数–>KiSwapThread–>KiSwapContext–>SwapContext
    2. 当前线程的时间片到期:KiDispatchInterrrupt–>KiQuantumEnd–>KiSwapContext–>SwapContext
    3. 有备用线程:KiDispatchInterrrupt–>SwapContext
    4. 如果时钟中断的时候时间片没有到期且没有备用线程,那么函数会直接返回,不会发生线程切换
    展开全文
  • 总结 缓存带来的可见性问题、线程切换带来的原子性问题和编译优化带来的有序性问题,是导致并发编程频繁出现诡异问题的三个源头,我们已经介绍了缓存带来的可见性问题和线程切换带来的原子性问题。下一篇中,我们...
    摘要:原子性是指一个或者多个操作在CPU中执行的过程不被中断的特性。原子性操作一旦开始运行,就会一直到运行结束为止,中间不会有中断的情况发生。

    本文分享自华为云社区《【高并发】解密导致并发问题的第二个幕后黑手——原子性问题》,作者: 冰 河。

    原子性

    原子性是指一个或者多个操作在CPU中执行的过程不被中断的特性。原子性操作一旦开始运行,就会一直到运行结束为止,中间不会有中断的情况发生。

    我们也可以这样理解原子性,就是线程在执行一系列操作时,这些操作会被当做一个不可拆分的整体执行,这些操作要么全部执行,要么全部不执行,不会存在只执行一部分的情况,这就是原子性操作。

    关于原子性操作一个典型的场景就是转账,例如,小明和小刚的账户余额都是200元,此时小明给小刚转账100元,如果转账成功,则小明的账户余额为100元,小刚的账户余额为300元;如果转账失败,则小明和小刚的账户余额仍然为200元。不会存在小明账户为100元,小刚账户为200元,或者小明账户为200元,小刚账户为300元的情况。

    这里,小明给小刚转账100元的操作,就是一个原子性操作,它涉及小明账户余额减少100元,小刚账户余额增加100元的操作,这两个操作是一个不可分割的整体,要么全部执行,要么全部不执行。

    小明给小刚转账成功,则如下所示。

    小明给小刚转账失败,则如下所示。

    不会出现小明账户为100元,小刚账户为200元的情况。

    也不会出现小明账户为200元,小刚账户为300元的情况。

    线程切换

    在并发编程中,往往设置的线程数目会大于CPU数目,而每个CPU在同一时刻只能被一个线程使用。而CPU资源的分配采用了时间片轮转策略,也就是给每个线程分配一个时间片,线程在这个时间片内占用CPU的资源来执行任务。当占用CPU资源的线程执行完任务后,会让出CPU的资源供其他线程运行,这就是任务切换,也叫做线程切换或者线程的上下文切换。

    如果大家还是不太理解的话,我们可以用下面的图来模拟线程在CPU中的切换过程。

    在图中存在线程A和线程B两个线程,其中线程A和线程B中的每个小方块代表此时线程占有CPU资源并执行任务,这个小方块占有的时间,被称为时间片,在这个时间片中,占有CPU资源的线程会在CPU上执行,未占有CPU资源的线程则不会在CPU上执行。而每个虚线部分就代表了此时的线程不占用CPU资源。CPU会在线程A和线程B之间频繁切换。

    原子性问题

    理解了什么是原子性,再看什么是原子性问题就比较简单了。

    原子性问题是指一个或者多个操作在CPU中执行的过程中出现了被中断的情况。

    线程在执行某项操作时,此时如果CPU发生了线程切换,CPU转而去执行其他的任务,中断了当前线程执行的操作,这就会造成原子性问题。

    如果你还不能理解的话,我们来举一个例子:假设你在银行排队办理业务,小明在你前面,柜台的业务员为小明办理完业务,正好排到你时,此时银行下班了,柜台的业务员微笑着告诉你:实在不好意思,先生(女士),我们下班了,您明天再来吧!此时的你就好比是正好占有了CPU资源的线程,而柜台的业务员就是那颗发生了线程切换的CPU,她将线程切换到了下班这个线程,执行下班的操作去了。

    Java中的原子性问题

    在Java中,并发程序是基于多线程技术来编写的,这也会涉及到CPU的对于线程的切换问题,正是CPU中对任务的切换机制,导致了并发编程会出现原子性的诡异问题,而原子性问题,也成为了导致并发问题的第二个“幕后黑手”。

    在并发编程中,往往Java语言中一条简单的语句,会对应着CPU中的多条指令,假设我们编写的ThreadTest类的代码如下所示。

    package io.mykit.concurrent.lab01;
    
    /**
     * @author binghe
     * @version 1.0.0
     * @description 测试原子性
     */
    public class ThreadTest {
    
        private Long count;
    
        public Long getCount(){
            return count;
        }
    
        public void incrementCount(){
            count++;
        }
    }

    接下来,我们打开ThreadTest类的class文件所在的目录,在cmd命令行输入如下命令。

    javap -c ThreadTest

    得出如下的结果信息,如下所示。

    d:>javap -c ThreadTest
    Compiled from "ThreadTest.java"
    public class io.mykit.concurrent.lab01.ThreadTest {
      public io.mykit.concurrent.lab01.ThreadTest();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public java.lang.Long getCount();
        Code:
           0: aload_0
           1: getfield      #2                  // Field count:Ljava/lang/Long;
           4: areturn
    
      public void incrementCount();
        Code:
           0: aload_0
           1: getfield      #2                  // Field count:Ljava/lang/Long;
           4: astore_1
           5: aload_0
           6: aload_0
           7: getfield      #2                  // Field count:Ljava/lang/Long;
          10: invokevirtual #3                  // Method java/lang/Long.longValue:()J
          13: lconst_1
          14: ladd
          15: invokestatic  #4                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
          18: dup_x1
          19: putfield      #2                  // Field count:Ljava/lang/Long;
          22: astore_2
          23: aload_1
          24: pop
          25: return
    }

    这里,我们主要关注下incrementCount()方法对应的CPU指令,如下所示。

    public void incrementCount();
        Code:
           0: aload_0
           1: getfield      #2                  // Field count:Ljava/lang/Long;
           4: astore_1
           5: aload_0
           6: aload_0
           7: getfield      #2                  // Field count:Ljava/lang/Long;
          10: invokevirtual #3                  // Method java/lang/Long.longValue:()J
          13: lconst_1
          14: ladd
          15: invokestatic  #4                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
          18: dup_x1
          19: putfield      #2                  // Field count:Ljava/lang/Long;
          22: astore_2
          23: aload_1
          24: pop
          25: return

    可以看到,Java语言中短短的几行incrementCount()方法竟然对应着那么多的CPU指令。这些CPU指令我们大致可以分成三步。

    • 指令1:把变量count从内存加载的CPU寄存器。
    • 指令2:在寄存器中执行count++操作。
    • 指令3:将结果写入缓存(可能是CPU缓存,也可能是内存)。

    在操作系统执行线程切换时,可能发生在任何一条CPU指令完成后,而不是程序中的某条语句完成后。如果线程A执行完指令1后,操作系统发生了线程切换,当两个线程都执行count++操作后,得到的结果是1而不是2。这里,我们可以使用下图来表示这个过程。

    由上图,我们可以看出:线程A将count=0加载到CPU的寄存器后,发生了线程切换。此时内存中的count值仍然为0,线程B将count=0加载到寄存器,执行count++操作,并将count=1写到内存。此时,CPU切换到线程A,执行线程A中的count++操作后,线程A中的count值为1,线程A将count=1写入内存,此时内存中的count值最终为1。

    所以,如果在CPU中存在正在执行的线程,恰好此时CPU发生了线程切换,则可能会导致原子性问题,这也是导致并发编程频繁出问题的根源之一。我们只有充分理解并掌握线程的原子性以及引起原子性问题的根源,并在日常工作中时刻注意编写的并发程序是否存在原子性问题,才能更好的编写出并发程序。

    总结

    缓存带来的可见性问题、线程切换带来的原子性问题和编译优化带来的有序性问题,是导致并发编程频繁出现诡异问题的三个源头,我们已经介绍了缓存带来的可见性问题和线程切换带来的原子性问题。下一篇中,我们继续深耕高并发中的有序性问题。

    点击关注,第一时间了解华为云新鲜技术~

    展开全文
  • Windows7 x86 线程切换KiSwapThread / KiSwapContext / SwapContext 逆向分析.

    目录

    前言

    调用API触发线程切换机制

    KiSwapThread

    参数分析

    函数功能

    KiSwapContext

    SwapContext


    前言

    • 通过研究KiSwapThread函数,就可以知道单核CPU是如何同时运行多个进程.
    • 需要了解进程内核结构KPROCESS,其成员CR3,ApcState等等.
    • 需要了解线程内核结构KTHREAD,会用到很多线程内核结构成员.并且对WindowsAPI调用有了解(系统调用),需要了解一个API是如何进R0,以及如何从R3堆栈切换到R0堆栈等.
    • 需要了解处理器控制区KPCR,线程切换会通过其成员ReadySummary / DisPatcherReadyListHead来查找当前核就绪线程等.
    • 建议阅读此文前先查看模拟Windows线程切换,Windows查找就绪线程.

    调用API触发线程切换机制

    IDA查看函数KiSwapThread.

    通过交叉引用查看哪些函数调用了线程切换函数.

    通过交叉引用查看哪些函数调用了KiCommitThreadWait

    通过KeWaitForSingleObject函数交叉引用可以看出,大部分API调用时最终会触发线程切换.

    分析可以得出结论:Windows中绝大部分API最终都会调用了KiSwapThread(SwapContext后文详解)函数,也就意味着调用API相当于主动切换线程.

    KiSwapThread

    参数分析

    通过交叉引用查看调用函数KiCommitThreadWait传递了哪些参数.

    使用ECX,DEX传参可以得出调用约定为FASTCALL.

    参数EDX来源于EBX,EBX函数上方赋值后指向_KPRCB

    参数ECX来源于EDI,EDI为当前函数参数1(KTHREAD)

    参考WRK定义

    LONG_PTR
    FASTCALL
    KiSwapThread (
        IN PKTHREAD OldThread, //当前线程
        IN PKPRCB CurrentPrcb //当前核KPRCB
        );

    函数功能

    线程切换KiSwapThread函数在不同版本系统下实现都有略微差异.只研究核心代码.

    发现其实现中调用了KiSearchForNewThread函数.

    分析得知函数首先直接获取_KPRCB结构中NextThread,如果此值不为空,将NextThread设置为0,并且把取出线程设置为KPRCB.CurrentThread,线程KTHREAD.State设置为2后直接返回该线程结构.

    如果_KPRCB结构中NextThread为空,通过KiSelectReadyThread函数继续查找,该函数实现为直接查找指定优先级的线程,如果找到线程设置对应状态后返回.

    如果KiSelectReadyThread函数也没找到,会执行KiFindReadyThread函数,通过KPRCB -> ReadySummary就绪位图循环查找符合执行条件的线程设置对应状态后返回.

    KiSearchForNewThread函数首先判断是否找到就绪线程,找到跳转(这里按照成功路线分析).

    取出线程不是IdleThread且不为当前线程,同时处于未运行状态会跳转.

    线程查询校验全部完毕后最终跳转此处(EDX为要切换线程,ECX为当前线程),执行函数KiSwapContext.

    KiSwapContext

    函数并未做任何功能实现,而是初始化参数后调用SwapContext函数

    SwapContext

    函数入口会循环(自旋)判断要执行线程运行状态,直到未运行时跳出.

    线程状态检测跳转后,会进行时间与浮点相关初始化,并且会将当前线程异常处理链表保存到堆栈中.

    初始化工作结束后,开始切换堆栈(相当于切换线程(模拟线程中有详细介绍)),并且判断当前线程所属进程来决定是否需要切换CR3.至此实际上已经完成线程切换.

    两个线程对应堆栈图如下:

    系统调用分析中说过TSS.ESP0永远存储当前线程_KTRAP_FRAME,论证如下

    内核堆栈结构以及通过_KTHREAD -> 堆栈相关回溯ESP0,获得_TRAP_FRAME:

    段寄存器相关文章中提起,x86中未使用GS段寄存器,线程切换就会清空GS值,论证如下:

    在介绍段寄存器说FS寄存器在R3时指向当前线程TEB,论证如下:

    线程主动切换分析完毕,按照大致流程以及对段,TSS,_KTRAP_FRAME补充.这里再次说明,参考WRK, Windows内核原理与实现, Windows内核情景分析时不要完全按照它们的内容进行比较,对自己内核文件进行分析最为准确.同时要搞懂全部实现比较难,按照主线路分析即可.

    当前内核文件MSDN搜索cn_windows_7_professional_with_sp1_x86_dvd_u_677162.iso

    展开全文
  • Windows线程切换之时钟切换分析(被动切换).
  • 为什么线程切换开销大

    千次阅读 2020-12-22 17:01:49
    线程切换的开销 我们都知道,线程切换会带来开销,如果频繁进行线程切换,所造成的开销是相当可观的。那么为什么线程切换会有开销呢,有哪些开销呢?这里涉及几个概念:CPU上下文切换、线程上下文切换、特权模式...
  • 进程/线程切换究竟需要多少开销?

    千次阅读 2020-11-05 09:00:00
    进程是我们开发同学非常熟悉的概念,我们可能也听说过进程上下文切换开销。那么今天让我们来思考一个问题,究竟一次进程上下文切换会吃掉多少CPU时间呢?线程据说比进程轻量,它的上下文切换会比进...
  • 进程切换与线程切换

    千次阅读 2020-09-07 21:44:56
    对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。 切换的性能消耗: 线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存...
  • 线程切换比进程快

    千次阅读 2020-11-30 17:53:57
    我们都知道线程切换的开销比进程切换的开销小,那么小在什么地方?切换的过程是怎样的? 无论是在多核还是单核系统中,一个CPU看上去都像是在并发的执行多个进程,这是通过处理器在进程间切换来实现的。 操作系统...
  • 前言 突发奇想想搞一个同步切换线程的Kotlin协程,而不用各种withContext(){},可以减少嵌套且逻辑更清晰,想实现的结果如下图: 分析 实现我们想要的结果,...其实控制线程切换是协程库内内置的一个拦截器类:Continu
  • 线程切换的开销

    千次阅读 2020-09-15 23:04:57
    从单线程应用到多线程应用带来的不仅仅是好处。也会带来开销。不要仅仅在一个应用中使用多线程仅仅是因为你能够(会)使用多线程。你应该能确定使用多线程带来的好处要远远多于它带来...线程切换开销 当一个cpu从一个线
  • 进程切换和线程切换

    千次阅读 2019-08-11 09:52:12
    这种行为被称为进程切换(process switch)、任务切换(task switch)或上下文切换(content switch)。 原文 :https://www.cnblogs.com/kkshaq/p/4547725.html 进程切换分两步: 1.切换页目录以使用新的地址...
  • Android中多线程切换的几种方法

    千次阅读 2019-05-25 19:30:36
    我们知道,多线程是Android开发中必现的场景,很多原生API和开源项目都有多线程的内容,这里简单总结和探讨一下常见的多线程切换方式。 我们先回顾一下Java多线程的几个基础内容,然后再分析总结一些经典代码中对于...
  • Rxjava线程切换原理

    千次阅读 2018-12-15 09:57:23
    昨天朋友面试被问Rxjava线程切换原理,当时有点蒙圈了,这里我写一篇博客讲一下。 PS:建议您对 RxJava 有一些了解或使用经验再看此文章,推荐结合源码品尝RxJava入门文章【Rxjava详解】  [给 Android 开发者的 ...
  • 进程切换与线程切换的区别?

    万次阅读 多人点赞 2019-07-25 17:00:04
    注意这个题目问的是进程切换与线程切换的区别,不是进程与线程的区别。当然这里的线程指的是同一个进程中的线程。 这个问题能很好的考察面试者对进程和线程的理解深度,有比较高的区分度。 要想正确回答这个问题,...
  • Handler到底是如何完成线程切换的?

    千次阅读 2019-05-09 18:01:48
    Handler到底是如何完成线程切换的?这个问题要从Handler最初的用法和原理讲起。 首先我们列出正常情况下一个Handler使用的步骤然后讲解分析如何实现,这里不对一些基础的概念做解释,具体的请查阅源码。 Handler的...
  • 线程切换的几个核心问题 切换线程切换了什么 什么设备管理了切线程的切换 CPU上下文 内核态,用户态 如何量化线程切换引起的开销 总的切换流程
  • 前言 为什么用多线程或多进程? 程序的大部分耗时在等待IO上,瓶颈不在CPU上时,可以提高CPU利用率 需要集中快速处理大量数据,并且不受先后顺序影响 评论区还可补充ing ...不需要用户态/内核态切换,速...
  • 26.模拟Windows线程切换

    千次阅读 2021-11-19 14:23:41
    滴水模拟Windows线程切换代码讲解,感谢唐老师,海哥,火哥.这份代码对研究Windows真正线程切换帮助很大.
  • RxJava 线程切换流程

    千次阅读 2020-06-08 18:56:56
    RxJava 线程切换流程 在客户端开发中有些时候需要多线程切换 作为Android开发者中最经典的模型是Handler+Looper+Message,这种写法稍有不慎会有内存泄漏的风险,后出了RxJava+RxAndroid 因为使用Rxjava切换线程比较...
  • Rxjava2 线程切换

    千次阅读 2018-05-28 10:14:30
    Rxjava2的线程切换使用subscribeOn、observeOn实现。 subscribeOn subscribeOn用于指定subscribe时,所处的线程,只可指定一次。 Flowable.create(new FlowableOnSubscribe&lt;String&gt;() { @...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 446,545
精华内容 178,618
关键字:

线程切换

友情链接: 9.rar