3.如何保存CPU现场信息

来源:互联网 发布:ipadmini1实用软件 编辑:程序博客网 时间:2024/05/14 18:35

3.讲完了进程和线程之后,我们的核心问题块来临了,操作系统到底是采用什么策略进行进程或线程调度的,也就是决定让哪个线程或者进程进入到CPU里面去执行。在讲这个问题之前我们还有一个很重要的上课讲的内容,进程或者线程切换的时候,我们如何保存其运行信息?

3.1线程的切换

我花费好几个小时去找这个的内容,因为我借的两本书上都没讲代码,而是直接给出了原理和答案,好在我还是找到了线程切换的一部分代码,但是我看了之后真的是发现好痛苦,不同的处理器体系结构代码是不同的,下面我以简单i386体系结构为例,来简单的描述一下操作系统是如何进程线程切换的,这里涉及到了很多的概念和内容,是需要对前面的内容进程拓展才能理解清楚的,相关方面的内容只能是自己去看书了。在WRK-v1.2\base\ntos\ke\amd64\Ctxswap.asm里面我们终于看到了代码:

includeksamd64.inc

......

;BOOLEAN

;KiSwapContext (

;    IN PKTHREAD OldThread,

;    IN PKTHREAD NewThread

;    )

       NESTED_ENTRY KiSwapContext, _TEXT$00

 

        GENERATE_EXCEPTION_FRAME        ; 生成异常页

        mov    rbx, gs:[PcCurrentPrcb] ; 获得当前PRCB地址

        mov    rdi, rcx                ; 旧线程地址换到rdi中

        mov    rsi, rdx                ; 新线程地址换到rsi总

        movzx  ecx, byte ptr ThWaitIrql[rdi] ; 禁止APC机制的中断影响

        call   SwapContext             ; //调用SwapContext

 

        RESTORE_EXCEPTION_STATE         ;

        ret                             ; 返回

 

        NESTED_END KiSwapContext, _TEXT$00

 

        subttl "Dispatch Interrupt"

/*这段程序是作为一个发生在 DISPATCH_LEVEL上的软中断的结果的输入,它的功能是处理DPC链表,当一个新的线程已经准备好到一个处理器上运行时,这个程序将进行新旧线程的上下文切换*/

DiFrame struct

        P1Home  dq ?                    ; PRCB地址段

        P2Home  dq ?                    ;

        P3Home  dq ?                    ;

        P4Home  dq ?                    ;

        SavedRbx dq ?                   ; 保存RBX

DiFrame ends

 

        NESTED_ENTRYKiDispatchInterrupt, _TEXT$00

 

        alloc_stack (sizeofDiFrame)    ; 进行堆的分配

        save_reg rbx, DiFrame.SavedRbx  ; 保存非易失性寄存器的内容

 

        END_PROLOGUE

 

        lea     rcx, (-128)[rbp]        ; 设置trap地址

        call    KiCheckForSListAddress  ; 检查SLIST地址

 

        mov     rbx, gs:[PcCurrentPrcb] ; 获得当前 PRCB地址

        and     byte ptr PbDpcInterruptRequested[rbx], 0;//清除请求

       KiDI10: cli       //上下文切换不能被中断,此处即禁止中断                      ;

        mov     eax, PbDpcQueueDepth[rbx] ; 获取DPC 队列深度        or      rax, PbTimerRequest[rbx] ; 合并计时器的值

 

ifndef NT_UP

 

        or      rax, PbDeferredReadyListHead[rbx] ; 合并延期准备表

endif

 

        jz      short KiDI20            ; if z, no DPCs to process

        mov     PbSavedRsp[rbx], rsp    ; save current stack pointer

        mov     rsp, PbDpcStack[rbx]    ; set DPC stack pointer

        mov     rcx, rbx                ; set PRCB address parameter

        call    KiRetireDpcList         ; process the DPC list

        mov     rsp, PbSavedRsp[rbx]    ; restore current stack pointer

 

