关于启动过程内存管理见《内存管理-之启动》
关于内核空间内存管理见《内存管理-之内核内存管理》
如果需要,内存管理五章整理成pdf了,下载地址http://download.csdn.net/detail/shichaog/8662135
进程的虚拟地址空间和内核的虚拟地址管理方法不一样,不论应用程序如何切换,内核始终是一个并且其一直驻留在内存中,而进程则不同,可以有多个进程同时驻留在内存中,并且从各个进程的角度来看,呈现的系统是一样的,并且它们并不会彼此干扰。
有一篇文章,《linux应用程序如何运行》分析的是应用程序调用execve()执行系统调用时发生的一些事,该文章有助于理解本章内容,图5.1的右下角给出了execve的主要功能。
5.1 进程准备知识
各进程的虚拟地址空间从0开始,最大到TASK_SIZE-1,这一范围就是通常所述的3G,从3G~4G-1是内核的地址空间,一个进程通常包括如下几个部分:
l 二进制可执行代码,比如hello.c编译得到的可执行代码,内核中将其称之为text。
l 程序使用的动态库,.so结尾的文件,该库是对系统调用的封装。
l 堆,存储全局和动态产生的数据。
l 栈,局部变量以及函数调用时对寄存器和返回地址的保存。
l 环境变量和命名参数
l 将文件内容映射到虚拟地址空间。
来看一个小例子:
hello.c
#include <stdio.h>
#include <unistd.h>
void main(void)
{
printf("\nHello, world ~!\n");
sleep(15);
printf("Goodby, world ~!\n");
}
编译
gcc –o hello hello.c
执行及输出结果
ge@u:~$ ./hello &
[1] 7931
ge@u:~$
Hello, world ~!
Goodby, world ~!
[1]+ Exit 17 ./hello
ge@u:~$
进程的内存使用情况,也可以使用pmapPID命令查看
ge@u:~$ cat /proc/7931/maps
08048000-08049000 r-xp 00000000 08:01 1447992 /home/ge/hello
08049000-0804a000 r--p 00000000 08:01 1447992 /home/ge/hello
0804a000-0804b000 rw-p 00001000 08:01 1447992 /home/ge/hello
b7545000-b7546000 rw-p 00000000 00:00 0
b7546000-b76ef000 r-xp 00000000 08:01 411089 /lib/i386-linux-gnu/libc-2.19.so
b76ef000-b76f0000 ---p 001a9000 08:01 411089 /lib/i386-linux-gnu/libc-2.19.so
b76f0000-b76f2000 r--p 001a9000 08:01 411089 /lib/i386-linux-gnu/libc-2.19.so
b76f2000-b76f3000 rw-p 001ab000 08:01 411089 /lib/i386-linux-gnu/libc-2.19.so
b76f3000-b76f6000 rw-p 00000000 00:00 0
b770c000-b770f000 rw-p 00000000 00:00 0
b770f000-b7710000 r-xp 00000000 00:00 0 [vdso]
b7710000-b7730000 r-xp 00000000 08:01 411098 /lib/i386-linux-gnu/ld-2.19.so
b7730000-b7731000 r--p 0001f000 08:01 411098 /lib/i386-linux-gnu/ld-2.19.so
b7731000-b7732000 rw-p 00020000 08:01 411098 /lib/i386-linux-gnu/ld-2.19.so
bf8e7000-bf908000 rw-p 00000000 00:00 0 [stack]
每个应用程序的起始地址都是一样的0x08048000(IA-32架构),如下查看cat进程的内存布局。
ge@u:~$ cat /proc/self/maps
08048000-08053000 r-xp 00000000 08:01 1966103 /bin/cat
08053000-08054000 r--p 0000a000 08:01 1966103 /bin/cat
08054000-08055000 rw-p 0000b000 08:01 1966103 /bin/cat
089c0000-089e1000 rw-p 00000000 00:00 0 [heap]
b7223000-b7388000 r--p 001c8000 08:01 1188875 /usr/lib/locale/locale-archive
b7388000-b7588000 r--p 00000000 08:01 1188875 /usr/lib/locale/locale-archive
b7588000-b7589000 rw-p 00000000 00:00 0
b7589000-b7732000 r-xp 00000000 08:01 411089 /lib/i386-linux-gnu/libc-2.19.so
b7732000-b7733000 ---p 001a9000 08:01 411089 /lib/i386-linux-gnu/libc-2.19.so
b7733000-b7735000 r--p 001a9000 08:01 411089 /lib/i386-linux-gnu/libc-2.19.so
b7735000-b7736000 rw-p 001ab000 08:01 411089 /lib/i386-linux-gnu/libc-2.19.so
b7736000-b7739000 rw-p 00000000 00:00 0
b774f000-b7750000 r--p 00855000 08:01 1188875 /usr/lib/locale/locale-archive
b7750000-b7752000 rw-p 00000000 00:00 0
b7752000-b7753000 r-xp 00000000 00:00 0 [vdso]
b7753000-b7773000 r-xp 00000000 08:01 411098 /lib/i386-linux-gnu/ld-2.19.so
b7773000-b7774000 r--p 0001f000 08:01 411098 /lib/i386-linux-gnu/ld-2.19.so
b7774000-b7775000 rw-p 00020000 08:01 411098 /lib/i386-linux-gnu/ld-2.19.so
bfefe000-bff1f000 rw-p 00000000 00:00 0 [stack]
它们对应的内存布局情况如图5.1所示。Vdso(virtualDynamic Shared Object)是一个比较小的共享库,内核动态将该库映射到所有应用程序的地址空间中。这主要是因为glibc更新太慢导致的,其详细的简介请看http://www.man7.org/linux/man-pages/man7/vdso.7.html。
图5.1 虚拟内存布局
每个进程都由一个structtask_struct结构体表示,其内存信息保存在struct mm_struct成员中,下面的结构体的部分成员的意义结合图5.1看更为清晰。
struct mm_struct {
struct vm_area_struct * mmap; /* 虚拟内存区域列表 */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache; /* 最近一次find_vma的结果*/
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endif
unsigned long mmap_base; /*Memory Mapping Segment起始地址*/
unsigned long task_size; /*进程虚拟地址空间大小,通常是3G */
/* if non-zero, the largest hole below free_area_cache */
unsigned long cached_hole_size;
/* first hole of size cached_hole_size or larger */
unsigned long free_area_cache;
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /*对该结构体的引用计数 */
int map_count; /* number of VMAs */
spinlock_t page_table_lock; /* Protects page tables and some counters */
struct rw_semaphore mmap_sem;
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
unsigned long hiwater_rss; /* High-watermark of RSS usage */
unsigned long hiwater_vm; /* High-water virtual memory usage */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long shared_vm; /* Shared pages (files) */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE */
unsigned long stack_vm; /* VM_GROWSUP/DOWN */
unsigned long def_flags;
unsigned long nr_ptes; /* Page table pages */
//见图5.1右侧,start_code、end_code代码段起止,start_data、end_data数据段起止。
unsigned long start_code, end_code, start_data, end_data;
//堆
unsigned long start_brk, brk, start_stack;
//参数和环境变量
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
/*
* Special counters, in some configurations protected by the
* page_table_lock, in other configurations by being atomic.
*/
struct mm_rss_stat rss_stat;
struct linux_binfmt *binfmt;
cpumask_var_t cpu_vm_mask_var;
/* Architecture-specific MM context */
mm_context_t context;
unsigned long flags; /* Must use atomic bitops to access the bits */
struct core_state *core_state; /* coredumping support */
/* store ref to file /proc/<pid>/exe symlink points to */
struct file *exe_file;
struct uprobes_state uprobes_state;
};
mm_struct 和VMA的关系如5.2所示。
图5.2 进程管理的vm_area_struct与进程虚拟地址空间的关联
struct vm_area_struct {
/* 以下两个成员的信息可用于遍历VMA树,其存储在第一缓存行中*/
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* 在vm_mm内结束地址之后的一个字节*/
/* 各进程的VM(虚拟内存)链表,按地址升序排列,vm_rb是红黑树的管理*/
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
/*
*该虚拟内存区的左侧最大空闲内存间隙,该间隙要么是VMA和vma->prev之间的间隙,要么就是该VMA之下的VMA去的红黑树和其自身的vm-prev的间隙,其作用是帮助get_unmapped_area 找到一个大小合适空闲的空间。*/
unsigned long rb_subtree_gap;
/* 第二个cache的缓存行存储从此开始. */
struct mm_struct *vm_mm; /* vm_area_struct属于的地址空间*/
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
/*对于有地址空间和后备存储器的区域将被链接到address_space->i_mmap优先树或者被链接到address_space->i_mmap_nonlinear链表上 */
union {
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} linear;
struct list_head nonlinear;
} shared;
/*在对文件的某一页执行了COW(copy on write)操作之后,一个文件的MAP_PRIVATE虚拟内存区可能在 i_mmap 树和anon_vma链表这两个地方,一个MAP_SHARED类型的虚拟内存区只能映射到i_mmap树。一个匿名MAP_PRIVATE虚拟内存区,栈或者堆只能被映射到anon_vma链表。*/
struct list_head anon_vma_chain; /* mmap_sem & page_table_lock 确保存取串行*/
struct anon_vma *anon_vma; /* page_table_lock确保存取串行(锁防止并发) */
/* 处理这个结构体的函数操作集,缺页异常时的处理函数,就是该操作集的fault指针*/
const struct vm_operations_struct *vm_ops;
/*后备存储器的信息*/
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* 指向映射的文件,如果没有则为NULL*/
void * vm_private_data; /* was vm_pte (shared mem) */
struct vm_region *vm_region; /* NOMMU mapping region */
};
5.2 文件和虚拟内存
图5.3文件和虚拟地址空间的映射关系
structaddress_space的i_mmap成员指向的是私有和共享映射的红黑树,而i_mmap_nonlinear则是VM_NONLINEAR映射的链表,它们链表的指向的成员对象均是vm_area_struct,
5.3 虚拟内存区操作
由5.1和5.2节可以看出,虚拟内存和structvm_area_struct关系还是挺大的,这节就来看看和vm_area_struct相关的一些操作。
5.3.1 find_vma
在5.1节提到structmm_struct的mmap_cache成员存放的是最近一次find_vma的结果。该函数用于查找用户空间地址中结束地址在给定地址之后的第一个区域,即满足addr>vm_area_struct->vm_end条件的第一个区域。
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;
/* 首先查找查找cache,命中率约35%*/
vma = ACCESS_ONCE(mm->mmap_cache);
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
//cache没有命中,则执行下面的查找。
struct rb_node *rb_node;
//获得红黑树根节点
rb_node = mm->mm_rb.rb_node;
vma = NULL;
while (rb_node) {
//不要陌生,linux内核的c文件早就可以在使用一个变量时再定义了
struct vm_area_struct *vma_tmp;
vma_tmp = rb_entry(rb_node,
struct vm_area_struct, vm_rb);
//如果获得的vma去的结束地址大于指定的地址,这就意味着找到了合适的vma区,否则决定是从左子树还是右子树开始查找,有点类似于二分查找。
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;
}
5.3.2 vma_merge
vma_merge用于区域合并,当新插入的区域可以和一个或多个区域合并时,合并操作将被执行。给定一个映射需求(addr,end,vm_flags,file,pgoff),判断是否可以进行区域合并。绝大多数情况下,当调用mmap,brk或者mremap时,vma_merged地址范围参数[addr,end)肯定还没被映射;但是当调用mprotect时,这段区域肯定被映射了。prev是紧邻着新区域之前的区域,addr、end以及vm_flags分别是新区域的开始地址、结束地址以及标志。如果是一个文件映射,file还会指向struct file实例,最后pgoff反映的是映射在文件数据内的偏移量。先来说明这一过程,首先截取图5.2的一部分。
图5.4 VMA和插入VMA关系
根据原VMA和插入VMA的关系分为两大类,共八中情况。A表示要插入的VMA区,P表示在要插入的VMA区的前面,N则表示位置处在要插入的VMA区的后面。
在所有情况中,能够合并的分为两大类,一类是前驱合并,即case1/2/3,对于case1,P区、插入的VMA区,以及N区,可连接组合和一个整块VMA,case2插入的VMA区和P可以合并其它情况依次类推。X意思是不关心的区域,因为这时添加进入的VMA区的end和next->vm_end是相等的,即它们是重叠的区域,所以会将N区后移,找N的后驱作为真正的后驱。
mm/mmap.c
struct vm_area_struct *vma_merge(struct mm_struct *mm,
struct vm_area_struct *prev, unsigned long addr,
unsigned long end, unsigned long vm_flags,
struct anon_vma *anon_vma, struct file *file,
pgoff_t pgoff, struct mempolicy *policy)
{
pgoff_t pglen = (end - addr) >> PAGE_SHIFT;
struct vm_area_struct *area, *next;
int err;
//如果紧邻新区域之前的区域非空,将获得next成员,这有点类似断开链表插入一个元素,否则获得该mm的第一个VMA元素
if (prev)
next = prev->vm_next;
else
next = mm->mmap;
area = next;
//如果存在next存在,并且vma的结束地址和给定的结束地址相同,则有重叠,需要调整vma
if (next && next->vm_end == end) /* cases 6, 7, 8 */
next = next->vm_next;
/*
* 是否和前一个vma进行合并?
前一个vma存在(prev非空)并且其结束地址等于新区域的开始地址。两者的struct mempolicy(NUMA使用)属性相同,另外还有vm_flags以及映射同一个文件等属性也要一致。前驱合并
*/
if (prev && prev->vm_end == addr && mpol_equal(vma_policy(prev), policy) &&
can_vma_merge_after(prev, vm_flags,//如果可以合并返回true。
anon_vma, file, pgoff)) {
/*
* 代码到这里,说明可以合并。现在查看是否可以对后一个vma也进行合并
*/
if (next && end == next->vm_start && can_vma_merge_before(next, vm_flags,
anon_vma, file, pgoff+pglen) &&is_mergeable_anon_vma(prev->anon_vma,
next->anon_vma, NULL)) {
/* cases 1, 6 */
err = vma_adjust(prev, prev->vm_start,
next->vm_end, prev->vm_pgoff, NULL);
} else /* cases 2, 5, 7 */
err = vma_adjust(prev, prev->vm_start,
end, prev->vm_pgoff, NULL);
if (err)
return NULL;
khugepaged_enter_vma_merge(prev);
return prev;
}
/*如果前面的失败,则从后面的vma开始进行和上面类似的合并操作。后驱合并
* Can this new request be merged in front of next?
*/
if (next && end == next->vm_start &&
mpol_equal(policy, vma_policy(next)) &&
can_vma_merge_before(next, vm_flags,
anon_vma, file, pgoff+pglen)) {
if (prev && addr < prev->vm_end) /* case 4 */
err = vma_adjust(prev, prev->vm_start,
addr, prev->vm_pgoff, NULL);
else /* cases 3, 8 */
err = vma_adjust(area, addr, next->vm_end,
next->vm_pgoff - pglen, NULL);
if (err)
return NULL;
khugepaged_enter_vma_merge(area);
return area;
}
return NULL;
}
vma_adjust用于完成实际的合并操作。
5.3.3 insert_vm_struct
该函数用于将vm结构体插入按地址排列的进程链表和inode的i_mmap树中,如果vm_file非空,则需要获得i_mmap_mutex互斥信号量。属于红黑树的插入。
int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)
{
struct vm_area_struct *prev;
struct rb_node **rb_link, *rb_parent;
if (!vma->vm_file) {
BUG_ON(vma->anon_vma);
vma->vm_pgoff = vma->vm_start >> PAGE_SHIFT;
}
if (find_vma_links(mm, vma->vm_start, vma->vm_end,
&prev, &rb_link, &rb_parent))
return -ENOMEM;
if ((vma->vm_flags & VM_ACCOUNT) &&
security_vm_enough_memory_mm(mm, vma_pages(vma)))
return -ENOMEM;
vma_link(mm, vma, prev, rb_link, rb_parent);
return 0;
}
5.3.4 创建区域
在插入新分配的内存区域之前,内核必须确认虚拟地址空间中是否有足够的空闲空间用于映射给定长度的区域。这一工作由get_unmapped_area函数完成,该函数是和体系结构是息息相关,具体体系结构实现的细节这里就不关心了。
5.4 创建地址区间
mmap,do_mmap
内核在创建一个新的VMA区时,会判断和一个已存在的VMA区是否可以合并,如果可以合并,则会合并成一个区域,而不创建一个一个新的VMA,但如果无法合并,则会创建一个新的VMA区。
do_mmap会调用do_mmap_pgoff完成实际的创建过程,该函数首先调用get_unmapped_area在虚拟地址空间中找到一个适当的区域用于映射。而calc_vm_prot_bits则是计算新VMA的权限标识。准备工作做好后,开始进行实际的区域映射工作,其由mmap_region来实现,mmap_region首先检查资源是否超出进程的资源限制,如果超出限制并且是固定映射则返回,否则计算可以映射多少VMA区,然后调用find_vma_links查找是否已有现存区域,如果有则需要调用do_munmap将该区域腾出来给新的VMA区。然后尝试不创建新的vm_area_struct实例,即使用vma_merge方法尝试将新的VMA区域合并到已有的vm_area_struct实例里去,如果合并不成功,则需要创建vm_area_struct实例,如果是文件映射,则file->f_op->mmap将文件和新创建的VMA区关联起来。最后调用vma_link将新的VMA区插入到线性虚拟地址空间去。
图5.5 创建新区域核心流程
5.5 删除地址区间
mumap,do_munmap()
用于从特定的进程地址空间中删除指定的VMA区,
mm/map.c
intdo_munmap(struct mm_struct *mm, unsigned long start, size_t len)
第一个参数用于删除所在的地址空间,第二个参数指定删除的VMA的起始地址,第三个参数指定删除的长度。
mm/map.c
int vm_munmap(unsigned long start, size_t len)
{
int ret;
struct mm_struct *mm = current->mm;
down_write(&mm->mmap_sem);
ret = do_munmap(mm, start, len);
up_write(&mm->mmap_sem);
return ret;
}
EXPORT_SYMBOL(vm_munmap);
SYSCALL_DEFINE2(munmap, unsigned long, addr, size_t, len)
{
profile_munmap(addr);
return vm_munmap(addr, len);
}
删除一个VMA区的流程如图5.6所示。该函数首先调用find_vma查找和要删除VMA的start地址重叠的VMA区,如果没有重叠VMA区,则直接返回,如果需要分离VMA区,则调用__split_vma分离VMA区,然后调用find_vma判断删除区域的end是否也有重叠的VMA区,有也要分离。detach_vmas_to_be_unmapped用于将要删除的VMA解映射掉,其遍历所有的vm_area_struct实例线性表,直到要解除映射的地址范围已经全部涵盖在内,然后调用unmap_region从表页中删除与映射相关的所有项,同时内核会刷新相关的TLB项。最后调用remove_vma_list使用vm_area_struct实例占用的空间。
图5.6 删除VMA区流程