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

来源:互联网 发布:剑侠情缘3正太捏脸数据 编辑:程序博客网 时间:2024/05/21 08:00

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



0 0