首页 > 技术文章 > 【操作系统】多线程栈空间模型

nntzhc 2021-05-28 11:44 原文

https://www.zhihu.com/question/323415592

单线程模型里,函数调用是怎么回事呢?

 很简单,通过CPU直接支持的栈区,自动维护“函数调用链”:

栈顶
printSth函数的局部变量
main函数里面调用printSth函数的那条指令的位置
main函数的局部变量
栈底

 

对于printSth函数,当它获得执行权时,只需知道当前栈顶位置,然后基于这个位置就能推断出属于自己的局部变量的位置。

当它执行结束之后,就要通过pop指令清除自己用过的局部变量,把main函数里面调用printf函数的那条指令的位置取出、然后通过ret指令跳转到下一条指令继续执行。

main() {
  fun1();
  printSth(...);
  fun2();
}

 

比如,对上面这个场景,printSth执行结束后,下一条指令就是调用fun2.

 

如果printSth里面还调用了fun3,可依此类推:

栈顶
fun3的局部变量
printSth里面调用fun3的那条指令的位置
printSth函数的局部变量
main函数里面调用printSth函数的那条指令的位置
main函数的局部变量
栈底

 

这就是所谓的调用链。

只要维护好这个调用链信息,程序就可以有条不紊的按设计预想执行了。

 


 

彻底搞明白调用链如何维护之后,我们很容易想到:如果我另外再申请一块内存,把它的起始地址放进CPU的堆栈寄存器;那么,是不是就可以用这块地址另外维护一条调用链了呢?

这就是线程的原理。

所谓“新开一条线程”,实质上就是另外申请了一块内存,然后把这块内存当作堆栈,维护另外一条调用链。

而所谓“线程获得执行权”呢,实质上就是把对应线程的栈顶指针等信息载入CPU的栈指示器,使得它沿着这条调用链继续执行下去——执行一段时间,把它的栈顶指针等信息找个地方保存、然后载入另一个线程的栈顶指针等信息,这就是所谓的“线程切换”。

 

线程有两种。

如果维护调用链(以及执行现场)的任务全部放在用户空间,不让操作系统知道,这就叫“用户态线程”。

反之,如果操作系统自己提供了开辟新线程以及维护它的调用链的一整套方法,这就叫“内核态线程”。

两者的差别就是后者是操作系统管理的,可以得到多CPU之类的直接支持。

 

但在内存空间使用上,两者并无根本区别:它们都是另外申请了一块空间用作堆栈,然后像传统的单线程程序一样,用这个堆栈维护调用链(以及局部变量等信息)。

 

线程和进程的区别就在于,线程只有调用链,而进程还包含常量区、全局变量区等其他区域,同时还有各种资源的所有权。

换句话说,操作系统认为,诸如动态申请内存、内核对象等各种资源,哪怕是在某个线程里面申请的,它的所有权仍然属于进程所有——所以,线程退出除了会清理调用链信息外,并不释放其他资源;而进程退出就会自动归还它申请的各种资源(某些特殊资源除外:并不能盲目认为一旦进程退出一切就会变回原样)。

 


 

明白了这个之后,问题迎刃而解:

1、所有线程都是在各自独立的栈区维护的调用链(以及执行现场)。

2、线程局部变量处于各自所属的栈区。

3、同一个进程的多个线程共享一个页表,所以,理论上属于同进程下的多个线程能访问彼此的栈空间,实际一般不允许跨线程直接传递局部变量的引用/指针,毕竟栈里面是局部变量,随时可能失效。

4、线程中取得的、进程生存期有效的资源,要么直接/间接挂载到全局变量/全局静态变量上,要么就一定要在线程结束前释放。不然就会造成资源泄露(搜索不被全局变量和局部变量索引的内存并主动释放,这正是垃圾回收的原理)。

5、线程由谁启动这个信息并不在调用链上。换句话说,所有线程都是平等的,它们各自独立使用自己的专属栈区(但主线程较为特殊,大多实现中,它的退出就意味着进程结束;除此之外,它们是平等的)。

推荐阅读