内核栈与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的保留空间
内核引入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);
/* ... */
}