Linux virtual address space layout and process and thread stack summary

Transfer: https://blog.csdn.net/freeelinux/article/details/53782986

This switched more than one blog, and finally there is my conclusion. I'm not alone from start to finish writing a summary of the reasons is that people have been very well written, I do not spend a lot of time is not reached this level.

 

A: Linux virtual address space layout

 

 In a multitasking operating system, each process runs in its own memory sandbox. The sandbox is the virtual address space (Virtual Address Space), in 32-bit mode, it is a block of memory address of 4GB. In the Linux system, the proportion of the virtual memory kernel and user processes share is 1: 3, and Windows systems is 2: 2 (by setting the Large-Address-Aware Executables mark may be 1: 3). This does not mean that much physical memory used by the kernel, saying only that it can dominate this part of the address space needed to be mapped to physical memory.

     Virtual addresses to physical memory by mapping the page table (Page Table), a page table maintained by the operating system and processor reference. Kernel space has a higher privilege level in the page table, so the user mode application attempts to access these pages can result in a page error (page fault) time. In Linux, kernel space is persistent, and are mapped to the same physical memory in all processes. Kernel code and data is always addressable, ready to handle interrupts and system calls. In contrast, user mode address space mapped with the process switching occurs constantly changing.

     Linux process standard memory segment in virtual memory layout as shown below:

     Wherein the user address space of the blue band mapped to physical memory corresponding to different memory segments, gray area represents the un-mapped parts. These segments are simply memory address range, there is no relationship with the segment Intel processors.

     Random stack offset figure above and other Random Random mmap offset value is intended to prevent a malicious program. Linux through the stack, memory mapping segment, the start address of the heap plus the random offset to disrupt the layout to avoid malicious programs by accessing the address calculation stack, library functions and the like. execve (2) is responsible for the process of code and data segments map the real content of the code and data segments are read into memory by the missing page exception handler system demand complete. In addition, execve (2) BSS segment will also be cleared.

     Stored content segment to a user process shown in the following table (in descending address order):

name

Memory contents

Stack

Local variables, function arguments, return address

stack

Dynamically allocated memory

BSS segment

The initial value of 0 or uninitialized global variables and static local variables

Data segment

Initialized and initial value is non-zero global variables and static local variable

Snippet

Executable code, string literals, variables readonly

     When the application is loaded into memory space to perform, is responsible for loading the operating system code segment, data segment and BSS segments, and to allocate space for these segments in memory. Also by the operating system stack allocation and management; stack is managed by the programmer, i.e., application and release explicit space.

     The BSS, data and code segments are segmented executable program when compiled, runtime also need to stack and heap.

 

     The following detailed description of the meaning of each segment.

 

1 kernel space

     Kernel always resides in memory, is part of the operating system. Kernel space reserved for the kernel, does not allow applications to read and write the contents of the core region or function code defines the immediate caller.

 

2 Stack (Stack)

     Stack also known as stack allocation is released automatically by the compiler, it behaves like a stack data structure (last out). Stack three main purposes:

  • Provide storage space for non-static local variable declared inside a function (C language called "automatic variables").
  • Record function call associated maintenance information, called a stack frame (Stack Frame) process or activity record (Procedure Activation Record). It includes the return address of the function, the function does not fit into the parameter register and store some register values. In addition to the recursive call, the stack is not necessary. It is known as local variables, parameters, and returns the required space, and assign them to the BSS segment address compile time.
  • Temporary storage portion for temporarily storing the results of arithmetic expressions long or alloca () function to allocate memory for the stack inside.

     Continued reuse stack space helps to stack memory remains active in the CPU cache, thus speeding up access. Process each thread has its own stack. When the stack is pressed into the continuous data, if beyond its capacity will run out of memory areas corresponding to the stack, thereby triggering a page fault. In this case, if the stack is below the maximum stack size RLIMIT_STACK (usually 8M), the stack grows dynamically, the program continues to run. Mapping the stack area extended to a required size, no shrinkage.

     Linux, ulimit -s command to view and set the maximum value of the stack, when the program uses this value exceeds stack, stack overflow occurs (Stack Overflow), receive a program segment error (Segmentation Fault). Note that the increase in stack size may increase the memory overhead and startup time.

     Growth can also be down stack grows upwards (towards lower memory addresses), depending on the particular implementation. The stack grows downwards herein.

     Adjust the size of the stack dynamically by the kernel at run time.

 

Memory mapping section 3 (the mmap)

     Here, the contents of the hard core file directly mapped into memory, any application can request this by mapping the Linux mmap () system call, or Windows CreateFileMapping () / MapViewOfFile (). Memory mapping is a convenient and efficient file I / O mode, which is used to load the dynamic shared libraries. Users can also create an anonymous memory mapping that has no corresponding file can be used to store program data. In Linux, if () requests a chunk of memory with malloc, C runtime will create an anonymous memory mapping, without the use of heap memory. "Chunk" means that the threshold value is greater than the MMAP_THRESHOLD defaults to 128KB, can () adjusted by mallopt.

     The area used to map the dynamic link library executables used. In versions of Linux 2.4, if the dependent shared library executable file, the system will assign the corresponding address space from 0x40000000 to start these dynamic libraries, and loads the program be loaded into this space. In the Linux 2.6 kernel, the start address of the shared library is moved up to a position closer to the stack area.

     Can be seen from the layout of the process address space, in case there is a shared library, the heap of free space left there are two: one is from the .bss section to 0x40000000, less than about 1GB of space; another is from the shared library into the space between the stacks, about less than 2GB. These two space depends on the size of the stack, the size and number of shared libraries. This point of view, if the maximum heap space applications can apply only 2GB? In fact, this is related to the Linux kernel version. In the process address space given above classic layout view, shared library load addresses 0x40000000, which is actually the case before the Linux kernel 2.6 version, version 2.6, the load address of the shared library has been moved close to the stack the maximum theoretical value of the memory location, i.e. located 0xBFxxxxxx, therefore, the scope of the stack in this case will not be shared library is divided into two "chips", so-bit Linux kernel 32 of the system 2.6, malloc application is about 2.9GB .

 

4 Heap (heap)

     Dynamically allocated heap memory segments used to store the process is running, dynamically expand or shrink. Heap content is anonymous and can not be accessed directly by name, it can only be accessed indirectly through a pointer. When a process calls malloc when (C) / new (C ++) functions such as memory allocation, memory is dynamic new allocation is added to the heap (expansion); When you call free (C) / delete (C ++) and other functions free the memory, is released memory from the heap removed (cut).

     Allocated heap memory is byte-aligned spatially to suit atomic operation. The heap manager to manage each application through the list of memory, due to the application and release heap is disordered, eventually fragment memory. General release heap memory allocated by the application, the recovery of memory available for re-use. If the programmer does not release the operating system may automatically recover at the end of the program.

     By the end of the stack pointer identifies break, when the stack manager needs more memory, the pointer can be moved break call brk () and the sbrk () to expand the stack through the system, generally called automatically by the system.

     Often if the heap both of these problems: memory ( "memory corruption") 1) release or rewrite still in use; 2) does not release memory ( "memory leaks") no longer in use. When released fewer than the number of applications may have been caused by a memory leak. Memory leaks often forget to release than the larger data structure, because the allocated memory is usually rounded to the next larger than the number of applications of the power of 2 (e.g., application 212B, will be rounded to 256B).

     Note that the heap data structure is different from the "stack", which behaves like a linked list.

[Further reading] the difference between stack and heap

① management: Stack managed automatically by the compiler; stack controlled by a programmer, easy to use, easy to produce but a memory leak.

② Growth Direction: Stack extended to the low address (i.e., "down growth"), contiguous area of ​​memory; stack to a high address extension (i.e., "grow up"), discontinuous regions of memory. This is because the system used to store the linked list of free memory address, a discontinuous nature, and the list traversal from lower to higher addresses.

