簡單的進程調度
Linux0.11的進程調度代碼在kernel/sched.c中,主要由四個函數實現,分別是schedule(),sleep_on(),wake_up(),switch_to()。
下面看看進程的數據結構:
struct task_struct {
/* these are hardcoded - don't touch */
long state; /* -1 unrunnable, 0 runnable, >0 stopped */
long counter;
long priority;
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;
};
進程創建時,優先級priority被賦一個初值,一般爲 0~70之間的數字,這個數字同時也是計數器counter的初值,就是說進程創建時兩者是相等的。字面上看,priority是"優先級"、 counter是"計數器"的意思,然而實際上,它們表達的是同一個意思-進程的"時間片"。Priority代表分配給該進程的時間片,counter 表示該進程剩餘的時間片。在進程運行過程中,counter不斷減少,而priority保持不變,以便在counter變爲0的時候(該進程用完了所分配的時間片)對counter重新賦值。當一個普通進程的時間片用完以後,並不馬上用priority對counter進行賦值,只有所有處於可運行狀態的普通進程的時間片(p->counter==0)都用完了以後,才用priority對counter重新賦值,這個普通進程纔有了再次被調度的機會。這說明,普通進程運行過程中,counter的減小給了其它進程得以運行的機會,直至counter減爲0時才完全放棄對CPU的使用,這就相對於優先級在動態變化,所以稱之爲動態優先調度。至於時間片這個概念,和其他不同操作系統一樣的,Linux的時間單位也是"時鐘滴答",只是不同操作系統對 一個時鐘滴答的定義不同而已(Linux爲10ms)。進程的時間片就是指多少個時鐘滴答,比如,若priority爲20,則分配給該進程的時間片就爲 20個時鐘滴答,也就是20*10ms=200ms。Linux中某個進程的調度策略(policy)、優先級(priority)等可以作爲參數由用戶自己決定,具有相當的靈活性。內核創建新進程時分配給進程的時間片缺省爲200ms(更準確的應爲210ms),用戶可以通過系統調用改變它。
下面我們首先來看schedule()函數,該函數首先檢查任務數組中的所有任務,對每一個任務先檢查它的報警定時器alarm,如果時間已經過期,則設置信號位圖位SIGALRM。
接着檢查信號位圖,如果出去被堵塞的信號外還有其他信號,並且任務是可中斷的,則把任務狀態設置爲可運行。
接着檢查每一個任務的時間值counter,選擇值最大的一個。
如果所有任務的時間值都爲0,則要根據優先級重新計算時間片,然後繼續循環。
最後通過函數switch_to()切換到所選擇任務號的進程執行。
void schedule(void)
{
int i,next,c;
struct task_struct ** p;
/* check alarm, wake up any interruptible tasks that have got a signal */
//檢查任務的狀態
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) {
if ((*p)->alarm && (*p)->alarm < jiffies) {
(*p)->signal |= (1<<(SIGALRM-1));
(*p)->alarm = 0;
}
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE)
(*p)->state=TASK_RUNNING;
}
/* this is the scheduler proper: */
//找到需要切換執行的任務的任務號
while (1) {
c = -1;
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;
}
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(next);//切換到該任務執行
}
如果要讓某個等待的隊列睡眠,則調用sleep_on()函數,比如在
tatic inline void wait_on_buffer(struct buffer_head * bh)
{
cli();
while (bh->b_lock)
sleep_on(&bh->b_wait);
sti();
}
需要當前進程睡眠,且調用sleep_on()函數即可。
參數 p是需要睡眠隊列的頭指針,要使當前進程睡眠,把他的狀態設置爲TASK_UNINTERRUPTIBLE,然後調用調度函數繼續執行,直到該隊列被喚醒,則會恢復到調度點繼續執行。
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
tmp = *p;
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
schedule();
if (tmp)//被喚醒的返回點,繼續執行
tmp->state=0;
}
在這裏構築了一個隱士的等待隊列:因爲tmp爲一個局部的變量,所以在當前進程空間堆棧上。
current Task C Task B Task A
|
tmp |
|
|
tmp |
|
|
tmp |
Task_struct |
|
tmp |
|
我們來看當前進程調用了sleep_on(&bh->b_wait)函數,並且bh->b_wait爲task C,則在緩衝區的等待進程的隊列上有task A,task B,task C;所以進入sleep_on()函數後,通過局部變量tmp指向了task C,則形成了隱士的隊列。然後把當前進程狀態設置成不可中斷的,通過*p = current;使bh->b_wait指向當前的進程。
隨後,調用schedule()自願放棄該進程對CPU的使用。
當其他某一進程調用了wake_up()函數後,則當前進程(liwei: 應爲當前掛起進程)被設置成就緒態,等待調度,然後把等待隊列的頭指針設置爲NULL。
如果當前進程(liwei: 應爲當前掛起進程)被調度到了, cpu會跳到switch_to中ljmp後面的那條指令,然後從schedule返回 (TSS中保存的EIP是ljmp的下一條指令地址)。執行if (tmp) tmp->state=0; ,
則當前進程把下一個進程task C 設置成可運行態,參與系統的調度。而當前進程繼續執行。
喚醒函數就是把睡眠的任務的狀態設置爲0(運行態)。
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0;
*p=NULL;
}
}
下面在講述任務切換之前,我們要補充其它的知識。
一、i386體系結構與kernel 存儲管理
提到進程管理就必須首先了解基本的i386體系結構和存儲管理。這是因爲體系結構決定了存儲管理的實現,而進程管理又與存儲管理密不可分。在現代操作系統中,存儲管理一般都採用虛擬存儲的方式,也就是系統使用的地址空間與實際物理地址空間不同,是“虛擬”的地址。處理器提供一定的機制將“虛擬”的地址轉換爲實際的地址。操作系統的存儲管理就是基於處理器提供的地址轉換機制來實現的。
基本的存儲管理有兩種方式,即段式和頁式。段式管理就是將內存劃分爲不同的段(segment),通過段指針來訪問各個段的方法。在比較早的系統中,如pdp-11等就是採用這種方法。這種方法的缺點是在設計程序時必須考慮段的劃分,這是很不方便的。頁式管理就是將內存劃分爲固定大小的若干個頁面(page),以頁面爲單位分配使用。通過頁映射表完成地址轉換。
i386 的存儲管理採用兩級虛擬的段頁式管理,就是說它先分段,再分頁。具體地說,它通過gdt(Global Descriptor Table)和ldt(Local Descriptor Table)進行分段,把虛擬地址轉換爲線性地址。然後採用兩級頁表結構進行分頁,把線性地址轉換爲物理地址。
在Linux中,操作系統處於處理器的0特權級,通過設置gdt,將它的代碼和數據放在獨立的段中,以區別於供用戶使用的用戶數據段和代碼段。所有的用戶程序都使用相同的數據段和代碼段,也就是說所有的用戶程序都處於同一個地址空間中。程序之間的保護是通過建立不同的頁表映射來完成的。
段變換:
首先段寄存器中放的是選擇符,指令所在的位置叫做偏移值。
通過選擇符,在GDT表中找到相應的描述符項,把描述符項中的基地址加上偏移值就得到線形地址,
頁變換:
然後把線形地址通過頁目錄和頁表找到相應的物理地址。
描述符爲:struct desc_struct {long a,b;};下面爲代碼段和數據段的描述符:
31 23 15 7 0
(BASE)位31..24 |
g |
x |
o |
Avl |
Limit 位19..16 |
p |
dpl |
1 |
type |
(BASE)位23..16 |
段基地址(BASE)位15..0 |
段限長(LIMIT)位15..0 |
BASE :32位的基地址;
LIMIT:20位長度;
如果g爲0,則以1字節爲單元;
如果g爲1,則以4K字節爲單元,段限長左移12位;
g(granularity)粒度位,指示LIMIT的單元類型;
type:區分不同類型的描述符;
dpl:descriptor privilege level描述符特權級0和3級;
p:present bit段存在位,該位爲1指示描述符存在;
一個描述符項共8字節。
線形地址的具體格式爲:
31 22 21 12 11 0
頁目錄(DIR) |
頁(PAGE) |
偏移值(OFFSET) |
高10位爲頁目錄的偏移值;
中間10位爲頁表項的偏移值;
低12位爲偏移值,該偏移爲在4K的物理頁面的偏移
二、 進程管理
進程是一個程序的一次執行的過程,是一個動態的概念。在i386體系結構中,任務和進程是等價的概念。進程管理涉及了系統初始化、進程創建/消亡、進程調度 以及進程間通訊等等問題。在Linux的內核中,進程實際上是一組數據結構,包括進程的上下文、調度數據、信號處理、進程隊列指針、進程標識、時間數據、 信號量數據等。這組數據都包括在進程控制塊PCB(Process Control Block)中。
Linux進程管理與前面的 i386體系結構關係十分密切。前面已經討論了i386所採用的段頁式內存管理的基本內容,實際上,i386中的段還有很多用處。例如,在進程管理中要用到一種特殊的段,就是任務狀態段tss(Task Status Segment)。每一個進程都必須擁有自己的tss,這是通過設置正確的tr寄存器來完成的。根據i386體系結構的定義,tr寄存器中存放的是tss 的選擇符(selector),該選擇符必須由gdt中的項(即描述符descriptor)來映射。同樣的,進程的ldt也有這種限制,即ldtr對應的選擇符也必須由gdt中的項來映射。
在kernel中,爲了滿足i386體系結構的這種要求,採用了預先分配進程所需要的gdt項目的方法。也就是爲每一個進程保留2項gdt條目。進程PCB中 的tr和ldtr值就是它在gdt中的選擇符。進程與它的gdt條目的對應關係可以由task數組來表示。
下面是詳細的分析。
1.系統初始化
在Linux中,一些與進程管理相關的數據結構是在系統初始化的時候被初始化的。其中最重要的是gdt和進程表task。
Gdt的初始化主要是確定需要爲多少個進程保留空間,也就是需要多大的gdt。
在boot/head.s文件中完成GDT表,IDT表和一個頁目錄和4個頁表的安裝;
部分程序代碼分析:
_pg_dir: /*頁目錄存放的位置,爲物理內存的絕對地址0x0000處*/
call setup_idt /*設置中斷描述符表*/
call setup_gdt /*設置gdt描述符表*/
…………………………..
jmp after_page_tables /*安裝頁目錄和頁表*/
setup_gdt: /* gdt描述符表開始處*/
lgdt gdt_descr /*load 全局描述符表寄存器*/
ret
gdt_descr: /*下面兩行是lgdt的6字節操作數:長度,基址。*/
.word 256*8-1 /*佔用2個字節,表示長度爲 2143=256*8-1個字節*/
.long _gdt /* 佔用4個字節,表示gdt的基地址*/
/*linux 0.11的全局表,前4項分別是:空項(不用)、內核代碼段描述符、內核數據段描述符、系統段描述符,其中系統段描述符沒有使用。後面還留有252項的空間,用於存放任務的局部描述符(LDT)和對應的任務狀態段(TSS)描述符*/
/*(0-null, 1-cs, 2-ds, 3-sys, 4-TSS0, 5-LDT0, 6-TSS1, 7-LDT1….)*/
_gdt: .quad 0x0000000000000000 /* NULL descriptor */
.quad 0x
.quad 0x
.quad 0x0000000000000000 /* TEMPORARY - don't use */
.fill 252,8,0 /* space for LDT's and TSS's etc */
/*252項,每項8字節,填0*/
我們算一下內核代碼段的長度最大長度是不是
而0x
而頁目錄和頁表的具體安裝代碼爲:
etup_paging: /*首先對5頁內存(1頁目錄和4頁頁表)清0。*/
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl /*頁目錄在地址0x000,本程序的開始的標號既是*/
/*下面代碼在頁目錄項中設置4個頁表的屬性和地址*/
movl $pg0+7,_pg_dir /* set present bit/user r/w */
movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
movl $pg2+7,_pg_dir+8 /* --------- " " --------- */
movl $pg3+7,_pg_dir+12 /* --------- " " --------- */
/*下面代碼填寫4個頁表的頁表項的內容,共有:4(頁表)*1024(項)=4096項, 每4K一個頁面,則能映射物理內存4096*4K=
每項的內容爲:當前映射的物理內存地址+該頁的標誌(7)。
使用方法是從最後一個頁表的最後一項開始按倒退順序填寫。一個頁表的最後一項在頁表中的位置是1023*4=4092。因此最後一頁的最後一項的位置就是$pg3+4092.
*/
/*物理內存也是從最後一個頁面的開始地址(
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
/*設置頁目錄基地址寄存器cr3的值,指向頁目錄表*/
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
/*啓動分頁機制,cr0的PG位31爲1*/
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
從這裏ret過後,先前被push進堆棧的main函數彈出運行。偉大的征程開始了!!!
初始化完成後,內核模塊在內存中的位置爲:
. |
|
Lib 模塊代碼 |
|
fs 模塊代碼 |
|
mm模塊代碼 |
|
Kernel 模塊代碼 |
|
mian.c程序代碼 |
|
全局描述符表(2k) |
|
中斷描述符表(2k) |
|
Head.s程序中部分代碼 |
|
軟盤緩衝區(1K) |
|
0x4000 |
|
0x3000 |
|
0x2000 |
|
0x1000 |
|
0x0000 |
其中綠色部分爲head.s程序的代碼,也就是說內存中的最低部分的代碼爲head的原因了。
進程表task實際上是一個PCB指針數組,其定義如下: Struct task_struct *task[NR_TASKS] = {&init_task,};
其中,init_task是系統的第0號進程,也是所有其它進程的父進程。系統在初始化的時候,必須手工設置這個進程,把它加到進程指針表中去,才能啓動進程管理機制。可以看出,這裏task的大小同樣依賴於NR_TASKS的定義。
2.進程創建
在Linux 中,進程是通過系統調用fork創建的,新的進程是原來進程的子進程。需要說明的是,不存在真正意義上的線程 (Thread)。Linux中常用的線程pthread實際上是通過進程來模擬的。也就是說linux中的線程也是通過fork創建的,是“輕”進程。 fork系統調用的流程如下:
fork()->system_call(kernel/system_call.s)->sys_fork(kernel/system_call.s)-> find_empty_process()->copy_process()
看看sys_fork的代碼:
_sys_fork:
call _find_empty_process
testl %eax,%eax
js
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process
addl $20,%esp
1: ret
所以在sys_fork中是調用兩個C函數來完成進程的創建的,find_empty_process()(kernel/fork.c)主要是用來在全局task表中找到一個空閒項,並返回該task數組中的標號,同時增長進程的last_pid(最新進程號)。
拷貝進程用於創建並複製進程的代碼段和數據段和環境。在進程複製過程中,主要牽涉進程數據結構中信息的設置。系統首先爲新創建的進程在主內存區中申請一頁內存來存放其任務數據結構信息並複製當前進程任務數據結構中的所有內容作爲新進程任務數據結構的模板。
然後對複製的任務數據結構進行修改。把當前進程設置爲新進程的父進程,清除信號位圖並復位新進程各統計值。接着根據當前任務進程設置任務狀態段(TSS)中各寄存器的值。由於創建時新進程返回值應該爲0,所以要設置tss.eax=0。新進程內核態堆棧指針tss.esp0被設置成新任務數據結構所在內存頁面的頂端,而堆棧段tss.ss0被設置成內核數據段選擇符。Tss.ldt設置爲局部描述符在GDT中的索引。
此後系統設置新任務的代碼和數據段基址、限長並複製當前進程內存分頁管理的頁表。如果父進程中有文件是打開的,則將對應文件的打開次數增1。接着在GDT中設置新任務的TSS和LDT描述符項,其中基地址信息指向新進程任務結構中的tss和ldt。最後再將新任務設置成可運行狀態並返回新進程號。
其中參數nr是調用find_empty_process()分配的任務數組項號。none是system_call.s中調用sys_call_table時壓入堆棧的返回地址。
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)
{
struct task_struct *p;
int i;
struct file *f;
p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
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;
p->signal = 0;
p->alarm = 0;
p->leader = 0; /* process leadership doesn't inherit */
p->utime = p->stime = 0;
p->cutime = p->cstime = 0;
p->start_time = jiffies;
//以下設置任務狀態段TSS所需要的數據
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p;//內核態堆棧指針,指向任務所在頁的頂端.
//ss0:esp0用作進程在內核態工作時的堆棧。
p->tss.ss0 = 0x10; //堆棧段選擇符,與內核數據段相同
p->tss.eip = eip;
p->tss.eflags = eflags;
p->tss.eax = 0;
p->tss.ecx = ecx;
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
p->tss.ebp = ebp;
p->tss.esi = esi;
p->tss.edi = edi;
p->tss.es = es & 0xffff;
p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
p->tss.ldt = _LDT(nr);//設置新任務的局部描述符表的選擇符(LDT描述符在GDT中)
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
//設置新任務的局部描述符ldt的代碼和數據段基址、限長並複製頁表。
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
//父進程中有文件是打開的,則將文件的計數增1
for (i=0; i<NR_OPEN;i++)
if (f=p->filp[i])
f->f_count++;
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
//在GDT中設置新任務的TSS和LDT描述符項,數據從task數組中取。
//當任務切換時,任務寄存器tr由CPU自動加載(tr中存放的是TSS在GDT中的選擇符)。
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
p->state = TASK_RUNNING; /* do this last, just in case */
return last_pid;
}
比較關鍵的步驟是:
a)將新進程的PCB加入到進程表中。從前面的討論可以知道,進程表 task的大小是預先定義的,所以首先要從task中找到一個空的表項:nr = find_empty_process()。如果不能找到空的表項,說明系統已經達到了最大進程數的上限,不能創建新進程。返回的nr是空閒表項的下標。
b)將父進程的地址空間複製到子進程中。在這一步中,還要複製父進程的ldt到子進程,然後在gdt中建立子進程的ldt描述符(descriptor),並將這個選擇符保存在PCB中。
爲子進程設置tss。同時在gdt中建立子進程的tss描述符,並將這個選擇符保存在PCB中。
拷貝內存函數首先取得當前進程的代碼段限長和數據段限長,從ldt中取得代碼段和數據段的基地址。然後計算新進程的基址,每一個進程的線形空間大小是
然後設置新進程的代碼段和數據段的基地址,並複製代碼和數據段。
int copy_mem(int nr,struct task_struct * p)
{
unsigned long old_data_base,new_data_base,data_limit;
unsigned long old_code_base,new_code_base,code_limit;
code_limit=get_limit(0x
data_limit=get_limit(0x17);//取局部描述符表中數據段描述符項ldt[2]的限長;
//從ldt[1]這個描述符項中取代碼段基址。
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
if (data_limit < code_limit)
panic("Bad data_limit");
new_data_base = new_code_base = nr * 0x4000000;// 基址=進程號*
p->start_code = new_code_base;
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return 0;
}
再來看看怎麼取得局部描述符表中代碼段或者數據段的基址,get_base()函數是一個宏,把ldt轉換成所在內存中的地址,結合描述符結構各字節的含義直接操作內存地址。
#define get_base(ldt) _get_base( ((char *)&(ldt)) )
/*從地址addr處描述符中取段基地址。.
edx—存放基地址base;%1—地址addr偏移2;%2—地址addr偏移4;%3—地址addr偏移7;
*/
#define _get_base(addr) ({/
unsigned long __base; /
__asm__("movb %3,%%dh/n/t" / //取[addr+7]處基地址高16位中的高8位 (位31-24)àdh
"movb %2,%%dl/n/t" / //取[addr+4]處基地址高16位中的低8位 (位23-16)àdl
"shll $16,%%edx/n/t" / //基地址高16位移到edx中高16位處。
"movw %1,%%dx" / //取[addr+2]基址base低16位 (位15-0)àdx
:"=d" (__base) / //從而edx中含有32位的段基地址。
:"m" (*((addr)+2)), /
"m" (*((addr)+4)), /
"m" (*((addr)+7))); /
__base;})
下面我們看看在GDT中怎麼設置新進程的TSS描述符項。
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
爲什麼用gdt加上一個數字了,因爲gdt的描述符項是8個字節爲一項的,所以gdt+4就直接跳到了任務0的TSS的描述符的地址處,nr<<1=nr*2表示一個任務要兩個描述符項(TSS和LDT)。而&(p->tss)表示新進程的TSS的地址。所以我們可以猜測在全局描述符表(GDT)中,新任務的TSS描述符項的基地址部分就是進程的tss所在的地址。
在全局描述符表中設置任務狀態段描述符。
//n—是該描述符的指針;addr—是描述符中的基地址值;任務狀態段描述符的類型是0x89。
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
//參數: n –在全局表中描述符項n所對應的地址;addr—狀態段/局部表所在內存的基地址;type--描述符中的標誌類型字節。
%0-eax(地址addr);%1--(描述符項n的地址);%2--(描述符項n的地址偏移2處);
%3-(描述符項n的地址偏移4處);%4-(描述符項n的地址偏移5處);
%5-(描述符項n的地址偏移6處);%6-(描述符項n的地址偏移7處);
#define _set_tssldt_desc(n,addr,type) /
__asm__ ("movw $104,%1/n/t" / //將TSS長度放入描述符長度域(第0-1字節)
"movw %%ax,%2/n/t" / //將基地址的低字放入描述符第2-3字節。
"rorl $16,%%eax/n/t" / //將基地址高字移入ax中。
"movb %%al,%3/n/t" / //將基地址高字中低字節移入描述符第4字節。
"movb $" type ",%4/n/t" / //將標誌類型字節移入描述符第5字節。
"movb $0x00,%5/n/t" / //描述符第6字節置0。
"movb %%ah,%6/n/t" / //將基地址高字中高字節移入描述符第7字節。
"rorl $16,%%eax" / //eax清0。
::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), /
"m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) /
)
下面是描述符各字節所代表的意思:
31 23 15 7 0
(BASE)位31..24 |
g |
x |
o |
Avl |
Limit 位19..16 |
p |
dpl |
1 |
type |
(BASE)位23..16 |
段基地址(BASE)位15..0 |
段限長(LIMIT)位15..0 |
基地址由下面偏移組成:描述符項n的地址偏移2處,描述符項n的地址偏移4處,描述符項n的地址偏移7處。
類型:描述符項n的地址偏移5處。
段限長:描述符項n的地址,描述符項n的地址偏移6處。
3. 進程調度 這裏我們暫不討論進程調度的算法,只是看一看進程切換時應該做的工作。
進程切換的思想:跳轉到一個任務的TSS段選擇符組成的地址處會造成CPU進行任務切換操作。
//輸入:%0—偏移地址(*&__tmp.a); %1—存放新TSS的選擇符;
dx—新任務n的TSS段選擇符; ecx—新任務指針task[n].
//其中臨時數據結構__tmp用於組建遠跳轉(far jump)指令的操作數。該操作數由4字節偏移地址和2字節段選擇符組成。因此__tmp中a的值是32位偏移值,而b的低2字節是新tss段的選擇符(高2字節不用),跳轉到TSS段選擇符會造成任務切換到該TSS對應的進程。對於造成任務的長跳轉,a值無用。內存間接跳轉指令使用6字節操作數作爲跳轉目的地的長指針,
其格式爲:jmp 16位段選擇符:32位偏移值。但在內存操作中操作數的表示順序與這裏正好相反。
#define switch_to(n) {/
struct {long a,b;} __tmp; /
__asm__("cmpl %%ecx,_current/n/t"/ //新任務n是當前任務嗎?(current==task[n])
"je
"movw %%dx,%1/n/t" / //將新任務16位選擇符存入__tmp.b中。
"xchgl %%ecx,_current/n/t" / //current=task[n];ecx=被切換出的任務。
"ljmp %0/n/t" / //執行長跳轉到*&__tmp,造成任務切換。
"cmpl %%ecx,_last_task_used_math/n/t" / //在任務切換回來後執行。
"jne
"clts/n" /
"1:" /
::"m" (*&__tmp.a),"m" (*&__tmp.b), /
"d" (_TSS(n)),"c" ((long) task[n])); /
}
ljmp %0可以解釋爲:ljmp *&__tmp.a
因爲__tmp是一個結構struct {long a,b;} __tmp; 所以在內存中是相臨的,則跳轉爲:ljmp *&__tmp.a,*&__tmp.b因爲組成一個地址是這樣的
ljmp selector:offset ;所以選擇符是16位的,offset是32位。
這是就是邏輯地址到線形地址的尋址。
那麼這個時候selector就是在全局描述符的偏移,也就是該TSS描述符項相對GDT開始的偏移字節。
INTEL CPU判斷這是一個TSS的描述符項的時候,會造成任務切換到該TSS對應的進程執行,就不需要偏移地址了。
LINUX任務的的兩個堆棧:
任務的內核態堆棧:和task_struct在同一個物理頁面,且在copy_process時,p->tss.ss0=0x10;爲內核態堆棧的堆棧段的選擇符和內核數據段相同, p->tss.esp0= PAGE_SIZE + (long) p; 爲內核態堆棧的堆棧指針指向PCB所在頁面的頂部;
使用:在任務通過系統調用進入內核態時,用到內核態堆棧。
任務的用戶態堆棧: 位於任務進程空間的末端,圖示:
代碼 |
數據 |
Bss |
堆棧 |
參數入棧部分 |
參數和環境變量 |
堆棧指針
此圖顯示爲fork()產生的進程空間的示意圖,黃色部分表示堆棧中已經有了內容,該邏輯地址已經和實際的物理地址對應上了,前面灰色的部分表示邏輯地址還沒有對應上物理地址。當程序執行時發生頁錯誤,產生中斷,則調用函數page_fault()(mm/page.s),該函數區分是缺少頁面或者是共享頁面引發的錯誤,如果是缺頁,則調用do_no_page()申請頁面,否則調用write protect (寫時保護)函數do_wp_page()分配頁面。
堆棧指針p->tss.esp,堆棧段p->tss.ss.調用fork()後和父進程的堆棧空間共享。如果在子進程中通過execve執行了一個程序,則重新計算進程的用戶態堆棧指針。
使用:用戶態運行時使用的堆棧。
用fork創建進程
除了進程0,其它所有的進程都是fork產生的。子進程是通過複製父進程的數據和代碼產生的。創建結束後,子進程和父進程的代碼段、數據段共享。但是子進程有自己的進程控制塊、內核堆棧和頁表。
我們知道一個進程需要有如下3個結構
1. task[]數組中的一項,即進程控制塊(task_struct)
2. GDT中的兩項,即TSS段和LDT段描述符
3. 頁目錄和頁表
所以fork()的任務就是爲一個新進程構造這3個結構。
sys_fork() 系統調用的實現在2個文件中。fork.c中的全部和system_call.s中208-291行。sys_fork()系統調用分成2步完成,第一步調用函數find_empty_process(),在task[]數組中找一項空閒項,第二步調用copy_process() 函數,複製進程。
原ss 原esp eflags cs eip ds es fs edx ecx ebx 硬件自動入棧 中斷處理程序入棧 Call的返回地址 gs esi edi ebp eax sys_fork()程序入棧 Call的返回地址 find_empty_process()的返回值,task[]數組空閒項的下標 call sys_call_table(,%eax,4)的返回地址。因爲是段內短轉移,所以只要把eip入棧
_sys_fork:
// 第一步,調用find_empty_process()函數,找task[]中的空閒項。
// 找到後數組下標放在eax中。如果沒找到直接跳轉到ret指令。
call _find_empty_process
testl %eax,%eax
js
push %gs // 中斷時沒有入棧的寄存器入棧,
// 作爲copy_process() 函數的參數
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
// 第二步,調用copy_process() 函數複製進程。
call _copy_process
addl $20,%esp
1: ret
程序調用copy_process() 函數時,
當前進程內核堆棧的情況如下:
有了這個圖就可以很好的理解進程從用戶態進入內核態時,進程內核堆棧的變化了。這個圖是一位oldlinux論壇上的網友畫的。
我們來看一下move_to_user_mode()這個宏是怎樣實現從內核態到用戶態的切換的。
#define move_to_user_mode() /
__asm__ ("movl %%esp,%%eax/n/t" /
"pushl $0x17/n/t" /
"pushl %%eax/n/t" /
"pushfl/n/t" /
"pushl $0x
"pushl $
"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")
5個push對應ss esp eflags cs eip
(1)任務0的用戶態堆棧段選擇符:SS=0X17。
(2)任務0的用戶堆棧棧頂指針:ESP=當前內核程序堆棧棧頂指針。
(3)CPU狀態=當前內核程序運行時CPU狀態
(4)任務0的代碼段選擇符CS=0x
(5)任務0的指令指針EIP指向任務0的初始化指令處(即move_to_user_mode()的第9行,爲”1 :”標號處)
Iret 返回後,ds,es,fs,gs的選擇符都爲0x17=0001 0111b,根據下面的理論:
每個任務都有自己的局部描述符表LDT。其中保存着該任務的代碼段和數據段描述符。而數據段描述符的選擇符是0x17,即
0b0001,0111
比特0,1表示該選擇符的請求者特權級RPL;
比特2指明示是當前局部表;
比特3--15是局部表中描述符的索引值。索引值2指向數據段(ldt[2])。
所以任務0轉到用戶態運行後就設置了自己的各個寄存器的值了!
參考參考文獻:
1、 <<intel 80386 programmer’s reference manual>> 1986
2、 <<linux0.11 內核完全註釋>>
隨後,調用schedule()自願放棄該進程對CPU的使用。