Chinaunix首页 | 论坛 | 博客
  • 博客访问: 15502657
  • 博文数量: 2005
  • 博客积分: 11986
  • 博客等级: 上将
  • 技术积分: 22535
  • 用 户 组: 普通用户
  • 注册时间: 2007-05-17 13:56
文章分类

全部博文(2005)

文章存档

2014年(2)

2013年(2)

2012年(16)

2011年(66)

2010年(368)

2009年(743)

2008年(491)

2007年(317)

分类: LINUX

2007-07-19 14:47:39

浅析armlinux-sp进程切换栈结构和切换函数__switch_to()

文章来源:http://gliethttp.cublog.cn

接续《浅析put_user之__put_user_1,2,4,8,bad》,分析进程2页内核堆栈sp,为什么就存有我们需要的current数据,:(sp & 0x1fff)地址处;[2.6内核采用1页内核堆栈,主要是因为系统长期运行之后,虽然使用了Buddy算法进行相邻伙伴的合并,但是两个伙伴很可能空闲在不同的free_area[order]上,所以此时对两个连续的页的获取很可能存在诸多困难]
对于用户堆栈的默认大小,我们可以通过ulimit -s指令查看;那么8k内核堆栈之下的空间又是怎么被使用的呢?让我们来粗略看看:
内核对进程的所有操作都是通过current这个变量完成的,那么current又是个什么样子的咚咚呢?
-----------------------------------------------------------------------
1.#define current (get_current())
current是一个宏,它被定义在/include/asm-arm/Current.h文件中,同时定义了get_current()函数如下:
static inline struct task_struct *get_current(void)
{
    register unsigned long sp asm ("sp");
    return (struct task_struct *)(sp & ~0x1fff);

//sp高8k之下的空间为current存储空间,所以一切读、写也就在current的sp高8k之下了
}
-----------------------------------------------------------------------
2.current->addr_limit的创建流程
load_elf_binary()->start_thread()->set_fs(USER_DS);

/include/asm-arm/proc-armv/uaccess.h
#define KERNEL_DS      0x00000000
#define USER_DS        PAGE_OFFSET                        //0xC0000000 3G
static inline void set_fs (mm_segment_t fs)
{
    current->addr_limit = fs;
    modify_domain(DOMAIN_KERNEL, fs ? DOMAIN_CLIENT : DOMAIN_MANAGER);

//设置cp15协处理器的domain为client[用户模式gliethttp]
}
最后就是下面的current的对应的sp是怎么设置的了

include/asm-arm/constants.h中定义了addr_limit的间接引用
-----------------------------------------------------------------------
3.sys_fork_wrapper函数
arch/arm/kernel/entry-common.S
...
sys_fork_wrapper:
        add    r0, sp, #S_OFF                              //在arch/arm/kernel/entry-header.S定义值:#8
        b    SYMBOL_NAME(sys_fork)
sys_execve_wrapper:
        add    r3, sp, #S_OFF
        b    SYMBOL_NAME(sys_execve)
...
5.sys_fork()函数
arch/arm/kernel/Sys_arm.c
asmlinkage int sys_fork(struct pt_regs *regs)
{
    return do_fork(SIGCHLD, regs->ARM_sp, regs, 0);        //传参:regs = r0
}
-----------------------------------------------------------------------
6.do_fork()函数
kernel/Fork.c
do_fork(regs)->copy_thread(regs)
int
copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
     unsigned long unused, struct task_struct * p, struct pt_regs * regs)
{
    struct pt_regs *childregs;
    struct context_save_struct * save;
    atomic_set(&p->thread.refcount, 1);
    childregs = ((struct pt_regs *)((unsigned long)p + 8192 - 8)) - 1;//拷贝到该p指向进程的内核上下文中
    *childregs = *regs;                                    //此处的regs就是由sys_fork传进来的regs
    childregs->ARM_r0 = 0;
    childregs->ARM_sp = esp;
    save = ((struct context_save_struct *)(childregs)) - 1;
    *save = INIT_CSS;                                      //新线程的默认cpsr,r4,r5,r6,r7,r8,r9,sl,fp,pc设置
    save->pc |= (unsigned long)ret_from_fork;              //返回pc值
    p->thread.save = save;                                 //供__switch_to使用
    return 0;
}
-----------------------------------------------------------------------
7.上面函数的p值是由alloc_task_struct()函数创建的
arch/arm/kernel/Process.c
struct task_struct *alloc_task_struct(void)
{
    struct task_struct *tsk;
    if (EXTRA_TASK_STRUCT)
        tsk = task_struct_head;                            //因为at91rm9200为32位处理器,所以执行此句
    else
        tsk = NULL;
    if (tsk) {
        task_struct_head = tsk->next_task;                 

//寻找一个可用task存储空间,task_struct_head最多会同时维持4个
        nr_task_struct -= 1;
    } else
        tsk = ll_alloc_task_struct();                      

//如果task_struct_head已经空了,那么使用__get_free_pages(GFP_KERNEL,1)申请2页[8k]内核空间[gliethttp]
#ifdef CONFIG_SYSRQ
    /*
     * The stack must be cleared if you want SYSRQ-T to
     * give sensible stack usage information
     */

