zoukankan      html  css  js  c++  java
  • 进程切换 TSS [转]

    [转] http://www.eefocus.com/article/09-06/74895s.html

    Intel i386 体系结构包括了一个特殊的段类型,叫任务状态段(TSS),如图5.4所示。每个任务包含有它自己最小长度为104字节的TSS段,在/include/ i386/processor.h 中定义为tss_struct结构:

    struct tss_struct {
    unsigned short back_link,__blh;
    unsigned long esp0;
    unsigned short ss0,__ss0h;/*0级堆栈指针,即Linux中的内核级 */
    unsigned long esp1;
    unsigned short ss1,__ss1h; /* 1级堆栈指针,未用*/
    unsigned long esp2;
    unsigned short ss2,__ss2h; /* 2级堆栈指针,未用*/
    unsigned long __cr3;
    unsigned long eip;
    unsigned long eflags;
    unsigned long eax,ecx,edx,ebx;
    unsigned long esp;
    unsigned long ebp;
    unsigned long esi;
    unsiged long edi;
    unsigned short es, __esh;
    unsigned short cs, __csh;
    unsigned short ss, __ssh;
    unsigned short ds, __dsh;
    unsigned short fs, __fsh;
    unsigned short gs, __gsh;
    unsigned short ldt, __ldth;
    unsigned short trace, bitmap;
    unsigned long io_bitmap[IO_BITMAP_SIZE+1];
    /*
    * pads the TSS to be cacheline-aligned (size is 0x100)
    */
    unsigned long __cacheline_filler[5];
    };

     每个TSS有它自己 8字节的任务段描述符(Task State Segment Descriptor ,简称TSSD)。这个描述符包括指向TSS起始地址的32位基地址域,20位界限域,界限域值不能小于十进制104(由TSS段的最小长度决定)。 TSS描述符存放在GDT中,它是GDT中的一个表项。

    后面将会看到,Linux在进程切换时,只用到TSS中少量的信息,因此Linux内核定义了另外一个数据结构,这就是thread_struct 结构

    struct thread_struct {
    unsigned long esp0;
    unsigned long eip;
    unsigned long esp;
    unsigned long fs;
    unsigned long gs;
    /* Hardware debugging registers */
    unsigned long debugreg[8]; /* %%db0-7 debug registers */
    /* fault info */
    unsigned long cr2, trap_no, error_code;
    /* floating point info */
    union i387_union i387;
    /* virtual 86 mode info */
    struct vm86_struct * vm86_info;
    unsigned long screen_bitmap;
    unsigned long v86flags, v86mask, v86mode, saved_esp0;
    /* IO permissions */
    int ioperm;
    unsigned long io_bitmap[IO_BITMAP_SIZE+1];
    };

    用这个数据结构来保存cr2寄存器、浮点寄存器、调试寄存器及指定给Intel 80x86处理器的其他各种各样的信息。需要位图是因为ioperm( ) 及 iopl( )系统调用可以允许用户态的进程直接访问特殊的I/O端口。尤其是,如果把eflag寄存器中的IOPL 域设置为3,就允许用户态的进程访问对应的I/O访问权位图位为0的任何一个I/O端口。

    那么,进程到底是怎样进行切换的?

    从第三章我们知道,在中断描述符表(IDT)中,除中断门、陷阱门和调用门外,还有一种“任务们”。任务门中包含有TSS段的选择符。当CPU因中断而穿 过一个任务门时,就会将任务门中的段选择符自动装入TR寄存器,使TR指向新的TSS,并完成任务切换。CPU还可以通过JMP或CALL指令实现任务切 换,当跳转或调用的目标段(代码段)实际上指向GDT表中的一个TSS描述符项时,就会引起一次任务切换。

    Intel的这种设计确实很周到,也为任务切换提供了一个非常简洁的机制。但是,由于i386的系统结构基本上是CISC的,通过JMP指令或 CALL(或中断)完成任务的过程实际上是“复杂指令”的执行过程,其执行过程长达300多个CPU周期(一个POP指令占12个CPU周期),因 此,Linux内核并不完全使用i386CPU提供的任务切换机制。

    由于i386CPU要求软件设置TR及TSS,Linux内核只不过“走过场”地设置TR及TSS,以满足CPU的要求。但是,内核并不使用任务门,也不 使用JMP或CALL指令实施任务切换。内核只是在初始化阶段设置TR,使之指向一个TSS,从此以后再不改变TR的内容了。也就是说,每个CPU(如果 有多个CPU)在初始化以后的全部运行过程中永远使用那个初始的TSS。同时,内核也不完全依靠TSS保存每个进程切换时的寄存器副本,而是将这些寄存器 副本保存在各个进程自己的内核栈中(参见上一章task_struct结构的存放)。

    这样以来,TSS中的绝大部分内容就失去了原来的意义。那么,当进行任务切换时,怎样自动更换堆栈?我们知道,新任务的内核栈指针(SS0和ESP0)应 当取自当前任务的TSS,可是,Linux中并不是每个任务就有一个TSS,而是每个CPU只有一个TSS。Intel原来的意图是让TR的内容(即 TSS)随着任务的切换而走马灯似地换,而在Linux内核中却成了只更换TSS中的SS0和ESP0,而不更换TSS本身,也就是根本不更换TR的内 容。这是因为,改变TSS中SS0和ESP0所化的开销比通过装入TR以更换一个TSS要小得多。因此,在Linux内核中,TSS并不是属于某个进程的 资源,而是全局性的公共资源。在多处理机的情况下,尽管内核中确实有多个TSS,但是每个CPU仍旧只有一个TSS。

    5.4.2 进程切换

    前面所介绍的schedule()中调用了switch_to宏,这个宏实现了进程之间的真正切换,其代码存放于include/ i386/system.h:

    1 #define switch_to(prev,next,last) do { \
    2 asm volatile("pushl %%esi\n\t" \
    3 "pushl %%edi\n\t" \
    4 "pushl %%ebp\n\t" \
    5 "movl %%esp,%0\n\t" /* save ESP */ \
    6 "movl %3,%%esp\n\t" /* restore ESP */ \
    7 "movl $1f,%1\n\t" /* save EIP */ \
    8 "pushl %4\n\t" /* restore EIP */ \
    9 "jmp __switch_to\n" \
    10 "1:\t" \
    11 "popl %%ebp\n\t" \
    12 "popl %%edi\n\t" \
    13 "popl %%esi\n\t" \
    14 :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
    15 "=b" (last) \
    16 :"m" (next->thread.esp),"m" (next->thread.eip), \
    17 "a" (prev), "d" (next), \
    18 "b" (prev)); \
    19 } while (0)

    switch_to宏是用嵌入式汇编写成,比较难理解,为描述方便起见,我们给代码编了行号,在此我们给出具体的解释:

    · thread的类型为前面介绍的thread_struct结构。
    · 输出参数有三个,表示这段代码执行后有三项数据会有变化,它们与变量及寄存器的对应关系如下:
    0%与prev->thread.esp对应,1%与prev->thread.eip对应,这两个参数都存放在内存,而2%与ebx寄存器对应,同时说明last参数存放在ebx寄存器中。
    · 输入参数有五个,其对应关系如下:
    3%与next->thread.esp对应,4%与next->thread.eip对应,这两个参数都存放在内存,而5%,6%和7%分 别与eax,edx及ebx相对应,同时说明prev,next以及prev三个参数分别放在这三个寄存器中。表5.1列出了这几种对应关系:

    · 第2~4行就是在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。
    · 第5行将prev的内核堆栈指针ebp存入prev->thread.esp中。
    · 第6行把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中。从现在开始,内核对next的内核栈进行操作,因 此,这条指令执行从prev到next真正的上下文切换,因为进程描述符的地址与其内核栈的地址紧紧地联系在一起(参见第四章),因此,改变内核栈就意味 着改变当前进程。如果此处引用current的话,那就已经指向next的task_struct结构了。从这个意义上说,进程的切换在这一行指令执行完 以后就已经完成。但是,构成一个进程的另一个要素是程序的执行,这方面的切换尚未完成。
    · 第7行将标号“1”所在的地址,也就是第一条popl指令(第11行)所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度运行而切入时的“返回”地址。
    · 第8行将next->thread.eip压入next的内核栈。那么,next->thread.eip究竟指向那个地址?实际上,它就是 next上一次被调离时通过第7行保存的地址,也就是第11行popl指令的地址。因为,每个进程被调离时都要执行这里的第7行,这就决定了每个进程(除 了新创建的进程)在受到调度而恢复执行时都从这里的第11行开始。
    · 第9行通过jump指令(而不是 call指令)转入一个函数__switch_to()。这个函数的具体实现将在下面介绍。当CPU执行到__switch_to()函数的ret指令 时,最后进入堆栈的next->thread.eip就变成了返回地址,这就是标号“1”的地址。
    · 第11~13行恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行。

    下面我们来讨论__switch_to()函数。

    在调用__switch_to()函数之前,对其定义了fastcall :

    extern void FASTCALL(__switch_to(struct task_struct *prev, struct task_struct *next));

    fastcall对函数的调用不同于一般函数的调用,因为__switch_to()从寄存器(如表5.1)取参数,而不像一般函数那样从堆栈取参数,也就是说,通过寄存器eax和edx把prev和next 参数传递给__switch_to()函数。

     

    void __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
    {
    struct thread_struct *prev = &prev_p->thread,
    *next = &next_p->thread;
    struct tss_struct *tss = init_tss + smp_processor_id();
    unlazy_fpu(prev_p);/* 如果数学处理器工作,则保存其寄存器的值*/
    /* 将TSS中的内核级(0级)堆栈指针换成next->esp0,这就是next 进程在内核
    栈的指针
    tss->esp0 = next->esp0;
    /* 保存fs和gs,但无需保存es和ds,因为当处于内核时,内核段
    总是保持不变*/
    asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));
    asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));
    /*恢复next进程的fs和gs */
    loadsegment(fs, next->fs);
    loadsegment(gs, next->gs);
    /* 如果next挂起时使用了调试寄存器,则装载0~7个寄存器中的6个寄存器,其中第4、5个寄存器没有使用 */
    if (next->debugreg[7]){
    loaddebug(next, 0);
    loaddebug(next, 1);
    loaddebug(next, 2);
    loaddebug(next, 3);
    /* no 4 and 5 */
    loaddebug(next, 6);
    loaddebug(next, 7);
    }
    if (prev->ioperm || next->ioperm) {
    if (next->ioperm) {
    /*把next进程的I/O操作权限位图拷贝到TSS中 */
    memcpy(tss->io_bitmap, next->io_bitmap,
    IO_BITMAP_SIZE*sizeof(unsigned long));
    /* 把io_bitmap在tss中的偏移量赋给tss->bitmap */
    tss->bitmap = IO_BITMAP_OFFSET;
    } else
    /*如果一个进程要使用I/O指令,但是,若位图的偏移量超出TSS的范围,
    * 就会产生一个可控制的SIGSEGV信号。第一次对sys_ioperm()的调用会
    * 建立起适当的位图 */
    tss->bitmap = INVALID_IO_BITMAP_OFFSET;
    }
    }

    从上面的描述我们看到,尽管Intel本身为操作系统中的进程(任务)切换提供了硬件支持,但是Linux内核的设计者并没有完全采用这种思想,而是用软件实现了进程切换,而且,软件实现比硬件实现的效率更高,灵活性更大。

    -----------------------------------------------------

    [转] http://www.linuxidc.com/Linux/2011-03/33367.htm

    tss的作用举例:保存不同特权级别下任务所使用的寄存器,特别重要的是esp,因为比如中断后,涉及特权级切换时(一个任务切换),首先要切换 栈,这个栈显然是内核栈,那么如何找到该栈的地址呢,这需要从tss段中得到,这样后续的执行才有所依托(在x86机器上,c语言的函数调用是通过栈实现 的)。只要涉及地特权环到高特权环的任务切换,都需要找到高特权环对应的栈,因此需要esp2,esp1,esp0起码三个esp,然而Linux只使用 esp0。

    tss是什么:tss是一个段,段是x86的概念,在保护模式下,段选择符参与寻址,段选择符在段寄存器中,而tss段则在tr寄存器中。

    intel的建议:为每一个进程准备一个独立的tss段,进程切换的时候切换tr寄存器使之指向该进程对应的tss段,然后在任务切换时(比如涉及特权级切换的中断)使用该段保留所有的寄存器。

    Linux的做法:

    1.Linux没有为每一个进程都准备一个tss段,而是每一个cpu使用一个tss段,tr寄存器保存该段。进程切换时,只更新唯一tss段中的esp0字段到新进程的内核栈。

    2.Linux的tss段中只使用esp0和iomap等字段,不用它来保存寄存器,在一个用户进程被中断进入ring0的时候,tss中取出esp0,然后切到esp0,其它的寄存器则保存在esp0指示的内核栈上而不保存在tss中。

    3.结果,Linux中每一个cpu只有一个tss段,tr寄存器永远指向它。符合x86处理器的使用规范,但不遵循intel的建议,这样的后果是开销更小了,因为不必切换tr寄存器了。

    Linux的实现:

    1.定义tss:
    struct tss_struct init_tss[NR_CPUS] __cacheline_aligned = { [0 ... NR_CPUS-1] = INIT_TSS };(arch/i386/kernel/init_task.c)
    INIT_TSS定义为:
    #define INIT_TSS  {                            \
        .esp0        = sizeof(init_stack) + (long)&init_stack,    \
        .ss0        = __KERNEL_DS,                    \
        .esp1        = sizeof(init_tss[0]) + (long)&init_tss[0],    \
        .ss1        = __KERNEL_CS,                    \
        .ldt        = GDT_ENTRY_LDT,                \
        .io_bitmap_base    = INVALID_IO_BITMAP_OFFSET,            \
        .io_bitmap    = { [ 0 ... IO_BITMAP_LONGS] = ~0 },        \
    }

    http://www.linuxidc.com/Linux/2011-03/33367.htm

  • 相关阅读:
    SpringBoot:第五篇 CommandLineRunner
    多列单个索引和联合索引的区别
    数据库从0到0.1 (一): LSM-Tree VS B-Tree
    SpringBoot:第四篇 集成Guava(本地缓存+分布式缓存)
    SpringBoot:第三篇 配置修改
    TSINGSEE青犀视频编译WebRTC Android版报错The only supported distros are xxxxxxx处理方式
    TSINGSEE青犀视频开发WebRTC视频推流播放平台前端获取视频流列表错误是什么原因?
    如何修改TSINGSEE青犀视频开发的多平台支持视频RTSPServer组件EasyRTSPServer-win的端口号?
    视频RTSPServer组件EasyRTSPServer-win测试视频无法拉取到流在VLC播放如何修复?
    TSINGSEE青犀视频开发EasyWasmPlayer H265播放器是如何实现视频截图功能的?
  • 原文地址:https://www.cnblogs.com/longdouhzt/p/2750329.html
Copyright © 2011-2022 走看看