您的位置:首页 > 其它

x86平台原子操作API的实现原理

2015-03-13 09:30 281 查看
原子操作的意义
对于软件来说,代码的行为必须是确定的,就是说通过手工分析代码也能预知运行结果。但是程序在并发和并行时,因为操作系统任务调度的不确定性和多处理器之间的相互影响,导致代码运行的结果无法预知。这种情况下,只有强制保证某些指令的执行是原子操作,代码运行结果才是可预知的,原子操作也是多任务操作系统设计的基石。正确的使用原子操作,可以避免多任务导致的脏数据,保证代码行为的正确。下面通过一段代码说明为什么程序在并发和并行时需要原子操作。

#include <stdio.h>
#include <assert.h>
#include <process.h>
#include <Windows.h>

int g_count1;
volatile long g_count2;
HANDLE h_event;
unsigned int CALLBACK func_callback(void *context);

int main(const int argc, const char *argv[])
{
unsigned long t_id;
HANDLE h[2];

h_event = CreateEvent(NULL, TRUE, FALSE, NULL);
h[0] = (HANDLE)_beginthreadex(NULL, 0, func_callback, NULL, 0, &t_id);
assert(h[0] != INVALID_HANDLE_VALUE);
h[1] = (HANDLE)_beginthreadex(NULL, 0, func_callback, NULL, 0, &t_id);
assert(h[1] != INVALID_HANDLE_VALUE);

g_count1 = g_count2 = 0;
SetEvent(h_event);
WaitForMultipleObjects(2, h, TRUE, INFINITE);
CloseHandle(h_event);
printf("g_count1=%d g_count2=%ld\n", g_count1, g_count2);
getchar();

return 0;
}

unsigned int CALLBACK func_callback(void *context)
{
int count;

count = 10000;
WaitForSingleObject(h_event, INFINITE);
while (count-- > 0)
{
g_count1++;
InterlockedIncrement(&g_count2);
}
return 0;
}

输出结果 g_count1=16627 g_count2=20000 可以看到g_count1的结果小于20000。可以看下反汇编来分析:
    while (count-- > 0)

00FB144C  mov         eax,dword ptr [count] 

00FB144F  mov         dword ptr [ebp-0D0h],eax 

00FB1455  mov         ecx,dword ptr [count] 

00FB1458  sub         ecx,1 

00FB145B  mov         dword ptr [count],ecx 

00FB145E  cmp         dword ptr [ebp-0D0h],0 

00FB1465  jle         func_callback+63h (0FB1473h) 

00FB1467  mov         dword ptr [ebp-0D4h],1 

00FB1471  jmp         func_callback+6Dh (0FB147Dh) 

00FB1473  mov         dword ptr [ebp-0D4h],0 

00FB147D  cmp         dword ptr [ebp-0D4h],0 

00FB1484  je          func_callback+93h (0FB14A3h) 

    {

        g_count1++;
00FB1486  mov         eax,dword ptr ds:[00FB8560h] 

00FB148B  add         eax,1 

00FB148E  mov         dword ptr ds:[00FB8560h],eax 
        InterlockedIncrement(&g_count2);
00FB1493  mov         eax,0FB855Ch 

00FB1498  mov         ecx,1 

00FB149D  lock xadd   dword ptr [eax],ecx 
    }

00FB14A1  jmp         func_callback+3Ch (0FB144Ch) 

    return 0;

00FB14A3  xor         eax,eax 

}
可以看到与g_count1++语句对应的汇编指令是由加操作和赋值操作两条指令完成的,在这两条指令之间可能发生任务切换。如果线程1执行加操作指令后,当前线程被切换到线程2,线程2重新执行取g_count1原始值的操作,然后切换回到线程1时,eax寄存器又被放入了g_count1的原始值,线程1的加1操作等于没有做。而操作系统提供的原子加1操作的API是一条指令,在指令执行期间不会发生任务切换,并且因为该指令有lock前缀,在多处理器架构中也不会受到其他处理器的影响。
原子操作/多任务/锁的一些基本概念
1 任务切换是用中断机制触发的,想发生任务切换必须向处理器通知一次中断的发生;

2 任务切换只能发生在指令边缘,就是说两条指令执行的间隙可能会发生任务切换,一条指令执行期间不会发生任务切换;

3 原子操作就是不可中断的一系列操作,如果被中断就会引起执行结果和预期不符;

4 单处理器架构下,一条指令的执行是原子操作;多处理器架构下,即使是一条指令执行期间也会受到其他处理器的干扰,导致指令执行结果错误。lock指令前缀的作用就是独占总线,保证在多处理器架构下一条指令的执行是原子操作。该指令前缀的实现必须是物理的,由处理器提供,软件无法实现。操作系统基于lock指令前缀封装一系列原子操作的API供上层应用使用;
单处理器架构下原子操作的实现
1 关中断

2 执行一系列指令,执行期间不会发生任务切换

3 开中断
多处理器架构下的原子操作的实现
在需要原子操作的指令前附加lock指令前缀,intel x86只有指定的几个指令才可以附加lock指令前缀。操作系统把这些附加了lock指令前缀的指令包装后,做成多种原子操作API供应用使用。
lock指令前缀的物理表现
当某条指令被加上lock指令前缀时,该指令在执行前,会把处理器的#HLOCK引脚拉低,该引脚被拉低导致总线被锁,其他处理器不能访问总线,直到指令执行完毕,处理器的#HLOCK引脚恢复以后,总线的访问权才被释放。



原子操作的缺点
独占总线,会影响处理器的效率。但是原子操作是保证多任务软件的执行正确性的最小粒度,别无选择。


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  x86 api