    if (tsk) {
        char *p = (char *)tsk;
        memzero(p+KERNEL_STACK_SIZE, KERNEL_STACK_SIZE);
    }
#endif
    return tsk;
}
对alloc_task_struct()函数的调用在do_fork(),摘录片断:
struct task_struct *p;
...
p = alloc_task_struct();                                   //返回内核专用的task上下文存储空间
 if (!p)
  goto fork_out;
*p = *current;                                             //将current数据转储到task的内核上下文p中
...
SET_LINKS(p);                                              //将task的内核上下文p连接到init_task上,
..,
-----------------------------------------------------------------------
在kernel/Sched.c的schedule(),
...
next = idle_task(this_cpu);                                //其中#define idle_task(cpu) (&init_task)
...
所以shedule中的next和prev都指向init_task链表上的内核空间地址代表的task上下,因此对next和prev地址的读、写操作,不受用户进程切换的影响,
一个新的进程创建时,内核在为它创建数据结构的同时也为它创建系统堆栈,内核shedule程序通过进程的系统堆栈,来完各种调度,实现多进程的换入、换出.
-----------------------------------------------------------------------
8.switch_to()宏定义
#define mb() __asm__ __volatile__ ("" : : : "memory")
#define rmb() mb()
#define wmb() mb()
#define nop() __asm__ __volatile__("mov\tr0,r0\t@ nop\n\t");

#define prepare_to_switch() do { } while(0)
/*
 * switch_to(prev, next) should switch from task `prev' to `next'
 * `prev' will never be the same as `next'.
 * The `mb' is to tell GCC not to cache `current' across this call.
 */

extern struct task_struct *__switch_to(struct task_struct *prev, struct task_struct *next);
#define switch_to(prev,next,last)        \
    do {                                 \
        last = __switch_to(prev,next);   \

//next task和prev task的内核上下文切换,以及current使用的sp恢复[gliethttp]
        mb();                            \
    } while (0)
-----------------------------------------------------------------------
__switch_to定义在arch/arm/kernel/entry-armv.S中
/*
 * Register switch for ARMv3 and ARMv4 processors
 * r0 = previous, r1 = next, return previous.
 * previous and next are guaranteed not to be the same.
 */

ENTRY(__switch_to)                                        

//next task和prev task的内核上下文切换,以及current使用的sp恢复[2007-07-17 gliethttp]
        stmfd     {r4 - sl, fp, lr}                       //将本进程的如上寄存器入本进程栈
        mrs    ip, cpsr                                        

//作为返回时的spsr,"之前状态控制器",参见arm-irq中断模式流程的spsr功用
        str    ip, [sp, #-4]!
        str    sp, [r0, #TSS_SAVE]                    

//将sp存入进程r0内核上下文prev_task->thread.save,而非current->thread.save
        ldr    sp, [r1, #TSS_SAVE]

//读取进程r1内核上下文next_task->thread.save,而非current->thread.save,此后current可用
        ldr    r2, [r1, #TSS_DOMAIN]

//r2 = r1->thread.domain
        ldr    ip, [sp], #4                                    

//获取之前存储的cpsr
        mcr    p15, 0, r2, c3, c0
        msr    spsr, ip
        ldmfd     {r4 - sl, fp, pc}^    

//因为之前已经执行了switch_mm(),用户空间已经可用,所以执行之后,cpu开始运行next进程[gliethttp 2007-07-17]
-----------------------------------------------------------------------
在调用__switch_to之前,linux会先调用switch_mm()进行进程页表切换,当然如果next->mm==0,即next为内核线程切换,那不会调用switch_mm()函数,而是直接使用prev的进程地址空间,next->mm!=0,即next为普通进程切换,使用switch_mm()函数将prev的进程空间换出,换入next进程空间;
/include/asm-arm/Mmu_context.h->switch_mm()->/include/asm-arm/Cpu-single.h->cpu_switch_mm(pgd,tsk)将pgd设置生效

具体操作参见《linux2.4.19下__ioremap函数中remap_area_pages虚拟地址映射建立函数的代码分析》.
所以通过
switch_mm()切换进程空间,
switch_to()切换内核上下文,实现真正的进程换进换出.[gliethttp]
对于sp是如何生成的,那需要从内核启动开始说起,在下一篇《浅析armlinux-sp的孵化流程,1号内核线程init的创建》中再作详细探讨.

阅读(4019) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~