内核抢占和schedule()函数的分析

时间:2020-12-02 14:37:13

1.线程描述符:

struct thread_info {
struct task_struct *task;
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int saved_preempt_count;
...
};

重要字段解释:
flags: 该字段中有一个位是用于设置内核是否应该被重新调度的标志–need_resched,set_tsk_need_resched()和clear_tsk_need_resched()函数实际上就是对该字段的某一位进行操作(第31位 ?)

saved_preempt_count: 内核此时是否可以进行抢占的标志,为0表示当前进程不持有锁,可以被抢占.大于0则表示此时持有锁,不能被抢占

preempt_counter 字段是32位的, 除了抢占计数器之外还包括其他标志位, 只要 preempt_counter 整体不为0, 就不能进行内核抢占, 这个设计一下子简化了对众多不能抢占的情况的检测: 

Bit 0-7: 抢占计数器, 表示显式禁用内核抢占的次数
Bit 8-15: 软中断计数器, 记录可延迟函数被禁用的次数
Bit 16-27: 硬中断计数器, 表示中断处理程序的嵌套数, irq_enter()递增它, irq_exit()递减它
Bit 28: PREEMPT_ACTIVE 标志, 内核抢占的标志

2.schedule()函数被调度的时机:

1.内核从系统调用或中断处理程序返回时:
1.内核要返回到用户空间时:

如果当前进程的thread_info结构中的need_resched标志被设置,那么就可以调用schedule(),因为内核返回用 
户空间时,它知道自己是安全的,因为既然它可以继续去执行当前进程,那么它当然可以再去选择一个新的进程去执行

2.内核要返回内核空间时:
如果当前进程的thread_info结构体中的need_resched标志被设置而且saved_preempt_count为0时, 通过调用preempt_schedule_irq()函数来间接调用schedule()函数:

asmlinkage void __sched preempt_schedule_irq(void)
{
struct thread_info *ti = current_thread_info();

/* Catch callers which need to be fixed */
BUG_ON(ti->preempt_count || !irqs_disabled());

do {
add_preempt_count(PREEMPT_ACTIVE); //设置thread_info->saved_preempt_count变量的第28位, 即PREEMPT_ACTIVE标志
local_irq_enable();
schedule(); //调度
local_irq_disable();
sub_preempt_count(PREEMPT_ACTIVE);

barrier();
} while (need_resched());
}

此时thread_info->saved_preempt_count中的PREEMPT_COUNT位被设置了, 表示schedule()函数的调用不是进程自己主动调用的,而是被抢占了

3.schedule函数 部分代码:

static void __sched __schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;

need_resched:
preempt_disable(); //禁止抢占
cpu = smp_processor_id(); //获得cpu号
rq = cpu_rq(cpu); //获得cpu上的运行队列
rcu_note_context_switch(cpu);
prev = rq->curr; //获得运行队列上的当前运行进程

schedule_debug(prev);

if (sched_feat(HRTICK))
hrtick_clear(rq);

smp_mb__before_spinlock();
raw_spin_lock_irq(&rq->lock); //关闭本地中断, 并获取本地运行队列的自旋锁

switch_count = &prev->nivcsw;

//1.当进程主动调用schedule函数时, prev->state不为TASK_RUNNING(0),且preempt_count和PREEMPT_ACTIVE都为0, 那么if语句条件满足
//2.当前运行进程是被抢占的, schedule函数是通过preempt_schedule_irq函数间接调用的, 则PREEMPT_ACTIVE会被设置, 则if语句条件不满足. 则被抢占的进程的on_rq还是为1, 会被后面的put_prev_task函数加入红黑树的, 不会失去重新调度的机会
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {


//当前进程的状态为TASK_INTERRUPTIBLE而且该进程有未决的信号未处理, 则if语句条件满足
if (unlikely(signal_pending_state(prev->state, prev))) {
prev->state = TASK_RUNNING;
} else {
deactivate_task(rq, prev, DEQUEUE_SLEEP); //好像此处没做什么事
prev->on_rq = 0; //on_rq为0, 表示该进程此时已经不处于运行状态了, 所以后面的put_prev_task函数不会将其加入红黑树
}
switch_count = &prev->nvcsw;
}

pre_schedule(rq, prev);

if (unlikely(!rq->nr_running)) //如果当前运行队列为空, 则从其他cpu上的运行队列拿来一些进程
idle_balance(cpu, rq);

put_prev_task(rq, prev); //将当前进程加入红黑树中
next = pick_next_task(rq); //挑选出下一个要运行的进程, 并将其设置为当前进程
clear_tsk_need_resched(prev); //清除prev进程的重新调度标志
clear_preempt_need_resched(); //清除需要抢占标志
rq->skip_clock_update = 0;

