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

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

#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引脚恢复以后,总线的访问权才被释放。

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



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值