在进程创建时,内核会为进程创建一系列数据结构,其中最重要的就是上章学习的task_struct结构,它就是进程描述符,表明进程在生命周期内的所有特征。同时,内核为进程创建两个栈,一个是用户栈,一个是内核栈,分别处于用户态和内核态使用的栈。本章主要包括以下内容
- 内核栈的概念
- thread_info的用途
1 内核态内核栈
在每个进程的生命周期内,经常会通过系统调用(SYSCALL)或者中断进入内核。在执行系统调用后,这些内核代码所使用的栈并不是原先用户空间的栈,而是一个内核空间的栈,这个栈被称作进程的“内核栈”。
由用户态切换到内核态,内核将用户态时的堆栈寄存器的值保存在内核栈中,以便从内核栈切换回进程栈时能找到用户栈的地址。但是,从进程栈切换到内核栈时,内核是如何找到该进程的内核栈的地址信息,这部分放到后续章节中详细介绍。
对于task_struct定义在include/linux/sched.h中,有和内核栈相关的数据项
struct task_struct {
struct thread_info thread_info;
...
void * stack;
...
}
其中,thread_info是一个体系相关的描述符,不同的硬件体系所需要记录的标志是不同,因此内核将和特定的硬件体系相关的标志定义在此结构中。
每个task的栈分成用户栈和内核栈两部分,进程内核栈在kernel中的定义在include/linux/sched.h中,如下:
union thread_unoin {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
}
每个task的内核栈大小THREAD_SIZE :
//ARM架构 , 8K
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
#define THREAD_START_SP (THREAD_SIZE - 8)
//ARM64架构, 16K
#define THREAD_SIZE 16384
#define THREAD_START_SP (THREAD_SIZE - 16)
//X86_64, 16K
#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
Linux 给每个 task 都分配了内核栈。在 32 位系统上 arch/x86/include/asm/page_32_types.h,是这样定义的:一个 PAGE_SIZE 是 4K,左移一位就是乘以 2,也就是 8K。但是内核栈在 64 位系统上arch/x86/include/asm/page_64_types.h,是这样定义的:在 PAGE_SIZE 的基础上左移两位,也即 16K,并且要求起始地址必须是 8192 的整数倍。
2. 通过 task_struct 找内核栈
进程在内核中相关的主要数据结构有进程描述符task_struct、thread_info和mm_struct。上面的共同体thread_union 里,就有thread_info。我们都熟悉进程描述符task_struct,那么thread_info有什么用?
如果有一个task_struct的stack指针在手,你可以通过下面的函数找到这个线程的内核栈:
//sched.h (include\linux 105464 2018/3/18 592)
static inline void *task_stack_page(const struct task_struct *task)
{
return task->stack;
}
从 task_struct 如何得到相应的 pt_regs 呢?我们可以通过下面的函数:
//processor.h (arch\x86\include\asm)
#define task_pt_regs(task) \
({ \
unsigned long __ptr = (unsigned long)task_stack_page(task); \
__ptr += THREAD_SIZE - TOP_OF_KERNEL_STACK_PADDING; \
((struct pt_regs *)__ptr) - 1; \
})
你会发现,这是先从 task_struct 找到内核栈的开始位置。然后这个位置加上 THREAD_SIZE 就到了最后的位置,然后转换为 struct pt_regs,再减一,就相当于减少了一个 pt_regs 的位置,就到了这个结构的首地址。
对于arm64也同样使用
#define task_pt_regs(p) \
((struct pt_regs *)(THREAD_START_SP + task_stack_page(p)) - 1)
所以我们可以通过task_struct,就能够轻松得到内核栈和内核寄存器,如下图所示
3. 通过内核栈找 task_struct
那如果一个当前在某个 CPU 上执行的进程,你同样也可以知道 task_struct 在哪里,这个艰巨的任务要交给thread_info这个结构。
ARM架构:
查看arm架构的源码发现,前面提到的CONFIG_THREAD_INFO_IN_TASK宏是关闭的,且没有提供对外kconfig接口。也就是说在32位 arm架构中,thread_info 结构肯定在进程内核栈中。下面这种current宏适用于所有合“thread_info 结构在内核栈中”的架构:
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
__u32 cpu; /* cpu */
__u32 cpu_domain; /* cpu domain */
struct cpu_context_save cpu_context; /* cpu context */
__u32 syscall; /* syscall number */
__u8 used_cp[16]; /* thread used copro */
unsigned long tp_value[2]; /* TLS registers */
#ifdef CONFIG_CRUNCH
struct crunch_state crunchstate;
#endif
union fp_state fpstate __attribute__((aligned(8)));
union vfp_state vfpstate;
#ifdef CONFIG_ARM_THUMBEE
unsigned long thumbee_state; /* ThumbEE Handler Base register */
#endif
};
这里面有个成员变量 task 指向 task_struct,所以我们常用 current_thread_info()->task 来获取 task_struct。
#define get_current() (current_thread_info()->task)
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
}
而 thread_info 的位置就是内核栈的最高位置,减去 THREAD_SIZE,就到了 thread_info 的起始地址。
ARM64架构:
通过发现在ARM64架构中,其定义如下:
#define get_current() (current_thread_info()->task)
static inline struct thread_info *current_thread_info(void)
{
unsigned long sp_el0;
asm ("mrs %0, sp_el0" : "=r" (sp_el0));
return (struct thread_info *)sp_el0;
}
ARM64使用sp_el0,在进程切换时暂存进程描述符地址,sp就是堆栈寄存器。在ARM64里,CPU运行在四个级别(或者叫运行空间),分别是el0、el1、el2、el3,el0则就是用户空间,el1则是内核空间。
X64架构(64位架构)
在x86上也可以采用和32位ARM类似的获取方式,然而在64位体系结构中,linux kernel一直采用的是另一种方式:使用了current_task这个每CPU变量,来存储当前正在使用cpu的进程描述符struct task_struct。
struct task_struct;
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
到这里,你会发现,新的机制里面,每个 CPU 运行的 task_struct 不通过 thread_info 获取了,而是直接放在 Per CPU 变量里面了。
4 总结
实际上在linux kernel中,task_struct、thread_info都用来保存进程相关信息,即进程 PCB 信息。然而不同的体系结构里,进程需要存储的信息不尽相同,linux使用task_struct存储通用的信息,将体系结构相关的部分存储在thread_info中。
- 在内核态,32 位和 64 位都使用内核栈,格式也稍有不同,主要集中在 pt_regs 结构上
- 在内核态,32 位和 64 位的内核栈和 task_struct 的关联关系不同。
- x86中32 位主要靠 thread_info,64 位主要靠 Per-CPU 变量,而ARM平台不论是32位还是64位,都是使用thread_info,其原理基本类似。
5. 参考文档
趣谈Linux操作系统
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。