您的位置:首页 > 运维架构 > Linux

[Linux内存管理] linux内存布局的内核实现--用户空间的映射方式

2014-07-01 23:53 731 查看


引用牛人的一个表格(本人绘制能力实在有限,引自:http://blog.csdn.net/absurd/archive/2006/06/19/814268.aspx)

Linux的内存模型,一般为:
地址
作用
说明
>=0xc000 0000
内核虚拟存储器
用户代码不可见区域
Stack(用户栈)
ESP指向栈顶



空闲内存
>=0x4000 0000
文件映射区

空闲内存
Heap(运行时堆)
通过brk/sbrk系统调用扩大堆,向上增长。
.data、.bss(读写段)
从可执行文件中加载
>=0x0804 8000
.init、.text、.rodata(只读段)
从可执行文件中加载
保留区域
这 里的数据是怎么得到的呢?我们一般用到的malloc是从哪里分配的内存呢?为什么从这里分配呢?实际上malloc是c库的内存分配管理函数,其种种弊 端使得人们不太喜欢它,从而使人们写出很多自己的内存分配函数实现,不管哪种实现在linux下都是调用系统调用brk实现的,在内核当中就是 sys_brk。

我们知道,一个进程的所有的地址空间是按照区域组织的,每个区域都是一个vm_area_struct的结构体,每个结构体内部的地址是连续的,而不同的 结构体之间可能留有空洞,既然这样,不管brk出来的空间还是其它,比如说映射得到的空间都是vm_area_struct的表现,那么为何0x4000 0000是个分界点呢?之上的就是映射空间而之下的就是brk空间即堆空间呢?我们只有看一下这两种实现的内核源代码,分别是:sys_mmap2和 sys_brk。在可能具体代码之前首先要明白一件事就是:为了管理方便,所有的vm_area_struct的首地址和末地址都是页对齐的。好了,现在
可以看相关的代码了(我省略了很多不相关的代码,那些很重要,但不是这篇文章要说的):

asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,

unsigned long prot, unsigned long flags,

unsigned long fd, unsigned long pgoff)

{

...

down_write(&mm->mmap_sem);

error = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);

... return error;

}

unsigned long do_mmap_pgoff(struct file * file, unsigned long addr,

unsigned long len, unsigned long prot,

unsigned long flags, unsigned long pgoff)

//见SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,

unsigned long, prot, unsigned long, flags,

unsigned long, fd, unsigned long, pgoff) in
mmap.c


{

len = PAGE_ALIGN(len); //对齐了长度,使得首地址加上长度也是页对齐

...

addr = get_unmapped_area(file, addr, len, pgoff, flags);//此函数内部对齐了addr

if (addr & ~PAGE_MASK) //如果现在都不是页对齐的,那么返回的肯定是错误码,返回之

return addr;

...

}

unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,

unsigned long pgoff, unsigned long flags)

{

unsigned long (*get_area)(struct file *, un
4000
signed long,

unsigned long, unsigned long, unsigned long);

get_area = current->mm->get_unmapped_area;

if (file && file->f_op && file->f_op->get_unmapped_area)

get_area = file->f_op->get_unmapped_area;

addr = get_area(file, addr, len, pgoff, flags);

if (IS_ERR_VALUE(addr)) //这个宏很有意思,值得研究,如果返回真,那么addr就是一个错误码了,返回之

return addr;

if (addr > TASK_SIZE - len)//跨到了内核空间,返回错误码

return -ENOMEM;

if (addr & ~PAGE_MASK) //到此还是非页对齐,返回错误码

return -EINVAL;

return arch_rebalance_pgtables(addr, len);

}

在很多平台get_unmapped_area就是mm/mmap.c中定义的arch_get_unmapped_area

unsigned long arch_get_unmapped_area(struct file *filp, unsigned long addr,

unsigned long len, unsigned long pgoff, unsigned long flags)

