本文将为你介绍内核同步算法中的自旋锁和信号量。在这之前,先了解一些概念。
执行线程:thread of execution,指任何正在执行的代码实例,可能是一个正在内核线程,一个中断处理程序等。有时候会将执行线程简称为线程。
临界区:critical region,即访问和操作共享数据的代码段。
多个执行线程并发访问同一资源通常是不安全的,通常使用自旋锁和信号量来实现对临界区互斥的访问。
自旋锁
自旋锁(spin lock)是一个对临界资源进行互斥访问的典型手段。自旋锁至多只能被一个执行线程持有。当一个执行线程想要获得一个已被使用的自旋锁时,该线程就会一直进行忙等待直至该锁被释放,就如同“自旋”所表达的意思那样:在原地打转。
我们也可以这么理解自旋锁:它如同一把门锁,而临界区就如同门后面的房间。当一个线程A进入房间后,它会关闭房门,使得其他线程不得进入。此时如果其他某个进程B需要进入房间,那么只能在门外“打转”。当A进程打开们后,进程B才能进入房间。
自旋锁的使用
1.定义初始化自旋锁
使用下面的语句就可以先定一个自旋锁变量,再对其进行初始化:
1 |
spinlock_t lock; |
2 |
spin_lock_init(&lock); |
也可以这样初始化一个自旋锁:
1 |
spinlock_t lock=SPIN_LOCK_UNLOCKED; |
2.获得自旋锁
1 |
void spin_lock(spinlock_t*);
|
2 |
int spin_trylock(spinlock_t*);
|
使用spin_lock(&lock)这样的语句来获得自旋锁。如果一个线程可以获得自旋锁,它将马上返回;否则,它将自旋至自旋锁的保持者释放锁。
另外可以使用spin_trylock(&lock)来试图获得自旋锁。如果一个线程可以立即获得自旋锁,则返回真;否则,返回假,此时它并不自旋。
3.释放自旋锁
1 |
void spin_unlock(spinlock_t*);
|
使用spin_unlock(&lock)来释放一个已持有的自旋锁。注意这个函数必须和spin_lock或spin_trylock函数配套使用。
关于自旋锁的说明
1.保护数据。我们使用锁的目的是保护临界区的数据而不是临界区的代码。
2.在使用自旋锁时候,必须关闭本地中断,否则可能出现双重请求的死锁。比如,中断处理程序打断正持有自旋锁的内核代码,如果这个中断处理程序又需要这个自旋锁,那么这个中断处理程序就会自旋等待自旋锁被释放;但是这个锁的持有者此刻被中断打断,因此不可能在中断完毕之前释放锁。此时就出现了死锁。
3.自旋锁是忙等锁。正如同前面所说的那样,当其他线程持有自旋锁时,如果另有线程想获得该锁,那么就只能循环的等待。这样忙的功能对CPU来说是极大的浪费。因此只有当由自旋锁保护的临界区执行时间很短时,使用自旋锁才比较合理。
4.自旋锁不能递归使用。这一点也很好理解,如果当前持有锁的线程又需要再持有该锁,那么它必须自旋,等待锁被释放;但是这个锁本身又被它自己持有,因此这个线程永远无法继续向前推进。
信号量
信号量(semaphore)是保护临界区的一种常用方法。它的功能与自旋锁相同,只能在得到信号量的进程才能执行临界区的代码。但是和自旋锁不同的是,一个进程不能获得信号量时,它会进入睡眠状态而不是自旋。
利用上述自旋锁的门和锁的例子,我们可以这样解释信号量。我们将信号量比作钥匙。进程A想要进入房间,就必要持有一把钥匙。当钥匙被使用完之后,如果又有进程B想进房间,那么这个进程在等待其他进程从房间出来给它钥匙的同时会打盹。当房间里的某个进程出来时会摇醒这个睡觉的进程B。
信号量的使用
0.数据结构
1 |
16struct semaphore { |
2 |
17 spinlock_t lock; |
3 |
18 unsigned int count;
|
4 |
19 struct list_head wait_list;
|
5 |
20}; |
count:初始化信号量时所设置的信号量值。
wait_list:等待队列,该队列中即为等待信号量的进程。
lock:自旋锁变量
1.定义初始化信号量
使用下面的代码可以定义并初始化信号量sem:
struct semaphore sem;
sem_init(&sem,val);
其中val即为信号量的初始值。
如果我们想使用互斥信号量,则使用下面的函数:
init_MUTEX(&sem);
这个函数会将sem的值初始为1,即等同于sem_init(&sem,1);
如果想将互斥信号量的初值设为0,则可以直接使用下面的函数:
init_MUTEX_LOCKED(&sem);
除上面的方法,可以使用下面的两个宏定义并初始化信号量:
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
其中name为变量名。
2.获得信号量
down(&sem);
进程使用该函数时,如果信号量值此时为0,则该进车会进入睡眠状态,因此该函数不能用于中断上下文中。
down_interruptibale(&sem);
该函数与down函数功能类似,只不过使用down而睡眠的进程不能被信号所打断,而使用down_interruptibale的函数则可以被信号打断。
如果想要在中断上下文使用信号量,则可以使用下面的函数:
dwon_try(&sem);
使用该函数时,如果进程可以获得信号量,则返回0;否则返回非0值,不会导致睡眠。
3.释放信号量
up(&sem);
该函数会释放信号量,如果此时等待队列中有进程,则唤醒一个。
信号量的同步
信号量除了使进程互斥的访问临界区外,还可以用于进程间的同步。比如,当B进程执行完代码区C后,A进程才会执行代码段D,两者之间有一定的执行顺序。