1 地址分布
现在采用虚拟内存的操作系统通常都使用平坦地址空间,平坦地址空间是指地址空间范围是一个独立的连续空间(比如,地址从0扩展到429496729位地址空间),对于32位的操作系统而言,每个进程的虚拟地址空间都是0x00000000~0xC0000000,合计3G大小。
进程的3G虚拟地址空间只有映射为物理地址空间,才能够被使用,那么进程是如何管理和分配它的3G虚拟地址空间呢?
这就用到了分治思想,进程虚拟地址空间按照不同的访问属性和功能划分为不同的内存区域,我们也叫虚拟内存区域(VMA)。
内存区域可以包含各种内存对象,比如:
- 代码段(text section):可执行文件的内存映射
- 数据段:可执行文件的已初始化全局变量和静态局部变量的内存映射
- bss段:未初始化的或者值为0的变量的内存映射
- lib库的代码段:(多个)
- lin库的数据段:(多个)
- lib库的bss段:(多个)
- 任何内存映射文件(有名mmap建立)
- 任何共享内存段(匿名mmap建立)
- 进程栈(stack)
- 进程堆(heap)
实际使用中的内存区域
可以使用/proc文件系统和pmap工具查看给定进程的内存空间和其中所包含的内存区域。
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("PID=%d\n",getpid());
while(1)
{
sleep(2);
}
return 0;
}
运行该程序,输入命令 cat /proc/<pid>/maps查看进程地址空间中的全部内存区域(我的机子是64位,所以使用的是64位虚拟地址空间)
进程的内存区域由vm_area_struct结构体描述,定义在文件linux/mm.h中
struct vm_area_struct {
struct mm_struct * vm_mm; /* The address space we belong to. */
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;
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, listed below. */
struct rb_node vm_rb;
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap prio tree, or
* linkage to the list of like vmas hanging off its node, or
* linkage of vma in the address_space->i_mmap_nonlinear list.
*/
union {
struct {
struct list_head list;
void *parent; /* aligns with prio_tree_node parent */
struct vm_area_struct *head;
} vm_set;
struct prio_tree_node prio_tree_node;
} 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_node; /* Serialized by anon_vma->lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
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) */
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
};
上面的图片中,输出由六列,每一列都是vm_area_struct的一项
内核vm_area_struct中的项 | /proc/pid/maps中的项及其含义 |
---|---|
vm_start | 第一列’-'前的数字,如55c4b4d68000 ,表示该虚拟内存区域的开始地址 |
vm_end | 第一列’-'后的数字 ,如55c4b4d69000 ,表示该虚拟内存区域的结束地址 |
vm_flags | 第二列,如r-xp,表示该虚拟内存区域的属性,每种属性用一个字段表示,r表示可读,w表示可写,x表示可执行,p和s共用一个字段,p表示私有段,s表示共享段,如果没有相应权限,用’-'代替 |
vm_pgoff | 第三列,如00001000,含义:对用有名映射,表示此虚拟内存起始地址在文件中以页为单位的编译,对匿名映射,它等于0或者vm_start/PAGE_SIZE |
vm_file->f_dentry->d_inode->i_sb->s_dev | 第四列,如08:01,表示映射文件所属设备号,对匿名映射来说,因为没有文件在磁盘上,所有没有设备号,始终为00:00,对有名映射来说,是映射的文件所在设备的设备号 |
vm_file->f_dentry->d_inode->i_ino | 第五列,如1724853,含义:映射文件所属节点号,对匿名文件来说,因为没有节点号,所以时钟是0,对有名映射来说,是映射文件的结点号 |
第六列,如/lib/x86_64-linux-gnu/libc-2.27.so,对有名映射来说,是映射的文件名,对匿名映射来说,是此段虚拟内存在进程中的角色,stack表示在进程中作为栈使用,heap表示堆 |
2 进程的虚拟地址描述
内核使用mm_struct来描述一个进程的地址空间,进程的地址空间由多个VMA组成,下面列举几个mm_struct管理内存的几个重要域:
struct mm_struct {
...
/* 指向虚拟内存区域的链表 */
struct vm_area_struct * mmap; /* list of VMAs */
/* 指向最近找到的虚拟内存区域 */
struct vm_area_struct * mmap_cache; /* last find_vma result */
/* 指向该进程的页目录表 */
pgd_t * pgd;
...
};
VMA用struct vm_area_struct描述,内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都有一致的属性,比如权限等。所以我们程序的代码段、数据段和bss段在内核里都分别有一个struct vm_area_struct结构体来描述。
进程由结构体task_struct描述,task_struct里面的mm域用来管理进程的内存,它指向mm_struct结构体,mm_struct的mmap域指向VMA链表,用来管理进程虚拟内存,虚拟内存地址又通过页表转换为物理地址,怎么转换的,由mm_struct的pgd页目录表来转换,从页目录表中找到物理地址。
用户空间mmap
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
在用户空间使用mmap就是给进程添加一个虚拟内存区域,即在VMA链表中添加一个vm_area_struct结构
线程之间共享内存地址的实现机制
在Linux中,如果clone()时设备CLONE_VM标志,我们把这样的进程称作为线程,线程之间共享同样的虚拟内存空间。即将父进程的mm域复制给子进程。