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,可以相互转换,所以该修改并无本质区别。
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定义如下:
init_stack定义为:
即将内核栈(sp0)指向了init_thread_union的内核栈顶。
init_thread_union为初始的任务描述符:
3)内核栈分配
内核在何时分配?
答案是在fork时。
Linux中进程均由fork创建(init除外),在fork时即会创建相应的内核栈,相应流程为:
do_fork->copy_process->dup_task_struct->alloc_thread_info_node
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 */ \
具体代码如下:
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
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 }, \
- }
点击(此处)折叠或打开
- #define init_stack (init_thread_union.stack)
点击(此处)折叠或打开
- 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;
- ...
- }
我们知道,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
- }
原文地址: http://blog.chinaunix.net/uid-14528823-id-4739291.html