Linux进程地址空间之初探:一
引言:现代操作系统提供了一种对内存的抽象概念,叫做虚拟存储器,它为每个进程提供了一个大的,一致的,和私有的地址空间。通过一个很清晰的机制,虚拟存储器提供了3个重要的能力:
1)它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效的使用了主存。
2)它为每个进程提供了一致的地址空间,从而简化了存储器管理。
3)它保护了每个进程的地址空间不被其他进程破坏。
Linux操作系统同样也采用了虚拟内存技术,对一个进程而言,它好像可以访问整个系统的所有物理内存,更重要的是,即使单独一个进程,它拥有的地址空间也可以远远大于系统物理内存。
一:Linux虚拟内存区域及地址空间
进程地址空间由进程可寻址的虚拟内存组成,对于某个虚拟内存地址,它要在地址空间范围内,例如: 0421f000,这个值表示的是进程32位地址空间中的一个特定的字节。尽管一个进程可以寻址4GB的虚拟内存(在32位的地址空间中),但是这并不代表它有权访问所有的虚拟地址。在地址空间中,我们更常用或者关心的是某个虚拟内存地址空间,比如 0848000-084c000,它们可以被进程访问。我们称这些可被访问的合法地址空间称为 虚拟内存区域。通过内核,进程可以给自己的地址空间动态的增加或减少虚拟内存区域。
Linux进程的虚拟内存区域一般有:代码段、数据段、堆、用户栈、共享段。每个存在的虚拟页面都保存在某个区域中,而不属于某个区域的虚拟页是不存在的,并且不能被进程访问。内核不用记录那些不存在的虚拟页,而这样的页也不占用存储器、磁盘或者内核本身的其他任何资源。
进程只能访问有效内存区域的内存地址,每个内存区域也具有相关权限,如可读、可写、可执行性质。如果一个进程访问了无效范围中的内存区域或者以不正确的方式访问了有效地址,那么内核就会终止该进程,并返回 “段错误”信息。
二:内存描述符
task_struct 中的一个条目 mm 指向mm_struct 即内存描述符,它描述了进程的虚拟内存当前状态。mm_struct 定义在linux/sched.h中:
1 struct mm_struct { 2 struct vm_area_struct * mmap; /* list of VMAs */ 3 struct rb_root mm_rb; 4 struct vm_area_struct * mmap_cache; /* last find_vma result */ 5 unsigned long free_area_cache; /* first hole */ 6 pgd_t * pgd; 7 atomic_t mm_users; /* How many users with user space? */ 8 atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */ 9 int map_count; /* number of VMAs */ 10 struct rw_semaphore mmap_sem; 11 spinlock_t page_table_lock; /* Protects task page tables and mm->rss */ 12 13 struct list_head mmlist; /* List of all active mm's. These are globally strung 14 * together off init_mm.mmlist, and are protected 15 * by mmlist_lock 16 */ 17 18 unsigned long start_code, end_code, start_data, end_data; 19 unsigned long start_brk, brk, start_stack; 20 unsigned long arg_start, arg_end, env_start, env_end; 21 unsigned long rss, total_vm, locked_vm; 22 unsigned long def_flags; 23 24 unsigned long saved_auxv[40]; /* for /proc/PID/auxv */ 25 26 unsigned dumpable:1; 27 cpumask_t cpu_vm_mask; 28 29 /* Architecture-specific MM context */ 30 mm_context_t context; 31 32 /* coredumping support */ 33 int core_waiters; 34 struct completion *core_startup_done, core_done; 35 36 /* aio bits */ 37 rwlock_t ioctx_list_lock; 38 struct kioctx *ioctx_list; 39 40 struct kioctx default_kioctx; 41 };
这里我们主要看3个字段:1:struct vm_area_struct * mmap; 2:struct rb_root mm_rb 3: pgd_t * pgd;
其中 pgd 指向第一级页表即页全局目录的基址,当内核运行这个进程时,它就将pgd存放在CR3寄存器内,根据它来进行地址转换工作。
mmap 和 mm_rb 这两个不同数据结构体描述的对象是相同的:该地址空间中的所有内存区域。
mmap 指向一个 vm_area_struct 结构的链表,利于简单、高效地遍历所有元素。
mm_rb 指向的是一个红-黑树结构节点,适合搜索指定元素。
其中每个 vm_area_struct 描述了当前虚拟地址空间的一个内存区域。这个结构定义在
文件 linux/mm.h中,如下所示:
1 struct vm_area_struct { 2 struct mm_struct * vm_mm; /* The address space we belong to. */ 3 unsigned long vm_start; /* Our start address within vm_mm. */ 4 unsigned long vm_end; /* The first byte after our end address 5 within vm_mm. */ 6 7 /* linked list of VM areas per task, sorted by address */ 8 struct vm_area_struct *vm_next; 9 10 pgprot_t vm_page_prot; /* Access permissions of this VMA. */ 11 unsigned long vm_flags; /* Flags, listed below. */ 12 13 struct rb_node vm_rb; 14 15 /* 16 * For areas with an address space and backing store, 17 * linkage into the address_space->i_mmap prio tree, or 18 * linkage to the list of like vmas hanging off its node, or 19 * linkage of vma in the address_space->i_mmap_nonlinear list. 20 */ 21 union { 22 struct { 23 struct list_head list; 24 void *parent; /* aligns with prio_tree_node parent */ 25 struct vm_area_struct *head; 26 } vm_set; 27 28 struct prio_tree_node prio_tree_node; 29 } shared; 30 31 /* 32 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma 33 * list, after a COW of one of the file pages. A MAP_SHARED vma 34 * can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack 35 * or brk vma (with NULL file) can only be in an anon_vma list. 36 */ 37 struct list_head anon_vma_node; /* Serialized by anon_vma->lock */ 38 struct anon_vma *anon_vma; /* Serialized by page_table_lock */ 39 40 /* Function pointers to deal with this struct. */ 41 struct vm_operations_struct * vm_ops; 42 43 /* Information about our backing store: */ 44 unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE 45 units, *not* PAGE_CACHE_SIZE */ 46 struct file * vm_file; /* File we map to (can be NULL). */ 47 void * vm_private_data; /* was vm_pte (shared mem) */ 48 49 #ifdef CONFIG_NUMA 50 struct mempolicy *vm_policy; /* NUMA policy for the VMA */ 51 #endif 52 };
每个内存区域描述符都对应于进程地址空间中的唯一地址空间:
vm_start 域:指向区域的首地址(最低地址),它本身在区间内。
vm_end 域:指向区域的尾地址(最高地址)之后的第一个字节,它本身在区间外。
vm_end - vm_start 的大小就是这个内存区域的长度,另外注意,在同一个地址空间内的不同内存区域不能重叠。
vm_next : 指向链表中下一个区域结构。
vm_flags:描述这个区域内的页面是共享的还是私有的。
vm_page_prot : 描述这个区域内包含的所有页的读写许可权限。
三:实际使用中的内存区域
下面我们分别用/proc 文件系统和 pmap工具查看下给定进程的内存空间及所包含的内存区域。
hello.c
1 #include<stdio.h> 2 int main() 3 { 4 5 printf("Hello\n"); 6 return 0; 7 }
下面列出该进程地址空间中的内存区域:
先通过/proc文件系统查看:
每行数据格式为:
起始地址 - 尾部地址 访问权限 偏移 主设备号:次设备号 i节点 文件名
下面是用pmap工具查看的结果:
从上面可以看出:
1:可执行对象hello的 代码段、数据段 、bss段的虚拟内存区间
2:C库中libc.so的代码段、数据段、bss段
3:动态链接程序ld.so的代码段、数据段、bss段
4:分配的内存段即相当于堆段 [anon] ,分配的栈段 [stack]
5: mappded: 2004K 表示该进程的全部地址空间大约为2004K
6:writeable/private 172k 表示可读写的内存空间大小,即消耗的物理内存空间大小。
进程访问了2003KB的数据和代码空间,而仅仅消耗了172KB的物理内存,如果一片内存区域是共享的或者不可写的,那么内核只需要在内存中为此保留一份映射。可以看出利用这种共享不可写内存的方法节约了大量内存空间。