{

struct mm_struct *mm = current->mm;

struct vm_area_struct *vma;

unsigned long start_addr;

if (len > TASK_SIZE) //跨越内核空间边界

return -ENOMEM;

if (flags & MAP_FIXED) //如果有MAP_FIXED标志那么就直接返回addr,其实在MAP_FIXED

设置的前提下,用户传入的addr必须是页对齐的,若不是则出错,这里将addr返回,如果addr不是页对齐,那么get_unmapped_area中的if (addr & ~PAGE_MASK)验证将无法通过。

return addr;

if (addr) { //如果用户提供了addr则优先考虑用户提供的addr,但是不保证这个addr就是最后返回的addr。下面还是要说一下这里的要点。

addr = PAGE_ALIGN(addr);

vma = find_vma(mm, addr);

if (TASK_SIZE - len >= addr &&

(!vma || addr + len vm_start))//addr的地址没有被映射,而且空洞足够我们这次的映射,那么返回addr以准备这次的映射

return addr;

}

if (len > mm->cached_hole_size) { //我们需要的长度大于当前的vm区域间的空洞

start_addr = addr = mm->free_area_cache;//从上次的查询位置开始

} else { //需要的长度小于当前空洞,为了不至于时间浪费,那么从0开始搜寻,这里的

搜寻基地址TASK_UNMAPPED_BASE很重要,用户mmap的地址的基地址必须在TASK_UNMAPPED_BASE之上,但是一定这样严格 吗?看上面的if (addr)判断,如果用户给了一个地址在TASK_UNMAPPED_BASE之下,映射实际上还是会发生的。

start_addr = addr = TASK_UNMAPPED_BASE;

mm->cached_hole_size = 0;//空洞大小从0开始,以后逐渐增加

}

full_search:

for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {

if (TASK_SIZE - len free_area_cache,因此下面的if判断当然可以通过,所以从新开始,从TASK_UNMAPPED_BASE开始搜索,若开 始地址就是TASK_UNMAPPED_BASE,那么肯定出错了

if (start_addr != TASK_UNMAPPED_BASE) {

addr = TASK_UNMAPPED_BASE;

start_addr = addr;

mm->cached_hole_size = 0;

goto full_search;

}

return -ENOMEM;

}

if (!vma || addr + len vm_start) {

mm->free_area_cache = addr + len;

return addr;

}

if (addr + mm->cached_hole_size vm_start)

mm->cached_hole_size = vma->vm_start - addr;//更新当前空洞长度

addr = vma->vm_end;

}

}

TASK_UNMAPPED_BASE就是mmap时的地址限制,我们看一眼TASK_UNMAPPED_BASE的定义:

#define TASK_UNMAPPED_BASE (PAGE_ALIGN(TASK_SIZE / 3))

就是1g的位置,即0x40000000的位置。可是这种限制并不是强制的,如果我做以下程序:

#include <sys><br>int main() <br>{ <br> char * buf; <br> buf = (char*)mmap(0x30000000,1024,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS,-1,0); <br>} <br>用gdb调试可以看出buf的地址确实是0x30000000,也就是0x40000000以下的地址,这么说,linux的内存布局并不是强制用户执行 的,把策略完全留给用户,内核真是惯坏了用户,仅仅提供给用户一个建议而已。说完了sys_mmap2的逻辑简单谈谈sys_brk就可以了,每个
mm_struct都有一个brk字段,这个字段记录着用户堆的位置,每次用户调用brk的时候sys_brk都回扩展或者缩减堆空间,本质上也是映射 vm_area_struct结构,而brk的限制是在elf文件里面确定的,elf文件格式规定了堆,bss变量,栈在哪里,但是这些都不是强制的。 <br>通过这里的分析,我们看到,linux的内存布局是非常灵活的,看到很多人问怎么把内核空间扩展成2g,正如windows一样,这个其实是很简单的,将 sys_mmap和sys_brk的检测条件一改就可以,实际不用改动任何这些代码,仅仅改TASK_SIZE宏就可以了,用户空间分配内存的唯一限制就
是不能超越内核边界,别的限制就不是那么重要了,那只是用户程序自己的事情,而分配用户内存就是映射vm_area_struct结构,进一步仅仅在 sys_mmap2和sys_brk里会有如此动作,因此扩展内核空间的任务就会很简单,那么内核的内存怎么映射呢?这就完全是内核的事情了,无非有两种 方式,一种是一一映射,另一种是非线性映射(包括类似高端内存的临时映射方式)。</sys>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  内存管理