x86平台原子操作API的实现原理
2015-03-13 09:30
281 查看
原子操作的意义
对于软件来说,代码的行为必须是确定的,就是说通过手工分析代码也能预知运行结果。但是程序在并发和并行时,因为操作系统任务调度的不确定性和多处理器之间的相互影响,导致代码运行的结果无法预知。这种情况下,只有强制保证某些指令的执行是原子操作,代码运行结果才是可预知的,原子操作也是多任务操作系统设计的基石。正确的使用原子操作,可以避免多任务导致的脏数据,保证代码行为的正确。下面通过一段代码说明为什么程序在并发和并行时需要原子操作。
输出结果 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引脚恢复以后,总线的访问权才被释放。
原子操作的缺点
独占总线,会影响处理器的效率。但是原子操作是保证多任务软件的执行正确性的最小粒度,别无选择。
对于软件来说,代码的行为必须是确定的,就是说通过手工分析代码也能预知运行结果。但是程序在并发和并行时,因为操作系统任务调度的不确定性和多处理器之间的相互影响,导致代码运行的结果无法预知。这种情况下,只有强制保证某些指令的执行是原子操作,代码运行结果才是可预知的,原子操作也是多任务操作系统设计的基石。正确的使用原子操作,可以避免多任务导致的脏数据,保证代码行为的正确。下面通过一段代码说明为什么程序在并发和并行时需要原子操作。
#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平台读取cpu支持sse2指令集的代码,以及原子操作的代码
- 聊聊并发(五)原子操作的实现原理
- Java并发机制底层——原子操作的实现原理
- Linux 互斥锁、原子操作实现原理
- 聊聊并发(五)原子操作的实现原理
- java 原子操作在x86下的实现
- 原子操作的实现原理
- 原子操作的实现原理
- 聊聊并发(五)——原子操作的实现原理
- 聊聊并发(五)——原子操作的实现原理
- java多线程-专题-聊聊并发(五)原子操作的实现原理
- Android原子操作的实现原理
- 原子操作的实现原理
- 聊聊并发(五)——原子操作的实现原理
- 聊聊并发(五)――原子操作的实现原理
- java原子操作的实现原理--转载
- 原子操作的实现原理
- Linux 互斥锁、原子操作实现原理
- Java实现原子操作的原理