if (likely(prev != next)) { //选出的进程不和之前的进程一样, 则进行上下文切换
rq->nr_switches++;
rq->curr = next;
++*switch_count;

context_switch(rq, prev, next); // 上下文切换

cpu = smp_processor_id();
rq = cpu_rq(cpu);
} else
raw_spin_unlock_irq(&rq->lock);

post_schedule(rq);

sched_preempt_enable_no_resched(); //打开内核抢占

//如果新选出的进程也有重新调度标志, 则继续调度
if (need_resched())
goto need_resched;
}

4.PREEMPT_ACTIVE位的作用:
考虑这样的代码:

        set_current_state(TASK_UNINTERRUPTBLE);
add_wait_queue(&wait_queue_head, &wait_queue);
if(condition)
break;
schedule();
finish_wait(&wq, &__wait);

如果在set_current_state()函数执行之后,产生了中断,这时如果没有设置PREEMPT_ACTIVE位的话,将会是这样一个执行情况:

    if (prev->state && !preempt_count()) { //preempt_count直接返回的是preempt_count的值, 在设置了PREEMPT_ACTIVE位的情况下, preempt_count()的结果也不为0
if (unlikely(signal_pending_state(prev->state, prev))) {
prev->state = TASK_RUNNING;
} else {
deactivate_task(rq, prev, DEQUEUE_SLEEP);
}

prev->state的状态是TASK_UNINTERUPTIBLE, !preempt_count()的结果也为1, 此时第一个if语句条件满足. 此时进程的状态为TASK_UNINTERRUPTIBLE, 第二个if语句条件不满足, 执行else语句, 此时该进程不会加入红黑树. 而在中断之前, 进程只是把自己的状态设为TASK_UNINTERRUPTIBLE, 还没有将自己加入等待队列. 这样进程不在运行队列中, 同时当等待队列条件满足时, 也不会被该等待队列给唤醒, 因为进程还没有加入到等待队列中, 所以此时进程就再也没有机会运行了

如果设置了PREEMPT_ACTIVE位的情况下, 上述代码将会是这样的执行情况.

内核通过调用preempt_schedule_irq()函数将saved_preempt_count的PREEMPT_ACTIVE位设置为1,此时上述的第一个if语句的条件就不会满足,因为!(preempt_count() & PREEMPT_ACTIVE)的值为0. 所以进程会被加入红黑树, 这样进程就还有被重新调度的机会

总的来说,PREEMPT_ACTIVE位的使用, 就是为了表示此时schedule()函数的调用是否是被进程自己主动调用的还是被内核抢占的. 如果是进程主动调用schedule()函数的, 那么当当前进程的状态不是TASK_RUNNING时, 内核不会把当前进程加入红黑树中. 如果是内核抢占的情况,则会把当前进程加入红黑树.

5.几个与抢占有关的函数:

preempt_disable(): 抢占计数加1, 禁止内核抢占

#define preempt_disable() \
do { \
preempt_count_inc(); \
barrier(); \
} while (0)

#define preempt_count_inc() preempt_count_add(1)
#define preempt_count_add(val) __preempt_count_add(val)

static __always_inline void __preempt_count_add(int val)
{
*preempt_count_ptr() += val;
}

static __always_inline int *preempt_count_ptr(void)
{
return &current_thread_info()->preempt_count;
}

preempt_enable(): 抢占计数减1, 并判断是否可以重新调度

#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \ //将抢占计数减一并检查是否可以重新调度
__preempt_schedule(); \ //如果可以则重新调度, 即抢占计数为0且TIF_NEED_RESCHED标志也已经被设置
} while (0)

#define preempt_count_dec_and_test() __preempt_count_dec_and_test()

static __always_inline bool __preempt_count_dec_and_test(void)
{
return !--*preempt_count_ptr() && tif_need_resched();
}

#define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)

#define test_thread_flag(flag) \
test_ti_thread_flag(current_thread_info(), flag)

static inline int test_ti_thread_flag(struct thread_info *ti, int flag)
{
return test_bit(flag, (unsigned long *)&ti->flags); //测试thread_inof->flags的TIF_NEED_RESCHED位
}

#define __preempt_schedule() preempt_schedule()

asmlinkage void __sched notrace preempt_schedule(void)
{
if (likely(!preemptible()))
return;

do {
__preempt_count_add(PREEMPT_ACTIVE); //设置抢占标志位
__schedule(); //调度
__preempt_count_sub(PREEMPT_ACTIVE);

barrier();
} while (need_resched());
}

从上述两个函数的源代码来看, 抢占计数和调度标志都是对当前进程的thread_info->preempt_count和thread_info->flags变量进行操作的

本文引述自:
http://www.cnblogs.com/openix/archive/2013/03/09/2952041.html
http://blog.chinaunix.net/uid-12461657-id-3353217.html
http://edsionte.com/techblog/archives/3819