1. 进程栈

进程栈是属于用户态栈,和进程虚拟地址空间 (Virtual Address Space) 密切相关。那我们先了解下什么是虚拟地址空间:在 32 位机器下,虚拟地址空间大小为 4G。这些虚拟地址通过页表 (Page Table) 映射到物理内存,页表由操作系统维护,并被处理器的内存管理单元 (MMU) 硬件引用。每个进程都拥有一套属于它自己的页表,因此对于每个进程而言都好像独享了整个虚拟地址空间。

Linux 内核将这 4G 字节的空间分为两部分,将最高的 1G 字节(0xC0000000-0xFFFFFFFF)供内核使用,称为 内核空间。而将较低的3G字节(0x00000000-0xBFFFFFFF)供各个进程使用,称为 用户空间。每个进程可以通过系统调用陷入内核态,因此内核空间是由所有进程共享的。虽然说内核和用户态进程占用了这么大地址空间,但是并不意味它们使用了这么多物理内存,仅表示它可以支配这么大的地址空间。它们是根据需要,将物理内存映射到虚拟地址空间中使用。

Linux 对进程地址空间有个标准布局,地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下:

  • 程序段 (Text Segment):可执行文件代码的内存映射
  • 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射
  • BSS段 (BSS Segment):未初始化的全局变量或者静态变量(用零页初始化)
  • 堆区 (Heap) : 存储动态内存分配,匿名的内存映射
  • 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等
  • 映射段(Memory Mapping Segment):任何内存映射文件

而上面进程虚拟地址空间中的栈区,正指的是我们所说的进程栈。进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,Linux 内核会根据入栈情况对栈区进行动态增长(其实也就是添加新的页表)。但是并不是说栈区可以无限增长,它也有最大限制 RLIMIT_STACK (一般为 8M),我们可以通过 ulimit 来查看或更改 RLIMIT_STACK 的值。


进程栈的动态增长实现

进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个 缺页异常 (page fault)。通过异常陷入内核态后,异常会被内核的 expand_stack() 函数处理,进而调用 acct_stack_growth() 来检查是否还有合适的地方用于栈的增长。

如果栈的大小低于 RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情,这是一种将栈扩展到所需大小的常规机制。然而,如果达到了最大栈空间的大小,就会发生 栈溢出(stack overflow),进程将会收到内核发出的 段错误(segmentation fault) 信号。

动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

2. 线程栈

从 Linux 内核的角度来说,其实它并没有线程的概念。Linux 把所有线程都当做进程来实现,它将线程和进程不加区分的统一到了 task_struct 中。线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎是进程和 Linux 中所谓线程的唯一区别。线程创建的时候,加上了 CLONE_VM 标记,这样 线程的内存描述符 将直接指向 父进程的内存描述符

if (clone_flags & CLONE_VM) {/** current 是父进程而 tsk 在 fork() 执行期间是共享子进程*/atomic_inc(&current->mm->mm_users);tsk->mm = current->mm;}

虽然线程的地址空间和进程一样,但是对待其地址空间的 stack 还是有些区别的。对于 Linux 进程或者说主线程,其 stack 是在 fork 的时候生成的,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝 (cow) 以及动态增长。然而对于主线程生成的子线程而言,其 stack 将不再是这样的了,而是事先固定下来的,使用 mmap 系统调用(实际上是进程的堆的一部分),它不带有 VM_STACK_FLAGS 标记。这个可以从 glibc 的nptl/allocatestack.c 中的 allocate_stack() 函数中看到:点击(此处)折叠或打开

