linux进程地址空间管理

进程地址空间

主要回答如下解决问题
如何管理进程的代码段、数据段、堆栈段?
如何进行文件映射?
如何实现进程间共享共一个虚拟地址空间,而不会冲突?

linux内核不仅要管理内核内存,也用管理用户态内存,这部分内存称为进程地址空间。它采用虚拟内存技术,使得所有进程共享内存资源。

地址空间

进程地址空间有进程可寻址的虚拟内存组成。每个进程拥有平坦(独立连续的地址区间)的32bit或64bit的地址空间,这由他们的体系结构决定。当进程的地址空间相同时,他们同属于一个进程中的线程。
进程的地址空间由一个个内存区域(memory area)构成,包括它的进程栈、堆、代码段、bss段、data段,并且设定了访问属性,如可读、可写和可执行等,所以进程地址空间限定了进程能访问的地址,如果不在自己进程地址空间的内存访问,都是不被允许的,内核将这些越权访问都kill掉。
进程地址空间的有效地址只能属于一个内存区域,各内存区域地址范围不能覆盖,重叠。内存区域可以包括如下内存对象:

  • 可执行文件代码内存映射,代码段
  • 可执行文件中已初始化全局变量的内存映射,数据段
  • 未初始化全局变量,bss段的零页内存映射
  • 进程栈的零页内存映射
  • 共享库的代码段、数据段和bss也将载入进程的地址空间
  • 任何内存映射文件
  • 任何共享内存段
  • 任何匿名内存映射,如malloc

内存描述符

内核使用mm_struct内存描述符表示每个进程的地址空间。

struct mm_struct{
  struct vm_area_struct *mmap;
  struct rb_root mm_rb;
  struct vm_area_struct *mmap_cache;
  pgd_t *pgd;
  atomic_t mm_users;
  atomic_t mm_count;
  int map_count;
  struct rw_semaphore mmap_mem;
  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 end_brk;
  unsigned long start_stack;
  unsigned long end_stack;
  unsigned long start_arg;
  unsigned long end_arg;
  unsigned long start_env;
  unsigned long end_env;
  ...
};

mm_users记录正使用该mm_struct的进程数目,有几个进程共享该结构,mm_users就为几。mm_count表示mm_struct的主引用数,当有进程使用该结构时,mm_count为1,当没有进程使用时,mm_count为0,此时应该释放该结构。
mmap指针和mm_rb都包含所有内存区域,但一个用链表来组织,一个用红黑树表示。前者便于遍历,后者用于搜索。
mmlist链表将所有进程的mm_struct用双向链表组织起来,链首是init进程的内存描述符。
另外,mm_struct结构还描述了顶级页表pgd,用于地址翻译;该结构还保持了进程的代码段、数据段、bss、栈、参数和环境变量的首尾地址。

分配内存描述符

每个进程都有唯一的mm_struct结构,表示唯一的地址空间。fork函数利用copy_mm函数复制父进程的内存描述符,子进程的mm_struct结构是调用allocate_mm宏从mm_cachep slab中分配的。如果父进程想与子进程共享同一个内存描述符,则在调用clone时,指定CLONE_VM标志,则新建的进程其实是作为一个线程。所以它也不会调用allocate_mm来创建新的内存描述符了。

撤销内存描述符

在进程退出时,会调用exit函数,由它来释放进程的资源(包括清理IO缓冲,使进程停止运行,清除内存空间以及其其他内核数据结构)。其中内存清理工作由exit_mm来完成,该函数会调用mmput来减少mm_users计数。如果该引用计数降为0了,则调用free_mm宏通过kmem_cache_free来释放mm_struct结构。

mm_struct与内核线程

内核线程没有进程地址空间,也没有相关的mm_struct内存描述符,即mm域指向NULL。因为内核线程不会访问任何用户空间地址,所以它将使用前一个进程的内存描述符。这避免了内核切换地址空间带来的开销,同时减少了内存的消耗。
当内核调度内核线程时,发现mm域为NULL,就会将进程描述符中active_mm指向前一个进程的内存描述符。

虚拟内存区域

前面说,进程的地址空间由一个个内存区域组成。这些内存区域使用vm_area_struct表示。内核也将每个内存区域作为一个独立的内存对象来管理,那个内存区域拥有相同的属性,如访问权限。

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;
  struct list_head anon_vma_node;
  struct anon_vma *anon_vma;
  struct vm_operation_struct *vm_ops;
  unsigned long vm_pgoff;
  struct file *vm_file;
  ...
};

