说完了进程的调度,就可以说下C语言的原子操作了。
原子操作,就是在执行的过程中、不会导致对数据的并发访问的、最小操作。
原子操作,是实现锁机制的基础。mutex,spinlock等,在其底层都有一个关键的原子操作。
原子操作的实现,需要CPU指令的支持,例如x86上的xchg/cmpxchg,ARM上的ldrex/strex。
最简单的原子操作,就是交换一个寄存器和一个内存地址的值。
typedef struct {
volatile int count;
} atomic_t;
static inline int atomic_xchg(atomic_t* v, int i)
{
int ret;
asm volatile(
"xchgl %0, %1"
:"=r"(ret)
:"m"(v->count),"0"(i)
);
return ret;
}
上面函数的功能,就是用值i去交换v->count的值,把交换到的值写到ret变量里,并返回。
根据这个函数,可以实现一个最简单的锁,锁的数据结构就是atomic_t,加锁函数如下:
void mutex_lock(atomic_t* v)
{
while (1 == atomic_xchg(v, 1)) {
sched_yield(); //获取锁失败后,放弃CPU
}
}
解锁函数如下:
void mutex_unlock(atomic_t* v)
{
v->count = 0;
}
v->count为1代表锁定,0代表没锁定,初始状态为0。
用1去交换v->count,如果返回0,则表示锁定成功,此时v->count为1。
如果返回1,则代表锁已经是锁定状态,本次获取锁失败,则放弃CPU并尝试继续获取锁,直到成功。
在锁定状态下,试图去获取锁,因为用1去交换,交换后v->count的值还是1,不会改变锁的状态。
以上需要CPU的指令xchg的原子性来保证。cmpxchg可以实现更复杂的原子操作。
如果是原子加或者减,则使用lock指令来锁定内存总线,然后执行add/sub指令。
atomic_dec_and_test原子操作,即减少引用计数并判断是否为0,在自动垃圾回收机制(GC)中经常用到,例如C++中的智能指针,java、Python等语言中的GC机制。
该函数的实现大概如下:
int atomic_dec_and_test(atomic_t* v)
{
char ret;
asm volatile(
"lock;decl %1\r\n" #锁定内存总线,并把v->count减1,该指令同时会设置EFLAGS
"sete %0" #把EFLAGS的零标志写到ret变量里,如果零标志被设置,则ret为1,否则0
:"r"(ret)
:"m"(v->count)
);
return ret;
}
也就是说,如果引用计数被减到0时,atomic_dec_and_test返回1,否则返回0。
如果某个带引用计数的对象,创建时引用计数设置为1,每次被引用时计数加1,释放时计数减1,当计数减少到0时,释放该对象的内存,就可以保证对象内存的正确释放了。
当然,在C++中,你故意(无意)多delete一次对象的指针,程序还是会崩的。
所以,高司令大神在java里直接不让delete了。只管new就行,什么时候delete交给高司令大神处理。
最后,lock机制是保证其下一条指令的内存操作是原子的,而再下一条就不归它管了。
sete指令之所以能正确工作,在于:
即使lock decl之后、sete之前发生了进程调度,内核也会保证切换回来时,EFLAGS的状态仍然是之前的状态。
所以,sete这条语句已经不需要原子性了。