1. 程式人生 > >作業系統實驗之基於核心棧切換的程序切換

作業系統實驗之基於核心棧切換的程序切換

一 複習Git基礎知識

1.首先複習下git的操作,實驗平臺使用的是實驗樓,沒有開通會員,所以每次需要把程式碼提交到自己建立的分支上面進行儲存,下次需要進行修改的時候,在進行下載
1)初始化git
git init
2) 配置基本資訊
git config –global user.name xxxx
git config –global user.emai xxxx
3) 檢視遠端的分支
git branch -d
4) 選擇分支,進行程式碼同步到本地
git checkout –track origin/分支名
這個會在本地建立選擇分支,同時切換到這個分分支
5) 檢視分支
git status
6) 選擇分支
git checkout 分支名
7) 提交分支程式碼
git push origin 分支名
git add 資料夾名
git commit -m “註釋”
8)刪除分支
  git brance -d 分支名

二 實驗指導書

1.基於TSS進行切換
在現在的Linux 0.11中,真正完成程序切換是依靠任務狀態段(Task State Segment,簡稱TSS)的切換來完成的。具體的說,在設計“Intel架構”(即x86系統結構)時,每個任務(程序或執行緒)都對應一個獨立的TSS,TSS就是記憶體中的一個結構體,裡面包含了幾乎所有的CPU暫存器的映像。有一個任務暫存器(Task Register,簡稱TR)指向當前程序對應的TSS結構體,所謂的TSS切換就將CPU中幾乎所有的暫存器都複製到TR指向的那個TSS結構體中儲存起來,同時找到一個目標TSS,即要切換到的下一個程序對應的TSS,將其中存放的暫存器映像“扣在”CPU上,就完成了執行現場的切換,如下圖所示。
這裡寫圖片描述


Intel架構不僅提供了TSS來實現任務切換,而且只要一條指令就能完成這樣的切換,即圖中的ljmp指令。具體的工作過程是:
(1)首先用TR中存取的段選擇符在GDT表中找到當前TSS的記憶體位置,由於TSS是一個段,所以需要用段表中的一個描述符來表示這個段,和在系統啟動時論述的核心程式碼段是一樣的,那個段用GDT中的某個表項來描述,還記得是哪項嗎?是8對應的第1項。此處的TSS也是用GDT中的某個表項描述,而TR暫存器是用來表示這個段用GDT表中的哪一項來描述,所以TR和CS、DS等暫存器的功能是完全類似的。
(2)找到了當前的TSS段(就是一段記憶體區域)以後,將CPU中的暫存器映像存放到這段記憶體區域中,即拍了一個快照。
(3)存放了當前程序的執行現場以後,接下來要找到目標程序的現場,並將其扣在CPU上,找目標TSS段的方法也是一樣的,因為找段都要從一個描述符表中找,描述TSS的描述符放在GDT表中,所以找目標TSS段也要靠GDT表,當然只要給出目標TSS段對應的描述符在GDT表中存放的位置——段選擇子就可以了,仔細想想系統啟動時那條著名的jmpi 0, 8指令,這個段選擇子就放在ljmp的引數中,實際上就jmpi 0, 8中的8。
(4)一旦將目標TSS中的全部暫存器映像扣在CPU上,就相當於切換到了目標程序的執行現場了,因為那裡有目標程序停下時的CS:EIP,所以此時就開始從目標程序停下時的那個CS:EIP處開始執行,現在目標程序就變成了當前程序,所以TR需要修改為目標TSS段在GDT表中的段描述符所在的位置,因為TR總是指向當前TSS段的段描述符所在的位置。
上面給出的這些工作都是一句長跳轉指令“ljmp 段選擇子:段內偏移”,在段選擇子指向的段描述符是TSS段時CPU解釋執行的結果,所以基於TSS進行程序/執行緒切換的switch_to實際上就是一句ljmp指令:

#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \
    "je 1f\n\t" \
    "movw %%dx,%1\n\t" \
    "xchgl %%ecx,current\n\t" \
    "ljmp *%0\n\t" \
    "cmpl %%ecx,last_task_used_math\n\t" \
    "jne 1f\n\t" \
    "clts\n" \
    "1:" \
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
    "d" (_TSS(n)),"c" ((long) task[n])); \
}

GDT表的結構如下圖所示,所以第一個TSS表項,即0號程序的TSS表項在第4個位置上,4<<3,即48,相當於TSS在GDT表中開始的位置(以位元組為單位),TSS(n)找到的是程序n的TSS位置,所以還要再加上n<<4,即n16,因為每個程序對應有1個TSS和1個LDT,每個描述符的長度都是8個位元組,所以是乘以16,其中LDT的作用就是上面論述的那個對映表,關於這個表的詳細論述要等到記憶體管理一章。TSS(n)=n16+48,得到就是程序n(切換到的目標程序)的TSS選擇子,將這個值放到dx暫存器中,並且又放置到結構體tmp中32位長整數b的前16位,現在64位tmp中的內容是前32位為空,這個32位數字是段內偏移,就是jmpi 0, 8中的0;接下來的16位是n16+48,這個數字是段選擇子,就是jmpi 0, 8中的8,再接下來的16位也為空。所以swith_to的核心實際上就是“ljmp 空, n16+48”,現在和前面給出的基於TSS的程序切換聯絡在一起了。
這裡寫圖片描述
2、基於核心棧的切換
不管使用何種方式進行程序切換(此次實驗不涉及執行緒),總之要實現排程程序的暫存器的儲存和切換,也就是說只要有辦法儲存被排程出cpu的程序的暫存器狀態及資料,再把排程的程序的暫存器狀態及資料放入到cpu的相應暫存器中即可完成程序的切換。由於切換都是在核心態下完成的所以兩個程序之間的tss結構中只有幾個資訊是不同的,其中esp和trace_bitmap是必須切換的,但在0.11的系統中,所有程序的bitmap均一樣,所以也可以不用切換。

排程程序的切換方式修改之前,我們考慮一個問題,程序0不是通過排程執行的,那程序0的上下文是如何建立的?因為在程序0執行時系統中並沒有其他程序,所以程序0的建立模板一定可以為程序棧切換方式有幫助。所以先來分析一下程序0的產生。程序0是在move_to_user_mode巨集之後直接進入的。在這之前一些準備工做主要是task_struct結構的填充。

#define move_to_user_mode() \ 
__asm__ ("movl %%esp,%%eax\n\t" \ 
    "pushl $0x17\n\t" \ 
    "pushl %%eax\n\t" \ 
    "pushfl\n\t" \ 
    "pushl $0x0f\n\t" \ 
    "pushl $1f\n\t" \ 
    "iret\n" \ 
    "1:\tmovl $0x17,%%eax\n\t" \ 
    "movw %%ax,%%ds\n\t" \ 
    "movw %%ax,%%es\n\t" \ 
    "movw %%ax,%%fs\n\t" \ 
    "movw %%ax,%%gs" \ 
    :::"ax")

這裡0x17表示使用者資料庫,0x10表示核心資料段,就是改變現在段寄存的值,改變選擇子
①需要對PCB資料結構進行補充,增加記錄當前任務一個指標指向棧空間,如下所示,同時需要注意的是:在system_call.s中有對task的硬編碼,需要進行修改,如下所示:

struct task_struct {
/* these are hardcoded - don't touch */
    long state; /* -1 unrunnable, 0 runnable, >0 stopped */
    long counter;
    long priority;
    /* 增加利用堆疊進行任務切換的,需要記錄當前任務的棧起始位,
    注意的是:linux0.11中棧和task在同一頁中,位於這頁的高地址中/
    unsigned long kernelstack;
    long signal;
    struct sigaction sigaction[32];
    long blocked;   /* bitmap of masked signals */
/* various fields */
    int exit_code;
    unsigned long start_code,end_code,end_data,brk,start_stack;
    long pid,father,pgrp,session,leader;
    unsigned short uid,euid,suid;
    unsigned short gid,egid,sgid;
    long alarm;
    long utime,stime,cutime,cstime,start_time;
    unsigned short used_math;
/* file system info */
    int tty;        /* -1 if no tty, so it must be signed */
    unsigned short umask;
    struct m_inode * pwd;
    struct m_inode * root;
    struct m_inode * executable;
    unsigned long close_on_exec;
    struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
    struct desc_struct ldt[3];
/* tss for this task */
    struct tss_struct tss;
};

