点我查看秘籍连载函数
进程其实都是在执行任务,而任务其实就是函数定义的(函数也称为方法、子程序等,本质都同样),因此进程的做用就是不断的执行函数。程序启动时,第一个要执行的函数是main()函数(有些语言隐藏了这个函数,但任何程序必定会有一个程序入口函数),而后在main()函数中调用其它函数,每当调用其它函数时,都会先进行函数跳转,转而让进程去执行被调用的函数,当被调函数执行完成后又回到调用函数的位置继续向下运行。操作系统
程序执行的基本流程以下图所示。右边是程序的伪代码,左边是程序运行过程。首先进程跳转到main函数处开始执行,而后执行一个赋值语句a=1,继续往下发现是调用一个函数func1(),因而跳转到func1(),同时还会保存好main中是从这个位置(假设称为位置1)处跳转的,以便执行完func1()后能够跳回到main()。而后开始执行func1()中的代码,在CPU执行func1()执行的时候,main()函数就没法继续向下执行了,它必须等待func1()执行完成后的返回,当func1()执行完后根据跳回到位置1,因而main函数继续向下执行,也就是赋值语句x=2,而后又以一样的流程调用func2()函数并返回,最终main()函数执行完成,进程终止,程序退出。3d
每当进程调用一次函数,都会在用户栈中为该函数分配一个栈帧(stack frame),也称为调用栈(call stack),当该函数返回时又会释放该栈帧。释放的栈帧不会从虚拟内存中移除,它能够被以后调用的函数从新使用,因此栈空间的大小是不会减少的。指针
根据这个特性并结合上图所描述的程序执行过程,能够推断出一个重要的结论。因为函数内部调用函数时,外部函数的栈帧不会释放,只有内部函数所有退出了才会继续执行外部函数并在执行完成的时候释放外部函数的栈帧,因此,递归函数(即函数内部调用函数自身)若是递归调用的层次太多(好比无限递归),会分配大量的栈帧,而且不会释放,直到栈空间不足,没法再分配新的栈帧,这时会报栈溢出(stack overflows)错误。因此,必需要合理编写递归函数,使得递归函数可以在达到某些条件时返回,从而释放栈帧,避免无限递归。blog
栈帧中保存了传递给该函数的参数、该函数中定义的局部变量、函数的返回值、调用该函数的程序计数器副本,以及一些其它重要信息。这里有必要解释下栈帧中的程序计数器副本。递归
什么是程序计数器(Program Counter,PC)?这是CPU中的一个寄存器,在这个寄存器中保存了下一个要执行指令的指针。因此,CPU每执行一个指令的时候,就会设置这个寄存器使它指向下一个指令。进程
前面描述程序执行流程的时候说过,当main()函数调用func1()函数的时候,须要保存main()函数中调用func1()的位置,以便func1()返回时能够跳转回main()函数继续向下执行。其实,main()函数在开始调用func1()函数的时候,PC寄存器就已经指向了这个指令,CPU能够将这个指令的指针的值(也就是PC的副本)保存在func1()函数的栈帧中,这样func1()执行完成后就能将这个指针从新放回到CPU的PC寄存器中,使得CPU从新回到main()函数调用func1()的位置处,从而调用者main能够取得函数func1()栈帧中的返回值(这时候func1()的栈帧被释放),并继续执行下面的代码。内存
操做系统还为每一个进程维护另外一个栈:内核栈。这个栈的位置在内核的内存区域中,只有内核可以访问,用户进程没法访问。get
内核栈的做用是存放上下文切换时的进程信息。变量
当进程A要切换到进程B时,首先要陷入内核,而后内核将CPU中关于进程A的进程信息(即某些寄存器中的值)保存在进程A的内核栈中,而后从进程B的内核栈中恢复进程B的信息到CPU的某些寄存器中,再退出内核模式回到进程B,这样CPU就开始执行进程B了。