vm_area_struct所描述的内存区间说不能重叠的,vm_start和vm_end分别指向这块内存区域的首尾地址。vm_mm则指向所属的内存描述符;vm_next则用单链表的方法将各个内存区域组织起来;vm_page_prot是访问权限域;vm_rb是表示一个rb_root中的节点,方便后续作查找。anon_vma用于匿名映射,如果是文件映射,则vm_file表示所映射的文件,vm_pgoff为文件中的偏移量。
vm_flags标示了内存区域VMA每个页面的标志信息,其中最常用的表示有,

  • VM_READ 页面可读
  • VM_WRITE 页面可写
  • VM_EXEC 页面可执行
  • VM_SHARED 页面可共享
  • VM_IO 用于映射设备IO
  • VM_RESERVED 内存区域不能被换出
  • VM_SEQ_READ 页面可能会被顺序读
  • VM_RAND_READ 页面可能会被随机读

VMA操作

vm_area_struct结构中vm_ops指向了内存区域相关的操作。

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 *, struct vm_fault *);
  int (*access)(struct vm_area_struct *, unsigned long, void *, int, int);
};

其中,open方法在一个vm_area_struct加入时被调用;当一个vm_area_struct从地址空间中删除时,close被调用;当物理页不存在(还没有映射时),缺页故障处理将调用fault;page_mkwrite用于使页面可写,否则使用COW共享。

内存区域基础操作

  • find_vma
    找到给定地址所属的内存区域指针。首先在mmap_cache缓存找,如果没有找到则在红黑树中找。
  • find_vma_prev
    与find_vma相似,它返回第一个小于address的内存区域指针。
  • find_vma_intersection
    返回第一个与指定地址区间相交的内存区域指针。

mmap和do_mmap:创建内存区间

内核使用do_mmap创建新的内存区域,但是如果创建的内存区域与该进程的地址空间中某个内存区间相邻,且具有相同的访问权限,则将他们合并。

unsigned long do_mmap(struct file *, unsigned long addr, unsigned long len, 
                     unsigned long prot, unsigned long flags, unsigned long offset);

如果指定了文件名和偏移量,则是文件映射,否则是匿名映射。prot指定了VMA页面的访问权限,如PORT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE;flags指定了VMA的标志,如MAP_SHARED、MAP_PRIVATE、MAP_ANONYMOUS等。
在用户空间则是通过调用mmap2来完成映射的。

void *mmap2(void *start, size_t length, int prot, int flags, int fd, off_t pgoff);

mummap和do_mummap:删除内存区间

do_mummap从特定的地址空间中删除某个指定地址区间,其函数原型为,

int do_mummap(struct mm_struct *mm, unsigned long start, size_t len);

在用户层,可以调用系统调用munmap来已映射的内存区间。

int munmap(unsigned long addr, size_t len);

页表

页表用于虚拟地址VA翻译成物理地址PA,该地址转换需要将虚拟地址分段,每段虚拟地址都作为一个索引指向页表,而页表项则指向下一级页表或指向最终的物理页面。早期的linux操作系统采用三级页表完成地址转换,利用多级页表能够节约地址转换所占用的内存。地址翻译过程如下,
页表翻译
顶级页表是页全局目录PGD,它是一个pgd_t类型的数组。PGD页表项中指向二级页目录中表项PMD。二级页表是中间页目录,是一个pmd_t类型的数组,其中的表项指向PTE中的表项。最后一级页表简称页表PTE,它是一个pte_t类型的数组,该页表项指向物理页面。在内存描述符中存储了页全局目录PGD的地址。
由于内存访问的局部性,对已经翻译过的物理页面缓存起来,就能加快访存速率,因为少了三次间接访存的开销,所以在多数体系结构中,利用硬件快表TLB(translate lookaside buffer)来实现虚拟到物理地址转换的缓存。每次地址翻译时,如果TLB命中,物理地址立刻返回,否则就从页表中查询,并填入TLB中。
后续在开一个帖子,来解析为什么要使用页表为什么要使用多级页表,以及当下linux的4级页表地址翻译过程

实际使用中的内存区域

可以使用/proc和pmap工具查看给定进程的内存空间和其中的内存区域VMA。内省
前三行分别对应C库中libc.so的代码段、数据段和bss段,接下来是动态链接程序ld.so的代码段、数据段和bss段。最后一行是该进程的用户栈。
该进程地址空间为1340KB,其中只有40KB是私有的。对于大多数程序都需要C库和动态链接库,所以将这部分内存映射到每个进程的内存区域,各个进程就可以共享。这使得1340KB的程序运行时只独占40KB的内存,大大节约了内存的开销。

总结

以一个图来总结全文的主要内容。文中主要分析进程描述符task_struct、内存描述符mm_struct、内存区域vm_area_struct、页表和进程代码段、数据段、堆栈段等的联系。
框架图
接着,来回答文初的提到的问题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值