STM32 RT-Thread 系统分析(3)-线程管理之线程切换(系统移植基础篇一)

前言

基本信息

名称描述说明
RT-Thread Studio 软件版本版本: 1.1.3
RT-Thread 系统版本4.0.2
STM32CubeIDE 软件版本1.4.0
STM32芯片型号STM32F013VG

前言说明

线程管理是RTOS系统的关键功能,在实时操作系统分时操作系统中的差异非常大。这也是做RT-thread系统移植的基础,其中系统移植必须要实现的功能就是线程切换功能,RT-thread作为多线程系统,大部分应用开发的功能都集中在线程上运行,线程切换是线程管理的基础功能,这是一个重要的知识点。下面的全部内容是按照CPU架构移植相关的函数的顺序进行讲解的。
第二篇文章地址: STM32 RT-Thread 系统分析(3)-线程管理之线程切换(系统移植基础篇二).

CPU 架构移植

在嵌入式领域有多种不同 CPU 架构,例如 Cortex-M、ARM920T、MIPS32、RISC-V 等等。为了使 RT-Thread 能够在不同 CPU 架构的芯片上运行,RT-Thread 提供了一个 libcpu 抽象层来适配不同的 CPU 架构。libcpu 层向上对内核提供统一的接口,包括全局中断的开关,线程栈的初始化,上下文切换等。

RT-Thread 的 libcpu 抽象层向下提供了一套统一的 CPU 架构移植接口,这部分接口包含了全局中断开关函数、线程上下文切换函数、时钟节拍的配置和中断函数、Cache 等等内容。下表是 CPU 架构移植需要实现的接口和变量。

函数和变量描述
rt_base_t rt_hw_interrupt_disable(void);关闭全局中断
void rt_hw_interrupt_enable(rt_base_t level);打开全局中断
rt_uint8_t *rt_hw_stack_init(void *tentry, void *parameter, rt_uint8_t *stack_addr, void *texit);线程栈的初始化,内核在线程创建和线程初始化里面会调用这个函数
void rt_hw_context_switch_to(rt_uint32 to);没有来源线程的上下文切换,在调度器启动第一个线程的时候调用,以及在 signal 里面会调用
void rt_hw_context_switch(rt_uint32 from, rt_uint32 to);从 from 线程切换到 to 线程,用于线程和线程之间的切换
void rt_hw_context_switch_interrupt(rt_uint32 from, rt_uint32 to);从 from 线程切换到 to 线程,用于中断里面进行切换的时候使用
rt_uint32_t rt_thread_switch_interrupt_flag;表示需要在中断里进行切换的标志
rt_uint32_t rt_interrupt_from_thread, rt_interrupt_to_thread;在线程进行上下文切换时候,用来保存 from 和 to 线程

上面是libcpu 移植相关 API,下面我们根据各个函数实际的代码一步一步进行分析和理解
新建一个工程后可以看到,代码位置和名称如下面截图所示 context_gcc.s,这部分代码就是与CPU移植相关的部分代码:
在这里插入图片描述

rt_hw_interrupt_disable 关闭全局中断

代码原文:

/*
 * rt_base_t rt_hw_interrupt_disable();关闭全局中断
 */
    .global rt_hw_interrupt_disable /*声明一个全局可调用变量*/
    .type rt_hw_interrupt_disable, %function  /*将 rt_hw_interrupt_disable 设置为函数类型*/
rt_hw_interrupt_disable:
    MRS     R0, PRIMASK /*读取中断屏蔽寄存器的值到函数返回值寄存器,用于保存当前没关闭中断前的中断屏蔽寄存器状态*/
    CPSID   I		/*CPSID I ;PRIMASK=1, ;关中断*/
    BX      LR    /*函数返回*/

知识点

中断屏蔽寄存器组

中断屏蔽寄存器组:PRIMASK, FAULTMASK 和 BASEPRI这三个寄存器用于控制异常的使能和除能。

