進程切換與TSS

[轉] http://www.eefocus.com/article/09-06/74895s.html

Intel i386 體系結構包括了一個特殊的段類型,叫任務狀態段(TSS),如圖5.4所示。每個任務包含有它自己最小長度爲104字節的TSS段,在/include/ i386/processor.h 中定義爲tss_struct結構:

struct tss_struct {
unsigned short back_link,__blh;
unsigned long esp0;
unsigned short ss0,__ss0h;/*0級堆棧指針,即Linux中的內核級 */
unsigned long esp1;
unsigned short ss1,__ss1h; /* 1級堆棧指針,未用*/
unsigned long esp2;
unsigned short ss2,__ss2h; /* 2級堆棧指針,未用*/
unsigned long __cr3;
unsigned long eip;
unsigned long eflags;
unsigned long eax,ecx,edx,ebx;
unsigned long esp;
unsigned long ebp;
unsigned long esi;
unsiged long edi;
unsigned short es, __esh;
unsigned short cs, __csh;
unsigned short ss, __ssh;
unsigned short ds, __dsh;
unsigned short fs, __fsh;
unsigned short gs, __gsh;
unsigned short ldt, __ldth;
unsigned short trace, bitmap;
unsigned long io_bitmap[IO_BITMAP_SIZE+1];
/*
* pads the TSS to be cacheline-aligned (size is 0x100)
*/
unsigned long __cacheline_filler[5];
};

 每個TSS有它自己 8字節的任務段描述符(Task State Segment Descriptor ,簡稱TSSD)。這個描述符包括指向TSS起始地址的32位基地址域,20位界限域,界限域值不能小於十進制104(由TSS段的最小長度決定)。TSS描述符存放在GDT中,它是GDT中的一個表項。

後面將會看到,Linux在進程切換時,只用到TSS中少量的信息,因此Linux內核定義了另外一個數據結構,這就是thread_struct 結構

struct thread_struct {
unsigned long esp0;
unsigned long eip;
unsigned long esp;
unsigned long fs;
unsigned long gs;
/* Hardware debugging registers */
unsigned long debugreg[8]; /* %%db0-7 debug registers */
/* fault info */
unsigned long cr2, trap_no, error_code;
/* floating point info */
union i387_union i387;
/* virtual 86 mode info */
struct vm86_struct * vm86_info;
unsigned long screen_bitmap;
unsigned long v86flags, v86mask, v86mode, saved_esp0;
/* IO permissions */
int ioperm;
unsigned long io_bitmap[IO_BITMAP_SIZE+1];
};

用這個數據結構來保存cr2寄存器、浮點寄存器、調試寄存器及指定給Intel 80x86處理器的其他各種各樣的信息。需要位圖是因爲ioperm( ) 及 iopl( )系統調用可以允許用戶態的進程直接訪問特殊的I/O端口。尤其是,如果把eflag寄存器中的IOPL 域設置爲3,就允許用戶態的進程訪問對應的I/O訪問權位圖位爲0的任何一個I/O端口。

那麼,進程到底是怎樣進行切換的?

從第三章我們知道,在中斷描述符表(IDT)中,除中斷門、陷阱門和調用門外,還有一種“任務們”。任務門中包含有TSS段的選擇符。當CPU因中斷而穿過一個任務門時,就會將任務門中的段選擇符自動裝入TR寄存器,使TR指向新的TSS,並完成任務切換。CPU還可以通過JMP或CALL指令實現任務切換,當跳轉或調用的目標段(代碼段)實際上指向GDT表中的一個TSS描述符項時,就會引起一次任務切換。

Intel的這種設計確實很周到,也爲任務切換提供了一個非常簡潔的機制。但是,由於i386的系統結構基本上是CISC的,通過JMP指令或CALL(或中斷)完成任務的過程實際上是“複雜指令”的執行過程,其執行過程長達300多個CPU週期(一個POP指令佔12個CPU週期),因此,Linux內核並不完全使用i386CPU提供的任務切換機制。

由於i386CPU要求軟件設置TR及TSS,Linux內核只不過“走過場”地設置TR及TSS,以滿足CPU的要求。但是,內核並不使用任務門,也不使用JMP或CALL指令實施任務切換內核只是在初始化階段設置TR,使之指向一個TSS,從此以後再不改變TR的內容了。也就是說,每個CPU(如果有多個CPU)在初始化以後的全部運行過程中永遠使用那個初始的TSS。同時,內核也不完全依靠TSS保存每個進程切換時的寄存器副本,而是將這些寄存器副本保存在各個進程自己的內核棧中(參見上一章task_struct結構的存放)。

