信号量、互斥锁、自旋锁

信号量、互斥锁、自旋锁


前言

信号量:是一种锁机制用于协调进程之间互斥的访问临界资源。以确保某种共享资源不被多个进程同时访问。
互斥锁(Mutex):是初始值为1的信号量
自旋锁:与互斥锁类似,但在无法得到资源时,互斥锁内核线程处于睡眠阻塞状态,而自旋锁处于忙等待状态


一、信号量

Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量(如semget,semop)是一样的,但是它绝不可能在内核之外使用,因此它与System V的IPC机制信号量毫不相干。

信号量的初始值:表示同时可以有几个任务可以访问该信号量保护的共享资源

一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1
…若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列,等待该信号量可用;
…若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。

任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,
如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务的其中一个。

1、信号量初始化api

静态定义:
DECLARE_MUTEX(name)
该宏声明一个信号量name并初始化它的值为1,即声明一个互斥锁。 
DECLARE_MUTEX_LOCKED(name)
该宏声明一个互斥锁name,但把它的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得。 

动态定义:
void sema_init (struct semaphore *sem, int val);
该函用于数初始化设置信号量的初值,它设置信号量sem的值为val。 
void init_MUTEX (struct semaphore *sem);
该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。 
void init_MUTEX_LOCKED (struct semaphore *sem);
该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。

2、获取信号量

void down(struct semaphore * sem);

该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。

int down_interruptible(struct semaphore * sem);

该函数功能与down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断。因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。

int down_trylock(struct semaphore * sem);

该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0。否则,表示不能获得信号量sem,返回值为非0值。因此,它不会导致调用者睡眠,可以在中断上下文使用

3、释放信号量

void up(struct semaphore * sem);

该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒所有这些等待者的其中一个等待任务。

4、使用案例

#define N 15
int num;
struct semaphore sem_1, sem_2;


int ThreadFunc1(void *context)
{
	char *tmp=(char*)context;
	
	while(num<N)
	{
		down(&sem_1);	
		printk("%s-before-num:%d\n",tmp,num);
		msleep(1000);
		num++;
		printk("%s-after-num:%d\n",tmp,num);
		up(&sem_1);
	}
	
	return 0;
}
int ThreadFunc2(void *context)
{
	char *tmp=(char*)context;
	while(num<N)
	{
		down(&sem_1);
		printk("%s-before-num:%d\n",tmp,num);
		msleep(1000);
		num++;
		printk("%s-after-num:%d\n",tmp,num);
		up(&sem_1);
	}
	return 0;
}

static int __init gpio_init(void)
{
	printk(" up. \n"); 
	sema_init(&sem_1, 1);
	sema_init(&sem_2, 0);
	
	kthread_run(ThreadFunc1,"123a", "123");
	kthread_run(ThreadFunc2,"456a", "456");

	return 0;
}

static void __exit gpio_exit(void)
{
	printk( " down.\n");
}

module_init(gpio_init);
module_exit(gpio_exit);

MODULE_LICENSE("Dual BSD/GPL");

二、互斥锁

如果初始值为1,表示只有1个任务可以访问,信号量变成互斥锁(Mutex)。可见互斥锁是信号量的特例

互斥锁(mutex)是在原子操作API的基础上实现的信号量行为。互斥锁不能进行递归锁定或解锁,能用于进程上下文,同一时间只能有一个任务持有互斥锁。

互斥锁功能上基本上与信号量一样,互斥锁占用空间比信号量小,运行效率比信号量高。

互斥锁的API

DEFINE_MUTEX(mutexname)  
静态创建和初始化互斥锁。  
mutex_init(struct mutex* lock);
动态创建和初始化互斥锁。

void mutex_lock(struct mutex *lock);
加锁。  若无法获得锁,则休眠
int mutex_trylock(struct mutex *lock); 
尝试加锁。  若已经无法获得锁,则直接返回。用在中断

void mutex_unlock(struct mutex *lock);
解锁,并唤醒其他需要锁的任务 

三、自旋锁

自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻**最多只能有一个执行单元(**进程或中断处理程序)获得锁。但是两者在调度机制上略有不同。

自旋锁实际上是忙等待,自旋锁可能导致系统死锁
死锁:简单说,就是当前任务在获得自旋锁之后,释放自旋锁之前,主动调用一些函数让出CPU的控制权(比如主动调用sleep休眠或主动调用内核调度函数schedule去调度其他任务),这时候,如果被调度进来的任务也想去获得该自旋锁,便会进入死循环轮询等待,即发生了死锁。

自旋锁不允许睡眠或阻塞
只有一个进程不会死锁,两个或两个以上的进程才会死锁

1、初始化

动态初始化:
spin_lock_init(spinlock_t *lock);
该宏用于初始化自旋锁x
静态:
DEFINE_SPINLOCK(spinlock_t x)
该宏声明一个自旋锁x并初始化它。
SPIN_LOCK_UNLOCKED
该宏用于静态初始化一个自旋锁。
//DEFINE_SPINLOCK(spinlock_t x)等同于spinlock_t x = SPIN_LOCK_UNLOCKED 

2、获得自旋锁

spin_trylock(lock)
该宏尽力获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,
否则不能立即获得锁,立即返回假。它不会自旋等待lock被释放。
spin_lock(lock)
该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,
否则,它将自旋在那里,直到该自旋锁的保持者释放,这时,它
获得锁并返回。总之,只有它获得锁才返回
spin_lock_irqsave(lock, flags)
该宏获得自旋锁的同时把标志寄存器的值保存到变量flags中并失效本地中断。
spin_lock_irq(lock)
该宏类似于spin_lock_irqsave,只是该宏不保存标志寄存器的值。
spin_lock_bh(lock)
该宏在得到自旋锁的同时失效本地软中断。
spin_trylock_irqsave(lock, flags)
该宏如果获得自旋锁lock,它也将保存标志寄存器的值到变量flags中,
并且失效本地中断,如果没有获得锁,它什么也不做。因此如果能够立即获得锁,
它等同于spin_lock_irqsave,如果不能获得锁,它等同于spin_trylock。
如果该宏获得自旋锁lock,那需要使用spin_unlock_irqrestore来释放。
spin_trylock_irq(lock)
该宏类似于spin_trylock_irqsave,只是该宏不保存标志寄存器。
如果该宏获得自旋锁lock,需要使用spin_unlock_irq来释放。
spin_trylock_bh(lock)
该宏如果获得了自旋锁,它也将失效本地软中断。如果得不到锁,
它什么也不做。因此,如果得到了锁,它等同于spin_lock_bh,
如果得不到锁,它等同于spin_trylock。如果该宏得到了自旋锁,
需要使用spin_unlock_bh来释放。

3、释放自旋锁

spin_unlock(lock)
该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用。
如果spin_trylock返回假,表明没有获得自旋锁,因此不必使用spin_unlock释放
spin_unlock_irqrestore(lock, flags)
该宏释放自旋锁lock的同时,也恢复标志寄存器的值为变量flags保存的值。
它与spin_lock_irqsave配对使用。
spin_unlock_irq(lock)
该宏释放自旋锁lock的同时,也使能本地中断。
它与spin_lock_irq配对应用。
spin_unlock_bh(lock)
该宏释放自旋锁lock的同时,也使能本地的软中断。
它与spin_lock_bh配对使用。
spin_unlock_wait(x)
该宏用于等待自旋锁x变得没有被任何执行单元保持,
如果没有任何执行单元保持该自旋锁,该宏立即返回,否则将循环在那里,
直到该自旋锁被保持者释放。

4、判断自旋锁

spin_is_locked(x)
该宏用于判断自旋锁x是否已经被某执行单元保持(即被锁),
如果是,返回真,否则返回假。
spin_can_lock(lock)
该宏用于判断自旋锁lock是否能够被锁,它实际是spin_is_locked取反。
如果lock没有被锁,它返回真,否则,返回假。

5、自旋锁使用注意事项

注意
自旋锁与中断:进程A占用锁后,内核的抢占暂时被禁止(但无法禁止中断抢占进程)。这时候突然发生中断,然后中断处理函数里想要获得这个锁时,发生了死锁。
所以需要改用spin_lock_irq或spin_lock_irqsave。解锁换成对应的spin_unlock_irq或spin_unlock_irqrestore。
进程上下文与软中断上下文
只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。
当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断,隐式地也失效了软中断。但是使用spin_lock_bh和spin_unlock_bh是最恰当的,它比其他两个快。
进程上下文 和 tasklet或timer上下文
在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况(进程上下文和软中断上下文)相同的获得和释放锁的宏,因为tasklet和timer是用软中断实现的。
进程上下文 和 中断上下文
在进程或软中断上下文需要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。 在中断处理句柄中使用,如果只有一个(硬),仅需要spin_lock和spin_unlock,不同的(硬)中断,使用spin_lock_irq和spin_unlock_irq
tasklet或timer上下文
不需要任何自旋锁保护,同一个tasklet或timer只能在一个CPU上运行
一个或多个软中断上下文
只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源需要用spin_lock和spin_unlock来保护,在两个或多个软中断上下文访问,那么这个共享资源当然更需要用spin_lock和spin_unlock来保护 , 没必要使用spin_lock_bh


信号量、互斥锁、自旋锁

上一篇:RT-Thread学习笔记2-互斥量与信号量


下一篇:线程