浅析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的创建》中再作详细探讨.
|