這樣以來,TSS中的絕大部分內容就失去了原來的意義。那麼,當進行任務切換時,怎樣自動更換堆棧?我們知道,新任務的內核棧指針(SS0和ESP0)應當取自當前任務的TSS,可是,Linux中並不是每個任務就有一個TSS,而是每個CPU只有一個TSS。Intel原來的意圖是讓TR的內容(即TSS)隨着任務的切換而走馬燈似地換,而在Linux內核中卻成了只更換TSS中的SS0和ESP0,而不更換TSS本身,也就是根本不更換TR的內容。這是因爲,改變TSS中SS0和ESP0所化的開銷比通過裝入TR以更換一個TSS要小得多。因此,在Linux內核中,TSS並不是屬於某個進程的資源,而是全局性的公共資源。在多處理機的情況下,儘管內核中確實有多個TSS,但是每個CPU仍舊只有一個TSS。

5.4.2 進程切換

前面所介紹的schedule()中調用了switch_to宏,這個宏實現了進程之間的真正切換,其代碼存放於include/ i386/system.h:

1 #define switch_to(prev,next,last) do { \
2 asm volatile("pushl %%esi\n\t" \
3 "pushl %%edi\n\t" \
4 "pushl %%ebp\n\t" \
5 "movl %%esp,%0\n\t" /* save ESP */ \
6 "movl %3,%%esp\n\t" /* restore ESP */ \
7 "movl $1f,%1\n\t" /* save EIP */ \
8 "pushl %4\n\t" /* restore EIP */ \
9 "jmp __switch_to\n" \
10 "1:\t" \
11 "popl %%ebp\n\t" \
12 "popl %%edi\n\t" \
13 "popl %%esi\n\t" \
14 :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
15 "=b" (last) \
16 :"m" (next->thread.esp),"m" (next->thread.eip), \
17 "a" (prev), "d" (next), \
18 "b" (prev)); \
19 } while (0)

switch_to宏是用嵌入式彙編寫成,比較難理解,爲描述方便起見,我們給代碼編了行號,在此我們給出具體的解釋:

· thread的類型爲前面介紹的thread_struct結構。
· 輸出參數有三個,表示這段代碼執行後有三項數據會有變化,它們與變量及寄存器的對應關係如下:
0%與prev->thread.esp對應,1%與prev->thread.eip對應,這兩個參數都存放在內存,而2%與ebx寄存器對應,同時說明last參數存放在ebx寄存器中。
· 輸入參數有五個,其對應關係如下:
3%與next->thread.esp對應,4%與next->thread.eip對應,這兩個參數都存放在內存,而5%,6%和7%分別與eax,edx及ebx相對應,同時說明prev,next以及prev三個參數分別放在這三個寄存器中。表5.1列出了這幾種對應關係:

· 第2~4行就是在當前進程prev的內核棧中保存esi,edi及ebp寄存器的內容。
· 第5行將prev的內核堆棧指針ebp存入prev->thread.esp中。
· 第6行把將要運行進程next的內核棧指針next->thread.esp置入esp寄存器中。從現在開始,內核對next的內核棧進行操作,因此,這條指令執行從prev到next真正的上下文切換,因爲進程描述符的地址與其內核棧的地址緊緊地聯繫在一起(參見第四章),因此,改變內核棧就意味着改變當前進程。如果此處引用current的話,那就已經指向next的task_struct結構了。從這個意義上說,進程的切換在這一行指令執行完以後就已經完成。但是,構成一個進程的另一個要素是程序的執行,這方面的切換尚未完成。
· 第7行將標號“1”所在的地址,也就是第一條popl指令(第11行)所在的地址保存在prev->thread.eip中,這個地址就是prev下一次被調度運行而切入時的“返回”地址。
· 第8行將next->thread.eip壓入next的內核棧。那麼,next->thread.eip究竟指向那個地址?實際上,它就是 next上一次被調離時通過第7行保存的地址,也就是第11行popl指令的地址。因爲,每個進程被調離時都要執行這裏的第7行,這就決定了每個進程(除了新創建的進程)在受到調度而恢復執行時都從這裏的第11行開始。
· 第9行通過jump指令(而不是 call指令)轉入一個函數__switch_to()。這個函數的具體實現將在下面介紹。當CPU執行到__switch_to()函數的ret指令時,最後進入堆棧的next->thread.eip就變成了返回地址,這就是標號“1”的地址。
· 第11~13行恢復next上次被調離時推進堆棧的內容。從現在開始,next進程就成爲當前進程而真正開始執行。

下面我們來討論__switch_to()函數。

在調用__switch_to()函數之前,對其定義了fastcall :

extern void FASTCALL(__switch_to(struct task_struct *prev, struct task_struct *next));

fastcall對函數的調用不同於一般函數的調用,因爲__switch_to()從寄存器(如表5.1)取參數,而不像一般函數那樣從堆棧取參數,也就是說,通過寄存器eax和edx把prev和next 參數傳遞給__switch_to()函數。

 

void __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
struct thread_struct *prev = &prev_p->thread,
*next = &next_p->thread;
struct tss_struct *tss = init_tss + smp_processor_id();
unlazy_fpu(prev_p);/* 如果數學處理器工作,則保存其寄存器的值*/
/* 將TSS中的內核級(0級)堆棧指針換成next->esp0,這就是next 進程在內核
棧的指針
tss->esp0 = next->esp0;
/* 保存fs和gs,但無需保存es和ds,因爲當處於內核時,內核段
總是保持不變*/
asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));
asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));
/*恢復next進程的fs和gs */
loadsegment(fs, next->fs);
loadsegment(gs, next->gs);
/* 如果next掛起時使用了調試寄存器,則裝載0~7個寄存器中的6個寄存器,其中第4、5個寄存器沒有使用 */
if (next->debugreg[7]){
loaddebug(next, 0);
loaddebug(next, 1);
loaddebug(next, 2);
loaddebug(next, 3);
/* no 4 and 5 */
loaddebug(next, 6);
loaddebug(next, 7);
}
if (prev->ioperm || next->ioperm) {
if (next->ioperm) {
/*把next進程的I/O操作權限位圖拷貝到TSS中 */
memcpy(tss->io_bitmap, next->io_bitmap,
IO_BITMAP_SIZE*sizeof(unsigned long));
/* 把io_bitmap在tss中的偏移量賦給tss->bitmap */
tss->bitmap = IO_BITMAP_OFFSET;
} else
/*如果一個進程要使用I/O指令,但是,若位圖的偏移量超出TSS的範圍,
* 就會產生一個可控制的SIGSEGV信號。第一次對sys_ioperm()的調用會
* 建立起適當的位圖 */
tss->bitmap = INVALID_IO_BITMAP_OFFSET;
}
}

從上面的描述我們看到,儘管Intel本身爲操作系統中的進程(任務)切換提供了硬件支持,但是Linux內核的設計者並沒有完全採用這種思想,而是用軟件實現了進程切換,而且,軟件實現比硬件實現的效率更高,靈活性更大。

 

-----------------------------------------------------

[轉] http://www.linuxidc.com/Linux/2011-03/33367.htm

tss的作用舉例:保存不同特權級別下任務所使用的寄存器,特別重要的是esp,因爲比如中斷後,涉及特權級切換時(一個任務切換),首先要切換棧,這個棧顯然是內核棧,那麼如何找到該棧的地址呢,這需要從tss段中得到,這樣後續的執行纔有所依託(在x86機器上,c語言的函數調用是通過棧實現的)。只要涉及地特權環到高特權環的任務切換,都需要找到高特權環對應的棧,因此需要esp2,esp1,esp0起碼三個esp,然而Linux只使用esp0。

tss是什麼:tss是一個段,段是x86的概念,在保護模式下,段選擇符參與尋址,段選擇符在段寄存器中,而tss段則在tr寄存器中。

intel的建議:爲每一個進程準備一個獨立的tss段,進程切換的時候切換tr寄存器使之指向該進程對應的tss段,然後在任務切換時(比如涉及特權級切換的中斷)使用該段保留所有的寄存器。

Linux的做法:

1.Linux沒有爲每一個進程都準備一個tss段,而是每一個cpu使用一個tss段,tr寄存器保存該段。進程切換時,只更新唯一tss段中的esp0字段到新進程的內核棧。

2.Linux的tss段中只使用esp0和iomap等字段,不用它來保存寄存器,在一個用戶進程被中斷進入ring0的時候,tss中取出esp0,然後切到esp0,其它的寄存器則保存在esp0指示的內核棧上而不保存在tss中。

3.結果,Linux中每一個cpu只有一個tss段,tr寄存器永遠指向它。符合x86處理器的使用規範,但不遵循intel的建議,這樣的後果是開銷更小了,因爲不必切換tr寄存器了。

4.在Linux中,對於同一個CPU,所有的進程都使用一個TSS,只是在進程切換時, 被切換到的進程將會把自己的ESP0保存到TSS.ESP0中去,那爲什麼不把自己的SS0也保存到TSS.SS0中 呢,這是因爲所有進程的SS0都是統一的,爲內核的SS,而內核在初始化的時候,已經將該TSS.SS0設置爲自己的SS,因此無需繼續設置SS0。


Linux的實現:

1.定義tss:
struct tss_struct init_tss[NR_CPUS] __cacheline_aligned = { [0 ... NR_CPUS-1] = INIT_TSS };(arch/i386/kernel/init_task.c)
INIT_TSS定義爲:
#define INIT_TSS  {                            \
    .esp0        = sizeof(init_stack) + (long)&init_stack,    \
    .ss0        = __KERNEL_DS,                    \
    .esp1        = sizeof(init_tss[0]) + (long)&init_tss[0],    \
    .ss1        = __KERNEL_CS,                    \
    .ldt        = GDT_ENTRY_LDT,                \
    .io_bitmap_base    = INVALID_IO_BITMAP_OFFSET,            \
    .io_bitmap    = { [ 0 ... IO_BITMAP_LONGS] = ~0 },        \
}

 

 

http://www.linuxidc.com/Linux/2011-03/33367.htm

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章