修改system_call.s如下:

KERNEL_STACK    = 12 //kernelstack的偏移
state   = 0     
counter = 4
priority = 8
signal  = 16    //修改
sigaction = 20  //修改    
blocked = (37*16)

②修改task結構,同時需要對程序0的修改,如下:

#define INIT_TASK \
/* state etc */ { 0,15,15, /*新增*/PAGE_SIZE+(long)&init_task,\
/* signals */   0,{{},},0, \
/* ec,brk... */ 0,0,0,0,0,0, \
/* pid etc.. */ 0,-1,0,0,0, \
/* uid etc */   0,0,0,0,0,0, \
/* alarm */ 0,0,0,0,0,0, \
/* math */  0, \
/* fs info */   -1,0022,NULL,NULL,NULL,0, \
/* filp */  {NULL,}, \
    { \
        {0,0}, \
/* ldt */   {0x9f,0xc0fa00}, \
        {0x9f,0xc0f200}, \
    }, \
/*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
     0,0,0,0,0,0,0,0, \
     0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
     _LDT(0),0x80000000, \
        {} \
    }, \
}

③現在就需要對棧空間進行初始化,保證兩套棧可以正常轉化,如下如所示,初始化為中斷棧空間資料一樣,修改fork.c如下:
這裡寫圖片描述

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, 
        long ebx,long ecx,long edx, 
        long fs,long es,long ds, 
        long eip,long cs,long eflags,long esp,long ss) 
{ 
    /*melon - 新增用來取得核心棧指標*/ 
    long * krnstack; 
    /*melon added End*/ 
    struct task_struct *p; 
    int i; 
    struct file *f; 

    p = (struct task_struct *) get_free_page(); 
    if (!p) 
        return -EAGAIN; 

    /*melon  -取得當前子程序的核心棧指標*/ 
    krnstack=(long)(PAGE_SIZE+(long)p); //實際上程序每次進入核心,棧頂都指向這裡。
    /*melon added End*/ 
    task[nr] = p; 
    *p = *current;    /* NOTE! this doesn't copy the supervisor stack */ 
    p->state = TASK_UNINTERRUPTIBLE; 
    p->pid = last_pid; 
    p->father = current->pid; 
    p->counter = p->priority; 
    //初始化核心棧內容,由於系統不再使用tss進行切換,所以核心棧內容要自已安排好 
    //下面部分就是進入核心後int之前入棧內容,即使用者態下的cpu現場
    *(--krnstack) = ss & 0xffff; //儲存使用者棧段暫存器,這些引數均來自於此次的函式呼叫, 
                      //即父程序壓棧內容,看下面關於tss的設定此處和那裡一樣。 
    *(--krnstack) = esp; //儲存使用者棧頂指標 
    *(--krnstack) = eflags; //儲存標識暫存器 
    *(--krnstack) = cs & 0xffff; //儲存使用者程式碼段暫存器 
    *(--krnstack) = eip; //儲存eip指標資料,iret時會出棧使用 ,這裡也是子程序執行時的語句地址。即if(!fork()==0) 那裡的地址,由父程序傳遞
    //下面是iret時要使用的棧內容,由於排程發生前被中斷的程序總是在核心的int中,
    //所以這裡也要模擬中斷返回現場,這裡為什麼不能直接將中斷返回時使用的
    //return_from_systemcall地址加進來呢?如果完全模仿可不可以呢?
    //有空我會測試一下。
    //根據老師的視訊講義和實驗指導,這裡儲存了段暫存器資料。
    //由switch_to返回後first_return_fromkernel時執行,模擬system_call的返回 
    *(--krnstack) = ds & 0xffff; 
    *(--krnstack) = es & 0xffff; 
    *(--krnstack) = fs & 0xffff; 
    *(--krnstack) = gs & 0xffff; 
    *(--krnstack) = esi; 
    *(--krnstack) = edi; 
    *(--krnstack) = edx; 
    *(--krnstack) = ecx; //這三句是我根據int返回棧內容加上去的,後來發現不加也可以
                //但如果完全模擬return_from_systemcall的話,這裡應該要加上。
    //*(--krnstack) = ebx; 
    //*(--krnstack) = 0; //此處應是返回的子程序pid//eax; 
   //其意義等同於p->tss.eax=0;因為tss不再被使用, 
   //所以返回值在這裡被寫入棧內,在switch_to返回前被彈出給eax; 

    //switch_to的ret語句將會用以下地址做為彈出進址進行執行 
    *(--krnstack) = (long)first_return_from_kernel; 
    //*(--krnstack) = &first_return_from_kernel; //討論區中有同學說應該這樣寫,結果同上
    //這是在switch_to一起定義的一段用來返回使用者態的彙編標號,也就是 
    //以下是switch_to函式返回時要使用的出棧資料
    //也就是說如果子程序得到機會執行,一定也是先 
    //到switch_to的結束部分去執行,因為PCB是在那裡被切換的,棧也是在那裡被切換的, 
    //所以下面的資料一定要事先壓到一個要執行的程序中才可以平衡。 
    *(--krnstack) = ebp; 
    *(--krnstack) = eflags; //新新增 
    *(--krnstack) = ecx; 
    *(--krnstack) = ebx; 
    *(--krnstack) = 0; //這裡的eax=0是switch_to返回時彈出的,而且在後面沒有被修改過。 
            //此處之所以是0,是因為子程序要返回0。而返回資料要放在eax中,
            //由於switch_to之後eax並沒有被修改,所以這個值一直被保留。
            //所以在上面的棧中可以不用再壓入eax等資料。
            //將核心棧的棧頂儲存到核心指標處 
    p->kernelstack=krnstack; //儲存當前棧頂 
    //p->eip=(long)first_switch_from;
        //上面這句是第一次被排程時使用的地址 ,這裡是後期經過測試後發現系統修改
        //後會發生不定期宕機,經分析後認為是ip不正確導致的,但分析是否正確不得
        //而知,只是經過這樣修改後問題解決,不知其他同學是否遇到這個問題。
    /*melon added End*/

④樣子已經做好了,現在就需要修改schedule.c

struct task_struct * pnext=&(init_task.task); 
//儲存需要切換的PCB,
//注意這裡初始化必須為程序0的地址,因為:但沒有程序可以切換的時候,就會呼叫0號程序,進行pause()操作
…
while (1) { 
        c = -1; 
        next = 0; 
        /*為pnext賦初值,讓其總有值可用。*/ 
        pnext=task[next]; //最初我並沒有加這句,導致如果系統沒有程序可以排程時傳遞進去的是一個空值,系統宕機,所以加上這句,這樣就可以在next=0時不會有空指標傳遞。
        /**/
i = NR_TASKS; 
        p = &task[NR_TASKS]; 
        while (--i) { 
            if (!*--p) 
                continue; 
            if ((*p)->state == TASK_RUNNING && (*p)->counter > c) 
                c = (*p)->counter, next = i,pnext=*p; //儲存要排程到的pcb指標
        } 
        if (c) break; 
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 
            if (*p) 
                (*p)->counter = ((*p)->counter >> 1) + 
                        (*p)->priority; 
    } 
    /*調度程序到執行態*/ 
    if(task[next]->pid != current->pid) 
    { 
        //判斷當前正在執行的程序狀態是否為TASK_RUNNING, 
        //如果是,則表明當前的程序是時間片到期被搶走的,這時當前程序的狀態還應是TASK_RUNNING, 
        //如果不是,則說明當前程序是主動讓出CPU,則狀態應為其他Wait狀態。 
        if(current->state == TASK_RUNNING) 
        { 
            //記錄當前程序的狀態為J,在此處,當前程序由執行態轉變為就緒態。 
            fprintk(3,"%ld\t%c\t%ld\n",current->pid,'J',jiffies); 
        } 
        fprintk(3,"%ld\t%c\t%ld\n",pnext->pid,'R',jiffies); 
    } 
    /**/ 

    //switch_tss(next); //由於此次實驗難度還是挺高的,所以一般不會一次成功,所以我沒有將switch_to巨集刪除,而只是將其改了一個名字,這樣,如果下面的切換出問題,就切換回來測試是否是其他程式碼出問題了。如果換回來正常,則說明問題就出現在下面的切換上。這樣可以減少盲目地修改。
    switch_to(pnext,_LDT(next));

⑤最後編寫我們的重點新增到system_call.s檔案中,switch_to方法,這個需要對程序進行精確控制,需要利用匯編語言進行編寫,如下所示:

.align 2 
switch_to: 
    pushl %ebp 
    movl %esp,%ebp        #上面兩條用來調整C函式棧態 
    pushfl            #將當前的核心eflags入棧!!!! 
    pushl %ecx 
    pushl %ebx 
    pushl %eax 
    movl 8(%ebp),%ebx    #此時ebx中儲存的是第一個引數switch_to(pnext,LDT(next)) 
    cmpl %ebx,current    #此處判斷傳進來的PCB是否為當前執行的PCB 
    je 1f            #如果相等,則直接退出 
    #切換PCB 
    movl %ebx,%eax        #ebx中儲存的是傳遞進來的要切換的pcb 
    xchgl %eax,current    #交換eax和current,交換完畢後eax中儲存的是被切出去的PCB 
    #TSS中核心棧指標重寫 
    movl tss,%ecx        #將全域性的tss指標儲存在ecx中 
    addl $4096,%ebx        #取得tss儲存的核心棧指標儲存到ebx中 
    movl %ebx,ESP0(%ecx)    #將核心棧指標儲存到全域性的tss的核心棧指標處esp0=4 
    #切換核心棧 
    movl %esp,KERNEL_STACK(%eax)    #將切出去的PCB中的核心棧指標存回去 
    movl $1f,KERNEL_EIP(%eax)    #將1處地址儲存在切出去的PCB的EIP中!!!! 
    movl 8(%ebp),%ebx    #重取ebx值, 
    movl KERNEL_STACK(%ebx),%esp    #將切進來的核心棧指標儲存到暫存器中 
#下面兩句是後來新增的,實驗指導上並沒有這樣做。
    pushl KERNEL_EIP(%ebx)        #將儲存在切換的PCB中的EIP放入棧中!!!! 
    jmp  switch_csip        #跳到switch_csip處執行!!!! 
#    原切換LDT程式碼換到下面
#    原切換LDT的程式碼在下面 
1:    popl %eax 
    popl %ebx 
    popl %ecx 
    popl %ebp 
#該語句用來出棧呼叫進行核心態到使用者態進行轉化處,
# first_return_from_kernel,這個是在fork.c中新增的
    ret            
.align 2
first_return_from_kernel:
    popl %edx
    popl %edi
    popl %esi
    pop  %gs
    pop  %fs
    pop  %es
    pop  %ds
    iret

整體的棧空間記憶體圖如下所示:
這裡寫圖片描述

在呼叫的過程中需要注意的是:
1)組合語言中定義的方法可以被其他呼叫需要:
.globl first_return_from_kernel
2) tss需要在sched.c中定義:
struct tss_struct *tss= &(init_task.task.tss);
3)fork.c中,schdule()方法中向棧中新增函式,需要宣告這個函式:
extern void first_return_from_kernel(void);