结合多种系统api来理解堆栈的概念

数据结构中栈具有后进先出的特点,我们提到堆和栈空间的时候,指的是数据在内存中的概念,对栈空间,基本的认知包括:

1、栈空间通常用来存放临时变量、参数,寄存器等数据;

2、栈空间不能被多个函数共享,只能使用堆内存进行不同函数间的数据共享;

3、栈空间有限,所以编码规范中通常定义函数的形参建议不超过6个,多了建议使用数据结构;

      临时变量也不能想创建多少就创建多少,特别是嵌入式设备中;


最近几次开发的过程中,就碰到因为在函数中定义了超过任务栈大小的临时数组变量,导致嵌入式设备复位的情况,所以,对嵌入式程序员来说,对栈空间的理解和应用要做到熟练和有技巧,问题定位的时候也能得心应手;


栈空间由谁来指定?一种说法是栈空间归属线程,线程创建时会指定栈空间大小,所以必须关注临时变量的使用不能超过线程创建时指定的栈空间;

所以特地找了下pthread创建的线程api,通常填NULL的pthread_attr_t指针中,确实有栈空间的设置,通常默认为0,表示栈会按需进行增长,

#include <pthread.h>

int pthread_create(pthread_t * thread, pthread_attr_t * attr, void * (*start_routine)(void *), void * arg);

见:https://www.sourceware.org/pthreads-win32/manual/pthread_create.html


FreeRTOS不同于裸机每个TASK都有一个任务栈。FreeRTOS的任务栈是在任务创建的时候从FreeRTOSConfig.h 定义的Heap 空间中申请:


#define configTOTAL_HEAP_SIZE                   ((size_t)1024 * 9) 

具体任务栈创建的格式如下:(任务栈的大小是 usStackDepth*4)

呱牛笔记


FreeRTOS 中每个任务都需要自己的栈空间,栈空间的大小需要考虑如下几个方面:

函数的嵌套调用;

函数局部变量;

函数形参,一般情况下函数的形参是直接使用的 CPU 寄存器,不需要使用栈空间,但是这个函数中如果还嵌套了一个函数的话,这个存储了函数形参的 CPU 寄存器内容是要入栈的;

函数返回地址,arm中一般函数的返回地址是专门保存到 LR(LinkRegister)寄存器中的,如果这个函数里面还调用了一个函数的话,这个存储了函数返回地址的 LR 寄存器内容是要入栈的;

函数内部的状态保存操作也需要额外的栈空间;

任务切换,任务切换时所有的寄存器都需要入栈;


ARM 在任务执行过程中,如果发生中断:

M3 内核的 MCU 有 8 个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套都是用的系统栈;

M4 内核的 MCU 有 8 个通用寄存器和 18 个浮点寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套都是用的系统栈。


参考:https://blog.csdn.net/shenjin_s/article/details/103086744



C内存模型

BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)

(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

呱牛笔记

 

函数调用过程中使用栈的数据结构,能很高效的完成函数的进入和退出操作;

一、对于通用寄存器传参的冲突,我们可以再调用子函数前,将通用寄存器临时压入栈中;在子函数调用完毕后,在将已保存的寄存器再弹出恢复回来;

二、而局部变量的空间申请,也只需要向下移动下栈顶指针;将栈顶指针向回移动,即可就可完成局部变量的空间释放;

三、对于函数的返回,也只需要在调用子函数前,将返回地址压入栈中,待子函数调用结束后,将函数返回地址弹出给 PC 指针,即完成了函数调用的返回;


Linux 中有几种栈?各种栈的内存位置?

内核将栈分成四种:

进程栈:用于多进程间的调度和数据恢复;当调度程序需要唤醒”进程”的时候,必然需要恢复进程的上下文环境,也就是进程栈

线程栈:对于 Linux 进程或者说主线程,其 stack 是在 fork 的时候生成的,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝 (cow) 以及动态增长。

内核栈:在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈。

中断栈:进程陷入内核态的时候,需要内核栈来支持内核函数调用。


进程栈是属于用户态栈,和进程 虚拟地址空间 (Virtual Address Space) 密切相关。那我们先了解下什么是虚拟地址空间:在 32 位机器下,虚拟地址空间大小为 4G。这些虚拟地址通过页表 (Page Table) 映射到物理内存,页表由操作系统维护,并被处理器的内存管理单元 (MMU) 硬件引用。每个进程都拥有一套属于它自己的页表,因此对于每个进程而言都好像独享了整个虚拟地址空间。


Linux 对进程地址空间有个标准布局,地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下: 

- 程序段 (Text Segment):可执行文件代码的内存映射 

- 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射 

- BSS段 (BSS Segment):未初始化的全局变量或者静态变量(用零页初始化) 

- 堆区 (Heap) : 存储动态内存分配,匿名的内存映射 

- 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等 

- 映射段(Memory Mapping Segment):任何内存映射文件


栈的大小是有上限的,一般默认为4kb,这个4kb会写到PE文件格式里,操作系统在加载时通过PE文件确定此程序的栈最大大小是多少,并记录到PCB进程控制块stack_max变量里,PCB进程控制块里有一个stack_sp记录栈顶地址,也就是栈的顶部地址,当你的栈顶部大小,超出stack_max时会被操作系统自动杀掉,这也叫栈边界越界,一般发生在递归函数里!




参考:https://zhuanlan.zhihu.com/p/188577062

https://www.sourceware.org/pthreads-win32/manual/pthread_attr_setstacksize.html


Java 内存模型参考:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf


本文为呱牛笔记原创文章,转载无需和我联系,但请注明来自呱牛笔记 ,it3q.com

请先登录后发表评论
  • 最新评论
  • 总共0条评论