计算机内什么叫函数,【计算机内功心法】八:函数运行时在内存中是什么样子?...

2024-03-01 07:30

本文主要是介绍计算机内什么叫函数,【计算机内功心法】八:函数运行时在内存中是什么样子?...,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在开始本篇的内容前,咱们先来思考几个问题。nginx

咱们先来看一段简单的代码:

void func(int a) {

if (a > 100000000) return;

int arr[100] = {0};

func(a + 1);

}

你能看出这段代码会有什么问题吗?程序员

咱们在以前的文章《高性能高并发服务器是如何实现的》一中提到了一项关键技术——协程,你知道协程的本质是什么吗?有的同窗可能会说是用户线程,那么什么是用户态线程,这是怎么实现的?

函数运行起来后是什么样子?

这个问题看似没什么关联,但这背后有同样东西你须要理解,这就是所谓的函数运行时栈,run time stack。服务器

接下来咱们就好好看看到底什么是函数运行时栈,为何完全理解函数运行时栈对程序员来讲很是重要。数据结构

从进程、线程到函数调用

汽车在高速上行驶时有不少信息,像速度、位置等等,经过这些信息咱们能够直观的感觉汽车的运行时状态。并发

f859512011adb643411c8cc3b5c80f6a.png

pexels-mike-945443

一样的,程序在运行时也有不少信息,像有哪些程序正在运行、这些程序执行到了哪里等等,经过这些信息咱们能够直观的感觉系统中程序运行的状态。app

其中,咱们创造了进程、线程这样的概念来记录有哪些程序正在运行,关于进程和线程的概念请参见《看完这篇还不懂进程和线程你来打我》。函数

进程和线程的运行体如今函数执行上,函数的执行除了函数内部执行的顺序执行还有子函数调用的控制转移以及子函数执行完毕的返回。其中函数内部的顺序执行乏善可陈,重点是函数的调用。高并发

所以接下来咱们的视角将从宏观的进程和线程拉近到微观下的函数调用,重点来讨论一下函数调用是怎样实现的。性能

函数调用的活动轨迹:栈

玩过游戏的同窗应该知道,有时你为了完成一项主线任务不得不去打一些支线的任务,支线任务中可能还有支线任务,当一个支线任务完成后退回到前一个支线任务,这是什么意思呢,举个例子你就明白了。spa

假设主线任务西天取经A依赖支线任务收服孙悟空B和收服猪八戒C,也就是说收服孙悟空B和收服猪八戒C完成后才能继续主线任务西天取经A;

支线任务收服孙悟空B依赖任务拿到紧箍咒D,只有当任务D完成后才能回到任务B;

整个任务的依赖关系如图所示:

7a2dfe8361c32878fce10bd0de866d5e.png

1603672619352

如今咱们来模拟一下任务完成过程。

首先咱们来到任务A,执行主线任务:

66769649d092758bfd9ec61c77e3ae74.png

1603672811135

执行任务A的过程当中咱们发现任务A依赖任务B,这时咱们暂停任务A去执行任务B:

b4b4a581477b9b9aed77c4774ca6d147.png

1603673078596

执行任务B的时候,咱们又发现依赖任务D:

450cba3a45962bab7d10d057134ce4ce.png

1603673983874

执行任务D的时候咱们发现该任务再也不依赖任何其它任务,所以C完成后咱们能够会退到前一个任务,也就是B:

7862dcfaa73eb684466ffb61fc052461.png

1603673078596

任务B除了依赖任务C外再也不依赖其它任务,这样任务B完成后就能够回到任务A:

932f9f2a07d92c41d98be5a9eeeffb68.png

1603672811135

如今咱们回到了主线任务A,依赖的任务B执行完成,接下来是任务C:

98a51bf3a6477fe7e2bd75d261f0a08d.png

1603673950126

和任务D同样,C不依赖任何其它其它任务,任务C完成后就能够再次回到任务A,再以后任务A执行完毕,整个任务执行完成。

让咱们来看一下整个任务的活动轨迹:

4caa5189cad1b4d3a087fddeda0681ca.png

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,如图所示:

46b0344b760705e619dec709fd5053ed.png

1603845345171

当前,CPU执行函数A的机器指令,该指令的地址为0x400564,接下来CPU将执行下一条机器指令也就是:

