Linux内核学习之 -- ARMv8中的Current宏

文章目录

环境

linux 4.19

current宏

定义在arch/arm64/include/asm/current.h:

#define current get_current()
...
...
static __always_inline struct task_struct *get_current(void)
{
	unsigned long sp_el0;

	asm ("mrs %0, sp_el0" : "=r" (sp_el0));

	return (struct task_struct *)sp_el0;
}

从代码中可以看到,这个current宏是通过读取sp_el0的值来获取当前进程对应的struct task_struct。

在x86下,是把thread_info放在内核栈最底端,然后先要找到thread_info再找到struct task_struct。归根结底是因为x86寄存器太少,而像struct task_struct这么重要且经常使用的结构体,应该放到某个容易寻找的位置或者寄存器中最合适,方便快速访问。这是《Linux内核设计与实现》这本书中介绍的方法,通过地址偏移来获取thread_info。但是现在已经不这么做了,等会将介绍现在是如何做的。

在ARMv8中,可以通过sp_el0来存放当前进程struct task_struct的位置的,请看:ThreadInfo结构和内核栈的两种关系。看完这篇文章后,可以知道:

  • 存在两种thread_info架构:①与x86一样,放在内核栈最底端,这样做可以通过地址偏移来获取到内核栈thread_info,然后再通过thread_info获取到struct task_struct;②开启CONFIG_THREAD_INFO_IN宏之后,thread_info放在struct task_struct中,struct task_struct
  • ARMv8使用的是第二种架构,所以需要通过某个能够获取到的信息来保存struct task_struct的地址
  • 在ARMv8中,使用sp_el0来保存当前进程的struct task_struct
  • 这篇文章还提到了,进程切换时也会切换sp_el0,时刻保持sp_el0存储的是当前进程的struct task_struct地址。

可以看到,对于arm64来讲,current宏是从sp_el0寄存器中获取的。但是有一点不理解,就是sp_el0不是用户态的程序使用的吗?啥时候被换成用来保存struct task_struct了呢?这就涉及到两个问题:

  1. 何时更改sp_el0指向的地址?也就是说什么时候使sp_el0指向当前进程的struct task_struct?
  2. 在使用sp_el0保存struct task_struct之前,这个结构体保存在哪里?

首先说一说x86现在的做法,已经不再是之前那样了。现在在x86体系下,Linux内核定义了名为current_task的Per-CPU变量,每个CPU上当前运行的进程task_struct指针都保存current_task变量中。

为了解决ARMv8是怎么做的,需要先从第1个问题入手,就是什么时候sp_el0保存的内容进行切换。在用户态下是不能使用current宏的,而且用户态下sp_el0有自己的用途,就是指向进程地址空间的栈。所以sp_el0指向的地址的切换,一定是在用户态切换到内核态时完成的。

用户态切内核态就几种方法,同步异常、中断等等,这些都离不开中断向量表。关于ARMv8的系统调用/中断等,可以看一下我写的另外一篇博客:Linux内核学习之 – ARMv8架构的系统调用笔记。我们以系统调用el0_sync为例(当时分析系统调用的时候基于linux 4.19内核,现在5.15内核已经不叫这个函数名了):

el0_sync:
	kernel_entry 0
	mrs	x25, esr_el1			// read the syndrome register, 寄存器esr_el1是在权限级EL1下可以访问的系统寄存器,该寄存器的相关状态就表明了异常发生的具体原因。
	lsr	x24, x25, #ESR_ELx_EC_SHIFT	// exception class, lsr: 逻辑右移指令,实现将寄存器进行右移操作, 將x25寄存器的值右移ESR_ELx_EC_SHIFT位后赋值给x24寄存器
	cmp	x24, #ESR_ELx_EC_SVC64		// SVC in 64-bit state
	b.eq	el0_svc					// b.eq:表示条件分支指令,当某个条件满足时,跳转到某个地址
	cmp	x24, #ESR_ELx_EC_DABT_LOW	// data abort in EL0
	b.eq	el0_da
	cmp	x24, #ESR_ELx_EC_IABT_LOW	// instruction abort in EL0
	b.eq	el0_ia
	cmp	x24, #ESR_ELx_EC_FP_ASIMD	// FP/ASIMD access
	b.eq	el0_fpsimd_acc
	cmp	x24, #ESR_ELx_EC_SVE		// SVE access
	b.eq	el0_sve_acc
	cmp	x24, #ESR_ELx_EC_FP_EXC64	// FP/ASIMD exception
	b.eq	el0_fpsimd_exc
	cmp	x24, #ESR_ELx_EC_SYS64		// configurable trap
	b.eq	el0_sys
	cmp	x24, #ESR_ELx_EC_SP_ALIGN	// stack alignment exception
	b.eq	el0_sp_pc
	cmp	x24, #ESR_ELx_EC_PC_ALIGN	// pc alignment exception
	b.eq	el0_sp_pc
	cmp	x24, #ESR_ELx_EC_UNKNOWN	// unknown exception in EL0
	b.eq	el0_undef
	cmp	x24, #ESR_ELx_EC_BREAKPT_LOW	// debug exception in EL0
	b.ge	el0_dbg
	b	el0_inv

