3.7.4.7. linux唤醒抢占

当在try_to_wake_up和wake_up_process和wanke_up_new_task中唤醒进程时,内核使用全局check_preemp_curr查看是否进程可以抢占当前进程。每个调度器 都会实现check_preempt_cuur, 在全局的check_preempt_cuur会调用进程所调度器类check_preempt_curr进行抢占检查

3.7.4.7.1. linux进程的睡眠

在linux中,仅等待CPU时间的进程称为就绪进程,他们被放置在一个运行队列,一个就绪进程的状态标志为TASK_RUNNING. 一旦一个运行中的进程时间片用完, linux内核的调度器会剥夺这个进程对CPU的控制权,并且从运行队列中选择一个合适的进程投入运行.当然一个进程也可以主动释放CPU的使用权,schedule()是 一个调度函数它可以被一个进程主动调用,从而调度其他进程占用CPU

linux中进程睡眠状态有两种

  1. 一种是可中断的睡眠状态,其状态标志位TASK_INTERRUPTIBLE. 可中断的睡眠状态的进程会睡眠直到某个条件变为真,比如产生一个硬件中断,释放进程正在等待的资源可以唤醒进程的条件

  2. 另一种是不可中断的睡眠状态,其状态标志位TASK_UNINTERRUPTIBLE。与不可中断的睡眠状态区别在于它不响应信号的唤醒

进程一般都是调用schedule的方法进入睡眠状态的,下面的代码演示了如何让正在运行的进程进入睡眠状态

sleeping_task = current;
set_curreent_state(TASK_INTERRUPTIBLE);
schedule();
func1();
....

3.7.4.7.2. linux进程的唤醒

  • wake_up_process

int wake_up_process(struct task_struct *p)
{
    return try_to_wake_up(p, TASK_NORMAL, 0);
}

在调用了wake_up_process以后,这个睡眠进程的状态会被设置为TASK_RUNNING,而且调度器会把它加入到运行队列中去,当然,这个进程只有在下次被调度器调度到的时候才能真正投入运行

  • try_to_wake_up

static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
    unsigned long flags;
    int cpu, success = 0;

    preempt_disable();
    if (p == current) {
        /*
         * We're waking current, this means 'p->on_rq' and 'task_cpu(p)
         * == smp_processor_id()'. Together this means we can special
         * case the whole 'p->on_rq && ttwu_remote()' case below
         * without taking any locks.
         *
         * In particular:
         *  - we rely on Program-Order guarantees for all the ordering,
         *  - we're serialized against set_special_state() by virtue of
         *    it disabling IRQs (this allows not taking ->pi_lock).
         */
        if (!(p->state & state))
            goto out;

        success = 1;
        cpu = task_cpu(p);
        trace_sched_waking(p);
        p->state = TASK_RUNNING;
        trace_sched_wakeup(p);
        goto out;
    }

    /*
     * If we are going to wake up a thread waiting for CONDITION we
     * need to ensure that CONDITION=1 done by the caller can not be
     * reordered with p->state check below. This pairs with mb() in
     * set_current_state() the waiting thread does.
     */
    raw_spin_lock_irqsave(&p->pi_lock, flags);
    smp_mb__after_spinlock();
    if (!(p->state & state))
        goto unlock;

    trace_sched_waking(p);

    /* We're going to change ->state: */
    success = 1;
    cpu = task_cpu(p);

    /*
     * Ensure we load p->on_rq _after_ p->state, otherwise it would
     * be possible to, falsely, observe p->on_rq == 0 and get stuck
     * in smp_cond_load_acquire() below.
     *
     * sched_ttwu_pending()                 try_to_wake_up()
     *   STORE p->on_rq = 1                   LOAD p->state
     *   UNLOCK rq->lock
     *
     * __schedule() (switch to task 'p')
     *   LOCK rq->lock                        smp_rmb();
     *   smp_mb__after_spinlock();
     *   UNLOCK rq->lock
     *
     * [task p]
     *   STORE p->state = UNINTERRUPTIBLE     LOAD p->on_rq
     *
     * Pairs with the LOCK+smp_mb__after_spinlock() on rq->lock in
     * __schedule().  See the comment for smp_mb__after_spinlock().
     */
    smp_rmb();
    if (p->on_rq && ttwu_remote(p, wake_flags))
        goto unlock;

