相对于自旋锁,信号量的最大特点是允许调用它的线程进入睡眠状态,这意味着试图获得某一信号量的进程会导致对处理器所有权的丧失,也就是出现了进程的切换。
1.信号量定义与初始化
信号量定义如下:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
其中,lock是自旋锁变量,用于实现对信号量count的互斥操作。
无符号整型变量count用于表示通过该信号量允许进入临界区的执行路径的个数。
wait_list用于管理所有在该信号量睡眠的进程,无法获得该信号量的进程将进入睡眠状态。
如果在驱动中使用信号量,那么不要直接通过赋值的形式初始化,而是应该通过sema_init函数初始化该信号量。
static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
#define __SEMAPHORE_INITIALIZER(name, n) \
{ \
.lock = __RAW_SPIN_LOCK_UNLOCKED((name).lock), \
.count = n, \
.wait_list = LIST_HEAD_INIT((name).wait_list), \
}
所以sema_init调用会把信号量sem的lock值设定为解锁状态,count值设定为函数的参数val,同时初始化wait_list链表头。
2.信号量down
信号量上的主要操作就是down和up,Linux内核中对信号量的down操作有:
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_killable(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
int down_timeout(struct semaphore *sem, long jiffies);
驱动中使用的最多的还是down_interruptile函数,下面看下它的实现:
int down_interruptible(struct semaphore *sem)
{
unsigned long flags;
int result = 0;
/*因为这里是临界区资源,所以用自旋锁维护原子操作*/
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
result = __down_interruptible(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
return result;
}
down_interruptible中,首先用自旋锁保护临界区,临界区中,判断count是否大于0,大于0表示能够获得信号量,count - -后直接返回即可,获取失败时调用__down_interruptible:
static noinline int __sched __down_interruptible(struct semaphore *sem)
{
return __down_common(sem, TASK_INTERRUPTIBLE, 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 = false;
for (;;) {
if (signal_pending_state(state, task))
goto interrupted;
if (unlikely(timeout <= 0))
goto timed_out;
__set_task_state(task, state);
raw_spin_unlock_irq(&sem->lock);
timeout = schedule_timeout(timeout);
raw_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;
}
函数在10行,定义一个waiter,并且在12-14行中加入到信号量的waiter链表中,重点看一下16-35行的主体部分:
17-18:用于多信号的处理
21:把当前进程的状态设置为TASK_INTERRUPTIBLE。
23:临时进行自旋锁的解锁,因为进入函数前,我们是处在自旋锁的临界区。
24:因为schedule_timeout,会睡眠,所以必须先临时解锁。
当进程再一次被调度执行时,也就是schedule_timeout开始返回,返回后再上锁,接着根据进程再次被调度的原因进行处理:如果waiter.up不为0,说明进程在信号量sem的wait_list队列中被信号量的UP操作唤醒,进程可以获得信号量,返回0。如果进程是因为被用户空间发送的信号所中断或者是超时引起的唤醒,则返回相应的错误代码。因此对down_interruptible的调用总是应该坚持检查其返回值,判断是获得信号量,还是错误返回。
看完down_interruptible,来看下其它几种的down操作。
void down(struct semaphore *sem)
跟down_interruptible相比,down函数是不可以中断的,意味着调用它的进程如果无法获得信号量,将一直睡眠直到别的进程释放信号量。从用户空间看,如果在down中阻塞了,则没法通过强制组合键结束进程,因此除非必要,否则驱动避免使用down函数。
int down_killable(struct semaphore *sem)
睡眠的进程可以因收到一些致命信号被唤醒而导致获取该信号量的操作被中断,极少使用
int down_trylock(struct semaphore *sem)
进程试图获得信号量,但若无法获得信号量则直接返回1而不进入睡眠状态,返回0意味着函数的调用者已经获得信号量。
int down_timeout(struct semaphore *sem, long timeout)
函数在无法获得信号量的情况下将进入睡眠状态,但是处于这种睡眠状态有时间限制,如果在时间到期前无法获得信号量,则返回一个错误码-ETIME,在到期前进程的睡眠状态为TASK_UNINTERRUPTILE。成功获得信号量返回0。
3.信号量up
信号的up操作,只有一个函数:
void up(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))/*如果等待列表为空,直接把计数值加1*/
sem->count++;
else
__up(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
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 = true;
wake_up_process(waiter->task);
}
up函数中6行判断信号量的等待队列中是否有进程在等待,没有的话,直接count++,返回即可,如果有进程在等待信号量调用__up(sem)。
__up函数首先用list_first_entry取得sem->wait_list链表上的第一个waiter节点C,然后将其从sem->wait_list链表中删除,并且设置waiter->up = 1,最后调用wake_up_process来唤醒waiter C上的进程C。这样进程C将从之前down_interruptile调用中的timeout = schedule_timeout处醒来,waiter->up = 1,down_interruptile返回0,进程c获得信号量。