Linux: ARM32各CPU模式下栈配置

news2024/12/17 19:41:42

文章目录

  • 1. 前言
  • 2. 背景
  • 3. ARM32 中断向量表 和 中断处理流程
    • 3.1 ARM32 中断向量表
    • 3.2 ARM32 中断处理流程
  • 4. ARM32 各CPU模式下的栈配置
    • 4.1 SVC模式下各CPU栈配置(内核栈配置)
      • 4.1.1 BOOT CPU SVC模式栈配置(内核栈配置)
      • 4.1.2 非 BOOT CPU SVC模式栈配置(内核栈配置)
    • 4.2 中断异常模式下各CPU栈配置
      • 4.2.1 系统启动阶段的中断异常模式下各CPU栈配置
      • 4.2.2 中断异常发生时各异常模式CPU栈配置
    • 4.3 User模式栈配置(用户空间栈配置)
      • 4.3.1 启动新程序时的堆栈配置流程
      • 4.3.2 子进程堆栈配置流程
      • 4.3.3 线程堆栈配置流程
      • 4.3.4 其它情形的栈配置
  • 5. 观察和调整进程栈空间
    • 5.1 观察进程栈空间
    • 5.2 调整进程栈空间
  • 6. 参考资料

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本文基于 ARM32 架构 + Linux 4.14 内核源码 进行分析。

3. ARM32 中断向量表 和 中断处理流程

看到这里,读者可能产生了疑问,栈的使用和中断有什么关系?这里先给出答案:应用的 各CPU 的内核模式栈(即 CPU SVC 模式栈),是在系统启动阶段进行配置;应用的用户栈(即 CPU User 模式栈),是在系统创建应用进程时进行配置;而除 CPU SVC/User 模式的栈外,其它异常模式的栈,在系统启动阶段为每个CPU配置了一个很小空间的栈,但是这个小空间,对于处理异常是不够的,于是在进入各异常模式向量后、在正式处理异常之前,将 CPU 模式切换到 SVC 模式,进而借用 CPU SVC 模式栈(即各 CPU 的内核栈)进行异常处理。有一个例外的是 ARM32 CPU 的 System 模式不被 Linux 使用,所以也不涉及到栈的配置。
由于在 CPU 异常模式下进行了栈配置,自然就涉及到中断处理流程,所以在这里先简单介绍下 ARM32 中断向量的组织结构,以及中断的处理简要流程。

3.1 ARM32 中断向量表

在 ARM32 架构的 Linux 内核代码,中断向量组织可以认为是一个二维数组 vectors[8][16]vectors[8][16] 数组的第一维索引是如下CPU 异常模式
在这里插入图片描述
vectors[8][16] 数组的第二维索引是如下CPU 模式的低4位
在这里插入图片描述
注意,第二维索引 是以上图中 Mode number ,即以 ARM32 CPU 模式的低4位为索引,ARM32 CPU 没有实现所有 5 位 CPU 模式的所有位模式组合,因此向量表的有些第二维的入口是非法的,定义为 __irq_invalid 。来看 ARM32 CPU 中断向量表 的具体定义,先看向量表第一维的组织形式:

/* arch/arm/kernel/entry-armv.S */

	/* 中断向量表第一维:按 CPU 异常模式 组织 */
	.section .vectors, "ax", %progbits
.L__vectors_start:
	W(b)	vector_rst
	W(b)	vector_und
	W(ldr)	pc, .L__vectors_start + 0x1000
	W(b)	vector_pabt
	W(b)	vector_dabt
	W(b)	vector_addrexcptn
	W(b)	vector_irq
	W(b)	vector_fiq

再看向量表的第二维组织形式:

/* arch/arm/kernel/entry-armv.S */

