信号量
1 信号量的简述
信号量是Linux内核保护临界资源的一种方式。在FreeRTOS等嵌入式系统中的信号量可能会分为互斥信号量或者计数信号量。Linux内核中的信号量也可分为这两种,但是大多情况下,信号量作为互斥锁方式来时用。Linux内核中的信号量只有当得到信号量的进程时才能够进入临界区,执行临界代码。当一个信号量试图去获取一个已经上锁的信号量(即该信号量已经被其他进行占用了)时,进程会将自身加入一个等待队列中去睡眠,直到拥有信号量的进程释放信号量后,处于等待队列中的那个进行才会被唤醒。唤醒之后,立即重睡眠处开始执行,再次试图获取信号量,当获得信号量后,程序才能继续运行。
2 信号量实现
使用信号量,内核代码需包含头文件
。Linux内核信号量用semaphore结构描述。其定义如下所示:
struct semaphore{
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
1.1 信号量的声明和初始化
(1)宏定义方式(静态):
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
(2)动态初始化(通过已分配或者动态分配的semaphore进行初始化):
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(strcut semaphore *sem);
(3)可设置信号量初始值的初始化接口:
void sema_init(struct semaphore *sem, int val);
上述(1)、(2)两种方式,相当于将信号量作为互斥体使用,当然使用(3)将val初始化为1或0也是一样的。不过(3)可以用来当做计数信号量使用。
1.2 信号量的获取和释放
(1) 获取信号量
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
- down()递减信号量值,并且等待需要的事件。
- down_interruptible()也是递减信号量值,但是操作是可以被中断的,它允许一个在等待一个信号量的用户空间进程被用户中断。比如说,我们可以发送ctrl+c软中断,让等待这个内核驱动返回的用户态进程退出。在睡眠时,能被中断信号终止,这个进程是可以被中断信号打断。再说个简单点的例子,我们在命令行中输入# sleep 10000,再按下ctrl+c,就给刚刚的进程发送终止信号,信号发送给用户空间,然后通过系统调用,把这个信号传递给驱动。信号只能发送给用户空间,无法直接发送给内核。如果操作被中断了,函数返回一个非零值,并且调用者不会持有信号量。当我们使用down_interruptible需要一直检查返回值并且针对性地响应。如下代码所示:
if(down_interruptible(&dev->sem))
return -ERESTATSYS;
- down_trylock()也是会递减信号量,但是该接口在信号量不可用时,进程是不会进入睡眠的,其会立马返回一个非零值。
如果一个进行已经成功调用down各个版本中的一个,就说明它持有信号量(已经“取得”后者“获得”信号量),这个进程现在有权使用被保护的临界区。当这个需要互斥的操作完成后,信号量必须被释放,Linux内核提供了up接口释放信号量。
(2)释放信号量
void up(struct semaphore *sem);
如果up()被调用,调用者就不再拥有信号量。
如果一个进程持有信号量时遇到了一个错误,信号量必须在返回错误状态给调用者之前释放。
3 读/写信号量
信号量大多被调用者作为互斥使用,不管每个进行可能会做什么。很多任务分为2中类型:只需要读取被保护的数据结构的类型,和必须做改变的类型。允许多个并发的读进程常常是可能的,只要没有人试图做任何改变,这样做能够显著的提供性能。只读任务可以并行的进行,不必等待其他读进行退出临界区。
Linux内核特意地为这种情况提供了一个特殊的信号量,称为rwsem(或者“reader/writer semaphore”)。rwsem在驱动中的使用相对较少,但是有时它们有用。
使用rwsem的代码必须包含。读/写信号量的相关数据类型是struct rw_semaphore。一个rwsem必须在运行时显式初始化,如下所示:
struct rw_semaphore {
/*读/写信号量定义:
* - 如果activity为0,那么没有激活的读者或写者。
* - 如果activity为+ve,那么将有ve个激活的读者。
* - 如果activity为-1,那么将有1个激活的写者。 */
__s32 activity; /*信号量值*/
spinlock_t wait_lock; /*用于锁等待队列wait_list*/
struct list_head wait_list; /*如果非空,表示有进程等待该信号量*/
#ifdef CONFIG_DEBUG_LOCK_ALLOC /*用于锁调试*/
struct lockdep_map dep_map;
#endif
};
void init_resem(struct rw_semaphore *sem);
3.1 读进程获取读写信号量
一个新初始化的rwsem对出现的下一个任务(读者或者写者)是可用的。如果需要只读保护资源的话,试图获取读信号量的接口如下:
void down_read(struct rw_semaphore *sem); // 获取读写信号量失败仅仅进入睡眠
int down_read_trylock(struct rw_semaphore *sem); // 获取读写信号量失败不会进入睡眠,会返回非0值。若成功返回0。
void up_read(struct rw_semaphore *sem);// 释放读写信号量。
- down_read和down_read_trylock仅当在写进程占用读写信号量的情况下,获取读写信号量失败。如果读写信号量没有被其它写进程则down_read和down_read_trylock能成功获取。如果读写信号量已经被其它读进程占用,其它读进程也是能获取的,这些读进程是可以并发地访问临界区。
- 读进程不再需要访问临界区时,应当调用up_read释放读写信号量。
3.2 写进程获取读写信号量
如果需要对临界区进行写操作,内核提供了下列的接口获取和释放读写信号量:
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
- down_write和wirte_trylock在读进程或者其它写进程占用读写信号量的情况下获取读写信号量失败。如果所有的读进程都释放了读写信号量且没有其它的写进程占用读写信号量的情况下,当前写进程才能获取信号量,从可进行临界区写操作。
- 写进程不再需要对临界区进行写操作,应当调用up_write释放写信号量。
- 写进程在写操作完成后,有一个长时间的读操作,可以使用调用downgrade_write来快速改变,以让其它读进程读取临界区。
一个rwsem允许一个或者多个读进行持有读写信号量,写者有优先权(信号量未上锁时,写者优先获取)。当一个写者试图进入临界区,就不会允许读者进入直到所有的写者完成了它们的工作。这个实现可能导致读者饥饿,读者被长时间拒绝存取,如果你有大量的写者来竞争信号量,由于这个原因,rwsemzuih最好用很少请求写的时候,并且写者只占用短时间。