call 0x400540

这条机器指令是什么意思呢?

这条机器指令对应的就是咱们在代码中所写的函数调用,注意call后有一条机器指令地址,注意观察上图你会看到,该地址就是函数B的第一条机器指令,从这条机器指令后CPU将跳转到函数B。

如今咱们已经解决了控制跳转的“要到哪里去”问题,当函数B执行完毕后怎么跳转回来呢?

原来,call指令除了给出跳转地址以外还有这样一个做用,也就是把call指令的下一条指令的地址,也就是0x40056a push到函数A的栈帧中,如图所示:

7176875fc6794edd1e3d8a7e7923aa07.png

1603845893680

如今,函数A的小盒子变大了一些,由于装入了返回地址:

ac19e21204f90ed76a08a2cca4854151.png

1603846004468

如今CPU开始执行函数B对应的机器指令,注意观察,函数B也有一个属于本身的小盒子(栈帧),能够往里面扔一些必要的信息。

bfc6ad28a2da00fe22ab01e207ead747.png

1603846305325

若是函数B中又调用了其它函数呢?

道理和函数A调用函数B是同样的。

让咱们来看一下函数B最后一条机器指令ret,这条机器指令的做用是告诉CPU跳转到函数A保存在栈帧上的返回地址,这样当函数B执行完毕后就能够跳转到函数A继续执行了。

至此,咱们解决了控制转移中“我从哪里来”的问题。

参数传递与返回值

函数调用与返回使得咱们能够编写函数,进行函数调用。但调用函数除了提供函数名称以外还须要传递参数以及获取返回值,那么这又是怎样实现的呢?

在x86-64中,多数状况下参数的传递与获取返回值是经过寄存器来实现的。

假设函数A调用了函数B,函数A将一些参数写入相应的寄存器,当CPU执行函数B时就能够从这些寄存器中获取参数了。

一样的,函数B也能够将返回值写入寄存器,当函数B执行结束后函数A从该寄存器中就能够读取到返回值了。

咱们知道寄存器的数量是有限的,当传递的参数个数多于寄存器的数量该怎么办呢?

这时那个属于函数的小盒子也就是栈帧又能发挥做用了。

原来,当参数个数多于寄存器数量时剩下的参数直接放到栈帧中,这样被调函数就能够从前一个函数的栈帧中获取到参数了。

如今栈帧的样子又能够进一步丰富了,如图所示:

414f1a7a626b6d49f2a8edf43d757e1b.png

1603948689593

从图中咱们能够看到,调用函数B时有部分参数放到了函数A的栈帧中,同时函数A栈帧的顶部依然保存的是返回地址。

局部变量

咱们知道在函数内部定义的变量被称为局部变量,这些变量在函数运行时被放在了哪里呢?

原来,这些变量一样能够放在寄存器中,可是当局部变量的数量超过寄存器的时候这些变量就必须放到栈帧中了。

所以,咱们的栈帧内容又一步丰富了。

2beaf0bc8edc224d97a04f7fad87d63b.png

1604018423586

细心的同窗可能会有这样的疑问,咱们知道寄存器是共享资源能够被全部函数使用,既然能够将函数A的局部变量写入寄存器,那么当函数A调用函数B时,函数B的局部变量也能够写到寄存器,这样的话当函数B执行完毕回到函数A时寄存器的值已经被函数B修改过了,这样会有问题吧。

这样的确会有问题,所以咱们在向寄存器中写入局部变量以前,必定要先将寄存器中开始的值保存起来,当寄存器使用完毕后再恢复原值就能够了。

那么咱们要将寄存器中的原始值保存在哪里呢?

有的同窗可能已经猜到了,没错,依然是函数的栈帧中。

3c46b07982e4fa02322b2aaeb03720ab.png

1604019378874

最终,咱们的小盒子就变成了如图所示的样子,当寄存器使用完毕后根据栈帧中保存的初始值恢复其内容就能够了。

如今你应该知道函数在运行时究竟是什么样子了吧,以上就是问题3的答案。

Big Picture

须要再次强调的一点就是,上述讨论的栈帧就位于咱们常说的栈区。

栈区,属于进程地址空间的一部分,如图所示,咱们将栈区放大就是图左边的样子。

091965a331b4d452e462e875a1c8490e.png

1604020019183

关于栈区详细的讲解你能够参考《深刻理解操做系统:程序员应如何理解内存》这篇。

最后,让咱们回到文章开始的这段简单代码:

void func(int a) {

if (a > 100000000) return;

int arr[100] = {0};

func(a + 1);

}

void main(){

func(0);

}

想想这段代码会有什么问题?

总结

本章咱们从几个看似没什么关联的问题出发,详细讲解了函数运行时栈是怎么一回事,为何咱们不能建立过多的局部变量。细心的同窗会发现第2个问题咱们没有解答,这个问题讲解放到下一篇,也就是协程中讲解。

但愿这篇文章能对你们理解函数运行时栈有所帮助。

这篇关于计算机内什么叫函数,【计算机内功心法】八:函数运行时在内存中是什么样子?...的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/761649

相关文章

C++对象布局及多态实现探索之内存布局(整理的很多链接)

本文通过观察对象的内存布局,跟踪函数调用的汇编代码。分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等 文章链接:http://dev.yesky.com/254/2191254.shtml      论C/C++函数间动态内存的传递 (2005-07-30)   当你涉及到C/C++的核心编程的时候,你会无止境地与内存管理打交道。 文章链接:http://dev.yesky

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

eclipse运行springboot项目,找不到主类

解决办法尝试了很多种,下载sts压缩包行不通。最后解决办法如图: help--->Eclipse Marketplace--->Popular--->找到Spring Tools 3---->Installed。

22.手绘Spring DI运行时序图

1.依赖注入发生的时间 当Spring loC容器完成了 Bean定义资源的定位、载入和解析注册以后,loC容器中已经管理类Bean 定义的相关数据,但是此时loC容器还没有对所管理的Bean进行依赖注入,依赖注入在以下两种情况 发生: 、用户第一次调用getBean()方法时,loC容器触发依赖注入。 、当用户在配置文件中将<bean>元素配置了 lazy-init二false属性,即让

21.手绘Spring IOC运行时序图

1.再谈IOC与 DI IOC(lnversion of Control)控制反转:所谓控制反转,就是把原先我们代码里面需要实现的对象创 建、依赖的代码,反转给容器来帮忙实现。那么必然的我们需要创建一个容器,同时需要一种描述来让 容器知道需要创建的对象与对象的关系。这个描述最具体表现就是我们所看到的配置文件。 DI(Dependency Injection)依赖注入:就是指对象是被动接受依赖类

《计算机英语》Unit 1 Computer Overview 计算机概述

期末试卷组成 1、选择20道 2、判断20道 3、词汇翻译(单词+词组,参照课后习题) 4、翻译2道(一道原题,参照作业) SectionA About Computer 关于计算机 algorithm          n.  算法  operate          v.  操作  digital           adj. 数字的  integrated circuit

【操作系统】信号Signal超详解|捕捉函数

🔥博客主页: 我要成为C++领域大神🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】 ❤️感谢大家点赞👍收藏⭐评论✍️ 本博客致力于知识分享,与更多的人进行学习交流 ​ 如何触发信号 信号是Linux下的经典技术,一般操作系统利用信号杀死违规进程,典型进程干预手段,信号除了杀死进程外也可以挂起进程 kill -l 查看系统支持的信号

java中查看函数运行时间和cpu运行时间

android开发调查性能问题中有一个现象,函数的运行时间远低于cpu执行时间,因为函数运行期间线程可能包含等待操作。native层可以查看实际的cpu执行时间和函数执行时间。在java中如何实现? 借助AI得到了答案 import java.lang.management.ManagementFactory;import java.lang.management.Threa

SQL Server中,isnull()函数以及null的用法

SQL Serve中的isnull()函数:          isnull(value1,value2)         1、value1与value2的数据类型必须一致。         2、如果value1的值不为null,结果返回value1。         3、如果value1为null,结果返回vaule2的值。vaule2是你设定的值。        如

tf.split()函数解析

API原型(TensorFlow 1.8.0): tf.split(     value,     num_or_size_splits,     axis=0,     num=None,     name='split' ) 这个函数是用来切割张量的。输入切割的张量和参数,返回切割的结果。  value传入的就是需要切割的张量。  这个函数有两种切割的方式: 以三个维度的张量为例,比如说一