内存描述符与线性区
用户的等级与内核的等级是不同的,这个原因造成了当用户进程请求动态内存的时候,会被“拖延”,同时内核对用户进程也充满警惕,时刻准备捕获用户进程引起的所有寻址错误。用户进程所获得的空间是虚拟内存,是被称为“线性区”的空间,是一堆线性地址空间的使用权。而描述这一框架的数据结构就是内存描述符,线性区对象。内存描述符在slab分配器中,内存描述符彼此形成一个链表,而其中的字段的功能之一就是指向线性区的开始,和最后一个引用的线性区。也就是字段:mmap,mmap_cache。而线性区也彼此形成一个链表。mmap指向的就是当前内存描述符之下的线性区链表头。而线性区对象中的字段:vm_start,vm_end就是用来标示在线性地址空间的内存段信息的。所以在用户看来,这个结构的结果就是使用的一段线性的非连续内存区。线性区对象链表的作用就是将这些非连续的内存区变成看起来连续的一段线性区地址空间。也就是每个线性区都由一组号码连续的页所组成。
线性区对象中的字段:vm_mm,所指向的就是线性区所在的内存描述符,通过这几个关键结构,内存描述符,线性区对象,线性区地址空间,三者之间形成彼此联系。而在进程中,他的描述符中字段:mm,就是指向内存描述符mm_struct的。以上就是从进程到线性地址空间的框架。
但是需要特别注意的一点是,在线性区对象看来,他们是彼此链接成一个链表的,但是在内存描述符中有个字段:mm_rb.同样在线性区对象中也有个字段:vm_rb.这两个的作用就是红黑树的头。之所以在链表之外再形成一个树形结构的目的,就在于减少对链表元素操作时的系统开销。也就是可以更快的定位,而不用扫描链表。
#define allocate_mm() (kmem_cache_alloc(mm_cachep, SLAB_KERNEL))
#define free_mm(mm) (kmem_cache_free(mm_cachep, (mm)))
struct mm_struct * mm_alloc(void)
{
struct mm_struct * mm;
mm = allocate_mm();
if (mm) {
memset(mm, 0, sizeof(*mm));
mm = mm_init(mm);
}
return mm;
}
通过一个申请内存描述符的函数,可以看到,追中还是还原到kmem_cache_alloc,也就是说,这些是被保存在slab分配器高速缓存中。联想在前面讨论slab分配器的矿价事,slab描述符之后是很多的同类slab对象。联系伙伴系统的讨论,基本就可以大体构建起一个内存管理的基本框架了。所有的内存描述符组成一个双向链表,而init_mm是初始化阶段进程0所使用的内存描述符。
描述线性区对象的数据结构是vm_area_struct.前面谈到,线性区是一堆连续的页来组成的。而同样,这些页也有很多特殊的标志。在字段:vm_flags中。以表示这个线性区中页的信息,以及线性区的信息。
#define VM_READ 0x00000001 /* currently active flags */
#define VM_WRITE 0x00000002
#define VM_EXEC 0x00000004
#define VM_SHARED 0x00000008
#define VM_MAYREAD 0x00000010 /* limits for mprotect() etc */
#define VM_MAYWRITE 0x00000020
#define VM_MAYEXEC 0x00000040
#define VM_MAYSHARE 0x00000080
#define VM_GROWSDOWN 0x00000100 /* general info on the segment */
#define VM_GROWSUP 0x00000200
#define VM_SHM 0x00000400 /* shared memory area, don't swap out */
#define VM_DENYWRITE 0x00000800 /* ETXTBSY on write attempts.. */
#define VM_EXECUTABLE 0x00001000
#define VM_LOCKED 0x00002000
#define VM_IO 0x00004000 /* Memory mapped I/O or similar */
/* Used by sys_madvise() */
#define VM_SEQ_READ 0x00008000 /* App will access data sequentially */
#define VM_RAND_READ 0x00010000 /* App will not benefit from clustered reads */
#define VM_DONTCOPY 0x00020000 /* Do not copy this vma on fork */
#define VM_DONTEXPAND 0x00040000 /* Cannot expand with mremap() */
#define VM_RESERVED 0x00080000 /* Don't unmap it from swap_out */
#define VM_ACCOUNT 0x00100000 /* Is a VM accounted object */
#define VM_HUGETLB 0x00400000 /* Huge TLB Page VM */
#define VM_NONLINEAR 0x00800000 /* Is non-linear (remap_file_pages) */
对于线性区的操作也是至关重要的。有几个函数负责这些工作。
find_vma,函数的名字寻找虚拟内存区。这就是作用,而两个参数mm,addr.前者是进程内存描述符地址,而后者是线性地址。通过第二个if语句,可以看出这个函数的具体功能:vma->vm_end > addr && vma->vm_start <= addr,当这两个为真的时候,那么addr,一定是在当前线性区中的,此时直接返回vma = mm->mmap_cache就可以了。而当不在当前线性区中的时候,就需要找到这个区。此时就是用红黑树来找。rb_node = mm->mm_rb.rb_node;这句就是将从内存描述符中的红黑树根节点来开始。while循环体的作用就在于此。rb_entry函数的作用就是从指向红黑树中一个节点的指针导出相应线性区描述符的地址。在函数中是从根节点开始判断的。当end>addr的时候,那么就要从当前节点开始,向他的左孩子寻找。直到start<addr的时候,跳出,否则以左孩子为当前节点,继续循环。也由此可以看出,此函数的作用就是找到end>addr的第一个线性区位置。然后设置指针,mm->mmap_cache = vma,表示最后一个引用的线性区对象地址。return vma。
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
if (mm) {
vma = mm->mmap_cache;
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
struct rb_node * rb_node;
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
struct vm_area_struct * vma_tmp;
vma_tmp = rb_entry(rb_node,struct vm_area_struct, vm_rb);
if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}
而通过以上函数,类似的还有一个函数:此函数通过引用上述函数,并修改参数变为start_addr,以及在判断中使用end_addr <= vma->vm_start,作用很明显,就是找到一个与start_add开始的地址相重叠的线性区。而类似的函数:insert_vm_area,向内存描述符链表中插入一个线性区,方式也同样。
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{
struct vm_area_struct * vma = find_vma(mm,start_addr);
if (vma && end_addr <= vma->vm_start)
vma = NULL;
return vma;
}
而分配与释放地址区间,有两个关键函数:do_mmap(),do_munmap().
static inline unsigned long do_mmap(struct file *file, unsigned long addr,unsigned long len, unsigned long prot,unsigned long flag, unsigned long offset)
{
…………
if (!(offset & ~PAGE_MASK))
ret = do_mmap_pgoff(file, addr, len, prot, flag, offset >> PAGE_SHIFT);
out:
return ret;
}
函数中,只是做了一些初步检查,然后就引用函数do_mmap_pgoff,这是个关键函数。他的参数所代表的信息是很丰富的。
file:这是一个文件描述符指针。
offset:文件偏移量。
len:线性地址区间的长度。
prot:指定这个线性区所包含页的访问权限。
#define PROT_READ 0x1 /* page can be read */
#define PROT_WRITE 0x2 /* page can be written */
#define PROT_EXEC 0x4 /* page can be executed */
#define PROT_SEM 0x8 /* page may be used for atomic ops */
#define PROT_NONE 0x0 /* page can not be accessed */
#define PROT_GROWSDOWN 0x01000000 /* mprotect flag: extend change to start of growsdown vma */
#define PROT_GROWSUP 0x02000000 /* mprotect flag: extend change to end of growsup vma */
flag:线性区的标志。
#define MAP_SHARED 0x01 /* Share changes */
#define MAP_PRIVATE 0x02 /* Changes are private */
#define MAP_TYPE 0x0f /* Mask for type of mapping */
#define MAP_FIXED 0x10 /* Interpret addr exactly */
#define MAP_ANONYMOUS 0x20 /* don't use a file */
#define MAP_GROWSDOWN 0x0100 /* stack-like segment */
#define MAP_DENYWRITE 0x0800 /* ETXTBSY */
#define MAP_EXECUTABLE 0x1000 /* mark it as an executable */
#define MAP_LOCKED 0x2000 /* pages are locked */
#define MAP_NORESERVE 0x4000 /* don't check for reservations */
#define MAP_POPULATE 0x8000 /* populate (prefault) pagetables */
#define MAP_NONBLOCK 0x10000 /* do not block on IO */
do_mmap_pgoff函数虽然较长,但是却容易阅读,程序的开始,对几个参数进行了验证,是对于请求能否通过的检查。所涉及的参数有file、prot、len、mm->map_count。都没有问题后,引用函数get_unmapped_area,得到新线性区的线性地址区间。紧接着是这一句:
vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) | mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
vm_flags中最后存放的是新线性描述符的标志。是通过prot,flags来计算而出的。以上就是准备工作,如果要分配新的描述符,那么肯定要先判断所申请的条件能否被满足,然后是申请新的线性地址空间,计算他的区间标志。这一切都完成后,就是找到插入位置。此时引用了函数:vma = find_vma_prepare(mm, addr, &prev, &rb_link, &rb_parent);这个函数和find_vm非常相似,只是所用是找到处于新区之前的线性区的位置和在红黑树中新区的位置。此时还有一个问题需要检验,就是所插入新区是否适合插入进程地址空间,此时就是是否符合进程地址空间大小的问题。
(mm->total_vm << PAGE_SHIFT)+len> current->signal->rlim[RLIMIT_AS].rlim_cur判断语句中的条件就是这个目的。也就是判断插入新的线性区后,进程地址空间的大小是否超过了进程描述符中的规定值。然后是关于页面方面的判断。当都完成没有问题后,就为这个新区申请一个新的线性区对象描述符。
vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);这个函数很熟悉,也同时表明,这就是位于slab分配器中的。以下就是对这个描述符的初始化语句。vma是vm_area_cachep结构的。
memset(vma, 0, sizeof(*vma));
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = protection_map[vm_flags & 0x0f];
vma->vm_pgoff = pgoff;
然后将其插入线性区链表和红黑树中,以上就是这个分配线性区函数的大体框架和思路。