zl程序教程

您现在的位置是:首页 >  系统

当前栏目

Linux 进程管理之current

Linux进程 管理 Current
2023-09-11 14:18:25 时间

前言

由于经常在内核中看到current,便记录下current的实现原理。

一、current简介

current表示当前正在运行进程的进程描述符struct task_struct。

在当前的x86_64和ARM64架构下 current的实现方式和早期Linux 版本current的实现的不一样,目前大多数内核教材都是描述的是早期Linux 版本current的实现方式:通过thread_info结构中的task成员可以获取task_struct结构的值。接下来我们便来讲述目前Linux版本的(比如常用的3.10.0版本)x86_64和ARM64架构下 current的实现原理。

二、x86_64 current 的实现

2.1 current_task 的实现

x86_64架构下每个 CPU 当前正在运行的 任务(task_struct)保存在 per-cpu变量中,当前进程的变量 current_task 就被声明为 per-cpu变量,如下所示:
关于x86_64架构per-cpu变量请参考:Linux per-cpu

// linux-4.10.1/arch/x86/include/asm/current.h
DECLARE_PER_CPU(struct task_struct *, current_task);

该per-cpu变量定义在:

// linux-4.10.1/arch/x86/kernel/cpu/common.c

/*
 * The following percpu variables are hot.  Align current_task to
 * cacheline size such that they fall in the same cacheline.
 */
DEFINE_PER_CPU(struct task_struct *, current_task) ____cacheline_aligned = &init_task;

系统刚刚初始化的时候,current_task 都指向 init_task:

// linux-4.10.1/init/init_task.c

/* Initial task structure */
struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);

当某个 CPU 上的进程进行切换的时候,current_task 被修改为将要切换到的目标进程,比如:进程切换函数 __switch_to 就会改变 current_task指向将要切换的进程 struct task_struct *next_p:

current_task = next_p;
// linux-4.10.1/arch/x86/kernel/process_64.c

/*switch_to(x,y) should switch tasks from x to y.*/

__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);
	......
	return prev_p;
}

2.2 读取current_task

// linux-4.10.1/arch/x86/include/asm/current.h

static __always_inline struct task_struct *get_current(void)
{
	return this_cpu_read_stable(current_task);
}

#define current get_current()
// linux-4.10.1/arch/x86/include/asm/percpu.h

#define this_cpu_read_stable(var)	percpu_stable_op("mov", var)

this_cpu_read() 使 gcc 每次访问var时都加载 percpu 变量,而 this_cpu_read_stable() 允许缓存该值var。
this_cpu_read_stable() 效率更高,如果保证其值在 cpu 中有效,则可以使用它。当前用户包括get_current()和get_thread_info(),它们实际上都是per-thread的变量,用per-cpu变量实现。因此在各自的任务期间是稳定的。

// linux-4.10.1/arch/x86/include/asm/percpu.h

#define percpu_stable_op(op, var)			\
({							\
	typeof(var) pfo_ret__;				\
	switch (sizeof(var)) {				\
	case 1:						\
		asm(op "b "__percpu_arg(P1)",%0"	\
		    : "=q" (pfo_ret__)			\
		    : "p" (&(var)));			\
		break;					\
	case 2:						\
		asm(op "w "__percpu_arg(P1)",%0"	\
		    : "=r" (pfo_ret__)			\
		    : "p" (&(var)));			\
		break;					\
	case 4:						\
		asm(op "l "__percpu_arg(P1)",%0"	\
		    : "=r" (pfo_ret__)			\
		    : "p" (&(var)));			\
		break;					\
	case 8:						\
		asm(op "q "__percpu_arg(P1)",%0"	\
		    : "=r" (pfo_ret__)			\
		    : "p" (&(var)));			\
		break;					\
	default: __bad_percpu_size();			\
	}						\
	pfo_ret__;					\
})

对于x86_64架构,struct task_struct *current_task,指针的大小为8个字节,所以sizeof(var) = 8

asm(op "q "__percpu_arg(P1)",%0"	\
    : "=r" (pfo_ret__)			\
    : "p" (&(var)));			\
// linux-4.10.1/arch/x86/include/asm/percpu.h
#ifdef CONFIG_X86_64
#define __percpu_seg		gs
#define __percpu_mov_op		movq

#ifdef CONFIG_SMP
#define __percpu_prefix		"%%"__stringify(__percpu_seg)":"

#define __percpu_arg(x)		__percpu_prefix "%" #x

展开:

#define percpu_stable_op(op, var)
{
	//定义一个current_task类型的变量 pfo_ret__
	//等价于 struct task_struct * pfo_ret__
	typeof(current_task) pfo_ret__;
	
	asm("movq" "%%gs:%P1","%0" 
		: "=r" (pfo_ret__) 		//输出部分
		:"p" (&(current_task))  //输入部分
	
	pfo_ret__;	
}

宏展开后其实主要就是一条mov指令,将gs寄存器里的地址,和%P1(current_task的地址)相加,即输入列表中的第一个参数。然后将相加后地址指向的内存空间里的值,移动到%0(%eax),即输出列表中的第一个参数 pfo_ret__。

简单说下内联汇编语法:
汇编指令中的数字和前缀%组合表示样板操作数,例如%0,%1等,用来依次指代后面的输出部分,输入部分等样板操作数。
gs是x86中的段寄存器,为了与%0,%1等区分开来,用两个%%来修饰寄存器,即%%gs。
%0表示pfo_ret__,r表示通用寄存器,=表示该操作符只写,一般用于输出操作数中。
%1表示&(current_task),p表示内存地址。
gcc汇编语法:Using-Assembly-Language-with-C

简单点说就是将gs寄存器里的地址与current_task的地址相加保存在pfo_ret__变量中,然后返回pfo_ret__变量。pfo_val__变量里存放的值,就是当前cpu执行的当前线程对象struct task_struct的地址。这样我们就得到了current_task的地址。

注意:
gs寄存器中存放的是当前cpu的percpu内存块的起始地址:base_address。
current_task的地址表示current_task变量在任意percpu内存块的位置:offest。
所以这两个地址相加:base_address + offest,得到的就是当前cpu的current_task变量的当前地址值。

小结:x86_64架构下每个 CPU 当前运行进程的 task_struct 的指针current_task存放到 Per CPU 变量 中; 可调用current(x86_64下实际调用的是 this_cpu_read_stable宏) 进行读取。

2.3 struct thread_info

task_struct 和 struct thread_info都用来保存进程相关信息,struct thread_info与体系架构相关。然而不同的体系结构里,进程需要存储的信息不尽相同,linux使用task_struct存储通用的信息,将体系结构相关的部分存储在thread_info中。早期的Linux版本中thread_info 结构在进程内核栈中。
早期版本的struct thread_info成员有struct task_struct *task,可以通过这个结构体来获取current。

// linux-2.6.32/arch/x86/include/asm/thread_info.h
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			preempt_count;	/* 0 => preemptable,
						   <0 => BUG */
	mm_segment_t		addr_limit;
	struct restart_block    restart_block;
	void __user		*sysenter_return;
	......
	int			uaccess_err;
};

通过esp寄存器的值和内核栈大小,就可以方便的计算出内核栈的栈底地址,该地址其实就是进程对应的struct thread_info结构的地址。

/* how to get the current stack pointer from C */
register unsigned long current_stack_pointer asm("esp") __used;

/* how to get the thread information struct from C */
static inline struct thread_info *current_thread_info(void)
{
	return (struct thread_info *)
		(current_stack_pointer & ~(THREAD_SIZE - 1));
}

用 current_thread_info()->task 来获取 task_struct:

#define get_current() (current_thread_info()->task)
#define current get_current()

这时候x86就已经对current的获取进行了优化,采用之前所说的 Per CPU 变量来获取current。

// linux-2.6.32/arch/x86/include/asmcurrent.h

struct task_struct;

DECLARE_PER_CPU(struct task_struct *, current_task);

static __always_inline struct task_struct *get_current(void)
{
	return percpu_read_stable(current_task);
}

#define current get_current()

小结:在早期的内核中,比如2.6.32,通过current_thread_info()->task得到struct task_struct在x86上也是支持的。所以通过current_thread_info()->task和 Per CPU 变量都可以来获取current。

现在的内核版本struct thread_info已经没有struct task_struct *task成员了,不能再通过current_thread_info()->task来获取current,必须通过之前所说的 Per CPU 变量来获取current。

// linux-4.10.1/arch/x86/include/asm/thread_info.h

struct thread_info {
	unsigned long		flags;		/* low level flags */
};

2.4 current_thread_info

如果配置了CONFIG_THREAD_INFO_IN_TASK选项,调用 current_thread_info获取当前struct thread_info,很简单就是将current指针转化为struct thread_info指针就可以了,因为struct thread_info是struct task_struct的第一个成员,两个结构体的首地址是一样的。
在这里插入图片描述
由于较高版本的内核,x86_64架构下struct thread_info成员较少,只有一个,CONFIG_THREAD_INFO_IN_TASK宏都是打开的,struct thread_info直接放在struct task_struct中。

// linux-4.10.1/arch/x86/include/asm/thread_info.h

struct thread_info {
	unsigned long		flags;		/* low level flags */
};
// linux-4.10.1/include/linux/thread_info.h

#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
 * For CONFIG_THREAD_INFO_IN_TASK kernels we need <asm/current.h> for the
 * definition of current, but for !CONFIG_THREAD_INFO_IN_TASK kernels,
 * including <asm/current.h> can cause a circular dependency on some platforms.
 */
