在开始本篇的内容前,我们先来思考几个问题。
1. 我们先来看一段简单的代码:
void func(int a) {
if (a > 100000000) return;
int arr[100] = {0};
func(a + 1);
}
你能看出这段代码会有什么问题吗?
2. 我们在上一篇文章《高性能高并发服务器是如何实现的》中提到了一项关键技术——协程,你知道协程的本质是什么吗?有的同学可能会说是用户态线程,那么什么是用户态线程,这是怎么实现的?
3. 函数运行起来后在内存中是什么样子?
这几个问题看似没什么关联,但这背后都指向一样东西,这就是所谓的函数运行时栈,run time stack。
接下来我们就好好看看到底什么是函数运行时栈,为什么彻底理解函数运行时栈对程序员来说非常重要。
从进程、线程到函数调用
汽车在高速上行驶时有很多信息,像速度、位置等等,通过这些信息我们可以直观的感受汽车的运行时状态。
同样的,程序在运行时也有很多信息,像有哪些程序正在运行、这些程序执行到了哪里等等,通过这些信息我们可以直观的感受系统中程序运行的状态。
其中,我们创造了进程、线程这样的概念来记录有哪些程序正在运行,关于进程和线程的概念请参见《看完这篇还不懂进程、线程与线程池你来打我》。
进程和线程的运行体现在函数执行上,函数的执行除了函数内部执行的顺序执行还有子函数调用的控制转移以及子函数执行完毕的返回。其中函数内部的顺序执行乏善可陈,重点是函数的调用。
因此接下来我们的视角将从宏观的进程和线程拉近到微观下的函数调用,重点来讨论一下函数调用是怎样实现的。
函数执行的活动轨迹:栈
玩过游戏的同学应该知道,有时你为了完成一项主线任务不得不去打一些支线的任务,支线任务中可能还有支线任务,当一个支线任务完成后退回到前一个支线任务,这是什么意思呢,举个例子你就明白了。
假设主线任务西天取经A依赖支线任务收服孙悟空B和收服猪八戒C,也就是说收服孙悟空B和收服猪八戒C完成后才能继续主线任务西天取经A;
支线任务收服孙悟空B依赖任务拿到紧箍咒D,只有当任务D完成后才能回到任务B;
整个任务的依赖关系如图所示:
现在我们来模拟一下任务完成过程。
首先我们来到任务A,执行主线任务: