uclinux内核的任务切换

快乐虾

http://blog.csdn.net/lights_joy/

[email protected]

本文适用于

ADSP-BF561

uclinux-2008r1.5-RC3(smp patch)

Visual DSP++ 5.0(update 5)

欢迎转载,但请保留作者信息

内核的任务切换由schedule函数完成,此函数的结构大致为:

asmlinkage void schedule(void)

{

……………………

if (likely(prev != next)) {

next->timestamp = next->last_ran = now;

rq->nr_switches++;

rq->curr = next;

++*switch_count;

prepare_task_switch(rq, next);

prev = context_switch(rq, prev, next);

barrier();

/*

* this_rq must be evaluated again because prev may have moved

* CPUs since it called schedule(), thus the 'rq' on its stack

* frame will be invalid.

*/

finish_task_switch((struct rq *)this_rq(), prev);

} else

spin_unlock_irq(&rq->lock);

…………………….

}

在经过一系列的条件判断后,判断新旧两个任务是否一致,如果不一致则需要将旧任务切换出来,开始执行新任务。在这里prevnext即代表了新旧两个任务的指针。实际的上下文切换将由context_switch完成。

/*

* context_switch - switch to the new MM and the new

* thread's register state.

*/

static /*inline*/ struct task_struct *

context_switch(struct rq *rq, struct task_struct *prev,

struct task_struct *next)

{

struct mm_struct *mm = next->mm;

struct mm_struct *oldmm = prev->active_mm;

/*

* For paravirt, this is coupled with an exit in switch_to to

* combine the page table reload and the switch backend into

* one hypercall.

*/

arch_enter_lazy_cpu_mode();

if (!mm) {

next->active_mm = oldmm;

atomic_inc(&oldmm->mm_count);

enter_lazy_tlb(oldmm, next);

} else

switch_mm(oldmm, mm, next);

if (!prev->mm) {

prev->active_mm = NULL;

WARN_ON(rq->prev_mm);

rq->prev_mm = oldmm;

}

/*

* Since the runqueue lock will be released by the next

* task (which is an invalid locking op but in the case

* of the scheduler it's an obvious special-case), so we

* do an early lockdep release here:

*/

#ifndef __ARCH_WANT_UNLOCKED_CTXSW

spin_release(&rq->lock.dep_map, 1, _THIS_IP_);

#endif

/* Here we just switch the register state and the stack. */

//switch_to(prev, next, prev);

do { \

copy_from_pda(task_info, &task_thread_info(prev)->pda_info); \

copy_to_pda(task_info, &task_thread_info(next)->pda_info); \

(prev) = resume(prev, next); \

} while (0);

return prev;

}

这个函数先切换了mm_struct,然后将PDA,也就是scratch pad sram(4k)的内容保存到老任务的结构体中,再将新任务的PDA复制到scratch pad sram中。最后进行关键的上下文切换resume

resume是一个定义在arch\blackfin\mach-common\entry.S中的函数:

ENTRY(_resume)

/*

* Beware - when entering resume, prev (the current task) is

* in r0, next (the new task) is in r1.

*/

p0 = r0;

p1 = r1;

[--sp] = rets;

[--sp] = fp;

[--sp] = (r7:4, p5:3);

/* save usp */

p2 = usp;

[p0+(TASK_THREAD+THREAD_USP)] = p2;

/* save current kernel stack pointer */

[p0+(TASK_THREAD+THREAD_KSP)] = sp;

/* save program counter */

r1.l = _new_old_task;

r1.h = _new_old_task;

[p0+(TASK_THREAD+THREAD_PC)] = r1;

/* restore the kernel stack pointer */

sp = [p1+(TASK_THREAD+THREAD_KSP)];

/* restore user stack pointer */

p0 = [p1+(TASK_THREAD+THREAD_USP)];

usp = p0;

/* restore pc */

p0 = [p1+(TASK_THREAD+THREAD_PC)];

jump (p0);

/*

* Following code actually lands up in a new (old) task.

*/

_new_old_task:

(r7:4, p5:3) = [sp++];

fp = [sp++];

rets = [sp++];

/*

* When we come out of resume, r0 carries "old" task, becuase we are

* in "new" task.

*/