mem = mmap (NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

由于线程的 mm->start_stack 栈地址和所属进程相同,所以线程栈的起始地址并没有存放在 task_struct 中,应该是使用 pthread_attr_t 中的 stackaddr 来初始化 task_struct->thread->sp(sp 指向 struct pt_regs 对象,该结构体用于保存用户进程或者线程的寄存器现场)。这些都不重要,重要的是,线程栈不能动态增长,一旦用尽就没了,这是和生成进程的 fork 不同的地方。由于线程栈是从进程的地址空间中 map 出来的一块内存区域,原则上是线程私有的。但是同一个进程的所有线程生成的时候浅拷贝生成者的 task_struct 的很多字段,其中包括所有的 vma,如果愿意,其它线程也还是可以访问到的,于是一定要注意。
3. 进程栈和线程栈大小的调整
进程和线程的栈分别是多大呢?首先从我们熟悉的ulimit -s说起,熟悉linux的人都应该知道通过ulimit -s可以修改栈的大小,除此之外还有getrlimit/setrlimit两个函数:

int getrlimit(int resource, struct rlimit *rlim);int setrlimit(int resource, const struct rlimit *rlim);

这两个函数当第一个参数传入RLIMIT_STACK时,可以设置和获取栈的大小,其作用和ulimit -s是一样的,只是单位不同,ulimit -s的单位是kB,而这两个函数的单位是B(字节),详细使用方法请参考man手册。
最后还有线程的pthread_attr_setstacksize/pthread_attr_getstacksize。使用setrlimit和使用ulimit -s设置栈大小效果相同,这两种方式都是针对进程栈大小设置,只不过前者只真对当前进程,后者针对当前shell;
而线程栈大小的关系就相对比较复杂点,前文说过线程大小是静态的,是在创建时就确定了的,当然如果使用pthread_attr_setstacksize可以在创建线程时指定线程栈大小,但如果不指定线程栈的话其默认大小是什么情况呢?想要了解线程栈的大小就要看glibc的线程创建函数,具体就是pthread_create->__pthread_create_2_1->allocate_stack。具体代码还是比较复杂的,这里简化为一个伪代码:

limit = getlimit(RLIMIT_STACK)
if (limit == RLIMIT_INFINITY)thread.rlimit = ARCH_STACK_DEFAULT_SIZE //2Melse if thread.rlimit < PTHREAD_STACK_MIN //16kthread.rlimit = PTHREAD_STACK_MIN

可以看出,线程默认栈大小和进程栈大小的关系:

  1. 如果ulimit(setrlimit)设置大小大于16k,则线程栈默认大小由ulimit(setrlimit)决定;
  2. 如果ulimit(setrlimit)设置大小小于16k,则线程栈默认大小为16;
  3. 如果ulimit(setrlimit)设置大小为无限制,则线程栈默认大小为2M;

所以我们如果使用ulimit设置进程栈大小是无限大其实栈大小反而相对比较小,这是为什么呢?前面我们已经讲过线程栈和进程栈的位置不同,线程栈其实是在进程的堆上分配的,并且不会动态增加,所以不可能设置一个无限大小的线程栈。

最后,我们再对进程栈和线程栈做一下总结和说明:

  1. ulimit -s决定进程栈的大小,但不是严格相等(实际测试稍大于ulimit -s设置);
  2. 创建线程时如果通过pthread_attr_setstacksize设置了线程栈大小,则使用该属性创建的线程栈大小就为其设置的值,但不影响线程默认属性的栈大小值,也不影响ulimit -s的值。
  3. 线程一旦创建就无法在修改其栈大小了,即使使用setrlimit。
  4. pthread_attr_setstacksize/pthread_attr_getstacksize的作用是获取和设置线程属性中的栈大小的,而不获取设置线程栈大小的。可以再创建前设置好线程属性,这样使用该属性创建线程就能影响线程的栈大小了。但通过pthread_attr_init,pthread_attr_getstacksize是无法获取当前线程栈大小的,只能获取默认属性的线程栈大小,其值未必就是当前线程栈大小。

以上有不足的地方欢迎指出讨论,觉得不错的朋友希望能得到您的转发支持,同时可以持续关注我,每天分享Linux C/C++后台开发干货内容!

最后,如果觉得学习资料难找的话,可以添加小编的 LinuxC/C++交流群 ,期待你的加入~

栈空间_Linux中的进程栈和线程栈相关推荐

  1. python进程池和线程池_Python中的进程池与线程池(包含代码)

    引入进程池与线程池 使用ProcessPoolExecutor进程池,使用ThreadPoolExecutor 使用shutdown 使用submit同步调用 使用submit异步调用 异步+回调函数 ...

  2. 进程栈大小 与 线程栈大小-转

    我在FC3,gcc3.4.2环境下进行该实验,证明线程的栈确实有固定大小,也就是ulimit -a显示的那个值,在我的实验室环境下为10M字节  实验1:  #include <stdio.h& ...

  3. 【编程基础】堆空间与栈空间

    在 C 语言中,内存分布的部分情况如下图所示: 有些部分并没有在图中表示出来,实际上内存分布的功能划分从高地址到低地址依次是: 内核空间:应用程序不允许访问的部分,只能由内核进行操作,操作系统的内核程 ...

  4. 栈空间和堆空间的区别

    栈空间用于存储函数参数和局部变量,所需空间由系统自动分配,回收也由系统管理,无需人工干预:堆空间用于存储动态分配的内存块,分配和释放空间均由程序员控制,有可能产生内存泄漏. 栈空间作为一个严格后进先出 ...

  5. 栈在前端中的应用,顺便再了解下深拷贝和浅拷贝!

    详解栈在前端中的应用 一.栈是什么 二.栈的应用场景 三.前端与栈:深拷贝与浅拷贝 1.JS数据类型 (1)js数据类型的分类 (2)js数据类型的定义和存储方式 (3)js数据类型的判断方式 2.深 ...

  6. [笔记]Windows核心编程《十六》线程栈

    系列文章目录 [笔记]Windows核心编程<一>错误处理.字符编码 [笔记]Windows核心编程<二>内核对象 [笔记]Windows核心编程<三>进程 [笔记 ...

  7. linux线程踩栈,Linux线程栈内存总结

    何为线程栈空间泄露? 何为anon内存? 虚拟内存.物理内存.anon内存的联系 anon与线程的联系 glibc源码库线程创建与销毁anon关系 使用pmap分析虚拟地址空间以及anon内存 何为线 ...

  8. 【Java 虚拟机原理】线程栈 | 栈帧 | 局部变量表 | 反汇编字节码文件 | Java 虚拟机指令手册 | 程序计数器

    文章目录 一.线程栈 二.栈帧 三.栈帧 - 局部变量表 四.反汇编字节码文件 五.Java 虚拟机指令手册 六.程序计数器 一.线程栈 装载 HelloWorld.class 字节码文件到 Java ...

  9. 类型,对象,线程栈和托管堆在运行时的相互关系(一)。

    当系统加载一个CLR的进程,进程里面可能有多个线程,这时候系统会给这个进程创建一个大小为1M的线程栈.这个线程栈用来存放方法调用的实参,和方法内部定义的局部变量.下图展示了一个线程栈的栈内存.线程栈的 ...

最新文章

  1. 【数据结构】最小生成树 Prim算法 Kruskal算法
  2. 神策 2020 数据驱动用户大会主会场亮点回顾(内附回放)
  3. 【图像】jpg与jpeg的区别
  4. quartz工程容器启动与 Service注入
  5. Node.js CLI 工具最佳实践
  6. 前端笔记-vue中使用router进行页面跳转及除掉url中的#
  7. 折半查找(非递归与递归实现)
  8. mysql的-x是什么命令_Mysql常用命令
  9. UVA 10917 Walk Through the Forest 最短路 + DP
  10. Windows 10 下载官方正版ISO镜像文件
  11. 求x的n次方编程_C++编程X的N次方程序
  12. html css 忽略,HTML与CSS中易被忽略的基础知识点
  13. 为什么计算机学硕人那么少,考研分数看似不高,为什么考上的人相对很少?总结得很精辟...
  14. 计【思考】如不解决这些问题,山东招远这类事情以后仍然会不断发生!!
  15. 马云:大数据时代_最重要的是做最好的自己
  16. 一起赚美元⑤ | Instapainting照片转油画服务,通过SEO优化,每月赚取3.2万美元的真实案例
  17. sqlite developer注册码
  18. Cartographer + Navigation 建图导航(个人定制版)
  19. html设定列的最小宽度,设置Grid Layout列最小宽度的方法
  20. html排列图片,css3+html实现微信朋友圈不同尺寸图片排列预览功能

热门文章

  1. lucene 增加相关性_事务性Lucene
  2. 坚实原则:单一责任原则
  3. 非静态方法可以访问Java中的静态变量/方法吗?
  4. java 使用jasper_使用Jasper Reports以Java创建报告
  5. couchbase_Couchbase:使用Twitter和Java创建大型数据集
  6. jms 如何测试_使用JMSTester对JMS层进行基准测试
  7. java 枚举内嵌枚举_Java枚举益智游戏
  8. 功能项目拼图将Java 9引入
  9. Apache Camel 2.14中的更多指标
  10. InterruptedException和中断线程的说明