Linux进程虚拟地址空间管理2

2017-04-12

前篇文章对Linux进程地址空间的布局以及各个部分的功能做了简要介绍,本文主要对各个部分的具体使用做下简要分析,主要涉及三个方面:1、MMAP文件的映射过程 2、用户 内存的动态分配


Linux进程虚拟地址空间管理2

Text:进程代码

Data:全局和静态数据区,但是已初始化

BSS:全局和静态数据区,但是未初始化

堆:动态内存分配

栈:函数参数,局部变量

1、MMAP文件映射过程

MMAP文件映射其实就是在磁盘文件和进程虚拟地址空间建立一种关系,用户空间通过调用mmap函数实现,mmap()是C运行库函数,实现把一个文件的某个区间映射到进程虚拟地址的某个区间,函数原型如下:void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

。在内核中对应于sys_mmap系统调用。该系统调用会调用sys_mmap_pgoff函数,而最终do_mmap_pgoff函数实现具体的功能。该函数主体包含两部分,一部分是获取到可用的虚拟地址 空间,一部分实现具体的映射。咱们一层一层的往下看

asmlinkage long sys_mmap(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, off_t off)
{
if (offset_in_page(off) != )
return -EINVAL; return sys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}

经过sys_mmap_pgoff函数,内部调用了vm_mmap_pgoff函数,在经过安全检查之后,调用do_mmap_pgoff函数,此时真正的工作开始了。

函数开始前期是针对保护位和flags做的一些处理

if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && (file->f_path.mnt->mnt_flags & MNT_NOEXEC)))
prot |= PROT_EXEC; if (!len)
return -EINVAL; if (!(flags & MAP_FIXED))
addr = round_hint_to_min(addr); /* Careful about overflows.. */
len = PAGE_ALIGN(len);
if (!len)
return -ENOMEM; /* offset overflow? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EOVERFLOW; /* Too many mappings? */
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;

检查下PROT_READ是否意味着可执行,如果是则给prot加上PROT_EXEC;如果映射的长度len为0,则返回;如果映射的地址可根据情况修正,则调用round_hint_to_min函数进行修正,具体来讲先对hint进行页对齐,如果对其后的地址小于mmap_min_addr,则根据mmap_min_addr来指定地址;同时对len也进行页对齐操作;然后检查页内偏移是否溢出,坦白讲这里我的确不太明白,pgoff加上一个树还会小于pgoff?不太可能吧;接着检查了当前进程映射的数目是否超额,如果超额,则返回。

经过一系列的检查,就调用get_unmapped_area(file, addr, len, pgoff, flags);函数获取一段可用的虚拟区间,进入函数内部

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); unsigned long error = arch_mmap_check(addr, len, flags);
if (error)
return error; /* Careful about overflows.. */
if (len > TASK_SIZE)
return -ENOMEM;
/*在进程结构的mm_stract中,有函数指针*/
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))
return addr; if (addr > TASK_SIZE - len)
return -ENOMEM;
if (addr & ~PAGE_MASK)
return -EINVAL; addr = arch_rebalance_pgtables(addr, len);
error = security_mmap_addr(addr);
return error ? error : addr;
}

这里内部检查了映射的长度是否超额,即是否超过TASK_SIZE,如果合理,则获取get_area函数,该函数在mm_struct中指定,当然如果file结构不为空且file操作表中也指定了该函数,则首选file结构中的函数。如果最终得到的addr>TASK_SIZE - len,即区间末尾溢出了,则返回;如果addr不是页对齐的,也返回;不出意外,这里就返回地址addr了,后面的arch_rebalance_pgtables发现并没有做什么,还有security_mmap_addr在没有第三方的安全模块时,才会进行适当的安全检查;返回的结果要么是正确的addr,要么是返回的错误码,那么到do_mmap_pgoff函数中继续,if (addr & ~PAGE_MASK)其实是验证是否是错误码,如果错误就直接返回了;到这里获取地址的工作就告一段落。

    addr = get_unmapped_area(file, addr, len, pgoff, flags);