名字功能描述
PRIMASK这是个只有 1 个位的寄存器。当它置 1 时, 就关掉所有可屏蔽的异常,只剩下 NMI和硬 fault 可以响应。它的缺省值是 0,表示没有关中断。
FAULTMASK这是个只有 1 个位的寄存器。当它置 1 时,只有 NMI 才能响应,所有其它的异常,包括中断和 fault,通通闭嘴。它的缺省值也是 0,表示没有关异常。
BASEPRI这个寄存器最多有 9 位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。但若被设成 0,则不关闭任何中断, 0 也是缺省值。

CPSID指令 :除能 PRIMASK(CPSID i)/ FAULTMASK(CPSID f)——置位相应的位

汇编指令B、BL、BX、BLX 和 BXJ

语法

op1{cond}{.W} <wbr />label     
op2{cond} <wbr />Rm
指令说明
B跳转。
BL带链接跳转
BLX带链接跳转并切换指令集。
BX跳转并切换指令集。
BLX带链接跳转并切换指令集。
BXJ跳转并转换为 Jazelle 执行。
cond是一个可选的条件代码。 cond 不能用于此指令的所有形式。
.W是一个可选的指令宽度说明符,用于强制要求在 Thumb-2 中使用 32 位 B 指令。
label是一个程序相对的表达式。
Rm是一个寄存器,包含要跳转到的目标地址。

所有这些指令均会引发跳转,或跳转到 label,或跳转到包含在 Rm 中的地址处。 此外:

  • BL 和 BLX 指令可将下一个指令的地址复制到 lr(r14,链接寄存器)中。
  • BX 和 BLX 指令可将处理器的状态从 ARM 更改为 Thumb,或从 Thumb 更改为 ARM
  • BLX label 无论何种情况,始终会更改处理器的状态。
  • BX Rm 和 BLX Rm 可从 Rm 的位 [0] 推算出目标状态:

如果 Rm 的位 [0] 为 0,则处理器的状态会更改为(或保持在)ARM 状态
如果 Rm 的位 [0] 为 1,则处理器的状态会更改为(或保持在)Thumb 状态。
BXJ 指令会将处理器的状态更改为 Jazelle

连接寄存器 R14( LR)

R14 是连接寄存器( LR)。在一个汇编程序中,你可以把它写作 both LR 和 R14。 LR 用于在调用子程序时存储返回地址。例如,当你在使用 BL(分支并连接, Branch and Link)指令时,就自动填充 LR 的值。

main ;主程序
…
BL function1 ; 使用“分支并连接”指令呼叫 function1
; PC= function1,并且 LR=main 的下一条指令地址
…
Function1
… ; function1 的代码
BX LR ; 函数返回(如果 function1 要使用 LR,必须在使用前 PUSH,
; 否则返回时程序就可能跑飞了)

尽管 PC 的 LSB 总是 0(因为代码至少是字对齐的), LR 的 LSB 却是可读可写的。这是历史遗留的产物。在以前,由位 0 来指示 ARM/Thumb 状态。因为其它有些 ARM 处理器支持ARM 和 Thumb 状态并存,为了方便汇编程序移植, CM3 需要允许 LSB 可读可写。

rt_hw_interrupt_enable 打开全局中断

代码原文:

/*
 * void rt_hw_interrupt_enable(rt_base_t level);打开全局中断
 */
    .global rt_hw_interrupt_enable /*声明一个全局可调用变量*/
    .type rt_hw_interrupt_enable, %function /*将 rt_hw_interrupt_enable 设置为函数类型*/
rt_hw_interrupt_enable:
    MSR     PRIMASK, R0   /*读取r0寄存器的值复制给PRIMASK*/
    BX      LR           /*函数返回*/

知识点

MSR 指令

特殊功能寄存器组
Cortex‐M3 中的特殊功能寄存器包括:

  • 程序状态寄存器组( PSRs 或曰 xPSR)
  • 中断屏蔽寄存器组( PRIMASK, FAULTMASK,以及 BASEPRI)
  • 控制寄存器( CONTROL)
    它们只能被专用的 MSR 和 MRS 指令访问,而且它们也没有存储器地址。
    MRS <gp_reg>, <special_reg> ;读特殊功能寄存器的值到通用寄存器
    MSR <special_reg>, <gp_reg> ;写通用寄存器的值到特殊功能寄存器

*rt_hw_stack_init 线程栈的初始化

在动态创建线程和初始化线程的时候,会使用到内部的线程初始化函数_rt_thread_init(),_rt_thread_init() 函数会调用栈初始化函数 rt_hw_stack_init()在栈初始化函数里会手动构造一个上下文内容,这个上下文内容将被作为每个线程第一次执行的初始值上下文在栈里的排布如下图所示
在这里插入图片描述
根据函数嵌套的顺序:rt_thread_init → _rt_thread_init→*rt_hw_stack_init

函数:rt_thread_init 代码内容

rt_err_t rt_thread_init(struct rt_thread *thread,
                        const char       *name,
                        void (*entry)(void *parameter),
                        void             *parameter,
                        void             *stack_start,
                        rt_uint32_t       stack_size,
                        rt_uint8_t        priority,
                        rt_uint32_t       tick)
{
    /* thread check */
    RT_ASSERT(thread != RT_NULL); //检查(thread != RT_NULL) 是否为假如果是假的就报错
    RT_ASSERT(stack_start != RT_NULL);//检查(stack_start != RT_NULL) 是否为假如果是假的就报错

    /* initialize thread object 按照线程类型初始化线程函数对象*/
    rt_object_init((rt_object_t)thread, RT_Object_Class_Thread, name);

    return _rt_thread_init(thread,
                           name,
                           entry,
                           parameter,
                           stack_start,
                           stack_size,
                           priority,
                           tick);
}

可以看到 rt_thread_init函数的输入参数是直接传递到_rt_thread_init函数中。
静态线程的线程句柄(或者说线程控制块指针)、线程栈由用户提供。静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。需要注意的是,用户提供的栈首地址需做系统对齐(例如 ARM 上需要做 4 字节对齐)
示例:

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 2 入口 */
static void thread2_entry(void *param)
{

}
    /* 初始化线程 2,名称是 thread2,入口是 thread2_entry */
    rt_thread_init(&thread2,
                   "thread2",
                   thread2_entry,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack),
                   THREAD_PRIORITY - 1, THREAD_TIMESLICE);
    rt_thread_startup(&thread2);

关键点说明:

void (*entry)(void *parameter)
*entry 是函数指针,
*parameter是参数列表指针,根据 ARM APCS 调用标准,将第一个参数保存在 r0 寄存器

线程初始化接口 rt_thread_init() 的参数和返回值见下表:

参数描述
thread线程句柄。线程句柄由用户提供出来,并指向对应的线程控制块内存地址
name线程的名称;线程名称的最大长度由 rtconfig.h 中定义的 RT_NAME_MAX 宏指定,多余部分会被自动截掉
entry线程入口函数
parameter线程入口函数参数
stack_start线程栈起始地址
stack_size线程栈大小,单位是字节。在大多数系统中需要做栈空间地址对齐(例如 ARM 体系结构中需要向 4 字节地址对齐)
priority线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0 ~ 255,数值越小优先级越高,0 代表最高优先级
tick线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
返回——
RT_EOK线程创建成功
-RT_ERROR线程创建失败

函数:_rt_thread_init 代码内容

static rt_err_t _rt_thread_init(struct rt_thread *thread,
                                const char       *name,
                                void (*entry)(void *parameter),
                                void             *parameter,
                                void             *stack_start,
                                rt_uint32_t       stack_size,
                                rt_uint8_t        priority,
                                rt_uint32_t       tick)
{
    /* init thread list */
    rt_list_init(&(thread->tlist));

    thread->entry = (void *)entry;
    thread->parameter = parameter;

    /* stack init */
    thread->stack_addr = stack_start;
    thread->stack_size = stack_size;

    /* init thread stack 初始化线程栈*/
    rt_memset(thread->stack_addr, '#', thread->stack_size);
#ifdef ARCH_CPU_STACK_GROWS_UPWARD //栈地址由低向高增长
    thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
                                          (void *)((char *)thread->stack_addr),
                                          (void *)rt_thread_exit);
