温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

进程栈、线程栈、内核栈分别是什么

发布时间:2021-06-29 09:59:04 来源:亿速云 阅读:454 作者:chen 栏目:大数据

本篇内容主要讲解“进程栈、线程栈、内核栈分别是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“进程栈、线程栈、内核栈分别是什么”吧!

进程描述符 task_struct

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

内存描述符mm_struct

进程栈、线程栈、内核栈分别是什么

进程栈:stack

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

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

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

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

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

  2. 为什么需要单独的线程栈?

    • 此时 A1 的栈指针 esp 如果为初始值 0x7ffc80000000,则线程 A1 一但出现函数调用,必然会破坏父进程 A 已入栈的数据。

    • 如果此时线程 A1 的栈指针和父进程最后更新的值一致,esp 为 0x7ffc8000FF00,那线程 A1 进行一些函数调用后,栈指针 esp 增加到 0x7ffc8000FFFF,然后线程 A1 休眠;调度器再次换成父进程 A 执行,那这个时候父进程的栈指针是应该为 0x7ffc8000FF00 还是 0x7ffc8000FFFF 呢?无论栈指针被设置到哪个值,都会有问题不是吗?

    • Linux 调度程序中并没有区分线程和进程,当调度程序需要唤醒”进程”的时候,必然需要恢复进程的上下文环境,也就是进程栈;但是线程和父进程完全共享一份地址空间,如果栈也用同一个那就会遇到以下问题。假如进程的栈指针初始值为 0x7ffc80000000;父进程 A 先执行,调用了一些函数后栈指针 esp 为 0x7ffc8000FF00,此时父进程主动休眠了;接着调度器唤醒子线程 A1: 

  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会调用对象的构造和析构函数。

到此,相信大家对“进程栈、线程栈、内核栈分别是什么”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI