您的位置:首页 > 其它

kernel 3.10内核源码分析--内核栈及堆栈切换

2016-04-01 17:57 337 查看
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

}

原文地址: http://blog.chinaunix.net/uid-14528823-id-4739291.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: