Linux虚拟地址空间布局以及进程栈和线程栈总结

本文转自多个博客,以及最后有我的总结。我没有单独从头到尾写一个总结的原因是别人已经写得很好了,我不花大量时间是无法达到这水平的。

 

一:Linux虚拟地址空间布局

(转自:Linux虚拟地址空间布局)

 

 在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块。在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,根据需要将其映射到物理内存。

     虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页时会导致一个页错误(page fault)。在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。

     Linux进程在虚拟内存中的标准内存段布局如下图所示:

     其中,用户地址空间中的蓝色条带对应于映射到物理内存的不同内存段,灰白区域表示未映射的部分。这些段只是简单的内存地址范围,与Intel处理器的段没有关系。

     上图中Random stack offset和Random mmap offset等随机值意在防止恶意程序。Linux通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算访问栈、库函数等地址。execve(2)负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将BSS段清零。

     用户进程部分分段存储内容如下表所示(按地址递减顺序):

名称

存储内容

局部变量、函数参数、返回地址等

动态分配的内存

BSS段

未初始化或初值为0的全局变量和静态局部变量

数据段

已初始化且初值非0的全局变量和静态局部变量

代码段

可执行代码、字符串字面值、只读变量

     在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;堆由程序员自己管理,即显式地申请和释放空间。

     BSS段、数据段和代码段是可执行程序编译时的分段,运行时还需要栈和堆。

 

     以下详细介绍各个分段的含义。

 

1 内核空间

     内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。

 

2 栈(stack)

     栈又称堆栈,由编译器自动分配释放,行为类似数据结构中的栈(先进后出)。堆栈主要有三个用途:

  • 为函数内部声明的非静态局部变量(C语言中称“自动变量”)提供存储空间。
  • 记录函数调用过程相关的维护性信息,称为栈帧(Stack Frame)或过程活动记录(Procedure Activation Record)。它包括函数返回地址,不适合装入寄存器的函数参数及一些寄存器值的保存。除递归调用外,堆栈并非必需。因为编译时可获知局部变量,参数和返回地址所需空间,并将其分配于BSS段。
  • 临时存储区,用于暂存长算术表达式部分计算结果或alloca()函数分配的栈内内存。

     持续地重用栈空间有助于使活跃的栈内存保持在CPU缓存中,从而加速访问。进程中的每个线程都有属于自己的栈。向栈中不断压入数据时,若超出其容量就会耗尽栈对应的内存区域,从而触发一个页错误。此时若栈的大小低于堆栈最大值RLIMIT_STACK(通常是8M),则栈会动态增长,程序继续运行。映射的栈区扩展到所需大小后,不再收缩。

     Linux中ulimit -s命令可查看和设置堆栈最大值,当程序使用的堆栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)。注意,调高堆栈容量可能会增加内存开销和启动时间。

     堆栈既可向下增长(向内存低地址)也可向上增长, 这依赖于具体的实现。本文所述堆栈向下增长。

     堆栈的大小在运行时由内核动态调整。

 

3 内存映射段(mmap)

     此处,内核将硬盘文件的内容直接映射到内存, 任何应用程序都可通过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左右。

 

4 堆(heap)

     堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。当进程调用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)正确释放本内存空间。

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

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

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

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

 

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
  1.  

上面对进程的地址空间有个比较全局的介绍,那我们看下 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;
  }
  1.  

虽然线程的地址空间和进程一样,但是对待其地址空间的 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);
  1.  

由于线程的 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)];
};                                                     
  1.  

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()                       
  1.  

  2.  

四、中断栈

进程陷入内核态的时候,需要内核栈来支持内核函数调用。中断也是如此,当系统收到中断事件后,进行中断处理的时候,也需要中断栈来支持函数调用。由于系统中断的时候,系统当然是处于内核态的,所以中断栈是可以和内核栈共享的。但是具体是否共享,这和具体处理架构密切相关。

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用户空间线程管理介绍之二:创建线程堆栈

 

 

怎么理解linux内核栈?

关于linux内核栈,一直很困惑,找不到详细资料。
1. linux内核栈是所有进程共享的吗,每个进程都有一个单独的内核栈?
2. 从内核模块编程的角度看(不涉及用户态进程),内核栈该怎么理解?和用户进程进行系统调用使用的栈空间有什么不同?
3. 怎么理解linux内核栈空间只有4KB或8KB,linux内核编程中的堆(heap)和栈(stack)有什么区别?

作者:向晨
链接:https://www.zhihu.com/question/57013926/answer/151306072
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

1. 读Linux内核以及相关的资料的时候,时刻要清醒地认识到它说的是内核态还是用户态的东西。

2. 一个用户态进程/线程在内核中都是用一个task_struct的实例描述的,这个有点类似设计模式里面的桥接模式(handle-body), 用户态看到的进程PID,线程TID都是handle, task_struct是body。

3. C语言书里面讲的堆、栈大部分都是用户态的概念,用户态的堆、栈对应用户进程虚拟地址空间里的一个区域,栈向下增长,堆用malloc分配,向上增长。

4. 用户空间的堆栈,在task_struct->mm->vm_area里面描述,都是属于进程虚拟地址空间的一个区域。

5.而内核态的栈在tsak_struct->stack里面描述,其底部是thread_info对象,thread_info可以用来快速获取task_struct对象。整个stack区域一般只有一个内存页(可配置),32位机器也就是4KB。

6. 所以说,一个进程的内核栈,也是进程私有的,只是在task_struct->stack里面获取。

7. 内核态没有进程堆的概念,用kmalloc()分配内存,实际上是Linux内核统一管理的,一般用slab分配器,也就是一个内存缓存池,管理所有可以kmalloc()分配的内存。所以从原理上看,在Linux内核态,kmalloc分配的所有的内存,都是可以被所有运行在Linux内核态的task访问到的。

 

作者:朱涵俊
链接:https://www.zhihu.com/question/57013926/answer/151472914
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

内核栈有不同含义。一是内核线程使用的栈,比如初始化线程,idle,kthread,这些仅在内核空间运行,只有内核栈,没有用户态以及用户空间栈。
还有就是用户线程发生中断,系统调用进入内核态时候使用的栈。由于中断处理很简单,而且不允许中断重入,使用的内核栈很少,4k/8k就够了。还有是异常栈,专门处理异常,跟中断栈分开,否则在中断处理发生异常就导致重入了。系统调用内核栈也是分开的。
但有时候中断处理,系统调用实际上要处理很多东西的,但内核不是直接在中断栈处理所有事情,而是处理最简单的部分,复杂的交给其他内核线程/软中断完成。
中断/异常处理/系统调用内核栈是所有进程共享的。但不是说只有一份,而是每个虚拟cpu核心一份。
那如果同一个cpu多个线程切换怎么办?比如系统调用过程中发生时钟硬件中断,这个时候发现需要切换线程。如果在内核栈处理一半过程中发生切换,那就会出现问题。为了避免这个问题,线程切换都发生在中断/系统调用返回的时候,即内核栈恢复到栈顶的时候发生任务切换。即本来硬件中断返回原来线程,如果发生切换,返回的时候发回b线程。对于需要长时间处理的系统调用,中断处理,会有内核线程进行处理,同样也是在中断/系统调用返回的时候切换到内核线程。

 

作者:Patrick Nicholas
链接:https://www.zhihu.com/question/57013926/answer/151506606
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

1. Linux 内核中使用 `task_struct` 作为进程描述符,该结构定义在<linux/sched.h>文件中:

struct task_struct {
 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
 void *stack;
 atomic_t usage;
 unsigned int flags; /* per process flags, defined below */
 unsigned int ptrace;
 int lock_depth; /* BKL lock depth */ 
 /* ...... */ 
}; 

可以发现 `task_struct` 中有一个 `stack` 成员,而 `stack` 正好用于保存内核栈地址。内核栈在进程创建时绑定在 `stack` 上。可以观察 `fork` 流程:Linux 通过 `clone()` 系统调用实现 `fork()`,然后由 `fork()` 去调用 `do_fork()`。定义在<kernel/fork.c>中的 `do_fork()` 负责完成进程创建的大部分工作,它通过调用 `copy_process()` 函数,然后让进程运行起来。`copy_process()` 完成了许多工作,这里重点看内核栈相关部分。`copy_process()` 调用 `dup_task_struct` 来创建内核栈、`thread_info` 和 `task_struct`:

