Linux内核栈与thread_info

内核栈与thread_info

Linux内核在x86平台下,PAGE_SIZE为4KB(32位和64位相同),THREAD_SIZE为8KB(32位)或者16KB(64位)。THREAD_SIZE表示了整个内核栈的大小,栈可以向下增长(栈低在高地址)或者向上增长(栈低在低地址),后面的分析都是基于向下增长的方式。如图中所示,整个内核栈可分为四个部分,从低地址开始依次为:

  • thread_info结构体
  • 溢出标志
  • 从溢出标志开始到kernel_stack之间的实际可用栈内存空间,kernel_stack为percpu变量,通过它可间接找到内核栈的起始地址
  • 从kernel_stack到栈底的长度为KERNEL_STACK_OFFSET的保留空间

kernel_stack

内核引入thread_info的一大原因是方便通过它直接找到进(线)程的task_struct指针,x86 平台的thread_info结构体定义在arch/x86/include/asm/thread_info.h。

// Linux 3.19.3 x86平台的thread_info
struct thread_info {
	struct task_struct	*task;		/* main task structure */
	struct exec_domain	*exec_domain;	/* execution domain */
	__u32			flags;		/* low level flags */
	__u32			status;		/* thread synchronous flags */
	__u32			cpu;		/* current CPU */
	int			saved_preempt_count;
	mm_segment_t		addr_limit;
	void __user		*sysenter_return;
	unsigned int		sig_on_uaccess_error:1;
	unsigned int		uaccess_err:1;	/* uaccess failed */
};

由于thread_info结构体恰好位于内核栈的低地址开始处,所以只要知道内核栈的起始地址,就可以通过其得到thread_info,进而得到task_struct,后面会分析这个过程的实现。


current宏

current宏在Linux 内核中负责获取当前cpu上的task_struct,通常是借助thread_info和内核栈实现,这种方式的主要逻辑是:

  • 定义kernel_stack percpu变量,通过kernel_stack + KERNEL_STACK_OFFSET - THREAD_SIZE即可获得内核栈的起始地址
  • 内核栈的起始地址即是thread_info的起始地址,所以通过thread_info->task即可获得task_struct指针
  • 在每次上下文切换时,更新kernel_stack percpu变量
// 以Linux 3.19.3 x86的源码为例
// 注意,arch/xxx/include/asm/...下的头文件是对应
// include/asm-generic/...下的平台相关实现,若arch
// 目录下没有相同的头文件,则使用asm-generic目录下
// 的,arch目录下的头文件可能直接include asm-generic
// 目录下的相关头文件。

// include/asm-generic/current.h
#define get_current() (current_thread_info()->task)
#define current get_current()


// arch/x86/include/asm/thread_info.h
DECLARE_PER_CPU(unsigned long, 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;
}


// arch/x86/kernel/process_64.c
__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
    /* ... */
    this_cpu_write(kernel_stack,
		  (unsigned long)task_stack_page(next_p) +
		  THREAD_SIZE - KERNEL_STACK_OFFSET);
    /* ... */
} 

但是Linux内核引入percpu变量之后,逐渐通过percpu变量来实现current宏,并且从Linux 4.1开始,x86移除了kernel_stack,并逐渐开始简化thread_info结构体,直到Linux 4.9彻底不再通过thread_info获取task_struct指针,而是直接通过current_struct percpu变量存放task_struct的指针,具体可参见此commit

// 以Linux 3.19.3 x86的源码为例

// arch/x86/include/asm/current.h
DECLARE_PER_CPU(struct task_struct *, current_task);
static __always_inline struct task_struct *get_current(void)
{
	return this_cpu_read_stable(current_task);
}
#define current get_current()


// arch/x86/kernel/process_64.c
__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
    /* ... */
    this_cpu_write(current_task, next_p);
    /* ... */
} 

comments powered by Disqus