进程栈、线程栈、内核栈分别是什么
本篇内容主要讲解"进程栈、线程栈、内核栈分别是什么",感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习"进程栈、线程栈、内核栈分别是什么"吧!
进程描述符 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 相关结构体 */};
为什么需要区分这些栈,其实都是设计上的问题。这里就我看到过的一些观点进行汇总,供大家讨论:
为什么需要单独的进程内核栈?
所有进程运行的时候,都可能通过系统调用陷入内核态继续执行。假设第一个进程 A 陷入内核态执行的时候,需要等待读取网卡的数据,主动调用
schedule()
让出 CPU;此时调度器唤醒了另一个进程 B,碰巧进程 B 也需要系统调用进入内核态。那问题就来了,如果内核栈只有一个,那进程 B 进入内核态的时候产生的压栈操作,必然会破坏掉进程 A 已有的内核栈数据;一但进程 A 的内核栈数据被破坏,很可能导致进程 A 的内核态无法正确返回到对应的用户态了;为什么需要单独的线程栈?
此时 A1 的栈指针 esp 如果为初始值 0x7ffc80000000,则线程 A1 一但出现函数调用,必然会破坏父进程 A 已入栈的数据。
如果此时线程 A1 的栈指针和父进程最后更新的值一致,esp 为 0x7ffc8000FF00,那线程 A1 进行一些函数调用后,栈指针 esp 增加到 0x7ffc8000FFFF,然后线程 A1 休眠;调度器再次换成父进程 A 执行,那这个时候父进程的栈指针是应该为 0x7ffc8000FF00 还是 0x7ffc8000FFFF 呢?无论栈指针被设置到哪个值,都会有问题不是吗?
Linux 调度程序中并没有区分线程和进程,当调度程序需要唤醒"进程"的时候,必然需要恢复进程的上下文环境,也就是进程栈;但是线程和父进程完全共享一份地址空间,如果栈也用同一个那就会遇到以下问题。假如进程的栈指针初始值为 0x7ffc80000000;父进程 A 先执行,调用了一些函数后栈指针 esp 为 0x7ffc8000FF00,此时父进程主动休眠了;接着调度器唤醒子线程 A1:
进程和线程是否共享一个内核栈?
No,线程和进程创建的时候都调用
dup_task_struct
来创建 task 相关结构体,而内核栈也是在此函数中alloc_thread_info_node
出来的。因此虽然线程和进程共享一个地址空间mm_struct
,但是并不共享一个内核栈。为什么需要单独中断栈?
这个问题其实不对,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执行的机器指令,代码区是可共享,并且是只读的。这部分区域的大小在程序运行前就已经确定**/#includeint 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会调用对象的构造和析构函数。
到此,相信大家对"进程栈、线程栈、内核栈分别是什么"有了更深的了解,不妨来实际操作一番吧!这里是网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!