static struct task_struct *dup_task_struct(struct task_struct *orig) { 
 struct task_struct *tsk;
 struct thread_info *ti;
 unsigned long *stackend;
 int err; prepare_to_copy(orig);
 tsk = alloc_task_struct();
 if (!tsk) return NULL;
 ti = alloc_thread_info(tsk); 
 if (!ti) { 
  free_task_struct(tsk);
  return NULL; 
 } 
 err = arch_dup_task_struct(tsk, orig);
 if (err) goto out;
 tsk->stack = ti;
 err = prop_local_init_single(&tsk->dirties);
 if (err) goto out;
 setup_thread_stack(tsk, orig);
 stackend = end_of_stack(tsk);
 *stackend = STACK_END_MAGIC;
 /* for overflow detection */
 #ifdef CONFIG_CC_STACKPROTECTOR 
 tsk->stack_canary = get_random_int();
 #endif 
 /* One for us, one for whoever does the "release_task()" 
 (usually parent) */
 atomic_set(&tsk->usage,2);
 atomic_set(&tsk->fs_excl, 0);
 #ifdef CONFIG_BLK_DEV_IO_TRACE
 tsk->btrace_seq = 0;
 #endif 
 tsk->splice_pipe = NULL;
 account_kernel_stack(ti, 1);
 return tsk;
out:
 free_thread_info(ti);
 free_task_struct(tsk);
 return NULL; 
} 

其中重点是下面部分:

tsk = alloc_task_struct(); 
if (!tsk) return NULL; 
ti = alloc_thread_info(tsk); 
if (!ti) { 
  free_task_struct(tsk); 
  return NULL; 
} 
err = arch_dup_task_struct(tsk, orig); 
if (err) goto out; 
tsk->stack = ti; 

这里可以看到内核栈的创建过程。可能会疑惑为何 `stack` 指向了 `thread_info`,那是因为在2.6以前的内核中,各个进程的 `task_struct` 存放在内核栈的尾端,这样做是为了在寄存器较少的体系结构中直接使用栈指针加偏移就可以算出它的位置。2.6以后使用slab分配器动态分配 `task_struct` ,所以只需要在栈顶创建一个 `thread_info` 记录 `task_struct` 的地址。

所以这里回答了第一个问题, 每个进程都有一个单独的内核栈。

2.

从内核模块编程的角度看(不涉及用户态进程),内核栈该怎么理解?和用户进程进行系统调用使用的栈空间有什么不同?

每个进程运行时都持有上下文,用于保证并行性。为了保证内核和用户态隔离,陷入内核不影响用户态,所以使用了不同的栈。内核栈只是对内核态上下文中的栈的称谓。

为了方便管理用户程序,限制用户程序权限,所以区分了内核态和用户态。内核态中拥有高特权级,能够执行io等特权指令,而用户态程序想要执行特权级指令则必须陷入内核态。从用户程序角度来看,内核更类似与库文件的存在。

内核通过虚拟地址访问权限来限制用户程序访问内存地址,比如内核空间的代码和数据不应该被用户程序访问到。因此内核运行时使用的栈不应该能被用户态代码访问到,否则用户态代码完全可以通过构造特定的数据控制内核(参考ret2libc)。因此,用户态使用的栈空间和内核栈并无本质区别,它们均处于同一块页表映射中,内核栈处于高特权级访问限制的虚拟地址中,防止用户态代码访问内核数据。

3.

怎么理解linux内核栈空间只有4KB或8KB,linux内核编程中的堆(heap)和栈(stack)有什么区别?

内核中的资源是非常宝贵的,而一个比较大的栈空间多数时间是浪费了。那为何不设计小一点,然后保证内核调用层次低、局部变量小,做到不溢出?

而内核编程中的堆和栈并非通常写程序时所说的堆和栈有严格的区分。

 

内核中的堆和栈没有严格的地址区分,只是程序角度的不同解释而已。

 

