当前位置: 首页 > java >正文

理解进程栈内存的使用

1.进程栈的初始化

进程在启动后会调用exec加载可执行文件,会为进程分配4KB的初始内存。系统会通过do_execvedo_execve_common函数完成可执行程序的实际加载过程。其中,do_execve_common会调用bprm_mm_init来申请新的mm_struct地址空间对象,为进程运行做好准备工作。

// file:fs/exec.c
static int bprm_mm_init(struct linux_binprm *bprm)
{// 申请一个全新的地址空间mm_struct对象bprm->mm = mm = mm_alloc();__bprm_mm_init(bprm);......
}

在申请完地址空间后,就会给新进程的栈申请一页大小的虚拟内存空间,作为给新进程准备的栈内存。申请完后把栈的指针保存到bprm->p中记录起来。

// file:fs/exec.c
static int __bprm_mm_init(struct linux_binprm *bprm)
{bprm->vma = vma = vm_area_alloc(mm);vma->vm_end = STACK_TOP_MAX;vma->vm_start = vma->vm_end - PAGE_SIZE;......bprm->p = vma->vm_end - sizeof(void *);
}

在 Linux 系统中,进程的虚拟地址空间通过 vm_area_struct 结构体来表示。因此,栈内存的申请实际上只是创建一个描述地址范围的 vma 对象,而非直接分配物理内存。

__bprm_mm_init 函数中,内核会调用 vm_area_alloc 来创建一个用作栈的 vma 对象。该对象的 vm_end 指向 STACK_TOP_MAX(地址空间顶部附近),而 vm_startvm_end 之间预留了一个页(4KB)的空间,作为默认的栈大小。最终,栈指针会被记录在 bprm->p 中。

如下图所示:

在进程初始化栈的过程中,内核通过 vm_area_struct 结构体为栈区划定一段连续的虚拟地址空间:以 STACK_TOP_MAX 为高地址参考,向下预留 4KB 大小的虚拟地址范围作为初始栈空间,并通过 vm_startvm_end 记录该范围的起止地址;同时,设置初始栈顶指针指向栈区的初始顶部,为后续函数调用等栈操作提供起始位置。这一步骤仅完成了栈的虚拟地址空间分配,后续进程实际使用栈时,才会通过页表机制将虚拟地址映射到物理内存(若物理内存尚未分配,会触发 “缺页异常”,由内核分配物理内存并建立映射)。
在这里插入图片描述

接下来的进程加载过程会使用 load_elf_binary 真正加载可执行二进制程序。在加载时,会把前面准备的进程栈的地址空间指针设置到新进程 mm 对象上,如下图所示:

task_struct里的mm指向mm_structmm_struct通过stack_start关联栈区,vm_area_structvm_startvm_end界定了栈区在进程地址空间的范围,进程地址空间按高到低地址分布,栈区靠近高地址,以STACK_TOP_MAX为高地址边界,初始有 4KB 大小,这样就完成了栈空间在进程内存管理结构中的初始化,为后续栈的使用奠定基础。
在这里插入图片描述

2.栈的自动增长

在进程启动加载时,栈内存默认仅分配4KB空间。随着程序运行,调用链和局部变量的增加会导致栈使用量超出初始容量。此时,缺页处理函数__do_page_fault将介入处理:当需要访问的地址address小于栈对应虚拟内存区域(vma)的起始地址时,系统会调用expand_stack函数扩展栈的虚拟地址空间。__do_page_fault函数的源码显示,栈空间扩展的具体实现正是由expand_stack完成的。

// file:arch/x86/mm/fault.c
static inline
void do_user_addr_fault(..., unsigned long address)
{......if (likely(vma->vm_start <= address))goto good_area;// 如果vma的开始地址比address大,则判断VM_GROWSDOWN是否可以动态扩充if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {bad_area(regs, hw_error_code, address);return;}// 对vma进行扩充if (unlikely(expand_stack(vma, address))) {bad_area(regs, error_code, address);return;}good_area:handle_mm_fault(vma, address, flags, regs);......
}

下图中展示了栈的自动增长逻辑:当进程地址空间中,访问的address超过了栈区原本由vm_area_structvm_startvm_end所界定的范围时,就会触发栈的扩充。此时会新增一个扩充区域,使栈的大小得以增加,以满足进程对栈空间的需求,整个进程地址空间按高地址到低地址(从STACK_TOP_MAX0x00000000)分布,栈区及扩充的新区域处于接近高地址的部分。
在这里插入图片描述
do_user_addr_fault函数里,首先判断要访问的变量地址address是否在vma(虚拟内存区域)内部,若在内部,就调用handle_mm_fault来分配实际的物理页。还有另一种逻辑:要是address超出了vma的范围(因为栈一般是从高地址向低地址增长的,当vma->vm_start大于address时,就说明栈空间不够用了),就会调用expand_stack对栈进行扩充。
以下代码是expand_stack函数内部实现细节:

// file:mm/mmap.c
int expand_stack(struct vm_area_struct *vma, unsigned long address)
{......return expand_downwards(vma, address);
}int expand_downwards(struct vm_area_struct *vma, unsigned long address)
{......// 计算栈扩大后的最后大小size = vma->vm_end - address;// 计算需要扩充几个页面grow = (vma->vm_start - address) >> PAGE_SHIFT;// 判断是否允许扩充acct_stack_growth(vma, size, grow);// 如果允许则开始扩充vma->vm_start = address;return ...
}

expand_downwards中先进行几个计算:

  • 计算新的栈大小。计算公式是size = vma->vm_end - address;
  • 计算需要增长的页数。计算公式是grow = (vma->vm_start - address) >> PAGE_SHIFT;

接着检查栈空间是否允许扩展,这一判断在acct_stack_growth函数中进行。若允许扩展,只需调整vma->vm_start的值即可。扩充的具体操作如下图所示:

在栈扩展过程中,内核通过vm_area_struct结构体管理进程虚拟地址空间中的栈区域。当需要扩展栈空间时,系统会根据访问地址address动态调整vma->vm_start的值。从进程地址空间布局来看,栈区域位于高地址端(靠近STACK_TOP_MAX),而低地址端起始于0x00000000。通过下移vma->vm_start边界,内核可以扩展栈区域的可用空间,从而满足进程运行时对栈空间的动态增长需求。
在这里插入图片描述
以下是acct_stack_growth函数进行的操作:

// file:mm/mmap.c
static int acct_stack_growth(struct vm_area_struct *vma,unsigned long size, unsigned long grow)
{......// 检查地址空间是否超出限制if (!may_expand_vm(mm, grow))return -ENOMEM;// 检查是否超出栈的大小限制if (size > rlimit(RLIMIT_STACK))return -ENOMEM;......return 0;
}

acct_stack_growth函数中主要执行一系列条件判断。其中,may_expand_vm用于检查增长后的页数是否超出虚拟地址空间的总大小限制;而rlim[RLIMIT_STACK].rlim_cur则存储着栈空间大小的限制值。这些限制参数都可以通过ulimit命令进行查看。

#ulimit -a
......
max memory size       (kbytes, -m) unlimited
stack size            (kbytes, -s) 8192
virtual memory        (kbytes, -v) unlimited

系统显示当前虚拟地址空间大小无限制,而栈空间限制为8MB。若进程栈大小超过此限制,系统将返回-ENOMEM错误。如需调整默认设置,可通过ulimit命令进行修改。

#ulimit -s 10240
#ulimit -a
stack size            (kbytes, -s) 10240

3.总结

本节深入解析进程栈内存的工作原理,主要分为三个关键机制:

(1)初始栈空间分配
进程加载时,内核会为其栈区域分配一个虚拟地址空间VMA对象。该对象默认预留4KB空间(一个Page大小),通过vm_start和vm_end指针界定范围。

(2)按需物理内存分配
进程运行期间,当首次访问栈上的变量时,若对应物理页尚未分配,会触发缺页中断。此时内核通过伙伴系统完成实际物理内存的分配。

(3)动态栈扩展机制
当栈使用量超过初始4KB时,系统会自动扩展栈空间。扩展上限可通过ulimit -s命令查看和配置,确保栈增长处于可控范围内。

http://www.xdnf.cn/news/20104.html

相关文章:

  • 嵌入式第四十六天(51单片机)
  • git提交代码
  • React笔记_组件之间进行数据传递
  • 只会git push?——git团队协作进阶
  • RAG(检索增强生成)-篇一
  • Linux-xargs-seq-tr-uniq-sort
  • Oracle 数据库使用事务确保数据的安全
  • 实现自己的AI视频监控系统-第三章-信息的推送与共享4
  • 如何在SpringBoot项目中优雅的连接多台Redis
  • vue3的 三种插槽 匿名插槽,具名插槽,作用域插槽
  • 无需Python:Shell脚本如何成为你的自动化爬虫引擎?
  • Dubbo消费者无法找到提供者问题分析和处理
  • 记录SSL部署,链路不完整问题
  • Eclipse 常用搜索功能汇总
  • 连接MCP,Lighthouse MCP Server和CNB MCP Server应用
  • 解密注意力计算的并行机制:从多头并张量操作到CUDA内核优化
  • 25年Docker镜像无法下载的四种对策
  • 【Spring Cloud Alibaba】Sentinel(一)
  • 【LeetCode数据结构】设计循环队列
  • Java 并发编程解析:死锁成因、可重入锁与解决方案
  • 人工智能机器学习——逻辑回归
  • go 初始化组件最佳实践
  • ai生成ppt工具有哪些?10款主流AI生成PPT工具盘点
  • 中州养老:角色管理的角色分页查询
  • 渗透测试与网络安全审计的关系
  • (论文速读)Navigation World Models: 让机器人像人类一样想象和规划导航路径
  • MySQL主从复制之进阶延时同步、GTID复制、半同步复制完整实验流程
  • aippt自动生成工具有哪些?一文看懂,总有一款适合你!
  • Java数据结构——栈(Stack)和队列(Queue)
  • Qt---状态机框架QState