rts;

ENDPROC(_resume)

这段代码的注释已经很清楚地说明了整个切换过程,比较有意思的是老线程的PC全部指向_new_old_task这个位置。这样,当此线程重新开始执行的时候,它仍然从_new_old_task这个地方开始执行。

那么用户程序是怎么开始执行的呢?答案在copy_thread

每一个用户线程的创建,最后都将调用copy_thread这一内核函数:

int

copy_thread(int nr, unsigned long clone_flags,

unsigned long usp, unsigned long topstk,

struct task_struct *p, struct pt_regs *regs)

{

struct pt_regs *childregs;

childregs = (struct pt_regs *) (task_stack_page(p) + THREAD_SIZE) - 1;

*childregs = *regs;

childregs->r0 = 0;

p->thread.usp = usp;

p->thread.ksp = (unsigned long)childregs;

p->thread.pc = (unsigned long)ret_from_fork;

return 0;

}

注意在这里将PC指向了ret_from_fork,也就是说当这个线程首次被切换进来执行的时候,这个函数将被执行。此函数的定义在arch\blackfin\kernel\entry.S中。

ENTRY(_ret_from_fork)

SP += -12;

call _schedule_tail;

SP += 12;

r0 = [sp + PT_IPEND];

cc = bittst(r0,1);

if cc jump .Lin_kernel;

RESTORE_CONTEXT

rti;

.Lin_kernel:

bitclr(r0,1);

[sp + PT_IPEND] = r0;

/* do a 'fake' RTI by jumping to [RETI]

* to avoid clearing supervisor mode in child

*/

r0 = [sp + PT_PC];

[sp + PT_P0] = r0;

RESTORE_ALL_SYS

jump (p0);

ENDPROC(_ret_from_fork)

注意到这里的rti指令,表示要退出中断的执行。那么要退出什么中断呢?这就需要想想在什么情况下会进行schedule了。

1.1 idle thread中执行切换

从内核启动开始一直执行下来的代码就是一个idle thread,它只在内核态中运行,在初始化完成后演变成一个死循环:

static void noinline __init_refok rest_init(void)

__releases(kernel_lock)

{

………………………….

/*

* The boot idle thread must execute schedule()

* at least one to get things moving:

*/

preempt_enable_no_resched();

schedule();

preempt_disable();

/* Call into cpu_idle with preempt disabled */

cpu_idle();

}

/*

* The idle thread. There's no useful work to be

* done, so just try to conserve power and have a

* low exit latency (ie sit in a loop waiting for

* somebody to say that they'd like to reschedule)

*/

void cpu_idle(void)

{

while (1) {

idle();

preempt_enable_no_resched();

schedule();

preempt_disable();

}

}

当第一次执行schedule时,此时dsp在中断15中运行,且没有加载用户程序,因此调度的只有kernel_initkthreadd这两个内核线程,在kernel_init里面又创建其它的内核线程。在schedule函数中切换到其它内核线程之后,这些线程也在中断15的状态下运行。即便再切换到idle thread,仍然是中断15的状态。这种状态将一直持续到加载第一个用户程序退出到用户态或者被迫接受内核时钟中断并在更高一级的中断状态下运行。

1.2 在内核时钟中断时执行切换

这是调度最多的一个地方,内核时钟每4msCONFIG_HZ = 250)产生一个中断,在这里进行可能的调度。此时有可能有几种情况:

第一种情况是切换到刚加载的用户程序,此时在schedule里面将转而执行_ret_from_fork这一段代码,由于这段代码的后面使用了reti语句,因此DSP将退出时钟中断的执行,进入用户态。

1 参考资料

uclinux第一个用户程序的加载(2009-4-23)

uclinux第一个内核线程的运行(2009-4-23)

uclinux内核线程的创建(2009-4-23)

fork_inituclinux内核的线程数量限制(2009-4-22)

uclinux内核的任务优先级及其load_weight(2009-4-22)

init_thread_union猜想(2009-1-17)

uclinux2.6(bf561)内核中的current_thread_info(2008/5/12)

你可能感兴趣的:(thread,IE,REST,UP,FP)