重新认识Intel任务切换(二)

文章系列:
重新认识Intel任务切换(一)
重新认识Intel任务切换(二)

利用堆栈切换实现的进程切换

堆栈实现进程切换原理

  • 一句话解释,TSS硬件机制实现任务切换的本质是为CPU的执行提供堆栈和指令地址,堆栈记录函数的调用和返回,指令地址实现代码的转移。只要把CPU的堆栈和指令换了,就能达到切换进程的目的,就是更改SS、ESP、CS、EIP的内容。
  • 寄存器内容更改原理
  1. SS、ESP两个寄存器通过普通的mov指令可以更新内容
  2. CS、EIP两个寄存器通过RETF或RET指令更新内容
  3. RETF 第一步将栈当前值POP出来给EIP,第二步移动指针将下一个值POP出来给CS
  4. RET 将栈当前值POP出来给EIP
  • 核心流程
  1. 当前SS、ESP、CS和EIP寄存器内容通过mov指令保存到内存,将下一个进程的SS、ESP从内存mov到SS和ESP寄存器。CS和EIP不能直接mov。
  2. 将下一个进程的CS和EIP push到堆栈,使用RETF指令加载CS和EIP。或者,将EIP push到堆栈,使用RET指令加载EIP。

Linux进程切换关键代码分析

各个CPU架构的进程切换实现不同,本文只关注Intel架构。kernel version: 2.6.30

1) 函数调用链
kernel/sched.c
schedule
	__schedule
		context_switch(rq, prev, next)	
			switch_to(prev, next, prev)
			
arch/x86/include/asm/system.h			
2)关键代码
define switch_to(prev, next, last)                 
do {                                    
    /*                              
     * Context-switching clobbers all registers, so we clobber  
     * them explicitly, via unused output variables.        
     * (EAX and EBP is not listed because EBP is saved/restored 
     * explicitly for wchan access and EAX is the return value of   
     * __switch_to())                       
     */                             
    unsigned long ebx, ecx, edx, esi, edi;              
                                    
    asm volatile("pushfl\n\t"       /* save    flags */ 
             "pushl %%ebp\n\t"      /* save    EBP   */ 
             "movl %%esp,%[prev_sp]\n\t"    /* save    ESP   */	
             保存ESP到内存(当前进程的thread_struct结构)
             "movl %[next_sp],%%esp\n\t"    /* restore ESP   */ 
             从内存(下个进程的thread_struct结构)中取出ESP加载到寄存器
             "movl $1f,%[prev_ip]\n\t"  /* save    EIP   */ 
             保存EIP到内存((当前进程的thread_struct结构)),EIP的值就是标号1处的偏移地址
             "pushl %[next_ip]\n\t" /* restore EIP   */ 	
             从内存((下个进程的thread_struct结构))中取出EIP,PUSH到当前堆栈,RET调用时就能加载到EIP
             __switch_canary                    
             "jmp __switch_to\n"    /* regparm call  */ 
             __switch_to函数返回时调用RET指令,将CPU的EIP改变,跳转到下一个任务的标号1:处继续执行
             "1:\t"                     
             "popl %%ebp\n\t"       /* restore EBP   */ 
             "popfl\n"          /* restore flags */ 
                                    
             /* output parameters */                
             : [prev_sp] "=m" (prev->thread.sp),        
               [prev_ip] "=m" (prev->thread.ip),        
               "=a" (last),                 
                                    
               /* clobbered output registers: */        
               "=b" (ebx), "=c" (ecx), "=d" (edx),      
               "=S" (esi), "=D" (edi)               
                                        
               __switch_canary_oparam               
                                    
               /* input parameters: */              
             : [next_sp]  "m" (next->thread.sp),        
               [next_ip]  "m" (next->thread.ip),        
                                        
               /* regparm parameters for __switch_to(): */  
               [prev]     "a" (prev),               
               [next]     "d" (next)                
                                    
               __switch_canary_iparam               
                                    
             : /* reloaded segment registers */         
            "memory");                  
} while (0)		

实验

  • 目标
    实现两个任务来回切换,分别在屏幕上打印’H’,'Y’字符
  • 任务切换实现
    Linux的实现中,没有更新CS和SS代码段,下面的实验中会更新(虽然CS的内容一样)。
    另外,由于没有实现存放CS和EIP的内存数据结构,实验中要切换任务的CS和EIP都是常量,直接使用,不从内存中去。
1)准备任务代码,两个代码段,分别打印两个字符
LABEL_SEG_TASK0:
    mov ax, SelectorVideo
    mov gs, ax
    mov edi,(80 * 11 + 79) * 2
    mov ah, 0Ch
    mov al, 'H'
    mov [gs:edi], ax
    mov ecx, 0ffffffh
.0:
    dec ecx
    jecxz   .1
    jmp .0
.1:
    mov ebx, 1	
    ;ebx作为一个参数指定要跳转的任务
    ;1:跳转到TASK1
    ;0:跳转到TASK0
    jmp SelectorInitCode:OffSwitchTask		;切换到任务1
    jmp SelectorTaskCode0:0	;执行完任务1后继续从头执行任务0

Task0CodeLen    equ $ -  LABEL_SEG_TASK0

LABEL_SEG_TASK1:
    mov ax, SelectorVideo
    mov gs, ax
    mov edi,(80 * 11 + 79) * 2
    mov ah, 0Ch
    mov al, 'Y'
    mov [gs:edi], ax
    mov ecx, 0ffffffh
.2:
    dec ecx
    jecxz   .3
    jmp .2
.3:
    mov ebx, 0
    jmp SelectorInitCode:OffSwitchTask	;切换到任务0
    jmp SelectorTaskCode1:0	;执行完任务0后继续从头执行任务1
Task1CodeLen    equ $ -  LABEL_SEG_TASK1

2)任务切换函数
LABEL_SEG_INIT:
    xor eax, eax
    mov ax, SelectorStack0	;初始化TASK0的堆栈,往里面PUSH CS和EIP,预先存放好要执行的代码
    mov ss, ax
    xor eax, eax
    mov ax, TopOfStack0
    mov sp, ax

    push SelectorTaskCode0	;PUSH CS到堆栈,Linux没有这一步;使用常量,没有从内存数据结构中取
    push 0 	;PUSH EIP到堆栈,相当于Linux中的pushl %[next_ip];使用常量,没有从内存数据结构中取

    xor eax, eax
    mov ax, SelectorStack1	;初始化TASK1的堆栈,往里面PUSH CS和EIP,预先存放好要执行的代码
    mov ss, ax
    xor eax, eax
    mov ax, TopOfStack1
    mov sp, ax

    push SelectorTaskCode1
    push 0 

    mov ebx, 1	;跳转到任务1
    jmp SwitchTask

SwitchTask:
OffSwitchTask   equ SwitchTask  -   $$
    cmp ebx, 1	;判断是要切换到任务1还是任务0
    je  .4

    ; switch to stack0
    xor eax, eax
    mov ax, SelectorStack0		将任务0的堆栈加载到SS,里面依次存放任务0代码段的EIP和CS
    mov ss, ax	
    mov ax, TopOfStack0 - 8
	mov sp, ax	;切换堆栈
	
    retf	;RETF依次POP EIP和CS,加载到CS和EIP寄存器,跳转到任务0
.4:
    ; switch to stack1
    xor eax, eax
    mov ax, SelectorStack1	;将任务1的堆栈加载到SS,里面依次存放任务1代码段的EIP和CS
    mov ss, ax	
 	mov ax, TopOfStack0 - 8
	mov sp, ax	;切换堆栈
	
    retf	;RETF依次POP EIP和CS,加载到CS和EIP寄存器,跳转到任务1

