转自:https://blog.csdn.net/freeelinux/article/details/53782986php
本文转自多个博客,以及最后有个人总结。我没有单独从头至尾写一个总结的缘由是别人已经写得很好了,我不花大量时间是没法达到这水平的。html
在多任务操做系统中,每一个进程都运行在属于本身的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块。在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(经过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,根据须要将其映射到物理内存。node
虚拟地址经过页表(Page Table)映射到物理内存,页表由操做系统维护并被处理器引用。内核空间在页表中拥有较高特权级,所以用户态程序试图访问这些页时会致使一个页错误(page fault)。在Linux中,内核空间是持续存在的,而且在全部进程中都映射到一样的物理内存。内核代码和数据老是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。linux
Linux进程在虚拟内存中的标准内存段布局以下图所示:程序员
其中,用户地址空间中的蓝色条带对应于映射到物理内存的不一样内存段,灰白区域表示未映射的部分。这些段只是简单的内存地址范围,与Intel处理器的段没有关系。数组
上图中Random stack offset和Random mmap offset等随机值意在防止恶意程序。Linux经过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以避免恶意程序经过计算访问栈、库函数等地址。execve(2)负责为进程代码段和数据段创建映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将BSS段清零。缓存
用户进程部分分段存储内容以下表所示(按地址递减顺序):数据结构
名称架构 |
存储内容app |
栈 |
局部变量、函数参数、返回地址等 |
堆 |
动态分配的内存 |
BSS段 |
未初始化或初值为0的全局变量和静态局部变量 |
数据段 |
已初始化且初值非0的全局变量和静态局部变量 |
代码段 |
可执行代码、字符串字面值、只读变量 |
在将应用程序加载到内存空间执行时,操做系统负责代码段、数据段和BSS段的加载,并在内存中为这些段分配空间。栈也由操做系统分配和管理;堆由程序员本身管理,即显式地申请和释放空间。
BSS段、数据段和代码段是可执行程序编译时的分段,运行时还须要栈和堆。
如下详细介绍各个分段的含义。
内核老是驻留在内存中,是操做系统的一部分。内核空间为内核保留,不容许应用程序读写该区域的内容或直接调用内核代码定义的函数。
栈又称堆栈,由编译器自动分配释放,行为相似数据结构中的栈(先进后出)。堆栈主要有三个用途:
持续地重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速访问。进程中的每一个线程都有属于本身的栈。向栈中不断压入数据时,若超出其容量就会耗尽栈对应的内存区域,从而触发一个页错误。此时若栈的大小低于堆栈最大值RLIMIT_STACK(一般是8M),则栈会动态增加,程序继续运行。映射的栈区扩展到所需大小后,再也不收缩。
Linux中ulimit -s命令可查看和设置堆栈最大值,当程序使用的堆栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)。注意,调高堆栈容量可能会增长内存开销和启动时间。
堆栈既可向下增加(向内存低地址)也可向上增加, 这依赖于具体的实现。本文所述堆栈向下增加。
堆栈的大小在运行时由内核动态调整。
此处,内核将硬盘文件的内容直接映射到内存, 任何应用程序均可经过Linux的mmap()系统调用或Windows的CreateFileMapping()/MapViewOfFile()请求这种映射。内存映射是一种方便高效的文件I/O方式, 于是被用于装载动态共享库。用户也可建立匿名内存映射,该映射没有对应的文件, 可用于存放程序数据。在 Linux中,若经过malloc()请求一大块内存,C运行库将建立一个匿名内存映射,而不使用堆内存。”大块” 意味着比阈值 MMAP_THRESHOLD还大,缺省为128KB,可经过mallopt()调整。
该区域用于映射可执行文件用到的动态连接库。在Linux 2.4版本中,若可执行文件依赖共享库,则系统会为这些动态库在从0x40000000开始的地址分配相应空间,并在程序装载时将其载入到该空间。在Linux 2.6内核中,共享库的起始地址被往上移动至更靠近栈区的位置。
从进程地址空间的布局能够看到,在有共享库的状况下,留给堆的可用空间还有两处:一处是从.bss段到0x40000000,约不到1GB的空间;另外一处是从共享库到栈之间的空间,约不到2GB。这两块空间大小取决于栈、共享库的大小和数量。这样来看,是否应用程序可申请的最大堆空间只有2GB?事实上,这与Linux内核版本有关。在上面给出的进程地址空间经典布局图中,共享库的装载地址为0x40000000,这其实是Linux kernel 2.6版本以前的状况了,在2.6版本里,共享库的装载地址已经被挪到靠近栈的位置,即位于0xBFxxxxxx附近,所以,此时的堆范围就不会被共享库分割成2个“碎片”,故kernel 2.6的32位Linux系统中,malloc申请的最大内存理论值在2.9GB左右。
堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。堆中内容是匿名的,不能按名字直接访问,只能经过指针间接访问。当进程调用malloc(C)/new(C++)等函数分配内存时,新分配的内存动态添加到堆上(扩张);当调用free(C)/delete(C++)等函数释放内存时,被释放的内存从堆中剔除(缩减) 。
分配的堆内存是通过字节对齐的空间,以适合原子操做。堆管理器经过链表管理每一个申请的内存,因为堆申请和释放是无序的,最终会产生内存碎片。堆内存通常由应用程序分配释放,回收的内存可供从新使用。若程序员不释放,程序结束时操做系统可能会自动回收。
堆的末端由break指针标识,当堆管理器须要更多内存时,可经过系统调用brk()和sbrk()来移动break指针以扩张堆,通常由系统自动调用。
使用堆时常常出现两种问题:1) 释放或改写仍在使用的内存(“内存破坏”);2)未释放再也不使用的内存(“内存泄漏”)。当释放次数少于申请次数时,可能已形成内存泄漏。泄漏的内存每每比忘记释放的数据结构更大,由于所分配的内存一般会圆整为下个大于申请数量的2的幂次(如申请212B,会圆整为256B)。
注意,堆不一样于数据结构中的”堆”,其行为相似链表。
【扩展阅读】栈和堆的区别 ①管理方式:栈由编译器自动管理;堆由程序员控制,使用方便,但易产生内存泄露。 ②生长方向:栈向低地址扩展(即”向下生长”),是连续的内存区域;堆向高地址扩展(即”向上生长”),是不连续的内存区域。这是因为系统用链表来存储空闲内存地址,天然不连续,而链表从低地址向高地址遍历。 ③空间大小:栈顶地址和栈的最大容量由系统预先规定(一般默认2M或10M);堆的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。 ④存储内容:栈在函数调用时,首先压入主调函数中下条指令(函数调用语句的下条可执行语句)的地址,而后是函数实参,而后是被调函数的局部变量。本次调用结束后,局部变量先出栈,而后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行下条可执行语句。堆一般在头部用一个字节存放其大小,堆用于存储生存期与函数调用无关的数据,具体内容由程序员安排。 ⑤分配方式:栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。动态分配由alloca函数在栈上申请空间,用完后自动释放。堆只能动态分配且手工释放。 ⑥分配效率:栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,所以效率较高。堆由函数库提供,机制复杂,效率比栈低得多。Windows系统中VirtualAlloc可直接在进程地址空间中分配一块内存,快速且灵活。 ⑦分配后系统响应:只要栈剩余空间大于所申请空间,系统将为程序提供内存,不然报告异常提示栈溢出。 操做系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,而后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能因为内存碎片太多),有可能调用系统功能去增长程序数据段的内存空间,以便有机会分到足够大小的内存,而后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。 此外,因为找到的堆结点大小不必定正好等于申请的大小,系统会自动将多余的部分从新放入空闲链表中。 ⑧碎片问题:栈不会存在碎片问题,由于栈是先进后出的队列,内存块弹出栈以前,在其上面的后进的栈内容已弹出。而频繁申请释放操做会形成堆内存空间的不连续,从而形成大量碎片,使程序效率下降。 可见,堆容易形成内存碎片;因为没有专门的系统支持,效率很低;因为可能引起用户态和内核态切换,内存申请的代价更为昂贵。因此栈在程序中应用最普遍,函数调用也利用栈来完成,调用过程当中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。因此,建议尽可能使用栈,仅在分配大量或大块内存空间时使用堆。 使用栈和堆时应避免越界发生,不然可能程序崩溃或破坏程序堆、栈结构,产生意想不到的后果。 |
BSS(Block Started by Symbol)段中一般存放程序中如下符号:
C语言中,未显式初始化的静态分配变量被初始化为0(算术类型)或空指针(指针类型)。因为程序加载时,BSS会被操做系统清零,因此未赋初值或初值为0的全局变量都在BSS中。BSS段仅为未初始化的静态分配变量预留位置,在目标文件中并不占据空间,这样可减小目标文件体积。但程序运行时需为变量分配内存空间,故目标文件必须记录全部未初始化的静态分配变量大小总和(经过start_bss和end_bss地址写入机器代码)。当加载器(loader)加载程序时,将为BSS段分配的内存初始化为0。在嵌入式软件中,进入main()函数以前BSS段被C运行时系统映射到初始化为全零的内存(效率较高)。
注意,尽管均放置于BSS段,但初值为0的全局变量是强符号,而未初始化的全局变量是弱符号。若其余地方已定义同名的强符号(初值可能非0),则弱符号与之连接时不会引发重定义错误,但运行时的初值可能并不是指望值(会被强符号覆盖)。所以,定义全局变量时,若只有本文件使用,则尽可能使用static关键字修饰;不然须要为全局变量定义赋初值(哪怕0值),保证该变量为强符号,以便连接时发现变量名冲突,而不是被未知值覆盖。
某些编译器将未初始化的全局变量保存在common段,连接时再将其放入BSS段。在编译阶段可经过-fno-common选项来禁止将未初始化的全局变量放入common段。
此外,因为目标文件不含BSS段,故程序烧入存储器(Flash)后BSS段地址空间内容未知。U-Boot启动过程当中,将U-Boot的Stage2代码(一般位于lib_xxxx/board.c文件)搬迁(拷贝)到SDRAM空间后必须人为添加清零BSS段的代码,而不可依赖于Stage2代码中变量定义时赋0值。
【扩展阅读】BSS历史 BSS(Block Started by Symbol,以符号开始的块)一词最初是UA-SAP汇编器(United Aircraft Symbolic Assembly Program)中的伪指令,用于为符号预留一块内存空间。该汇编器由美国联合航空公司于20世纪50年代中期为IBM 704大型机所开发。 后来该词被做为关键字引入到了IBM 709和7090/94机型上的标准汇编器FAP(Fortran Assembly Program),用于定义符号而且为该符号预留指定字数的未初始化空间块。 在采用段式内存管理的架构中(如Intel 80x86系统),BSS段一般指用来存放程序中未初始化全局变量的一块内存区域,该段变量只有名称和大小却没有值。程序开始时由系统初始化清零。 BSS段不包含数据,仅维护开始和结束地址,以便内存能在运行时被有效地清零。BSS所需的运行时空间由目标文件记录,但BSS并不占用目标文件内的实际空间,即BSS节段应用程序的二进制映象文件中并不存在。 |
数据段一般用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。
数据段保存在目标文件中(在嵌入式系统里通常固化在镜像文件中),其内容由程序初始化。例如,对于全局变量int gVar = 10,必须在目标文件数据段中保存10这个数据,而后在程序加载时复制到相应的内存。
数据段与BSS段的区别以下:
1) BSS段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。
对于大型数组如int ar0[10000] = {1, 2, 3, ...}和int ar1[10000],ar1放在BSS段,只记录共有10000*4个字节须要初始化为0,而不是像ar0那样记录每一个数据一、二、3...,此时BSS为目标文件所节省的磁盘空间至关可观。
2) 当程序读取数据段的数据时,系统会出发缺页故障,从而分配相应的物理内存;当程序读取BSS段的数据时,内核会将其转到一个全零页面,不会发生缺页故障,也不会为其分配相应的物理内存。
运行时数据段和BSS段的整个区段一般称为数据区。某些资料中“数据段”指代数据段 + BSS段 + 堆。
代码段也称正文段或文本段,一般用于存放程序执行代码(即CPU执行的机器指令)。通常C语言执行语句都编译成机器代码保存在代码段。一般代码段是可共享的,所以频繁执行的程序只须要在内存中拥有一份拷贝便可。代码段一般属于只读,以防止其余程序意外地修改其指令(对该段的写操做将致使段错误)。某些架构也容许代码段为可写,即容许修改程序。
代码段指令根据程序设计流程依次执行,对于顺序指令,只会执行一次(每一个进程);如有反复,则需使用跳转指令;若进行递归,则须要借助栈来实现。
代码段指令中包括操做码和操做对象(或对象地址引用)。若操做对象是当即数(具体数值),将直接包含在代码中;如果局部数据,将在栈区分配空间,而后引用该数据地址;若位于BSS段和数据段,一样引用该数据地址。
代码段最容易受优化措施影响。
位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常状况。
它并非一个单一的内存区域,而是对地址空间中受到操做系统保护而禁止用户进程访问的地址区域的总称。大多数操做系统中,极小的地址一般都是不容许访问的,如NULL。C语言将无效指针赋值为0也是出于这种考虑,由于0地址上正常状况下不会存放有效的可访问数据。
在32位X86架构的Linux系统中,用户进程可执行程序通常从虚拟地址空间0x08048000开始加载。该加载地址由ELF文件头决定,可经过自定义连接器脚本覆盖连接器默认配置,进而修改加载地址。0x08048000如下的地址空间一般由C动态连接库、动态加载器ld.so和内核VDSO(内核提供的虚拟共享库)等占用。经过使用mmap系统调用,可访问0x08048000如下的地址空间。
经过cat /proc/self/maps命令查看加载表以下:
【扩展阅读】分段的好处 进程运行过程当中,代码指令根据流程依次执行,只需访问一次(固然跳转和递归可能使代码执行屡次);而数据(数据段和BSS段)一般须要访问屡次,所以单独开辟空间以方便访问和节约空间。具体解释以下: 当程序被装载后,数据和指令分别映射到两个虚存区域。数据区对于进程而言可读写,而指令区对于进程只读。两区的权限可分别设置为可读写和只读。以防止程序指令被有意或无心地改写。 现代CPU具备极为强大的缓存(Cache)体系,程序必须尽可能提升缓存命中率。指令区和数据区的分离有利于提升程序的局部性。现代CPU通常数据缓存和指令缓存分离,故程序的指令和数据分开存放有利于提升CPU缓存命中率。 当系统中运行多个该程序的副本时,其指令相同,故内存中只须保存一份该程序的指令部分。若系统中运行数百进程,经过共享指令将节省大量空间(尤为对于有动态连接的系统)。其余只读数据如程序里的图标、图片、文本等资源也可共享。而每一个副本进程的数据区域不一样,它们是进程私有的。 此外,临时数据及须要再次使用的代码在运行时放入栈区中,生命周期短。全局数据和静态数据可能在整个程序执行过程当中都须要访问,所以单独存储管理。堆区由用户自由分配,以便管理。 |
(转自:Linux 中的各类栈:进程栈 线程栈 内核栈 中断栈,不过我只转了他的部份内容,感兴趣能够去看)
介绍完栈的工做原理和用途做用后,咱们回归到 Linux 内核上来。内核将栈分红四种:
进程栈是属于用户态栈,和进程 虚拟地址空间 (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
的值。
【扩展阅读】:如何确认进程栈的大小
咱们要知道栈的大小,那必须得知道栈的起始地址和结束地址。栈起始地址 获取很简单,只须要嵌入汇编指令获取栈指针 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) r Starting program: /home/home/misc-code/setrlimit Program received signal SIGSEGV, Segmentation fault. blow_stack () at setrlimit.c:4 4 blow_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 相关结构体 */ };
【扩展阅读】:进程栈的动态增加实现
进程在运行的过程当中,经过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个 缺页异常 (page fault)。经过异常陷入内核态后,异常会被内核的
expand_stack()
函数处理,进而调用acct_stack_growth()
来检查是否还有合适的地方用于栈的增加。若是栈的大小低于
RLIMIT_STACK
(一般为8MB),那么通常状况下栈会被加长,程序继续执行,感受不到发生了什么事情,这是一种将栈扩展到所需大小的常规机制。然而,若是达到了最大栈空间的大小,就会发生 栈溢出(stack overflow),进程将会收到内核发出的 段错误(segmentation fault) 信号。动态栈增加是惟一一种访问未映射内存区域而被容许的情形,其余任何对未映射内存区域的访问都会触发页错误,从而致使段错误。一些被映射的区域是只读的,所以企图写这些区域也会致使段错误。
从 Linux 内核的角度来讲,其实它并无线程的概念。Linux 把全部线程都当作进程来实现,它将线程和进程不加区分的统一到了 task_struct 中。线程仅仅被视为一个与其余进程共享某些资源的进程,而是否共享地址空间几乎是进程和 Linux 中所谓线程的惟一区别。线程建立的时候,加上了 CLONE_VM 标记,这样 线程的内存描述符 将直接指向 父进程的内存描述符。
if (clone_flags & CLONE_VM) { /* * current 是父进程而 tsk 在 fork() 执行期间是共享子进程 */ atomic_inc(¤t->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
,若是愿意,其它线程也仍是能够访问到的,因而必定要注意。
在每个进程的生命周期中,必然会经过到系统调用陷入内核。在执行系统调用陷入内核以后,这些内核代码所使用的栈并非原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称做进程内核栈。进程内核栈在进程建立的时候,经过 slab 分配器从 thread_info_cache
缓存池中分配出来,其大小为 THREAD_SIZE
,通常来讲是一个页大小 4K;
union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; };
thread_union
进程内核栈 和 task_struct
进程描述符有着紧密的联系。因为内核常常要访问 task_struct
,高效获取当前进程的描述符是一件很是重要的事情。所以内核将进程内核栈的头部一段空间,用于存放 thread_info
结构体,而此结构体中则记录了对应进程的描述符,二者关系以下图(对应内核函数为 dup_task_struct()
):
有了上述关联结构后,内核能够先获取到栈顶指针 esp,而后经过 esp 来获取 thread_info
。这里有一个小技巧,直接将 esp 的地址与上 ~(THREAD_SIZE - 1)
后便可直接得到 thread_info
的地址。因为 thread_union
结构体是从thread_info_cache
的 Slab 缓存池中申请出来的,而 thread_info_cache
在 kmem_cache_create
建立的时候,保证了地址是 THREAD_SIZE
对齐的。所以只须要对栈指针进行 THREAD_SIZE 对齐,便可得到 thread_union
的地址,也就得到了 thread_union
的地址。成功获取到 thread_info
后,直接取出它的 task 成员就成功获得了task_struct
。其实上面这段描述,也就是 current 宏的实现方法:
register unsigned long current_stack_pointer asm ("sp"); static inline struct thread_info *current_thread_info(void) { return (struct thread_info *) (current_stack_pointer & ~(THREAD_SIZE - 1)); } #define get_current() (current_thread_info()->task) #define current get_current()
进程陷入内核态的时候,须要内核栈来支持内核函数调用。中断也是如此,当系统收到中断事件后,进行中断处理的时候,也须要中断栈来支持函数调用。因为系统中断的时候,系统固然是处于内核态的,因此中断栈是能够和内核栈共享的。可是具体是否共享,这和具体处理架构密切相关。
X86 上中断栈就是独立于内核栈的;独立的中断栈所在内存空间的分配发生在 arch/x86/kernel/irq_32.c
的irq_ctx_init()
函数中(若是是多处理器系统,那么每一个处理器都会有一个独立的中断栈),函数使用 __alloc_pages
在低端内存区分配 2个物理页面,也就是8KB大小的空间。有趣的是,这个函数还会为 softirq
分配一个一样大小的独立堆栈。如此说来,softirq
将不会在 hardirq
的中断栈上执行,而是在本身的上下文中执行。
而 ARM 上中断栈和内核栈则是共享的;中断栈和内核栈共享有一个负面因素,若是中断发生嵌套,可能会形成栈溢出,从而可能会破坏到内核栈的一些重要数据,因此栈空间有时候不免会捉襟见肘。
为何须要区分这些栈,其实都是设计上的问题。这里就我看到过的一些观点进行汇总,供你们讨论:
为何须要单独的进程内核栈?
schedule()
让出 CPU;此时调度器唤醒了另外一个进程 B,碰巧进程 B 也须要系统调用进入内核态。那问题就来了,若是内核栈只有一个,那进程 B 进入内核态的时候产生的压栈操做,必然会破坏掉进程 A 已有的内核栈数据;一但进程 A 的内核栈数据被破坏,极可能致使进程 A 的内核态没法正确返回到对应的用户态了;为何须要单独的线程栈?
进程和线程是否共享一个内核栈?
dup_task_struct
来建立 task 相关结构体,而内核栈也是在此函数中 alloc_thread_info_node
出来的。所以虽然线程和进程共享一个地址空间 mm_struct
,可是并不共享一个内核栈。为何须要单独中断栈?
上面的图都很好,但我以为这张图更形象,32位进程栈大小是8M,理论上堆区最大大小约为2.9G,因此仍是蛮大的。
从上面两篇文章,我知道的线程栈是使用mmap系统调用分配的空间,可是mmap分配的系统空间是什么呢?也就是上图中的mmap区域或者说共享的内存映射区域是什么呢?它的方向是向上生长仍是向下生长的?
下面两幅图给出了答案:
图一:
图二:
因此,mmap其实和堆同样,实际上能够说他们都是动态内存分配,可是严格来讲mmap区域并不属于堆区,反而和堆区会争用虚拟地址空间。
这里要提到一个很重要的概念,内存的延迟分配,只有在真正访问一个地址的时候才创建这个地址的物理映射,这是Linux内存管理的基本思想。Linux内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并无分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是经过释放先行区,找到其对应的物理页面,将其所有释放的过程。
这篇文章关于mmap生长方向说的也挺详细的: 进程地址空间的布局(整理)
最后还有一个mmap机制的源代码分析博客,我水平暂时不够,只能看懂意思,待往后阅读内核源码再来回顾一遍:Linux用户空间线程管理介绍之二:建立线程堆栈。