接口简介
down_timeout接口,在指定时间内获取信号量。
整体流程:
用户尝试获取信号量
- 如果资源充足(count>0),则count自减,代表占用一个资源;
- 如果资源不足(count<=0),则进行睡眠等待;
- 如果直到超时,资源还没有被释放,则返回 -ETIME;
- 如果在等待期间线程被中断,则返回 -EINTR;
- 如果在超时前获取到了资源,则返回0。
代码实现
先看下接口的代码实现:
// kernel/locking/semaphore.c
int down_timeout(struct semaphore *sem, long timeout)
{
unsigned long flags;
int result = 0;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
result = __down_timeout(sem, timeout);
raw_spin_unlock_irqrestore(&sem->lock, flags);
return result;
}
EXPORT_SYMBOL(down_timeout);
总结下就是两句话:
- sem->count > 0 时,sem->count--
- sem->count <= 0 时, 等待资源
两个疑问
1. 如何对sem进行占用
其实获取资源的实现很简单,就是看当前资源是否有余量。
先看下semaphore结构的实现:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
总结起来就是看count值的大小:
大于0,则利用自减代表被当前线程占用一个资源
小于等于0,则等待资源。
2. 为什么else分支没有count--
2.1 else分支存在的问题
我们看上面down_timeout的总结:
- sem->count > 0 sem->count--
- sem->count <= 0 等待资源(好像对sem->count什么都没有做)
这里就比较有意思了,为什么不是等到资源后,保持与if分支一致,对 sem->count--呢?
我们继续深入看下else分支在做什么:
// else分支:
result = __down_timeout(sem, timeout);
// __down_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 (;;) {
if (signal_pending_state(state, current))
goto interrupted;
if (unlikely(timeout <= 0))
goto timed_out;
__set_current_state(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;
}
再上面的流程总结一下:
- 先使用list_add_tail把当前的线程压入等待队列
- 死循环等待schedule_timeout超时
2.2 up接口的实现
我们先把上面的思路压栈,去看下另外一个接口:up
有down去申请资源,那必然要有up去释放资源;
有入队,那肯定也要有出队。
void up(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(up);
上面的流程再总结下:
- 如果队列为空,sem->count++
- 如果队列非空,则调用 __up接口
我们再看__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);
}
总结:
- 取出处在等待队列队首的semaphore_waiter
- 从队列中删除这个semaphore_waiter
- 激活当前进程
回顾上面的__down_common,这里的代码正好跟上面__down_common实现对应上。
因为__down_common是入队去睡眠,__up是出队并激活。
留意这里的激活流程,也并没有sem->count++的操作。所以这里又跟down_timeout的else分支正好对应上。
举个粟子:
故事背景:
现在排队进屋,由于小屋面积有限,只能容纳固定人数(sem->count)。
而你是守门员,每进一个人,你需要把可容纳人数减1(sem->count--)。
-
当人少时
有的时候进屋的人很少,你又想玩游戏,所以不想在门口一直守着。
于是你就把几把钥匙(sem->count把)挂在门外,谁想进来就取一把开门,并把钥匙带进屋,谁要离开就再把钥匙挂回去。 -
当人多时
有的时候准备进屋的人太多,外面已经排起一个很长的队伍,这个时候屋里一直是满员的。
所以此时不应再和之前一样,把钥匙挂出去等别人拿,因为要拿的人就在外面等着呢。
最省事的方法是当有人出去时,把钥匙留在屋里,直接开门叫队伍最前面的人进来就好了。
而此时屋外始终是没有挂钥匙的,所以不会有人乱入,人数也就不会超过sem->count。
结语
看完这个故事,再去通读一遍上面的代码,应该就可以理解 up 与 down 两个接口对else分支的定义了。都不需要处理这把钥匙(sem->count)。
不得不说,开源社区的大佬们,是真的厉害。