linux内核唤醒源,《LINUX3.0内核源代码分析》第三章:内核同步(3)

摘要:本文主要讲述linux如何处理ARM cortex A9多核处理器的内核同步部分。主要包括信号量介绍。

法律声明:《LINUX3.0内核源代码分析》系列文章由谢宝友()发表于LINUX3.0源代码遵循GPL协议。除此以外,文档中的其他内容由作者保留所有版权。谢绝转载。

1.1信号量

1.1.1经典的信号量

信号量也是一种锁,它与自旋锁的区别在于:当资源不可用时,它会使进程挂起,而自旋锁是让等待者忙等。一般情况下,信号量用于进程上下文,自旋锁用于中断上下文。

信号量的结构如下:

/**

*信号量

*/

struct semaphore {

/**

*保护信号量数据结构的信号量

*/

spinlock_tlock;

/**

*可用计数。如果仅仅用于互斥,那么其初值为1;如果用于计数信号量,则大于1.

*/

unsigned intcount;

/**

*等待获得此信号量的进程列表。

*/

struct list_headwait_list;

};

可以看到,linux3.0中的信号量数据结构与linux2.6.11相比,有了非常大的变化。总的来说,变得很清晰了。

获得信号量的原语是:

/**

*获得信号量,如果信号量不可用,则睡眠。

*根据代码注释,不推荐直接使用此函数,而应当使用down_interruptible和down_killable。

*直接使用此函数会导致进程处于"D"状态,不能被kill掉。

*/

void down(struct semaphore *sem)

{

unsigned long flags;

/**

*获得保护此信号量的自旋锁,并关中断。

*这里需要关中断,因为有另外一个down_trylock的原语能够在中断里面使用。如果不关中断,则在中断中调用down_trylock用形成自旋锁死锁。

*/

spin_lock_irqsave(&sem->lock, flags);

if (likely(sem->count > 0))/*自旋锁处于可用状态。*/

sem->count--;/*将信号量可用计数减1后直接返回,表示信号量被当前进程获得的事实*/

else

__down(sem);/*信号量不可用,调用__down进入慢速处理流程。*/

spin_unlock_irqrestore(&sem->lock, flags);/*释放自旋锁*/

}

当信号量不可用时,调用__down将当前进程挂起:

static noinline void __sched __down(struct semaphore *sem)

{

/**

*请注意TASK_UNINTERRUPTIBLE标志,它表示将任务置为不可中断状态。这里down原语不被推荐使用的原因。

* MAX_SCHEDULE_TIMEOUT表示没有超时,进程会一直等待。

*/

__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 (;;) {/*这里使用一个死循环,是考虑到等待过程可能被信号打断,导致睡眠时间不足。这主要是用于down_timeout */

/**

*如果是TASK_INTERRUPTIBLE或TASK_WAKEKILL状态,并且有信号将进程唤醒,则将当前进程从等待队列中取出并退出。

*/

if (signal_pending_state(state, task))

goto interrupted;

if (timeout <= 0)/*进程不是被信号唤醒的,而是等待的超时时间到达,则向上层返回超时错误。*/

goto timed_out;

/**

*将进程设置为指定的状态,并调度出去。

*/

__set_task_state(task, state);

/**

*在调度出去之前,需要将自旋锁释放。

*/

spin_unlock_irq(&sem->lock);

/**

*根据调用者指定的睡眠时间,调用schedule_timeout。

*如果由于信号等原因导致进程过早的被唤醒,那么timeout就是还需要继续睡眠的时间。

*/

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;

}

释放信号量的函数是up:

/**

*释放信号量

*注意,信号量不仅可以用于互斥,也可以用于同步。

*也就是说,某个进程即使没有调用down,也可以直接调用up唤醒等待的任务。

*也可以在中断上下文使用。

*/

void up(struct semaphore *sem)

{

unsigned long flags;

spin_lock_irqsave(&sem->lock, flags);/*获得保护信号量的自旋锁。*/

if (likely(list_empty(&sem->wait_list)))/*没有进程在等待此信号量*/

sem->count++;/*直接将信号量计数加1.注意,这与vxworks的二进制信号量的不同*/

else

__up(sem);/*唤醒等待队列上的任务*/

spin_unlock_irqrestore(&sem->lock, flags);/*释放自旋锁*/

}

/**

*唤醒等待队列上的任务。

*这个实现已经与linux2.6.11有了非常大的变化。不再考虑一次唤醒多个任务的情况。只唤醒队列头上的任务。

*这样的实现简单,但是不支持按优先级排队。影响系统实时性。这是因为实时信号量可以使用rt_mutex。

*/

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);

/**

*设置up标志,表示任务是被信号量唤醒的。

*而不是被信号或者定时器唤醒的。

*/

waiter->up = 1;

/**

*唤醒等待任务。

*/

wake_up_process(waiter->task);

}

除了down以外,linux还提供了以下原语供使用:

函数名

功能

备注

down

获取信号量,不可被信号唤醒

不推荐使用

down_interruptible

获取信号量,但可被信号唤醒

down_killable

获取信号量,但可被致命信号唤醒

down_trylock

获取信号量,如果信号量不可用,则返回失败而不等待

可用于中断上下文

down_timeout

获取信号量,如果在指定的时间内,任务没有被信号量唤醒,则返回失败

1.1.2Rt-mutex

根据我们的分析,普通信号量的实现很简单。在信号量不可用时,直接将当前进程挂入信号量的等待队列末尾。而没有按等待任务的优先级进行排除。

如果获得信号量的任务被抢占,导致信号量迟迟不能释放,那么等待的任务就会一直被阻塞。即使等待的任务优先级再高也没有用。这在实时系统中是不可容忍的。这是典型的“优先级反转”。要解决这个问题,需要信号量实现优先级可继承。业界典型的实现有一级优先级可继承和二级优先级可继承。

优先级反转问题曾经引起火星探测的故障。有兴趣的同学可以深入了解一下。

Linux3.0在rtmutex.c中实现了另外一种互斥信号量,这种信号量实现了优先级继承协议,仅仅能够用于互斥而不能象信号量一样也用于同步。

详细的文档请参见documentation/rt-mutex.txt和documentation/rt-mutex-design.txt。

1.1.3completionlinux还实现了另外一种类似于信号量的东东:completion。从代码上来看,它可以用于互斥和同步。但是在linux中,几乎都是将它用于同步。

它的实现方法和信号量非常相似,同时保留两套接口,可能是出于兼容的原因。

Completion数据结构的声明是:

struct completion {

/**

*相当于信号量中的计数变量。

*当done大于0时,表示任务已经完成。

*/

unsigned int done;

/**

*等待事件完成的队列。

*/

wait_queue_head_t wait;

};

等待事件完成的函数是wait_for_completion:

void __sched wait_for_completion(struct completion *x)

{

wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);

}

其中wait_for_common是一个通用函数,除wait_for_completion外,其他几个等待completion完成的函数也是对它的封装函数。

/**

*等待completion事件完成的通用函数。

*completion:要等待的完成变量

*timeout:超时时间,以tick为单位。MAX_SCHEDULE_TIMEOUT表示永远等待。

*state:当需要阻塞时,将进程设置为何种状态。

*/

static long __sched

wait_for_common(struct completion *x, long timeout, int state)

{

might_sleep();/*这个调试函数用于通知调用者,本函数可能睡眠,如果在不可睡眠的上下文(如中断上下文或关抢占的上下文)调用了此函数,则系统会产生警告*/

/**

*在关中断状态下,是不应当调用本函数的。

*因此,可以确保此时系统是开中断的,因此也就可用spin_lock_irq来强开强关中断了。

*/

spin_lock_irq(&x->wait.lock);

/**

*如果事件还没有完成,那么就将当前任务挂到等待队列并挂起。

*/

timeout = do_wait_for_common(x, timeout, state);

/**

*释放自旋锁并强开中断。

*/

spin_unlock_irq(&x->wait.lock);

return timeout;

}

/**

*等待事件完成。

*需要注意本函数的返回值:0表示超时,大于0表示成功。小于0表示需要重新执行系统调用。

*/

static inline long __sched

do_wait_for_common(struct completion *x, long timeout, int state)

{

if (!x->done) {/*事件还没有完成,进入等待流程*/

DECLARE_WAITQUEUE(wait, current);/*定义一个等待节点,并将当前任务设置为等待任务*/

/**

*将当前任务加入到等待队列中。

*/

__add_wait_queue_tail_exclusive(&x->wait, &wait);

do {/*循环等待,直到事件完成或者被信号唤醒或者超时*/

if (signal_pending_state(state, current)) {/*被信号唤醒了*/

timeout = -ERESTARTSYS;/*返回ERESTARTSYS,表示需要重新执行系统调用。*/

break;

}

/*设置任务状态,必须在开中断之前执行,否则可能会使中断中唤醒任务的操作失效。请读者在熟悉调度以后慢慢领会。*/

__set_current_state(state);

/**

*释放自旋锁并开中断。

*/

spin_unlock_irq(&x->wait.lock);

/**

*调度出去,等待被唤醒。

*/

timeout = schedule_timeout(timeout);

/**

*被唤醒后,需要对completion的状态进行判断,必须再次获得自旋锁。

*/

spin_lock_irq(&x->wait.lock);

} while (!x->done && timeout);/*如果被事件唤醒或者超时时间到,则退出*/

/**

*无论如何,都需要将任务从等待队列中取出。

*/

__remove_wait_queue(&x->wait, &wait);

if (!x->done)/*事件还没有完成,如果是被信号打断,则返回ERESTARTSYS,否则返回0表示超时。*/

return timeout;

}

/**

*运行到这里,说明事件已经完成,则递减done。

*/

x->done--;

/**

*请注意返回值,这里确保不会返回0值。因为0值表示超时。

*/

return timeout ?: 1;

}

当事件完成时,调用complete通知等待队列上的任务。

/**

*完成事件,并唤醒等待的任务。

*/

void complete(struct completion *x)

{

unsigned long flags;

/**

*获得等待队列的自旋锁并关中断

*注意,这里不是强关强开中断。因为可以在关中断上下文(如果中断上下文)中调用此函数。不能简单的强开中断。

*/

spin_lock_irqsave(&x->wait.lock, flags);

x->done++;/*增加计数,表示事件已经完成。*/

__wake_up_common(&x->wait, TASK_NORMAL, 1, 0, NULL);/*唤醒等待队列上的任务。请注意:只唤醒了第一个任务*/

spin_unlock_irqrestore(&x->wait.lock, flags);

}

系统还提供以下相关函数:

函数名

作用

备注

complete_all

唤醒等待队列上的所有任务

wait_for_completion_timeout

等待事件完成,直到预定的时间到达

wait_for_completion_interruptible

等待事件完成,可以被信号打断

wait_for_completion_interruptible_timeout

等待事件完成,直到预定的时间到达,或者被信号打断

wait_for_completion_killable

等待事件完成,可以被致命信号打断

wait_for_completion_killable_timeout

等待事件完成,直到预定的时间到达,或者被致命信号打断

try_wait_for_completion

等待事件完成,如果事件没有完成,则返回错误信息并直接返回,不等待。

completion_done

判断事件是否完成

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值