③ space: the maximum capacity of the stack and stack address specified by the system in advance (usually the default or 1OM 2M); size of the heap is effectively limited to a computer system virtual memory, 32-bit Linux system heap memory up to 2.9G space.

④ stored content: function call stack, entered the first pressure adjusting function of the next instruction (the function call statement article executable statement) address and a function argument, then the local variable called function. After this call, first-out stack of local variables, then the parameters of the last stack pointer points to the beginning address of the instruction memory, the program continues to run from this point article executable statement. Stack typically stored in a single byte header size, bulk data storage for the survival of the independent function call, the specific content arranged by the programmer.

⑤ allocation: stack can be allocated statically or dynamically allocated. Static assignment is done by the compiler, such as the allocation of local variables. Alloca function for the dynamic allocation of space on the stack, after use is automatically released. Dynamic allocation can only heap and manual release.

⑥ allocation efficiency: stack support provided by the underlying computer: allocating a dedicated register holds the address of the stack, the stack is performed by a dedicated push instruction, and therefore higher efficiency. Heap provided by the library, complex mechanisms, efficiency is much lower than the stack. VirtualAlloc Windows system can allocate a block of memory in the process address space direct, fast and flexible.

After ⑦ distribution system response: As long as the application stack space is greater than the remaining space, the system will provide program memory, otherwise, an exception in the stack overflow.

     操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。

     此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。

⑧碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。

     可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆。

     使用栈和堆时应避免越界发生,否则可能程序崩溃或破坏程序堆、栈结构,产生意想不到的后果。

 

5 BSS段

     BSS(Block Started by Symbol)段中通常存放程序中以下符号:

  • 未初始化的全局变量和静态局部变量
  • 初始值为0的全局变量和静态局部变量(依赖于编译器实现)
  • 未定义且初值不为0的符号(该初值即common block的大小)

     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节段应用程序的二进制映象文件中并不存在。

 

6 数据段(Data)

     数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。

     数据段保存在目标文件中(在嵌入式系统里一般固化在镜像文件中),其内容由程序初始化。例如,对于全局变量int gVar = 10,必须在目标文件数据段中保存10这个数据,然后在程序加载时复制到相应的内存。

     数据段与BSS段的区别如下: 

     1) BSS段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。

     对于大型数组如int ar0[10000] = {1, 2, 3, ...}和int ar1[10000],ar1放在BSS段,只记录共有10000*4个字节需要初始化为0,而不是像ar0那样记录每个数据1、2、3...,此时BSS为目标文件所节省的磁盘空间相当可观。

     2) 当程序读取数据段的数据时,系统会出发缺页故障,从而分配相应的物理内存;当程序读取BSS段的数据时,内核会将其转到一个全零页面,不会发生缺页故障,也不会为其分配相应的物理内存。

     运行时数据段和BSS段的整个区段通常称为数据区。某些资料中“数据段”指代数据段 + BSS段 + 堆。

 

7 代码段(text)

     代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。一般C语言执行语句都编译成机器代码保存在代码段。通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。某些架构也允许代码段为可写,即允许修改程序。

     代码段指令根据程序设计流程依次执行,对于顺序指令,只会执行一次(每个进程);若有反复,则需使用跳转指令;若进行递归,则需要借助栈来实现。

     代码段指令中包括操作码和操作对象(或对象地址引用)。若操作对象是立即数(具体数值),将直接包含在代码中;若是局部数据,将在栈区分配空间,然后引用该数据地址;若位于BSS段和数据段,同样引用该数据地址。

     代码段最容易受优化措施影响。

 

8 保留区

     位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。

     它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称。大多数操作系统中,极小的地址通常都是不允许访问的,如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 中的各种栈:进程栈 线程栈 内核栈 中断栈,不过我只转了他的部分内容,感兴趣可以去看)

 

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) 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 相关结构体 */
};

 

mm_struct 内存段

【扩展阅读】:进程栈的动态增长实现

