Linux是一个虚拟内存操作系统,系统中的所有进程以虚拟方式共享内存。从进程的视角来看,它独占系统中所有的物理内存,并且一个进程的地址空间可以远远大于物理内存的大小。
进程的地址空间由可寻址的虚拟内存组成,进程通过地址空间中的虚拟地址访问内存。一个进程可以选择和其他进程共享地址空间,这样的进程就是我们说的线程。
尽管进程最多可以寻址4GB的虚拟内存(在32-bit的地址空间中),但是这不代表它可以访问所有的虚拟地址,地址空间中的进程可以访问的区间如0848000-0804c000被称为为内存区域(memory area),进程可以通过内核给自己的地址空间动态的增加或者删除内存区域。
进程只能访问有效内存区域中的内存地址。内存区域有对应的权限,如可读、可写、可执行。如果内存访问的内存地址不在一个有效的内存区域中或者违反了该内存区域的访问权限,内核会杀死这个进程,并返回 “Segmentation Fault”消息。
不同的内存片段(如栈、对象代码、全局变量、映射的文件)在内核中都各自对应的独立的内存区域。
内核使用内存描述符来表示进程的地址空间,文件描述符是使用mm_struct结构来存储的,该结构包含了和进程地址空间相关的全部信息,定义在
struct mm_struct {
struct vm_area_struct *mmap; /* list of memory areas */
struct rb_root mm_rb; /* red-black tree of VMAs */
struct vm_area_struct *mmap_cache; /* last used memory area */
unsigned long free_area_cache; /* 1st address space hole */
pgd_t *pgd; /* page global directory */
atomic_t mm_users; /* address space users */
atomic_t mm_count; /* primary usage counter */
int map_count; /* number of memory areas */
struct rw_semaphore mmap_sem; /* memory area semaphore */
spinlock_t page_table_lock; /* page table lock */
struct list_head mmlist; /* list of all mm_structs */
unsigned long start_code; /* start address of code */
unsigned long end_code; /* final address of code */
unsigned long start_data; /* start address of data */
unsigned long end_data; /* final address of data */
unsigned long start_brk; /* start address of heap */
unsigned long brk; /* final address of heap */
unsigned long start_stack; /* start address of stack */
unsigned long arg_start; /* start of arguments */
unsigned long arg_end; /* end of arguments */
unsigned long env_start; /* start of environment */
unsigned long env_end; /* end of environment */
unsigned long rss; /* pages allocated */
unsigned long total_vm; /* total number of pages */
unsigned long locked_vm; /* number of locked pages */
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* saved auxv */
cpumask_t cpu_vm_mask; /* lazy TLB switch mask */
mm_context_t context; /* arch-specific data */
unsigned long flags; /* status flags */
int core_waiters; /* thread core dump waiters */
struct core_state *core_state; /* core dump support */
spinlock_t ioctx_lock; /* AIO I/O list lock */
struct hlist_head ioctx_list; /* AIO I/O list */
};
其中:
进程描述符task_struct结构中的mm域指向该进程使用的地址空间的文件描述符mm_struct结构。copy_mm()函数在fork时将父进程的内存描述符复制给子进程。mm_struct结构使用slab allocator来进行分配和回收。通常情况下一个进程都有一个独占的mm_struct结构,即独占的地址空间。如果进程在clone()时传递CLONE_VM参数,就会将mm_struct结构(即地址空间)共享给复制的子进程,这就是线程了。
进程结束时,会调用exit_mm(),这个函数会调用mmput()将内存描述符mm_struct的使用计数mm_users减1,如果mm_users变为0,就会调用mmdrop()将mm_count减1,如果mm_count变为0了,那么就调用free_mm()宏,通过kmem_chache_frer()将mm_struct结构释放给mm_cachep slab 缓存。
内核线程没有自己的进程地址空间,因此没有对应的内存描述符,所以内核线程的进程描述符的mm域为NULL。内核线程使用的是前一个进程的内存描述符,即使用该内存的地址空间。这样做的好处是避免了内核线程为内核描述符和页表浪费内存,同时避免了地址空间的切换,提高了效率。
内核线程执行时,会将进程描述符中的active_mm指针指向上一个进程的内存描述符mm_struct结构,这样在需要时,内核线程也可以使用上一个进程的页表。内核线程不访问用户空间的内存,只使用内核地址空间,
内核使用vm_area_struct结构来表示内存区域,在linux中内存区域经常被称为虚拟内存区域(virtual memory area,缩写VMA)。vm_area_struct结构描述了一个给定地址空间中一个单独的内存区域,内核将每个内存区域作为一个单独的内存对象进行管理。每个内存区域都有特定的属性、访问权限和一系列相关的操作。这样使得每个VMA结构都可以表示不同类型的内存区域,比如进程的用户栈等。
vm_area_struct结构定义在 每个VMA都对应进程地址空间中唯一的一个区间,vm_start域指向区间首地址,vm_end指向区间的尾地址(不含),内存区域的区间为[vm_start, vm_end)。同一个进程地址空间中的内存区域之间是不可以重叠的。vm_mm域指向了VMA所在的进程地址空间对应的内存描述符mm_struct结构。 vm_flags域包含bit flags。规定了该VMA中所有page的行为和信息。下表给出了所有可能的flag。 其中: vm_area_struct结构中的vm_ops指向VMA的操作函数表,内核使用函数表中的函数操作VMA。 在linux中通过cat /proc/pid/maps 或者 pmap pid可以查看进程的有哪些内存区域以及他们的位置、大小、权限等信息。 如果一个内存区域是共享的或者不可写的,那么进程只为backing file保存一份映射。查看进程的内存区域时,会发现有很多.so这样的C库,因为C库是共享的,内核不需要为每个使用C库的进程创建一个VMA保存其使用的C库的text、data、bss内存区域,节省了大量的空间。 内核中定义了多个管理内存区域的函数。 struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr); struct vm_area_struct * find_vma_prev(struct mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev) static inline struct vm_area_struct *find_vma_intersection(struct mm_struct *mm, unsigned long start_addr,unsigned long end_addr) unsigned long do_mmap(struct file *file, unsigned long addr,unsigned long len, unsigned long prot,unsigned long flag, unsigned long offset) void * mmap2(void *start, size_t length, int prot, int flags, int fd, off_t pgoff) int do_munmap(struct mm_struct *mm, unsigned long start, size_t len) int munmap(void *start, size_t length) 虽然应用程序操作的是虚拟内存,但是处理器直接操作的是物理内存。所以当应用程序访问一个虚拟地址时,虚拟地址会被转换为物理地址,然后再交给处理器。这个操作是通过页表来完成的。页表中的页表项指向下一级页表或者一个物理页。 Linux使用的是三级页表,多级页表可以减少内存占用。第一级是PGD(page global directory),PGD是一个pgd_t类型的数组(pgd_t通常为unsigned long),PGD的页表项指向二级页表。二级页表是PMD(page middle directory),是一个pmd_t类型的数组,PMD的页表项指向三级页表。三级页表就是原始的页表,页表项为pte_t类型,指向一个物理页。使用页表进行虚拟地址转换时,首先访问一级页表中对应的页表项,然后访问该页表项指向的二级页表,在二级页表中找到对应的页表项,然后访问该页表项指向的三级页表,最终在三级页表中找到虚拟地址所在页对应的物理页,然后将虚拟地址转换成物理地址。使用多级页表时,上级流表的页表项是可能为空,即不指向任何下级流表,这种情况下对应的下级流表在内存中是不存在的的,从而减少内存开销。因为大部分进程都有自己的流表项,所以总共节省的内存空间是很可观的。 每个进程都有自己的页表(线程会共享地址空间,所以页表是共享的),内存描述符mm_struct结构的pgd域指向的是一级页表PGD。由于几乎每次对虚拟内存页的访问都需要先进行地址转换,所以页表的性能非常重要,但是在内存中查找的速度是有限的,为了加快查找速度,大部分处理器都实现了TLB(translation lookaside buffer)。TLB是虚拟地址-物理地址映射的硬件缓存,每次访问一个虚拟地址时,处理器都会首先检查TLB是否缓存了该虚拟地址到物理地址的映射,如果有,直接返回,如果没有,就需要通过多级页表进行地址转换。 《Linux Kernel Development 3rd Edition》 struct vm_area_struct
{
struct mm_struct *vm_mm; /* associated mm_struct */
unsigned long vm_start; /* VMA start, inclusive */
unsigned long vm_end; /* VMA end , exclusive */
struct vm_area_struct *vm_next; /* list of VMA’s */
pgprot_t vm_page_prot; /* access permissions */
unsigned long vm_flags; /* flags */
struct rb_node vm_rb; /* VMA’s node in the tree */
/* links to address_space->i_mmap or i_mmap_nonlinear */
union
{
struct
{
struct list_head list;
void *parent;
struct vm_area_struct *head;
} vm_set;
struct prio_tree_node prio_tree_node;
} shared;
struct list_head anon_vma_node; /* anon_vma entry */
struct anon_vma *anon_vma; /* anonymous VMA object */
struct vm_operations_struct *vm_ops; /* associated ops */
unsigned long vm_pgoff; /* offset within file */
struct file *vm_file; /* mapped file, if any */
void *vm_private_data; /* private data */
};
Flag
Effect on the VMA and Its Pages
VM_READ
Pages can be read from.
VM_WRITE
Pages can be written to.
VM_EXEC
Pages can be executed.
VM_SHARED
Pages are shared.
VM_MAYREAD
The VM_READ flag can be set.
VM_MAYWRITE
The VM_WRITE flag can be set.
VM_MAYEXEC
The VM_EXEC flag can be set.
VM_MAYSHARE
The VM_SHARE flag can be set.
VM_GROWSDOWN
The area can grow downward.
VM_GROWSUP
The area can grow upward.
VM_SHM
The area is used for shared memory.
VM_DENYWRITE
The area maps an unwritable file.
VM_EXECUTABLE
The area maps an executable file.
VM_LOCKED
The pages in this area are locked.
VM_IO
The area maps a device’s I/O space.
VM_SEQ_READ
The pages seem to be accessed sequentially.
VM_RAND_READ
The pages seem to be accessed randomly.
VM_DONTCOPY
This area must not be copied on fork().
VM_DONTEXPAND
This area cannot grow via mremap().
VM_RESERVED
This area must not be swapped out.
VM_ACCOUNT
This area is an accounted VM object.
VM_HUGETLB
This area uses hugetlb pages.
VM_NONLINEAR
This area is a nonlinear mapping.
操作内存区域
find_vma()
找到第一个包含addr的内存区域或者第一个起始地址大于addr的内存区域,没有则返回NULL。find_vma_prev()
返回位于addr前面的最后一个内存区域,没有则返回NULL。find_vma_intersection()
返回第一个和给定的地址区间有重叠的内存区域,没有则返回NULL。do_mmap()
创建一个新的地址区间,如果这个新的内存区域和一个已有的内存区域相邻并且有着同样的访问权限,就将这个两个内存区域合并为一个。如果不能合并就创建一个新的内存区域。分配失败会返回一个负值。mmap()
这个是暴露给用户空间的系统调用接口,定义为mmap2()是因为这是mmap()第二个变种。mmap会调用do_mmap()来进行地址区间的分配。do_munmap()
从地址空间中移除一个地址区间,start规定了区间的起始位置,len规定了区间的字节长度。munmap()
这是暴露给用户空间的系统调用接口,munmap()会调用do_munmap()来移除由start和length规定的地址区间。页表(page table)
参考资料
《Understanding The Linux Kernel 3rd Edition》