浅谈Linux 中的进程栈、线程栈、内核栈、中断栈( 二 )


可见每个任务都有自己的栈空间,正是有了独立的栈空间,为了代码重用,不同的任务甚至可以混用任务的函数体本身,例如可以一个main函数有两个任务实例 。至此之后的操作系统的框架也形成了,譬如任务在调用 sleep() 等待的时候,可以主动让出 CPU 给别的任务使用,或者分时操作系统任务在时间片用完是也会被迫的让出 CPU 。不论是哪种方法,只要想办法切换任务的上下文空间,切换栈即可 。

浅谈Linux 中的进程栈、线程栈、内核栈、中断栈

文章插图
 
Linux 中有几种栈?各种栈的内存位置?内核将栈分成四种:
  • 进程栈
  • 线程栈
  • 内核栈
  • 中断栈
一、进程栈
进程栈是属于用户态栈,和进程 虚拟地址空间 (Virtual Address Space) 密切相关 。那我们先了解下什么是虚拟地址空间:在 32 位机器下,虚拟地址空间大小为 4G 。这些虚拟地址通过页表 (Page Table) 映射到物理内存,页表由操作系统维护,并被处理器的内存管理单元 (MMU) 硬件引用 。每个进程都拥有一套属于它自己的页表,因此对于每个进程而言都好像独享了整个虚拟地址空间 。
Linux 内核将这 4G 字节的空间分为两部分,将最高的 1G 字节(0xC0000000-0xFFFFFFFF)供内核使用,称为 内核空间 。而将较低的3G字节(0x00000000-0xBFFFFFFF)供各个进程使用,称为 用户空间 。每个进程可以通过系统调用陷入内核态,因此内核空间是由所有进程共享的 。虽然说内核和用户态进程占用了这么大地址空间,但是并不意味它们使用了这么多物理内存,仅表示它可以支配这么大的地址空间 。它们是根据需要,将物理内存映射到虚拟地址空间中使用 。
浅谈Linux 中的进程栈、线程栈、内核栈、中断栈

文章插图
 
Linux 对进程地址空间有个标准布局,地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下:
- 程序段 (Text Segment):可执行文件代码的内存映射
- 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射
- BSS段 (BSS Segment):未初始化的全局变量或者静态变量(用零页初始化)
- 堆区 (Heap) : 存储动态内存分配,匿名的内存映射
- 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等
- 映射段(Memory MApping Segment):任何内存映射文件
浅谈Linux 中的进程栈、线程栈、内核栈、中断栈

文章插图
 
而上面进程虚拟地址空间中的栈区,正指的是我们所说的进程栈 。进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,Linux 内核会根据入栈情况对栈区进行动态增长(其实也就是添加新的页表) 。但是并不是说栈区可以无限增长,它也有最大限制 RLIMIT_STACK (一般为 8M),我们可以通过 ulimit 来查看或更改 RLIMIT_STACK的值 。
【扩展阅读】:如何确认进程栈的大小
我们要知道栈的大小,那必须得知道栈的起始地址和结束地址 。栈起始地址 获取很简单,只需要嵌入汇编指令获取栈指针 esp 地址即可 。栈结束地址的获取有点麻烦,我们需要先利用递归函数把栈搞溢出了,然后再 GDB 中把栈溢出的时候把栈指针 esp 打印出来即可 。代码如下:
/* file name: stacksize.c */void *orig_stack_pointer;void blow_stack() {blow_stack();}int main() {__asm__("movl %esp, orig_stack_pointer");blow_stack();return 0;}$ g++ -g stacksize.c -o ./stacksize$ gdb ./stacksize(gdb) rStarting program: /home/home/misc-code/setrlimitProgram received signal SIGSEGV, Segmentation fault.blow_stack () at setrlimit.c:44blow_stack();(gdb) print (void *)$esp$1 = (void *) 0xffffffffff7ff000(gdb) print (void *)orig_stack_pointer$2 = (void *) 0xffffc800(gdb) print 0xffffc800-0xff7ff000$3 = 8378368// Current Process Stack Size is 8M上面对进程的地址空间有个比较全局的介绍,那我们看下 Linux 内核中是怎么体现上面内存布局的 。内核使用内存描述符来表示进程的地址空间,该描述符表示着进程所有地址空间的信息 。内存描述符由 mm_struct 结构体表示,下面给出内存描述符结构中各个域的描述,请大家结合前面的 进程内存段布局 图一起看:
struct mm_struct {struct vm_area_struct *mmap;/* 内存区域链表 */struct rb_root mm_rb;/* VMA 形成的红黑树 */...struct list_head mmlist;/* 所有 mm_struct 形成的链表 */...unsigned long total_vm;/* 全部页面数目 */unsigned long locked_vm;/* 上锁的页面数据 */unsigned long pinned_vm;/* Refcount permanently increased */unsigned long shared_vm;/* 共享页面数目 Shared pages (files) */unsigned long exec_vm;/* 可执行页面数目 VM_EXEC & ~VM_WRITE */unsigned long stack_vm;/* 栈区页面数目 VM_GROWSUP/DOWN */unsigned long def_flags;unsigned long start_code, end_code, start_data, end_data;/* 代码段、数据段 起始地址和结束地址 */unsigned long start_brk, brk, start_stack;/* 栈区 的起始地址,堆区 起始地址和结束地址 */unsigned long arg_start, arg_end, env_start, env_end;/* 命令行参数 和 环境变量的 起始地址和结束地址 */.../* Architecture-specific MM context */mm_context_t context;/* 体系结构特殊数据 *//* Must use atomic bitops to access the bits */unsigned long flags;/* 状态标志位 */.../* Coredumping and NUMA and HugePage 相关结构体 */};


推荐阅读