进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个 缺页异常 (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(&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,如果愿意,其它线程也还是可以访问到的,于是一定要注意。

三、进程内核栈

在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈。进程内核栈在进程创建的时候,通过 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 上中断栈和内核栈则是共享的;中断栈和内核栈共享有一个负面因素,如果中断发生嵌套,可能会造成栈溢出,从而可能会破坏到内核栈的一些重要数据,所以栈空间有时候难免会捉襟见肘。


 

Linux 为什么需要区分这些栈?

为什么需要区分这些栈,其实都是设计上的问题。这里就我看到过的一些观点进行汇总,供大家讨论:

  1. 为什么需要单独的进程内核栈?

    • 所有进程运行的时候,都可能通过系统调用陷入内核态继续执行。假设第一个进程 A 陷入内核态执行的时候,需要等待读取网卡的数据,主动调用 schedule() 让出 CPU;此时调度器唤醒了另一个进程 B,碰巧进程 B 也需要系统调用进入内核态。那问题就来了,如果内核栈只有一个,那进程 B 进入内核态的时候产生的压栈操作,必然会破坏掉进程 A 已有的内核栈数据;一但进程 A 的内核栈数据被破坏,很可能导致进程 A 的内核态无法正确返回到对应的用户态了;
  2. 为什么需要单独的线程栈?

    • Linux 调度程序中并没有区分线程和进程,当调度程序需要唤醒”进程”的时候,必然需要恢复进程的上下文环境,也就是进程栈;但是线程和父进程完全共享一份地址空间,如果栈也用同一个那就会遇到以下问题。假如进程的栈指针初始值为 0x7ffc80000000;父进程 A 先执行,调用了一些函数后栈指针 esp 为 0x7ffc8000FF00,此时父进程主动休眠了;接着调度器唤醒子线程 A1: 
      • 此时 A1 的栈指针 esp 如果为初始值 0x7ffc80000000,则线程 A1 一但出现函数调用,必然会破坏父进程 A 已入栈的数据。
      • 如果此时线程 A1 的栈指针和父进程最后更新的值一致,esp 为 0x7ffc8000FF00,那线程 A1 进行一些函数调用后,栈指针 esp 增加到 0x7ffc8000FFFF,然后线程 A1 休眠;调度器再次换成父进程 A 执行,那这个时候父进程的栈指针是应该为 0x7ffc8000FF00 还是 0x7ffc8000FFFF 呢?无论栈指针被设置到哪个值,都会有问题不是吗?
  3. 进程和线程是否共享一个内核栈?

    • No,线程和进程创建的时候都调用 dup_task_struct 来创建 task 相关结构体,而内核栈也是在此函数中 alloc_thread_info_node 出来的。因此虽然线程和进程共享一个地址空间 mm_struct,但是并不共享一个内核栈。
  4. 为什么需要单独中断栈?

    • 这个问题其实不对,ARM 架构就没有独立的中断栈。

 

三:自己的总结

 

上面的图都很好,但我觉得这张图更形象,32位进程栈大小是8M,理论上堆区最大大小约为2.9G,所以还是蛮大的。

从上面两篇文章,我知道的线程栈是使用mmap系统调用分配的空间,但是mmap分配的系统空间是什么呢?也就是上图中的mmap区域或者说共享的内存映射区域是什么呢?它的方向是向上生长还是向下生长的?

下面两幅图给出了答案:

 

图一:

图二:

所以,mmap其实和堆一样,实际上可以说他们都是动态内存分配,但是严格来说mmap区域并不属于堆区,反而和堆区会争用虚拟地址空间。

这里要提到一个很重要的概念,内存的延迟分配,只有在真正访问一个地址的时候才建立这个地址的物理映射,这是Linux内存管理的基本思想。Linux内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放先行区,找到其对应的物理页面,将其全部释放的过程。

这篇文章关于mmap生长方向说的也挺详细的: 进程地址空间的布局(整理)

 

最后还有一个mmap机制的源代码分析博客,我水平暂时不够,只能看懂意思,待日后阅读内核源码再来回顾一遍:Linux用户空间线程管理介绍之二:创建线程堆栈

Guess you like

Origin www.cnblogs.com/dongzhiquan/p/11415722.html