作業系統實驗之基於核心棧切換的程序切換
一 複習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);