Chinaunix首页 | 论坛 | 博客
  • 博客访问: 163643
  • 博文数量: 37
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 15
  • 用 户 组: 普通用户
  • 注册时间: 2016-01-30 09:54
文章分类
文章存档

2018年(2)

2017年(14)

2016年(21)

我的朋友

分类: LINUX

2016-05-24 16:53:36

1、概念
Linux中有3种栈:
1)用户栈。当进程处于用户态时使用,位于进程地址空间(用户态部分(如:0-0xc0000000))底部,用户态分配局部变量和函数调用时时,使用该栈,跟平时我们见到和理解的一样,就是虚拟地址空间中的一段。
2)内核栈。跟用户栈独立,属于进程,即每个进程都有自己的内核栈,单独分配,大小为8k,跟thread_info结构放在一起,在用户态和内核态切换时,需要进行切换。
3)中断栈。老版本内核中默认认跟内核栈共享,新版本内核中与内核栈独立,且软中断和硬中断单独使用自己的中断栈。中断、异常、软中断使用此栈。    
本文主要讲解内核栈、用户栈和内核栈切换的相关实现。

2、实现
1)内核栈定义和实现
内核栈跟thread_info结构放在一起,共用一个union:thread_union,
union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};
2)其它与内核栈相关的定义
a、task_struct.thread.sp0
任务描述符(task_struct)中的thread成员(thread_struct)用于保存进程上下文信息,包含主要寄存器信息,在进程上下文切换(如调度)时使用,thread成员定义为thread_struct结构体,其中的sp0成员用于保存内核栈指针。
b、task_struct.stack
任务描述符(task_struct)中的stack成员是新版本内核中新加入的成员,同样指向内核栈顶,用于替代老版本中的thread_info成员,由于thread_info和内核栈实际是放在一起的,共享同一个联合体thread_union,可以相互转换,所以该修改并无本质区别。

点击(此处)折叠或打开

  1. /*进程描述符,每个进程(线程)都由此结构描述*/
  2. struct task_struct {
  3. ...
  4. /*Fixme:新内核版本中去除了thread_info成员,用这个代替(thread_info和内核栈放在一起)*/
  5. void *stack;
  6. ...
  7. /*进程上下文,包含主要寄存器信息,在进程上下文切换时使用*/
  8. struct thread_struct thread;
  9. ...
  10. }

点击(此处)折叠或打开

  1. struct thread_struct {
  2. ...
  3. /*内核堆栈(与thread_info放在一起,共享thread_union联合体,大小为8k)的指针。TSS中有相应的字段,在特权级发生变化时,硬件会自动从TSS中读取sp0,并进行堆栈切换。*/
  4. unsigned long    sp0;
  5. ...
  6. }

c、kernel_stack per-CPU变量
用于指向当前CPU上运行的进程的内核栈,由于内核栈与thread_info是放在一起的,所以,内核中也用这个变量来获取当前进程的thread_info:

点击(此处)折叠或打开

  1. /*
  2.   * 取当前进程的thread_info,该信息与内核栈(或中断栈)公用联合体(thread_union或irq_ctx),
  3.   * 且作为per-CPU变量(kernel_stack)放在了指定区域。
  4.   */
  5. static inline struct thread_info *current_thread_info(void)
  6. {
  7. struct thread_info *ti;
  8. ti = (void *)(this_cpu_read_stable(kernel_stack) +
  9.      KERNEL_STACK_OFFSET - THREAD_SIZE);
  10. return ti;
  11. }

d、tss_struct.sp0

TSS任务状态段是X86架构中包含的一个特殊的段,用户保存硬件上下文,包含了当前进程的特权级(ring)信息和寄存器信息。在进程切换时使用,Linux中使用的情况比较少,而用户栈和内核栈的切换就是其中一处关键的应用。Linux内核定义了tss_struct结构体来描述该段中的内容,其中的x86_tss(x86_hw_tss结构)中保存了相应的硬件状态信息,其中sp0即为内核态(ring0)中的堆栈指针,ss0为内核态堆栈段寄存器,sp为用户态(ring3)堆栈指针,Linux中主要使用了这几个字段。

点击(此处)折叠或打开

  1. /*用于描述TSS段中的内容*/
  2. struct tss_struct {
  3. /*
  4. * The hardware state:
  5. */
  6. /*硬件状态信息*/
  7. struct x86_hw_tss    x86_tss;


  8. /*
  9. * The extra 1 is there because the CPU will access an
  10. * additional byte beyond the end of the IO permission
  11. * bitmap. The extra byte must be all 1 bits, and must
  12. * be within the limit.
  13. */
  14. /*IO权位图*/
  15. unsigned long    io_bitmap[IO_BITMAP_LONGS + 1];


  16. /*
  17. * .. and then another 0x100 bytes for the emergency kernel stack:
  18. */
  19. /*备用内核栈*/
  20. unsigned long    stack[64];


  21. } ____cacheline_aligned;

点击(此处)折叠或打开

  1. /* This is the TSS defined by the hardware. */
  2. struct x86_hw_tss {
  3. unsigned short    back_link, __blh;
  4. unsigned long    sp0; /*内核栈指针*/
  5. unsigned short    ss0, __ss0h; /*内核栈段描述符*/
  6. unsigned long    sp1;
  7. /* ss1 caches MSR_IA32_SYSENTER_CS: */
  8. unsigned short    ss1, __ss1h;
  9. unsigned long    sp2;
  10. unsigned short    ss2, __ss2h;
  11. unsigned long    __cr3;
  12. unsigned long    ip;
  13. unsigned long    flags;
  14. unsigned long    ax;
  15. unsigned long    cx;
  16. unsigned long    dx;
  17. unsigned long    bx;
  18. unsigned long    sp; /*用户态栈指针*/
  19. unsigned long    bp;
  20. unsigned long    si;
  21. unsigned long    di;
  22. unsigned short    es, __esh;
  23. unsigned short    cs, __csh;
  24. unsigned short    ss, __ssh;
  25. unsigned short    ds, __dsh;
  26. unsigned short    fs, __fsh;
  27. unsigned short    gs, __gsh;
  28. unsigned short    ldt, __ldth;
  29. unsigned short    trace;
  30. unsigned short    io_bitmap_base;


  31. } __attribute__((packed))

内核中定义针对每个CPU定义了一个了tss_struct结构体类型变量init_tss,在进程上下文切换(堆栈切换)时使用

点击(此处)折叠或打开

  1. /*
  2.  * per-CPU TSS segments. Threads are completely 'soft' on Linux,
  3.  * no more per-task TSS's. The TSS size is kept cacheline-aligned
  4.  * so they are allowed to end up in the .data..cacheline_aligned
  5.  * section. Since TSS's are completely CPU-local, we want them
  6.  * on exact cacheline boundaries, to eliminate cacheline ping-pong.
  7.  */
  8. DEFINE_PER_CPU_SHARED_ALIGNED(struct tss_struct, init_tss) = INIT_TSS;

INIT_TSS定义如下:

点击(此处)折叠或打开

  1. /*
  2.  * Note that the .io_bitmap member must be extra-big. This is because
  3.  * the CPU will access an additional byte beyond the end of the IO
  4.  * permission bitmap. The extra byte must be all 1 bits, and must
  5.  * be within the limit.
  6.  */
  7. #define INIT_TSS {     \
  8. .x86_tss = {     \
  9. .sp0    = sizeof(init_stack) + (long)&init_stack, \
  10. .ss0    = __KERNEL_DS,     \
  11. .ss1    = __KERNEL_CS,     \
  12. .io_bitmap_base    = INVALID_IO_BITMAP_OFFSET,     \
  13. },     \
  14. .io_bitmap    = { [0 ... IO_BITMAP_LONGS] = ~0 },     \
  15. }
init_stack定义为:

点击(此处)折叠或打开

  1. #define init_stack    (init_thread_union.stack)
即将内核栈(sp0)指向了init_thread_union的内核栈顶。init_thread_union为初始的任务描述符:

点击(此处)折叠或打开

  1. union thread_union init_thread_union __init_task_data =
  2. { INIT_THREAD_INFO(init_task) };
  3. struct task_struct init_task = INIT_TASK(init_task);
  4. #define INIT_TASK(tsk)    \
  5. {    \
  6. .state    = 0,    \
  7. .stack    = &init_thread_info,    \
  8. .usage    = ATOMIC_INIT(2),    \
  9. .flags    = PF_KTHREAD,    \
  10. .prio    = MAX_PRIO-20,    \
  11. .static_prio    = MAX_PRIO-20,    \
  12. .normal_prio    = MAX_PRIO-20,    \
  13. .policy    = SCHED_NORMAL,    \
  14. .cpus_allowed    = CPU_MASK_ALL,    \
  15. .nr_cpus_allowed= NR_CPUS,    \
  16. .mm    = NULL,
  17. ...
  18. }

3)内核栈分配
内核在何时分配?
答案是在fork时。
Linux中进程均由fork创建(init除外),在fork时即会创建相应的内核栈,相应流程为:
do_fork->copy_process->dup_task_struct->alloc_thread_info_node

点击(此处)折叠或打开

  1. /*分配thread_info,即分配内核栈*/
  2. static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,
  3.  int node)
  4. {
  5. /*THREAD_SIZE_ORDER为1,即2页,即内核栈和thread_info共用的空间大小为8k*/
  6. struct page *page = alloc_pages_node(node, THREADINFO_GFP_ACCOUNTED,
  7.     THREAD_SIZE_ORDER);


  8. return page ? page_address(page) : NULL;
  9. }

点击(此处)折叠或打开

  1. static struct task_struct *dup_task_struct(struct task_struct *orig)
  2. {
  3. ...
  4. /*分配thread_info,即分配内核栈*/
  5. ti = alloc_thread_info_node(tsk, node);
  6. ...
  7. /*设置新进程的内核栈(stack成员指向)为新分配的thread_info,如此即设置好了内核栈*/
  8. tsk->stack = ti;
  9. ...
  10. }
4)用户栈和内核栈切换
我们知道,x86硬件结构中,SS为堆栈段寄存器,ESP为堆栈指针寄存器,而这两个寄存器在每个CPU上都是唯一的,但是Linux中有多种栈(用户栈、内核栈、中断栈),如果都需要使用的话,那就必须在各个栈之间进行切换。
当CPU运行的特权级(ring)发生变化时,就需要切换相应的堆栈,Linux中,只使用了两个ring:ring0(内核态)和ring3(用户态),当发生内核态和用户态间的状态切换时,需要考虑堆栈的切换,通常的切换时机有:系统调用、中断和异常及其返回处。
前面我们已经了解了内核栈的概念和定义,那么如何进行堆栈切换呢?那就需要使用上面描述的TSS段了,如之前所述,tss_struct中包括了内核栈指针sp0和内核堆栈段寄存器ss0。

(1)用户栈到内核栈的切换
X86硬件结构中,中断、异常和系统调用都是通过中断门或陷阱门实现的,在通过中断门或陷阱门时,硬件会自动利用TSS,完成堆栈切换的工作。硬件完成的操作包括:
  a、找到ISR的入口。具体包括:确定中断或异常相关的中断向量、读取IDTR寄存器获取IDT表地址、从IDT表中读取中断向量对应的项、读取GDTR寄存器获取GDT表地址,在GDT表中查找IDT表项中的段选择符标识的段描述符,该描述符中指定了中断/异常处理程序(ISR)所在的段基址,结合IDT表项中的段偏移地址,即可找到ISR的入口地址。
  b、权限检查。确认中断的来源是否合法,主要比对当前特权级(CS寄存器的低两位)和段描述符(GDT中)的DPL比较。如果CPL小于DPL,就产生GP(通用保护)异常。对于异常,还需做进一步的安全检查:比对CPL与IDT中的门描述符的DPL,如果DPL小于CPL,也产生GP(通用保护)异常,由此可以避免用户应用程序访问特殊的陷阱门和中断门。 
  c、检查特权级的变化。如果ring发生了变化(通常是从ring3到ring0,即用户态切换到内核态),即CPL与段描述符的DPL不同,则进行一下处理(这里实际上进行了堆栈切换): 
    c1. 读tr寄存器,获取TSS段。 
    c2. 读取TSS中的新特权级(内核态)的堆栈段和堆栈指针,将其load到SS和ESP寄存器。 
    c3. 在新特权级的栈(内核栈)中保存原始的(即用户态的)SS和ESP的值。
  d. 如果发生的是异常,则将引起异常的指令地址装载cs和eip寄存器,如此可以使这条指令在异常处理程序执行完后能被再次执行,这也是中断和异常的主要区别之一;如果发生的中断,则跳过此步骤。
  e. 在新栈(内核栈)中压入eflag、cs和eip。 
  f. 如果是异常且产生了硬件出错码,则将它压入栈中。 
  g. 用之前通过IDT和GDT获取到的ISR的入口地址和段选择符装载EIP和CS寄存器,如此即可开始执行ISR。 
