zoukankan      html  css  js  c++  java
  • 用户态到内核态切换之奥秘解析

    用户态到内核态切换之奥秘解析

    学号:SA12**6112

    本文将主要研究在X86体系下Linux系统中用户态到内核态切换条件,及切换过程中内核栈和任务状态段TSS在中断机制/任务切换中的作用及相关寄存器的变化

    一:用户态到内核态切换途径:

            1:系统调用        2:中断   3:异常

    对应代码,在3.3内核中,可以在/arch/x86/kernel/entry_32.S文件中查看。

    二:内核栈

    内核栈:Linux中每个进程有两个栈,分别用于用户态和内核态的进程执行,其中的内核栈就是用于内核态的堆栈,它和进程的task_struct结构,更具体的是thread_info结构一起放在两个连续的页框大小的空间内。

    在内核源代码中使用C语言定义了一个联合结构方便地表示一个进程的thread_info和内核栈:

    此结构在3.3内核版本中的定义在include/linux/sched.h文件的第2106行:

    2016  union thread_union {
    2017          struct thread_info thread_info;
    2018          unsigned long stack[THREAD_SIZE/sizeof(long)];
    2019     };        

    其中thread_info结构的定义如下:

    3.3内核 /arch/x86/include/asm/thread_info.h文件第26行:

    复制代码
     26   struct thread_info {
     27         struct task_struct      *task;          /* main task structure */
     28         struct exec_domain      *exec_domain;   /* execution domain */
     29         __u32                   flags;          /* low level flags */
     30         __u32                   status;         /* thread synchronous flags */
     31         __u32                   cpu;            /* current CPU */
     32         int                     preempt_count;  /* 0 => preemptable,
     33                                                    <0 => BUG */
     34         mm_segment_t            addr_limit;
     35         struct restart_block    restart_block;
     36         void __user             *sysenter_return;
     37 #ifdef CONFIG_X86_32
     38         unsigned long           previous_esp;   /* ESP of the previous stack in
     39                                                    case of nested (IRQ) stacks
     40                                                 */
     41         __u8                    supervisor_stack[0];
     42 #endif
     43         unsigned int            sig_on_uaccess_error:1;
     44         unsigned int            uaccess_err:1;  /* uaccess failed */
     45 };
    复制代码

    它们的结构图大致如下:

      esp寄存器是CPU栈指针,存放内核栈栈顶地址。在X86体系中,栈开始于末端,并朝内存区开始的方向增长。从用户态刚切换到内核态时,进程的内核栈总是空的,此时esp指向这个栈的顶端。

      在X86中调用int指令型系统调用后会把用户栈的%esp的值及相关寄存器压入内核栈中,系统调用通过iret指令返回,在返回之前会从内核栈弹出用户栈的%esp和寄存器的状态,然后进行恢复。所以在进入内核态之前要保存进程的上下文,中断结束后恢复进程上下文,那靠的就是内核栈

      这里有个细节问题,就是要想在内核栈保存用户态的esp,eip等寄存器的值,首先得知道内核栈的栈指针,那在进入内核态之前,通过什么才能获得内核栈的栈指针呢?答案是:TSS

    三:TSS

    X86体系结构中包括了一个特殊的段类型:任务状态段(TSS),用它来存放硬件上下文。TSS反映了CPU上的当前进程的特权级。

    linux为每一个cpu提供一个tss段,并且在tr寄存器中保存该段。

    在从用户态切换到内核态时,可以通过获取TSS段中的esp0来获取当前进程的内核栈 栈顶指针,从而可以保存用户态的cs,esp,eip等上下文。


    :linux中之所以为每一个cpu提供一个tss段,而不是为每个进程提供一个tss段,主要原因是tr寄存器永远指向它,在任务切换的适合不必切换tr寄存器,从而减小开销。

    下面我们看下在X86体系中Linux内核对TSS的具体实现:

    内核代码中TSS结构的定义:

    3.3内核中:/arch/x86/include/asm/processor.h文件的第248行处:

    复制代码
    248   struct tss_struct {
    249         /*
    250          * The hardware state:
    251          */
    252         struct x86_hw_tss       x86_tss;
    253 
    254         /*
    255          * The extra 1 is there because the CPU will access an
    256          * additional byte beyond the end of the IO permission
    257          * bitmap. The extra byte must be all 1 bits, and must
    258          * be within the limit.
    259          */
    260         unsigned long           io_bitmap[IO_BITMAP_LONGS + 1];
    261 
    262         /*
    263          * .. and then another 0x100 bytes for the emergency kernel stack:
    264          */
    265         unsigned long           stack[64];
    266 
    267 } ____cacheline_aligned;    
    复制代码

    其中主要的内容是:

    硬件状态结构 :       x86_hw_tss

    IO权位图 :    io_bitmap

    备用内核栈:    stack

    其中硬件状态结构:其中在32位X86系统中x86_hw_tss的具体定义如下:

    /arch/x86/include/asm/processor.h文件中第190行处:

    复制代码
    190#ifdef CONFIG_X86_32
    191 /* This is the TSS defined by the hardware. */
    192 struct x86_hw_tss {
    193         unsigned short          back_link, __blh;
    194         unsigned long           sp0;              //当前进程的内核栈顶指针
    195         unsigned short          ss0, __ss0h;       //当前进程的内核栈段描述符
    196         unsigned long           sp1;
    197         /* ss1 caches MSR_IA32_SYSENTER_CS: */
    198         unsigned short          ss1, __ss1h;
    199         unsigned long           sp2;
    200         unsigned short          ss2, __ss2h;
    201         unsigned long           __cr3;
    202         unsigned long           ip;
    203         unsigned long           flags;
    204         unsigned long           ax;
    205         unsigned long           cx;
    206         unsigned long           dx;
    207         unsigned long           bx;
    208         unsigned long           sp;            //当前进程用户态栈顶指针
    209         unsigned long           bp;
    210         unsigned long           si;
    211         unsigned long           di;
    212         unsigned short          es, __esh;
    213         unsigned short          cs, __csh;
    214         unsigned short          ss, __ssh;
    215         unsigned short          ds, __dsh;
    216         unsigned short          fs, __fsh;
    217         unsigned short          gs, __gsh;
    218         unsigned short          ldt, __ldth;
    219         unsigned short          trace;
    220         unsigned short          io_bitmap_base;
    221 
    222 } __attribute__((packed));
    复制代码

    linux的tss段中只使用esp0和iomap等字段,并且不用它的其他字段来保存寄存器,在一个用户进程被中断进入内核态的时候,从tss中的硬件状态结构中取出esp0(即内核栈栈顶指针),然后切到esp0,其它的寄存器则保存在esp0指的内核栈上而不保存在tss中。

    每个CPU定义一个TSS段的具体实现代码:

    3.3内核中/arch/x86/kernel/init_task.c第35行:

    复制代码
     35  * per-CPU TSS segments. Threads are completely 'soft' on Linux,
     36  * no more per-task TSS's. The TSS size is kept cacheline-aligned
     37  * so they are allowed to end up in the .data..cacheline_aligned
     38  * section. Since TSS's are completely CPU-local, we want them
     39  * on exact cacheline boundaries, to eliminate cacheline ping-pong.
     40  */
    41 DEFINE_PER_CPU_SHARED_ALIGNED(struct tss_struct, init_tss) = INIT_TSS;
    复制代码

    INIT_TSS的定义如下:

    3.3内核中 /arch/x86/include/asm/processor.h文件的第879行:

    复制代码
    879 #define INIT_TSS  {                                                       
    880         .x86_tss = {                                                      
    881                 .sp0            = sizeof(init_stack) + (long)&init_stack, 
    882                 .ss0            = __KERNEL_DS,                            
    883                 .ss1            = __KERNEL_CS,                            
    884                 .io_bitmap_base = INVALID_IO_BITMAP_OFFSET,               
    885          },                                                               
    886         .io_bitmap              = { [0 ... IO_BITMAP_LONGS] = ~0 },       
    887 }
    复制代码

    其中init_stack是宏定义,指向内核栈:

    61 #define init_stack              (init_thread_union.stack)

    这里可以看到分别把内核栈栈顶指针、内核代码段、内核数据段赋值给TSS中的相应项。从而进程从用户态切换到内核态时,可以从TSS段中获取内核栈栈顶指针,进而保存进程上下文到内核栈中。

    总结:有了上面的一些准备,现总结在进程从用户态到内核态切换过程中,Linux主要做的事:

    1:读取tr寄存器,访问TSS段

    2:从TSS段中的sp0获取进程内核栈的栈顶指针

    3:  由控制单元在内核栈中保存当前eflags,cs,ss,eip,esp寄存器的值。

    4:由SAVE_ALL保存其寄存器的值到内核栈

    5:把内核代码选择符写入CS寄存器,内核栈指针写入ESP寄存器,把内核入口点的线性地址写入EIP寄存器

    此时,CPU已经切换到内核态,根据EIP中的值开始执行内核入口点的第一条指令。

     
     
  • 相关阅读:
    【crontab】误删crontab及其恢复
    New Concept English there (7)
    New Concept English there (6)
    New Concept English there (5)
    New Concept English there (4)
    New Concept English there (3)
    New Concept English there (2)Typing speed exercise
    New Concept English there (1)Typing speed exercise
    New Concept English Two 34 game over
    New Concept English Two 33 94
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3158547.html
Copyright © 2011-2022 走看看