内存描述符
- 内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息
#include <linux/sched.h>
struct mm_struct {
struct vm_area_struct *mmap;
struct rb_root mm_rb;
struct vm_area_struct *mmap_cache;
unsigned long free_area_cache;
pgd_t *pgd;
atomic_t mm_users;
atomic_t mm_count;
int map_count;
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock;
struct list_head mmlist;
unsigned long start_code;
unsigned long end_code;
unsigned long start_data;
unsigned long end_data;
unsigned long start_brk;
unsigned long brk;
unsigned long start_stack;
unsigned long arg_start;
unsigned long arg_end;
unsigned long env_start;
unsigned long env_end;
unsigned long rss;
unsigned long total_vm;
unsigned long locked_vm;
unsigned long saved_auxv[AT_VECTOR_SIZE];
cpumask_t cpu_vm_mask;
mm_context_t context;
unsigned long flags;
int core_waiters;
struct core_state *core_state;
spinlock_t ioctx_lock;
struct hlist_head ioctx_list;
}
- mm_users:记录正在使用该地址的进程数目,比如两个线程共享该地址空间,那么mm_users的值等于2
- mm_count:是mm_struct结构体的主引用计数,所有的mm_users都等于mm_count的增加量,当mm_users的值减为0(所有正使用该地址空间的线程都退出)时,mm_count的值才变为0,当mm_count的值等于0,说明没有任何指向该mm_struct结构体的引用,此时该结构体会被撤销
- mmap和mm_rb两个不同数据结构体描述的对象时相同的,该地址空间中的全部内存区域,但是前者以链表形式存放,而后者以红-黑树形式存放
- 所有的mm_struct结构体都通过自身的mmlist连接在一个双向链表中,该链表的首元素是init_mm内存描述符,代表init进程的地址空间,操作该链表的时候需要使用mmlist_lock锁来防止并发访问,该锁定义在文件kernel/fork.c中
分配内存描述符
- 在进程的进程描述符(<linux/sched.h>定义的task_struct结构体表示进程描述符)中,mm成员存放该进程使用的内存描述符,所有current->mm便指向当前进程的内存描述符
- fork()函数利用copy_mm()函数复制父进程的内存描述符,也就是把current->mm成员给与子进程,而子进程中的mm_struct结构体实际通过文件kernel/fork.c中的allocate_mm()宏从mm_cachep slab缓冲中分配得到
- 每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间
- 如果父进程希望和子进程共享地址空间,可以调用clone()时,设置CLONE_VM标志,这样的进程称为线程(是否共享地址空间是进程和Linux所谓的线程本质上唯一的区别,线程对于内核来说仅仅是一个共享特定资源的进程而已)
- 当CLONE_VM被指定后,内核就不再需要调用allocate_mm()函数,而仅仅需要在调用copy_mm()函数中将mm成员指向其父进程的内存描述符即可
if (clone_flags & CLONE_VM) {
atomic_inc(¤t->mm->mm_users);
tsk->mm = current->mm;
}
撤销内存描述符
- 当进程退出时,内核会调用定义在kernel/exit.c中的exit_mm()函数,该函数执行一些常规的撤销工作,同时更新一些统计量
- exit_mm()函数会调用mmput()函数减少内存描述符的mm_users用户计数,如果用户计数降到零,将调用mmdrop()函数,减少mm_count使用计数,如果使用计数也等于零,说明内存描述符不再有任何使用者,那么调用free_mm()宏通过kmem_cache_free()函数将mm_struct结构体归还mm_cache缓存中
mm_struct与内核线程
- 内核线程没有进程地址空间,也没有相关的内存描述符,所有内核线程对应的进程描述符中mm成员为NULL,事实上,正是内核线程的真实含义——没有用户上下文
- 因为内核线程不需要访问用户空间的内存,而且内核线程在用户空间没有任何页,所以实际上内核线程不需要有自己的内存描述符和页表。为了避免内核线程为内存描述符和页表浪费内存,也为了当新内核线程运行时,避免浪费处理器周期向新地址空间进行切换,内核线程将直接使用前一个进程的内存描述符
- 当一个进程被调度时,该进程的mm成员指向的地址空间被装载到内存,进程描述符中的active_mm成员被更新,指向新的地址空间
- 内核线程没有地址空间,所以mm成员为NULL,于是,当一个内核线程被调度时,内核发现mm成员为NULL,就会保留前一个进程的地址空间,随后内核更新内核线程对应的进程描述符中的active_mm成员,使其指向前一个进程的内存描述符,所以需要时,内核线程便可以使用前一个进程的页表,因为内核线程不访问用户空间的内存,所以仅仅使用地址空间中和内核内存相关的信息,这些信息的含义和普通进程完全一样
虚拟内存区域
- 内存区域由vm_area_struct结构体描述,定义在文件<linux/mm_types.h>中,内存区域在Linux内核页经常被称为虚拟内存区域(virtual memoryAreas,VMAs)
- vm_area_struct结构体描述指定地址空间内连续区间上的一个独立内存范围,内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都拥有一致的属性,比如访问权限等,另外,相应的操作也一致
- 每一个VMA可以代表不同类型的内存区域(比如内存映射文件或者进程用户空间栈)
#include <linux/mm_types.h>
struct vm_area_struct {
struct mm_struct *vm_mm;
unsigned long vm_start;
unsigned long vm_end;
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
struct rb_node vm_rb;
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;
struct anon_vma *anon_vma;
struct vm_operations_struct *vm_ops;
unsigned long vm_pgoff;
struct file *vm_file;
void *vm_private_data;
}
- 每个内存描述符都对应进程地址空间中唯一区间
- vm_start成员指向区间的首地址,vm_end指向区间的尾地址之后第一个字节,也就是说vm_start是内存区间的开始地址(本身在区间内),vm_end是内存区间的结束地址(本身在区间外),因此vm_end - vm_start的大小便是内存区间的长度,在同一地址空间内的不同内存区间不能重叠
- vm_mm成员指向和VMA相关的mm_struct结构体,每个VMA对其相关的mm_struct结构体来说是唯一的,所以即使两个独立的进程将同一个文件映射到各自的地址空间,分别都有一个vm_area_struct结构体标志自己的内存区域,反过来,如果两个线程共享一个地址空间,那么也同时共享其中的所以vm_area_struct结构体
VMA标志
- VMA标志定义<linux/mm.h>,包含在vm_flags成员中,标志内存区域所包含的页面的行为和信息
- 和物理页的访问权限不同,VMA标志反映内核处理页面所需要的行为准则
- vm_flags同时也包含了内存区域中每个页面的信息,或内存区域的整体信息
标志 | 对VMA及其页面的影响 |
---|
VM_READ | 页面可读取 |
VM_WRITE | 页面可写 |
VM_EXEC | 页面可执行 |
VM_SHARED | 页面可共享 |
VM_MAYREAD | VM_READ标志可被设置 |
VM_MAYWRITE | VM_WRITE 标志可被设置 |
VM_MAYEXEC | VM_EXEC 标志可被设置 |
VM_MAYSHARE | VM_SHARED 标志可被设置 |
VM_GROWSDOWN | 区域可向下增长 |
VM_GROWSUP | 区域可向上增长 |
VM_SHM | 区域可用作共享内存 |
VM_DENYWRITE | 区域映射一个不可写文件 |
VM_EXECUTABLE | 区域映射一个可执行文件 |
VM_LOCKED | 区域中的页面被锁定 |
VM_IO | 区域映射设备I/O空间 |
VM_SEQ_READ | 页面可能被连续访问 |
VM_RAND_READ | 页面可能被随机访问 |
VM_DONTCOPY | 区域不能在fork()时被拷贝 |
VM_DONTEXPAND | 区域不能通过mremap()增加 |
VM_RESERVED | 区域不能被换出 |
VM_ACCOUNT | 该区域是一个记账VM对象 |
VM_HUGETLB | 区域使用hugetlb()页面 |
VM_NONLINEAR | 该区域是非线性映射的 |
- VM_READ、VM_WRITE和VM_EXEC标志了内存区域中页面的读、写和执行权限,这些标志根据要求组合构成VMA的访问控制权限,当访问VMA时,需要查看其访问权限,比如进程的对象代码映射区域可能标志为VM_READ和VM_EXEC,而没有标志为VM_WRITE;另一方面,可执行对象数据段的映射区域标志为VM_READ和VM_WRITE,而VM_EXEC标志对它就毫无意义,也就是说只读文件数据段的映射区域仅可被标志为VM_READ
- VM_SHARD指明内存区域包含的映射是否可以在多进程间共享,如果该标志被设置,则称为共享映射;如果未被设置,而仅仅只有一个进程可以使用该映射的内容,称为私有映射
- VM_IO标志内存区域中包含对设备I/O空间的映射,该标志通常在设备驱动程序执行mmap()函数进行I/O空间映射时才被设置,同时该标志也表示该区域不能包含在任何进程的存放转存(core dump)中,VM_RESERVED标志规定了内存区域不能被换出,也是在设备驱动程序进行映射时被设置
- VM_SEQ_READ标志暗示内核应用程序对映射内容执行有序的(线性和连续的)读操作;内核可以有选择地执行预读文件,VM_RAND_READ标志的意义正好相反,暗示应用程序对映射内容执行随机的(非有序的)读操作。因此内核可以有选择地减少或彻底取消文件预读,这两个标志可以通过系统调用madvise()设置,设置参数分别时MADV_SEQUENTIAL和MADV_RANDOM。文件预读是指在读数据时有意地按顺序多读取一些本次请求以外的数据——希望多读的数据能够很快被用到
VMA操作
- vm_area_struct结构中的vm_ops成员指向与指定内存区域相关的操作函数表,内核使用表中的方法操作VMA
- vm_area_struct作为通用对象代表任何类型的内存区域,而操作表描述针对特定的对象实例的特定方法
#include <linux/mm.h>
struct vm_operations_struct {
void (*open)(struct vm_area_struct*);
void (*close)(struct vm_area_struct*);
int (*fault)(struct vm_area_struct*, struct vm_fault*);
int (*page_mkwrite)(struct vm_area_struct *vm, struct vm_fault *vmf);
int (*access)(struct vm_area_struct*, unsigned long, void*, int, int);
}
void open(struct vm_area_struct *area);
void close(struct vm_area_struct *area);
int fault(struct vm_area_struct *area, struct vm_fault *vmf);
int page_mkwrite(struct vm_area_struct *area, struct vm_fault *vmf);
int access(struct vm_area_struct *vm, unsigned long address, void *buf, int len, int write);
内存区域的树型结构和内存区域的链表结构
- 可以通过内存描述符中的mmap和mm_rb两个成员之一访问内存区域。这两个成员各自独立地指向与内存描述符相关的全体内存区域对象,其实,它们包含完全相同的vm_area_struct结构体的指针
- mmap使用单独链表连接所有的内存区域对象,每一个vm_area_struct结构体通过自身的vm_next成员连接入链表,所有的区域按地址增长的方向排序,mmap成员指向链表中第一个内存区域,链表最后一个结构体指针指向空
- mm_rb成员使用红-黑树连接所有的内存区域对象,mm_rb成员指向红-黑树的根节点,地址空间中每一个vm_area_struct结构体通过自身的vm_rb成员连接到树中
实际使用中的内存区域
- 可以使用/proc文件系统和pmap工具查看给定进程的内存空间和其中所包含的内存区域
- /proc/<pid>/maps输出显示该进程地址空间中的全部内存区域,如
$cat /proc/1488/maps
00e80000-00faf000 r-xp 00000000 03:01 208530 /lib/tbs/libc-2.5.1.so
00faf000-00fb2000 rw-p 0012f000 03:01 208530 /lib/tbs/libc-2.5.1.so
00fb2000-00fb4000 rw-p 00000000 00:00 208530 0
......
......
$pmap 1488
example [1488]
00e80000(1212KB) r-xp (03:01 208530) /lib/tbs/libc-2.5.1.so
......
......
- 每个和进程相关的内存区域都对应一个vm_area_struct结构体,另外进程不同于线程,进程结构体task_struct包含唯一的mm_struct结构体引用
操作内存区域
- 内核时常需要在某个内存区域上执行一些操作,比如某个指定地址是否包含在某个内存区域中
- 为了方便执行这类对内存区域的操作,内核定义许多的辅助函数,声明在文件<linux/mm.h>
find_vma()
- find_vma()函数提供一个给定的内存地址属于哪一个内存区域,该函数定义在文件<mm/mmap.c>中
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;
}
fine_vma_prev()
- fine_vma_prev()函数和fine_vma_prev()工作方式相同,但是它返回第一个小于addr的VMA
- 该函数定义和声明分别在文件mm/mmap.c和<linux/mm.h>中
- pprev参数存放指向先于addr的VMA指针
#include <linux/mm.h>
struct vm_area_struct *find_vma_prev(struct mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev);
fine_vma_intersection()
- fine_vma_intersection()函数返回第一个和指定地址区间相交的VMA,该函数时内联函数,定义在文件<linux/mm.h>
- 第一个参数mm是要搜索的地址空间,start_addr是区间的开始首位置,end_addr是区间的尾位置
#include <linux/mm.h>
static inline struct vm_area_struct *fine_vma_intersection(struct mm_struct *mm, unsigned long start_addr, unsigned long end_addr)
{
struct vm_area_struct *vma;
vma = find_vma(mm, start_addr);
if (vma && end_addr <= vma->vm_start)
vma = NULL;
return vma;
}
mmap()和do_mmap()创建地址空间
- 内核使用do_mmap()函数创建一个新的线性地址空间
- 如果创建的地址空间和一个已经存在的地址区间相邻,并且具有相同的访问权限,那么两个区间将合并为一个,如果不能合并,就创建一个新的VMA
- do_mmap()函数都将一个地址空间加入到进程的地址空间中——无论扩展已存在的区域还是创建一个新的区域
#include <linux/mm.h>
unsigned long do_mmap(struct file *file,
unsigned long addr,
unsigned ling len,
unsigned long prot,
unsigned long flag,
unsigned long offset);
- 该函数映射由file指定的文件,具体映射的是文件中从偏移offset处开始,长度为len字节的范围内的数据。如果file参数为NULL,并且offset蚕食也是0,那么代表这次映射没有和文件相关,称为“匿名映射”(anonymous mapping),如果指定文件名和偏移名,称为“文件映射”(file-backed mapping)
- addr可选参数,指定搜索空闲区域的起始位置
- prot参数指定内存区域中页面的访问权限。访问权限标志定义在文件<asm/mman.h>中,不同体系结构标志的定义有所不同,但是对所有体系结构而言,都包含如下标志
标志 | 对新键区间页的要求 |
---|
PROT_READ | 对应于VM_READ |
PROT_WRITE | 对应与VM_WRITE |
PROT_EXEC | 对应与VM_EXEC |
PROT_NONE | 页不可被访问 |
- flag参数指定了VM标志,这些标志指定类型并改变映射的行为,在文件<asm/mman.h>定义
标志 | 对新区间的要求 |
---|
MAP_SHARED | 映射可以被共享 |
MAP_PRIVATE | 映射不能被共享 |
MAP_FIXED | 新区间必须开始于指定的地址addr |
MAP_ANONYMOUS | 映射不是file-backed,而是匿名的 |
MAP_GROWSDOWN | 对应于VM_GROWSDOWN |
MAP_DENYWRITE | 对应于VM_DENYWRITE |
MAP_EXECUTABLE | 对应于VM_EXECUTABLE |
MAP_LOCKED | 对应于VM_LOCKED |
MAP_NORESERVE | 不需要为映射保留空间 |
MAP_POPULATE | 填充页表 |
MAP_NONBLOCK | 在I/O操作上不堵塞 |
- 如果系统调用do_mmap()的参数有无效参数,那么返回一个负值;否则,将在虚拟内存中分配一个合适的新内存区域
- 如果有可能,将新区域和邻近区域进行合并,否则内核从vm_area_cachep长字节(slab)缓存中分配一个vm_area_struct结构体,并且使用vma_link()函数将新分配的内存区域添加到地址空间的内存区域链表和红-黑树中,随后更新内存描述中的tatal_vm域,然后才返回新分配的地址空间的初始地址
用户空间
- 在用户空间可以通过mmap()系统调用获取内核函数do_mmap()的功能,mmap()系统调用如下
void *mmap2(void *start, size_t length, int prot, int flags, int fa, off_t pgoff)
mummap()和do_mummap()删除地址空间
- do_mummap()函数从特定的进程地址空间中删除指定地址区间,第一个参数指定要删除区域的所在地址空间,删除从地址start开始,长度为len字节的地址区间,如果成功,返回零,否则,返回负的错误码,该函数定义在文件<linux/mm.h>
#include <linux/mm.h>
int do_mummap(struct mm_struct *mm, unsigned long start, size_t len);
- 系统调用munmap()给用户空间程序提供从自身地址空间中删除指定地址区间的方法,与系统调用mmap()作用相反
int munmap(void *start, size_t length);
- 该系统调用定义在文件mm/mmap.c中,对do_mummap()函数的一个简单的封装
asmlinkage long sys_munmap(unsigned long addr, size_t len)
{
int ret;
struct mm_struct *mm;
mm = current->mm;
down_write(&mm->nmap_sem);
ret = do_munmap(mm, addr, len);
up_write(&mm->mmap_sem);
return ret;
}
页表
- Linux使用三级页表完成地址转换。Linux对所有的体系结构,包括对哪些不支持三级页表的体系结构(有些体系结构只使用二级页表或者使用散列表完成地址转换)都使用三级页表管理
- 顶级页表是页全局目录(PGD),包含一个pgd_t类型数组,多数体系结构中pgd_t类型等同于无符号长整形类型。PGD中的表项指向二级页目录中的表项:PMD
- 二级页表是中间页目录(PMD),是pmd_t类型数组,其中的表项指向PTE中的表项
- 最后一级的页表简称页表,其中包含pte_t类型的页表项,该页表指向物理页面
- 多数体系结构中,搜索页表的工作是由硬件完成的(至少在某种程度上)。
- 每个进程都有自己的页表(线程会共享页表)。内存描述符的pgd成员指向的就是进程的页全局目录。操作和检索页表时必须使用page_table_lock锁,该锁在相应的进程的内存描述符中,以防竞争条件
- 页表对应的结构体依赖具体的体系结构,定义在文件<asm/page.h>中
- 多数体系结构都实现一个地址转换缓冲器(translate lookaside buffer,TLB),TLB将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器将首先检查TLB中是否缓存了该虚拟地址到物理地址的映射,如果缓存中直接命中,物理地址立刻返回,否则,需要通过页表搜索需要的物理地址
- 2.6版内核对页表管理的改进:从高端内存分配部分页表