在Linux内核代码中,讯号量被定义成semaphore结构体(代码坐落include/linux/semaphore.h中):
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
这个结构体由三部份组成:
所有插入wait_list等待数组的进程节点由semaphore_waiter结构体表示:
struct semaphore_waiter {
struct list_head list;
struct task_struct *task;
bool up;
};
这个结构体也由三部份组成:
初始化讯号量
讯号量在使用之前要对其进行初始化,通常有两种方式:
DEFINE_SEMAPHORE(sem1);
struct semaphore sem2;
sema_init(&lock2);
第一种方式是用宏直接定义而且初始化一个次序锁变量:
#define __SEMAPHORE_INITIALIZER(name, n)
{
.lock = __RAW_SPIN_LOCK_UNLOCKED((name).lock),
.count = n,
.wait_list = LIST_HEAD_INIT((name).wait_list),
}
#define DEFINE_SEMAPHORE(name)
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
所以红帽子linux,直接通过宏定义初始化就是定义了一个semaphore结构体变量,将其内部的载流子锁lock初始化为未加锁,将表示共享资源数目的count变量初始化为1,将等待进程数组初始化为空数组。count的值为1,表示这个讯号量只能同时由一个进程持有,也就是说被保护的共享资源只能被互斥的访问,这些特殊的讯号量也叫做二值(Binary)讯号量。并且,一般你们用的都是这些二值讯号量,用法如下:
down(&sem);
/* 临界区 */
up(&sem);
第二种方式是自己定义一个semaphore结构体变量,之后调用sema_init函数将其初始化:
static inline void sema_init(struct semaphore *sem, int val)
{
......
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
......
}
最终也是通过__SEMAPHORE_INITIALIZER对讯号量进行的初始化。并且linux系统界面,通过这些方法初始化可以指定count的值,而不像后者默认设置成1。
获取讯号量
要想获取一个讯号量,一般是通过调用down函数:
void down(struct semaphore *sem)
{
unsigned long flags;
/* 获得自旋锁并关中断 */
raw_spin_lock_irqsave(&sem->lock, flags);
/* 如果信号量的count大于0 */
if (likely(sem->count > 0))
/* 直接获得该信号量并将count值递减 */
sem->count--;
else
__down(sem);
/* 释放自旋锁并开中断 */
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(down);
假如讯号量的count值小于0,则直接将其递减之后返回,调用的进程直接获得该讯号量。假如大于或等于0,则接着调用__down函数:
static noinline void __sched __down(struct semaphore *sem)
{
__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
__down函数直接调用了__down_common函数。传入的第二个参数是TASK_UNINTERRUPTIBLE,表示;传入的第三个参数是MAX_SCHEDULE_TIMEOUT,表示会仍然等待该讯号量,直至获得为止,没有到期时间。
不仅最基本的down函数外,还可以通过下边几个函数来获得讯号量:
所有获得讯号量的方式最终都是调用了__down_common函数,只不过传入的第二个和第三个参数不一样。
static noinline int __sched __down_interruptible(struct semaphore *sem)
{
return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
static noinline int __sched __down_killable(struct semaphore *sem)
{
return __down_common(sem, TASK_KILLABLE, MAX_SCHEDULE_TIMEOUT);
}
static noinline int __sched __down_timeout(struct semaphore *sem, long timeout)
{
return __down_common(sem, TASK_UNINTERRUPTIBLE, timeout);
}
我们接着回去看__down_common函数的实现:
static inline int __sched __down_common(struct semaphore *sem, long state,
long timeout)
{
struct semaphore_waiter waiter;
/* 将表示等待进程的节点插入信号量的等待列表中 */
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = current;
waiter.up = false;
for (;;) {
/* 如果有待处理的信号则跳到interrupted标签处 */
if (signal_pending_state(state, current))
goto interrupted;
/* 如果超时了则跳到timed_out标签处 */
if (unlikely(timeout lock);
/* 当前进程睡眠 */
timeout = schedule_timeout(timeout);
/* 再次获得自旋锁 */
raw_spin_lock_irq(&sem->lock);
/* 如果是因为自旋锁释放而被唤醒的则返回0 */
if (waiter.up)
return 0;
}
timed_out:
list_del(&waiter.list);
return -ETIME;
interrupted:
list_del(&waiter.list);
return -EINTR;
}
该函数先将表示等待进程的节点插入讯号量的等待列表中,该节点的task变量指向表示当前进程的task_struct结构体,up变量被初始化为否。之后会步入一个大的循环中,循环的退出条件有三个:
当前进程有待处理的讯号。当前进程等待讯号量超时了。当前进程被另一个释放讯号量的进程唤起。
signal_pending_state函数拿来判定当前进程是否有待处理的讯号(代码坐落include/linux/sched/signal.h中):
static inline int signal_pending_state(long state, struct task_struct *p)
{
if (!(state & (TASK_INTERRUPTIBLE | TASK_WAKEKILL)))
return 0;
if (!signal_pending(p))
return 0;
return (state & TASK_INTERRUPTIBLE) || __fatal_signal_pending(p);
}
所以,当前进程的状态必须包含TASK_INTERRUPTIBLE或TASK_WAKEKILL才行。也就是说,只有通过调用down_interruptible或down_killable函数获得讯号量时,signal_pending_state函数才有可能返回真。
schedule_timeout拿来将当前进程休眠,同时设置到期时间(代码坐落kernel/time/timer.c中):
signed long __sched schedule_timeout(signed long timeout)
{
struct process_timer timer;
unsigned long expire;
switch (timeout)
{
case MAX_SCHEDULE_TIMEOUT:
/* 没有到期时间间隔直接睡眠 */
schedule();
goto out;
default:
if (timeout state = TASK_RUNNING;
goto out;
}
}
/* 计算到期时间 */
expire = timeout + jiffies;
timer.task = current;
/* 设置定时器到期处理函数为process_timeout */
timer_setup_on_stack(&timer.timer, process_timeout, 0);
/* 设置定时器 */
__mod_timer(&timer.timer, expire, 0);
/* 休眠 */
schedule();
/* 删除定时器 */
del_singleshot_timer_sync(&timer.timer);
......
/* 计算距离到期时间还剩多少时间 */
timeout = expire - jiffies;
out:
return timeout < 0 ? 0 : timeout;
}
EXPORT_SYMBOL(schedule_timeout);
假如传入的到期时间间隔被设置成了MAX_SCHEDULE_TIMEOUT,表示当前进程在获得讯号量时没有设置超时linux内核信号量,因而直接调用schedule调度另一个进程执行,本进程步入休眠。假如设置了一个有效的到期时间间隔,这么在调用schedule之前必需要先添加一个定时器,让其在到期时间点被触发。并且,表示定时器本身的结构并不包含由那个进程设置它的信息,所以还须要定义一个新的结构体process_timer:
struct process_timer {
struct timer_list timer;
struct task_struct *task;
};
注意,表示定时器的结构体timer_list是在process_timer结构体中的第一个变量。这样,假如我们有了一个指向表示定时器的timer_list结构体的表针,这么它虽然也是指向process_timer结构体的。通过上面的剖析可以看见,定时器的到期函数被设置成了process_timeout,它的参数就是一个指向timer_list结构体的表针:
static void process_timeout(struct timer_list *t)
{
/* 从timer_list指针获得包含它的process_timer */
struct process_timer *timeout = from_timer(timeout, t, timer);
/* 唤醒到期进程 */
wake_up_process(timeout->task);
}
就是通过指向定时器结构体timer_list的表针获得包含它的process_timer结构体表针,因而获得设置该定时器的进程,之后唤起它。
释放讯号量
要想释放一个讯号量,只能通过调用up函数:
void up(struct semaphore *sem)
{
unsigned long flags;
/* 获得自旋锁并关中断 */
raw_spin_lock_irqsave(&sem->lock, flags);
/* 如果等待进程链表为空 */
if (likely(list_empty(&sem->wait_list)))
/* 直接将count递增 */
sem->count++;
else
__up(sem);
/* 释放自旋锁并开中断 */
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(up);
假如释放的时侯发觉等待进程数组是空的,表示当前没有被的进程在等这个讯号量,直接递增count值就行了;倘若不为空,表示有别的进程在等这个讯号量被释放,这么当前进程须要接着调用__up函数试着将某个等待进程唤起。
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);
}
功能很简单,获得等待数组中的第一个节点linux内核信号量,之后唤起该节点表示的进程。须要把该节点的up数组置为真,让被唤起的进程晓得其是由于讯号量被释放才被唤起的。
使用场景
要使用讯号量,通常要满足以下的一些使用场景或条件: