进程地址空间
内核除了管理自身的内存外,还必须管理用户空间中进程的内存,称该内存为进程地址空间,也就是系统中每个用户空间进程所看到的内存。Linux采用虚拟内存技术,所有进程之间以虚拟方式共享内存,对一个进程而言,好像都能访问整个系统的所有物理内存,甚至远远大于物理内存。
1.地址空间
进程地址空间由进程可寻址的虚拟内存组成,通常每个进程都有唯一的地址空间(连续的)。若两个进程的地址空间由相同的内存地址,又互不相干,那么这样的进程就是线程。
对于一个32位机,进程可以寻址4GB的虚拟内存,但这并不代表它有权访问所有的虚拟地址,那些可被访问的合法地址空间称为内存区域(memory areas)。通过内核,进程可以给自己的地址空间动态添加或减少内存区域。
进程只能访问有效的内存区域内的内存地址,否则内核会终止该进程,并返回段错误(尼玛,初学C语言时经常报段错误啊!)
内存区域可以包含各种内存对象,比如:
- 可执行文件代码的内存映射,称代码段(text section)
- 可执行文件已初始化全局变量的内存映射,称数据段(data section)
- 未初始化的全局变量,也就是bss段的零页内存映射(C标准中强制规定未初始化的全局变量赋予特殊默认值,0居多,避免目标文件中显式初始化,减少空间浪费)
- 用于进程用户空间栈的零页内存映射
- 每一个诸如C库或动态链接程序等共享库的代码段,数据段,bss都会载入进程的地址空间
- 任何内存映射文件
- 任何共享内存段
- 任何匿名的内存映射,比如
malloc()
分配的内存
进程地址空间中的任何有效地址都只能位于唯一的区域,因此上述的内存对象都对应于一个独立的内存区域。
2.内存描述符
内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。内存描述符由mm_struct
结构体表示,定义在文件<linux/sched.h>
(现在是在mm_type.h
,前面文章如果找不到函数请查看一下头文件,一般能找到线索)中:
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 def_flags; /* default access flags */
unsigned long cpu_vm_mask; /* lazy TLB switch mask */
unsigned long swap_address; /* last scanned address */
unsigned dumpable:1; /* can this mm core dump? */
int used_hugetlb; /* used hugetlb pages? */
mm_context_t context; /* arch-specific data */
int core_waiters; /* thread core dump waiters */
struct completion *core_startup_done; /* core start completion */
struct completion core_done; /* core end completion */
rwlock_t ioctx_list_lock; /* AIO I/O list lock */
struct kioctx *ioctx_list; /* AIO I/O list */
struct kioctx default_kioctx; /* AIO default I/O context */
};
-
mm_users
记录正在使用该地址的进程数目; -
mm_count
记录mm_struct
的主引用次数,如果有9个线程共享某地址,则mm_user
为9,mm_count
为1。当内核在一个地址空间上操作,并需要使用与该地址相关联的引用计数时,内核便增加mm_count
-
mmap
和mm_rb
描述的对象是相同的——该地址空间中全部的内存区域。但是前者以李阿尼奥形式存放,后者以红黑树存放。链表简单便于遍历和删除,红黑树适合检索 -
mmlist
,所有的mm_struct
结构体都通过自身的mmlist
域连接在一个双向链表中,该链表的首元素是init_mm
内存描述符,它代表init
进程的地址空间。另外需要注意,操作该链表的时候需要使用mmlist_lock
锁来防止并发访问,该锁定义在文件<kernel/fork.c>
中。
分配内存描述符
在进程的进程描述符中,mm
域存放该进程使用的内存描述符,所以current->mm
便指向当前进程的内存描述符。fork()
函数利用copy_mm()
函数复制父进程的内存描述符,也就是current->mm
域赋给子进程,而子进程中mm_struct
结构实际通过文件kernel/fork.c
中的allocate_mm()
宏从mm_cachep
的slab缓存中分配得到。通常每个进程有唯一的mm_struct
结构体,即唯一的进程地址空间,但是也有列外的,比如线程(linux中线程就是进程,只是与父进程共享地址空间)。
撤销内存描述符
进程退出时,内核会调用定义在kernel/exit.c
中的exit_mm()
函数,该函数执行一些常规的撤销工作,同时更新一些统计量(前面讲的mm_users
和mm_count
)。如果该内存描述符没有任何使用者了,那么调用free_mm()
宏通过kmem_cache_free()
函数将mm_struct
结构体归还到mm_cachep
的slab缓存中。
mm_struct与内核线程
内核线程没有进程地址空间,也没有相关的内存描述符,所以内核线程对应的进程描述符中mm
域为空。事实上,这也正是内核线程的真实含义——没有用户上下文。
省了进程地址空间再好不过,因为内核线程并不需要访问任何用户空间的内存,而且因为内核线程在用户空间中没有任何页,所以实际上他们并不需要有自己的内存描述符和页表。但是,访问内核内存,内核线程还是需要一些数据的,比如页表。为了避免内核线程为内存描述符和页表浪费内存,同时避免浪费处理器周期向新地址空间进行切换,内核线程将直接使用前一个进程的内存描述符。
当一个进程被调度时,该进程的mm
域指向的地址空间被装载到内存,进程描述符中的active_mm
域会被更新,指向新的地址空间。内核线程没有地址空间,所以mm
域为NULL。当一个内核 线程被调度时,内核发现它的mm
域为NULL,就会保留前一进程的地址空间,随后内核更新内核线程对应的进程描述符中的active_mm
域,使其指向前一个进程的内存描述符。所以需要时,内核线程便可以使用前一个进程的页表。因为内核线程不访问用户空间的内存,所以它们仅仅使用地址空间中和内核内存相关的信息,这些信息的含义和普通进程完全相同。
3.虚拟内存区域
内存区域由vm_area_struct
结构体描述,定义在文件<linux/mm_type.h>
中。内群区域在Linux中也常被称为虚拟内存区域(virtual memory Areas, VMAs)。
vm_area_struct
结构体描述了指定地址空间内连续区间上的一个独立内存范围。内核将每个内核区域作为一个单独的内存对象管理。每一个VMA代表不同类型的内存区域(比如内存映射文件或进程用户空间栈):
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 */
union { /* links to address_space->i_mmap or i_mmap_nonlinear */
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 */
};
和内存管理中对象一样,VMA对象同样定义了操作函数,由vm_operations_struct
结构体表示,定义在文件<linux/mm.h>
:
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
int (*mremap)(struct vm_area_struct * area);
int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
int (*pmd_fault)(struct vm_area_struct *, unsigned long address,
pmd_t *, unsigned int flags);
void (*map_pages)(struct fault_env *fe,
pgoff_t start_pgoff, pgoff_t end_pgoff);
/* notification that a previously read-only page is about to become
* writable, if an error is returned it will cause a SIGBUS */
int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
/* same as page_mkwrite when using VM_PFNMAP|VM_MIXEDMAP */
int (*pfn_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
/* called by access_process_vm when get_user_pages() fails, typically
* for use by special VMAs that can switch between memory and hardware
*/
int (*access)(struct vm_area_struct *vma, unsigned long addr,
void *buf, int len, int write);
};
- open:当制定内存区域被加入到一个地址空间时,该函数被调用
- close:当制定的内存区域从地址空间删除时,该函数被调用
- fault:当没有出现在物理内存中的页面被访问时,该函数被页面故障处理调用
- page_mkwrite:当某个页面为只读页面时,该函数被页面故障处理调用
- access:当
get_user_pages()
函数调用失败时,该函数被access_process_vm()
调用
4.操作内存区域
内核时常需要在某个内核区域上执行一些操作,比如某个制定地址是否包含在某个内存区域中。这类操作非常频繁。内核定义了许多辅助函数,都声明在<linux/mm.h>中
find_vma()
用于找到一个给定的内存地址属于哪一个内存区域提供的函数,定义在<mm/mmap.c>
中:
struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr)
该函数在指定的地址空间中搜索第一个vm_end
大于addr
的内存区域,如果没发现这样的区域,该函数返回NULL;否则返回指向该区域的vm_area_struct
结构体指针。注意,由于返回的VMA首地址可能大于addr
,所以指定的地址不一定就包含在返回的VMA中,因为很可能在对某个VMA执行操作后,还有更多操作会对该VMA接着操作,所以find_vma()
函数返回的结果被缓存在内存描述符的mmap_cache
域中。检查缓存的VMA如果指定地址不在缓存中,那么必须搜索和内存描述符相关的所有内存区域(通过红黑树进行)
find_vma_prev()和find_vma_intersection()
find_vma_prev和find_vma()工作方式相同,但是返回第一个小于addr
的VMA。find_vma_intersection()函数返回第一个和指定地址区间向交的VMA。这两个函数均是定义在<linux/mm.h>
中。
5.创建地址区间
内核使用do_mmap()
函数创建一个新的线性地址区间。如果创建的区间和已经存在的区间相邻,且权限相同,则合并两个区间。如果不能合并就创建一个新的VMA。最终do_mmap()
函数会将地址区间加入到进程的地址区间,do_mmap()
函数定义在文件<linux/mm.h>
:
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long offset)
该函数映射有file
指定的文件,具体映射的是文件中偏移offset
处开始,长度为len
字节的范围内的数据。
在用户空间可以通过mmap()
系统调用获取内核函数do_mmap()
的功能.
6.删除地址区间
do_mummap()
函数从特定的进程地址空间删除制定地址区间,该函数定义在文件<linux/mm.h>
中:
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
系统调用mummap()
给用户空间程序提供了一种从自身地址空间中删除制定地址区间的方法,他和系统调用mmap()作用相反。
7.页表
虽然应用程序操作的对象是映射到物理内存上的虚拟地址,但是处理器直接操作的却是物理内存。所以应用程序访问一个虚拟地址时,首先必须将虚拟地址转化为物理地址,然后处理器才能解析地址访问请求。地址转换工作需要通过查询页表才能完成,概括地讲,地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表则指向一级别的页表或者指向最终的物理页面(如果学过网络的同学,这个页表我们就可以看做网络中的网络号,我们先通过网络号找到子网,在子网中继续找网络号或者IP地址,或者把页表当做树查询)。
一个逻辑地址(32位系统,页大小 4K) 可以被分为 :一个20位的页号 +一个12位的偏移。如果对页表进行再分页,那么页号分解为:一个10位的页号 +一个10位的偏移。
Linux中使用三级页表完成地址转换,多级页表能够节约地址转换所需占用的存放空间。
如果每次对虚拟内存中的页面访问都必须解析它,从而获得物理内存中对应的地址,需要消耗一定的时间。因此多数体系结构实现了一个翻译后缓冲器(translate lookaside buffer,TLB)。TLB作为一个将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器首先检查TLB中是否缓存了虚拟地址到物理地址的映射,没查找到的前提下再通过页表搜索。