linux进程空间的内存管理

前言

写的还很乱,没把全部知识点串起来,以后再来吧,另外感觉没图读起来确实不通顺..学习自用,有错麻烦提一下

二更,这篇讲的是进程空间的结构即分区(没讲到内核空间),涉及到一些linux管理进程空间的数据结构,与fork()和exec对应的过程串联起来,还有用户空间的分配,如何管理分配与空闲块,会讲到malloc等调用,

进程空间的分配

进程空间的分配要与内核空间的分配区别开,内存空间分配可以看成是物理空间的分配,进程空间的分配是虚拟页的分配;进程空间会采用一个延时分配,也就是写时复制机制

Linux内核内存管理算法Buddy和Slab

一个进程会有相应的用户地址空间,这里说的进程也要与磁盘上的程序(可执行文件)给区分开来,首先讲一下进程的空间结构:

进程的空间结构

编程语言里常说的内存五区

  • 全局区(用来存全局变量的区域);静态区;代码区;堆栈(函数栈和自由分配区即malloc的区域)

而实际linux中进程的分为以下几段

  • data section,用来存可执行文件的已初始化全局变量,如函数之外的int count=5;

  • BSS(未初始化变量),与data section相对,存的是未初始化的全局变量,如全局的int array[10]

  • text section的映射,即代码段,,机器指令,可被共享,只读

  • stack,用户栈,要区分开内核栈(可见下图两者区别)

  • memory mapped files(可通过mmap调用来建立映射),或者叫共享内存区域,可映射如C库lib.so共享库(包含.text和.data段),(好像malloc也能用?)

  • 堆,有些书管这个叫匿名映射区,也是malloc分配的区域

这篇只讨论图中用户空间

这几个区域,对应到linux的PCB结构中,也就是task_struct结构体中,会在其中的mm成员中的mmap成员有体现(比较拗口,见下图),每一个内存区域是一个区,用链表串联起来,最基础的四个区域:data,text(code),heap,stack,但是对于一些程序(比如OO数据库,特殊的调试器)会用到更多的区域,,为了防止区域过多难以查找,task_struct中除了用链表还会用红黑树来管理,

进程空间的管理

在进程管理中,一个进程会产生一个task_struct用来存储进程相关信息,也就是我们课本里经常提到的PCB,当一个进程结束运行的时候会进入僵尸态,会只剩下个空壳等父进程来收尸,这个空格就是task_struct了(可能还留有其他结构?)

这个task_struct有一个名为mm的成员,就是用来描述进程空间的,这个mm成员存取此进程能合法寻址的区域的相关信息,即process address space

这个区域用memory descriptor结构来表示,这个mm成员的类型为mm_struct结构(这个数据结构也是由slab layer分配的),这个结构体里包含:

  1. 内存区域(memory area)的链表和红黑树(至于为什么要多余地用两个这里不深究)

  2. 堆栈,代码区,数据区的起始位置,即

  3. 用来同步的spinlock等

而上面第一点提到的内存区域memory area则用vm_area_struct表示,每一个vm_area_struct作为一个链表/红黑树节点,这个内存区域就是一段连续的空间,要记得在内存空间里这些都是虚拟页的连续空间,这段空间在linux内核中又叫virtual memory area(VMA),这个vm_area_struct里包含成员:

  • vm_start and vm_end下一个节点

  • 可对这个区域执行的操作(如什么呢?)

进程是通过内核来对进程地址空间(process address space)进行添加删除memory area的,刚才说的整体可以看下图,刚才说的连续的内存区域指的就是上文提到的内存五区之类的

 

在linux中,一个进程开始运行的时候,虽然有很大的address sapce(可寻址的空间),但是实际可以不占任何物理页,这个策略名为demand page.等到进程去访问某个页的时候,才会引发缺页exception,再去分配物理空间;我们调用malloc或者brk实际就是这么个情况,也就是文章开头提到的延时分配,具体的物理页的分配可以看此专栏内核那篇文章

wm_arem_struct这俩图都是一个意思

动态内存映射

brk()

在进程的堆区中,有一个标志位用来指示堆的水位,调用brk用于将此水位上涨(这个标志位拿来防止越界的吗?,怎么让他上涨,修改vm_area里的边界吗?)

mmap()

mmap映射匿名空间到进程的地址空间区域内,它的底层实现是do_mmap,这整个函数的作用可以看成是分配一段虚拟空间给process address space;mmap可以用来增加上面vm_area_struct的内存区域,,当用pthread_create的时候也会调用mmap来产生一个新的栈,

mmap映射文件,与mmap相对的读取文件的函数就是read了,相比较mmap更高效,不过mmap也只是建立一个逻辑的映射,也就是并没有把磁盘上的文件读进内存中,等到真正要使用到那个区域的时候才会发生缺页中断来读入目标页

共享区域,也就是shared lib,比如C库中的lib.so等动态链接库会被mmap进这个区域,这个区域位于堆栈之间(具体哪个位置单独区分开的吗.向着堆区生长吗),对于mmap出来的vm_area_struct,

  • Q:如果相邻的区域地址连续且可操作权限相同,则两者会合并吧
  • Q:所有vm_area_struct只有op成员来表示权限,没写哪个是栈,内核是默认顺序区域和权限来识别哪个是栈,堆,code和data吗?
  • Q:内存片段的起始地址自己传入参数还是操作系统决定

malloc()

具体的malloc实现有很多不同的方案,详细可以看文末参考资料13,这里说一下glibc版本的

在linux中malloc的调用会用到ptmalloc()函数,而ptmalloc对于申请空间小于128KB会调用sbrk(),这个函数会将heap区的brk水位上涨(见上面内存分布图的brk位),对于大于128KB会调用mmap()来多加个

这样做有点像STL空间配置器的做法,可以说glibc在应用层层面又多加了个防止碎片的操作

(在并且brk调用的会连续分配吗,不会产生碎片吗)

进程的创建fork

概述::使用fork会看上去返回两次,实际一次是在父进程中的返回一次是在子进程中的返回,在代码里用if(父进程返回值>0)else(子进程返回0);小于0则是有错误发生

fork调用首先会创建一个新的PCB也就是task_struct结构(上面有说),这个结构里的信息都是子进程独有的,比如进程标识符PID,和进程有关的静态动态优先级,调度策略等信息,

但是虚拟页在创建时候是共享的,子进程会直接复制父进程的area struct和page tables(page tables是单独分出来的?不太懂怎么改),area struct也就是上面提及的task_struct 里mm位的mm_struct(来自CSAPP),所以进程复制出来都有基本的内存四区

并把两个进程中的每一页都设为只读(通过设置page table的吗?),,当父子进程任意一个修改了其中某一页才会引发缺页中断真正分配一个独立的物理页给子进程,这就是一个写时复制机制,这样干的原因也是为了把fork复用,两个用途:为创建子进程和fork+exec来创建完全新的进程,这里不深究

会用到的调用

:首先一点最重要的,

fork/vfork->clone->do_fork->copy_process

copy_process作用:设置process descriptor和其他子进程的数据结构

copy_process过程:调用dup_task_struct,产生一个和父进程相同的thread_info,task_struct

根据参数,来决定是否拷贝/共享打开的文件,文件系统,信号处理函数,地址空间,命名空间(thread的这些都是共享的)

do_fork过程

  • 分配新的PID给进程,
  • 检查父进程的ptrace,即是否被trace,(这个是调试相关的)
  • 调用copy_process来复制一份process descriptor
  • 根据参数来决定子进程是否运行在同一个CPU
  • 略..

(上面这个过程,感觉就是一个初始化task_struct的过程)

写时复制

因为fork会复制所有进程地址空间给子进程,如果不用写时复制机制的话,fork后调用exec会导致原本复制过来的地址空间又被清空(因为execv第一步就是把现有的清空),而使用copy on write,作用是不会全部复制给子进程,而是共享这些地址片段如代码区,数据区,堆栈都是共享的,而当父子进程修改了某一个页,会产生缺页中断,中断处理函数再分配单独的物理空间,顾名思义,写的时候再真正复制,没写就是共享地址空间

另外一点,为了防止fork完以后父进程先运行而修改页面而导致复制,fork完会优先让子进程先运行(好像是哪个版本后的实现)

而vfork是,子进程直接写在父进程的地址空间里

execv()

大概印象:把目标磁盘上的程序覆盖目前的进程,(只是覆盖text段吗,还要修改从属的父进程为INIT进程吧),exec是一系列函数,在这里只大概提一下

详细过程:

  1. 映射private区域,先把目前进程的area structs删掉,把目标程序文件如a.out的区域映射到进程区域里(包括code,data,bss区域等),这里会给每个内存区域生成新的area structs(看来这个area structs是每个内存区域分配一个了),注意每个区域(即堆栈,text,data等)会采用写时复制机制,即不会分配对应的物理页,
  2. 映射共享区域,上文提到了进程的分区里是包含共享区域的,这些来自于其他地方比如C的共享库,把他们动态链接进来
  3. 初始化设置程序计数器PC

ps:(第一点可以了解下程序文件ELF有哪些区域,并且那些区域是怎么映射到进程中相应的区域的;第三点这里可以了解下程序启动如何变成进程的)

虚地址到实地址转换

地址空间的转换是通过mm_struct转三级页表再到struct page,从而找到物理页的(这段我还不熟TODO)


以下写的都挺乱的

分配空间

32位的系统的寻址空间是2的32次方,也就是4G空间,其中1G会被分配给内核,3G分给用户空间;内核空间实际上只有839MB,其中100多MB预留给硬件,而这么小的空间,不能直接寻址全部区域,超过寻址部分的就叫high memory,会通过其他手段映射,可以直接寻址的就叫low memory,

其他部分:分页分段杂谈

为什么要虚拟空间,如果没用虚拟空间,进程间的地址越位很容易发生,用虚拟空间并加判断是否越界来互相保护;另外暂时不用的数据移到外存是否有了虚拟空间更容易实现呢

另一个问题,页表项数目,地址个数除以页大小等于页数目,也就是页表项

用两个方法来解决,倒排索引(?)和多级页表

参考资料

  1. 博客园

  2. CSDN

  3. understanding linux kernel

  4. linux development

  5. https://mp.weixin.qq.com/s/sjfkYyvqmsgYUN5EOxU3vQ

  6. CSAPP

  7. https://blog.csdn.net/yueyansheng2/article/details/78860040

  8. https://zhuanlan.zhihu.com/p/57863097

  9. https://zhuanlan.zhihu.com/p/26137521

  10. https://juejin.im/post/5e9d1c97f265da47ce6cc76d上面vm_struct的图

  11. https://mp.weixin.qq.com/s/yXbUnesmoyhDyfRrQAmKsw上面用户到伙伴系统的图

  12. https://zhuanlan.zhihu.com/p/36140017用户空间到的内核空间的图

  13. https://zhuanlan.zhihu.com/p/24753861各种malloc版本

  14. https://www.cnblogs.com/feng9exe/p/6883614.html好文啊,task_struct成员解析

  15. https://zhuanlan.zhihu.com/p/188577062知乎linux的栈生成

TODO

  1. 把CSAPP关于进程内存分布的也融合进去
  2. 栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配??
  3. malloc的内存池,fast bin之类的是啥这个链接,
  4. https://blog.csdn.net/z_ryan/article/details/79950737这个是ptmalloc的实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值