kernel 3.10内核源码分析--内核栈及堆栈切换
生活随笔
收集整理的這篇文章主要介紹了
kernel 3.10内核源码分析--内核栈及堆栈切换
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
1、概念
Linux中有3種棧:
1)用戶棧。當進程處于用戶態時使用,位于進程地址空間(用戶態部分(如:0-0xc0000000))底部,用戶態分配局部變量和函數調用時時,使用該棧,跟平時我們見到和理解的一樣,就是虛擬地址空間中的一段。
2)內核棧。跟用戶棧獨立,屬于進程,即每個進程都有自己的內核棧,單獨分配,大小為8k,跟thread_info結構放在一起,在用戶態和內核態切換時,需要進行切換。
3)中斷棧。老版本內核中默認認跟內核棧共享,新版本內核中與內核棧獨立,且軟中斷和硬中斷單獨使用自己的中斷棧。中斷、異常、軟中斷使用此棧。 ? ?
本文主要講解內核棧、用戶棧和內核棧切換的相關實現。
2、實現
1)內核棧定義和實現
內核棧跟thread_info結構放在一起,共用一個union:thread_union,
union thread_union {
? ? struct thread_info thread_info;
? ? unsigned long stack[THREAD_SIZE/sizeof(long)];
};
2)其它與內核棧相關的定義
a、task_struct.thread.sp0
任務描述符(task_struct)中的thread成員(thread_struct)用于保存進程上下文信息,包含主要寄存器信息,在進程上下文切換(如調度)時使用,thread成員定義為thread_struct結構體,其中的sp0成員用于保存內核棧指針。
b、task_struct.stack
任務描述符(task_struct)中的stack成員是新版本內核中新加入的成員,同樣指向內核棧頂,用于替代老版本中的thread_info成員,由于thread_info和內核棧實際是放在一起的,共享同一個聯合體thread_union,可以相互轉換,所以該修改并無本質區別。
/*進程描述符,每個進程(線程)都由此結構描述*/
struct task_struct {
...
/*Fixme:新內核版本中去除了thread_info成員,用這個代替(thread_info和內核棧放在一起)*/
void *stack;
...
/*進程上下文,包含主要寄存器信息,在進程上下文切換時使用*/
struct thread_struct thread;
...
}
struct thread_struct {
...
/*內核堆棧(與thread_info放在一起,共享thread_union聯合體,大小為8k)的指針。TSS中有相應的字段,在特權級發生變化時,硬件會自動從TSS中讀取sp0,并進行堆棧切換。*/
unsigned long????sp0;
...
}
c、kernel_stack per-CPU變量
用于指向當前CPU上運行的進程的內核棧,由于內核棧與thread_info是放在一起的,所以,內核中也用這個變量來獲取當前進程的thread_info:
/*
??* 取當前進程的thread_info,該信息與內核棧(或中斷棧)公用聯合體(thread_union或irq_ctx),
??* 且作為per-CPU變量(kernel_stack)放在了指定區域。
??*/
static inline struct thread_info *current_thread_info(void)
{
struct thread_info *ti;
ti = (void *)(this_cpu_read_stable(kernel_stack) +
?????KERNEL_STACK_OFFSET - THREAD_SIZE);
return ti;
}
d、tss_struct.sp0
TSS任務狀態段是X86架構中包含的一個特殊的段,用戶保存硬件上下文,包含了當前進程的特權級(ring)信息和寄存器信息。在進程切換時使用,Linux中使用的情況比較少,而用戶棧和內核棧的切換就是其中一處關鍵的應用。Linux內核定義了tss_struct結構體來描述該段中的內容,其中的x86_tss(x86_hw_tss結構)中保存了相應的硬件狀態信息,其中sp0即為內核態(ring0)中的堆棧指針,ss0為內核態堆棧段寄存器,sp為用戶態(ring3)堆棧指針,Linux中主要使用了這幾個字段。
/*用于描述TSS段中的內容*/
struct tss_struct {
/*
* The hardware state:
*/
/*硬件狀態信息*/
struct x86_hw_tss????x86_tss;
/*
* The extra 1 is there because the CPU will access an
* additional byte beyond the end of the IO permission
* bitmap. The extra byte must be all 1 bits, and must
* be within the limit.
*/
/*IO權位圖*/
unsigned long????io_bitmap[IO_BITMAP_LONGS + 1];
/*
* .. and then another 0x100 bytes for the emergency kernel stack:
*/
/*備用內核棧*/
unsigned long????stack[64];
} ____cacheline_aligned;
/* This is the TSS defined by the hardware. */
struct x86_hw_tss {
unsigned short????back_link, __blh;
unsigned long????sp0; /*內核棧指針*/
unsigned short????ss0, __ss0h; /*內核棧段描述符*/
unsigned long????sp1;
/* ss1 caches MSR_IA32_SYSENTER_CS: */
unsigned short????ss1, __ss1h;
unsigned long????sp2;
unsigned short????ss2, __ss2h;
unsigned long????__cr3;
unsigned long????ip;
unsigned long????flags;
unsigned long????ax;
unsigned long????cx;
unsigned long????dx;
unsigned long????bx;
unsigned long????sp; /*用戶態棧指針*/
unsigned long????bp;
unsigned long????si;
unsigned long????di;
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;
unsigned short????io_bitmap_base;
} __attribute__((packed))
內核中定義針對每個CPU定義了一個了tss_struct結構體類型變量init_tss,在進程上下文切換(堆棧切換)時使用
/*
?* per-CPU TSS segments. Threads are completely 'soft' on Linux,
?* no more per-task TSS's. The TSS size is kept cacheline-aligned
?* so they are allowed to end up in the .data..cacheline_aligned
?* section. Since TSS's are completely CPU-local, we want them
?* on exact cacheline boundaries, to eliminate cacheline ping-pong.
?*/
DEFINE_PER_CPU_SHARED_ALIGNED(struct tss_struct, init_tss) = INIT_TSS;
INIT_TSS定義如下:
/*
?* Note that the .io_bitmap member must be extra-big. This is because
?* the CPU will access an additional byte beyond the end of the IO
?* permission bitmap. The extra byte must be all 1 bits, and must
?* be within the limit.
?*/
#define INIT_TSS {???? \
.x86_tss = {???? \
.sp0????= sizeof(init_stack) + (long)&init_stack, \
.ss0????= __KERNEL_DS,???? \
.ss1????= __KERNEL_CS,???? \
.io_bitmap_base????= INVALID_IO_BITMAP_OFFSET,???? \
},???? \
.io_bitmap????= { [0 ... IO_BITMAP_LONGS] = ~0 },???? \
}
init_stack定義為:
#define init_stack????(init_thread_union.stack)
即將內核棧(sp0)指向了init_thread_union的內核棧頂。
init_thread_union為初始的任務描述符:
union thread_union init_thread_union __init_task_data =
{ INIT_THREAD_INFO(init_task) };
struct task_struct init_task = INIT_TASK(init_task);
#define INIT_TASK(tsk)????\
{????\
.state????= 0,????\
.stack????= &init_thread_info,????\
.usage????= ATOMIC_INIT(2),????\
.flags????= PF_KTHREAD,????\
.prio????= MAX_PRIO-20,????\
.static_prio????= MAX_PRIO-20,????\
.normal_prio????= MAX_PRIO-20,????\
.policy????= SCHED_NORMAL,????\
.cpus_allowed????= CPU_MASK_ALL,????\
.nr_cpus_allowed= NR_CPUS,????\
.mm????= NULL,
...
}
3)內核棧分配
內核在何時分配?
答案是在fork時。
Linux中進程均由fork創建(init除外),在fork時即會創建相應的內核棧,相應流程為:
do_fork->copy_process->dup_task_struct->alloc_thread_info_node
/*分配thread_info,即分配內核棧*/
static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,
?int node)
{
/*THREAD_SIZE_ORDER為1,即2頁,即內核棧和thread_info共用的空間大小為8k*/
struct page *page = alloc_pages_node(node, THREADINFO_GFP_ACCOUNTED,
????THREAD_SIZE_ORDER);
return page ? page_address(page) : NULL;
}
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
...
/*分配thread_info,即分配內核棧*/
ti = alloc_thread_info_node(tsk, node);
...
/*設置新進程的內核棧(stack成員指向)為新分配的thread_info,如此即設置好了內核棧*/
tsk->stack = ti;
...
}
4)用戶棧和內核棧切換
我們知道,x86硬件結構中,SS為堆棧段寄存器,ESP為堆棧指針寄存器,而這兩個寄存器在每個CPU上都是唯一的,但是Linux中有多種棧(用戶棧、內核棧、中斷棧),如果都需要使用的話,那就必須在各個棧之間進行切換。
當CPU運行的特權級(ring)發生變化時,就需要切換相應的堆棧,Linux中,只使用了兩個ring:ring0(內核態)和ring3(用戶態),當發生內核態和用戶態間的狀態切換時,需要考慮堆棧的切換,通常的切換時機有:系統調用、中斷和異常及其返回處。
前面我們已經了解了內核棧的概念和定義,那么如何進行堆棧切換呢?那就需要使用上面描述的TSS段了,如之前所述,tss_struct中包括了內核棧指針sp0和內核堆棧段寄存器ss0。
(1)用戶棧到內核棧的切換
X86硬件結構中,中斷、異常和系統調用都是通過中斷門或陷阱門實現的,在通過中斷門或陷阱門時,硬件會自動利用TSS,完成堆棧切換的工作。硬件完成的操作包括:
? a、找到ISR的入口。具體包括:確定中斷或異常相關的中斷向量、讀取IDTR寄存器獲取IDT表地址、從IDT表中讀取中斷向量對應的項、讀取GDTR寄存器獲取GDT表地址,在GDT表中查找IDT表項中的段選擇符標識的段描述符,該描述符中指定了中斷/異常處理程序(ISR)所在的段基址,結合IDT表項中的段偏移地址,即可找到ISR的入口地址。
? b、權限檢查。確認中斷的來源是否合法,主要比對當前特權級(CS寄存器的低兩位)和段描述符(GDT中)的DPL比較。如果CPL小于DPL,就產生GP(通用保護)異常。對于異常,還需做進一步的安全檢查:比對CPL與IDT中的門描述符的DPL,如果DPL小于CPL,也產生GP(通用保護)異常,由此可以避免用戶應用程序訪問特殊的陷阱門和中斷門。?
? c、檢查特權級的變化。如果ring發生了變化(通常是從ring3到ring0,即用戶態切換到內核態),即CPL與段描述符的DPL不同,則進行一下處理(這里實際上進行了堆棧切換):?
????c1. 讀tr寄存器,獲取TSS段。?
????c2. 讀取TSS中的新特權級(內核態)的堆棧段和堆棧指針,將其load到SS和ESP寄存器。?
????c3. 在新特權級的棧(內核棧)中保存原始的(即用戶態的)SS和ESP的值。
? d. 如果發生的是異常,則將引起異常的指令地址裝載cs和eip寄存器,如此可以使這條指令在異常處理程序執行完后能被再次執行,這也是中斷和異常的主要區別之一;如果發生的中斷,則跳過此步驟。
? e. 在新棧(內核棧)中壓入eflag、cs和eip。?
? f. 如果是異常且產生了硬件出錯碼,則將它壓入棧中。?
? g. 用之前通過IDT和GDT獲取到的ISR的入口地址和段選擇符裝載EIP和CS寄存器,如此即可開始執行ISR。?
再次強調:上述操作均由硬件自動完成。從上述的步驟c可以看出,當用戶態切換到內核態時,用戶棧切換到內核棧實質是由硬件自動完成的,軟件需要做的是預先設置好TSS中的相關內容(比如sp0和ss0)。
(2)內核棧到用戶棧的切換
該過程實際是“用戶棧到內核棧切換”的逆過程,發生時機在系統調用、中斷和異常的返回處,實際的切換過程還是由硬件自動完成的。
中斷或異常返回時,必然會執行iret指令,然后將控制器交回給之前被中斷打斷的進程,硬件自動完成如下操作:
??a. 從當前棧(內核棧)中彈出cs、eip和eflag,并load到相應的寄存器中寄存器。(如之前有硬件錯誤碼入棧,需要先彈出這個錯誤碼)。?
??b. 權限檢查。比對ISR的CPL是否等于cs中的低兩位的值。如果是,iret終止返回;否則,轉入下一步。?
??c. 從當前棧(內核棧)中彈出之前壓入的用戶態堆棧相關的ss和esp,并load到相應寄存器,至此,即完成了從內核棧到用戶棧的切換。?
? d. 后續處理。主要包括:檢查ds、es、fs及gs段寄存器,如果其中一個寄存器包含的選擇符是一個段描述符,并且其DPL值小于CPL,那么,清相關的段寄存器。目的是為了防止用戶態的程序利用內核以前所用的段寄存器,以防止惡意用戶程序利用其訪問內核地址空間。?
(3)進程上下文切換時的堆棧切換
內核調度時,當被選中的next進程不是current進程時,會發生上下文切換,此時也必然會涉及堆棧切換。這里涉及到兩個相關的問題:
a、從current進程的堆棧切換到next進程的堆棧
由于調度肯定發生在內核態,那么進程上下文切換時,也必然處于內核態,那么此時的堆棧切換實質為current進程的內核棧到next進程內核棧的切換。相應工作在switch_to宏中用匯編實現,通過next進程的棧指針(內核態)裝載到ESP寄存器中實現。
? ? "movl %[next_sp],%%esp\n\t" /* restore ESP ? */ \
具體代碼如下:
/*
??* 上下文切換,在schedule中調用,current進程調度出去,當該進程被再次調度到時,重新從__switch_to后面開始執行
??* prev:被替換的進程
??* next:被調度的新進程
??* last:當切換回原來的進程(prev)后,被替換的另外一個進程。
??*/
#define switch_to(prev, next, last)????\
do {????\
/*????\
* Context-switching clobbers all registers, so we clobber????\
* them explicitly, via unused output variables.????\
* (EAX and EBP is not listed because EBP is saved/restored????\
* explicitly for wchan access and EAX is the return value of????\
* __switch_to())????\
*/????\
unsigned long ebx, ecx, edx, esi, edi;????\
\
asm volatile("pushfl\n\t"????/* save flags */????/*將eflags寄存器值壓棧*/\
????"pushl %%ebp\n\t"????/* save EBP */????/*將EBP壓棧*/\
/*將當前棧指針(內核態)保存到prev進程的thread.sp中*/
????"movl %%esp,%[prev_sp]\n\t"????/* save ESP */ \
????/*將next進程的棧指針(內核態)裝載到ESP寄存器中*/
????"movl %[next_sp],%%esp\n\t"????/* restore ESP */ \
????/*保存"標號1"的地址到prev進程的thread.ip,以便當prev進程重新被調度運行時,可以從"標號1處"重新開始執行*/
????"movl $1f,%[prev_ip]\n\t"????/* save EIP */????\
????/*
?????????* 將next進程的IP(通常都是"標號1"的地址,因為通常都是經歷過這里的調度過程的,上一行代碼中即保存了這個IP)
???* 壓入當前的(即next進程的)堆棧中。結合后面的jmp指令(注意:不是call指令)一起理解,當__switch_to執行完ret返回時,
???* 會自動從當前的堆棧中彈出該地址作為函數的返回地址接著執行,如此即可實現新進程的運行。
???????????????????????*/
????"pushl %[next_ip]\n\t"????/* restore EIP */????\
????__switch_canary????\
????/*
?????????*jmp到__switch_to函數執行,當此函數返回時,自動跳轉到[next_ip]開始執行,實現新進程的調度。注意不是call,jmp指令
?????????* 不會自動將當前地址壓棧,call會自動壓棧
?????????*/
????"jmp __switch_to\n"????/* regparm call */????\
????/*當prev進程再次被調度到時,從這里開始執行*/
????"1:\t"????\
????/*恢復EBP*/
????"popl %%ebp\n\t"????/* restore EBP */????\
????/*恢復eflags*/
????"popfl\n"????/* restore flags */????\
\
????/* output parameters */????\
????: [prev_sp] "=m" (prev->thread.sp),????\
??????[prev_ip] "=m" (prev->thread.ip),????\
??????"=a" (last),????\
\
??????/* clobbered output registers: */????\
??????"=b" (ebx), "=c" (ecx), "=d" (edx),????\
??????"=S" (esi), "=D" (edi)????\
??????\
??????__switch_canary_oparam????\
\
??????/* input parameters: */????\
????: [next_sp] "m" (next->thread.sp),????\
??????[next_ip] "m" (next->thread.ip),????\
??????\
??????/* regparm parameters for __switch_to(): */????\
??????[prev] "a" (prev),????\
??????[next] "d" (next)????\
\
??????__switch_canary_iparam????\
\
????: /* reloaded segment registers */????\
"memory");????\
} while (0)
b、TSS中內核棧(sp0)的切換
由于Linux的具體實現中,TSS不是針對每進程,而是針對每CPU的,即每個CPU對應一個tss_struct,那在進程上下文切換時,需要考慮當前CPU上TSS中的內容的更新,其實就是內核棧指針的更新,更新后,當新進程再次進入到內核態執行時,才能確保CPU硬件能從TSS中自動讀取到正確的內核棧指針(sp0)的值,以保證從用戶態切換到內核態時,相應的堆棧切換正常。
相應的切換在__switch_to中由load_sp0函數完成,
__notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
...
struct tss_struct *tss = &per_cpu(init_tss, cpu);
...
/*
* Reload esp0.
*/
/*將next進程的內核棧指針(next->thread->sp0)值更新到當前CPU的TSS中*/
load_sp0(tss, next);
...
}
static inline void load_sp0(struct tss_struct *tss,
???struct thread_struct *thread)
{
native_load_sp0(tss, thread);
}
static inline void
native_load_sp0(struct tss_struct *tss, struct thread_struct *thread)
{
/*將thread中的內核棧指針賦給TSS中的相應字段*/
tss->x86_tss.sp0 = thread->sp0;
#ifdef CONFIG_X86_32
/* Only happens when SEP is enabled, no need to test "SEP"arately: */
if (unlikely(tss->x86_tss.ss1 != thread->sysenter_cs)) {
tss->x86_tss.ss1 = thread->sysenter_cs;
wrmsr(MSR_IA32_SYSENTER_CS, thread->sysenter_cs, 0);
}
#endif
}
(4)TSS初始化
如前面描述,Linux內核中使用tss_struct來描述TSS,那么硬件上的TSS如何跟內核中的tss_struct關聯起來的呢?
答案是在內核初始化的過程中,會進行相應的初始化,本質上是將TSS中相應的base地址設置為tss_struct per-CPU變量init_tss的地址,如此以來,修改init_tss后,相應的值即會體現到TSS(硬件)中。 TSS段初始化流程如下:
a、BSP上的初始化流程
rest_init->kernel_init->kernel_init_freeable->smp_init->cpu_up->native_cpu_up->do_boot_cpu->start_secondary->cpu_init->set_tss_desc->__set_tss_desc
b、ASP上的初始化流程
start_kernel->trap_init->cpu_init->set_tss_desc->__set_tss_desc
void __cpuinit cpu_init(void)
{
...
t = &per_cpu(init_tss, cpu);
...
load_sp0(t, thread);
set_tss_desc(cpu, t);
load_TR_desc();
...
}
/*
??* 設置TSS段描述符,將GDT中TSS段描述符中的base地址設置為指定的addr,即讓TSS段指向指定的addr.
??*/
static inline void __set_tss_desc(unsigned cpu, unsigned int entry, void *addr)
{
/*獲取per-CPU的gdt(全局描述符表)*/
struct desc_struct *d = get_cpu_gdt_table(cpu);
tss_desc tss;
/*
* sizeof(unsigned long) coming from an extra "long" at the end
* of the iobitmap. See tss_struct definition in processor.h
*
* -1? seg base+limit should be pointing to the address of the
* last valid byte
*/
/*TSS段描述符中的base地址設置為指定的addr,即讓TSS段指向指定的addr.*/
set_tssldt_descriptor(&tss, (unsigned long)addr, DESC_TSS,
?????IO_BITMAP_OFFSET + IO_BITMAP_BYTES +
?????sizeof(unsigned long) - 1);
/*設置GDT中的TSS對應的entry為新設置的tss*/
write_gdt_entry(d, entry, &tss, DESC_TSS);
}
static inline void set_tssldt_descriptor(void *d, unsigned long addr, unsigned type, unsigned size)
{
#ifdef CONFIG_X86_64
struct ldttss_desc64 *desc = d;
memset(desc, 0, sizeof(*desc));
desc->limit0????= size & 0xFFFF;
/*設置描述符的base地址為addr*/
desc->base0????= PTR_LOW(addr);
desc->base1????= PTR_MIDDLE(addr) & 0xFF;
desc->type????= type;
desc->p????= 1;
desc->limit1????= (size >> 16) & 0xF;
desc->base2????= (PTR_MIDDLE(addr) >> 8) & 0xFF;
desc->base3????= PTR_HIGH(addr);
#else
pack_descriptor((struct desc_struct *)d, addr, size, 0x80 | type, 0);
#endif
}
Linux中有3種棧:
1)用戶棧。當進程處于用戶態時使用,位于進程地址空間(用戶態部分(如:0-0xc0000000))底部,用戶態分配局部變量和函數調用時時,使用該棧,跟平時我們見到和理解的一樣,就是虛擬地址空間中的一段。
2)內核棧。跟用戶棧獨立,屬于進程,即每個進程都有自己的內核棧,單獨分配,大小為8k,跟thread_info結構放在一起,在用戶態和內核態切換時,需要進行切換。
3)中斷棧。老版本內核中默認認跟內核棧共享,新版本內核中與內核棧獨立,且軟中斷和硬中斷單獨使用自己的中斷棧。中斷、異常、軟中斷使用此棧。 ? ?
本文主要講解內核棧、用戶棧和內核棧切換的相關實現。
2、實現
1)內核棧定義和實現
內核棧跟thread_info結構放在一起,共用一個union:thread_union,
union thread_union {
? ? struct thread_info thread_info;
? ? unsigned long stack[THREAD_SIZE/sizeof(long)];
};
2)其它與內核棧相關的定義
a、task_struct.thread.sp0
任務描述符(task_struct)中的thread成員(thread_struct)用于保存進程上下文信息,包含主要寄存器信息,在進程上下文切換(如調度)時使用,thread成員定義為thread_struct結構體,其中的sp0成員用于保存內核棧指針。
b、task_struct.stack
任務描述符(task_struct)中的stack成員是新版本內核中新加入的成員,同樣指向內核棧頂,用于替代老版本中的thread_info成員,由于thread_info和內核棧實際是放在一起的,共享同一個聯合體thread_union,可以相互轉換,所以該修改并無本質區別。
點擊(此處)折疊或打開
點擊(此處)折疊或打開
c、kernel_stack per-CPU變量
用于指向當前CPU上運行的進程的內核棧,由于內核棧與thread_info是放在一起的,所以,內核中也用這個變量來獲取當前進程的thread_info:
點擊(此處)折疊或打開
d、tss_struct.sp0
TSS任務狀態段是X86架構中包含的一個特殊的段,用戶保存硬件上下文,包含了當前進程的特權級(ring)信息和寄存器信息。在進程切換時使用,Linux中使用的情況比較少,而用戶棧和內核棧的切換就是其中一處關鍵的應用。Linux內核定義了tss_struct結構體來描述該段中的內容,其中的x86_tss(x86_hw_tss結構)中保存了相應的硬件狀態信息,其中sp0即為內核態(ring0)中的堆棧指針,ss0為內核態堆棧段寄存器,sp為用戶態(ring3)堆棧指針,Linux中主要使用了這幾個字段。
點擊(此處)折疊或打開
點擊(此處)折疊或打開
內核中定義針對每個CPU定義了一個了tss_struct結構體類型變量init_tss,在進程上下文切換(堆棧切換)時使用
點擊(此處)折疊或打開
INIT_TSS定義如下:
點擊(此處)折疊或打開
點擊(此處)折疊或打開
點擊(此處)折疊或打開
3)內核棧分配
內核在何時分配?
答案是在fork時。
Linux中進程均由fork創建(init除外),在fork時即會創建相應的內核棧,相應流程為:
do_fork->copy_process->dup_task_struct->alloc_thread_info_node
點擊(此處)折疊或打開
點擊(此處)折疊或打開
我們知道,x86硬件結構中,SS為堆棧段寄存器,ESP為堆棧指針寄存器,而這兩個寄存器在每個CPU上都是唯一的,但是Linux中有多種棧(用戶棧、內核棧、中斷棧),如果都需要使用的話,那就必須在各個棧之間進行切換。
當CPU運行的特權級(ring)發生變化時,就需要切換相應的堆棧,Linux中,只使用了兩個ring:ring0(內核態)和ring3(用戶態),當發生內核態和用戶態間的狀態切換時,需要考慮堆棧的切換,通常的切換時機有:系統調用、中斷和異常及其返回處。
前面我們已經了解了內核棧的概念和定義,那么如何進行堆棧切換呢?那就需要使用上面描述的TSS段了,如之前所述,tss_struct中包括了內核棧指針sp0和內核堆棧段寄存器ss0。
(1)用戶棧到內核棧的切換
X86硬件結構中,中斷、異常和系統調用都是通過中斷門或陷阱門實現的,在通過中斷門或陷阱門時,硬件會自動利用TSS,完成堆棧切換的工作。硬件完成的操作包括:
? a、找到ISR的入口。具體包括:確定中斷或異常相關的中斷向量、讀取IDTR寄存器獲取IDT表地址、從IDT表中讀取中斷向量對應的項、讀取GDTR寄存器獲取GDT表地址,在GDT表中查找IDT表項中的段選擇符標識的段描述符,該描述符中指定了中斷/異常處理程序(ISR)所在的段基址,結合IDT表項中的段偏移地址,即可找到ISR的入口地址。
? b、權限檢查。確認中斷的來源是否合法,主要比對當前特權級(CS寄存器的低兩位)和段描述符(GDT中)的DPL比較。如果CPL小于DPL,就產生GP(通用保護)異常。對于異常,還需做進一步的安全檢查:比對CPL與IDT中的門描述符的DPL,如果DPL小于CPL,也產生GP(通用保護)異常,由此可以避免用戶應用程序訪問特殊的陷阱門和中斷門。?
? c、檢查特權級的變化。如果ring發生了變化(通常是從ring3到ring0,即用戶態切換到內核態),即CPL與段描述符的DPL不同,則進行一下處理(這里實際上進行了堆棧切換):?
????c1. 讀tr寄存器,獲取TSS段。?
????c2. 讀取TSS中的新特權級(內核態)的堆棧段和堆棧指針,將其load到SS和ESP寄存器。?
????c3. 在新特權級的棧(內核棧)中保存原始的(即用戶態的)SS和ESP的值。
? d. 如果發生的是異常,則將引起異常的指令地址裝載cs和eip寄存器,如此可以使這條指令在異常處理程序執行完后能被再次執行,這也是中斷和異常的主要區別之一;如果發生的中斷,則跳過此步驟。
? e. 在新棧(內核棧)中壓入eflag、cs和eip。?
? f. 如果是異常且產生了硬件出錯碼,則將它壓入棧中。?
? g. 用之前通過IDT和GDT獲取到的ISR的入口地址和段選擇符裝載EIP和CS寄存器,如此即可開始執行ISR。?
再次強調:上述操作均由硬件自動完成。從上述的步驟c可以看出,當用戶態切換到內核態時,用戶棧切換到內核棧實質是由硬件自動完成的,軟件需要做的是預先設置好TSS中的相關內容(比如sp0和ss0)。
(2)內核棧到用戶棧的切換
該過程實際是“用戶棧到內核棧切換”的逆過程,發生時機在系統調用、中斷和異常的返回處,實際的切換過程還是由硬件自動完成的。
中斷或異常返回時,必然會執行iret指令,然后將控制器交回給之前被中斷打斷的進程,硬件自動完成如下操作:
??a. 從當前棧(內核棧)中彈出cs、eip和eflag,并load到相應的寄存器中寄存器。(如之前有硬件錯誤碼入棧,需要先彈出這個錯誤碼)。?
??b. 權限檢查。比對ISR的CPL是否等于cs中的低兩位的值。如果是,iret終止返回;否則,轉入下一步。?
??c. 從當前棧(內核棧)中彈出之前壓入的用戶態堆棧相關的ss和esp,并load到相應寄存器,至此,即完成了從內核棧到用戶棧的切換。?
? d. 后續處理。主要包括:檢查ds、es、fs及gs段寄存器,如果其中一個寄存器包含的選擇符是一個段描述符,并且其DPL值小于CPL,那么,清相關的段寄存器。目的是為了防止用戶態的程序利用內核以前所用的段寄存器,以防止惡意用戶程序利用其訪問內核地址空間。?
(3)進程上下文切換時的堆棧切換
內核調度時,當被選中的next進程不是current進程時,會發生上下文切換,此時也必然會涉及堆棧切換。這里涉及到兩個相關的問題:
a、從current進程的堆棧切換到next進程的堆棧
由于調度肯定發生在內核態,那么進程上下文切換時,也必然處于內核態,那么此時的堆棧切換實質為current進程的內核棧到next進程內核棧的切換。相應工作在switch_to宏中用匯編實現,通過next進程的棧指針(內核態)裝載到ESP寄存器中實現。
? ? "movl %[next_sp],%%esp\n\t" /* restore ESP ? */ \
具體代碼如下:
點擊(此處)折疊或打開
b、TSS中內核棧(sp0)的切換
由于Linux的具體實現中,TSS不是針對每進程,而是針對每CPU的,即每個CPU對應一個tss_struct,那在進程上下文切換時,需要考慮當前CPU上TSS中的內容的更新,其實就是內核棧指針的更新,更新后,當新進程再次進入到內核態執行時,才能確保CPU硬件能從TSS中自動讀取到正確的內核棧指針(sp0)的值,以保證從用戶態切換到內核態時,相應的堆棧切換正常。
相應的切換在__switch_to中由load_sp0函數完成,
點擊(此處)折疊或打開
點擊(此處)折疊或打開
點擊(此處)折疊或打開
(4)TSS初始化
如前面描述,Linux內核中使用tss_struct來描述TSS,那么硬件上的TSS如何跟內核中的tss_struct關聯起來的呢?
答案是在內核初始化的過程中,會進行相應的初始化,本質上是將TSS中相應的base地址設置為tss_struct per-CPU變量init_tss的地址,如此以來,修改init_tss后,相應的值即會體現到TSS(硬件)中。 TSS段初始化流程如下:
a、BSP上的初始化流程
rest_init->kernel_init->kernel_init_freeable->smp_init->cpu_up->native_cpu_up->do_boot_cpu->start_secondary->cpu_init->set_tss_desc->__set_tss_desc
b、ASP上的初始化流程
start_kernel->trap_init->cpu_init->set_tss_desc->__set_tss_desc
點擊(此處)折疊或打開
點擊(此處)折疊或打開
點擊(此處)折疊或打開
原文地址: http://blog.chinaunix.net/uid-14528823-id-4739291.html
總結
以上是生活随笔為你收集整理的kernel 3.10内核源码分析--内核栈及堆栈切换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux kernel 3.10内核源
- 下一篇: kernel 3.10内核源码分析--中