自旋锁+信号量

时间:2021-01-19 15:12:42

本文将为你介绍内核同步算法中的自旋锁和信号量。在这之前,先了解一些概念。

执行线程: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,两者之间有一定的执行顺序。