信号量

信号量

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值。

没有必要的时候用普通信号量就足够了。。。

posted @ 2020-05-18 17:24  宅胖儿  阅读(416)  评论(0编辑  收藏  举报