#ifdef CONFIG_SMP
    /*
     * Ensure we load p->on_cpu _after_ p->on_rq, otherwise it would be
     * possible to, falsely, observe p->on_cpu == 0.
     *
     * One must be running (->on_cpu == 1) in order to remove oneself
     * from the runqueue.
     *
     * __schedule() (switch to task 'p')    try_to_wake_up()
     *   STORE p->on_cpu = 1                  LOAD p->on_rq
     *   UNLOCK rq->lock
     *
     * __schedule() (put 'p' to sleep)
     *   LOCK rq->lock                        smp_rmb();
     *   smp_mb__after_spinlock();
     *   STORE p->on_rq = 0                   LOAD p->on_cpu
     *
     * Pairs with the LOCK+smp_mb__after_spinlock() on rq->lock in
     * __schedule().  See the comment for smp_mb__after_spinlock().
     */
    smp_rmb();

    /*
     * If the owning (remote) CPU is still in the middle of schedule() with
     * this task as prev, wait until its done referencing the task.
     *
     * Pairs with the smp_store_release() in finish_task().
     *
     * This ensures that tasks getting woken will be fully ordered against
     * their previous state and preserve Program Order.
     */
    smp_cond_load_acquire(&p->on_cpu, !VAL);

    p->sched_contributes_to_load = !!task_contributes_to_load(p);
    p->state = TASK_WAKING;

    if (p->in_iowait) {
        delayacct_blkio_end(p);
        atomic_dec(&task_rq(p)->nr_iowait);
    }

    cpu = select_task_rq(p, p->wake_cpu, SD_BALANCE_WAKE, wake_flags);
    if (task_cpu(p) != cpu) {
        wake_flags |= WF_MIGRATED;
        psi_ttwu_dequeue(p);
        set_task_cpu(p, cpu);
    }

#else /* CONFIG_SMP */

    if (p->in_iowait) {
        delayacct_blkio_end(p);
        atomic_dec(&task_rq(p)->nr_iowait);
    }

#endif /* CONFIG_SMP */

    ttwu_queue(p, cpu, wake_flags);
unlock:
    raw_spin_unlock_irqrestore(&p->pi_lock, flags);
out:
    if (success)
        ttwu_stat(p, cpu, wake_flags);
    preempt_enable();

    return success;
}
  • wake_up_new_task

之前进入睡眠状态的可以通过try_to_wake_up和wake_up_process完成唤醒,而我们fork新创建的进程在完成自己的创建工作后,可以通过wake_up_new_task完成唤醒工作.

  • check_preempt_cuur

wake_up_new_task中唤醒进程时,内核使用全局check_preempt_cuur检查是否进程可以抢占当前运行的进程

void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
    const struct sched_class *class;

    if (p->sched_class == rq->curr->sched_class) {
        rq->curr->sched_class->check_preempt_curr(rq, p, flags);
    } else {
        for_each_class(class) {
            if (class == rq->curr->sched_class)
                break;
            if (class == p->sched_class) {
                resched_curr(rq);
                break;
            }
        }
    }

    /*
     * A queue event has occurred, and we're going to schedule.  In
     * this case, we can save a useless back to back clock update.
     */
    if (task_on_rq_queued(rq->curr) && test_tsk_need_resched(rq->curr))
        rq_clock_skip_update(rq);
}

3.7.4.7.3. 无效唤醒

几乎在所有的情况下,进程都会在检查了某些条件之后发现条件不满足才进入睡眠,可以有时候进程却会判断条件为真后开始睡眠,如果这样的话进程就会无限期的 休眠下去,这就是所谓的无效唤醒问题

在操作系统中,当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,就会发生竞争条件,这是操作系统中一个典型的问题,无效 唤醒恰恰就是由于竞争条件导致的

A进程

spin_lock(&list_lock);
if(list_empty((&list_head)))
{
        spin_unlock(&list_lock);
        set_current_state(TASK_INTERRUPTIBLE);
        schedule();
        spin_lock(&list_lock);
}
/* rest of the code ... */
spin_unlock(&list_lock);

B进程

spin_lock(&list_lock);
list_add_tail(&list_head, new_node);
spin_unlock(&list_lock);
wake_up_process(A);

这里会出现一个问题,当A进程执行到spin_unlock(&list_lock);之后,B进程被另外一个CPU调度投入运行在这个时间片内B进执行完了它所有的指令,因此它试图唤醒A进程,而此时的A进程还没有 进入睡眠,所以唤醒操作无效。而A进程之后将自己的状态设置为TASK_INTERRUPTIBLE状态,错过了唤醒时机,A进程会无限期的睡眠下去

这样修改以后可以避免无效唤醒的问题了

A进程

set_current_state(TASK_INTERRUPTIBLE);
spin_lock(&list_lock);
if(list_empty(&list_head))
{
    spin_unlock(&list_lock);
    schedule();
    spin_lock(&list_lock);
}
set_current_state(TASK_RUNNING);
/* rest of the code ... */
spin_unlock(&list_lock);

在linux操作系统中,内核的稳定性至关重要,为了避免在linux操作系统内核中出现无效唤醒问题,linux内核在需要进程睡眠的时候应该使用类似如下的操作

/* q 是我们希望睡眠的等待队列 */
DECLARE_WAITQUEUE(wait, current);
add_wait_queue(q, &wait);
set_current_state(TASK_INTERRUPTIBLE);

/* 或TASK_INTERRUPTIBLE */
while(!condition)
schedule();
set_current_state(TASK_RUNNING);
remove_wait_queue(q, &wait);