引用牛人的一个表格(本人绘制能力实在有限,引自: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)
{
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 *, unsigned 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>