函数运行时在内存中是什么样子?

在开始本篇的内容前,我们先来思考几个问题 。
1. 我们先来看一段简单的代码:
void func(int a) {if (a > 100000000) return;int arr[100] = {0};func(a + 1);}你能看出这段代码会有什么问题吗?
你知道协程的本质是什么吗?有的同学可能会说是用户态线程,那么什么是用户态线程,这是怎么实现的?3. 函数运行起来后在内存中是什么样子?这几个问题看似没什么关联,但这背后都指向一样东西,这就是所谓的函数运行时栈,run time stack 。接下来我们就好好看看到底什么是函数运行时栈,为什么彻底理解函数运行时栈对程序员来说非常重要 。
 
从进程、线程到函数调用汽车在高速上行驶时有很多信息,像速度、位置等等,通过这些信息我们可以直观的感受汽车的运行时状态 。
同样的,程序在运行时也有很多信息,像有哪些程序正在运行、这些程序执行到了哪里等等,通过这些信息我们可以直观的感受系统中程序运行的状态 。
其中,我们创造了进程、线程这样的概念来记录有哪些程序正在运行,进程和线程的运行体现在函数执行上,函数的执行除了函数内部执行的顺序执行还有子函数调用的控制转移以及子函数执行完毕的返回 。其中函数内部的顺序执行乏善可陈,重点是函数的调用 。因此接下来我们的视角将从宏观的进程和线程拉近到微观下的函数调用,重点来讨论一下函数调用是怎样实现的 。
 
更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.

函数运行时在内存中是什么样子?

文章插图
 

函数运行时在内存中是什么样子?

文章插图
 
Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂
函数执行的活动轨迹:栈
玩过游戏的同学应该知道,有时你为了完成一项主线任务不得不去打一些支线的任务,支线任务中可能还有支线任务,当一个支线任务完成后退回到前一个支线任务,这是什么意思呢,举个例子你就明白了 。假设主线任务西天取经A依赖支线任务收服孙悟空B和收服猪八戒C,也就是说收服孙悟空B和收服猪八戒C完成后才能继续主线任务西天取经A;支线任务收服孙悟空B依赖任务拿到紧箍咒D,只有当任务D完成后才能回到任务B;整个任务的依赖关系如图所示:
函数运行时在内存中是什么样子?

文章插图
 
现在我们来模拟一下任务完成过程 。首先我们来到任务A,执行主线任务:
函数运行时在内存中是什么样子?

文章插图
 
 
 
【函数运行时在内存中是什么样子?】执行任务A的过程中我们发现任务A依赖任务B,这时我们暂停任务A去执行任务B:
函数运行时在内存中是什么样子?

文章插图
 
?
 
执行任务B的时候,我们又发现依赖任务D:
函数运行时在内存中是什么样子?

文章插图
 
?
 
执行任务D的时候我们发现该任务不再依赖任何其它任务,因此C完成后我们可以会退到前一个任务,也就是B:
函数运行时在内存中是什么样子?

文章插图
 
?
 
任务B除了依赖任务C外不再依赖其它任务,这样任务B完成后就可以回到任务A:
函数运行时在内存中是什么样子?

文章插图
 
现在我们回到了主线任务A,依赖的任务B执行完成,接下来是任务C:
函数运行时在内存中是什么样子?

文章插图
 
?
 
和任务D一样,C不依赖任何其它其它任务,任务C完成后就可以再次回到任务A,再之后任务A执行完毕,整个任务执行完成 。让我们来看一下整个任务的活动轨迹:
函数运行时在内存中是什么样子?

文章插图
 
?
 
仔细观察,实际上你会发现这是一个First In Last Out 的顺序,天然适用于栈这种数据结构来处理 。再仔细看一下栈顶的轨迹,也就是A、B、D、B、A、C、A,实际上你会发现这里的轨迹就是任务依赖树的遍历过程,是不是很神奇,这也是为什么树这种数据结构的遍历除了可以用递归也可以用栈来实现的原因 。


推荐阅读