2023-06-11
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_10946851.html

在进程创建时,内核会为进程创建一系列数据结构,其中最重要的就是上章学习的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,就能够轻松得到内核栈和内核寄存器,如下图所示

202306111303556611.png

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,其原理基本类似。

202306111303566322.png

5. 参考文档

趣谈Linux操作系统

阅读全文