在进程创建时,内核会为进程创建一系列数据结构,其中最重要的就是上章学习的task_struct结构,它就是进程描述符,表明进程在生命周期内的所有特征。同时,内核为进程创建两个栈,一个是用户栈,一个是内核栈,分别处于用户态和内核态使用的栈。本章主要包括以下内容
在每个进程的生命周期内,经常会通过系统调用(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 的整数倍。
进程在内核中相关的主要数据结构有进程描述符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,就能够轻松得到内核栈和内核寄存器,如下图所示
那如果一个当前在某个 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 变量里面了。
实际上在linux kernel中,task_struct、thread_info都用来保存进程相关信息,即进程PCB信息。然而不同的体系结构里,进程需要存储的信息不尽相同,linux使用task_struct存储通用的信息,将体系结构相关的部分存储在thread_info中。
趣谈Linux操作系统