信号量
Linux中的信号量是一种睡眠锁
睡眠机制:如果一个任务试图获取一个不可用(正在被占用)的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有的信号量可用(被释放)后,处于等待队列的那个任务将被唤醒,并获得该信号量。
-
由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长期持有的情况。
-
由于执行线程在锁被征用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中是不能进行调度的。
-
等待信号量时会陷入睡眠,因此在占用信号量的同时不能占用自旋锁,因为在等待信号量时可能会睡眠,而持有自旋锁的时候是不允许睡眠的。
计数信号量
初始化时设置信号量的数值大于1的非0值,在这种情况下允许在一个时刻同时有多个锁持有者,这种信号量称为计数信号量。
计数信号量不能用作强制互斥,因为它允许多个执行线程同时访问临界区。内核中使用的不多。
二值信号量
顾名思义,与计数信号量相对应,二值信号量即信号量值非0即1,也称为互斥信号量。在使用信号量时大部分用的都是二值信号量。
创建和初始化信号量
信号量的实现与体系结构相关,具体定义在<asm/semaphore.h>中。(我只在include/linux下找到了这个文件)
/* Please don't access any members of this structure directly */
struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
静态声明信号量:
struct semaphore name;
sema_init(&name , count);
如果是互斥信号量,还可以简化声明:
static DECLARE_MUTEX(name);
更常见的情况: 信号量作为一个大数据结构的一部分动态创建,此时只有指向该动态创建的信号量的间接指针,可以使用如下函数对它进行初始化:
sema_init(sem,count);//sem是指针,count是信号量的使用者数量
与前面类似,初始化一个动态创建的互斥信号量也可以使用如下函数:
init_MUTEX(sem);
使用信号量
方法 | 描述 |
---|---|
sema_init( struct semaphore * , int) | 以指定的计数值初始化动态创建的信号量 |
init_MUTEX( struct semaphore * ) | 以计数值1初始化动态创建的信号量 |
init_MUTEX_LOCKED(struct semaphore *) | 以计数值0初始化动态创建的信号量 (初始化就为加锁状态) |
down_interruptible(struct semaphore *) | 以试图获得指定的信号量,如果信号量已被争用 则进入可中断睡眠状态 |
down(struct semaphore *) | 以试图获得指定的信号量,如果信号量已被争用 则进入不可中断睡眠状态 |
down_trylock(struct semaphore *) | 以试图获得指定的信号量,如果信号量已被争用 则立刻返回非0值 |
up(struct semaphore *) | 以释放指定的信号量,如果睡眠队列不空 则唤醒其中一个任务 |
内核中对down的细化操作
extern void down(struct semaphore *sem);
具体函数实现如下:
void down(struct semaphore *sem)
{
unsigned long flags;
spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
__down(sem);
spin_unlock_irqrestore(&sem->lock, flags);
}
显然,信号量的操作使用了自旋锁,这从上面信号量的声明也可以看出,对信号量的操作需要使用自旋锁同步,防止信号量操作产生竞争冒险。
在spinlock锁区域内部,通过判断当前信号量是否为0进行相应操作,若大于0则自减即可;但是如果信号量被持有,则调用__down函数进一步处理。具体函数如下:
static noinline void __sched __down(struct semaphore *sem)
{
__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
static inline int __sched __down_common(struct semaphore *sem, long state,
long timeout)
{
struct task_struct *task = current;
struct semaphore_waiter waiter;
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = task;
waiter.up = 0;
for (;;) {
if (signal_pending_state(state, task))
goto interrupted;
if (timeout <= 0)
goto timed_out;
__set_task_state(task, state);
spin_unlock_irq(&sem->lock);
timeout = schedule_timeout(timeout);
spin_lock_irq(&sem->lock);
if (waiter.up)
return 0;
}
timed_out:
list_del(&waiter.list);
return -ETIME;
interrupted:
list_del(&waiter.list);
return -EINTR;
}
注意,这里对task的状态赋值state为uninterruptible
处理过程大概目的就是使当前请求线程进入睡眠等待队列,内核中为信号量等待队列单独设计了结构体,内容如下:
struct semaphore_waiter {
struct list_head list;
struct task_struct *task;
int up;
};
这个结构体中的up在后面的for循环中一直在被检测,直到被置1的时候才会返回,否则线程依然会重新陷入睡眠,或者调度时间time_out耗尽返回错误。
内核中对up的细化操作
extern void up(struct semaphore *sem);
void up(struct semaphore *sem)
{
unsigned long flags;
spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem);
spin_unlock_irqrestore(&sem->lock, flags);
}
同理,up的操作也使用了自旋锁来保持信号量操作的互斥,在spinlock锁区域进行判断,当睡眠等待队列为空时代表没有人在等待该信号量,直接将信号量释放即可。但是当队列非空时要进行一个重要的操作:唤醒睡眠进程
static noinline void __sched __up(struct semaphore *sem)
{
struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,
struct semaphore_waiter, list);
list_del(&waiter->list);
waiter->up = 1;
wake_up_process(waiter->task);
}
显然,该操作只对等待链表的第一个成员进行操作,避免多个睡眠进程争用信号量。最后将上文提到的up置1,使睡眠进程唤醒。
读/写信号量
与自旋锁一样,信号量也有区分读/写访问的功能。与读写自旋锁和普通自旋锁之间的关系差不多,读写信号量也要比普通信号更具优势。
读写信号量在内核中由rw_semaphore表示,定义在文件<linux/rwsem.h>中。(2.6.34声明在rwsem-spinlock.h中)
/*
* the rw-semaphore definition
* - if activity is 0 then there are no active readers or writers
* - if activity is +ve then that is the number of active readers
* - if activity is -1 then there is one active writer
* - if wait_list is not empty, then there are processes waiting for the semaphore
*/
struct rw_semaphore {
__s32 activity;
spinlock_t wait_lock;
struct list_head wait_list;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
通过注释可见,读写信号量引入了activity表征读写状态,同时也使用了自旋锁和等待队列。
同样,读写信号量也可以静态声明:
#define DECLARE_RWSEM(name)
struct rw_semaphore name = __RWSEM_INITIALIZER(name)
或者是动态声明:
/*
* Initialize an rwsem:
*/
void __init_rwsem(struct rw_semaphore *sem, const char *name,
struct lock_class_key *key)
{
#ifdef CONFIG_DEBUG_LOCK_ALLOC
/*
* Make sure we are not reinitializing a held semaphore:
*/
debug_check_no_locks_freed((void *)sem, sizeof(*sem));
lockdep_init_map(&sem->dep_map, name, key, 0);
#endif
sem->count = RWSEM_UNLOCKED_VALUE;
spin_lock_init(&sem->wait_lock);
INIT_LIST_HEAD(&sem->wait_list);
}
注意:所有的读写信号量都是互斥信号量。(从结构体描述来看,它只针对写互斥)
对其操作也使用up和down:
extern void __down_read(struct rw_semaphore *sem);
extern int __down_read_trylock(struct rw_semaphore *sem);
extern void __down_write(struct rw_semaphore *sem);
extern void __down_write_nested(struct rw_semaphore *sem, int subclass);
extern int __down_write_trylock(struct rw_semaphore *sem);
extern void __up_read(struct rw_semaphore *sem);
extern void __up_write(struct rw_semaphore *sem);
extern void __downgrade_write(struct rw_semaphore *sem);
extern int rwsem_is_locked(struct rw_semaphore *sem);
函数的使用不作赘述。
但是要注意:这里的trylock操作与普通信号量的返回方式截然相反:读写信号量被争用返回0,而申请成功返回非0值。
没有必要的时候用普通信号量就足够了。。。