[轉] 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寄存器了。
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