本文主要是介绍计算机内什么叫函数,【计算机内功心法】八:函数运行时在内存中是什么样子?...,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
在开始本篇的内容前,咱们先来思考几个问题。nginx
咱们先来看一段简单的代码:
void func(int a) {
if (a > 100000000) return;
int arr[100] = {0};
func(a + 1);
}
你能看出这段代码会有什么问题吗?程序员
咱们在以前的文章《高性能高并发服务器是如何实现的》一中提到了一项关键技术——协程,你知道协程的本质是什么吗?有的同窗可能会说是用户线程,那么什么是用户态线程,这是怎么实现的?
函数运行起来后是什么样子?
这个问题看似没什么关联,但这背后有同样东西你须要理解,这就是所谓的函数运行时栈,run time stack。服务器
接下来咱们就好好看看到底什么是函数运行时栈,为何完全理解函数运行时栈对程序员来讲很是重要。数据结构
从进程、线程到函数调用
汽车在高速上行驶时有不少信息,像速度、位置等等,经过这些信息咱们能够直观的感觉汽车的运行时状态。并发
pexels-mike-945443
一样的,程序在运行时也有不少信息,像有哪些程序正在运行、这些程序执行到了哪里等等,经过这些信息咱们能够直观的感觉系统中程序运行的状态。app
其中,咱们创造了进程、线程这样的概念来记录有哪些程序正在运行,关于进程和线程的概念请参见《看完这篇还不懂进程和线程你来打我》。函数
进程和线程的运行体如今函数执行上,函数的执行除了函数内部执行的顺序执行还有子函数调用的控制转移以及子函数执行完毕的返回。其中函数内部的顺序执行乏善可陈,重点是函数的调用。高并发
所以接下来咱们的视角将从宏观的进程和线程拉近到微观下的函数调用,重点来讨论一下函数调用是怎样实现的。性能
函数调用的活动轨迹:栈
玩过游戏的同窗应该知道,有时你为了完成一项主线任务不得不去打一些支线的任务,支线任务中可能还有支线任务,当一个支线任务完成后退回到前一个支线任务,这是什么意思呢,举个例子你就明白了。spa
假设主线任务西天取经A依赖支线任务收服孙悟空B和收服猪八戒C,也就是说收服孙悟空B和收服猪八戒C完成后才能继续主线任务西天取经A;
支线任务收服孙悟空B依赖任务拿到紧箍咒D,只有当任务D完成后才能回到任务B;
整个任务的依赖关系如图所示:
1603672619352
如今咱们来模拟一下任务完成过程。
首先咱们来到任务A,执行主线任务:
1603672811135
执行任务A的过程当中咱们发现任务A依赖任务B,这时咱们暂停任务A去执行任务B:
1603673078596
执行任务B的时候,咱们又发现依赖任务D:
1603673983874
执行任务D的时候咱们发现该任务再也不依赖任何其它任务,所以C完成后咱们能够会退到前一个任务,也就是B:
1603673078596
任务B除了依赖任务C外再也不依赖其它任务,这样任务B完成后就能够回到任务A:
1603672811135
如今咱们回到了主线任务A,依赖的任务B执行完成,接下来是任务C:
1603673950126
和任务D同样,C不依赖任何其它其它任务,任务C完成后就能够再次回到任务A,再以后任务A执行完毕,整个任务执行完成。
让咱们来看一下整个任务的活动轨迹:
1603674440674
仔细观察,实际上你会发现这是一个First In Last Out 的顺序,自然适用于栈这种数据结构来处理。
再仔细看一下栈顶的轨迹,也就是A、B、D、B、A、C、A,实际上你会发现这里的轨迹就是任务依赖树的遍历过程,是否是很神奇,这也是为何树这种数据结构的遍历除了能够用递归也能够用栈来实现的缘由。
A box
函数调用也是一样的道理,你把上面的ABCD换成函数ABCD,本质不变。
所以,如今咱们知道了,使用栈这种结构就能够用来保存函数调用信息。
和游戏中的每一个任务同样,当函数在运行时每一个函数也要有本身的一个“小盒子”,这个小盒子中保存了函数运行时的各类信息,这些小盒子经过栈这种结构组织起来,这个小盒子就被称为栈帧,stack frames,也有的称之为call stack,无论什么命名方式,总之,就是这里所说的小盒子,这个小盒子就是函数运行起来后占用的内存,这些小盒子构成了咱们一般所说的栈区。关于栈区详细的讲解你能够参考《深刻理解操做系统:程序员应如何理解内存》一文。
那么函数调用时都有哪些信息呢?
函数调用与返回信息
咱们知道当函数A调用函数B的时候,控制从A转移到了B,所谓控制其实就是指CPU执行属于哪一个函数的机器指令,CPU从开始执行属于函数A的指令切换到执行属于函数B的指令,咱们就说控制从函数A转移到了函数B。
控制从函数A转移到函数B,那么咱们须要有这样两个信息:
我从哪里来 (返回)
要到去哪里 (跳转)
是否是很简单,就比如你出去旅游,你须要知道去哪里,还须要记住回家的路。
函数调用也是一样的道理。
当函数A调用函数B时,咱们只要知道:
函数A对于的机器指令执行到了哪里 (我从哪里来,返回)
函数B第一条机器指令所在的地址 (要到哪里去,跳转)
有这两条信息就足以让CPU开始执行函数B对应的机器指令,当函数B执行完毕后跳转回函数A。
那么这些信息是怎么获取并保持的呢?
如今咱们就能够打开这个小盒子,看看是怎么使用的了。
假设函数A调用函数B,如图所示:
1603845345171
当前,CPU执行函数A的机器指令,该指令的地址为0x400564,接下来CPU将执行下一条机器指令也就是:
call 0x400540
这条机器指令是什么意思呢?
这条机器指令对应的就是咱们在代码中所写的函数调用,注意call后有一条机器指令地址,注意观察上图你会看到,该地址就是函数B的第一条机器指令,从这条机器指令后CPU将跳转到函数B。
如今咱们已经解决了控制跳转的“要到哪里去”问题,当函数B执行完毕后怎么跳转回来呢?
原来,call指令除了给出跳转地址以外还有这样一个做用,也就是把call指令的下一条指令的地址,也就是0x40056a push到函数A的栈帧中,如图所示:
1603845893680
如今,函数A的小盒子变大了一些,由于装入了返回地址:
1603846004468
如今CPU开始执行函数B对应的机器指令,注意观察,函数B也有一个属于本身的小盒子(栈帧),能够往里面扔一些必要的信息。
1603846305325
若是函数B中又调用了其它函数呢?
道理和函数A调用函数B是同样的。
让咱们来看一下函数B最后一条机器指令ret,这条机器指令的做用是告诉CPU跳转到函数A保存在栈帧上的返回地址,这样当函数B执行完毕后就能够跳转到函数A继续执行了。
至此,咱们解决了控制转移中“我从哪里来”的问题。
参数传递与返回值
函数调用与返回使得咱们能够编写函数,进行函数调用。但调用函数除了提供函数名称以外还须要传递参数以及获取返回值,那么这又是怎样实现的呢?
在x86-64中,多数状况下参数的传递与获取返回值是经过寄存器来实现的。
假设函数A调用了函数B,函数A将一些参数写入相应的寄存器,当CPU执行函数B时就能够从这些寄存器中获取参数了。
一样的,函数B也能够将返回值写入寄存器,当函数B执行结束后函数A从该寄存器中就能够读取到返回值了。
咱们知道寄存器的数量是有限的,当传递的参数个数多于寄存器的数量该怎么办呢?
这时那个属于函数的小盒子也就是栈帧又能发挥做用了。
原来,当参数个数多于寄存器数量时剩下的参数直接放到栈帧中,这样被调函数就能够从前一个函数的栈帧中获取到参数了。
如今栈帧的样子又能够进一步丰富了,如图所示:
1603948689593
从图中咱们能够看到,调用函数B时有部分参数放到了函数A的栈帧中,同时函数A栈帧的顶部依然保存的是返回地址。
局部变量
咱们知道在函数内部定义的变量被称为局部变量,这些变量在函数运行时被放在了哪里呢?
原来,这些变量一样能够放在寄存器中,可是当局部变量的数量超过寄存器的时候这些变量就必须放到栈帧中了。
所以,咱们的栈帧内容又一步丰富了。
1604018423586
细心的同窗可能会有这样的疑问,咱们知道寄存器是共享资源能够被全部函数使用,既然能够将函数A的局部变量写入寄存器,那么当函数A调用函数B时,函数B的局部变量也能够写到寄存器,这样的话当函数B执行完毕回到函数A时寄存器的值已经被函数B修改过了,这样会有问题吧。
这样的确会有问题,所以咱们在向寄存器中写入局部变量以前,必定要先将寄存器中开始的值保存起来,当寄存器使用完毕后再恢复原值就能够了。
那么咱们要将寄存器中的原始值保存在哪里呢?
有的同窗可能已经猜到了,没错,依然是函数的栈帧中。
1604019378874
最终,咱们的小盒子就变成了如图所示的样子,当寄存器使用完毕后根据栈帧中保存的初始值恢复其内容就能够了。
如今你应该知道函数在运行时究竟是什么样子了吧,以上就是问题3的答案。
Big Picture
须要再次强调的一点就是,上述讨论的栈帧就位于咱们常说的栈区。
栈区,属于进程地址空间的一部分,如图所示,咱们将栈区放大就是图左边的样子。
1604020019183
关于栈区详细的讲解你能够参考《深刻理解操做系统:程序员应如何理解内存》这篇。
最后,让咱们回到文章开始的这段简单代码:
void func(int a) {
if (a > 100000000) return;
int arr[100] = {0};
func(a + 1);
}
void main(){
func(0);
}
想想这段代码会有什么问题?
总结
本章咱们从几个看似没什么关联的问题出发,详细讲解了函数运行时栈是怎么一回事,为何咱们不能建立过多的局部变量。细心的同窗会发现第2个问题咱们没有解答,这个问题讲解放到下一篇,也就是协程中讲解。
但愿这篇文章能对你们理解函数运行时栈有所帮助。
这篇关于计算机内什么叫函数,【计算机内功心法】八:函数运行时在内存中是什么样子?...的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!