KiDI20: sti                            ; enable interrupts

        cmp     byte ptr PbQuantumEnd[rbx], 0 ; check ifquantum end request

        je      short KiDI40            ; if e, quantum end not requested

        and     byte ptr PbQuantumEnd[rbx], 0 ; clearquantum end indicator

        call    KiQuantumEnd            ; process quantum end

 

KiDI30: mov     rbx,DiFrame.SavedRbx[rsp] ; restore nonvolatile register

        add     rsp, (sizeof DiFrame)   ; deallocate stack frame

        ret                             ; return

 

        mov     rbx, DiFrame.SavedRbx[rsp] ; restorenonvolatile register

        add     rsp, (sizeof DiFrame)   ; deallocate stack frame

        jmp     short KxDispatchInterrupt ;

 

        NESTED_ENDKiDispatchInterrupt, _TEXT$00

 

        NESTED_ENTRYKxDispatchInterrupt, _TEXT$00

 

        GENERATE_EXCEPTION_FRAME        ; generate exception frame

 

        mov     rbx, gs:[PcCurrentPrcb] ; get current PRCBaddress

        mov     rdi, PbCurrentThread[rbx] ; get old threadaddress

 

 

ifndef NT_UP

 

        mov     ecx, SYNCH_LEVEL        ; set IRQL to SYNCH level

 

        SetIrql                         ;

 

        mov     byte ptr ThSwapbusy[rdi], 1 ; set contextswap busy

 

        AcquireSpinLockPbPrcbLock[rbx] ; acquire current PRCB lock

 

endif

 

        mov     rsi, PbNextThread[rbx]  ; get next thread address

        and    qword ptr PbNextThread[rbx], 0 ; clear next thread address

        mov     PbCurrentThread[rbx], rsi ; set currentthread address

        mov     byte ptr ThState[rsi], Running ; set newthread state

        mov     byte ptr ThWaitReason[rdi], WrDispatchInt; set wait reason

        mov     rcx, rdi                ; set address of old thread

        mov     rdx, rbx                ; set address of current PRCB

        call    KiQueueReadyThread      ; queue ready thread for execution

        mov     ecx,APC_LEVEL          ; set APC interruptbypass disable

        call    SwapContext             ; call context swap routine

 

        ldmxcsr ExMxCsr[rsp]            ; restore current MXCSR value

 

        RESTORE_EXCEPTION_STATE         ; restore exception state/deallocate

 

        ret                             ; return

 

        NESTED_ENDKxDispatchInterrupt, _TEXT$00

 

        subttl  "Swap Context"

        NESTED_ENTRY SwapContext,_TEXT$00

 

        alloc_stack(KSWITCH_FRAME_LENGTH - (1 * 8)) ; allocate stack frame

        save_reg rbp, SwRbp             ; save nonvolatile register

 

        END_PROLOGUE

 

        mov     SwApcBypass[rsp], cl    ; save APC bypass disable

 

 

ifndef NT_UP

 

KiSC00: cmp     byte ptrThSwapBusy[rsi], 0 ; check if swap busy for new thread

 

        Yield                           ; yield processorexecution

 

        jne     short KiSC00            ; if ne, context busy for newthread

 

endif

 

        mov     rax, (PcPerfGlobalGroupMask - PcPrcb)[rbx]; get global mask address

        test    rax, rax                ; test if logging enabled

        je      short KiSC05            ; if e, logging not enabled

        test    dword ptr PERF_CONTEXTSWAP_OFFSET[rax],PERF_CONTEXTSWAP_FLAG ; check flag

        jz      short KiSC05            ; if z, context swap events notenabled

        mov     rcx, rdi                ; set address of old thread

        mov     rdx, rsi                ; set address of new thread

        call    WmiTraceContextSwap     ; call trace routine

 

 

