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
-
}
阅读(1313) | 评论(0) | 转发(0) |