3)声明代码段和数据段,生成选择子
[SECTION .gdt]
; GDT
;                              段基址,       段界限     , 属性
LABEL_GDT:     Descriptor       0,              0,      0   ; 空描述符
LABEL_DESC_INITCODE: Descriptor 0, InitCodeLen - 1, DA_C + DA_32; 非一致代码段
LABEL_DESC_TASKCODE0: Descriptor    0, Task0CodeLen - 1,    DA_C + DA_32 ; 非一致代码段
LABEL_DESC_TASKCODE1: Descriptor    0, Task1CodeLen - 1,    DA_C + DA_32 ; 非一致代码段
LABEL_DESC_STACK0:  Descriptor  0,      TopOfStack0,    DA_DRW; 堆栈段 
LABEL_DESC_STACK1:  Descriptor  0,      TopOfStack1,    DA_DRW; 堆栈段 
LABEL_DESC_TSS0:  Descriptor        0,  TSS0Len -1,    DA_386TSS; 
LABEL_DESC_TSS1:  Descriptor        0,  TSS1Len -1,    DA_386TSS; 
LABEL_DESC_VIDEO:  Descriptor 0B8000h,       0ffffh,    DA_DRW; 显存首地址

; GDT 结束
GdtLen      equ $ - LABEL_GDT; GDT长度
GdtPtr      dw  GdtLen - 1  ; GDT界限
            dd  0       ; GDT基地址

; GDT 选择子
SelectorInitCode    equ (LABEL_DESC_INITCODE    - LABEL_GDT)
SelectorTaskCode0   equ (LABEL_DESC_TASKCODE0   - LABEL_GDT)
SelectorTaskCode1   equ (LABEL_DESC_TASKCODE1   - LABEL_GDT)
SelectorStack0      equ (LABEL_DESC_STACK0  - LABEL_GDT)
SelectorStack1      equ (LABEL_DESC_STACK1  - LABEL_GDT)
SelectorTSS0        equ (LABEL_DESC_TSS0    - LABEL_GDT)
SelectorTSS1        equ (LABEL_DESC_TSS1    - LABEL_GDT)
SelectorVideo       equ (LABEL_DESC_VIDEO   - LABEL_GDT)
; END of [SECTION .gdt]

  • 结果分析
  1. SelectorInitCode中执行指令mov ebx, 1; jmp SwitchTask前,TASK0和TASK1的代码段分别对应选择子0x10和0x18,堆栈段分别对应0x20和0x28,打印出堆栈的内容,可以看到堆栈中已经存放了两个任务的代码段选择子0x10和0x18,由与EIP写入的是0,所以区别不出来。当前堆栈SS=0x28是任务1的堆栈。
    在这里插入图片描述
  2. 从SwitchTask返回时调用retf,ebx传入的1表示切换到任务1。当前堆栈本来就使用的任务1的堆栈,因此SS没有变化,RETF执行后会跳转到任务1SelectorTaskCode1:0指向的地方。RETF执行后CS和IP的值都更新成TASK1堆栈中的值。
    在这里插入图片描述
  3. 从任务1切换到任务0代码如下
mov ebx, 0
jmp SelectorInitCode:OffSwitchTask

.4:
    ; switch to stack1
    xor eax, eax
    mov ax, SelectorStack1	;将任务1的堆栈加载到SS,里面依次存放任务1代码段的EIP和CS
    mov ss, ax	;
    mov ax, TopOfStack0 - 8
	mov sp, ax	;切换堆栈
    retf
jmp SelectorTaskCode1:0

执行retf前,可以看到SS时0x20,是TASK0的堆栈,里面存放的时TASK0的CS和EIP。RETF执行后,CS和EIP将更新成TASK0堆栈的值。
在这里插入图片描述在这里插入图片描述

Linux进程调度总结

  • Linux进程调度的原理是切换堆栈后使用ret指令,将EIP寄存器加载成下一个任务要执行的IP
  • Linux进程调度没有使用TSS的机制,不能使用TSS现成的数据结构,因此需要申请一段内存保存各个任务的硬件上下文,thread_struct结构体的作用就是这个
  • Linux任务切换所有动作都在内核空间完成,不涉及特权级改变时的堆栈切换
    附: 实验完整源码见my github Linux任务切换
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值