KiSC05: cmp     byte ptrThNpxState[rdi], LEGACY_STATE_SWITCH ; check if switched

        jne     short KiSC10            ; if ne, legacy state not switched

        mov     rbp, ThInitialStack[rdi] ; get previousthread initial stack

        fxsave  [rbp]                  ; save legacy floatingpoint state

 

KiSC10: mov     ThKernelStack[rdi],rsp ; save old kernel stack pointer

        mov     rsp, ThKernelStack[rsi] ; get new kernelstack pointer

 

        mov     r14, ThApcState + AsProcess[rsi] ; get newprocess address

        cmp     r14, ThApcState + AsProcess[rdi] ; checkif process match

        je      short KiSC20            ; if e, process addresses match

 

 

ifndef NT_UP

 

        mov     rdx, ThApcState + AsProcess[rdi] ; get oldprocess address

        mov     rcx, PbSetMember[rbx]   ; get processor set member

   lock xor     PrActiveProcessors[rdx], rcx ; clear bitin previous set

 

if DBG

 

        test    PrActiveProcessors[rdx], rcx ; test if bitclear in previous set

        jz      short @f                ; if z, bit clear in previous set

        int     3                       ; debug break -incorrect active mask

@@:                                    ; reference label

Endif

endif

 

ifndef NT_UP

 

   lock xor     PrActiveProcessors[r14], rcx ; set bit innew set

 

if DBG

 

        test    PrActiveProcessors[r14], rcx ; test if bitset in new set

        jnz     short @f                ; if nz, bit set in new set

        int     3                       ; debug break -incorrect active mask

@@:                                     ; reference label

 

endif

 

endif

 

 

        mov     rdx, PrDirectoryTableBase[r14] ; get newdirectory base

        mov     cr3, rdx                ; flush TLB and set newdirectory base

 

 

KiSC20:                                ;

 

ifndef NT_UP

 

        mov     byte ptr ThSwapBusy[rdi], 0  ; set context swap idle

 

endif

 

 

        mov     r15, (PcTss - PcPrcb)[rbx] ; get processorTSS address

        mov     rbp, ThInitialStack[rsi] ; get new stackbase address

        mov     TssRsp0[r15], rbp       ; set stack base address in TSS

        mov     PbRspBase[rbx], rbp     ; set stack base address in PRCB

 

 

        cmp     byte ptr ThNpxState[rsi],LEGACY_STATE_UNUSED ; check if kernel thread

        je      short KiSC30            ; if e, legacy state unused

        fxrstor [rbp]                   ; restore legacy floatingpoint state

 

 

        mov     eax, ThTeb[rsi]         ; compute compatibility mode TEBaddress

        add     eax, CmThreadEnvironmentBlockOffset ;

        mov     rcx, (PcGdt - PcPrcb)[rbx] ; get GDT baseaddress

        mov     KgdtBaseLow + KGDT64_R3_CMTEB[rcx], ax ;set CMTEB base address

        shr     eax, 16                 ;

        mov     KgdtBaseMiddle + KGDT64_R3_CMTEB[rcx], al;

        mov     KgdtBaseHigh + KGDT64_R3_CMTEB[rcx],ah   ;

 

 

        mov     eax, ds                 ; compute sum of segmentselectors

        mov     ecx, es                 ;

        and     eax, ecx                ;

        mov     ecx, gs                 ;

        and     eax, ecx                ;

        cmp     ax, (KGDT64_R3_DATA or RPL_MASK) ; checkif sum matches

        je      short KiSC25            ; if e, sum matches expected value

        mov     ecx, KGDT64_R3_DATA or RPL_MASK ; reloaduser segment selectors

        mov     ds, ecx                 ;

        mov     es, ecx                 ;

 

        mov     eax, (PcSelf - PcPrcb)[rbx] ; get currentPCR address

        mov     edx, (PcSelf - PcPrcb + 4)[rbx] ;

        cli                             ; disableinterrupts

        mov     gs, ecx                 ; reload GS segment selector

        mov     ecx, MSR_GS_BASE        ; get GS base MSR number

        wrmsr                           ; write system PCRbase address

        sti                             ; enableinterrupts

KiSC25: mov     eax, KGDT64_R3_CMTEBor RPL_MASK ; reload FS segment selector

        mov     fs, eax                 ;

        mov     eax, ThTeb[rsi]         ; get low part of user TEB address

        mov     edx, ThTeb + 4[rsi]     ; get high part of user TEB address

        mov     (PcTeb - PcPrcb)[rbx], eax ; set user TEBaddress in PCR

        mov     (PcTeb - PcPrcb + 4)[rbx], edx ;

        mov     ecx, MSR_GS_SWAP        ; get GS base swap MSR number

        wrmsr                           ; write user TEB base address

 

KiSC30: cmp     byte ptrPbDpcRoutineActive[rbx], 0 ; check if DPC active

        jne     short KiSC50            ; if ne, DPC is active

 

        inc     dword ptr ThContextSwitches[rsi] ; threadcount

 

 

        cmp     byte ptr ThApcState +AsKernelApcPending[rsi], TRUE ; check if APC pending

        jne     short KiSC40            ; if ne, kernel APC not pending

        movzx   ax, byte ptr SwApcBypass[rsp] ; get disableIRQL level

        or      ax, ThSpecialApcDisable[rsi] ; mergespecial APC disable

        jz      short KiSC40            ; if z, kernel APC enabled

        mov     ecx, APC_LEVEL          ; request APC interrupt

        call    __imp_HalRequestSoftwareInterrupt ;

        or      rcx, rsp                ; clear ZF flag

KiSC40: setz    al                      ; set return value

        mov     rbp, SwRbp[rsp]         ; restore nonvolatile register

        add     rsp, KSWITCH_FRAME_LENGTH - (1 * 8) ;deallocate stack frame

        ret                             ; return

 

 

KiSC50: xor     r9, r9                  ; clear register

        mov     SwP5Home[rsp], r9       ; set parameter 5

        mov     r8, rsi                 ; set new thread address

        mov     rdx, rdi                ; set old thread address

        mov     ecx, ATTEMPTED_SWITCH_FROM_DPC ; setbugcheck code

        call    KeBugCheckEx            ; bugcheck system - no return

        ret                             ; return

 

        NESTED_END SwapContext,_TEXT$00

 

        end

    光是这段代码就能让我疯掉,而事实上我只解析了一部分就放弃了,因为这段代码有英文注释,我也就没全部翻译过来,但是大体上还是明白了到底如何保存上下文信息。

     从代码里面我们发现虽然系统调用的是KiSwapContext,但是其实系统调用 的SwapContext函数,KiSwapContext函数有两个参数,一个是OldThread,一个是NewThread,代码里,系统通过寄存器获取旧指令的指令指针,内核栈的指针,运行地址空间的指针,然后将这些信息保存到当前线程的内核模式栈中,更新栈指针,然后将栈指针保存在当前线程的KTHREAD块中,然后,内核栈指针被设置成新线程的内核栈,此时,新线程就变成了当前进程,但是此时还没有切换页面映射表,如果新旧线程属于同一个进程,就不需要切换,但是如果不属于同一进程,那么系统会将新进程的页表目录加载到一个专门的处理器寄存器中,从而使用新进程的地址空间,接着就是TEB的切换了,这需要去修改GDT中的TEB项,从之前的内容我们找到各个线程的的TEB段起点地址保存在Teb字段中,在代码里面我发现程序是直接禁掉了APC,但是根据潘爱民先生和毛德操先生对这里的解释不是很清楚,而根据我自己的对代码的理解,在进行上下文切换的时候是禁掉了APC,之后才使能的,而如果上下文切换期间发生了APC请求,就会产生一个IRQL的中断,上下文切换之后就会去检查是否有这个中断,如果有的话是要去执行的,到这里为止,算是比较简单的了解了一下线程的上下文切换了,关于此部分内容真是是很难弄懂,我也是磕磕碰碰的了解了一点。

   3.2进程和线程的优先级问题。

       Windows下线程是从两个角度来分配的,Windows API和Winsows内核,Windows API首先根据进程创建时所分配的攸县级类别来组织线程(数字代表能被内核识别的内部PROCESS_PRIORITY_CLASS_索引):实时:(4),高(3),普通(2),普通之上(6),普通之下(5),空闲(1),未知(0)。

     #define PROCESS_PRIORITY_CLASS_UNKNOWN      0

     #definePROCESS_PRIORITY_CLASS_IDLE         1

     #definePROCESS_PRIORITY_CLASS_NORMAL       2

     #definePROCESS_PRIORITY_CLASS_HIGH         3

     #definePROCESS_PRIORITY_CLASS_REALTIME     4

     #definePROCESS_PRIORITY_CLASS_BELOW_NORMAL 5

     #definePROCESS_PRIORITY_CLASS_ABOVE_NORMAL 6

     然后分配这些进程内部单个线程的相对优先级。这里数字代表了应用于进程基本优先级的差值:时间关键(15),最高(2),普通之上(1),普通(0),普通之下(-1),最低(-2),空闲(-15)。             

