进程栈、线程栈、内核栈

进程描述符 task_structnode

线程建立的时候,加上了 CLONE_VM 标记,这样 线程的内存描述符 将直接指向 父进程的内存描述符。程序员

内存描述符mm_struct数组

mm_struct 内存段

进程栈:stack架构

线程栈:使用mmap系统调用分配的空间,可是mmap分配的系统空间是什么呢?也就是上图中的mmap区域或者说共享的内存映射区域是什么呢?它的方向是向上生长仍是向下生长的?app

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

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

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

 

为何须要区分这些栈,其实都是设计上的问题。这里就我看到过的一些观点进行汇总,供你们讨论:atom

  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 架构就没有独立的中断栈。

 

进程空间中堆和栈的区别:

空间大小:栈系统指定大小限制在8M(M 级别),栈是连续空间;堆没有限定,是不连续存储空间,靠链表连接。

分配方式:堆都是程序员代码中动态分配和回收的,没有回收会产生内存泄露;栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,好比局部变量的分配。动态分配由alloca函数进行分配,可是栈的动态分配和堆是不一样的,他的动态分配是由编译器进行释放,无需咱们手工实现。

分配效率:栈分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行;堆是经过调用库函数

栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,而后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,而后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,而后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:通常是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

总之,栈比堆效率高,但没有堆灵活,优先使用栈,大内存使用堆。

char a[] = "hello"; //字符数组a的容量是6个字符,其内容为hello。a的内容能够改变,如a[0]= ‘X’

char *p = "world";//指针p指向常量字符串“world”(位于静态存储区,内容为world),常量字符串的内容是不能够被修改的。

/**
*内核空间
*栈:grow down,大小系统设置~8M,连续空间,编译器自动分配
*Memory Mapping Seg:堆栈共享,线程栈
*堆:grow up,大小硬件定,不连续空间,程序员malloc
*BSS:Block Started by Symbol,未初始化的全局变量和静态变量(静态data区)
*数据段:存放已初始化的全局变量、静态变量(全局和局部)、const常量数据(常量data区)
*代码段:存放CPU执行的机器指令,代码区是可共享,而且是只读的。这部分区域的大小在程序运行前就已经肯定
**/

#include <string>
int a=0;    //数据段:全局初始化变量
char *p1;   //BSS:全局未初始化变量
void main()
{
    int b;//栈
    char s[] = "abc";   //栈
    char *p2;         //栈
    char *p3="123456";   //123456\0在常量区(代码段??),p3在栈上。
    static int c=0;   //数据段:全局(静态)初始化区
    *p1 = (char*)malloc(10);  //分配得来的10字节区域在堆上
    *p2 = (char*)malloc(20);  //分配得来的20字节区域在堆上。
    strcpy(p1,"123456");   //123456\0放在常量区,编译器可能会将它与p3所向"123456\0"优化成一个地方。
}

//const 常量 或右值常量如"123456"放在数据段仍是代码段??

malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们均可用于申请动态内存和释放内存。可是new/delete会调用对象的构造和析构函数。

相关文章
相关标签/搜索