同步与互斥基础
一、临界区
临界区:具体一点,在程序中,临界区就是一段代码区域,这段代码区在任何时间点至多只有一个进程在运行它的代码。
二、竞争条件
打个比方:有个大米库所,里面装了若干袋的大米,前台有个记录册记录了当前库所的库存。当一个员工运来大米时,他首先参看记录册中记录了多少袋大米,然后将自己送来的大米放入仓库,最后将自己新加入的大米的袋数与他之前记下的记录册记录的袋数相加,并将结果更新到记录册中。
这个过程有个问题,你想,他记下记录册中的袋数,然后将送来的大米搬到库所中,最后更新记录册。在记下记录册中的袋数到最后更新记录期间要是有其他的员工过来运送大米怎么办?这里,数据就会出现不一致了。例如:
库所库存:storage = 10
上午9:00,员工A来送大米,记下库所的当前库存:a=10,然后负责将大米搬到库所中。
上午9:10,员工B来送大米,记下库所的当前库存:b=10,然后负责将大米搬到库所中。
上午9:30,员工A搬运完送来的大米,数了自己送来的大米袋数为20,故更新storage = a + 20 = 30。
上午9:40,员工B搬运完送来的大米,数了自己送来的大米袋数为20,故更新storage = a + 20 = 30。
这样最后的记录册上的库所库存为30,但实际因该是50才对!
回到计算机世界。
cpu会将内存中的内容读到寄存器中[load指令],然后在寄存器中进行计算[一到多条计算指令],然后将计算完的结果送回到内存中[store指令]。在允许中断的情况下,每一条指令周期结束的时候,cpu都会检查是否产生了中断,如果产生中,则转到中断处理程序,而在中断返回的时候就可能发生调度,挂起当前进程,执行其它进程。
竞争条件:某个进程将一个内存中的共享变量读到寄存器中参与计算,在其将更新之后的内容写回到内存之前了发生调度,切换到了其它进程,而这个新的进程需要将相同内存地址中的这个共享变量读取然后操作。很明显,若不采取必要的措施,这两个进程就会像前面例子中提到的两个员工一样,错误的时间,错误的地点,做了错误的事情…
三、原子操作区
cpu在某个进程上执行程序,当走到临界区时,调度发生,cpu切换到其他进程上执行程序,在某个时刻可能出现下图中的情况:
好几个进程都走到了临界区前,而且当前的原子操作区是开放的,这就意味着接下来这几个进程中谁首先被内核调度运行,谁就能够进入到临界区,并将原子操作区关闭,不让其他进程进入临界区,如下图:
在进程退出临界区的时候,原子操作区被打开,再次允许其他进程进入:
四、锁与硬件支持
4.1 锁
一种机制,可以保证对临界区的互斥访问。
4.2 硬件支持
有这么一条机器指令,他能将两个变量中的内容对换。我们姑且叫他swap指令:
swap [A] [B]
//将A和B存储单位中的内容互换。有这样一条机器指令就能实现锁机制了:
//---LOCK.h文件-------- extern bool lock = false;//全局锁变量,初始化为false void _LOCK(); void _UNLOCK();
//---LOCK.c文件-------- //伪代码: #include "LOCK.h" void _LOCK(){ bool key = true; //局部栈变量 //*********************************************************// swap: swap [lock] [key]; //假设存在这么一条汇编指令swap,既然是一条指令,那么必然是原子的。 //正真的汇编指令不是这个名,内联汇编的格式也不是这样的,不过这里这么写的好处是简 //单直接。你就假设它是一条汇编指令,且能完成交互两个变量中的内容就行了。 //*********************************************************// if(key==true) goto swap; }
仔细想想,多个进程可以同时进入_LOCK()函数,但是能跳出循环的只能是最先执行到swap指令的进程,当这个进程执行完swap之后,第一个进程的局部栈变量key变成了false,全局变量lock变成了true。这个进程能跳出循环,进入到临界区,而其他进程执行到swap时,lock变成了true,由于他的局部栈变量key是true,所以,他跳不出这个循环,会一直处在循环之中,等待lock变为false:
//---LOCK.c文件-------- void _UNLOCK(){ lock = false; }
在临界区中的进程退出时调用 _UNLOCK()函数,释放锁,这样其他等待这个锁的进程中的第一个执行到swap指令的进程就能进入到临界区了。这样就实现了自旋锁的申请与释放。
//---test1.c文件--- //大概的框架如下: #include "LOCK.h" _LOCK();//请求锁 /* 临界区,do something... */ _UNLOCK();//释放锁
五、信号量
有了锁机制的支持,信号量的实现也就好办了,其实不过是对底层机制的应用与封装。
//---semaphore.h文件--- #include "LOCK.h" typedef struct { int value; struct list_head *wait_list; }semaphore; void P(semaphore* S); void V(semaphore* S);
//---semaphore.c文件--- #include "semaphore.h" void P(semaphore* S){ _LOCK(); S->value--; if(S->value < 0){ add this task_struct to S->wait_list; _UNLOCK(); block(); return; } _UNLOCK(); } void V(semaphore* S){ _LOCK(); S->value++; if(S->value <= 0){ remove a task_struct P from S->wait_list; _UNLOCK(); wakeup(P); return; } _UNLOCK(); }
//---test2.c文件--- //大概的框架如下: #include "semaphore.h" extern semaphore S = ...; ... P(S);//请求一个信号量 /* 临界区,do something... */ V(S);//释放一个信号量