Linux内存管理

 本文首先介绍一下Linux内存管理方式,着重说明一下用户空间的内存管理,包括Linux虚拟映射以及GLIBC中malloc的实现;然后简要介绍单进程多线程的内存管理方式,主要涉及各线程堆栈空间的分配;

Linux内存管理

Linux采用两级保护机制,隔离内核空间和用户程序空间,使用户程序无法直接访问内核,而只能通过系统调用的方式。

对于32CPU来说,Linux虚拟内存空间大小为4G,其中内核占据3G~4G空间(依据CPU体系的不同而有差别),应用程序占据0~3G空间。其中用户空间又分为代码段、数据段、BSS、堆及栈空间,各段分布如图所示:

上图不连续部分,表示可能存在隔离空间。实际上在我虚拟机上的Linux,内核为2.6.9,动态链接库是映射到低地址的128M空间里的,可网上众多资料都说其映射到堆栈之间,有待以后研究。下面就来分别介绍各部分内存空间。

内核空间

对于X86体系来说,内核占据高端1G虚拟内存,其中又分为两部分linux内核区域及vmalloc区域。

1.        内核区域

Linux内核可以认为是实地址模式,物理地址经过简单的偏移即构成逻辑地址,即内核编程时,我们可以认为操作的是物理内存,并且这段内存是物理连续的。其中用到两种内存分配方式,整页分配(大内存)、slab分配(小内存)。

Linux内核提供了比较完善的内存管理方式,页分配_get_free_page,基于slab的小内存分配kmalloc。这两个接口的实现都首先调用__alloc_pages(),分配物理页面,然后kmalloc在分配的物理页面上实现slab管理。分配物理页面基于buddy算法,在使用 buddy 实现的物理内存管理中最小分配粒度是以页为单位的,大小通常为4K__alloc_pages() 函数中,多次尝试调用get_page_from_freelist() 函数从 zonelist 中取得相关 zone,并从其中返回一个可用的物理页面。

实际上,内核空间的起始地址是物理内存的低地址,内核有自己的页表,经过简单的偏移转换成逻辑地址,另外每个进程也都有自己的页表。

2.        vmalloc

这个区域一般占据最高地址128M空间,也属于内核区域,不过它对应不连续的物理空间,这里的情况很类似于用户空间分配虚拟内存,内存逻辑上连续,其实映射到并不一定连续的物理内存上。Linux内核借用了这个技术,允许内核程序在内核地址空间中分配虚拟地址,同样也利用页表(内核页表)将虚拟地址映射到分散的内存页上。内核提供vmalloc函数分配内核虚拟内存,该函数不同于kmalloc,它可以分配较Kmalloc大得多的内存空间(可远大于128K,但必须是页大小的倍数),但相比Kmalloc来说,Vmalloc需要对内核虚拟地址进行重映射,必须更新内核页表,因此分配效率上要低一些(用空间换时间)。

用户空间

当应用程序以elf格式被加载时, Linux加载器首先将其加载到0X0804 8000处,对X86PPC来说,0X0804 8000~0XC000 0000这段地址空间才是程序可用的,任何访问到这个范围以外的指令,都会引起段错误。至于为什么Linux放着前128M内存空间不用,google一下也没有搜索到答案,有待以后研究。

上面已经说过,每个进程都有自己的页表,记录着逻辑地址与物理页面的对应关系,当进程切换时,新进程页表首地址就被加载到CR3寄存器中,内存的访问首先都是从CR3开始,找到TLB(页表缓冲区),通过逻辑地址找出对应的物理页面。另外进程PCB中还有许多vm_struct指针,每个vm_struct记录着一段逻辑地址空间以及对应的操作函数,比如代码空间、栈空间、mmap映射空间等,不同的空间,其读写函数以及权限也是不一样的。这些结构体对应的逻辑空间都会有间隔,防止越界访问。

当发生缺页异常时,即访问的逻辑地址对应的物理页面还未映射,或是已经被交换出去时,异常处理程序首先找到这个逻辑地址对应的vm_struct。如果找不到对应的vm_struct,说明发生了非法访问,否则找到相应的vm_struct,查找这段逻辑地址对应的设备文件的地址,并应用结构体中注册的相关函数将其内容调入物理页面。

一般未发生缺页异常或重新映射到物理内存后,内存访问经过页表转换时,页表条目中相应的位代表访问该页的一系列权限,这时就会进行权限检查,做到内存保护。

关于栈空间与mmap映射动态链接库的空间,都是由内核管理的,在必要的时候也可以挪动mmap映射的位置,为栈的增长提供空间。这里我们主要讨论一下堆空间,即malloc内存,它是调用brk实现的。这个调用只是扩张堆空间边界,并不对应物理内存,只是在使用时,发生缺页异常后,才由内核映射物理内存。但是,并不是每次malloc都会引起brk调用,malloc自身维护一个空闲链,用于收集堆空间上已经释放的空间,当然这个是逻辑地址空间。每次malloc调用,首先试图从该空闲链中获取该空间,当链上的元素都不能满足时,才会调用brk,扩张堆空间,以获得足够大的逻辑空间。每次free时,都会将释放的空间放入空闲链中,并检查该空间相邻的逻辑空间,如果也空闲,则合并为一块较大的空间。实际上malloc管理操作会比以上所说的更复杂,具体细节只有研究glibc的代码,网上并没有很好的资料。

所以,malloc这种管理方式类似于VxWorks的内存管理方式,如果频繁的申请、释放小内存,同样会在逻辑空间产生内存碎片。虽然对于不同的内存部分,其逻辑空间并不会连续(像栈空间和堆空间),但是堆空间内部,通过malloc申请的内存仍然有可能发生越界,破坏本进程相邻的变量内存空间,不过这种越界比较容易定位,因为只是限于本进程内部。至于野指针,其影响也只限于本进程,对系统和其它进程不会构成威胁。

Linux线程库

线程最主要的目的就是更好地支持SMP以及减小进程上下文切换开销,针对这两大意义,分别开发了核心线程和用户级线程。Linux内核仅支持轻量级进程,所以linux下的线程库不可能实现完全意义上的POSIX线程机制,不能实现用户级调度,以减小切换开销。

Linux线程库栈空间的分配,依据个CPU架构的不同而有所区别,这里只能分析i386平台所使用的两种栈组织方式:FLOATING_STACK方式和用户自定义方式。

FLOATING_STACK方式下,LinuxThreads利用mmap()分配8MB空间(i386系统缺省的最大栈空间大小,如果有运行限制(rlimit),则按照运行限制设置),使用mprotect()设置其中第一页为非访问区。该8M空间的功能分配如下图:

 

低地址被保护的页面用来监测栈溢出。对于用户指定的栈,在按照指针对界后,设置线程栈顶,并计算出栈底,不做保护,正确性由用户自己保证,我想应该也可以调用mprotect设置隔离区域,监测堆栈溢出。不论哪种组织方式,线程描述结构总是位于栈顶紧邻堆栈的位置。

至于堆空间,和单线程分配的机制是一样的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值