Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3844307
  • 博文数量: 146
  • 博客积分: 3918
  • 博客等级: 少校
  • 技术积分: 8584
  • 用 户 组: 普通用户
  • 注册时间: 2010-10-17 13:52
个人简介

个人微薄: weibo.com/manuscola

文章分类

全部博文(146)

文章存档

2016年(3)

2015年(2)

2014年(5)

2013年(42)

2012年(31)

2011年(58)

2010年(5)

分类:

2011-05-31 23:56:27

正如深入linux内核架构中提到的,插入新的内存区域之前,首先要确认,虚拟内存空间中有足够的空间,可用于给定的长度。

这个判断虚拟内存空间中是否有足够的空间的工作是有arch_get_unmapped_area来实现。
系统中的每一个进程都有一个struct mm_struct的实例,这个实例的作用是保存了进程的内存管理信息。
我们看到进程中能够找到查找空闲虚拟内存的方法  arch_get_unmapped_area。

void arch_pick_mmap_layout(struct mm_struct *mm)
{
/*
* Fall back to the standard layout if the personality
* bit is set, or if the expected stack growth is unlimited:
*/
if (mmap_is_legacy()) {
mm->mmap_base = TASK_UNMAPPED_BASE;
mm->get_unmapped_area = arch_get_unmapped_area;
mm->unmap_area = arch_unmap_area;
} else {
mm->mmap_base = mmap_base();
mm->get_unmapped_area = arch_get_unmapped_area_topdown;
mm->unmap_area = arch_unmap_area_topdown;
}
}

--------------------------------------------------------------------------------------
介绍 arch_get_unmapped_area 这个函数

1 首先是介绍用户进程虚拟地址空间的分布情况。

我们知道 3G-4G 的地址空间的分配给Kernel用的,用户进程可用的地址空间是0-3G
这个分界线就是 TASK_SIZE
TASK_SIZE 的大小为 0xc0000000
当然我是描述的是IA-32系统 。严格意义上用户地址空间并不是0~3G,

用户的地址空间里面分成  代码段、堆,内存映射的区域,栈,
其中 
A  代码段的起始地址并不是0,取决于ELF标准。 对于IA-32 ,代码段的起始地址是0x08048000
    从0地址到text段起始地址之间的部分,大约128M用于捕获NULL指针。
B  堆,没啥好讲的
C 内存映射的起始地址是 mmap->base,一般是 TASK_SIZE的1/3。即从1G开始
D 栈是从上往下生长的,栈的起始位置记为 STACK_TOP,一般起始地址是用户空间的最高地址 3G,当然
  这个位置是可以调整一个随机数,每次进程启动的时候,栈顶位置随机改变。



通过上面得描述可以看到,这种布局存在的问题
 用户的堆空间可用的比较少,因为 内存映射的起始位置在1G,堆 长着长着就长到内存映射mmap区域里面去了,  这是不能允许的。

所以后来提出了新的布局方案,就是将内存映射区域 mmap区域上移,让它更靠近 栈,同时要求mmap向下生长。
这种布局就是上面代码中的mm->get_unmapped_area = arch_get_unmapped_area_topdown;。

本文之描述mmap区域向上的经典布局。



2 分析函数arch_get_unmapped_area
addr是个参考位置,如果首先align,按页面对齐。
在寻找vma,满足vma的end_addr 大于 addr。
如果找到了这个vma,并且,空间没有交叉, 即 addr + len <= vma->vm_start,
那么OK,打道回府。
if (addr) {
addr = PAGE_ALIGN(addr);
vma = find_vma(mm, addr);
if (TASK_SIZE - len >= addr &&
    (!vma || addr + len <= vma->vm_start))
return addr;
}
  
接下来的代码比较诡异,其实分析mmap区域中hole的来源,就比较好理解了。
查找的起始位置有两种情况:
如果我要分配的长度比mm->cached_hole_size长,也就是说 mmap区域中记录的那个空洞不够长,
我无法把使用这个空洞,因为它不够长,OK,我从空洞开始找起。

如果mmap记录的空洞很大,我完全可以把我要分配的空间分配到洞里面,充分利用空间,
那么,我要从头开始分配,因为,找着找着,很可能就找到这个空洞了,然后就充分利用空洞的空间。

这种机制其实是为了防止洞越来越多。

至于这个洞的起始地址和洞的大小是怎么来的,后面的代码可以清晰的看出来。
if (len > mm->cached_hole_size) {
        start_addr = addr = mm->free_area_cache;
} else {
        start_addr = addr = TASK_UNMAPPED_BASE;
        mm->cached_hole_size = 0;
}

有了遍历整个mmap空间的起始位置,我们可以使用find_vma开始遍历了。
if (TASK_SIZE - len < addr)  这个判断条件是指 一步小心找到了顶 ,3G的位置,惨了,没找的,都找到
天涯海角去了。