#include <asm/current.h>
#define current_thread_info() ((struct thread_info *)current)
#endif
// linux-4.10.1/include/linux/sched.h

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
	/*
	 * For reasons of header soup (see current_thread_info()), this
	 * must be the first element of task_struct.
	 */
	struct thread_info thread_info;
	......
}

三、ARM64 current 的实现

由于ARM64的通用寄存器很多,直接采用通用寄存器来存储 current_task。这样获取current直接读取对应的寄存器便可以了。

/*
 * We don't use read_sysreg() as we want the compiler to cache the value where
 * possible.
 */
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()

Armv8-A 架构定义了一组异常级别,EL0 到 EL3:

EL0 Applications.
EL1 OS kernel and associated functions that are typically described as privileged.
EL2 Hypervisor.
EL3 Secure monitor.

In addition, in AArch64 state, most register names include the lowest Exception level that can access the register as a suffix to the register name:

 <register_name>_ELx, where x is 0, 1, 2, or 3.

sp就是堆栈寄存器。在ARM64里,CPU可以运行在四个级别中,分别是el0、el1、el2、el3,el0则就是用户空间,el1则是内核空间。sp_el0就是用户堆栈寄存器。

看一下ARM64架构下进程切换函数 __switch_to :

// linux-4.10.1/arch/arm64/kernel/process.c

/*
 * Thread switching.
 */
struct task_struct *__switch_to(struct task_struct *prev,
				struct task_struct *next)
{
	struct task_struct *last;

	.......

	/* the actual thread switch */
	last = cpu_switch_to(prev, next);

	return last;
}

// linux-4.10.1/arch/arm64/kernel/entry.S

/*
 * Register switch for AArch64. The callee-saved registers need to be saved
 * and restored. On entry:
 *   x0 = previous task_struct (must be preserved across the switch)
 *   x1 = next task_struct
 * Previous and next are guaranteed not to be the same.
 *
 */
ENTRY(cpu_switch_to)
	mov	x10, #THREAD_CPU_CONTEXT
	add	x8, x0, x10
	mov	x9, sp
	stp	x19, x20, [x8], #16		// store callee-saved registers
	stp	x21, x22, [x8], #16
	stp	x23, x24, [x8], #16
	stp	x25, x26, [x8], #16
	stp	x27, x28, [x8], #16
	stp	x29, x9, [x8], #16
	str	lr, [x8]
	add	x8, x1, x10
	ldp	x19, x20, [x8], #16		// restore callee-saved registers
	ldp	x21, x22, [x8], #16
	ldp	x23, x24, [x8], #16
	ldp	x25, x26, [x8], #16
	ldp	x27, x28, [x8], #16
	ldp	x29, x9, [x8], #16
	ldr	lr, [x8]
	mov	sp, x9
	msr	sp_el0, x1
	ret
ENDPROC(cpu_switch_to)

简单介绍下上述汇编:
在这里插入图片描述

STP(stp):Store Pair of Registers 根据 base register value和 immediate offset 计算地址,并将两个32位字或两个64位双字从两个寄存器存储到计算的地址。
简单点说就是将一对寄存器的值存储到内存地址中。

64-bit variant
STP <Xt1>, <Xt2>, [<Xn|SP>], #<imm>

LDP(ldp):Load Pair of Registers 根据 base register value地址和 immediate offset,从内存中加载两个 32 位字或两个 64 位双字,并将它们写入两个寄存器。
简单点说就是将内存中的值加载到一对寄存器中。

64-bit variant
LDP <Xt1>, <Xt2>, [<Xn|SP>], #<imm>

这段汇编的意思是:
(1)x0 = previous task_struct,store callee-saved registers
将previous task_struct 的x19 - x29、 x9、 lr寄存器都存储在内核堆栈中。
(2)x1 = next task_struct,restore callee-saved registers
将next task_struct的x19 - x29、 x9、 lr寄存器从堆栈中恢复。

我们看到了:

/* x1 = next task_struct */
msr	sp_el0, x1

其中x1就是next进程的struct task_struct结构,将next task_struct存储在sp_el0寄存器中。在进程切换的时候,把将要运行进程的struct task_struct存储在sp_el0寄存器中,这要直接读取sp_el0寄存器就获得当前正在运行的struct task_struct,即current。

总结

x86_64架构下每个 CPU 当前运行进程的 task_struct 的指针current_task存放到 per-cpu 变量中。
ARM64架构下每个 CPU 当前运行进程的 task_struct 的指针current_task存放到 sp_el0 寄存器中。

任务切换的时候必须要更新一下current,指向 next task_struct。

参考链接

Linux 4.10.0
Linux 2.6.32

ARM64官方文档

https://zhuanlan.zhihu.com/p/340985476
https://www.cnblogs.com/crybaby/p/14082593.html

https://blog.csdn.net/longwang155069/article/details/104346778
https://zhuanlan.zhihu.com/p/296750228