/* reset 异常向量表 */
vector_rst:
 ARM(	swi	SYS_ERROR0	)
 THUMB(	svc	#0		)
 THUMB(	nop			)
	b	vector_und

/* Undefined 异常向量表 */
vector_stub	und, UND_MODE /* vector_und */
	.long	__und_usr			@  0 (USR_26 / USR_32) /* User 模式 未定义指令异常 入口 */
	.long	__und_invalid			@  1 (FIQ_26 / FIQ_32)
	.long	__und_invalid			@  2 (IRQ_26 / IRQ_32)
	.long	__und_svc			@  3 (SVC_26 / SVC_32) /* SVC 模式 未定义指令异常 入口 */
	.long	__und_invalid			@  4
	.long	__und_invalid			@  5
	.long	__und_invalid			@  6
	.long	__und_invalid			@  7
	.long	__und_invalid			@  8
	.long	__und_invalid			@  9
	.long	__und_invalid			@  a
	.long	__und_invalid			@  b
	.long	__und_invalid			@  c
	.long	__und_invalid			@  d
	.long	__und_invalid			@  e
	.long	__und_invalid			@  f

/* 预取(Prefetch)异常向量表 */
vector_stub	pabt, ABT_MODE, 4 /* vector_pabt */
	.long	__pabt_usr			@  0 (USR_26 / USR_32)
	.long	__pabt_invalid			@  1 (FIQ_26 / FIQ_32)
	.long	__pabt_invalid			@  2 (IRQ_26 / IRQ_32)
	.long	__pabt_svc			@  3 (SVC_26 / SVC_32)
	.long	__pabt_invalid			@  4
	.long	__pabt_invalid			@  5
	.long	__pabt_invalid			@  6
	.long	__pabt_invalid			@  7
	.long	__pabt_invalid			@  8
	.long	__pabt_invalid			@  9
	.long	__pabt_invalid			@  a
	.long	__pabt_invalid			@  b
	.long	__pabt_invalid			@  c
	.long	__pabt_invalid			@  d
	.long	__pabt_invalid			@  e
	.long	__pabt_invalid			@  f

/* 数据异常(Data Abort)向量表 */
vector_stub	dabt, ABT_MODE, 8 /* vector_dabt */
	.long	__dabt_usr			@  0  (USR_26 / USR_32)
	.long	__dabt_invalid			@  1  (FIQ_26 / FIQ_32)
	.long	__dabt_invalid			@  2  (IRQ_26 / IRQ_32)
	.long	__dabt_svc			@  3  (SVC_26 / SVC_32)
	.long	__dabt_invalid			@  4
	.long	__dabt_invalid			@  5
	.long	__dabt_invalid			@  6
	.long	__dabt_invalid			@  7
	.long	__dabt_invalid			@  8
	.long	__dabt_invalid			@  9
	.long	__dabt_invalid			@  a
	.long	__dabt_invalid			@  b
	.long	__dabt_invalid			@  c
	.long	__dabt_invalid			@  d
	.long	__dabt_invalid			@  e
	.long	__dabt_invalid			@  f

/* 地址异常(Address Exception)向量表 */
vector_addrexcptn:
	b	vector_addrexcptn
	
/* IRQ 异常向量表 */
vector_stub	irq, IRQ_MODE, 4 /* vector_irq */
	.long	__irq_usr			@  0  (USR_26 / USR_32)
	.long	__irq_invalid			@  1  (FIQ_26 / FIQ_32)
	.long	__irq_invalid			@  2  (IRQ_26 / IRQ_32)
	.long	__irq_svc			@  3  (SVC_26 / SVC_32)
	.long	__irq_invalid			@  4
	.long	__irq_invalid			@  5
	.long	__irq_invalid			@  6
	.long	__irq_invalid			@  7
	.long	__irq_invalid			@  8
	.long	__irq_invalid			@  9
	.long	__irq_invalid			@  a
	.long	__irq_invalid			@  b
	.long	__irq_invalid			@  c
	.long	__irq_invalid			@  d
	.long	__irq_invalid			@  e
	.long	__irq_invalid			@  f

/* FIQ 异常向量表 */
vector_stub	fiq, FIQ_MODE, 4 /* vector_fiq */
	.long	__fiq_usr			@  0  (USR_26 / USR_32)
	.long	__fiq_svc			@  1  (FIQ_26 / FIQ_32)
	.long	__fiq_svc			@  2  (IRQ_26 / IRQ_32)
	.long	__fiq_svc			@  3  (SVC_26 / SVC_32)
	.long	__fiq_svc			@  4
	.long	__fiq_svc			@  5
	.long	__fiq_svc			@  6
	.long	__fiq_abt			@  7
	.long	__fiq_svc			@  8
	.long	__fiq_svc			@  9
	.long	__fiq_svc			@  a
	.long	__fiq_svc			@  b
	.long	__fiq_svc			@  c
	.long	__fiq_svc			@  d
	.long	__fiq_svc			@  e
	.long	__fiq_svc			@  f

3.2 ARM32 中断处理流程

发生中断异常后,系统自动将执行流程跳转到中断上述向量表 .L__vectors_start 中,对应异常模式的向量表入口,如发生了 IRQ 中断,则会进入到向量表 .L__vectors_start 中 vector_irq 向量表项;然后再根据 CPU 的模式是 SVC 还是 User ,分别跳转到第二维中断向量表 vector_stub irq, IRQ_MODE, 4__irq_svc(SVC模式)__irq_usr(User 模式) 入口进行执行;在处理完中断后,再返回到被中断的程序继续执行。这就是中断执行的主干流程,更多关于中断处理流程的细节,可参考 Linux: 中断实现简析 。

4. ARM32 各CPU模式下的栈配置

4.1 SVC模式下各CPU栈配置(内核栈配置)

4.1.1 BOOT CPU SVC模式栈配置(内核栈配置)

/* arch/arm/kernel/head-common.S */

	__INIT
__mmap_switched: /* 此处代码运行于 MMU 开启状况 */
	adr	r3, __mmap_switched_data /* r3 = __mmap_switched_data 虚拟地址 */
	...

	/*
	 * r4 = &processor_id (arch/arm/kernel/setup.c)
	 * r5 = &__machine_arch_type (arch/arm/kernel/setup.c)
	 * r6 = &__atags_pointer (arch/arm/kernel/setup.c)
	 * r7 = &cr_alignment (arch/arm/kernel/entry-armv.S)
	 * sp = 当前 CPU 的 swapper 进程内核栈指针
	 */
	 ARM(	ldmia	r3, {r4, r5, r6, r7, sp})

	...
	b	start_kernel /* 跳转到 start_kernel() 执行 */

	align	2
	.type	__mmap_switched_data, %object
__mmap_switched_data:
	...
	/* 当前 CPU 的 swapper 进程内核栈指针 */
	.long	init_thread_union + THREAD_START_SP @ sp
	.size	__mmap_switched_data, . - __mmap_switched_data

4.1.2 非 BOOT CPU SVC模式栈配置(内核栈配置)

/* arch/arm/kernel/smp.c */

/* 非 BOOT CPU 启动流程中 */
__cpu_up(cpu, idle)
	...
	
	/* 配置 @cpu (非 BOOT CPU) 的首进程 swapper 的内核栈空间 */
	secondary_data.stack = task_stack_page(idle) + THREAD_START_SP;

	...

之后的 非 BOOT CPU 启动流程__cpu_up() -> ... -> secondary_startup

/* arch/arm/kernel/head.S */

ENTRY(secondary_startup)
	...
	
	/*
	 * Use the page tables supplied from  __cpu_up.
	 */
	adr	r4, __secondary_data /* r4 = __secondary_data 的当前物理地址 */
	/*
	 * r5 = __secondary_data 的链接虚拟地址
	 * r7 = secondary_data 的链接虚拟地址
	 * r12 = __secondary_switched 的链接虚拟地址
	 */
	ldmia	r4, {r5, r7, r12}		@ address to jump to after
	...
	/* r13 = __secondary_switched 的链接虚拟地址, __enable_mmu 后跳转到此处执行 */
	mov	r13, r12			@ __secondary_switched address
	...
ENDPROC(secondary_startup)

之后会经历 secondary_startup -> __turn_mmu_on

/* arch/arm/kernel/head.S */

ENTRY(__turn_mmu_on)
	...
	/*
	 * BOOT CPU: r13 = __mmap_switched
	 * 非 BOOT CPU: r13 = __secondary_switched 的链接虚拟地址
	 */
	mov	r3, r13
	/*
	 * BOOT CPU: 返回到 __mmap_switched 处
	 * 非 BOOT CPU: 返回到 __secondary_switched 处
	 */
	ret	r3
__turn_mmu_on_end:

ENTRY(__secondary_switched)
	/* 配置非 BOOT CPU SVC 模式栈(即内核栈) */
	ldr	sp, [r7, #12]			@ get secondary_data.stack
	mov	fp, #0
	b	secondary_start_kernel
ENDPROC(__secondary_switched)

4.2 中断异常模式下各CPU栈配置

4.2.1 系统启动阶段的中断异常模式下各CPU栈配置

start_kernel()
	setup_arch()
		setup_processor()
			cpu_init()
/* arch/arm/kernel/setup.c */

void notrace cpu_init(void)
{
#ifndef CONFIG_CPU_V7M
unsigned int cpu = smp_processor_id();
	struct stack *stk = &stacks[cpu];

	if (cpu >= NR_CPUS) {
		pr_crit("CPU%u: bad primary CPU number\n", cpu);
		BUG();
	}

	/*
	 * This only works on resume and secondary cores. For booting on the
	 * boot cpu, smp_prepare_boot_cpu is called after percpu area setup.
	 */
	set_my_cpu_offset(per_cpu_offset(cpu)); // TODO: per CPU 相关

	/*
	 * arch/arm/mm/proc-v7.S, cpu_v7_proc_init
	 * ...
	 */
	cpu_proc_init();

	/*
	 * Define the placement constraint for the inline asm directive below.
	 * In Thumb-2, msr with an immediate value is not allowed.
	 */
#ifdef CONFIG_THUMB2_KERNEL
#define PLC	"r"
#else
#define PLC	"I"
#endif

	/*
	 * setup stacks for re-entrant exception handlers
	 */
	__asm__ (
	"msr	cpsr_c, %1\n\t" /* CPU 切换到 IRQ 模式 */
	"add	r14, %0, %2\n\t"
	"mov	sp, r14\n\t" /* 设置 IRQ 模式堆栈 */
	"msr	cpsr_c, %3\n\t" /* CPU 切换到 Abort 模式 */
	"add	r14, %0, %4\n\t"
	"mov	sp, r14\n\t" /* 设置 ABT 模式堆栈 */
	"msr	cpsr_c, %5\n\t" /* CPU 切换到 Undefined 模式 */
	"add	r14, %0, %6\n\t"
	"mov	sp, r14\n\t" /* 设置 UND 模式堆栈 */
	"msr	cpsr_c, %7\n\t" /* CPU 切换到 FIQ 模式 */
	"add	r14, %0, %8\n\t"
	"mov	sp, r14\n\t" /* 设置 FIQ 模式堆栈 */
	"msr	cpsr_c, %9" /* CPU 切回 SVC 模式 */
	    :
	    : "r" (stk), /* %0 */
	      PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE), /* %1 */
	      "I" (offsetof(struct stack, irq[0])), /* %2 */
	      PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE), /* %3 */
	      "I" (offsetof(struct stack, abt[0])), /* %4 */
	      PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE), /* %5 */
	      "I" (offsetof(struct stack, und[0])), /* %6 */
	      PLC (PSR_F_BIT | PSR_I_BIT | FIQ_MODE), /* %7 */
	      "I" (offsetof(struct stack, fiq[0])), /* %8 */
	      PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE) /* %9 */
	    : "r14");