if (addr & ~PAGE_MASK)
return addr; /* Do simple checking here so the lower-level routines won't have
* to. we assume access permissions have been handled by the open
* of the memory object, so we don't do any here.
*/
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; if (flags & MAP_LOCKED)
if (!can_do_mlock())
return -EPERM; /* mlock MCL_FUTURE? */
if (vm_flags & VM_LOCKED) {
unsigned long locked, lock_limit;
locked = len >> PAGE_SHIFT;
locked += mm->locked_vm;
lock_limit = rlimit(RLIMIT_MEMLOCK);
lock_limit >>= PAGE_SHIFT;
if (locked > lock_limit && !capable(CAP_IPC_LOCK))
return -EAGAIN;
}

接下来就是设置vm_flags,这里貌似并没有做什么检查,直接就设置了几乎所有的标志。如果设置了MAP_LOCKED,还要检查当前是否可以满足条件:即当前是否有锁定内存的能力,在这还要RLIMIT_MEMLOCK要大于0,否则返回;如果vm_flags包含了VM_LOCKED,需要当前lock后,lock的页面是否超额。接下来就要进行具体映射了,当然前期还是进行一些检查。这里分为两部分,文件文件映射和匿名映射。

inode = file ? file_inode(file) : NULL;

    if (file) {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
return -EACCES; /*
* Make sure we don't allow writing to an append-only
* file..
*/
if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
return -EACCES; /*
* Make sure there are no mandatory locks on the file.
*/
if (locks_verify_locked(inode))
return -EAGAIN; vm_flags |= VM_SHARED | VM_MAYSHARE;
if (!(file->f_mode & FMODE_WRITE))
vm_flags &= ~(VM_MAYWRITE | VM_SHARED); /* fall through */
case MAP_PRIVATE:
if (!(file->f_mode & FMODE_READ))
return -EACCES;
if (file->f_path.mnt->mnt_flags & MNT_NOEXEC) {
if (vm_flags & VM_EXEC)
return -EPERM;
vm_flags &= ~VM_MAYEXEC;
} if (!file->f_op || !file->f_op->mmap)
return -ENODEV;
break; default:
return -EINVAL;
}
} else {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
/*
* Ignore pgoff.
*/
pgoff = ;
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
/*
* Set pgoff according to addr for anon_vma.
*/
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
} /*
* Set 'VM_NORESERVE' if we should not account for the
* memory use of this mapping.
*/
if (flags & MAP_NORESERVE) {
/* We honor MAP_NORESERVE if allowed to overcommit */
if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
vm_flags |= VM_NORESERVE; /* hugetlb applies strict overcommit unless MAP_NORESERVE */
if (file && is_file_hugepages(file))
vm_flags |= VM_NORESERVE;
}

如果函数参数中file不为空,则肯定为文件映射,那么获取其对应的inode节点。进入switch,根据不同的映射类型,进行检查,然后主要是设置vm_flags。检查类型如下:

MAP_SHARED:

如果保护位中允许写而文件操作模式中不允许,则返回;如果文件模式允许写,而inode节点是追加型节点,则返回;如果inode被强制加锁,则返回;如果通过这些检查,则给vm_flags添加VM_SHARED , VM_MAYSHARE;而file没有写权限,则从vm_flags去除VM_MAYWRITEVM_SHARED。

MAP_PRIVATE:

如果为私有映射,如果文件不允许读,则返回;如果文件对应的文件系统没有执行权且vm_flags中包含了执行权,则返回,如果没有包含,则从vm_flags去除VM_MAYEXEC。接下来如果file中的f_op为空,或者f_op中的mmap函数为空,则返回。

如果file为空,则表明为匿名映射,映射的目的不是映射文件,而是划定一块内存区,这种情况下进行下面判断

MAP_SHARED:就忽略pgoff,增加vm_flags的共享权限。

MAP_PRIVATE:设置pgoff为 addr >> PAGE_SHIFT

接下来是对MAP_NORESERVE的判断,根据情况判断是否给vm_flags添加VM_NORESERVE,前期工作就到此为止了,剩下的调用了 mmap_region(file, addr, len, vm_flags, pgoff);

if (!may_expand_vm(mm, len >> PAGE_SHIFT)) {
unsigned long nr_pages; /*
* MAP_FIXED may remove pages of mappings that intersects with
* requested mapping. Account for the pages it would unmap.
*/
if (!(vm_flags & MAP_FIXED))
return -ENOMEM; nr_pages = count_vma_pages_range(mm, addr, addr + len); if (!may_expand_vm(mm, (len >> PAGE_SHIFT) - nr_pages))
return -ENOMEM;
}

