目录
4.3 内存映射原理
4.4 数据结构
4.4.1 树和链表
4.4.2 虚拟内存区域VMA的表示
4.4.3 相关数据结构
本节讲VMA结构体struct vm_area_struct和struct address_space。
4.3 内存映射原理
所有进程的虚拟空间总和比物理内存大得多,因此只有最常用的虚拟空间才映射到物理内存。
当访问一个未映射物理内存的虚拟内存时,进行按需调页。
按需调页步骤:
进程访问用户空间虚拟地址,但无法通过页表找到对应物理地址。
CPU触发缺页异常。
通过虚拟地址对应address_space结构体,找到磁盘数据。
分配物理内存页,读取磁盘数据到内存。
建立正确页表,进程恢复执行。
后续详解。
4.4 数据结构
struct mm_struct:提供了进程在内存布局的所有必要信息。
struct mm_struct {
...
struct vm_area_struct *mmap; 链表,用于遍历该进程所有vma
struct rb_root mm_rb; 红黑树,用于查找和插入该进程vma
struct vma_area_struct *mmap_cache; 缓存上一次find_vma结果
...
}
4.4.1 树和链表
每个区域(VMA)都用一个vm_area_struct表示,如代码段,数据段,堆,栈。
一个进程的所有VMA同时有两种排序方式:
1. 单链表:mm_struct->mmap
VMA根据起始地址以递增顺序放入mm_struct->mmap链表。
作用:链表使用于遍历VMA。
2. 红黑树:根节点为mm_struct->mm_rb
VMA太多时,查找/插入操作等用红黑树更快。
4.4.2 虚拟内存区域VMA的表示
虚拟内存区域:即VMA。如:
一个进程空间的代码段,数据段,堆,栈,mmap映射区域等。
每个VMA用struct vm_area_struct实例表示。
应用层哪些操作时会生成一个VMA:
mmap函数。
运行可执行文件或共享库。
堆栈增长。
shmat创建共享内存。
malloc函数大内存。
两种映射:
1. 匿名映射:没有对应映射的文件,如:
堆,栈,mmap的MAP_ANONYMOUS类型映射。
创建映射方法:
void *addr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); //fd为-1
2. 文件映射:有对应映射源文件,如:
数据段,代码段(对应可执行文件的.data和.text section)。
创建映射方法:
void *addr = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
struct vm_area_struct {
struct mm_struct *vm_mm;
所属mm_struct
unsigned long vm_start,vm_end;
该vma起始与结束地址
struct vm_area_struct *vm_next, *vm_prev;
mm_struct中vma链表中前/后一个VMA(如上图)
pgprot_t vm_page_prot;
unsigned long vm_flags;
struct rb_node vm_rb;
union {
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} linear;
struct list_head nonlinear;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
struct vm_operations_struct *vm_ops;
unsigned long vm_pgoff;
映射的文件位置的偏移量。
若映射整个文件则为0。
struct file *vm_file;
表示被映射的文件。
若匿名映射,则为NULL。
}
成员解释:
1. pgprot_t vm_page_prot:
该vma对应物理页的权限。值有:
PROT_READ:允许读取页面。
PROT_WRITE:允许写入页面。
PROT_EXEC:允许执行页的代码。
PROT_NONE:不允许任何访问。
用法:entry = mk_pte(page, vma->vm_page_prot); //生成对应权限的页表项。
2. vm_flags:
该VMA访问权限。值有:
VM_READ,VM_WRITE,VM_EXEC:可读,可写,可执行
VM_SHARD:该VMA由多进程共享。
VM_PRIVATE:该VMA是私有的
VM_MAYREAD:可设置为可读。
VM_GROWSUP:该VMA向上扩展,如栈。
VM_DONTCOPY:fork函数时不复制该VMA。
根据VMA的访问权限(vm_flags)获取对应页面的保护权限(vm_page_prot)
vma->vm_page_prot = vm_get_page_prot(vma->vm_flags) ;
3. struct vm_area_struct *vm_next, *vm_prev;
mm_struct中struct vm_area_struct * mmap:是一个链表头。
链表作用:连接了该进程的所有vma,便于遍历VMA。
vm_prev:前一个VMA
vm_next:后一个VMA
注意:VMA地址递增排序。
4. struct rb_node vm_rb;
mm_struct中struct rb_root mm_rb:一个红黑树根节点。
mm_rb作用:
包含该进程的所有VMA到红黑树中,便于高效查询VMA。
vm_rb:树节点,用于插入到红黑根节点中。
5. union {
struct {
struct rb_node rb; 线性映射
unsigned long rb_subtree_last;
} linear;
struct list_head nonlinear; 非线性映射
} shared;
用于文件映射的VMA:
1. VMA是线性映射
struct rb_node rb作用:
将该VMA插入到以address_space->i_mmap为根结点的红黑树。
2. VMA是非线性映射(即该VMA映射到多个非连续的物理内存区域。)
struct list_head nonlinear作用:
将该VMA连接到address_space->i_mmap_nonlinear链表中。
address_space作用:
同一文件被映射到不同进程,有多个VMA。
address_space通过红黑树或链表管理所有VMA。
所以通过address_space,可知道一个文件被映射到哪些VMA。
线性映射:
将虚拟地址简单偏移得到物理地址。如文件和共享库的映射。
非线性映射:
被映射到多个非连续物理内存。如大文件映射。
6. struct list_head anon_vma_chain
struct anon_vma *anon_vma
(后续章节详解)
刚创建子进程后,父子进程会共享相同地址空间,包括匿名页。
管理共享同一匿名页的所有VMA。
匿名页时,stuct page->struct address_space *mapping不是指向struct address_space,而是struct anon_vma。
struct address_space管理文件映射的VMA,而struct anon_vma管理匿名映射的VMA。
如何找到映射该匿名页的所有VMA:
page -> struct anon_vma -> struct anon_vma_chain -> VMA
7. struct vm_operations_struct vm_ops:
该VMA的操作函数。
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
创建/删除vma,不常用,设为NULL。
int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
};
fault函数使用场景:
缺页异常:
某虚拟地址没有分配对应物理页,触发缺页异常。
执行fault函数,分配页,并读取数据到页中。
写时复制COW:
向只读映射写数据时,触发写时复制。
执行fault函数,分配页作为副本并写入数据,不影响其他进程共享页面。
共享页面拷贝:
进程尝试写共享页时,fault将创建分配页,作为私有副本,并写入数据。
多进程共享同一物理页可节约内存。使用场景:
执行相同程序:可共享代码段的页。
动态库:共享库的代码段和数据段。基于写时复制,同一变量在不同进程的值不同。
文件映射:映射同一文件到多进程,如映射数据库系统。
共享内存IPC:多进程映射同一内存,进行进程间通信。
4.4.3 相关数据结构
struct file:内核中表示一个打开的文件。
struct file {
struct inode *f_inode; 对应磁盘文件
struct address_space *f_mapping; 该文件映射信息
}
struct address_space {
struct inode *host; 缓存的磁盘文件
struct radix_tree_root page_tree;
该树连接了缓存了文件host的所有页page。
作用:
1. 通过该树,可查找到缓存文件指定偏移index的页。
2. 可知某文件指定偏移处是否被缓存。若缓存,直接操作页,不用从磁盘读取。
//radix_tree_insert(&mapping->page_tree, index, page);
struct rb_root i_mmap;
红黑树,连接该文件的线性映射的所有VMA。
struct list_head i_mmap_nonlinear;
链表,连接该文件的非线性映射的所有VMA。
struct address_space_operations *a_ops;
}
address_space作用:
1. 关联了文件和映射该文件的页(页缓存)。
即关联struct inode *host和struct radix_tree_root page_tree。
2. 保存映射该文件所有VMA。
即struct rb_root i_mmap树和struct list_head i_mmap_nonlinear链表。
struct inode { inode对应一个磁盘文件
struct address_space *i_mapping;
}
struct file { file对应一个已打开的文件
struct inode *f_inode;
struct address_space *f_mapping;
}
打开文件时:
file->f_mapping = inode->i_mapping;
一个进程多次打开同一个文件,内核为每次打开创建一个struct file实例。
而struct inode只有一个。
struct inode和struct file都包含struct address_space。
通过address_space的host成员可找到文件inode。
再通过inode中主/次设备号找到块设备文件。
如何将文件1.c的从头偏移100到1000区域映射到内存中?
fd = open("/tmp/1.c", O_RDONLY);
mmap(NULL, 900, PROT_READ, MAP_PRIVATE, fd, 100);
除了映射文件,mmap还可直接映射raw设备。
作用:绕过文件系统接口和文件缓冲区。
fd = open("/dev/mtdlbock1", O_RDONLY);
mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, offset);