问:栈空间是一开始就全部分配好的吗?
答
不会一开始就把栈的最大容量全部分配成物理内存。
程序或线程创建时,系统先建立栈的虚拟地址空间;函数调用时通过移动栈指针划出栈帧;真正访问新的栈页面时,才通过缺页异常按需分配物理页。
建立栈的虚拟地址空间
↓
函数移动栈指针,划出栈帧
↓
第一次访问新的栈页面
↓
缺页异常,内核分配物理页
一、栈大小不等于物理内存占用
Linux 下执行:
ulimit -s
经常会看到 8192,表示主线程栈的软限制通常为 8 MiB。
这个数值表示栈最多可以增长到多大,不代表程序启动时已经占用了 8 MiB 物理内存。
| 概念 | 含义 |
|---|---|
| 虚拟地址空间 | 线程可以使用的一段地址范围 |
| 物理内存 | 当前真正驻留在 RAM 中的页面 |
栈页面通常只有在实际读写时,才会获得物理内存。
二、函数调用时如何分配栈帧?
编译器在编译阶段确定函数需要的栈帧大小。函数进入时,通常只需要移动栈指针。
void function()
{
int value = 10;
char buffer[64];
}
在 x86-64 上,可能生成类似指令:
sub rsp, 80
这只是把栈指针向下移动,为当前函数划出空间,不是调用 malloc(),通常也不需要系统调用。
函数返回时恢复栈指针,原栈帧就失效了:
add rsp, 80
ret
局部变量也不一定真的位于栈上,编译器可能把它放进寄存器或直接优化掉。
三、物理内存什么时候分配?
当程序第一次访问尚未映射的栈页面时:
- CPU 发现页表中没有有效映射。
- 触发缺页异常,进入内核。
- 内核判断地址是否位于合法栈范围。
- 合法则分配物理页、更新页表并继续执行。
- 超出栈限制则通常触发
SIGSEGV。
因此,移动栈指针主要消耗虚拟地址空间,触碰页面才会逐步占用物理内存。
四、主线程栈和普通线程栈
| 对比项 | 主线程栈 | pthread / std::thread 栈 |
|---|---|---|
| 创建时机 | 程序启动时建立 | 创建线程时建立 |
| 虚拟空间 | 可以在限制范围内按需扩展 | 通常在线程创建时确定固定范围 |
| 大小控制 | 主要受 RLIMIT_STACK 限制 |
可用 pthread_attr_setstacksize() 设置 |
| 物理内存 | 访问页面时按需分配 | 同样按需分配 |
| 越界保护 | 超过合法范围会异常 | 通常带有不可访问的保护区 |
主线程的初始栈还会保存 argc、argv、环境变量和辅助向量等程序启动信息。
普通线程虽然可能预留数 MiB 的栈虚拟地址空间,但不会立刻占用等量物理内存。不过线程数量过多仍会增加虚拟地址空间、页表和实际内存开销。
五、为什么递归过深会栈溢出?
每次函数调用都可能产生新的栈帧。递归层数持续增加时,栈指针会不断向边界移动。
void recursion()
{
char data[1024];
recursion();
}
当栈超过大小限制或触碰保护区域时,通常会收到 SIGSEGV。
常见原因包括:
- 无限递归或递归层数过深。
- 在栈上创建很大的数组或对象。
- 使用
alloca()分配大量空间。 - 创建线程时设置的栈空间过小。
大型缓冲区通常更适合使用 std::vector 在堆上分配。
六、如何观察?
查看栈大小限制:
ulimit -s
查看进程地址空间:
cat /proc/<pid>/maps
[stack] 表示主线程栈。
查看虚拟映射大小和实际驻留内存:
cat /proc/<pid>/smaps
Size:虚拟内存映射大小。Rss:当前驻留在物理内存中的大小。
面试总结
面试时可以直接回答:
栈不会一开始就把最大容量全部分配成物理内存。程序或线程创建时先建立栈的虚拟地址空间,函数调用通过移动栈指针创建栈帧,不会每次调用 malloc。第一次访问新的栈页面时会触发缺页异常,由内核按需分配物理页。主线程栈可以在限制范围内扩展,pthread 栈通常在线程创建时确定固定的虚拟地址范围。
记住四点:
- 栈大小限制不等于物理内存占用。
- 创建栈帧通常只是移动栈指针。
- 第一次触碰新页面时才分配物理页。
- 主线程栈与 pthread 栈的虚拟空间分配方式不同。
参考资料
- Linux man-pages:
execve(2)— https://man7.org/linux/man-pages/man2/execve.2.html - Linux man-pages:
getrlimit(2)— https://man7.org/linux/man-pages/man2/getrlimit.2.html - Linux man-pages:
pthread_create(3)— https://man7.org/linux/man-pages/man3/pthread_create.3.html - Linux man-pages:
proc_pid_maps(5)— https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html