#endif
}

上面的代码,为每个 CPU 的各异常模式 IRQ, Abort, Undefined, FIQ ,配置了定义在 stacks[] 中的栈空间,看一下 stacks[] 定义:

/*
 * 除 User,System,SVC 3个模式外的堆栈. 
 * User,SVC 3个模式的堆栈,分别由应用程序,或 各 CPU 启动阶段各自设置,
 * 而 Linux 内核模式不适用 System 模式。
 */
struct stack {
	u32 irq[3];
	u32 abt[3];
	u32 und[3];
	u32 fiq[3];
} ____cacheline_aligned;

#ifndef CONFIG_CPU_V7M
static struct stack stacks[NR_CPUS]; /* 每 CPU、每模式(除 SVC/System/User 模式外)的 堆栈 */
#endif

看到了吧,每个 CPU 的各异常模式的栈空间大小3 个 u32 大小,总共 12 字节,对于异常处理流程,这个真是太小了。我们继续看下一小节,看 ARM32 Linux 内核是怎么处理这个问题的。

4.2.2 中断异常发生时各异常模式CPU栈配置

异常发生是,会跳转到中断向量表中去执行。假设发生了一个 IRQ 中断,将跳转到 vector_irq 处执行:

vector_stub	irq, IRQ_MODE, 4 /* vector_irq */
	.long	__irq_usr			@  0  (USR_26 / USR_32)
	.long	__irq_invalid			@  1  (FIQ_26 / FIQ_32)
	.long	__irq_invalid			@  2  (IRQ_26 / IRQ_32)
	.long	__irq_svc			@  3  (SVC_26 / SVC_32)
	...