第一部分检查了地址空间的限制,如果已经映射的页面加上本次需要的页面数还小于指定额度,则没问题,否则进入if判断,在这里,因为已有的空间已经不足,如果没有指定MAP_FIXED,就不能把发生冲突的map撤销,就只能返回错误了;如果指定了MAP_FIXED,就调用count_vma_pages_range函数计算发生冲突的页面数,然后预先作为这些页面可用再次计算地址空间是否充足,如果仍然不够,则只能返回错误,否则可以继续。

munmap_back:
if (find_vma_links(mm, addr, addr + len, &prev, &rb_link, &rb_parent)) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
goto munmap_back;
}

上面验证结束后,就进入了munmap_back标记,这里就是找到冲突的映射,然后撤销。但是这里还真有疑问,因为看find_vma_links函数,要么返回0,要么返回错误,不会返回正值,那么这里就if判断始终是false,不晓得什么情况会进去!!

if (accountable_mapping(file, vm_flags)) {
charged = len >> PAGE_SHIFT;
if (security_vm_enough_memory_mm(mm, charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
} /*
* Can we just expand an old mapping?
*/
vma = vma_merge(mm, prev, addr, addr + len, vm_flags, NULL, file, pgoff, NULL);
if (vma)
goto out;
/*
* Determine the object being mapped and call the appropriate
* specific mapper. the address has already been validated, but
* not unmapped, but the maps are removed from the list.
*/
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
} vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
INIT_LIST_HEAD(&vma->anon_vma_chain);

accountable_mapping函数检查内存的可用性,具体还是不太明白。然后调用vma_merge函数尝试扩展一个旧的vma结构,如果可以就goto到out,否则需要新创建一个vma,并进行设置。vma结构通过slab缓存管理,这里直接获取一个即可。

if (file) {
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
goto free_vma;
if (vm_flags & VM_DENYWRITE) {
error = deny_write_access(file);
if (error)
goto free_vma;
correct_wcount = ;
}
vma->vm_file = get_file(file);
/*调用f_op->mmap进行映射*/
error = file->f_op->mmap(file, vma);
if (error)
goto unmap_and_free_vma; /* Can addr have changed??
*
* Answer: Yes, several device drivers can do it in their
* f_op->mmap method. -DaveM
* Bug: If addr is changed, prev, rb_link, rb_parent should
* be updated for vma_link()
*/
WARN_ON_ONCE(addr != vma->vm_start); addr = vma->vm_start;
pgoff = vma->vm_pgoff;
vm_flags = vma->vm_flags;
} else if (vm_flags & VM_SHARED) {
if (unlikely(vm_flags & (VM_GROWSDOWN|VM_GROWSUP)))
goto free_vma;
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}

接下来又需要分情况讨论,如果是文件映射,会调用file->f_op->mmap(file, vma)函数进行映射,否则对于匿名映射,调用shmem_zero_setup函数对其进行设置。

vma_link(mm, vma, prev, rb_link, rb_parent);
file = vma->vm_file; /* Once vma denies write, undo our temporary denial count */
if (correct_wcount)
atomic_inc(&inode->i_writecount);
out:
perf_event_mmap(vma); vm_stat_account(mm, vm_flags, file, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
if (!((vm_flags & VM_SPECIAL) || is_vm_hugetlb_page(vma) ||
vma == get_gate_vma(current->mm)))
mm->locked_vm += (len >> PAGE_SHIFT);
else
vma->vm_flags &= ~VM_LOCKED;
} if (file)
uprobe_mmap(vma); return addr; unmap_and_free_vma:
if (correct_wcount)
atomic_inc(&inode->i_writecount);
vma->vm_file = NULL;
fput(file); /* Undo any partial mapping done by a device driver. */
unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
charged = ;
free_vma:
kmem_cache_free(vm_area_cachep, vma);
unacct_error:
if (charged)
vm_unacct_memory(charged);
return error;
}

2、用户内存的动态分配

用户内存的动态分配主要从来来自于两个地方:堆、MMAP区域,C运行时库对用户提供了同一的动态分配接口malloc,而运行库同样有自己的内存管理器,内存管理器根据分配内存的大小采取不同的分配方式,具体来讲调用不同的系统调用

当分配的内存<128KB时,从进程地址空间的堆空间分配。涉及函数brk(),sbrk()。

当分配的内存>=128KB时,从进程地址空间的MMAP区域分配。涉及函数mmap()。

这里先介绍下用户空间的内存管理器,在一个进程首次调用malloc申请内存的时候,其会首次向内核申请远大于用户申请额度的内存区,申请之后把用户请求的大小返回给用户,而剩下的由内存管理器管理,在下次申请的时候就优先从这里申请而不用陷入到内核,这样就减少了和内核交互的次数,从而提高性能。当前几种常用的内存管理器有ptmallloc,temalloc和jcmalloc。

堆分配

堆的分配起始于brk系统调用,我们就跟着这条线看下

SYSCALL_DEFINE1(brk, unsigned long, brk)
{
unsigned long rlim, retval;
unsigned long newbrk, oldbrk;
struct mm_struct *mm = current->mm;
unsigned long min_brk;
bool populate; down_write(&mm->mmap_sem); #ifdef CONFIG_COMPAT_BRK
/*
* CONFIG_COMPAT_BRK can still be overridden by setting
* randomize_va_space to 2, which will still cause mm->start_brk
* to be arbitrarily shifted
*/
if (current->brk_randomized)
min_brk = mm->start_brk;
else
min_brk = mm->end_data;
#else
min_brk = mm->start_brk;
#endif
if (brk < min_brk)
goto out; /*
* Check against rlimit here. If this check is done later after the test
* of oldbrk with newbrk then it can escape the test and let the data
* segment grow beyond its set limit the in case where the limit is
* not page aligned -Ram Gupta
*/
rlim = rlimit(RLIMIT_DATA);
if (rlim < RLIM_INFINITY && (brk - mm->start_brk) +
(mm->end_data - mm->start_data) > rlim)
goto out; newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
if (oldbrk == newbrk)
goto set_brk; /* Always allow shrinking brk. */
if (brk <= mm->brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk))
goto set_brk;
goto out;
} /* Check against existing mmap mappings. */
if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
goto out; /* Ok, looks good - let it rip. */
if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
goto out; set_brk:
mm->brk = brk;
populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != ;
up_write(&mm->mmap_sem);
if (populate)
mm_populate(oldbrk, newbrk - oldbrk);
return brk; out:
retval = mm->brk;
up_write(&mm->mmap_sem);
return retval;
}

函数不太长,就一次性列举了。brk参数是数据段的终止位置。如果配置了栈兼容,代码首先判断是否有随机地址,如果有则指定min_brk为mm->start_brk,否则指定为 mm->end_data。这里可以结合前面咱们的图。如果没有栈兼容,则直接指定为mm->start_brk。作为堆的起始,申请地址肯定必须大于等于起始地址的。同时每个进程有个RLIMIT_DATA的限制,用于限制data段的长度,这里主要包括堆、已经初始化的变量区、未初始化的变量区,分配后堆的大小加上已经初始化的数据区的大小不能大于数据段的大小限制,否则不允许。但是这里我没明白为啥没加上未经初始化的区的大小。如果通过就对新的brk位置和旧的位置进行对齐操作。如果两个相等,就直接设置brk,实际上是返回当前堆结束地址;如果newbrk<oldbrk就需要释放多余的堆了,调用do_munmap函数撤销映射。接下来就检查下申请的区间(本次扩充的堆空间)是否已经存在映射(存在VMA),如果存在就直接返回,否则调用do_brk函数进行映射,然后移动mm_struct中的brk指针,并返回最终的brk结束地址,到这里可以看到,堆空间的增长和减少都是线性的,即不存在从中间某个位置分配或者回收的情况,而这一机制需要用户空间的内存分配器进行保证,我们常见的malloc和free并不会直接和堆打交道,而是直接和用户空间的分配器如ptmalloc交互,对于用户空间分配器,需要管理其从内核申请的堆空间,而面对内核,其需要做的规整一些,即不管是申请还是回收,直接给出堆的结束地址(细细品味)。do_brk函数即mmap函数的实现很类似,具体可参考前面的分析。

MMAP分配

当用户申请的空间大于128kb时,内存管理器会直接调用mmap函数进行映射,不过不是映射文件,仅仅是占用进程地址空间的一段内存区。具体同样在前面的分析中,感兴趣可以参考。

参考:

linux3.10.1内核源码

上一篇:Linux计算机进程地址空间与内核装载ELF


下一篇:深入理解javascript函数进阶系列第一篇——高阶函数