再次强调:上述操作均由硬件自动完成。从上述的步骤c可以看出,当用户态切换到内核态时,用户栈切换到内核栈实质是由硬件自动完成的,软件需要做的是预先设置好TSS中的相关内容(比如sp0和ss0)。

(2)内核栈到用户栈的切换
该过程实际是“用户栈到内核栈切换”的逆过程,发生时机在系统调用、中断和异常的返回处,实际的切换过程还是由硬件自动完成的。
中断或异常返回时,必然会执行iret指令,然后将控制器交回给之前被中断打断的进程,硬件自动完成如下操作:
  a. 从当前栈(内核栈)中弹出cs、eip和eflag,并load到相应的寄存器中寄存器。(如之前有硬件错误码入栈,需要先弹出这个错误码)。 
  b. 权限检查。比对ISR的CPL是否等于cs中的低两位的值。如果是,iret终止返回;否则,转入下一步。 
  c. 从当前栈(内核栈)中弹出之前压入的用户态堆栈相关的ss和esp,并load到相应寄存器,至此,即完成了从内核栈到用户栈的切换。 
  d. 后续处理。主要包括:检查ds、es、fs及gs段寄存器,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相关的段寄存器。目的是为了防止用户态的程序利用内核以前所用的段寄存器,以防止恶意用户程序利用其访问内核地址空间。 

(3)进程上下文切换时的堆栈切换
内核调度时,当被选中的next进程不是current进程时,会发生上下文切换,此时也必然会涉及堆栈切换。这里涉及到两个相关的问题:
a、从current进程的堆栈切换到next进程的堆栈
由于调度肯定发生在内核态,那么进程上下文切换时,也必然处于内核态,那么此时的堆栈切换实质为current进程的内核栈到next进程内核栈的切换。相应工作在switch_to宏中用汇编实现,通过next进程的栈指针(内核态)装载到ESP寄存器中实现。
    "movl %[next_sp],%%esp\n\t" /* restore ESP   */ \
具体代码如下:

点击(此处)折叠或打开

  1. /*
  2.   * 上下文切换,在schedule中调用,current进程调度出去,当该进程被再次调度到时,重新从__switch_to后面开始执行
  3.   * prev:被替换的进程
  4.   * next:被调度的新进程
  5.   * last:当切换回原来的进程(prev)后,被替换的另外一个进程。
  6.   */
  7. #define switch_to(prev, next, last)    \
  8. do {    \
  9. /*    \
  10. * Context-switching clobbers all registers, so we clobber    \
  11. * them explicitly, via unused output variables.    \
  12. * (EAX and EBP is not listed because EBP is saved/restored    \
  13. * explicitly for wchan access and EAX is the return value of    \
  14. * __switch_to())    \
  15. */    \
  16. unsigned long ebx, ecx, edx, esi, edi;    \
  17. \
  18. asm volatile("pushfl\n\t"    /* save flags */    /*将eflags寄存器值压栈*/\
  19.     "pushl %%ebp\n\t"    /* save EBP */    /*将EBP压栈*/\
  20. /*将当前栈指针(内核态)保存到prev进程的thread.sp中*/
  21.     "movl %%esp,%[prev_sp]\n\t"    /* save ESP */ \
  22.     /*将next进程的栈指针(内核态)装载到ESP寄存器中*/
  23.     "movl %[next_sp],%%esp\n\t"    /* restore ESP */ \
  24.     /*保存"标号1"的地址到prev进程的thread.ip,以便当prev进程重新被调度运行时,可以从"标号1处"重新开始执行*/
  25.     "movl $1f,%[prev_ip]\n\t"    /* save EIP */    \
  26.     /*
  27.          * 将next进程的IP(通常都是"标号1"的地址,因为通常都是经历过这里的调度过程的,上一行代码中即保存了这个IP)
  28.    * 压入当前的(即next进程的)堆栈中。结合后面的jmp指令(注意:不是call指令)一起理解,当__switch_to执行完ret返回时,
  29.    * 会自动从当前的堆栈中弹出该地址作为函数的返回地址接着执行,如此即可实现新进程的运行。
  30.                        */
  31.     "pushl %[next_ip]\n\t"    /* restore EIP */    \
  32.     __switch_canary    \
  33.     /*
  34.          *jmp到__switch_to函数执行,当此函数返回时,自动跳转到[next_ip]开始执行,实现新进程的调度。注意不是call,jmp指令
  35.          * 不会自动将当前地址压栈,call会自动压栈
  36.          */
  37.     "jmp __switch_to\n"    /* regparm call */    \
  38.     /*当prev进程再次被调度到时,从这里开始执行*/
  39.     "1:\t"    \
  40.     /*恢复EBP*/
  41.     "popl %%ebp\n\t"    /* restore EBP */    \
  42.     /*恢复eflags*/
  43.     "popfl\n"    /* restore flags */    \
  44. \
  45.     /* output parameters */    \
  46.     : [prev_sp] "=m" (prev->thread.sp),    \
  47.       [prev_ip] "=m" (prev->thread.ip),    \
  48.       "=a" (last),    \
  49. \
  50.       /* clobbered output registers: */    \
  51.       "=b" (ebx), "=c" (ecx), "=d" (edx),    \
  52.       "=S" (esi), "=D" (edi)    \
  53.       \
  54.       __switch_canary_oparam    \
  55. \
  56.       /* input parameters: */    \
  57.     : [next_sp] "m" (next->thread.sp),    \
  58.       [next_ip] "m" (next->thread.ip),    \
  59.       \
  60.       /* regparm parameters for __switch_to(): */    \
  61.       [prev] "a" (prev),    \
  62.       [next] "d" (next)    \
  63. \
  64.       __switch_canary_iparam    \
  65. \
  66.     : /* reloaded segment registers */    \
  67. "memory");    \
  68. } while (0)

b、TSS中内核栈(sp0)的切换
由于Linux的具体实现中,TSS不是针对每进程,而是针对每CPU的,即每个CPU对应一个tss_struct,那在进程上下文切换时,需要考虑当前CPU上TSS中的内容的更新,其实就是内核栈指针的更新,更新后,当新进程再次进入到内核态执行时,才能确保CPU硬件能从TSS中自动读取到正确的内核栈指针(sp0)的值,以保证从用户态切换到内核态时,相应的堆栈切换正常。
相应的切换在__switch_to中由load_sp0函数完成,

点击(此处)折叠或打开

  1. __notrace_funcgraph struct task_struct *
  2. __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
  3. {
  4. ...
  5. struct tss_struct *tss = &per_cpu(init_tss, cpu);
  6. ...
  7. /*
  8. * Reload esp0.
  9. */
  10. /*将next进程的内核栈指针(next->thread->sp0)值更新到当前CPU的TSS中*/
  11. load_sp0(tss, next);
  12. ...
  13. }

点击(此处)折叠或打开

  1. static inline void load_sp0(struct tss_struct *tss,
  2.    struct thread_struct *thread)
  3. {
  4. native_load_sp0(tss, thread);
  5. }

