大三暑假,送室友坐高铁,临上车前,我说:“我去买几个橘子,你就站在此地,不要走动。
室友愣了一下,说:“你他妈什么时候都不忘占我便宜。

上面这个栈便宜的段子,您看懂了吗?
上一次笨叔点滴里和大家瞎聊了啥是栈,那今天和大家聊聊那些具体一点的栈。
01 内核栈
我们先来看看一道题目。
这道题目出题老师太有水平了,什么叫做系统栈?
每个进程在生命周期里都有自己的栈,内核在fork孵化一个新的进程时,也就是在创建进程控制块task_struct数据结构的同时,也为进程创建自己栈。
一个进程有2个栈,用户栈和系统栈;用户栈的空间指向用户地址空间,内核栈的空间指向内核地址空间。进程内核栈在进程创建的时候,通过 slab 分配器从 thread_info_cache 缓存池中分配出来,其大小为 THREAD_SIZE,一般8KB大小。内核栈见笨叔画的下面的这个图。
当进程在用户态运行时,CPU栈指针寄存器 (SP)指向的 用户栈地址,使用用户堆栈;当进程运行在内核态时,CPU栈指针寄存器指向的是内核栈空间地址,使用的是内核栈。
当一个进程的时间片用完或者被其他高优先级的进程抢占时候,进程需要让出CPU给其他进程运行,内核需要进行进程切换。
Linux 的进程切换是通过调用函数进程切换函数schedule来实现的。进程切换主要分为2个步骤:
1. 调用switch_mm()函数进行进程页表的切换。
2. 调用 switch_to() 函数进行 CPU寄存器切换,调用的过程见笨叔画的下面的图。
当进程A切换到进程B的时候,需要把当前上下文保存到进程A的内核栈空间里,然后让CPU的SP寄存器指向进程B的内核栈,这样就完成了栈的切换。
这时候你会问,为啥子 内核栈不用 全局的一个,而是每个进程一个呢? 这不是浪费空间嘛?大家可以在评论区留言说出您的答案。
02 进程栈
进程运行在用户态时候,也需要一个栈,这个就是用户栈了。下面看一下笨叔画的这个图。
一个简单的test进程跑起来之后,整个用户空间的布局情况见上图所示。以32位处理器的Linux内核来说,用户空间通常是在0 ~ 3GB这个范围,用户栈是在用户空间的最上面的,栈下面是mmap映射区域,mmap映射区域是从1GB到 栈。那么从数据段结束的地方到1GB是堆的区域。
地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下: 
  1. 程序段 (Text Segment):可执行文件代码的内存映射 
  2. 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射 
  3. BSS段 (BSS Segment):未初始化的全局变量或者静态变量(用零页初始化) 
  4. 堆区 (Heap) : 存储动态内存分配,匿名的内存映射 
  5. 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等 
  6. MMAP段(Memory Mapping Segment):任何内存映射文件
每个进程运行之后,在/proc/pid/maps这个节点会列出当前进程的映射情况。例如查看进程pid为788的进程的映射情况。
/ # cat /proc/788/maps
00010000-00087000 r-xp00000000 00:02 7466       /test.elf
00096000-00098000 rw-p00076000 00:02 7466       /test.elf
00098000-000bb000 rw-p00000000 00:00 0          [heap]
bebd5000-bebf6000 rw-p00000000 00:00 0          [stack]
bec3f000-bec40000 r-xp00000000 00:00 0          [sigpage]
ffff0000-ffff1000 r-xp00000000 00:00 0          [vectors]
/ #
另外/proc/pid/smaps节点会提供更多的一些映射的细节。比如以代码段的VMA和堆的VMA为例:
     / # cat /proc/788/smaps
笨叔的VIP群里有人问:进程栈的VMA是什么开始创建的?有兴趣的同学可以在评论区留言。
进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,Linux 内核会根据入栈情况对栈区进行动态增长。但是并不是说栈区可以无限增长,它也有最大限制 RLIMIT_STACK (一般为 8M),我们可以通过 ulimit 来查看或更改 RLIMIT_STACK 的值。
进程在运行的过程通过不断向栈区压入数据,当超出栈区容量时,会触发一个 缺页异常 。在缺页异常的处理过程,调用 expand_stack() 来处理用户栈的问题。在__do_page_fault()函数里判断即可,因为栈是从高地址往下生长,当新地址小于VMA->start的时候,就应该去判断是不是用户栈的问题了。
下面是一种某某老外画的经典的一个图,其实想表达的意思是,mm_struct里有很多成员来表示栈,mmap,堆,BBS段,数据段,代码段等等这些区域的地方,帮助我们理解mm。
03 线程栈
Linux内核没有区分进程还是线程,都统一使用task_struct数据结构来描述,但是在内存空间上是有区别的。线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎是进程和 Linux 中所谓线程的唯一区别。线程创建的时候,加上了 CLONE_VM 标记,这样 线程的内存描述符 将直接指向 父进程的内存描述符。
我们使用pthread库来创建线程的时候,最终调用到clone()这个系统调用。
参数列表中第二个参数void *child_stack,用来指定线程的用户栈的地址空间。因此,调用clone()的时候是要自己提供子task的栈空间的, 它是在pthread_handle_create() ->pthread_allocate_stack()函数里使用mmap接口来分配栈空间的。所以,pthread_create()创建线程时,若不指定分配堆栈大小,系统会分配默认值,可以使用ulimit -a命令来查看。
04 git课程
最后和大家介绍笨叔最新搞的一个关于git的课程,如果大家对如下git使用问题都很熟悉的话,那就没有必要看笨叔的事情。如果还有点模棱两可,别吝啬一个汉堡的钱哟!
来吧,一起玩git,只要99, vim+git 超级套餐!点击阅读原文,进入笨叔的淘宝店。
大家踊跃对上面笨叔留下的疑问发表自己观点,笨叔从精彩评论中送“奔跑吧Linux内核”珍藏版T Shirt。
继续阅读
阅读原文