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


    学号: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中的值开始执行内核入口点的第一条指令。

  • 相关阅读:
    sql 从A表复制数据到B表
    sql union和union all
    sql 类型转换
    SQL聚合函数
    数据存储类型
    asp.net中XML如何做增删改查操作(基础操作)
    数据库分页总结
    javascript 和 jquery 初学总结
    File FileStream StreamReader和StreamWriter
    oracle建数据库
  • 原文地址:https://www.cnblogs.com/spinsoft/p/3158531.html
Copyright © 2020-2023  润新知