#define THREAD_PRIORITY_TIME_CRITICAL 15 

#define THREAD_PRIORITY_HIGHEST    2  

#define THREAD_PRIORITY_ABOVE_NORMAL   (1) 

#define THREAD_PRIORITY_NORMAL  (0)

#define THREAD_PRIORITY_BELOW_NORMA  (-1)

#define THREAD_PRIORITY_LOWEST (-2)

#define THREAD_PRIORITY_IDLE  (-15)

    在内核中,进程优先级类别通过PspPriortyTable表和前面讲的PROCESS_PRIORITY_CLASS索引值,转换成一个基本优先级,这些优先级分别被设为4,8,13,14,6和10,然后,基本的线程优先级被作为一个差值,加到基本的进程优先级上。

const KPRIORITYPspPriorityTable[PROCESS_PRIORITY_CLASS_ABOVE_NORMAL+1] = {8,4,8,13,24,6,10};

 

#define LOW_PRIORITY 0              // Lowest thread priority level

#define LOW_REALTIME_PRIORITY 16    // Lowest realtime priority level

#define HIGH_PRIORITY 31            // Highest thread priority level

#define MAXIMUM_PRIORITY 32         // Number of thread priority levels

此问题算是比较圆满的解答了,我们也知道了进程和线程的优先级是怎么回事了,当让我们可以往下追踪一些很有意思的内容,这里就不在赘述了。

   3.3线程状态

      

关于这个就不详细解释了,很简单的。而且这个不同的操作系统版本给出的解释同,也就无所谓了,源代码如下:

typedef enum _KTHREAD_STATE {

   Initialized,

   Ready,

   Running,

   Standby,

   Terminated,

   Waiting,

   Transition,

   DeferredReady,

   GateWait

} KTHREAD_STATE;

3.3分发器数据库

   讲完了上面的内容,我们还是不知道操作系统去哪儿找就绪队列,接着去执行。接下来我们就去解决这个问题。如果让你来设计操作系统,你会如何做呢,是让一个寄存器保存就绪队列的启示地址,然后给出一个偏移?如果这样的话,是不是得需要好几个寄存器分别取记录,挂起队列,等待队列等等,如果只有三态的话也无所谓了,但是事实上的线程状态远远多于三态,所以,我们应该从全局的高度来做这件事,给一个专门的一组结构去记录这些线程状态的信息,操作系统只需要去访问这些结构,其他的信息就由这些结构去负责统计数据,这样就各司其职,模块化设计了,不多说了,来一张图:

每个处理器不会去管有多少个除就绪外的其他队列,它只关注就绪队列,其余的都不是它的第一选择,所以windows多处理器系统每个处理器都有一个分发器就绪队列数据器,每个CPU只需要去检查它自己的就绪队列来找到下一个要运行的线程,而不必对整个系统范围内进行搜索。

3.4运行时间

   关于这个问题在网上有很多人对此展开了一些讨论,主要问题是windows是基于抢先式多任务系统,但是系统为每个程序分配一定的CPU时间,当程序的运行超过规定时间后,系统就会中断该程序并把CPU控制权转交给别的程序,这不是轮转时间片多任务系统?其实看了代码你就会发现这根本不冲突的,首先多任务系统我们最好的办法就是时间片轮转法了,但是这样的话优先级就没意义了,所以我们才有了抢占式多任务系统的说法。那么每个线程到底运行多久时间?这个问题还真的是难道我了,依据网上和我自己从书上看到的信息来说的话能理解清楚,但是在代码里我还真没找到过这些数据,后来才知道,这些都不是直接固定的,是和硬件有关的,下面我摘引一下《深入理解Windows操作系统》一书中的简要介绍:“时钟间隔的长度随着硬件平台的不同而有所不同,始终中断的平率取决于HAL,而不是内核,例如,大多数x86单处理器系统的时钟间隔是10毫秒左右,而大多数x86和x64多处理器系统则是15毫秒左右,这一时间间隔值保存在内核变量KeMaximuumIncrement中,它的单位是百纳秒。

系统使用处理器周期数为线程运行时间记账,因此,虽然线程运行时间仍是时钟间隔的整数倍,系统却并不使用时钟间隔的数量作为主要依据来衡量一个线程运行了多少时间,或它是否用完它的时限。而是,系统在启动时计算好一个时钟间隔所等价的处理器时钟周期数。计算过程是吧处理器速度的赫兹书(每秒的CPU周期数)乘以单个时钟间隔所对应的秒数(基于前面讲过的KeMaximumIncrement)。”

上述便是潘爱民老师给出的解释了,然后我仔细的在WRK-v1.2里面找相关的代码,很可惜,除了找到一个KeMaximumIncrement外,其余的代码就没看到了,不过之后我还是会去看看的,算是一个遗憾吧。那运行时间到底如何算呢?目标时限值=时限重置值*每时限单元的时钟周期数,其中时限重置值是在KTHREAD结构里面的一个参数,而时限单元值被保存为时钟嘀嗒的1/3,这意味着客户版本的Windows系统上,默认情况下,线程的时限重置值为6(2*3),而在服务器上,时限值为36(12*3)。这些都是理解来的,但是相关代码我看了一下,在很多的地方都是零零散散的存在,这里就不详细的看了,总结一下就是,没有更高优先级来抢占的情况下,一个正在运行的线程在运行完自己的时限的时候是有保障的,但是一旦出现更高优先级的线程来抢占下,就立马让出自己的“位置”给更高优先级的线程。

3.5优先级的改变

优先级是不是一直不变呢?想想都知道肯定是不行的,主要是因为如果一个低优先级的线程一直被抢占,那很有可能一直轮不到它执行,但是如果这个低优先级的线程的结果对某个高优先级的线程很重要呢,想想这种情况,所以在设计的时候,就有一套机制来保证一个低优先级的线程不会处于永远等待的情况。优先级提升的情况有如下几种:

1.由于调度器/分发器时间而提升

2.由于I/O操作完成而提升

3.由于UI输入而提升

4.由于线程等待某个执行体资源的时间太长而提升

5.一个准备运行的线程在相当一段时间内没有被运行过而提升

   这里就不详细介绍了,我也还没完全弄懂,设计的地方不是一个地方,我开篇就讲过,很多东西没有达到那个高度的时候是没办法理解的。算是给自己一个偷懒的理由。

0 0
原创粉丝点击