avatar

Catalog
多核同步之临界区

本篇来说一下多核同步相关联的知识,原本这是位于滴水教程中驱动部分的内容,但实际上更偏向于同步的知识点,同步相关的内容会在APC更新完后再作更新。这里也算是简单的作个预习。

并发与同步

并发是指多个线程同时执行:

  • 单核(是分时执行,不是真正的同时)
  • 多核(在某一个时刻,会同时有多个线程在执行)

同步则是保证在并发执行的环境中各个线程可以有序的执行

单行指令的同步

单行代码安全吗

有了并发与同步的概念,下面我们来看一个代码:

c
1
2
3
4
5
DWORD dwVal = 0;	//全局变量
....
....
//某个线程中
dwVal++; //这行代码安全吗?

dwVal是一个全局变量,在某个线程中,有一个对dwVal的值进行自增的代码。简要思考一下,这行代码安全吗?

实际上是不安全的,我们来看一下这行指令的反汇编:

Code
1
2
3
mov eax, [0x12345678]
add eax, 1
mov [0x12345678], eax

dwVal++这一条自增语句,对应了3条汇编指令,也就是说程序在执行dwVal++时需要按照顺序执行3条汇编指令,才能实现这条语句的功能。

现在我们来考虑一种情况,线程A和线程B均要进行dwVal++这条指令,理想状态下,这两个线程执行完后,该全局变量的值会增加2。但是如果在线程A执行完第二条指令后发生了线程切换,情况就会变的不一样了。参考如下代码:

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
//线程A
mov eax, [0x12345678]
add eax, 1

//发生线程切换
//线程B
mov eax, [0x12345678]
add eax, 1
mov [0x12345678], eax

//发生线程切换
//线程A
mov [0x12345678], eax

根据推演出的情况,在线程A执行完第二条指令后发生了线程切换,切换到了线程B,这样CPU执行的过程就会如上述代码所示,当两个线程执行完后,全局变量的值dwVal只会增加1,并不会增加2,结果和预期的并不一样。所以用单行代码对一个全局变量进行修改,是不安全的。

单行指令安全吗

既然单行代码不安全,那单行指令安全吗?参考如下汇编指令:

Code
1
inc dword ptr ds:[0x12345678]	//一行汇编指令,安全吗?

这条汇编指令仅有一行,看上去不会出现上述的情况。即使线程发生切换了,也不会造成指令的重复执行。的确,在单核的情况下,这条指令是安全的,但是多核的情况下,还是有可能发生,不同线程同时执行这条指令的情况,所以这条指令并不安全。那如何才能在多核的情况下,依旧保证线程间的同步呢?那就需要用到下面介绍的这条指令了。

LOCK指令

我们只需要增加一个lock指令,就能让单条指令在多核的情况下做到同步,参考如下代码:

Code
1
lock inc dword ptr ds:[0x12345678]

lock指令有什么用呢?lock指令可以锁定当前指令执行时线程所访问的内存,上述代码执行时,会对0x12345678地址处的值进行修改,有了lock指令的限制,此时其它线程不能访问或修改0x12345678地址处的值的,只有在这条指令执行完后,其余线程才可以对此地址的值进行访问或者修改。这样就避免了多核情况下,不同线程同时修改此地址处的值的情况。

像上面这样通过Lock指令进行限制,从而不可被中断的操作叫做原子操作。Windows提供了一部分API(主要位于Kernel32.dll和Ntdll.dll)供用户使用从而保证在多核情况下的线程同步。

c
1
2
3
4
5
//原子操作相关的API:
InterlockedIncrement InterlockedExchangeAdd
InterlockedDecrement InterlockedFlushSList
InterlockedExchange InterlockedPopEntrySList
InterlockedCompareExchange InterlockedPushEntrySList

InterlockedIncrement

下面我们分析Windows提供的原子函数InterlockedIncrement,来理解一下这种同步的实现的本质:

IDA打开Kernel32.dll,搜索InterlockedIncrement便可找到,这是一个导出函数。

Code
1
2
3
4
5
6
7
_asm {
mov ecx, [esp+lpAddend]
mov eax, 1
lock xadd [ecx], eax
inc eax
retn 4
}

这个代码非常简单,一共就5行,我们逐行分析:

  1. 将需要修改的变量(往往是个全局变量,它的地址会作为参数传入InterlockedIncrement函数)的地址赋给ecx,此时可以通过[ecx]访问该变量的值

  2. 给eax赋值1

  3. 这行指令分为两部分看,第一部分是lock,我们已经知道,它用来锁住内存;另一部分核心在于xadd指令,该指令接收2个参数,先交换两个操作数的值,再进行算术加法操作。可以按下述伪代码理解:

    c
    1
    2
    3
    4
    5
    DWORD temp;
    temp = [ecx];
    [ecx] = eax;
    eax = temp;
    [ecx] += eax;
  4. 这一步对eax自增1,此时eax和[ecx]的值相同,eax可以起到返回值的作用(尽管已经完成对[ecx]值的修改)

  5. 平衡堆栈并返回

分析完后就很好理解InterlockedIncrement函数了,本质上就是用lock指令完成一个原子操作以保证多核状态下的线程同步。

小节

这下,我们对于单行指令的同步已经理解到位了,只需在指令前加上lock,就可以保证这条指令执行期间,不会有其它线程访问当前指令访问的内存。下面我们来学习一下多行指令的同步概念。

多行指令的同步

多行指令原子操作

如果有多行指令要求原子操作,单独加lock可行吗?观察如下代码:

Code
1
2
3
4
5
_asm {
lock inc dword ptr ds:[0x12345678]
lock inc dword ptr ds:[0x23456789]
lock inc dword ptr ds:[0x3456789A]
}

答案是不可行,lock指令锁住的是内存,线程该切换还是切换,例如线程A在对0x12345678地址上的值进行修改时,发生了线程切换,尽管线程B此时并不能对0x12345678上的值进行修改,但是可以对0x3456789A地址上的值进行修改,如果这时又发生线程切换,切了回去,线程B甚至可以把线程A锁住,不让线程A去修改0x3456789A地址处的值。这样还是没有办法保证,对于这3行指令进行原子操作。这时,就需要引入一个新的概念,临界区

临界区

可以设置一个临界区,一次只允许一个线程进入直到离开,从而保证线程的同步(这里的临界区指的是广义的临界区,各个操作系统根据临界区的思想都有各自的实现,后面也会学习到Windows提供的临界区实现)参考如下代码:

c
1
2
3
4
5
6
7
8
9
10
11
12
DWORD dwFlag = 0;		//实现临界区的方式就是加锁
//锁:全局变量 进去加一 出去减一

if(dwFlag == 0) //进入临界区
{
dwFlag = 1
.......
.......
.......

dwFlag = 0 //离开临界区
}

该代码使用一个全局变量定义了一个锁,这是一个用来进入临界区的锁,当锁为0时,即可进入临界区,进入临界区去,将锁的值修改为1,这样别的线程就无法再进入临界区了。此时临界区中执行的代码可以看作原子操作。理论上可以解决多行指令的多核同步问题

不过我们再仔细观察一遍这个代码,其实是有问题的,考虑一种情况,在进入临界区后,如果dwFlag = 1这条指令还没有执行,此时发生了线程切换,这时,切换后的新线程也可以进入临界区,这样临界区的作用就失效了。

所以,我们可以换一种方式,先修改锁的值,然后再进入临界区,参考如下伪代码:

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//定义全局变量
Flag = 0;

//进入临界区
Lab:
mov eax,1
//多核情况下必须加lock
lock xadd [Flag],eax
cmp eax,0
jz endLab
dec [Flag]
//线程等待Sleep..
jmp Lab

//临界区内部
endLab:
ret

//离开临界区
lock dec [Flag]

这份伪代码提供了一个新的思路,在进入临界区之前,先判断锁的值,若达到进入临界区的条件,则先修改锁的值再进入临界区若条件不符合,则调用sleep()函数,进行线程等待。一段时间后,在跳回进入临界区的地方重新判断。在线程退出临界区后,通过原子操作还原锁的值

小结:临界区可以通过添加全局变量的方式实现。

总结

  1. lock指令可以实现单条指令的多核同步
  2. 临界区可以实现多行指令的多核同步

参考链接

参考教程:https://www.bilibili.com/video/av68700135?p=64

参考文章:https://blog.csdn.net/qq_41988448/article/details/103585673

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/05/07/%E5%A4%9A%E6%A0%B8%E5%90%8C%E6%AD%A5%E4%B9%8B%E4%B8%B4%E7%95%8C%E5%8C%BA/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