呢,另外PC 本身是不是寄存器和栈,和栈指针有关系吗

【已解决】任务堆栈是做什么用的?怎样确定其大小?-电子产品世界论坛
【已解决】任务堆栈是做什么用的?怎样确定其大小?
UCOS里边创建任务需要设置堆栈,uTenux创建任务也需要设置堆栈。
可为什么要设置这个堆栈啊?这个堆栈是干什么用的?大小怎么确定?
问了度娘,答案如下:
第一,当任务运行时,它用来保存一些局部变量;
第二,当任务挂起时,它负责保存任务的运行现场,也就是CPU寄存器的值。
uCOS任务堆栈的深入分析(转)
栈作用的就是用来保存局部变量,从本质上讲也就是将CPU寄存器的值保存到RAM
中。在uCOS中,每一个任务都有一个独立的任务堆栈。为了深入理解任务堆栈的作用,不妨分析任务从“出生”到“消亡”的整个过程,具体就是分析任务的建
立,运行,挂起几种状态中任务堆栈的变化情况。
现在假设系统运行着一个由用户创建的用以完成打印工作的任务TPrint。
TPrint最初通过OSTaskCreate()函数创建,在该函数中与任务堆栈有关的第一段代码是大家比较熟悉的函数
OSTaskStkInit(),这个函数是在uCOS移植过程中必须实现的,其作用是“初始化堆栈”,其实就是预先在RAM中的一块区域中把任务将来运
行开始时CPU寄存器应处的状态(正确值)准备好,之后,任务第一次被内核调度器调度运行时,将这些准备好的数据(寄存器的值)推到CPU的寄存器中,如
果数据设计的合理,CPU便会按照我们预先设计好的思路运行。所以,“初始化堆栈”实际上是做了一个“未雨绸缪”的工作。这个过程中有两点是必须慎重考虑
的,一是PC该如何定位,二是CPU的其它寄存器(除PC之外)该怎么处理。先说第一点,因为任务是第一次运行,而任务从本质上将就是一段代码,所以PC
指针应该定位到这段代码的第一行处,即所谓的入口地址(Entry
Point)处,这个地址由任务指针保存着,所以把该指针值赋给PC即可。第二,这段代码还未被执行过,所以代码中的变量与CPU的其它寄存器一点关系也
没有,因此R0-R12,R14可随便给值,或者不赋值也可,让这些寄存器保持原来的值,显然后者更为简单。最后再给CPSR赋值,用户可以根据实际需要
使系统运行于系统模式或管理模式。经过入栈和出栈,此时SP指向任务堆栈的最底端(就是已经定义好的任务堆栈数组的最后一个元素)。
后任务代码开始正式运行,因为CPU的寄存器是有限的,所以在运行时不可避免地要把
一些临时变量暂时保存到堆栈中。具体应保存到哪个地址呢,不用担心,SP知道(任务第一次运行时,这个地址就是任务堆栈数组的最后一个元素的地址)。任务
堆栈的大小和任务代码中临时变量的数目有关,如果这段代码临变量特别多,堆栈就应设计的大一些。
然后,TPrint任务由于某种原因将
要被挂起,所以应把任务的运行现场放到堆栈里保
护起来,TPrint任务再次运行时再把这个现场还原,任务就能从上次断点处紧接着运行。那么,这个现场是什么呢?从本质上讲,TPrint任务的运行过
程就是CPU在执行一段特定的代码,所以这个现场就是CPU的现场,也就是寄存器的值。这些寄存器的值包含了代码执行时的所有信息,包括当前运行到了这段
代码的哪个位置处(由PC值指明)。因此,把CPU的寄存器的值推入堆栈,然后记住栈顶指针的位置(SP由
OSTCBCur-&OSTCBStkPtr保存),当任务再次将要运行前,从SP指向的地址处依次把先前保存的CPU寄存器的值放到CPU的寄存
器中,任务就可以从上次中断的地方准确无误地执行。这个过程就像突然把任务冻结了,与任务有关的任何东西都不能动了,一段时间之后又把任务解冻,与它有关
的东西又变得可用,于是任务又可以活蹦乱跳地跑起来了。
从以上分析可以看出,任务堆栈至始至终伴随着任务,与之生死与共,它的作用可以
概括为两点:第一,当任务运行时,它用来保存一些局部变量;第二,当任务挂起时,它负责保存任务的运行现场,也就是CPU寄存器的值。有
些朋友正是忽视了第一点,产生了“任务堆栈大小应是固定值的疑问”。我感觉,这可能与对函数OSTaskStkInit()的理解有关,我们都称之为堆栈
初始化函数,但此处的“初始化”与我们理解的初始化不太一样,平时讲的(变量的)初始化似乎指的是将变量的所有成员都一一初始化。而此处的堆栈的初始化仅
仅是初始化了很大一个堆栈的一小部分,因为当前只有这部分是有用的,而剩余的大部分用不到,所以不用初始化,就像有些变量不用初始化一样(有默认值或随机
值)。更深入一点考虑,当任务挂起时,任务堆栈中保存任务挂起前CPU寄存器的这一连续的区域肯定在整个堆栈的最上面;当任务重新开始运行时,SP弹出寄
存器的值,这段区域变成空白的区域。而且,任务每次挂起前用来保存当前CPU寄存器这一连续区域在整个任务堆栈空间中是浮动的
百度还是很强大的。
匿名不能发帖!请先 [
Copyright (C) 《电子产品世界》杂志社 版权所有博客分类:
(1)、寄存器(Registers):
&&&& 这是速度最快的存储场所,因为寄存器位于处理器内部,这一点和其他的存储媒介都不一样。不过寄存器个数是有限的。在内存中的寄存器区域是由编译器根据需要来分配的。我们程序开发人员不能够通过代码来控制这个寄存器的分配。所以说,这第一个存储区域寄存器,我们只能够看看,而不能够对其产生任何的影响。,也没办法在程序里头感觉到寄存器的任何存在迹象。
(2)、Stack(堆栈):
&&& 位于一般的RAM中。处理器经由指针提供直接支持。当程序配置一块新的内存时,stack指针便往后移;释放内存时,指针则往前移。这种方式不仅很快,效率也高,速度仅次于寄存器。用于存放对象引用以及基本的数据类型对象,不能用于存储Java对象本身。
(3)、Heap(堆):
&&& 一种通用的内存空间,用来存放Java对象。Heap不同于stack之处在于,编译器不需知道究竟得从heap中配置多少空间,也不需知道从heap上配置的空间究竟需要存在多久。因此,自heap配置存储空间可以获得高度的弹性。每当你需要产生对象,只需在程序中使用new,那么执行的时候,便会自heap配置空间。当然,你得为这样的弹性付出代价:从heap配置空间,比从stack配置,所耗费的时间多了不少。
(4)静态存储区域与常量存储区域:
&&& 静态存储区域就是指在固定的位置存放应用程序运行时一直存在的数据,Java在内存中专门划分了一个静态存储区域来管理一些特殊的数据变量如静态的数据变量,需要明确的一点就是,Java对象是不保存在这个地方的,而只是把对象中的一些特殊元素放置这。
&&& 在Java对象中还有一类特殊的元素,我们叫做常量。由于常量的值是稳定不变的,如圆周率。为此把他们放在代码的内部是可行的。不过有些时候,在进行一些嵌入式系统开发的时候,我们往往不这么做。而是会把常量元素跟代码分开来保存。如我们会根据情况把常量的值存放在一些只读存储器中。这主要是为了一些特殊的功能考虑的。如出于版权控制的需要。如在打印机上为了保护原装耗材的版权,往往把常量跟代码分开存放
(5)非RAM存储:
&&& 有时候,有些程序运行所需要的数据我们还会放置在其他地方。如在一些系统中需要用到流对象,这个对象的数据并没有保存在上面所谈到的任何一个存储区域,这个对象直接被转为为字节流,发送到其他的主机上去了。另外有一种叫做持久化的对象,其是被存储在硬盘中的
速度:
&&& 寄存器 & 堆栈 & 堆 & 其他
浏览: 68828 次
来自: 安徽
解释的挺好 给力
官网上不去, 这里下到了, 感谢
通篇错误。我晕掉了。
光有数量每有质量啊!~
居然把大牛都引出来了,哈哈哈!兴奋啊,向您致敬!我会努力下去的 ...
(window.slotbydup=window.slotbydup || []).push({
id: '4773203',
container: s,
size: '200,200',
display: 'inlay-fix'学习ARM过程中的堆栈初始化详解【arm吧】_百度贴吧
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&签到排名:今日本吧第个签到,本吧因你更精彩,明天继续来努力!
本吧签到人数:0成为超级会员,使用一键签到本月漏签0次!成为超级会员,赠送8张补签卡连续签到:天&&累计签到:天超级会员单次开通12个月以上,赠送连续签到卡3张
关注:14,153贴子:
学习ARM过程中的堆栈初始化详解收藏
1、寄存器 R13 在 ARM 指令中常用作堆栈指针2、对于 R13 寄存器来说,它对应6个不同的物理寄存器,其中的一个是用户模式与系统模式共用,另外5个物理寄存器对应于其他5种不同的运行模式。采用以下的记号来区分不同的物理寄存器:R13_&mode&其中,mode为以下几种模式之一:usr、fiq、irq、svc、abt、und。3、 寄存器R13在ARM指令中常用作堆栈指针,但这只是一种习惯用法,用户也可使用其他的寄存器作为堆栈指针。而在Thumb指令集中,某些指令强制性的要 求使用R13作为堆栈指针。由于处理器的每种运行模式均有自己独立的物理寄存器R13,在用户应用程序的初始化部分,一般都要初始化每种模式下的R13, 使其指向该运行模式的栈空间,这样,当程序的运行进入异常模式时,可以将需要保护的寄存器放入R13所指向的堆栈,而当程序从异常模式返回时,则从对应的 堆栈中恢复,采用这种方式可以保证异常发生后程序的正常执行。4、有四种类型的堆栈:堆栈是一种数据结构,按先进后出(First In Last Out,FILO)的方式工作,使用一个称作堆栈指针的专用寄存器指示当前的操作位置,堆栈指针总是指向栈顶。当堆栈指针指向最后压入堆栈的数据时,称为满堆栈(Full Stack),而当堆栈指针指向下一个将要放入数据的空位置时,称为空堆栈(Empty Stack)。同 时,根据堆栈的生成方式,又可以分为递增堆栈(Ascending Stack)和递减堆栈(DecendingStack),当堆栈由低地址向高地址生成时,称为递增堆栈,当堆栈由高地址向低地址生成时,称为递减堆栈。 这样就有四种类型的堆栈工作方式,ARM 微处理器支持这四种类型的堆栈工作方式,即:◎ Full descending 满递减堆栈堆栈首部是高地址,堆栈向低地址增长。栈指针总是指向堆栈最后一个元素(最后一个元素是最后压入的数据)。ARM-Thumb过程调用标准和ARM、Thumb C/C++ 编译器总是使用Full descending 类型堆栈。◎ Full ascending 满递增堆栈堆栈首部是低地址,堆栈向高地址增长。栈指针总是指向堆栈最后一个元素(最后一个元素是最后压入的数据)。◎ Empty descending 空递减堆栈堆栈首部是低地址,堆栈向高地址增长。栈指针总是指向下一个将要放入数据的空位置。◎ Empty ascending 空递增堆栈堆栈首部是高地址,堆栈向低地址增长。栈指针总是指向下一个将要放入数据的空位置。5、操作堆栈的汇编指令堆栈类型
出栈指令Full descending
STMFD (STMDB)
LDMFD (LDMIA)Full ascending
STMFA (STMIB)
LDMFA (LDMDA)Empty descending
STMED (STMDA)
LDMED (LDMIB)Empty ascending
STMEA (STMIA)
LDMEA (LDMDB)例子:STMFD r13!, {r0-r5} ; Push onto a Full Descending StackLDMFD r13!, {r0-r5} ; Pop from a Full Descending Stack.
登录百度帐号程序的内存布局——函数调用栈的那点事
[注]此文是《程序员的自我修养》的总结,其中掺杂着一些个人的理解,若有不对,欢迎拍砖。
程序的内存布局
现代的应用程序都运行在一个虚拟内存空间里,在32位的里,这个内存空间拥有4GB的寻址能力。现代的应用程序可以直接使用32位的地址进行寻址,整个内存是一个统一的地址空间,用户可以使用一个32位的指针访问任意内存位置。
在进程的不同地址区间上有着不同的地位,Windows在默认情况下会将高地址的2GB空间分配给内核,而默认将高地址的1GB空间分配给内核,具体的内存布局如下图:
(1)代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指并执行。
(2)数据区:用于存储全局变量、常量。
(3)堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点。
(4)栈区:用于动态地存储函数之间的关系,以保证被调用函数在返回时恢复到母函数中继续执行。
高级语言写出的程序经过编译链接,最终会变成可执行文件。当可执行文件被装载运行后,就成了所谓的进程。
可执行文件代码段中包含的二进制级别的机器代码会被装入内存的代码区(.text);
处理器将到内存的这个区域一条一条地取出指令和操作数,并送入运算逻辑单元进行运算;
如果代码中请求开辟动态内存,则会在内存的堆区分配一块大小合适的区域返回给代码区的代码使用;
当函数调用发生时,函数的调用关系等信息会动态地保存在内存的栈区,以供处理器在执行完被调用函数的代码时,返回母函数。
如果把计算机看成一个有条不紊的工厂,我们可以得到如下类比:
* CPU是干活的工人。
* 数据区、堆区、栈区等则是用来存放原料、半成品、成品等各种东西的场所。
* 存放在代码区的指令则告诉CPU要做什么,怎么做,到哪里去领原材料,用什么工具来做,做完以后把成品放到哪个货仓去。
在经典的操作系统里,栈总是向下增长的。栈顶由esp寄存器定位。压栈操作使栈顶的地址减小,弹出操作使栈顶地址增大。
当函数调用的时候发生了什么?
int main(void)
foo(1,2,3) ;
return 0 ;
当方法main需要调用foo时,它的标准行为:
1、在main方法的调用栈中,将 foo的参数从右向左 依次push到栈中。
2、把main方法当前指令的 下一条指令地址 (即return address)push到栈中。(隐藏在call指令中)
3、使用call指令调用目标函数体foo。
请注意,以上3步都处于main的调用栈,其中ebp保存其栈底,而esp保存其栈顶。
接下来,在foo函数中:
1、push ebp: 将ebp的当前值push到栈中,即保存ebp。
2、mov ebp,esp: 将esp的值赋给ebp,则意味着进入了foo方法的调用栈。
3、[可选]sub esp, XXX: 在栈上分配XXX字节的临时空间。(抬高栈顶)(编译器根据函数中的局部变量的总大小确定临时空间的大小)
4、[可选]push XXX: 保存(push)一些寄存器的值。
【注意:push寄存器的值,这一操作,可以在分配临时空间之前,也可在其之后,《程序员的自我修养》写的是在开辟临时变量之后】
(编译器中保存的有相应的变量名对应的临时空间中的位置)
而在foo方法调用完毕后,便执行前面阶段的逆操作:
1、保存返回值: 通常将函数的返回值保存在寄存器eax中。
2、[可选]恢复(pop)一些寄存器的值。
3、mov esp,ebp: 恢复esp同时回收局部变量空间。(恢复原栈顶)
4、pop ebp: 将栈顶的值赋给ebp,即恢复main调用栈的栈底。(恢复原栈底)
5、ret: 从栈顶获得之前保留的return address,并跳转到此位置继续执行。
main方法先将foo方法所需的参数压入栈中,然后再改变ebp,进入foo方法的调用栈。
因此,如果在foo方法中需要访问那些参数,则需要根据当前ebp中的值,再向高地址偏移后进行访问&&因为高地址才是main方法的调用栈。
也就是说,地址ebp + 8存放了foo方法的第1个参数,地址ebp + 12存放了foo方法的第2个参数,以此类推。那么地址ebp + 4存放了什么呢?它存放的是return address,即foo方法返回后,需要继续执行下去的main方法指令的地址。
若需在函数中保存被调函数保存寄存器(如ESI、EDI),则编译器在保存EBP值时进行保存,或延迟保存直到局部变量空间被分配。在栈帧中并未为被调函数保存寄存器的空间指定标准的存储位置。
【注:几个相关的寄存器(关于详细的介绍,见王爽汇编)】
(1)esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。(ebp在当前栈帧内位置固定,故函数中对大部分数据的访问都基于ebp进行)
(3)eip:指令寄存器(extended instruction pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址。 可以说如果控制了EIP寄存器的内容,就控制了进程&&我们让eip指向哪里,CPU就会去执行哪里的指令。eip可被jmp、call和ret等指令隐含地改变(事实上它一直都在改变)(ret指令就是把当前栈顶保存的返回值地址 弹到eip中)
函数栈帧的大小并不固定,一般与其对应函数的局部变量多少有关。函数运行过程中,其栈帧大小也是在不停变化的。
函数的调用方和被调用方对于函数如何调用需要遵守同样的约定,函数才能被正确地调用,这样的约定称为**调用惯例**。
* 函数参数的传递顺序和方式
调用惯例要规定参数压栈的顺序:是从左至右,还是从右至左。有些调用惯例还允许使用寄存器传递参数,以提高性能。
* 栈的维护方式
(谁负责弹出形参?)
在被调函数返回时,需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方完成,也也可以由被函数完成。
* 名字修饰规则
为了链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰,不同的调用惯例有不同的名字修饰策略。
参数压栈方向
下划线+函数名
下划线+函数名@参数字节数
头两个参数放入寄存器,其它从右至左
@函数名字名@参数字节数
是CDeclaration的缩写,表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数无需要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。(典型的如printf函数)
是Standard Call的缩写,是C++的标准调用方式:所有参数从右到左依次入栈。这些堆栈中的参数由被调用的函数在返回后清除,使用的指令是 retn X,X表示参数占用的字节数,CPU在ret之后自动弹出X个字节的堆栈空间。称为自动清栈。函数在编译的时候就必须确定参数个数,并且调用者必须严格的控制参数的生成,不能多,不能少,否则返回后会出错。
几乎我们写的每一个WINDOWS API函数都是_stdcall类型的,因为不同的编译器产生栈的方式不尽相同,调用者不一定能正常的完成清除工作。如果使用_stdcall,上面的问题就解决了,函数自己解决清除工作。所以,在跨平台的调用中,我们都使用_stdcall(虽然有时是以WINAPI的样子出现)。
但当我们遇到这样的函数如printf()它的参数是可变的,不定长的,被调用者事先无法知道参数的长度,事后的清除工作也无法正常的进行,因此,这种情况我们只能使用\_cdecl。到这里我们有一个结论,如果你的程序中没有涉及可变参数,最好使用_stdcall关键字。
函数返回值传递
一般情况下,寄存器eax是传递返回值的通道,函数将返回值存储在eax中,返回后函数的调用方再读取eax。
但是eax本身只有4字节,那么大于4字节的返回值是如何传递的呢?
对于返回5~8字节数据的情况,一般采用eax和edx联合返回的方式进行的。其中eax存储返回值的低4字节,edx存储返回值的高4字节。
对于超过8字节的返回类型:
typedef struct big_thing
char buf[128] ;
big_thing return_test();
//---------------------------
int main(void)
big_thing n = return_test() ;
big_thing return_test()
b.buf[0] = 0 ;
分析这段代码:
首先,在主调函数main中,肯定有一个128字节的变量n,在被调函数return_test中,肯定有一个128字节的变量b。
那被调函数如何返回128字节的变量?直接从b拷贝到n么?你这样直接改变主调函数中变量的值,似乎不符合返回值传值的规则。
那么实际上,编译器是怎么设计大尺寸返回值传递的呢?
* main函数在其栈中的局部变量区域中额外开辟一片空间,将其一部分作为传递返回值的临时对象temp。
* 将temp对象的地址作为隐藏参数传递给return_test函数。
* return_test函数将数据拷贝给temp对象,并将temp对象的地址用eax传出。
* return_test返回后,main函数将eax指向的temp对象的内容拷贝给n。
(return_test是没有真正的参数的,只有一个&伪参数&由函数的调用方悄悄传入)
函数返回值的传递:小于8字节的返回值,以**寄存器**为中转。大于8字节的,以主调函数中新开辟的同样大小的中间变量temp为中转。
C语言对于尺寸太大的返回值类型,会使用一个临时的栈上内存区域作为中转,结果返回值对象会被拷贝两次。故不到万不得已,不要轻易返回大尺寸对象。
C++函数的返回值传递
C++处理大返回值略有不同,其可能是像C那样,1次拷贝到栈上的临时对象里,然后把临时对象拷贝到存储返回值的对象里。
但,有些编译器会进行返回值优化RVO(Return Value Optimization),这样,对象拷贝会减少一次,即没有临时对象temp了,直接拷贝到主调函数的相应对象中。
struct cpp_obj
cout&& &ctor\n& ;
cpp_obj(const cpp_obj& c)
cout&& &copy ctor\n& ;
cpp_obj& operator=(const cpp_obj& rhs)
cout&& &operator=\n& ;
~cpp_obj()
cout&& &dtor\n& ;
cpp_obj foo()
cout && &before foo return\n& ;
int main()
n = foo() ;
cout && &before main return\n& ;
return 0 ;
//---------运行结果---------
before foo return
before main return
此例子是在g++下编译运行。此例就没有设置一个临时变量temp,而是直接把被调函数局部变量的值直接拷贝到主调函数中去。
C++对于返回值还有一种更&激进&的优化策略&&NRV(Named Return Value)具名返回值优化
这种优化是甚至连被调函数中的局部变量都不要了!直接在主调函数中操作对象(根据隐藏参数传入的对象的引用)。
关于NRV要注意两点:(自己总结的,若有不对,请拍砖)
1、在被调函数foo中,其局部变量声明处即是调用主调函数main中对象的默认构造函数处。main中的对象定义处,只是开辟一个空间,当时并不调用构造函数。
2、为何在主调函数中 CObj obj = foo() 会触发NRV优化
而分开写: CO obj = foo() ; 没有NRV优化呢?
程序员必须给class X定义拷贝构造函数才能触发NRV优化,不然还是按照最初的较慢的方式执行。(我们的第二种方式没有涉及到拷贝构造函数,故不会触发NRV优化)
但现在的编译器即使去掉类中的拷贝构造函数,也一样会有NRV优化,但必须是向在对象初始化时调用子函数才会有NRV。
(若没有NRV优化,则被调函数中会生成局部对象,但这个局部对象直接拷贝到主函数相应的对象中,也不会像C那样还要生成一个临时变量)
若把上面的例子的调用方式改为: cpp_obj n = foo() ;
则会触发NRV优化,执行结果就是:
//cpp_obj n = foo() ;改为:
//foo实际就被改为:
void foo(cpp_obj& __result)
// 调用__result的默认构造函数
__result.cpp_obj::cpp_obj();
// 处理__result
//---------NRV后的运行结果---------
before foo return
before main return
(一定注意:只有CObj obj = foo();形式的调用才会有NRV优化!)
关于NRV优化详细见《深入理解C++对象模型》
堆是一块巨大的内存空间,常常占据整个虚拟地址空间的绝大部分。在这片空间里,程序可以请求一块连续内存,并自由地使用,这块内存在程序主动放弃之前都会一直保持有效。在C语言中我们可以用malloc函数在堆上申请空间。
malloc的实现:
操作系统内核管理着进程的地址空间,它通过的有系统调用,若让malloc调用这个系统调用实现申请内存,可完成这个工作。
但是,这样做性能较差,因为每次进行申请释放空间都需要进行系统调用,系统调用的开销比较大,会进行内核态和用户态的切换。
比较好的做法是程序向操作系统申请一块适当大小的堆空间,然后由程序自己管理这块空间,管理着堆空间分配的往往是程序的运行库(一般是操作系统提供的共享库)。
malloc实际上就是对这共享库中函数的包装。
&批发-零售&类比:
运行库相当于是向操作系统批发了一块较大的堆空间,然后零售给程序用。运行库在向程序零售空间时,必须管理此空间,不能把一块空间出售两次。
当空间不够用时,运行库再向操作系统批发(调用OS相应的系统调用)。
注意:这个运行库一般也是操作系统或语言提供给我们的,其包含了管理堆空间的算法,其运行在用户态下。
(我们自己也可以实现这个分配算法,但常用的分配算法已经被各种系统、库实现了无数遍,没有必要重复发明轮子)
每个进程在创建时都会有一个默认堆,这个堆在进程启动时创建,并且直到进程结束都一直存在。在Windows中默认堆大小为1MB。
(注意:在Windows中堆不一定是向上增长的)
问:malloc申请的空间是不是连续的?
答:若&空间&指的是虚拟空间的话,那么答案是连续的,即每一次malloc分配后返回的空间都可以看做是一块连续的地址。(进程中可能存在多个堆,但一次能够分配的最大堆空间取决于最大的那个堆)
如果空间值的是物理空间,则不一定连续,因为一块连续的虚拟地址空间有可能是若干个不连续的物理页拼凑成的。
堆空间管理算法
* 1、空闲链表法
把堆中各个空闲块按链表的方式连接起来,当用户请求时遍历链表找到合适的块。
* 2、位图(这个思想好)
将整个堆划分为大量的大小相同的块。当用户请求时分配整数个空间给用户。我们可以用一个整数数组的位来记录分配状况。
(每个块只有头/使用/空闲三种状态,即用两个位就可表示一个块,因此称为位图。头是用来标记定界的作用)璁$畻鏈虹郴缁熶腑CPU涓?殑base瀵勫瓨鍣ㄥ拰limit瀵勫瓨鍣ㄧ殑浣滅敤鏄?紵
璁℃暟鍣ㄥ瘎瀛樺櫒
鍫嗘爤鎸囬拡瀵勫瓨鍣

我要回帖

更多关于 基于栈 基于寄存器 的文章

 

随机推荐