1、每个进程被创建的时候,在生成进程描述符task_struct的同时,会生成两个栈,一个是用户栈,位于用户地址空间;一个是内核栈,位于内核空间。当进程在用户地址空间中执行的时候,使用的是用户栈,CPU堆栈指针寄存器中存的是用户栈的地址;同理,当进程在内核空间执行时,CPU堆栈指针寄存器中放的是内核栈的地址。

2、当位于用户空间的进程进行系统调用时,它会陷入内核,让内核代其执行。此时,进程用户栈的地址会被存进内核栈中,CPU堆栈指针寄存器中的内容也会变为内核栈的地址。当系统调用执行完毕,进程从内核栈找到用户栈的地址,继续在用户空间中执行,此时CPU堆栈指针寄存器就变为了用户栈的地址。

3、我的理解是,因为一个进程就对应着一个内核栈,而系统一般默认可同时存在的进程数目是32768,如果按每个内核栈空间4KB计算,32768个进程就已经占用了128MB内存,而且单个内存栈过大也容易造成内存空间浪费的结果。



作者:纸糊哲也
链接:https://www.zhihu.com/question/57013926/answer/151604453
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

作者:个性耀世界
链接:https://www.zhihu.com/question/57013926/answer/184379387
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

有人点赞,那我得完善一下回答了,之前是自己看,逻辑不太清楚。

周末,好好补充补充。

这个问题我看了好久了。我也是初学linux,尝试着答一答,抛砖引玉,也希望各位的指正。

1. linux内核栈是所有进程共享的吗,每个进程都有一个单独的内核栈?

不是进程共享的,每个进程有单独的内核栈。

```

union thread_union{

struct thread_info thread_info;

unsigned long stack[THREAD_SIZE/sizeof(long)];

}

```

线程描述符thread_info中有一个指向进程描述符的指针,这里也可以看出是进程私有的。

这个可以参考深入理解linux内核第三版第86,90页,linux内核设计与实现第22,23页。

2. 从内核模块编程的角度看(不涉及用户态进程),内核栈该怎么理解?和用户进程进行系统调用使用的栈空间有什么不同?

不牵扯用户态进程的内核栈,我的理解是内核线程拥有的。内核线程没有独立的地址空间,只在内核空间运行。系统调用的时候,用的是内核栈,内核运行在进程上下文中。 之前疑惑我的地方是,把linux单内核,理解成了所有的只有一个内核栈了。单内核的含义是所有内核服务都在一个大内核地址空间上运行。多个内核线程,还有进程的内核栈,系统运行时是有多个内核栈的。

还有一点就是linux内核设计与实现第18页上有一句话:每个处理器都有自己的内核栈。这句我感觉好奇怪,内核栈是和线程相关的,怎么和处理器绑定了?英文原文是Each process receives its own stack。这里receives翻译成拥有真的好吗,应该是get something的意思。

 

linux中,每个进程的内核栈,在内核空间中是如何存放的?

 android linux

deRong_Qiu: 

如题,有些地方不太明白。
linux中每个进程的内核栈是8 KB,但为什么说所有进程共享内核空间,每个进程的内核栈与整个内核空间有什么关系?
感谢!!

1.每个进程的内核栈是独立的,存放在每个task上,记录该进程陷入内核时的内核栈,但它们在地址空间上是共享的!

打个比方 进程A的内核栈(虚拟或物理)地址 0x4000 - 0x6000,进程B的内核栈(虚拟或物理) 0x8000 - 0x10000

它们是不重叠的!

用户栈就不一样的,它们的用户栈 虚拟地址空间完全可以是重叠的。

  • deRong_Qiu: 

    感谢!可以这么理解吗,对于32位系统,各个进程的内核栈虚拟地址都在1GB的内核空间内,且不重叠。而各个进程用户栈的虚拟地址空间完全重叠,但映射到物理地址各不相同。

      回复  2019-04-26

  • deRong_Qiu: 

    内核栈共享内核地址空间的意思,并不是内核栈之间数据的共享,只是都存在内核地址空间而已?

      回复  2019-04-26

  • 187J3X1: 

    是。所以内核编程也要特别小心,因为你需要确保你访问的内存不是其他进程的空间。而用户态程序通常不需要考虑这个问题

一、用户态和内核态

内核态和用户态是操作系统的两种运行级别,用于区分不同程序的不同权利。