#else
    thread->sp = (void *)rt_hw_stack_init(thread->entry, thread->parameter,
                                          (rt_uint8_t *)((char *)thread->stack_addr + thread->stack_size - sizeof(rt_ubase_t)),
                                          (void *)rt_thread_exit);
 /*********后面的省略暂不分析**********/

可以看到这个宏ARCH_CPU_STACK_GROWS_UPWARD 为栈地址由低向高增长的意思,而context-M3正好与之相反,由高向低生长。具体可以查看下面的知识点:向下生长的满栈模型
*rt_hw_stack_init函数是用来获取当前线程的栈顶指针地址的。
关键点:
_rt_thread_init函数 *rt_hw_stack_init 函数 输入的参数为

参数说明
thread->entry由thread->entry = (void *)entry;这个代码可以看出输入的是指向线程运行函数的起始地址的指针
thread->parameter由thread->parameter = parameter;可以看出,这是个输入线程函数的参数列表指针
(rt_uint8_t *)((char *)thread->stack_addr + thread->stack_size - sizeof(rt_ubase_t))这个是线程栈的栈顶地址值。栈指针SP指向最后一个被压入栈的32位数值 。因此栈顶地址值为:(char *)thread->stack_addr(栈起始地址) + thread->stack_size(栈占用空间大小) -sizeof(rt_ubase_t)(4个字节占用空间大小)
rt_thread_exit执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。(所有线程均使用同一个 rt_thread_exit() 函数)
关键点:栈地址计算:

在这个函数最关键和难理解的就是栈顶地址计算和传递。举例来说,声明一个线程的栈为

static rt_uint8_t timer_thread_stack[512];//示例
 /* start software timer thread */
 rt_thread_init(&timer_thread,
                "timer",
                rt_thread_timer_entry,
                RT_NULL,
                &timer_thread_stack[0],
                sizeof(timer_thread_stack),
                RT_TIMER_THREAD_PRIO,
                10);

因此timer_thread线程的传递的 **&timer_thread_stack[0]**地址值为:0x20000d48(32位地址值),传递到_rt_thread_init 函数仍旧为void *指针类型传递
注意: 传递给*rt_hw_stack_init 函数 之前对stack_start指向的地址的值取了出来进行了计算,并强制转换为rt_uint8_t *指针类型(原来是void *类型)。此时,在*rt_hw_stack_init 函数 中,stack_addr输入参数的指向地址值由原来的0x20000d48变为:0x20000f44=0x20000d48(十六进制)+512(十进制)-4(十进制)。

  • sizeof(rt_ubase_t) 中的rt_ubase_t表示typedef unsigned long 在ARM中是32位占用4字节。
  • 传递到*rt_hw_stack_init 函数之前对栈地址进行地址值计算和指针类型转换。
  • 强制转换为rt_uint8_t *指针类型的原因是因为timer_thread_stack数组的数据类型为rt_uint8_t类型 。后续的指针地址值的加减操作,均是表示指向timer_thread_stack数组中元素的指针的地址值的操作,改变了指针指向的地址值,就相当于指针指向timer_thread_stack数组中的元素位置变了。例如指针原来的地址值:0x20000d48(此时指向数组最后一个元素),将其值减去64变为0x20000d08(此时指向数组倒数第64个元素),因为指针类型是rt_uint8_t类型,减去64则相当于向后走了64个字节。

函数:*rt_hw_stack_init 代码内容

下代码是栈初始化的代码:
在栈里构建上下文

