malloc()分配的内存。内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。
内存描述符由mm_struct结构体表示,定义在文件中。
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs 内存区域链表 */
struct rb_root mm_rb; /* VMA 形成的红黑树 */
struct vm_area_struct * mmap_cache; /* last find_vma result 最近使用的内存区域 */
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
#endif
unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
unsigned long task_size; /* size of task vm space */
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; /* How many references to "struct mm_struct" (users count as 1) 主使用计数器 */
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; /* 所有mm_struct 形成的链表 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 */
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 核心转储的支持 */
#ifdef CONFIG_AIO
spinlock_t ioctx_lock; /* AIO I/O 链表锁*/
struct hlist_head ioctx_list; /* AIO I/O 链表 */
#endif
mmap 和 mm_rb 这两个不同数据结构体描述的对象是相同的:该地址空间中的全部内存区域。mmap 是以链表形式存放的,mm_rb以红黑树的形式存放。
所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表init进程的地址空间。
操作该链表的时候需要使用mmlist_lock锁来防止并发访问,该锁定义在文件kernel/fork.c中。
copy_mm()
在进程的进程描述符(task_struct)中,mm域存放着该进程使用内存描述符(mm_struct),所以current->mm指向当前进程的内存描述符。
fork()函数利用copy_mm()函数复制父进程的内存描述符,也就是current->mm域给其子进程,而子进程中mm_struct结构体实际是通过文件kernel/fork.c中的allocate_mm()宏从mm_cachep slab缓存中分配得到的。因此通常,每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间。
是否共享地址空间几乎是进程和Linux中所谓的线程间本质上的唯一区别。线程对内核来说仅仅是一个共享特定资源的进程而已。
如何让父进程和子进程共享地址空间?
在调用clone()时,设置CLONE_VM标志。(这样的子进程称为线程)当CLONE_VM被指定后,内核就不再需要调用allocate_mm()函数,而仅仅需要在调用copy_mm()函数中将mm域指向其父进程的内存描述符就可以了。
exit_mm -> mmput -> mmdrop -> free_mm -> kmem_cache_freekernel/exit.c中的exit_mm()函数,该函数执行一些常规的撤销工作,同时更新一些统计量。其中,该函数会调用mmput()函数减少内存描述符中mm_users用户计数,如果用户计数降到零,将调用mmdrop()函数,减少mm_count使用计数。free_mm()宏通过kmem_cache_free()函数将mm_struct结构体归还到mm_cachep slab缓存中。mm_struct与内核线程
mm域为空,即task_struct里的mm_struct为空。(因为内核线程没有用户上下文)mm域为空,使用谁的?active_mm域的作用。当一个进程被调度时,该进程的mm域指向的地址空间被装载到内存,进程描述符中active_mm域会被更新,指向新的地址空间。内核线程没有地址空间,所有mm域为NULL。于是,当一个内核线程被调度时,内核发现它的mm域为NULL,就会保留前一个进程的地址空间,随后内核更新内核线程对应的进程描述符中active_mm域,使其指向前一个进程的内存描述符。所以在需要的时候,内核线程便可以使用前一个进程的页表。因为内核线程不访问用户空间的内存,所以它们仅仅使用地址空间和内核内存相关的信息。vm_area_struct结构体描述,定义在文件中。Linux内核中页称作虚拟内存区域(VMAs:virtual memortAreas)。vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围。struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* Our start address within vm_mm. 区间的首地址 */
unsigned long vm_end; /* 区间的尾地址 The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev; /* VMA 链表 */
struct rb_node vm_rb; /* 树上该VMA的节点 */
/*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap;
/* Second cache line starts here. */
struct mm_struct *vm_mm; /* The address space we belong to. 相关的mm_struct内存描述符结构体 */
pgprot_t vm_page_prot; /* Access permissions of this VMA. 访问控制权限 */
unsigned long vm_flags; /* Flags, see mm.h. 标志 */
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree, or
* linkage of vma in the address_space->i_mmap_nonlinear list.
*
* For private anonymous mappings, a pointer to a null terminated string
* in the user process containing the name given to the vma, or NULL
* if unnamed.
*/
union {
struct {
struct rb_node rb; /* 树上该VMA的节点 */
unsigned long rb_subtree_last;
} linear;
struct list_head nonlinear;
const char __user *anon_name;
} shared;
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock 匿名VMA对象 */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops; /* 相关的操作表 */
/* Information about our backing store: */
unsigned long vm_pgoff; /* 文件中的偏移量 Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). 被映射的文件(如果存在) */
void * vm_private_data; /* was vm_pte (shared mem) 私有数据 */
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
};
vm_start域指向区间的首(最低)地址,vm_end域指向区间的尾(最高)地址之后的第一个字节–vm_start在区间内,vm_end在区间外。vm_mm域指向和VMA相关的mm_struct结构体,注意,每个VMA对其相关的mm_struct结构体来说都是唯一的,所以即使两个独立的进程将同一个文件映射到各自的地址空间,它们分别都会有一个vm_area_struct结构体来标志自己的内存区域;vm_area_struct结构体。VMA 标志
VMA标志是一种位标志,其定义在。
它包含在vm_flags域内,标志了内存区域所包含的页面的行为和信息。
VMA标志可取值如下表:
| 标志 | 对VMA及其页面的影响 |
|---|---|
| VM_READ | 页面可读取 |
| VM_WRITE | 页面可写 |
| VM_EXEC | 页面可执行 |
| VM_SHARED | 页面可共享 |
| VM_MAYREAD | VM_READ 标志可被设置 |
| VM_MAYWRITE | VM_WRITE 标志可被设置 |
| VM_MAYEXEC | VM_EXEC 标志可被设置 |
| VM_MAYSHARE | VM_SHARE 标志可被设置 |
| 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 | 该区域是非线性映射的 |
VMA 操作 -> vm_ops -> vm_operations_struct
vm_area_struct 结构体中的 vm_ops 域指向与指定内存区域相关的操作函数表,内核使用表中的方法操作VMA。vm_area_struct 作为通用对象代表了任何类型的内存区域,而操作表描述符对特定的对象实例的特定方法。vm_operations_struct结构体表示,定义在文件 中:/*
* These are the virtual MM functions - opening of an area, closing and
* unmapping it (needed to keep files on disk up-to-date etc), pointer
* to the functions called when a no-page or a wp-page exception occurs.
*/
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
/* 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);
/* 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);
#ifdef CONFIG_NUMA
/*
* set_policy() op must add a reference to any non-NULL @new mempolicy
* to hold the policy upon return. Caller should pass NULL @new to
* remove a policy and fall back to surrounding context--i.e. do not
* install a MPOL_DEFAULT policy, nor the task or system default
* mempolicy.
*/
int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
/*
* get_policy() op must add reference [mpol_get()] to any policy at
* (vma,addr) marked as MPOL_SHARED. The shared policy infrastructure
* in mm/mempolicy.c will do this automatically.
* get_policy() must NOT add a ref if the policy at (vma,addr) is not
* marked as MPOL_SHARED. vma policies are protected by the mmap_sem.
* If no [shared/vma] mempolicy exists at the addr, get_policy() op
* must return NULL--i.e., do not "fallback" to task or system default
* policy.
*/
struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
unsigned long addr);
int (*migrate)(struct vm_area_struct *vma, const nodemask_t *from,
const nodemask_t *to, unsigned long flags);
#endif
/* called by sys_remap_file_pages() to populate non-linear mapping */
int (*remap_pages)(struct vm_area_struct *vma, unsigned long addr,
unsigned long size, pgoff_t pgoff);
};
| 函数 | 描述 |
|---|---|
*void open(struct vm_are_struct *area) | 当制定的内存区域被加入到一个地址空间时,该函数被调用 |
*void close(struct vm_area_struct *area) | 当指定的内存区域从地址空间删除时,该函数被调用 |
*void 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 *vma, unsigned long address, void *buf, int int write) | 当get_uset_pages()函数调用失败时,该函数被access_process_vm()函数调用 |
mm_struct之*mmap和mm_rb
mmap域使用单独链表连接所有的内存区域对象。每一个vm_area_struct结构体通过自身的vm_next域被连入链表,所有的区域按地址增长的方法排序,mmap域指向链表中第一个内存区域,链中最后一个结构体指针指向空。mm_rb域使用红黑树连接所有的内存区域对象。mm_rb域指向红黑树的根节点,地址空间中每一个vm_area_struct结构体通过自身的vm_rb域连接到树中。/proc文件系统或pmap(1)工具
/proc文件系统和pmap(1)工具查看给定进程的内存空间和其中所含的内存区域。
cat /proc/\/maps 或pmap \列出该进程地址空间中包含的内存区域。libc.so和ld.so对应的上述三种内存区域。此外,地址空间还要包含进程栈对应的内存区域。

00:00,索引节点标志也为0,这个区域就是零页–零页映射的内容全为零。vm_area_struct结构体,进程不同于线程,进程结构体stask_struct包含唯一的mm_struct结构体引用,而线程则是共享了地址空间,即copy_mm。find_vma()
find_vma()函数用来找到一个给定的内存地址属于哪一个内存区域(在指定的地址空间中搜索第一个vm_end大于addr的内存区域,也就是寻找第一个包含addr或者首地址大于addr的内存区域)。定义在文件中:struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);
find_vma_prev()
find_vma_prev()函数和find_vma()工作方式相同,但它返回第一个小于addr的VMA。mm/mmap.c和文件中:struct vm_area_struct *find_vma_prev(struct mm_struct *mm,unsigned long addr,struct vm_area_struct **pprev);
pprev参数存放指向先于addr的VMA指针。find_vma_intersection()
find_vma_intersection()函数返回第一个和指定地址区间相交的VMA。中:/* Look up the first VMA which intersects the interval start_addr..end_addr-1,
NULL if none. Assume start_addr < end_addr. */
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{
struct vm_area_struct * vma = find_vma(mm,start_addr);
if (vma && end_addr <= vma->vm_start)
vma = NULL;
return vma;
}
mm是要搜索的地址空间;start_addr是区间的开始首地址;end_addr是区间的尾地址。find_vma()返回NULL,那么find_vma_interesection()也会返回NULL。find_vma()返回有效的VMA,find_vma_intersection()只有在该VMA的起始位置于给定的地址区间结束位置之前,才将其返回。如果VMA的起始位置大于指定位置范围的结束位置,则该函数返回NULL。mmap()和do_mmap():创建地址区间mmap()->do_mmap()
内核使用do_mmap()函数创建一个新的线性地址区间。但是说该函数创建了一个新的VMA并不非常准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限,两个区间合并为一个。如果不能合并,则确实需要创建一个新的VMA。
无论哪种情况,do_mmap()函数都会将一个地址区间加入到进程的地址空间中–无论是扩展已存在的内存区域还是创建一个新的区域。
do_mmap()函数定义在文件kernel/arch/powerpc/kernel/syscalls.c,以内联形式展示,书中中。 ps:比较旧的内核版本是do_mmap,后面更新成do_mmap2,区别仅仅在于最后一个参数,后者使用页面偏移作为最后一个参数,使用页面偏移量可以映射更大的文件和更大的偏移位置:
unsigned long sys_mmap2(unsigned long addr, size_t len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff)
{
return do_mmap2(addr, len, prot, flags, fd, pgoff, PAGE_SHIFT-12);
}
unsigned long sys_mmap(unsigned long addr, size_t len,
unsigned long prot, unsigned long flags,
unsigned long fd, off_t offset)
{
return do_mmap2(addr, len, prot, flags, fd, offset, PAGE_SHIFT);
}
static inline unsigned long do_mmap2(unsigned long addr, size_t len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long off, int shift)
{
unsigned long ret = -EINVAL;
if (!arch_validate_prot(prot))
goto out;
if (shift) {
if (off & ((1 << shift) - 1))
goto out;
off >>= shift;
}
ret = sys_mmap_pgoff(addr, len, prot, flags, fd, off);
out:
return ret;
}
映射没有和文件相关,该情况称作匿名映射(anonymous mapping);
如果指定了文件名和偏移量,则该映射称为文件映射(file-backed mapping);
虽然C库仍然可以使用原始版本的映射方法(mmap),但它其实还是基于函数mmap2()进行的,因为对原始mmap()方法的调用是通过将字节偏移转化为页面偏移,从而转化为对mmap2()函数的调用来实现的。
munmap()(kernel/include/linux/syscalls.h) ->do_munmap()do_munmap()函数从特定的进程地址空间中删除指定地址区间,该函数定义在文件kernel/mm/nommu.c,书中是。int do_munmap(struct mm_struct *mm,unsigned long start, size_t len);
start开始,长度为len字节的地址区间;成功返回零,失败返回负数错误码。munmap()给用户空间程序提供了一种从自身地址空间删除指定地址区间的方法,和系统调用mmap()作用相反:asmlinkage long sys_munmap(unsigned long addr, size_t len);
虽然应用程序操作的对象是映射到物理内存上的虚拟内存,但是处理器直接操作的却是物理内存。
页表为何存在?
当应用程序访问一个虚拟地址时,首先必须将虚拟地址转化成物理地址,然后处理器才能解析地址访问请求。地址的转换工作需要通过查询页表才能完成,即地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表项则指向下一级别或指向最终的物理页面。
Linux中使用三级页表完成地址转换。利用多级页表能够节约地址转换需占用的存放空间。

顶级页表是页全局目录(PGD),包含了一个pgd_t类型数组,多数体系结构图中pgd_t类型等同于无符号长整型类型。PGD中的表项指向二级页目录中的表项:PMD;
二级页表是中间页目录(PMD),是个pmd_t类型数组,其中的表项指向PTE中的表项;
最后一级的页表简称页表(PTE),包含了pte_t类型的页表项,该页表项指向物理页面。
每个进程都有自己的页表(线程会共享页表)。
内存描述符的pgd域指向的就是进程的页全局目录(PGD)。
操作和检索页表时,必须使用page_table_lock锁,该锁在相应的进程的内存描述符中,以防止竞争条件。
页表对应的结构体依赖于具体的体系结构,定义在文件。
页表机制的性能?
translate lookaside buffer,TLB)。TLB作为一个将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器将首先检查TLB中是否缓存了该虚拟地址到物理地址的映射,如果在缓存中直接命中,物理地址立刻返回;否则,就需要再通过页表搜索需要的物理地址。