Linux进程地址空间详解

描述

RAM 的某些部分永久地分配给内核, 并用来存放内核代码以及静态内核数据结构. RAM 的其余部分称为动态内存 (dynamic memory). 动态内存不仅是进程所需的宝贵资源, 也是内核本身所需的宝贵资源. 实际上,整个系统的性能取决于如何有效地管理动态内存. 因此, 现在所有多任务操作系统都在尽力优化对动态内存的使用, 也就是说, 尽可能做到当需要时分配, 不需要时释放.

Linux

当给内核分配动态内存时, 是相对容易的, 有如下两点原因:

内核是操作系统中优先级最高的成分. 如果某个内核函数请求动态内存, 那么, 必定有正当的理由发出这个请求, 因此, 没有道理试图推迟这个请求.

内核信任自己. 所有的内核函数都被假定是没有错误的, 因此内核函数不必针对程序错误施加任何保护措施.

而当给用户态进程分配内存时, 情况完全不同:

进程对动态内存的请求被认为是不紧急的. 例如, 当进程对应在磁盘上所存储的可执行文件被装入内存时, 进程并不一定会立即对所有的代码和数据进行访问. 类似地, 当进程调用 malloc() 以请求获得额外的动态内存时, 也并不意味着进程很快就会访问所获得的额外的动态内存. 因此, 一般来说, 内核总是尽量推迟给用户态进程分配动态内存.

由于用户进程是不可信任的, 因此, 内核必须能随时准备捕获用户态进程引起的所有寻址错误.

为了使得动态内存得到最大限度的使用, 内核使用一种新的资源成功实现了对进程动态内存的推迟分配. 当用户态进程请求动态内存时, 并没有获得请求的动态内存, 而仅仅得到了对一个新的线性地址区间的使用权, 这样的线性地址区间有很多, 由允许进程使用的全部线性地址区间所组成的集合就叫做进程地址空间.

与进程地址空间有关的全部信息都包含在一个叫做内存描述符的数据结构中 (实际上就是描述进程虚拟内存的数据结构), 这个结构的类型为 mm_struct, 进程描述符的 mm 字段就指向这个结构.

 


struct mm_struct *mm;

如下为Linux 2.6.11版的内核中mm_struct的实现.

struct mm_struct {
  struct vm_area_struct * mmap;    
  struct rb_root mm_rb;
  struct vm_area_struct * mmap_cache;  
  unsigned long (*get_unmapped_area) (struct file *filp,
        unsigned long addr, unsigned long len,
        unsigned long pgoff, unsigned long flags);
  void (*unmap_area) (struct vm_area_struct *area);
  unsigned long mmap_base;    
  unsigned long free_area_cache;    
  pgd_t * pgd;
  atomic_t mm_users;      
  atomic_t mm_count;      
  int map_count;        
  struct rw_semaphore mmap_sem;
  spinlock_t page_table_lock;    


  struct list_head mmlist;    
             * together off init_mm.mmlist, and are protected
             * by mmlist_lock
             */


  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 rss, anon_rss, total_vm, locked_vm, shared_vm;
  unsigned long exec_vm, stack_vm, reserved_vm, def_flags, nr_ptes;


  unsigned long saved_auxv[42]; 


  unsigned dumpable:1;
  cpumask_t cpu_vm_mask;


  
  mm_context_t context;


  
  unsigned long swap_token_time;
  char recent_pagein;


  
  int core_waiters;
  struct completion *core_startup_done, core_done;


  
  rwlock_t    ioctx_list_lock;
  struct kioctx    *ioctx_list;


  struct kioctx    default_kioctx;


  unsigned long hiwater_rss;  
  unsigned long hiwater_vm;  
};

 

其中用来标识相应进程特定线性区的字段如下:

Linux

start_code, end_code

正文代码的起始地址和终止地址.

start_data, end_data

已初始化数据的起始地址和终止地址.

start brk, brk

堆的起始地址和当前终止地址.

start_stack

用户态堆栈的起始地址.

arg_start, arg_end

命令行参数的起始地址和终止地址.

env_start, env_end

环境变量的起始地址和终止地址.

