linux进程用户栈和内核栈,Linux进程内核栈与thread_info结构详解-Go语言中文社区

前言

为什么需要内核栈

进程在内核态运行时需要自己的堆栈信息, 因此linux内核为每个进程都提供了一个内核栈kernel stack,struct task_struct

{

// ...

void *stack; // 指向内核栈的指针

// ...

};

内核态的进程访问处于内核数据段的栈,这个栈不同于用户态的进程所用的栈。

用户态进程所用的栈,是在进程线性地址空间中;

而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,需要切换堆栈,那么内核空间中使用的就是这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈。

需要注意的是,内核态堆栈仅用于内核例程,Linux内核另外为中断提供了单独的硬中断栈和软中断栈。

为什么需要thread_info

内核还需要存储每个进程的PCB信息, linux内核是支持不同的体系结构, 但是不同的体系结构可能进程需要存储的信息不尽相同, 这就需要我们实现一种通用的方式, 我们将体系结构相关的部分和无关的部门进行分离。

用一种通用的方式来描述进程, 这就是struct task_struct, 而thread_info就保存了特定体系结构的汇编代码段需要访问的那部分进程的数据,我们在thread_info中嵌入指向task_struct的指针, 则我们可以很方便的通过thread_info来查找task_struct

将两种结构融合在一起

linux将内核栈和进程控制块thread_info融合在一起, 组成一个联合体thread_union

通常内核栈和thread_info一同保存在一个联合体中, thread_info保存了线程所需的所有特定处理器的信息, 以及通用的task_struct的指针

内核数据结构描述

thread_union

对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中:

1)一个是内核态的进程堆栈stack

2)另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。

这两个结构被紧凑的放在一个联合体中thread_union中,、

union thread_union

{

struct thread_info thread_info;

unsigned long stack[THREAD_SIZE/sizeof(long)];

};

这块区域32位上通常是8K=8192(占两个页框),64位上通常是16K,其实地址必须是8192的整数倍。

架构                     THREAD_SIZE

x86                      arch/x86/include/asm/page_32_types.h, line 21

x86_64                arch/x86/include/asm/page_64_types.h, line 11

arm                      arch/arm/include/asm/thread_info.h, line 20

arm64                  arch/arm64/include/asm/thread_info.h, line 32

出于效率考虑,内核让这8K(或者16K)空间占据连续的两个页框并让第一个页框的起始地址是213的倍数。

下图中显示了在物理内存中存放两种数据结构的方式。线程描述符驻留与这个内存区的开始,而栈顶末端向下增长。 下图摘自ULK3,进程内核栈与进程描述符的关系如下图:

6c7854f3fe03ccda53b0ea1d4bed462c.png

在这个图中,

1)esp寄存器是CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于顶端,并朝着这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的。因此,esp寄存器指向这个栈的顶端。一旦数据写入堆栈,esp的值就递减。

同时我们可以看到,

2)thread_info和内核栈虽然共用了thread_union结构, 但是thread_info大小固定, 存储在联合体的开始部分, 而内核栈由高地址向低地址扩展, 当内核栈的栈顶到达thread_info的存储空间时, 则会发生栈溢出

3)系统的current指针指向了当前运行进程的thread_union(或者thread_info)的地址

4)进程task_struct中的stack指针指向了进程的thread_union(或者thread_info)的地址, 在早期的内核中这个指针用struct thread_info *thread_info来表示, 但是新的内核中用了一个更浅显的名字void *stack, 即内核栈

即,进程的thread_info存储在进程内核栈的最低端

task_struct中的内核栈stack

我们之前在描述task_struct时就提到了其stack指针指向的是内核栈的地址。

其被定义在include/linux/sched.h中

形式如下:

struct task_struct

{

// ...

void *stack; // 指向内核栈的指针

// ...

};

在早期的linux内核中进程描述符中是不包含内核栈的, 相反包含着指向thread_info的指针

但是在2007年的一次更新(since 2.6.22)中加入了stack内核栈指针, 替代了原来的thread_info的指针

进程描述符task_struct结构中没有直接指向thread_info结构的指针,而是用一个void指针类型的成员表示,然后通过类型转换来访问thread_info结构。

stack指向了内核栈的地址(其实也就是thread_info和thread_union的地址),因为联合体中stack和thread_info都在起始地址, 因此可以很方便的转型

相关代码在include/linux/sched.h中

task_thread_info用于通过task_struct来查找其thread_info的信息, 只需要一次指针类型转换即可

#define task_thread_info(task) ((struct thread_info *)(task)->stack)

内核栈数据结构描述thread_info

thread_info是体系结构相关的,结构的定义在thread_info.h中,保存了进程所有依赖于体系结构的信息, 同时也保存了一个指向进程描述符task_struct的指针

架构                   定义链接

x86                    linux-4.5/arch/x86/include/asm/thread_info.h, line 55

arm                    linux-4.5arch/arm/include/asm/thread_info.h, line 49

arm64                linux/4.5/arch/arm64/include/asm/thread_info.h, line 47

函数接口

内核栈与thread_info的通用操作

原则上, 只要设置了预处理器常数__HAVE_THREAD_FUNCTIONS通知内核, 那么各个体系结构就可以随意在stack数组中存储数据。

在这种情况下, 他们必须自行实现task_thread_info和task_stack_page, 这两个函数用于获取给定task_struct实例的线程信息和内核栈。

另外, 他们必须实现dup_task_struct中调用的函数setup_thread_stack, 以便确定stack成员的具体内存布局, 当前只有ia64等少数架构不依赖于内核的默认方法

下标给出了不同架构的task_thread_info和task_stack_page的实现

// 未定义__HAVE_THREAD_FUNCTIONS的时候使用内核的默认操作

#ifndef __HAVE_THREAD_FUNCTIONS

// 通过进程的task_struct来获取进程的thread_info

#define task_thread_info(task) ((struct thread_info *)(task)->stack)

// 通过进程的task_struct来获取进程的内核栈

#define task_stack_page(task) ((task)->stack)

// 初始化thread_info, 指定其存储结构的内存布局

static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)

{

*task_thread_info(p) = *task_thread_info(org);

task_thread_info(p)->task = p;

}

/*

* Return the address of the last usable long on the stack.

*

* When the stack grows down, this is just above the thread

* info struct. Going any lower will corrupt the threadinfo.

*

* When the stack grows up, this is the highest address.

* Beyond that position, we corrupt data on the next page.

*/

static inline unsigned long *end_of_stack(struct task_struct *p)

{

#ifdef CONFIG_STACK_GROWSUP

return (unsigned long *)((unsigned long)task_thread_info(p) + THREAD_SIZE) - 1;

#else

return (unsigned long *)(task_thread_info(p) + 1);

#endif

}

#endif

在内核的某个特定组建使用了较多的栈空间时, 内核栈会溢出到thread_info部分, 因此内核提供了kstack_end函数来判断给出的地址是否位于栈的有效部分

#ifndef __HAVE_ARCH_KSTACK_END

static inline int kstack_end(void *addr)

{

/* Reliable end of stack detection:

* Some APM bios versions misalign the stack

*/

return !(((unsigned long)addr+sizeof(void*)-1) & (THREAD_SIZE-sizeof(void*)));

}

#endif

详细内容可移步该博客:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值