浅谈Linux 中的进程栈、线程栈、内核栈、中断栈

栈是什么?栈有什么作用?首先,栈 (stack) 是一种串列形式的 数据结构 。这种数据结构的特点是 后入先出 (LIFO, Last In First Out),数据只能在串列的一端 (称为:栈顶 top) 进行 推入 (push) 和 弹出 (pop) 操作 。根据栈的特点,很容易的想到可以利用数组,来实现这种数据结构 。但是本文要讨论的并不是软件层面的栈,而是硬件层面的栈 。

浅谈Linux 中的进程栈、线程栈、内核栈、中断栈

文章插图
 
大多数的处理器架构,都有实现硬件栈 。有专门的栈指针寄存器,以及特定的硬件指令来完成 入栈/出栈 的操作 。例如在 ARM 架构上,R13 (SP) 指针是堆栈指针寄存器,而 PUSH 是用于压栈的汇编指令,POP 则是出栈的汇编指令 。
下面我们来看看栈有什么作用 。栈作用可以从两个方面体现:函数调用 和 多任务支持  。
一、函数调用我们知道一个函数调用有以下三个基本过程:
- 调用参数的传入
- 局部变量的空间管理
- 函数返回
函数的调用必须是高效的,而数据存放在 CPU通用寄存器 或者 RAM 内存 中无疑是最好的选择 。以传递调用参数为例,我们可以选择使用 CPU通用寄存器 来存放参数 。但是通用寄存器的数目都是有限的,当出现函数嵌套调用时,子函数再次使用原有的通用寄存器必然会导致冲突 。因此如果想用它来传递参数,那在调用子函数前,就必须先 保存原有寄存器的值,然后当子函数退出的时候再 恢复原有寄存器的值  。
函数的调用参数数目一般都相对少,因此通用寄存器是可以满足一定需求的 。但是局部变量的数目和占用空间都是比较大的,再依赖有限的通用寄存器未免强人所难,因此我们可以采用某些 RAM 内存区域来存储局部变量 。但是存储在哪里合适?既不能让函数嵌套调用的时候有冲突,又要注重效率 。
这种情况下,栈无疑提供很好的解决办法 。一、对于通用寄存器传参的冲突,我们可以再调用子函数前,将通用寄存器临时压入栈中;在子函数调用完毕后,在将已保存的寄存器再弹出恢复回来 。二、而局部变量的空间申请,也只需要向下移动下栈顶指针;将栈顶指针向回移动,即可就可完成局部变量的空间释放;三、对于函数的返回,也只需要在调用子函数前,将返回地址压入栈中,待子函数调用结束后,将函数返回地址弹出给 PC 指针,即完成了函数调用的返回;
于是上述函数调用的三个基本过程,就演变记录一个栈指针的过程 。每次函数调用的时候,都配套一个栈指针 。即使循环嵌套调用函数,只要对应函数栈指针是不同的,也不会出现冲突 。
浅谈Linux 中的进程栈、线程栈、内核栈、中断栈

文章插图
 
需要C/C++ linux服务器架构师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
浅谈Linux 中的进程栈、线程栈、内核栈、中断栈

文章插图
 
二、多任务支持然而栈的意义还不只是函数调用,有了它的存在,才能构建出操作系统的多任务模式 。我们以 main 函数调用为例,main 函数包含一个无限循环体,循环体中先调用 A 函数,再调用 B 函数 。
func B():return;func A():B();func main():while (1)A();【浅谈Linux 中的进程栈、线程栈、内核栈、中断栈】试想在单处理器情况下,程序将永远停留在此 main 函数中 。即使有另外一个任务在等待状态,程序是没法从此 main 函数里面跳转到另一个任务 。因为如果是函数调用关系,本质上还是属于 main 函数的任务中,不能算多任务切换 。此刻的 main 函数任务本身其实和它的栈绑定在了一起,无论如何嵌套调用函数,栈指针都在本栈范围内移动 。
由此可以看出一个任务可以利用以下信息来表征:
1. main 函数体代码
2. main 函数栈指针
3. 当前 CPU 寄存器信息
假如我们可以保存以上信息,则完全可以强制让出 CPU 去处理其他任务 。只要将来想继续执行此 main 任务的时候,把上面的信息恢复回去即可 。有了这样的先决条件,多任务就有了存在的基础,也可以看出栈存在的另一个意义 。在多任务模式下,当调度程序认为有必要进行任务切换的话,只需保存任务的信息(即上面说的三个内容) 。恢复另一个任务的状态,然后跳转到上次运行的位置,就可以恢复运行了 。


推荐阅读