如下为进程地址空间的布局, 由一个一个的线性地址区间组成, 线性地址 (linear address), 也称虚拟地址 (virtual address) 是一个 32 位无符号整数 (unsigned long), 可以用来表示数值高达 4GB 的地址, 也就是 4,294,967,296 个内存单元. 线性地址通常用十六进制数字表示, 值的范围从 0x00000000 到 0xffffffff.

Linux

0x00000000 ~ 0xbfffffff 这一线性地址区间被称为用户空间, 大小为 3GB; 而0xc0000000 ~ 0xffffffff 这一线性地址区间被称为内核空间, 大小为 1GB.

可以通过以下代码对进程地址空间的布局图进行验证.

 


#include 
#include 


int uninitialized_global_var;
int initialized_global_var = 100;


int main(int argc, char *argv[], char *envp[])
{
    printf("Code address:%p
", main);  
    printf("Initialized Data address:%p
", &initialized_global_var);  
    printf("Uninitialized Data address:%p
", &uninitialized_global_var);  
    int *p = (int*)malloc(sizeof(int));
    printf("Heap address:%p
", p);  
    printf("Stack address:%p
", &p);  


    for (int i = 0; i < argc; i++) {
        printf("Command-line Arguments address:%p
", argv[i]);  
    }
    
    for (int i = 0; envp[i]; i++) {
        printf("Environment Variables address:%p
", envp[i]);  
    }
    return 0;
}

 

运行结果如下, 与进程地址空间的布局相吻合.

Linux

线性地址(虚拟地址)的集合称为虚拟内存, 物理地址的集合称为物理内存, 进程对于内存访问的终点是物理内存而不是虚拟内存, 所以必然存在一种将虚拟内存转化为物理内存的结构, 这种结构被称为页表.

页 (Page) && 页帧 (Page Frame)

内核使用 struct page 作为基本单位来管理物理内存, 在内核看来, 所有的 RAM 都被划分成了固定长度的页帧 (页帧也叫页框, 通常大小为4KB). 每一个页帧包含了一个页, 也就是说一个页帧的长度和一个页的长度相同. 页和页帧的区别在于, 页是抽象的数据结构, 可以存放在任意地方, 而页帧是真实的存储区域, 属于主存的一部分.

如下为Linux 2.6.11版的内核中struct page的实现.

 


struct page {
  page_flags_t flags;    
           * updated asynchronously */
  atomic_t _count;    
  atomic_t _mapcount;    
           * to show when page is mapped
           * & limit reverse map searches.
           */
  unsigned long private;    
           * usually used for buffer_heads
           * if PagePrivate set; used for
           * swp_entry_t if PageSwapCache
           * When page is free, this indicates
           * order in the buddy system.
           */
  struct address_space *mapping;  
           * inode address_space, or NULL.
           * If page mapped as anonymous
           * memory, low bit is set, and
           * it points to anon_vma object:
           * see PAGE_MAPPING_ANON below.
           */
  pgoff_t index;      
  struct list_head lru;    
           * protected by zone->lru_lock !
           */
  
   * On machines where all RAM is mapped into kernel address space,
   * we can simply calculate the virtual address. On machines with
   * highmem some memory is mapped into kernel virtual memory
   * dynamically, so we need a place to store that address.
   * Note that this field could be 16 bits on x86 ... ;)
   *
   * Architectures with slow multiplication can define
   * WANT_PAGE_VIRTUAL in asm/page.h
   */
#if defined(WANT_PAGE_VIRTUAL)
  void *virtual;      
             not kmapped, ie. highmem) */
#endif 
};

 

CPU 管理物理地址, 因而虚拟地址需要转化为物理地址才能给 CPU 使用. 用于将进程(虚拟)地址空间映射成物理地址空间的数据结构称为页表.

Linux

进程地址空间, 页表的存在有什么意义?

让所有进程以统一的视角看待内存, 进程地址空间的存在让我们在编写程序的时候只需关注虚拟地址, 而无需关注数据在物理内存当中实际的存储位置.

页表的存在让进程在间接访问内存的时候, 增加一个转换的过程, 在这个转换的过程中, 内核对进程的寻址请求进行检查, 如果该进程的寻址请求异常, 则该请求被操作系统拦截, 从而实现对物理内存的保护.

进程地址空间与页表的存在, 让内核对于进程管理模块与内存管理模块进行了解耦.

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分