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

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

 
  1. #include <stdio.h>

  2. #include <assert.h>

  3. #include <process.h>

  4. #include <Windows.h>

  5.  
  6. int g_count1;

  7. volatile long g_count2;

  8. HANDLE h_event;

  9. unsigned int CALLBACK func_callback(void *context);

  10.  
  11. int main(const int argc, const char *argv[])

  12. {

  13. unsigned long t_id;

  14. HANDLE h[2];

  15.  
  16. h_event = CreateEvent(NULL, TRUE, FALSE, NULL);

  17. h[0] = (HANDLE)_beginthreadex(NULL, 0, func_callback, NULL, 0, &t_id);

  18. assert(h[0] != INVALID_HANDLE_VALUE);

  19. h[1] = (HANDLE)_beginthreadex(NULL, 0, func_callback, NULL, 0, &t_id);

  20. assert(h[1] != INVALID_HANDLE_VALUE);

  21.  
  22. g_count1 = g_count2 = 0;

  23. SetEvent(h_event);

  24. WaitForMultipleObjects(2, h, TRUE, INFINITE);

  25. CloseHandle(h_event);

  26. printf("g_count1=%d g_count2=%ld\n", g_count1, g_count2);

  27. getchar();

  28.  
  29. return 0;

  30. }

  31.  
  32. unsigned int CALLBACK func_callback(void *context)

  33. {

  34. int count;

  35.  
  36. count = 10000;

  37. WaitForSingleObject(h_event, INFINITE);

  38. while (count-- > 0)

  39. {

  40. g_count1++;

  41. InterlockedIncrement(&g_count2);

  42. }

  43. return 0;

  44. }

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


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

猜你喜欢

转载自blog.csdn.net/qq_35119422/article/details/81156865