Linux-进程的管理与调度9(基于6.1内核)

Linux-进程的管理与调度9(基于6.1内核)---Linux进程内核栈

一、内核栈


1.1、为什么需要内核栈?


  • 进程在内核态运行时需要自己的堆栈信息, 因此linux内核为每个进程都提供了一个内核栈kernel stack,
struct task_struct
{
...
    void *stack;    //  指向内核栈的指针
...
};

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

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

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

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

1.2、为什么需要thread_info?


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

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

	struct thread_info thread_info;

1.3、将两种结构融合在一起


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

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

二、内核数据结构描述


2.1、thread_union


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

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

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

这两个结构被紧凑的放在一个联合体中thread_union中,include/linux/sched.h

union thread_union {
#ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK
	struct task_struct task;
#endif
#ifndef CONFIG_THREAD_INFO_IN_TASK
	struct thread_info thread_info;
#endif
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};

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

/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT		12
#define PAGE_SIZE		(_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK		(~(PAGE_SIZE-1))

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

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

在这个图中,

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

同时发现:

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

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

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

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

2.2、task_struct中的内核栈stack


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

形式如下:

struct task_struct
{
...
    void *stack;    //  指向内核栈的指针
...
};

进程描述符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的信息, 只需要一次指针类型转换即可

#ifdef CONFIG_THREAD_INFO_IN_TASK
# define task_thread_info(task)	(&(task)->thread_info)
#elif !defined(__HAVE_THREAD_FUNCTIONS)
# define task_thread_info(task)	((struct thread_info *)(task)->stack)
#endif

2.3、内核栈数据结构描述thread_info


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

struct thread_info {
	unsigned long		flags;		/* low level flags */
	unsigned long		syscall_work;	/* SYSCALL_WORK_ flags */
	u32			status;		/* thread synchronous flags */
#ifdef CONFIG_SMP
	u32			cpu;		/* current CPU */
#endif
};

三、函数接口


3.1、内核栈与thread_info的通用操作


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

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

另外, 他们必须实现dup_task_struct中调用的函数setup_thread_stack, 以便确定stack成员的具体内存布局。include/linux/sched/task_stack.h

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

#ifdef CONFIG_THREAD_INFO_IN_TASK

/*
 * When accessing the stack of a non-current task that might exit, use
 * try_get_task_stack() instead.  task_stack_page will return a pointer
 * that could get freed out from under you.
 */
static __always_inline void *task_stack_page(const struct task_struct *task)
{
	return task->stack;
}

#define setup_thread_stack(new,old)	do { } while(0)

static __always_inline unsigned long *end_of_stack(const struct task_struct *task)
{
#ifdef CONFIG_STACK_GROWSUP
	return (unsigned long *)((unsigned long)task->stack + THREAD_SIZE) - 1;
#else
	return task->stack;
#endif
}

#elif !defined(__HAVE_THREAD_FUNCTIONS)

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

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

之前分析do_fork创建进程的时候, 提到dup_task_struct会复制父进程的task_struct和thread_info实例的内容, 但是stack则与新的thread_info实例位于同一个内存, 这意味着父子进程的task_struct此时除了栈指针之外完全相同。

3.2、获取当前在CPU上正在运行进程的thread_info


所有的体系结构都必须实现两个currentcurrent_thread_info的符号定义宏或者函数,

  • current_thread_info可获得当前执行进程的thread_info实例指针, 其地址可以根据内核指针来确定, 因为thread_info总是位于起始位置,

    因为每个进程都有自己的内核栈, 因此进程到内核栈的映射是唯一的, 那么指向内核栈的指针通常保存在一个特别保留的寄存器中(多数情况下是esp)

  • current给出了当前进程进程描述符task_struct的地址,该地址往往通过current_thread_info来确定
    current = current_thread_info()->task

关键就是current_thread_info的实现了,即如何通过esp栈指针来获取当前在CPU上正在运行进程的thread_info结构。

进程最常用的是进程描述符结构task_struct而不是thread_info结构的地址。为了获取当前CPU上运行进程的task_struct结构,内核提供了current宏,由于task_struct *task在thread_info的起始位置,该宏本质上等价于current_thread_info()->task,在include/asm-generic/current.h:

#ifndef __ASSEMBLY__
#include <linux/thread_info.h>

#define get_current() (current_thread_info()->task)
#define current get_current()
#endif

#endif /* __ASM_GENERIC_CURRENT_H */

这个定义是体系结构无关的,当然linux也为各个体系结构定义了更加方便或者快速的current。

3.3、分配和销毁thread_info


进程通过alloc_thread_info_node函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈。 

static int alloc_thread_stack_node(struct task_struct *tsk, int node)
{
	struct vm_struct *vm;
	void *stack;
	int i;

	for (i = 0; i < NR_CACHED_STACKS; i++) {
		struct vm_struct *s;

		s = this_cpu_xchg(cached_stacks[i], NULL);

		if (!s)
			continue;

		/* Reset stack metadata. */
		kasan_unpoison_range(s->addr, THREAD_SIZE);

		stack = kasan_reset_tag(s->addr);

		/* Clear stale pointers from reused stack. */
		memset(stack, 0, THREAD_SIZE);

		if (memcg_charge_kernel_stack(s)) {
			vfree(s->addr);
			return -ENOMEM;
		}

		tsk->stack_vm_area = s;
		tsk->stack = stack;
		return 0;
	}

	/*
	 * Allocated stacks are cached and later reused by new threads,
	 * so memcg accounting is performed manually on assigning/releasing
	 * stacks to tasks. Drop __GFP_ACCOUNT.
	 */
	stack = __vmalloc_node_range(THREAD_SIZE, THREAD_ALIGN,
				     VMALLOC_START, VMALLOC_END,
				     THREADINFO_GFP & ~__GFP_ACCOUNT,
				     PAGE_KERNEL,
				     0, node, __builtin_return_address(0));
	if (!stack)
		return -ENOMEM;

	vm = find_vm_area(stack);
	if (memcg_charge_kernel_stack(vm)) {
		vfree(stack);
		return -ENOMEM;
	}
	/*
	 * We can't call find_vm_area() in interrupt context, and
	 * free_thread_stack() can be called in interrupt context,
	 * so cache the vm_struct.
	 */
	tsk->stack_vm_area = vm;
	stack = kasan_reset_tag(stack);
	tsk->stack = stack;
	return 0;
}

static void free_thread_stack(struct task_struct *tsk)
{
	if (!try_release_thread_stack_to_cache(tsk->stack_vm_area))
		thread_stack_delayed_free(tsk);

	tsk->stack = NULL;
	tsk->stack_vm_area = NULL;
}

#  else /* !CONFIG_VMAP_STACK */

static void thread_stack_free_rcu(struct rcu_head *rh)
{
	__free_pages(virt_to_page(rh), THREAD_SIZE_ORDER);
}

static void thread_stack_delayed_free(struct task_struct *tsk)
{
	struct rcu_head *rh = tsk->stack;

	call_rcu(rh, thread_stack_free_rcu);
}

static int alloc_thread_stack_node(struct task_struct *tsk, int node)
{
	struct page *page = alloc_pages_node(node, THREADINFO_GFP,
					     THREAD_SIZE_ORDER);

	if (likely(page)) {
		tsk->stack = kasan_reset_tag(page_address(page));
		return 0;
	}
	return -ENOMEM;
}

static void free_thread_stack(struct task_struct *tsk)
{
	thread_stack_delayed_free(tsk);
	tsk->stack = NULL;
}

 其中,THREAD_SIZE_ORDER宏的定义

/* Thread information allocation.  */
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (2*PAGE_SIZE)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值