点击(此处)折叠或打开

  1. static inline void
  2. native_load_sp0(struct tss_struct *tss, struct thread_struct *thread)
  3. {
  4. /*将thread中的内核栈指针赋给TSS中的相应字段*/
  5. tss->x86_tss.sp0 = thread->sp0;
  6. #ifdef CONFIG_X86_32
  7. /* Only happens when SEP is enabled, no need to test "SEP"arately: */
  8. if (unlikely(tss->x86_tss.ss1 != thread->sysenter_cs)) {
  9. tss->x86_tss.ss1 = thread->sysenter_cs;
  10. wrmsr(MSR_IA32_SYSENTER_CS, thread->sysenter_cs, 0);
  11. }
  12. #endif
  13. }

(4)TSS初始化
如前面描述,Linux内核中使用tss_struct来描述TSS,那么硬件上的TSS如何跟内核中的tss_struct关联起来的呢?
答案是在内核初始化的过程中,会进行相应的初始化,本质上是将TSS中相应的base地址设置为tss_struct per-CPU变量init_tss的地址,如此以来,修改init_tss后,相应的值即会体现到TSS(硬件)中。TSS段初始化流程如下:
a、BSP上的初始化流程
rest_init->kernel_init->kernel_init_freeable->smp_init->cpu_up->native_cpu_up->do_boot_cpu->start_secondary->cpu_init->set_tss_desc->__set_tss_desc
b、ASP上的初始化流程
start_kernel->trap_init->cpu_init->set_tss_desc->__set_tss_desc

点击(此处)折叠或打开

  1. void __cpuinit cpu_init(void)
  2. {
  3. ...
  4. t = &per_cpu(init_tss, cpu);
  5. ...
  6. load_sp0(t, thread);
  7. set_tss_desc(cpu, t);
  8. load_TR_desc();
  9. ...
  10. }

点击(此处)折叠或打开

  1. /*
  2.   * 设置TSS段描述符,将GDT中TSS段描述符中的base地址设置为指定的addr,即让TSS段指向指定的addr.
  3.   */
  4. static inline void __set_tss_desc(unsigned cpu, unsigned int entry, void *addr)
  5. {
  6. /*获取per-CPU的gdt(全局描述符表)*/
  7. struct desc_struct *d = get_cpu_gdt_table(cpu);
  8. tss_desc tss;


  9. /*
  10. * sizeof(unsigned long) coming from an extra "long" at the end
  11. * of the iobitmap. See tss_struct definition in processor.h
  12. *
  13. * -1? seg base+limit should be pointing to the address of the
  14. * last valid byte
  15. */
  16. /*TSS段描述符中的base地址设置为指定的addr,即让TSS段指向指定的addr.*/
  17. set_tssldt_descriptor(&tss, (unsigned long)addr, DESC_TSS,
  18.      IO_BITMAP_OFFSET + IO_BITMAP_BYTES +
  19.      sizeof(unsigned long) - 1);
  20. /*设置GDT中的TSS对应的entry为新设置的tss*/
  21. write_gdt_entry(d, entry, &tss, DESC_TSS);
  22. }

点击(此处)折叠或打开

  1. static inline void set_tssldt_descriptor(void *d, unsigned long addr, unsigned type, unsigned size)
  2. {
  3. #ifdef CONFIG_X86_64
  4. struct ldttss_desc64 *desc = d;


  5. memset(desc, 0, sizeof(*desc));


  6. desc->limit0    = size & 0xFFFF;
  7. /*设置描述符的base地址为addr*/
  8. desc->base0    = PTR_LOW(addr);
  9. desc->base1    = PTR_MIDDLE(addr) & 0xFF;
  10. desc->type    = type;
  11. desc->p    = 1;
  12. desc->limit1    = (size >> 16) & 0xF;
  13. desc->base2    = (PTR_MIDDLE(addr) >> 8) & 0xFF;
  14. desc->base3    = PTR_HIGH(addr);
  15. #else
  16. pack_descriptor((struct desc_struct *)d, addr, size, 0x80 | type, 0);
  17. #endif
  18. }

阅读(2883) | 评论(0) | 转发(0) |
0

上一篇:没有了

下一篇:kernel 3.10内核源码分析--进程上下文切换

给主人留下些什么吧!~~