如果是从 TASK_UNMAPPED_BASE 开始找的,表示没戏了,找不到空间了返回用户 ENOMEM。

但是如果是从上面提到的洞的位置开始找的,因为只找了洞后面的空间,虽然没找到,但是洞前面可能会找到,从TASK_UNMAPPED_BASE 继续找。


什么情况下是表示找到了我要的空间呢:
1 !vma   表示现存的空间,没有结束位置没有在addr之前的,表示addr到了一片边缘地带,我最偏远,
  OK ,这个无人耕种的地就为我用了,返回addr
2  addr + len <= vma->vm_start
 这个条件的意思是,不但 vm_end在我上面,vm_start也在我上面,OK,我从addr开始伸腿,腿长len,
 不会骚扰到vma,因为我们没有交集。 这种情况也很好,表示我找到一个空间来伸腿。即addr 。
这种情况下可能会有洞,我伸腿伸到的最远处,到vma的起始位置,这篇区域属于无人认领的区域,
所以把洞的信息更新一下,洞的起始位置为 addr + len;

if (!vma || addr + len <= vma->vm_start) {
/*
* Remember the place where we stopped the search:
*/
mm->free_area_cache = addr + len;
return addr;
}


如果这两种条件都不满足,表示还没找到,需要继续找,因为你当前的位置 你尝试了下伸腿,发现伸到了
vma的区域里面去了,那么你不能用这块小空地。这就有可能有个空洞,(当然也可能没有空洞,就是说你坐在了vma的地盘之内,你伸不伸腿都侵犯了vma的地盘。这并不重要)。既然有空洞,就有可能目前这个洞比之前的洞还大。这种情况下需要更新洞的信息 。

if (addr + mm->cached_hole_size < vma->vm_start)
       mm->cached_hole_size = vma->vm_start - addr;

然后从vma_end继续找,即代码addr = vma->vm_end;

-------------------------------------------------------------------------------------
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)
return addr;

if (addr) {
addr = PAGE_ALIGN(addr);
vma = find_vma(mm, addr);
if (TASK_SIZE - len >= addr &&
   (!vma || addr + len <= vma->vm_start))
return addr;
}
if (len > mm->cached_hole_size) {
       start_addr = addr = mm->free_area_cache;
} else {
       start_addr = addr = TASK_UNMAPPED_BASE;
       mm->cached_hole_size = 0;
}

full_search:
for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {
/* At this point:  (!vma || addr < vma->vm_end). */
if (TASK_SIZE - len < addr) {
/*
* Start a new search - just in case we missed
* some holes.
*/
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 <= vma->vm_start) {
/*
* Remember the place where we stopped the search:
*/
mm->free_area_cache = addr + len;
return addr;
}
if (addr + mm->cached_hole_size < vma->vm_start)
       mm->cached_hole_size = vma->vm_start - addr;
addr = vma->vm_end;
}
}

阅读(6878) | 评论(4) | 转发(2) |
给主人留下些什么吧!~~

GFree_Wind2011-06-01 23:29:00

Bean_lee: 我的理解是这样的,你说的BIOS 显卡ROM等是映射到的是linux 内核空间,
3G 以上的空间。因为 Linux 内核的代码段是从c0100000开始,前面的1M空间是留给 BIOS 等.....
是我记错了,那个BIOS确实是映射到内存空间的开头部分。
找到了一种解释,为啥是从0x0804800开始。
The reason that SVR4 chose that particular address is that the stack top was located at 0x08000000 (growing downward, of course), and then the area between 0x08000000 and 0x08048000 was reserved for libc and possibly other system service code.

Bean_lee2011-06-01 20:58:16

GFree_Wind: A  代码段的起始地址并不是0,取决于ELF标准。 对于IA-32 ,代码段的起始地址是0x08048000
    从0地址到text段起始地址之间的部分,大约128M用于捕获NULL指针。
我的理解是这样的,你说的BIOS 显卡ROM等是映射到的是linux 内核空间,
3G 以上的空间。因为 Linux 内核的代码段是从c0100000开始,前面的1M空间是留给 BIOS 等的。

我讨论的用户空间。深入linux内核架构中137页有相关的描述。

当然,我的理解不一定对,需要大家交流切磋。

GFree_Wind2011-06-01 12:24:08

A  代码段的起始地址并不是0,取决于ELF标准。 对于IA-32 ,代码段的起始地址是0x08048000
    从0地址到text段起始地址之间的部分,大约128M用于捕获NULL指针。

--------------是取决于ELF标准吗?我觉得更是由于平台的限制。前128M,应该是留给BIOS,还有I/O端口映射,以及其他的一些用途。