rt_uint8_t *rt_hw_stack_init(void       *tentry,
                             void       *parameter,
                             rt_uint8_t *stack_addr,
                             void       *texit)
{
    struct stack_frame *stack_frame;
    rt_uint8_t         *stk;
    unsigned long       i;

    /* 对传入的栈指针做对齐处理 ,堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的*/
    stk  = stack_addr + sizeof(rt_uint32_t);
    stk  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
    stk -= sizeof(struct stack_frame);

    /* 得到上下文的栈帧的指针 */
    stack_frame = (struct stack_frame *)stk;

    /* init all register 把所有寄存器的默认值设置为 0xdeadbeef*/
    for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
    {
        ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
    }
    /* 根据 ARM  APCS 调用标准,将第一个参数保存在 r0 寄存器 */
    stack_frame->exception_stack_frame.r0  = (unsigned long)parameter; /* r0 : argument */
    /* 将剩下的参数寄存器都设置为 0 */
    stack_frame->exception_stack_frame.r1  = 0;                        /* r1 */
    stack_frame->exception_stack_frame.r2  = 0;                        /* r2 */
    stack_frame->exception_stack_frame.r3  = 0;                        /* r3 */
    /* 将 IP(Intra-Procedure-call scratch register.) 设置为 0 */
    stack_frame->exception_stack_frame.r12 = 0;                        /* r12 */
    /* 将线程退出函数的地址保存在 lr 寄存器 */
    stack_frame->exception_stack_frame.lr  = (unsigned long)texit;     /* lr */
    /* 将线程入口函数的地址保存在 pc 寄存器 */
    stack_frame->exception_stack_frame.pc  = (unsigned long)tentry;    /* entry point, pc */
    /* 设置 psr 的值为 0x01000000L,表示默认切换过去是 Thumb 模式 */
    stack_frame->exception_stack_frame.psr = 0x01000000L;              /* PSR */

    /* return task's current stack address  返回当前线程的栈地址   */
    return stk;
}
*rt_hw_stack_init 输入参数说明
void *tentry函数执行起始地址
void *parameter线程入口函数参数
rt_uint8_t *stack_addr栈地址
void *texit(void *)rt_thread_exit任务的执行函数使用任务栈,任务栈动态增减。当执行函数返回,任务栈弹栈,作为 lr 的 rt_thread_exit 的地址恢复到 PC 中,程序继续执行此函数,完成任务的正常退出。
第一段代码分析:
    struct stack_frame *stack_frame;
    rt_uint8_t         *stk; 
    unsigned long       i;
    /* 对传入的栈指针做对齐处理 ,堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的 */
    stk  = stack_addr + sizeof(rt_uint32_t);
    stk  = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);
    stk -= sizeof(struct stack_frame);

分析:
指针stk最终会返回,赋值给thread->sp指针指针stk的数据类型为rt_uint8_t 因此计算均为1个字节的偏差进行计算。

  • stk = stack_addr + sizeof(rt_uint32_t); 表示:stk指向地址值=输入的栈地址值+4个字节。
  • stk = (rt_uint8_t *)RT_ALIGN_DOWN((rt_uint32_t)stk, 8);表示:stk指向地址值按照8字节对齐
  • stk -= sizeof(struct stack_frame);表示:stk指向地址值向低地址偏移64个字节(sizeof(struct stack_frame)=64)。偏移出的空间用于stack_frame结构体内容的存放。
    下面是stack_frame的代码:
struct exception_stack_frame
{
    rt_uint32_t r0;
    rt_uint32_t r1;
    rt_uint32_t r2;
    rt_uint32_t r3;
    rt_uint32_t r12;
    rt_uint32_t lr;
    rt_uint32_t pc;
    rt_uint32_t psr;
};
struct stack_frame
{
    /* r4 ~ r11 register */
    rt_uint32_t r4;
    rt_uint32_t r5;
    rt_uint32_t r6;
    rt_uint32_t r7;
    rt_uint32_t r8;
    rt_uint32_t r9;
    rt_uint32_t r10;
    rt_uint32_t r11;
    struct exception_stack_frame exception_stack_frame;
};
第二段代码分析:
	/* 得到上下文的栈帧的指针 */
    stack_frame = (struct stack_frame *)stk;
    /* init all register 把所有寄存器的默认值设置为 0xdeadbeef*/
    for (i = 0; i < sizeof(struct stack_frame) / sizeof(rt_uint32_t); i ++)
    {
        ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;
    }

分析:

  • stack_frame = (struct stack_frame *)stk;表示:将stk指针类型由原来的rt_uint8_t类型 强制转换为 struct stack_frame类型。转换后stack_frame 结构体内的各种元素与stk指针指向的栈数组元素一一对应,操作stack_frame 结构体的元素就是对栈数组元素的操作。
  • ((rt_uint32_t *)stack_frame)[i] = 0xdeadbeef;表示:这是一个遍历的for循环,是对stack_frame指针对应的地址值对应的数组元素操作操作的数据类型为rt_uint32_t类型。当i=0时,偏移的地址值为0 ,当i=1时,偏移的地址值为1个rt_uint32_t类值的大小为4个字节。遍历范围i的值从0到16不包括16(sizeof(struct stack_frame) =64除以 sizeof(rt_uint32_t)=4)。
    后续的对stack_frame的结构体操作均是将对应数值存入线程栈数组

注意:

  • 静态线程栈数组是全局变量,是用于存放线程的出栈和入栈的数据的,它的一开始编译就确定了在内存中的位置的。
  • 动态线程栈数组是用内存申请的方式来获取空间的,它与静态线程不同的是的内存位置属于,动态线程的内存地址生长方向是向上生长的。具体请看下面关于堆栈理解的知识点。

知识点