其中有个macro宏,叫kernel_entry,这是系统调用的入口处理函数,也是大部分中断/同步异常的入口处理函数:

arch/arm64/kernel/entry.S:
	.macro	kernel_entry, el, regsize = 64
	.if	\regsize == 32
	mov	w0, w0				// zero upper 32 bits of x0
	.endif
	stp	x0, x1, [sp, #16 * 0]			// 此时sp已经被硬件自动切换到ELx状态的栈地址(x > 0),比如系统调用,已经切换到内核态EL1
	stp	x2, x3, [sp, #16 * 1]			// 就系统调用而言,现在要做的是保存用户态的x0~x29寄存器的数据
	stp	x4, x5, [sp, #16 * 2]
	stp	x6, x7, [sp, #16 * 3]
	stp	x8, x9, [sp, #16 * 4]
	stp	x10, x11, [sp, #16 * 5]
	stp	x12, x13, [sp, #16 * 6]
	stp	x14, x15, [sp, #16 * 7]
	stp	x16, x17, [sp, #16 * 8]
	stp	x18, x19, [sp, #16 * 9]
	stp	x20, x21, [sp, #16 * 10]
	stp	x22, x23, [sp, #16 * 11]
	stp	x24, x25, [sp, #16 * 12]
	stp	x26, x27, [sp, #16 * 13]
	stp	x28, x29, [sp, #16 * 14]

	.if	\el == 0
	clear_gp_regs
	mrs	x21, sp_el0
	ldr_this_cpu	tsk, __entry_task, x20	// Ensure MDSCR_EL1.SS is clear,这里有个静态全局变量:__entry_task,是个perCPU变量。
	ldr	x19, [tsk, #TSK_TI_FLAGS]	// since we can unmask debug
	disable_step_tsk x19, x20		// exceptions when scheduling.

	apply_ssbd 1, x22, x23

	.else
	add	x21, sp, #S_FRAME_SIZE
	get_thread_info tsk
	/* Save the task's original addr_limit and set USER_DS */
	ldr	x20, [tsk, #TSK_TI_ADDR_LIMIT]
	str	x20, [sp, #S_ORIG_ADDR_LIMIT]
	mov	x20, #USER_DS
	str	x20, [tsk, #TSK_TI_ADDR_LIMIT]
	/* No need to reset PSTATE.UAO, hardware's already set it to 0 for us */
	.endif /* \el == 0 */
	mrs	x22, elr_el1
	mrs	x23, spsr_el1
	stp	lr, x21, [sp, #S_LR]

	/*
	 * In order to be able to dump the contents of struct pt_regs at the
	 * time the exception was taken (in case we attempt to walk the call
	 * stack later), chain it together with the stack frames.
	 */
	.if \el == 0
	stp	xzr, xzr, [sp, #S_STACKFRAME]
	.else
	stp	x29, x22, [sp, #S_STACKFRAME]
	.endif
	add	x29, sp, #S_STACKFRAME

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
	/*
	 * Set the TTBR0 PAN bit in SPSR. When the exception is taken from
	 * EL0, there is no need to check the state of TTBR0_EL1 since
	 * accesses are always enabled.
	 * Note that the meaning of this bit differs from the ARMv8.1 PAN
	 * feature as all TTBR0_EL1 accesses are disabled, not just those to
	 * user mappings.
	 */
alternative_if ARM64_HAS_PAN
	b	1f				// skip TTBR0 PAN
alternative_else_nop_endif

	.if	\el != 0
	mrs	x21, ttbr0_el1
	tst	x21, #TTBR_ASID_MASK		// Check for the reserved ASID
	orr	x23, x23, #PSR_PAN_BIT		// Set the emulated PAN in the saved SPSR
	b.eq	1f				// TTBR0 access already disabled
	and	x23, x23, #~PSR_PAN_BIT		// Clear the emulated PAN in the saved SPSR
	.endif

	__uaccess_ttbr0_disable x21
1:
#endif

	stp	x22, x23, [sp, #S_PC]

	/* Not in a syscall by default (el0_svc overwrites for real syscall) */
	.if	\el == 0
	mov	w21, #NO_SYSCALL
	str	w21, [sp, #S_SYSCALLNO]
	.endif

	/*
	 * Set sp_el0 to current thread_info.
	 */
	.if	\el == 0
	msr	sp_el0, tsk			// 把得到的tsk,也就是该进程的struct task_struct放在sp_el0中了。
	.endif

	/*
	 * Registers that may be useful after this macro is invoked:
	 *
	 * x21 - aborted SP
	 * x22 - aborted PC
	 * x23 - aborted PSTATE
	*/
	.endm

关注一下其中一行汇编:

	msr	sp_el0, tsk			// 把得到的tsk,也就是该进程的struct task_struct放在sp_el0中了。

msr操作sp_el0,把tsk的值存到sp_el0中,所以很显然,tsk目前保存的就是当前进程的struct task_struct。现在解决了第一个问题:sp_el0何时保存了当前进程的struct task_struct。现在问题的关键在于第二点,tsk从哪里得到的呢?这需要看另外一行汇编:

arch/arm64/kernel/entry.S:
	ldr_this_cpu	tsk, __entry_task, x20	// Ensure MDSCR_EL1.SS is clear,这里有个静态全局变量:__entry_task,是个perCPU变量。

arch/arm64/include/asm/assembler.h:
	/*
	 * @dst: Result of READ_ONCE(per_cpu(sym, smp_processor_id()))
	 * @sym: The name of the per-cpu variable
	 * @tmp: scratch register
	 */
	.macro ldr_this_cpu dst, sym, tmp
	adr_l	\dst, \sym
alternative_if_not ARM64_HAS_VIRT_HOST_EXTN
	mrs	\tmp, tpidr_el1			
alternative_else
	mrs	\tmp, tpidr_el2
alternative_endif
	ldr	\dst, [\dst, \tmp]			/* 这一句有点看不明白,后续再研究研究,这一句等价于ldr tsk [__entry_task, tpidr_el1]*/
	.endm

关于这几个alternative函数,没找到啥资料,只有官方资料:

Syntax of the Framework's Macro
The macro syntax is similar to an if-then-else statement and is prefixed with the word alternative_. 
For example, the alternative_if is similar to the if statement, the alternative_if_not is similar to 
the if not, the alternative_else is similar to an else statement, and so on. The if macro marks the 
beginning of a code section, and the else macro starts a new code section. Finally, an endif macro 
ends the clause.

大致意思就是,在macro中,这几个函数和if,elss,endif没啥区别。所以这一段是通过这个if else判断是在EL1(内核态)还是EL2(虚拟机),从而获取到对应等级的tpidr_elx,这个寄存器会保存当前运行进程的pid。

一开始没看懂最后一句什么意思,因为被一些博客误导了。很多博客都说这个寄存器存放的是运行在cpu上的线程的id,所以一直在想这与ldr这条汇编指令有什么关系。但其实不是的,真正存放的是percpu的offset值,会在函数启动时进行初始化,会在其他笔记中介绍一下。

根据这个macro宏的注释就知道,这个宏函数会根据cpu的id,返回一个per_cpu的变量。在这里,我们返回的就是当前cpu的__entry_task变量。**该变量存放进程的进程描述符地址.**这样就解决了第二个问题,在sp_el0保存task_struct之前,task_struct存放在哪里。

接下来就涉及到__entry_task这个变量本身的问题,这个单独写了一篇博客,记录了一下percpu变量的学习笔记:LInux内核学习 – perCPU变量

<think> 在Linux内核中,`current`用于获取当前正在运行的进程的`task_struct`结构指针。这是内核中一个非常重要的机制,因为多任务操作系统需要频繁地访问当前任务的信息。下面我们将详细解释其实现原理,并给出相关的代码示例。 ### 实现原理 `current`的实现依赖于体系结构,因为不同架构的寄存器使用和栈布局可能不同。在大多数架构中,`current`的实现利用了内核栈上存储的当前任务结构的信息。以下是一些常见架构的实现方式: 1. **x86架构**(包括32位和64位): - 在x86架构中,内核栈的顶部(或底部,取决于栈增长方向)通常存储着当前任务的`thread_info`结构,而`thread_info`结构中包含一个指向`task_struct`的指针。 - 具体来说,内核栈的大小通常是8KB或4KB,并且是页面对齐的。因此,通过将栈指针(SP)的低位清零(即屏蔽掉低位的地址)就可以得到`thread_info`的地址,然后通过`thread_info`中的成员访问`task_struct`。 2. **ARM架构**: - 在ARM架构中,同样使用类似的方式。通常,内核栈的大小为8KB,并且栈顶(或底)存储着`thread_info`结构,其中包含指向`task_struct`的指针。 3. **使用专用寄存器**: - 有些架构(如PowerPC)使用专用的寄存器(例如`current`寄存器)来存储当前任务的`task_struct`指针,这样访问速度更快。 ### 代码示例 下面我们以x86_64架构为例,展示`current`的实现: 在Linux内核源代码中,通常可以在`arch/x86/include/asm/current.h`中找到定义: ```c #ifndef _ASM_X86_CURRENT_H #define _ASM_X86_CURRENT_H #include <linux/compiler.h> #include <asm/percpu.h> #ifndef __ASSEMBLY__ 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() #endif /* __ASSEMBLY__ */ #endif /* _ASM_X86_CURRENT_H */ ``` 在这个实现中: - `DECLARE_PER_CPU`声明了一个每CPU变量`current_task`,该变量在每个CPU核心上存储一个指向当前正在运行的任务的`task_struct`的指针。 - `get_current`函数通过`this_cpu_read_stable`读取当前CPU核心上的`current_task`变量,这个操作是高效的,因为它直接使用每CPU数据段。 ### 工作流程 1. **上下文切换**:当内核进行任务切换时,它会更新目标CPU核心的`current_task`变量,使其指向新任务的`task_struct`。 2. **获取当前任务**:当代码调用`current`时,它实际上读取了当前CPU核心上的`current_task`变量,从而获得了当前任务的指针。 ### 为什么需要每CPU变量 在多核系统中,每个CPU核心可能同时运行不同的任务。因此,每个核心都需要独立地记录自己当前正在运行的任务。使用每CPU变量可以避免锁的开销,并且访问速度很快。 ### 其他架构的实现 在ARM64架构中,`current`的实现通常使用`sp_el0`寄存器(如果可用)来存储当前任务的`task_struct`指针。这是因为ARMv8架构允许在EL1(内核模式)下使用`sp_el0`作为通用寄存器。这样,通过一条指令就可以获取当前任务的指针。 ```c // 在arch/arm64/include/asm/current.h中 #define get_current() ((struct task_struct *)arch_task_struct_addr(read_sysreg(sp_el0))) #define current get_current() ``` ### 总结 - `current`是获取当前任务`task_struct`指针的标准方法。 - 其实现是体系结构相关的,但核心思想都是快速访问当前CPU核心上正在运行的任务。 - 在x86架构中,通常使用每CPU变量来实现;在ARM64架构中,则可能使用专用寄存器。 §§相关问题§§: 1. Linux内核中`task_struct`结构体包含哪些重要信息? 2. 在Linux内核中,如何进行上下文切换(context switch)? 3. 每CPU变量(per-CPU variables)在内核中是如何实现和使用的? 4. 在ARM架构中,为什么使用`sp_el0`寄存器来存储当前任务结构? </think> # Linux 内核中 `current` 的实现原理 在 Linux 内核中,`current` 是用于获取当前正在运行的进程的 `task_struct` 指针的关键机制。这个的实现依赖于体系结构,但核心原理是相似的。下面我将详细解释其工作原理并提供相关代码示例。 ## 基本原理 `current` 的实现机制主要分为两种: 1. **通过线程信息结构**:在栈上存储线程信息,通过栈指针找到 `thread_info` 结构 2. **通过专用寄存器**:使用特定寄存器存储当前任务的指针(如 x86 的 `gs` 寄存器) ### 通用实现方式 ```c // 典型的内核栈布局 union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; }; // 通过栈指针获取 thread_info #define current_thread_info() \ ((struct thread_info *)(current_stack_pointer & ~(THREAD_SIZE - 1))) // 通过 thread_info 获取 task_struct #define current (current_thread_info()->task) ``` ## x86_64 架构的实现 在 x86_64 架构中,Linux 内核使用每 CPU 变量和 `gs` 寄存器来实现 `current` : ```c // 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() ``` ### 工作流程解析 1. **声明每 CPU 变量**: ```c DECLARE_PER_CPU(struct task_struct *, current_task); ``` 2. **上下文切换时更新**: 在 `context_switch()` 函数中,当切换到新任务时更新当前 CPU 的 `current_task`: ```c static __always_inline struct rq * context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next, struct rq_flags *rf) { // ... this_cpu_write(current_task, next); // ... } ``` 3. **通过 `gs` 寄存器访问**: x86_64 使用 `gs` 寄存器指向每 CPU 数据区: ```c // arch/x86/include/asm/percpu.h #define this_cpu_read_stable(var) \ ({ typeof(var) *ptr__; \ asm volatile("mov %%gs:%P1, %0" : "=r" (ptr__) : "p" (&(var))); \ *ptr__; }) ``` ## ARM64 架构的实现 在 ARM64 架构中,Linux 内核使用 `sp_el0` 寄存器存储当前任务指针: ```c // arch/arm64/include/asm/current.h static __always_inline struct task_struct *get_current(void) { unsigned long sp_el0; asm ("mrs %0, sp_el0" : "=r" (sp_el0)); return (struct task_struct *)sp_el0; } #define current get_current() ``` ### 工作流程解析 1. **任务切换时更新**: 在 `__switch_to()` 函数中更新 `sp_el0` 寄存器: ```c // arch/arm64/kernel/process.c __notrace_funcgraph struct task_struct *__switch_to( struct task_struct *prev, struct task_struct *next) { // ... mtspr(SPR_SP_EL0, next); // 将新任务指针写入 sp_el0 // ... } ``` 2. **通过 `mrs` 指令读取**: `get_current()` 函数使用汇编指令直接读取 `sp_el0` 寄存器值 ## 实现原理总结 | 架构 | 实现方式 | 关键寄存器 | 性能特点 | |---------|---------------------------|------------|----------------| | x86_64 | 每 CPU 变量 + gs 段寄存器 | gs | 高效内存访问 | | ARM64 | sp_el0 专用寄存器 | sp_el0 | 零内存访问延迟 | | PowerPC | r2 专用寄存器 | r2 | 快速访问 | | RISC-V | tp 线程指针寄存器 | tp | 高效实现 | ## 使用示例 在内核代码中使用 `current` 获取当前任务信息: ```c #include <linux/sched.h> void print_current_info(void) { struct task_struct *task = current; printk("Current process: %s (PID: %d)\n", task->comm, task->pid); printk("Process state: %ld\n", task->state); if (task->mm) { printk("Memory usage: %lu KB\n", task->mm->total_vm * PAGE_SIZE / 1024); } } ``` ## 性能优化考虑 1. **寄存器缓存**: ```c register struct task_struct *cur asm("r15") = current; ``` 在频繁访问的代码路径中,可将 `current` 存入寄存器 2. **避免多次调用**: 错误用法: ```c if (current->pid == ...) { ... current->comm ... } ``` 正确用法: ```c struct task_struct *cur = current; if (cur->pid == ...) { ... cur->comm ... } ``` 3. **RCU 保护**: 在抢占敏感区域使用 RCU 保护: ```c rcu_read_lock(); struct task_struct *cur = current; // 安全访问任务结构 rcu_read_unlock(); ``` ## 内核开发注意事项 1. **不可在中断上下文外使用**: 在中断处理程序中访问 `current` 可能不安全 2. **任务可能退出**: ```c struct task_struct *task = get_task_struct(current); // 现在可以安全使用 put_task_struct(task); ``` 3. **跨架构兼容性**: ```c #if defined(CONFIG_X86) // x86 特定代码 #elif defined(CONFIG_ARM64) // ARM64 特定代码 #endif ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值