汇编宏 vector_stub 定义了 vector_irq 中断向量,但关于 vector_stub 一些至关重要的细节,我们在前面没有展开,这里补充一下:

	.macro	vector_stub, name, mode, correction=0
	.align	5

vector_\name:
	.if \correction
	sub	lr, lr, #\correction
	.endif

	/*
	 * 刚进入中断异常时,使用的是各模式独立的栈,除 User/SVC/System 模式的栈,
	 * 其它在各 CPU 启动阶段初始化:
	 * start_kernel()
	 *		setup_arch()
	 *			setup_processor()
	 *				cpu_init()
	 *					// static struct stack stacks[NR_CPUS]; 
	 * User 模式的栈,由 C 启动代码初始化;
	 * SVC 模式的栈是各 CPU 启动阶段,或进程切换是,设置为进程内核栈;
	 * 另外,Linux 不适用 System 模式。
	 * 可以看到,各 CPU 在 cpu_init() 设置的栈:
	 * struct stack {
	 *		u32 irq[3];
	 *		u32 abt[3];
	 *		u32 und[3];
	 *		u32 fiq[3];
	 * } ____cacheline_aligned;
	 * 各模式(除 User/System/SVC 模式外)仅有 3 个 u32 的空间,从
	 * 下面的代码可以看到,这3个 u32 用来存储当前的 {r0, lr, spsr},
	 * 如进入 Undefined 模式,则对应 CPU 的:
	 * stack::und[0] = r0 (被 und 中断时 r0 的值, r0 所有模式共用)
	 * stack::und[1] = lr_und
	 * stack::und[2] = spsr_und (被 und 中断模式下的 cpsr)
	 * 就这样,当前异常模式下的 3 个 u32 空间,已经被消耗光了,
	 * 接下来如果还要使用栈空间,该怎么办? 答案是,代码将 CPU 切换
	 * 到 SVC 模式,复用 SVC 模式的栈空间。
	 */
	stmia	sp, {r0, lr}		@ save r0, lr
	mrs	lr, spsr /* lr = spsr_und (被异常中断模式的 cpsr, 如 cpsr_usr) */
	str	lr, [sp, #8]		@ save spsr

	/* 切换到 SVC 模式,复用 SVC 模式的栈空间 */
	mrs	r0, cpsr
	eor	r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
	msr	spsr_cxsf, r0 /* 切换到 SVC 模式 */

	/* 
	 * 仅保留 spsr (被异常中断模式的 cpsr) 低 4 位,即
	 * CPU 模式低 4 位,用来索引异常模式向量表 .vectors ,
	 * 以定义异常模式向量入口。
	 */
	and	lr, lr, #0x0f

	/*
 	 * 现在 CPU 已经位于 SVC 模式,sp 也指向了 SVC 模式的内核栈.
 	 * 因为接下来会复用 SVC 模式栈空间, 可能修改 sp 的值,所以先保存 SVC 的 sp 指针到 r0
 	 */
	mov	r0, sp /* r0 = SVC 模式栈指针当前值 */
	/* 
	 * 定位异常向量表入口, 根据前面的代码:
	 * and	lr, lr, #0x0f
	 * 以及此处使用
	 * pc + (lr << 2)
	 * 来定义为异常向量入口,因为此时 pc 指向下一条指令
	 * movs	pc, lr
	 * 索引这要求当前异常模式的向量表,必须紧跟着
	 * movs	pc, lr
	 * 指令,中间不能存在任何空隙。
	 */
 ARM(	ldr	lr, [pc, lr, lsl #2]	) /* lr = CPU 当前异常模式向量入口 (如 __und_svc / __und_usr) */
 	/* 跳转到异常向量入口 (如 __und_svc / __und_usr) */
	movs	pc, lr			@ branch to handler in SVC mode
ENDPROC(vector_\name)

看到了吧,异常处理的代码,会先保存 r0,lr,spsr_xxx 到 cpu_init() 配置的异常模式栈上,然后将 CPU 模式切换到 SVC 模式,然后使用 SVC 模式的栈(内核栈),进行异常处理流程。上述分析中,涉及到多核CPU启动过程,细节可参考 Linux: 多核CPU启动流程简析 。

4.3 User模式栈配置(用户空间栈配置)

4.3.1 启动新程序时的堆栈配置流程

bash 通过 fork + exec() 系统调用来启动一个新程序:系统调用返回用户空间前,配置了堆栈空间;系统调用返回用户空间时,将设置配置好的堆栈指针值到 sp 寄存器。来看细节:

/* fs/exec.c */

/* 只看 exec() 系统调用过程,这是重点 */
static int do_execveat_common(int fd, struct filename *filename,
         struct user_arg_ptr argv,
         struct user_arg_ptr envp,
         int flags)
{
	...
	struct linux_binprm *bprm;
	...

	...

	bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);

	...

	/*
	  * 为可执行程序创建新的进程地址空间管理数据并初始化
	  * . 为新程序创建并初始化内存管理对象 mm_struct: 创建页目录表等
	  * . 为新程序初始化栈空间: [STACK_TOP_MAX - PAGE_SIZE, STACK_TOP_MAX]
	  *                          @bprm->p 指向 栈空间第1个可用位置 STACK_TOP_MAX - sizeof(void *)
	  */
	retval = bprm_mm_init(bprm);

	...

	/* 将可执行程序文件名压入程序栈 bprm->p (相应的移动栈指针 bprm->p 到下一个可用位置) */
 	retval = copy_strings_kernel(1, &bprm->filename, bprm);

	bprm->exec = bprm->p; /* 指向可执行程序文件名 */
	retval = copy_strings(bprm->envc, envp, bprm); /* 拷贝环境变量 */
	
	/* 将程序用户参数压入程序栈 (相应的移动栈指针 bprm->p 到下一个可用位置) */
 	retval = copy_strings(bprm->argc, argv, bprm);

	...

	/* 调用具体类型的程序加载器, 来加载新程序 */
 	retval = exec_binprm(bprm); /* 以 ELF 格式程序加载为例: -> load_elf_binary() */
 	...

	return retval;
	...
}

ELF 程序加载过程中,重新设定堆栈空间位置和大小:

/* fs/binfmt_elf.c */

static int load_elf_binary(struct linux_binprm *bprm)
{
	...
	
	/* 
	  * 随机化栈顶后, 重新设定程序栈空间位置和大小. 
	  * 如果栈空间位置和大小发生了变化:
	  * . 将之前压入到栈上的程序名和参数移动到新栈空间的相应位置;
	  * . 相应的调整栈空间 VMA (bprm->vma) 和 栈指针 bprm->p
	  */
	 retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
     			executable_stack);

	...

	/* N.B. passed_fileno might not be initialized? */
	 current->mm->end_code = end_code;
	 current->mm->start_code = start_code;
	 current->mm->start_data = start_data;
	 current->mm->end_data = end_data;
	 current->mm->start_stack = bprm->p; /* 程序启动前, 栈指针位置 */

	...

	/* 
	  * 启动 程序 或 解释器程序: 
	  * 程序 或 解释器程序初始化后, 然后进入程序代码开始运行。
	  *
	  * 注意到,这里最主要的动作是赋值了用户模式下的 PC SP 等寄存器,
	  * 程序代码真正运行起来是在系统调用 sys_execve*() 返回用户空间之后。
	  */
	 start_thread(regs, elf_entry, bprm->p);
	 retval = 0;
	 ...
out_ret:
 	return retval;
}

看一下 start_thread() 到底做了什么工作:

/* arch/arm/include/asm/processor.h */

#define start_thread(regs,pc,sp)     \
({         \
	memset(regs->uregs, 0, sizeof(regs->uregs));   \
	if (current->personality & ADDR_LIMIT_32BIT)   \
		regs->ARM_cpsr = USR_MODE;    \
	else        \
		regs->ARM_cpsr = USR26_MODE;    \
	if (elf_hwcap & HWCAP_THUMB && pc & 1)    \
		regs->ARM_cpsr |= PSR_T_BIT;    \
	regs->ARM_cpsr |= PSR_ENDSTATE;     \
	regs->ARM_pc = pc & ~1;  /* pc */   \
	/* 设置程序用户态堆栈指针 */        \
	regs->ARM_sp = sp;  /* sp */   \
	nommu_start_thread(regs);     \
})

start_thread() 并不会像它的名字一样,将新进程调度起来。新程序真正调度起来,是在系统调用返回用户空间时完成的,此时会配置用户空间的堆栈指针 sp 寄存器:

/* arch/arm/kernel/entry-armv.S */

/*
 * Register switch for ARMv3 and ARMv4 processors
 * r0 = previous task_struct, r1 = previous thread_info, r2 = next thread_info
 * previous and next are guaranteed not to be the same.
 */
ENTRY(__switch_to)
	add ip, r1, #TI_CPU_SAVE
 ARM( stmia ip!, {r4 - sl, fp, sp, lr} ) @ Store most regs on stack
 	ldr r4, [r2, #TI_TP_VALUE]
 	ldr r5, [r2, #TI_TP_VALUE + 4]
 	......	
	mov r0, r5
	/* 加载新进程的寄存器(包括 sp, pc),然后切换到新进程运行 */
 ARM( ldmia r4, {r4 - sl, fp, sp, pc}  ) @ Load all regs saved previously	
ENDPROC(__switch_to)

应该了解的是,前面只是配置了进程堆栈的虚拟地址空间,而真正的物理内存分配,是在写入时触发缺页中断完成的。

4.3.2 子进程堆栈配置流程

可以通过 fork() 系统调用来创建子进程,这些进程会在创建时复制父进程的地址空间(包含堆栈);然后在写入时发生写时拷贝(COW: Copy-On-Write),进而建立进程自己独立的内存空间。来看细节:

/* kernel/fork.c */

sys_fork()
	_do_fork()
		copy_process()
			copy_mm()
			copy_thread_tls()
				copy_thread() /* 细节见 4.3.3 */

static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
	struct mm_struct *mm, *oldmm;
	
	...

	oldmm = current->mm;

	...

	/* initialize the new vmacache entries */
	vmacache_flush(tsk);

	mm = dup_mm(tsk);
	...

good_mm:
	tsk->mm = mm;
	tsk->active_mm = mm;
	return 0;
}

static struct mm_struct *dup_mm(struct task_struct *tsk)
{
	struct mm_struct *mm, *oldmm = current->mm;

	mm = allocate_mm();

	memcpy(mm, oldmm, sizeof(*mm));

	if (!mm_init(mm, tsk, mm->user_ns))
		goto fail_nomem;

	err = dup_mmap(mm, oldmm);
	
	...

	return mm;
}

4.3.3 线程堆栈配置流程

不同于上小节通过 fork() 系统调用创建的进程,应用编程中,我们常见到使用 pthread_create() 来创建线程(传递 CLONE_VM 标志位给系统调用 clone()),这些线程会共享主线程(线程组leader)的地址空间:

/* kernel/fork.c */

sys_clone()
	_do_fork()
		copy_process()
			copy_mm()

static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
	struct mm_struct *mm, *oldmm;
	
	...
	
	oldmm = current->mm;
	
	...
	
	if (clone_flags & CLONE_VM) { /* clone() / vfork(): 共享主线程(线程组leader)的地址空间 */
		mmget(oldmm); /* 增加地址空间 mm_struct 引用计数 */
		mm = oldmm;
		goto good_mm;
	}

	...

good_mm:
	tsk->mm = mm;
	tsk->active_mm = mm;
	return 0;
}

新线程共享了主线程(线程组leader)的地址空间,也就意味着共享了堆栈空间,这显然是不行的:线程组内线程 A 将堆栈指针 sp 放置了位置 P1,这时候切换到线程 B 执行,线程 B 将堆栈指针 sp 切换到比 P1 地址更大的 P2(假设堆栈由高地址向低地址增长),然后一通写,当再切回到线程 A 的时候,线程 A 发现自己栈上数据已经被写乱了,因为线程 A 和 B 共享了堆栈空间。 这时候该怎么办?Linux 内核为这种情形预留了方案:可以通过预先分配线程栈空间,然后将分配的栈空间地址和大小传递给 clone() 系统调用 来解决。来看具体细节,首先 glibcpthread 在创建线程时,用 mmap() 调用预分配一段进程虚拟地址空间,作为新线程的栈空间:

pthread_create()
	/* 用 mmap() 从进程地址空间,划分一段虚拟地址空间,用作新线程栈空间 */
	allocate_stack (iattr, &pd, &stackaddr, &stacksize)
		mem = __mmap (NULL, size, (guardsize == 0) ? prot : PROT_NONE,
				MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
		...
		pd->stackblock = mem;
		...
		*stacksize = stacktop - pd->stackblock;
		*stack = pd->stackblock;
	/* 将栈空间范围传递给 clone() 系统调用 */
	create_thread (pd, iattr, &stopped_start, stackaddr, stacksize, &thread_ran);
		const int clone_flags = (CLONE_VM | CLONE_SETTLS | ...);
		...
		struct clone_args args =
		{
			...
			/* 指定新线程的栈空间虚拟地址范围 */
			.stack = (uintptr_t) stackaddr,
			.stack_size = stacksize,
			...
		};
		__clone_internal (&args, &start_thread, pd); /* 调用系统调用 clone() */

接着看系统调用 clone() 是怎么处理线程栈空间的:

/* kernel/fork.c */

sys_clone()
	_do_fork()
		copy_process()
			copy_mm()
			copy_thread_tls(clone_flags, stack_start, stack_size, p, tls)
				copy_thread(clone_flags, sp, arg, p)
/* arch/arm/kernel/process.c */

int
copy_thread(unsigned long clone_flags, unsigned long stack_start,
	unsigned long stk_sz, struct task_struct *p)
{
	struct thread_info *thread = task_thread_info(p);
	struct pt_regs *childregs = task_pt_regs(p);

	memset(&thread->cpu_context, 0, sizeof(struct cpu_context_save));
	...

	if (likely(!(p->flags & PF_KTHREAD))) { /* 非内核线程 */
		*childregs = *current_pt_regs(); /* 复制父进程的寄存器 */
		childregs->ARM_r0 = 0; /* 子进程 fork()/vfork()/clone() 返回值为 0 */
		if (stack_start) /* 有线程独立的栈空间吗 */
			childregs->ARM_sp = stack_start; /* 设置线程独立的栈空间: sp 指针指向该栈空间 */
	} else {
		...
	}

	/*
	 * 子进程从 fork()/vfork()/clone() 系列系统调用返回到 ret_from_fork,
	 * 然后从 ret_from_fork 返回用户空间。如:
	 * sys_fork() -> ... -> wake_up_new_task() -> ... -> ret_from_fork -> 用户空间
	 * 进入内核空间时,会将用户空间寄存器保存到内核栈上;从内核返回用户空间时,会进行
	 * 对应的出栈操作,所以这里的 sp 指向将要出栈的寄存器空间。
	 */
	thread->cpu_context.pc = (unsigned long)ret_from_fork;
	thread->cpu_context.sp = (unsigned long)childregs;

	...

	return 0;
}

更多关于系统调用的实现细节,可参考 Linux系统调用实现简析 。

4.3.4 其它情形的栈配置

还没说到的情形就只有 vfork() 了,虽然 vfork() 父子进程共享地址空间,但由于父进程在子进程退出之前都不会运行,所以情形就简单了,反正父子进程不存在冲突访问。更多关于 vfork() 的细节可参考 Linux: vfork() 程序异常退出问题分析 。

5. 观察和调整进程栈空间

5.1 观察进程栈空间

可以通过命令 ulimit -s 来观察系统中全局默认的进程栈大小,同样也可以通过该命令配置系统中全局默认的进程栈大小。进程自身的资源限制状况(包括堆栈)也可以通过 /proc/<PID>/limits 进行观察。getrlimit(), pctrl() 接口可以用来获取系统资源的配置状况。

5.2 调整进程栈空间

setrlimit() 可以用来配置系统全局资源限制,进而可以影响进程栈空间大小,pctrl(PR_SET_MM) 也是类似的接口。

6. 参考资料

《ARM Architecture Reference Manual.pdf》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/554964.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实现快速多点触控,让App自动化测试操作更方便

目录 前言&#xff1a; PyAutoGUI简介&#xff1a; 代码示例&#xff1a; 总结&#xff1a; 前言&#xff1a; 随着智能设备的普及&#xff0c;触摸点的数量和触摸操作的复杂度也在不断增加。要想在触控界面上获得更高效率和更好的体验&#xff0c;多点触控操作是必不可少的…

历经70+场面试,我发现了大厂面试的套路都是···

今年的金三银四刚刚过去&#xff0c;我又想起了我在去年春招时面试了50余家&#xff0c;加上暑期实习面试了20余家&#xff0c;加起来也面试了70余场的面试场景了。 基本把国内有名的互联网公司都面了一遍&#xff0c;不敢说自己的面试经验很丰富&#xff0c;但也是不差的。 …

【JAVAEE】认识网络及网络通信

目录 1.网络发展史 1.1独立模式 1.2网络互连 1.2.1局域网 1.2.2广域网 2.网络通信基础 2.1IP地址 2.2端口号 2.3协议 2.4五元组 2.5协议分层 2.5.1什么是协议分层 2.5.2协议分层的作用 2.5.3TCP/IP五层&#xff08;或四层&#xff09;模型 3.封装和分用 1.网络发…

调用百度API自动生成春联

目录 1、作者介绍2、百度智能春联介绍录2.1 功能介绍2.2 技术特色 3、智能春联API接口介绍3.1 请求参数3.2 返回参数 4. 操作流程5. 代码实现 1、作者介绍 范宇帅&#xff0c;男&#xff0c;西安工程大学电子信息学院&#xff0c;2022级研究生 研究方向&#xff1a;多机器人协…

〖C++11〗线程库详解

「前言」文章是关于C11线程库相关的 「归属专栏」C嘎嘎 「笔者」枫叶先生(fy) 「座右铭」前行路上修真我 「枫叶先生有点文青病」 「每篇一句」 请不要把陌生人的些许善意&#xff0c; 视为珍稀的瑰宝&#xff0c; 却把身边亲近人的全部付出&#xff0c; 当做天经地义的事情&am…

比赛记录:Codeforces Round 874 (Div. 3) A~G

传送门:CF 前题提要:赛时A出了5道题,并且都是一遍过的,F题也已经找到了解决方法,但是没时间完成了.以为应该能上分,但是没想到赛后E题被hack掉了…绝了.然后打完这场 d i v 3 div3 div3后立马阳了,加上一大堆烦心事(包括但不限于各类考试).就导致现在才写出这篇题解. A题:A. …

移动机器人 | 火星探矿机器人

01、需求分析 “火星探矿机器人”旨在要开发若干个自主机器人&#xff0c;将其送到火星上去搜寻和采集火星上的矿产资源。 火星环境对于开发者和自主机器人而言事先不可知&#xff0c;但是可以想象火星表面会有多样化的地形情况&#xff0c;如河流、巨石、凹坑等&#xff0c;机…

公司从字节招来一个28K的测试工程师,让我见识到了什么才是真正的测试天花板

今天上班开早会就是新人见面仪式&#xff0c;听说来了个很厉害的大佬&#xff0c;年纪还不大&#xff0c;是上家公司离职过来的&#xff0c;薪资已经达到中高等水平&#xff0c;很多人都好奇不已&#xff0c;能拿到这个薪资应该人不简单&#xff0c;果然&#xff0c;自我介绍的…

队列——“数据结构与算法”

各位CSDN的uu们你们好呀&#xff0c;又好久不见啦&#xff0c;最近有点摆烂&#xff0c;甚是惭愧&#xff01;&#xff01;&#xff01;&#xff01;今天&#xff0c;小雅兰的内容是队列&#xff0c;下面&#xff0c;让我们进入队列的世界吧&#xff01;&#xff01;&#xff0…

Trigger +Pipeline 完整实战案例

2.4.1 案例环境说明 示例项目&#xff1a;http://code.icloud2native.com/root/spring-boot-helloWorld.git 触发机制: 用户推送代码至项目仓库由Push Hook 自东触发pipeline的流水线的执行 2.4.2 项目实现 1、在k8s上部署一个gitlab&#xff0c;前面上节已经完成。 2、运…

海睿思分享 | 类chatgpt模型在信息抽取领域的应用

大语言模型&#xff08;LLM&#xff0c;Large Language Model&#xff09;是指能够处理海量数据、拥有百亿级参数的深度学习模型&#xff0c;它已成为⼈⼯智能领域中的新热点。2022 年 11 ⽉ 30 号 ChatGPT 发布&#xff0c;其卓越的性能表现给整个⾏业带来了巨⼤的冲击。⼈们不…

2023年春季期末网球理论复习资料

&#xff08;含2023/2022/2021时事题&#xff0c;基于2012年期末网球理论复习资料修改&#xff09; 目录 网球的起源 网球的主要赛事 三大网球协会 大满贯 网球的场地 1. 球场线 2. 网球的球网 3.场地的类型 网球的规则 1.发球规则 2.计分方法 3.通则 4.赛…

在Octane中提升渲染速度的技巧(第1部分)

Mike Griggs是一位数字内容创建者&#xff0c;在为众多客户创建Mograph&#xff0c;VFX和CGI方面拥有超过二十年的经验。迈克格里格斯&#xff08;Mike Griggs&#xff09;在Creative Bloq上写了很多博客&#xff0c;该博客是国际媒体集团和领先的数字出版商Future plc的一部分…

手势识别q

本文介绍使用光电传感器的手势识别。 光电传感器手势识别区别于视觉手势识别&#xff0c;没有复杂的算法。LED发射光&#xff0c;当光线接触到手发生反射&#xff0c;反射光被传感器检测到&#xff0c;传感器检测到不同的手势反射的光不同&#xff0c;再根据芯片的内置算法判别…

【笔试强训编程题】Day5.( 统计回文 45842 ) 和( 连续最大和 58539)

作者简介&#xff1a;大家好&#xff0c;我是未央&#xff1b; 博客首页&#xff1a;未央.303 系列专栏&#xff1a;笔试强训编程题 每日一句&#xff1a;人的一生&#xff0c;可以有所作为的时机只有一次&#xff0c;那就是现在&#xff01;&#xff01;&#xff01;! 文章目录…

【嵌入式烧录/刷写文件】-2.5-Fill填充Intel Hex文件

案例背景(共8页精讲)&#xff1a;该篇将告诉你&#xff0c;如何对一个Hex文件进行填充&#xff1a; 对“起始地址”和“结束地址”内的非连续的Block块&#xff0c;进行填充&#xff1b;自定义填充范围。 目录 1 为什么要“Fill填充” 2 使用Vector HexView工具“填充”Hex…

【Linux0.11代码分析】09 之 ELF可执行程序02 - Section Headers解析

【Linux0.11代码分析】09 之 ELF可执行程序02 - Section Headers解析 一、ELF概述二、ELF的组成结构2.1 ELF header&#xff1a;解析出 section headers 含31个section节和 program headers 含13个segment段2.2 Section Headers&#xff1a;获取当前程序的31个section节区信息2…

18-03 MySQL高可用方案与选择

主从复制 读写分离 流程 原理 bin log STATEMENT 优点&#xff1a;记录的是执行的SQL,比较省空间&#xff0c;降低了主从复制时的IO开销缺点&#xff1a;由于记录的是SQL&#xff0c;所以MySQL多个节点之间复制的时候&#xff0c;特定场景下会导致数据不一致的情况 ROW 优点…

【多线程进阶二】JUC工具类 线程安全的集合类 死锁

目录 一、JUC工具类 &#x1f345;1、Callable接口 &#x1f345;2、ReentrantLock &#x1f345;3、原子类 &#x1f345;4、Semaphore信号量 &#x1f345;5、CountDownLatch 二、线程安全的集合类 ​&#x1f345;1、多线程环境下&#xff0c;怎么使用线程安全…

第十三届蓝桥杯国赛JavaB组题解

A. 重合次数 思路&#xff1a; 枚举不同的时刻&#xff0c;判断哪些时刻秒针和分针表示的数字是相同的。这道题坑就坑在:xx:59:59 xx:00:00分针和时。也就是说一个小时会重叠两次。 题目要求是分钟和秒钟的重叠次数,故时钟,分钟,秒钟同时重叠的次数不算(这题还是有点咬文嚼字了…