RT_ASSERT(EX)
#define RT_ASSERT(EX)                                                         \
if (!(EX))                                                                    \
{                                                                             \
    rt_assert_handler(#EX, __FUNCTION__, __LINE__);                           \
}

EX就是一个值 当EX是1是真 那就没事儿 但是为0是假就会出事
比如serial != RT_NULL是假 的话就会rt_assert_handler(#EX, FUNCTION, LINE);

void rt_assert_handler(const char *ex_string, const char *func, rt_size_t line)
{
  rt_kprintf("(%s) assertion failed at function:%s, line number:%d \n", ex_string, func, line);
}

#EX 会string的显示serial != RT_NULL 再显示函数的行号!!!

ARM微处理器支持的四种类型堆栈工作方式

说明:堆栈严格来说应该叫做栈(Stack)是限定仅在一端进行插入或删除操作的线性表。因此,对栈来说,可以进行插入或删除操作的一端端称为栈顶(top)相应地,另一端称为栈底(bottom)。不含元素的空表称为空栈。由于堆栈只允许在一端进行操作,因而按照后进先出(LIFO-LastIn First Out)的原理运作。
从栈顶的定义来看,栈顶的位置是可变的。空栈时,栈顶和栈底重合;满栈时,栈顶离栈底最远。

项目Value
Full descending 满递减堆栈堆栈首部是高地址,堆栈向低地址增长。栈指针总是指向堆栈最后一个元素(最后一个元素是最后压入的数据)。ARM-Thumb过程调用标准和ARM、Thumb C/C++ 编译器总是使用Full descending 类型堆栈。
Full ascending 满递增堆栈堆栈首部是低地址,堆栈向高地址增长。栈指针总是指向堆栈最后一个元素(最后一个元素是最后压入的数据)。
Empty descending 空递减堆栈堆栈首部是低地址,堆栈向高地址增长。栈指针总是指向下一个将要放入数据的空位置。
Empty ascending 空递增堆栈堆栈首部是高地址,堆栈向低地址增长。栈指针总是指向下一个将要放入数据的空位置。

满堆栈的关键词是最后一个已使用的地址,空堆栈是第一个没有使用的地址。
四种组合:满递增(FA)、空递增(EA)、满递减(FD)、空递减(ED)。
入栈规律:
(1)满堆栈操作先调整SP,然后存入数据。
(2)空堆栈操作先存入数据,然后调整SP。
(3)递增堆栈调整SP时,执行SP=SP+4
(4)递减堆栈调整SP时,执行SP=SP-4
出栈规律正好与入栈相反,也就是入栈的逆操作。
(1)空堆栈操作先调整SP,然后存入数据。
(2)满堆栈操作先存入数据,然后调整SP。
(3)递减堆栈调整SP时,执行SP=SP+4
(4)递增堆栈调整SP时,执行SP=SP-4

关于内存堆栈的理解

C++内存区域分为5个区域。分别是自由存储区全局/静态存储区和常量存储区

名称说明
由编译器在需要的时候分配,在不需要的时候自动清除的变量存储区。里面通常是局部变量,函数参数等。
由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区由malloc等分配的内存块,和堆十分相似,不过它使用free来结束自己的生命。
全局/静态存储区全局变量和静态变量被分配到同一块内存中,在以前的c语言中。全局变量又分为初始化的和未初始化的,在c++里面没有这个区分了,他们共同占用同一块内存。
常量存储区这是一块比较特殊的存储区,里面存放的是常量,不允许修改。

C++内存区域中堆和栈的区别:

  1. 管理方式不同:栈是由编译器自动管理,无需我们手工控制;对于堆来说,释放由程序员完成,容易产生内存泄漏。
  2. 空间大小不同:一般来讲,在32为系统下面,堆内存可达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定空间大小的,例如,在vc6下面,默认的栈大小好像是1M。当然,也可以自己修改:打开工程。 project–>setting–>link,在category中选中output,然后再reserve中设定堆栈的最大值和 commit。
  3. 能否产生碎片:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题。
  4. 生长方向不同:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方式是向下的,是向着内存地址减小的方向增长。
  5. 分配方式不同:堆都是动态分配的;栈有静态和动态两种分配方式。静态分配由编译器完成,比如局部变量的分配。动态分配由alloca函数进行、但栈的动态分配和堆是不同的,它的动态分配由编译器进行释放,无需我们手工实现。
  6. 分配效率不同:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是c/c++库函数提供的,机制很复杂。库函数会按照一定的算法进行分配。显然,堆的效率比栈要低得多。

理解关键点:

  1. 在内存中,“堆”和“栈”共用全部的自由空间,只不过各自的起始地址和增长方向不同,它们之间并没有一个固定的界限,如果在运行时,“堆”和 “栈”增长到发生了相互覆盖时,称为“栈堆冲突”,系统肯定垮台。由于开销方面的原因,各种编译在实现中都没有考虑解决这个问题,只有靠设计者自己解决,比如增加内存等。
  2. 进程内存中的映像,主要有代码区,堆(动态存储区,new/delete的动态数据),栈,静态存储区
  3. 内存区域地址从低到高的方向:代码区,静态存储区,堆,栈
  4. 堆”和“栈”是独立的概念平常说的“堆栈”实际上是两个概念:“堆”和“栈”。在英文中,堆是heap,栈是stack,不知道什么时候,什么原因,在中文里,这两个不同的概念硬是被搞在一起了,所以,围绕这个混合词所发生的误解和争执这几年就没有断过。
  5. “栈”一般是由硬件(CPU)实现的,CPU用栈来保存调用子程序(函数)时的返回地址,高级语言有时也用它作为局部变量的存储空间。
  6. “堆”是个实实在在的软件概念,使用与否完全由编程者“显示地(explicitly)”决定,如malloc。
向下生长的满栈模型(满递减堆栈)

Cortex‐M3 使用的是“向下生长的满栈”模型。堆栈指针 SP 指向最后一个被压入堆栈的 32位数值。在下一次压栈时, SP 先自减 4,再存入新的数值。

下图是入栈操作:在这里插入图片描述
下图是出栈操作:
在这里插入图片描述
虽然 POP 后被压入的数值还保存在栈中,但它已经无效了,因为为下次的 PUSH 将覆盖它的值!在进入 ISR 时, CM3 会自动把一些寄存器压栈,这里使用的是进入 ISR 之前使用的 SP指针( MSP 或者是 PSP)。离开 ISR 后,只要 ISR 没有更改过 CONTROL[1],就依然使用先前的 SP 指针来执行出栈操作。
下图是Context-M3CPU的堆栈生长图:
在这里插入图片描述
限于篇幅原因后续的分析放在第二篇中。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值