Linux的原子操作与同步机制

Linux的原子操作与同步机制


并发问题
例如C语言语句“count++;”在未经编译器优化时生成的汇编代码为多条机器指令来实现的。


例子:
假设count变量初始值为0。进程1执行完“mov eax, [count]”后,寄存器eax内保存了count的值0。此时,
进程2被调度执行,抢占了进程1的CPU的控制权。进程2执行“count++;”的汇编代码,将累加后的count值
1写回到内存。然后,进程1再次被调度执行,CPU控制权回到进程1。进程1接着执行,计算count的累加
值仍为1,写回到内存。虽然进程1和进程2执行了两次“count++;”操作,但是count实际的内存值为1,而
不是2!



单处理器原子操作
解决这个问题的方法是,将“count++;”语句翻译为单指令操作。
Intel x86指令集支持内存操作数的inc操作,这样“count++;”操作可以在一条指令内完成。因为进程的上下文
切换是在总是在一条指令执行完成后,所以不会出现上述的并发问题。对于单处理器来说,一条处理器指令就
是一个原子操作。



多处理器原子操作
但是在多处理器的环境下,例如SMP架构,这个结论不再成立。我们知道“inc [count]”指令的执行过程分为三步:
1)从内存将count的数据读取到cpu。
2)累加读取的值。
3)将修改的值写回count内存。
这又回到前面并发问题类似的情况,只不过此时并发的主题不再是进程,而是处理器。

Intel x86指令集提供了指令前缀lock用于锁定前端串行总线(FSB),保证了指令执行时不会受到其他处理器的干扰。
使用lock指令前缀后,处理器间对count内存的并发访问(读/写)被禁止,从而保证了指令的原子性。



arm原子操作实现
在arm的指令集中,不存在指令前缀lock,那如何完成原子操作呢?
在ARMv6之前,swp指令就是通过锁定总线的方式完成原子的数据交换,但是影响系统性能。ARMv6之后,一般使用ldrex
和strex指令对代替swp指令的功能。



自旋锁中的原子操作
	1:
	lock   decb [lock->slock]
	jns    3
	2:
	rep    nop
	cmpb   $0, [lock->slock]
	jle    2
	jmp    1
	3:


其中lock->slock字段初始值为1,执行原子操作decb后值为0。符号位为0,执行jns指令跳转到3,完成自旋锁的加锁。

当再次申请自旋锁时,执行原子操作decb后lock->slock值为-1。符号位为1,不执行jns指令。进入标签2,执行一组
nop指令后比较lock->slock是否小于等于0,如果小于等于0回到标签2进行循环(自旋)。否则跳转到标签1重新申请
自旋锁,直到申请成功。

自旋锁释放时会将lock->slock设置为1,这样保证了其他进程可以获得自旋锁。



信号量中的原子操作
信号量的申请操作由函数down实现
	lock   decl [sem->count]
	js 2
	1:
	<========== another section ==========>
	2:
	lea    [sem->count], eax
	call   __down_failed
	jmp 1


信号量的sem->count一般初始化为一个正整数,申请信号量时执行原子操作decl,将sem->count减1。如果该值减为负数
(符号位为1)则跳转到另一个段内的标签2,否则申请信号量成功。

标签2被编译到另一个段内,进入标签2后,执行lea指令取出sem->count的地址,放到eax寄存器作为参数,然后调用函数
__down_failed表示信号量申请失败,进程加入等待队列。最后跳回标签1结束信号量申请。


信号量的释放操作由函数up实现。
	lock   incl sem->count
	jle     2
	1:
	<========== another section ==========>
	2:
	lea    [sem->count], eax
	call   __up_wakeup
	jmp    1


释放信号量时执行原子操作incl将sem->count加1,如果该值小于等于0,则说明等待队列有阻塞的进程需要唤醒,跳转到标签2,
否则信号量释放成功。

标签2被编译到另一个段内,进入标签2后,执行lea指令取出sem->count的地址,放到eax寄存器作为参数,然后调用函数__up_wakeup
唤醒等待队列的进程。最后跳回标签1结束信号量释放。

例子:

代码1(低优先级)(线程1)
代码1down
代码1down处理代码
代码1up


代码2(高优先级)(线程2)
代码2down
代码2down处理代码
代码2up

分析:
代码1down成功就运行代码1down处理代码,(假如代码2刚运行到代码2down,因没有得到信号量,所以被挂起了),代码1处理完
代码1down处理代码,到代码1up时会看有没有其它线程等待信号量(线程2等待着),所以系统恢复线程2进行运行,运行
代码2down处理代码,再运行到代码2up,发现没有就退出直到线程2不再运行,系统恢复线程1进行运行,运行到代码1up时被
系统挂起时下一条代码,再运行代码1其他代码直到再次被挂起。


原文参考: http://www.cnblogs.com/fanzhidongyzby/p/3654855.html

猜你喜欢

转载自huangyongxing310.iteye.com/blog/2318097