内核态就是拥有资源多的状态,或者说访问资源多的状态,也称为特权态。相对来说,用户态就是非特权态,访问的而资源将受到限制。如果一个程序运行在特权态,该程序就可以访问计算机的任何资源,它的资源访问权限不受限制。如果一个程序运行在用户态,其资源需求将受到各种限制。如:要访问操作系统的内核数据结构,如进程表,则需要在特选态下才能办到。如果要访问用户程序里的数据,在用户态即可。

二、用户栈和内核栈

内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每一个进程都有两个栈,一个用户栈,存在于用户空间;一个内核栈,存在于内核空间。当进程在用户空间运行时,CPU堆栈指针寄存器里面的内容都是用户栈地址,使用用户栈;当进程在内核空间时,CPU堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。

当进程因为中断或者系统调用陷入到内核态时,进程所使用的堆栈也要从用户栈转到内核栈。进程陷入到内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之后时,在内核态之后的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了用户栈和内核栈的互转。

那么,知道从内核转到用户态时,用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,如何知道内核栈的地址?关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为当进程在用户态运行时,使用的用户栈,当进程陷入到内核态时,内核保存进程在内核态运行的相关信息,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。

 

 

 

Linux源码解析-内核栈与thread_info结构详解

2018-09-14阅读 3790

1.什么是进程的内核栈?

在内核态(比如应用进程执行系统调用)时,进程运行需要自己的堆栈信息(不是原用户空间中的栈),而是使用内核空间中的栈,这个栈就是进程的内核栈

2.进程的内核栈在计算机中是如何描述的?

linux中进程使用task_struct数据结构描述,其中有一个stack指针

struct task_struct
{
    // ...
    void *stack;    //  指向内核栈的指针
    // ...
};

task_struct数据结构中的stack成员指向thread_union结构(Linux内核通过thread_union联合体来表示进程的内核栈)

union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];  
};

struct thread_info是记录部分进程信息的结构体,其中包括了进程上下文信息:

struct thread_info {
    struct pcb_struct   pcb;        /* palcode state */
 
    struct task_struct  *task;      /* main task structure */  /*这里很重要,task指针指向的是所创建的进程的struct task_struct
    unsigned int        flags;      /* low level flags */
    unsigned int        ieee_state; /* see fpu.h */
 
    struct exec_domain  *exec_domain;   /* execution domain */  /*表了当前进程是属于哪一种规范的可执行程序,
                                                                        //不同的系统产生的可执行文件的差异存放在变量exec_domain中
    mm_segment_t        addr_limit; /* thread address space */
    unsigned        cpu;        /* current CPU */
    int         preempt_count; /* 0 => preemptable, <0 => BUG */
 
    int bpt_nsaved;
    unsigned long bpt_addr[2];      /* breakpoint handling  */
    unsigned int bpt_insn[2];
 
    struct restart_block    restart_block;
};

从用户态刚切换到内核态以后,进程的内核栈总是空的。因此,esp寄存器指向这个栈的顶端,一旦数据写入堆栈,esp的值就递减

(hy:进程描述符是不是只保存在内核栈中,用户栈中不保存?)

3.thread_info的作用是?

这个结构体保存了进程描述符中中频繁访问和需要快速访问的字段,内核依赖于该数据结构来获得当前进程的描述符(为了获取当前CPU上运行进程的task_struct结构,内核提供了current宏。

    #define get_current() (current_thread_info()->task)
    #define current get_current()

内核还需要存储每个进程的PCB信息, linux内核是支持不同体系的的, 但是不同的体系结构可能进程需要存储的信息不尽相同,

这就需要我们实现一种通用的方式, 我们将体系结构相关的部分和无关的部分进行分离,用一种通用的方式来描述进程, 这就是struct task_struct, 而thread_info

就保存了特定体系结构的汇编代码段需要访问的那部分进程的数据,我们在thread_info中嵌入指向task_struct的指针, 则我们可以很方便的通过thread_info来查找task_struct

4.内核栈的大小?

进程通过alloc_thread_info函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈,查看源码

alloc_thread_info函数通过调用__get_free_pages函数分配2个页的内存(8192字节)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值