linux内存布局的内核实现

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

Linux的内存模型,一般为:

地址

作用

说明

>=0xc000 0000

内核虚拟存储器

用户代码不可见区域

<0xc000 0000

Stack(用户栈)

ESP指向栈顶

?

空闲内存

>=0x4000 0000

文件映射区

?

<0x4000 0000

空闲内存

?

Heap(运行时堆)

通过brk/sbrk系统调用扩大堆,向上增长。

?

.data、.bss(读写段)

从可执行文件中加载

>=0x0804 8000

.init、.text、.rodata(只读段)

从可执行文件中加载

<0x0804 8000

保留区域

?

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

你不勇敢,没人替你坚强。

linux内存布局的内核实现

相关文章:

你感兴趣的文章:

标签云: