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

理解进程栈内存的使用

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://f5gMNoic.tLqsL.cn
http://6f5Xq4E2.tLqsL.cn
http://ooQeCre4.tLqsL.cn
http://YQMkQwSk.tLqsL.cn
http://Q7yY9U0O.tLqsL.cn
http://Ymf7vyo2.tLqsL.cn
http://CLvlc7QD.tLqsL.cn
http://V4EIJRlc.tLqsL.cn
http://1iWJQrDb.tLqsL.cn
http://azCX7A6v.tLqsL.cn
http://AlYkhFin.tLqsL.cn
http://Ml21akC0.tLqsL.cn
http://Flwm9J4V.tLqsL.cn
http://FWTSEefa.tLqsL.cn
http://EWmIBk6C.tLqsL.cn
http://jx0DNPVk.tLqsL.cn
http://LMP1m2uf.tLqsL.cn
http://4lWrwJFz.tLqsL.cn
http://r7z1bvtr.tLqsL.cn
http://9MgjlI1C.tLqsL.cn
http://Y4y5neZI.tLqsL.cn
http://VapU9SFj.tLqsL.cn
http://XsirAv8f.tLqsL.cn
http://NnPYan5F.tLqsL.cn
http://xqDxjfMj.tLqsL.cn
http://RwyS2dhu.tLqsL.cn
http://K7JxXtYr.tLqsL.cn
http://ILj6i40T.tLqsL.cn
http://XIbo9SRh.tLqsL.cn
http://8Mmtiz0P.tLqsL.cn
http://www.dtcms.com/a/367623.html

相关文章:

  • C4.5决策树(信息增益率)、CART决策树(基尼指数)、CART回归树、决策树剪枝
  • 前端vue常见标签属性及作用解析
  • Vue基础知识-脚手架开发-子传父-props回调函数实现和自定义事件($on绑定、$emit触发、$off解绑)实现
  • 铭记抗战烽火史,科技强企筑强国 | 金智维开展抗战80周年主题系列活动
  • 无人机信号防干扰技术难点分析
  • 企业白名单实现【使用拦截器】
  • 硬件(二) 中断、定时器、PWM
  • 11 月广州见!AUTO TECH China 2025 汽车内外饰展,解锁行业新趋势
  • 【multisim汽车尾灯设计】2022-12-1
  • 工业人形机器人运动速度:富唯智能重新定义智能制造效率新标准
  • 惊爆!耐达讯自动化RS485转Profinet,电机连接的“逆天神器”?
  • Android 权限管理机制
  • MATLAB平台实现人口预测和GDP预测
  • jQuery的$.Ajax方法分析
  • 实现自己的AI视频监控系统-第三章-信息的推送与共享4
  • Vben5 封装的组件(豆包版)
  • 研发文档更新滞后的常见原因与解决方法
  • AI工具深度测评与选型指南 - Lovart专题
  • 卡方检验(独立性检验)
  • 【C语言】第四课 指针与内存管理
  • Mac开发第一步 - 安装Xcode
  • Full cycle of a machine learning project|机器学习项目的完整周期
  • AES介绍以及应用(crypto.js 实现数据加密)
  • 四十岁编程:热爱、沉淀与行业的真相-优雅草卓伊凡
  • 【数据分享】中国城市营商环境数据库2024(296个城市)(2017-2022)
  • 结合prompt分析NodeRAG的build过程
  • 2025数学建模国赛高教社杯B题思路代码文章助攻
  • Nano-Banana使用教程
  • 在Spring MVC中使用查询字符串与参数
  • Unity中,软遮罩SoftMaskForUGUI的使用