首先,纠正几个问题:
栈空间不能动态增长,是指栈的虚地址空间是固定的,但不代表栈的物理地址一定是分配好的,首次访问的时候才分配实际物理内存的做法是存在的。
主流操作系统(Windows/Linux)的栈的地址空间是固定的,但不代表所有操作系统都是这样的。
Windows、Linux不使用动态增长的几个原因:
1. 如果不预先占用好空间,那么运行时再分配时候,可能已经没地址空间可用了。比如,假设栈地址范围是0x10000~0x20000,不够用的时候需要往前扩展,结果发现0x0F000已经被人用了,那就最多扩展0x1000,意义不是很大。
2. 如果说预留一大片范围的地址都给栈扩展使用,那么跟现在的设计基本没差别,反倒是假如栈是够用的,这段地址空间是浪费的。
3. 每个线程都有一个栈,线程数量可能会很多,如果每个栈都预留扩展的区域,那么总预留空间是一个很大的数值。一个线程预留10MB扩展空间,100个线程就是1GB,太浪费了,尤其在32位环境下,用户可用的地址空间本来也没多少。
4. 因为操作系统无法完全预测应用程序的行为,操作系统也不应该严格限制应用程序的行为,所以才有了现在的这种栈的设计,说到底,是这种需求很少见,栈太大本身就是一种不合理的设计。
如果你觉得不够用,完全可以手写汇编然后构造一个动态栈出来(malloc一个大内存然后把sp指过去),不管是操作系统还是CPU,都没限制用户程序的sp指针必须指向默认的栈空间。
看下面的图:
题主之所以能问出这个问题,也许是因为大多数内存的理论模型都会画成左边那样子。那么自然而然的Heap可以不断往上扩展;Stack可以往下扩展。
但现实中是
1. 主流操作系统都是有多线程支持的,而多线程需要每个线程分配一个独立的Stack,每个Stack内部可以满足“向下增长“,但是必须要有个界限,不然没法实现了。否则下个Stack从哪开始呢?
2. heap和mmap segment的存在。mmap是有很多用途的。比如
上图中之所以把heap,mmap segement和stack画成一个颜色,是因为他们本质上差不太多,都是程序运行时动态分配和调整的。stack和mmap都是“一块内存”,因此实现中并不一定非得是Stack永远比mmap的地址数值更大。只要不重叠就行了。Linux本身的api也允许创建新线程时,指定一段内存作为“Stack”,而这个内存自然也需要通过malloc得到,这又回到了mmap/heap上了。
回到应用层面,巨大的Stack除了应付非常深的递归之外没有什么太大的用处。而非常深的递归一般就是程序哪里写bug了。为了不太有用的场景去做设计并不是理智的行为。
我们常规意义上说“Stack”实际只是在用CPU对一段内存的地址做指令寄存器的push和pop而已。如果这不够用你可以自行定制一个喜欢的形式来实现对Stack的管理。有些语言可以把Stack的管理玩出花。比如go,自己实现了对go routine的管理,每个routine的Stack都可以不是连续的,这样既避免浪费,又能轻松扩展。