fork()创建的子进程拥有全新且物理隔离的栈空间,与父进程栈完全独立;实验显示其栈地址不重叠、栈溢出不传染、ru_maxrss从零计数,证实进程级内存隔离。

直接运行 compute() 时,函数在主线程栈上逐层调用,栈深度随递归或嵌套调用自然增长;而 fork() 创建的是独立进程,子进程拥有**全新的、从零开始的栈空间**,与父进程栈完全物理隔离——这不依赖调度或并发,是操作系统进程模型的底层保证。
一、用栈帧地址直观对比“同一函数”的栈位置
以下 C 代码在 Linux 下可直接编译运行(gcc -o stackdepth stackdepth.c):
#include <stdio.h>
#include <unistd.h><p>void compute(int depth) {
char dummy;
printf("compute(%d): &dummy = %p, pid = %d\n",
depth, &dummy, getpid());
if (depth </p>
<p>int main() {
printf("main: &dummy = %p, pid = %d\n", &main, getpid());
compute(0);</p>
<pre class="brush:php;toolbar:false;">if (fork() == 0) {
// 子进程
printf("child: fork() returned 0 → new process\n");
compute(0); // 从 depth=0 重新开始,栈地址完全不同
return 0;
}
wait(NULL); // 父进程等待
return 0;
}
输出关键特征:
- 父进程中
compute(0)、compute(1)、compute(2)的&dummy地址连续下降(典型栈向下增长),pid 相同; - 子进程中首次
compute(0)的&dummy地址与父进程中任意一层都**不接近**,且 pid 不同; - 子进程的栈地址范围与父进程无重叠——这是
fork()时内核为子进程分配独立虚拟地址空间(含新栈)的直接证据。
二、用 ulimit 和递归爆栈验证“分离”不可传递
父进程设栈限制后触发崩溃,不影响子进程:
ulimit -s 128 # 将栈限制为 128KB ./stackdepth
现象:
- 父进程在
compute(20)左右因栈溢出被SIGSEGV终止; - 子进程仍能完整执行自己的
compute(0)→compute(1)→...,直到它自己触达 128KB 限制(起始点不同,崩溃深度也不同); - 证明:栈溢出是进程级故障,不会跨
fork()传染——因为根本就不是同一块物理/虚拟内存。
三、不用递归,用 getrusage 精确看栈用量
替换 compute() 为分配栈数组并查用量:
void compute() {
char stack_used[8192]; // 占用 8KB 栈
struct rusage usage;
getrusage(RUSAGE_SELF, &usage);
printf("pid %d: ru_maxrss=%ld KB, stack usage ~%zu KB\n",
getpid(), usage.ru_maxrss, sizeof(stack_used)/1024);
}
运行后可见:
- 父进程
ru_maxrss随多次调用缓慢上升(栈复用,RSS 不立即释放); - 子进程第一次调用
compute()时ru_maxrss显著低于父进程当前值,且其栈数组地址与父进程毫无关联; -
ru_maxrss统计的是驻留集大小,子进程从零开始计数,再次印证内存页未共享(写时复制前,栈页也是各自私有)。
四、关键提醒:别混淆 fork() 与线程栈
注意:此结论仅对 fork() 成立。若用 pthread_create() 启线程,所有线程共享同一进程的虚拟地址空间,栈虽逻辑分离但仍在同一地址空间内(只是栈基址不同),且可能映射到相邻内存页——这与 fork() 的**进程级物理/虚拟隔离**有本质区别。验证时务必用 fork(),而非线程。











