8.进程地址空间

进程地址空间

内核除了管理自身的内存外,还必须管理用户空间中进程的内存,称该内存为进程地址空间,也就是系统中每个用户空间进程所看到的内存。Linux采用虚拟内存技术,所有进程之间以虚拟方式共享内存,对一个进程而言,好像都能访问整个系统的所有物理内存,甚至远远大于物理内存。

1.地址空间

进程地址空间由进程可寻址的虚拟内存组成,通常每个进程都有唯一的地址空间(连续的)。若两个进程的地址空间由相同的内存地址,又互不相干,那么这样的进程就是线程

对于一个32位机,进程可以寻址4GB的虚拟内存,但这并不代表它有权访问所有的虚拟地址,那些可被访问的合法地址空间称为内存区域(memory areas)。通过内核,进程可以给自己的地址空间动态添加或减少内存区域。

进程只能访问有效的内存区域内的内存地址,否则内核会终止该进程,并返回段错误(尼玛,初学C语言时经常报段错误啊!)

内存区域可以包含各种内存对象,比如:

  • 可执行文件代码的内存映射,称代码段(text section)
  • 可执行文件已初始化全局变量的内存映射,称数据段(data section)
  • 未初始化的全局变量,也就是bss段的零页内存映射(C标准中强制规定未初始化的全局变量赋予特殊默认值,0居多,避免目标文件中显式初始化,减少空间浪费)
  • 用于进程用户空间栈的零页内存映射
  • 每一个诸如C库或动态链接程序等共享库的代码段,数据段,bss都会载入进程的地址空间
  • 任何内存映射文件
  • 任何共享内存段
  • 任何匿名的内存映射,比如malloc()分配的内存

进程地址空间中的任何有效地址都只能位于唯一的区域,因此上述的内存对象都对应于一个独立的内存区域。

2.内存描述符

内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。内存描述符mm_struct结构体表示,定义在文件<linux/sched.h>(现在是在mm_type.h,前面文章如果找不到函数请查看一下头文件,一般能找到线索)中:

struct mm_struct {
        struct vm_area_struct  *mmap;               /* list of memory areas */
        struct rb_root         mm_rb;               /* red-black tree of VMAs */
        struct vm_area_struct  *mmap_cache;         /* last used memory area */
        unsigned long          free_area_cache;     /* 1st address space hole */
        pgd_t                  *pgd;                /* page global directory */
        atomic_t               mm_users;            /* address space users */
        atomic_t               mm_count;            /* primary usage counter */
        int                    map_count;           /* number of memory areas */
        struct rw_semaphore    mmap_sem;            /* memory area semaphore */
        spinlock_t             page_table_lock;     /* page table lock */
        struct list_head       mmlist;              /* list of all mm_structs */
        unsigned long          start_code;          /* start address of code */
        unsigned long          end_code;            /* final address of code */
        unsigned long          start_data;          /* start address of data */
        unsigned long          end_data;            /* final address of data */
        unsigned long          start_brk;           /* start address of heap */
        unsigned long          brk;                 /* final address of heap */
        unsigned long          start_stack;         /* start address of stack */
        unsigned long          arg_start;           /* start of arguments */
        unsigned long          arg_end;             /* end of arguments */
        unsigned long          env_start;           /* start of environment */
        unsigned long          env_end;             /* end of environment */
        unsigned long          rss;                 /* pages allocated */
        unsigned long          total_vm;            /* total number of pages */
        unsigned long          locked_vm;           /* number of locked pages */
        unsigned long          def_flags;           /* default access flags */
        unsigned long          cpu_vm_mask;         /* lazy TLB switch mask */
        unsigned long          swap_address;        /* last scanned address */
        unsigned               dumpable:1;          /* can this mm core dump? */
        int                    used_hugetlb;        /* used hugetlb pages? */
        mm_context_t           context;             /* arch-specific data */
        int                    core_waiters;        /* thread core dump waiters */
        struct completion      *core_startup_done;  /* core start completion */
        struct completion      core_done;           /* core end completion */
        rwlock_t               ioctx_list_lock;     /* AIO I/O list lock */
        struct kioctx          *ioctx_list;         /* AIO I/O list */
        struct kioctx          default_kioctx;      /* AIO default I/O context */
};
  • mm_users记录正在使用该地址的进程数目;
  • mm_count记录mm_struct的主引用次数,如果有9个线程共享某地址,则mm_user为9,mm_count为1。当内核在一个地址空间上操作,并需要使用与该地址相关联的引用计数时,内核便增加mm_count
  • mmapmm_rb描述的对象是相同的——该地址空间中全部的内存区域。但是前者以李阿尼奥形式存放,后者以红黑树存放。链表简单便于遍历和删除,红黑树适合检索
  • mmlist,所有的mm_struct结构体都通过自身的mmlist域连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表init进程的地址空间。另外需要注意,操作该链表的时候需要使用mmlist_lock锁来防止并发访问,该锁定义在文件<kernel/fork.c>中。

分配内存描述符

在进程的进程描述符中,mm域存放该进程使用的内存描述符,所以current->mm便指向当前进程的内存描述符。fork()函数利用copy_mm()函数复制父进程的内存描述符,也就是current->mm域赋给子进程,而子进程中mm_struct结构实际通过文件kernel/fork.c中的allocate_mm()宏从mm_cachep的slab缓存中分配得到。通常每个进程有唯一的mm_struct结构体,即唯一的进程地址空间,但是也有列外的,比如线程(linux中线程就是进程,只是与父进程共享地址空间)。

撤销内存描述符

进程退出时,内核会调用定义在kernel/exit.c中的exit_mm()函数,该函数执行一些常规的撤销工作,同时更新一些统计量(前面讲的mm_usersmm_count)。如果该内存描述符没有任何使用者了,那么调用free_mm()宏通过kmem_cache_free()函数将mm_struct结构体归还到mm_cachep的slab缓存中。

mm_struct与内核线程

内核线程没有进程地址空间,也没有相关的内存描述符,所以内核线程对应的进程描述符中mm域为空。事实上,这也正是内核线程的真实含义——没有用户上下文。

省了进程地址空间再好不过,因为内核线程并不需要访问任何用户空间的内存,而且因为内核线程在用户空间中没有任何页,所以实际上他们并不需要有自己的内存描述符和页表。但是,访问内核内存,内核线程还是需要一些数据的,比如页表。为了避免内核线程为内存描述符和页表浪费内存,同时避免浪费处理器周期向新地址空间进行切换,内核线程将直接使用前一个进程的内存描述符。

当一个进程被调度时,该进程的mm域指向的地址空间被装载到内存,进程描述符中的active_mm域会被更新,指向新的地址空间。内核线程没有地址空间,所以mm域为NULL。当一个内核 线程被调度时,内核发现它的mm域为NULL,就会保留前一进程的地址空间,随后内核更新内核线程对应的进程描述符中的active_mm域,使其指向前一个进程的内存描述符。所以需要时,内核线程便可以使用前一个进程的页表。因为内核线程不访问用户空间的内存,所以它们仅仅使用地址空间中和内核内存相关的信息,这些信息的含义和普通进程完全相同。

3.虚拟内存区域

内存区域由vm_area_struct结构体描述,定义在文件<linux/mm_type.h>中。内群区域在Linux中也常被称为虚拟内存区域(virtual memory Areas, VMAs)。

vm_area_struct结构体描述了指定地址空间内连续区间上的一个独立内存范围。内核将每个内核区域作为一个单独的内存对象管理。每一个VMA代表不同类型的内存区域(比如内存映射文件或进程用户空间栈):

struct vm_area_struct {
        struct mm_struct             *vm_mm;        /* associated mm_struct */
        unsigned long                vm_start;      /* VMA start, inclusive */
        unsigned long                vm_end;        /* VMA end , exclusive */
        struct vm_area_struct        *vm_next;      /* list of VMA's */
        pgprot_t                     vm_page_prot;  /* access permissions */
        unsigned long                vm_flags;      /* flags */
        struct rb_node               vm_rb;         /* VMA's node in the tree */
        union {         /* links to address_space->i_mmap or i_mmap_nonlinear */
                struct {
                        struct list_head        list;
                        void                    *parent;
                        struct vm_area_struct   *head;
                } vm_set;
                struct prio_tree_node prio_tree_node;
        } shared;
        struct list_head             anon_vma_node;     /* anon_vma entry */
        struct anon_vma              *anon_vma;         /* anonymous VMA object */
        struct vm_operations_struct  *vm_ops;           /* associated ops */
        unsigned long                vm_pgoff;          /* offset within file */
        struct file                  *vm_file;          /* mapped file, if any */
        void                         *vm_private_data;  /* private data */
};

和内存管理中对象一样,VMA对象同样定义了操作函数,由vm_operations_struct结构体表示,定义在文件<linux/mm.h>

 struct vm_operations_struct {
     void (*open)(struct vm_area_struct * area);
     void (*close)(struct vm_area_struct * area);
     int (*mremap)(struct vm_area_struct * area);
     int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
     int (*pmd_fault)(struct vm_area_struct *, unsigned long address,
                         pmd_t *, unsigned int flags);
     void (*map_pages)(struct fault_env *fe,
             pgoff_t start_pgoff, pgoff_t end_pgoff);

     /* 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);

     /* same as page_mkwrite when using VM_PFNMAP|VM_MIXEDMAP */
     int (*pfn_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);
};
  • open:当制定内存区域被加入到一个地址空间时,该函数被调用
  • close:当制定的内存区域从地址空间删除时,该函数被调用
  • fault:当没有出现在物理内存中的页面被访问时,该函数被页面故障处理调用
  • page_mkwrite:当某个页面为只读页面时,该函数被页面故障处理调用
  • access:当get_user_pages()函数调用失败时,该函数被access_process_vm()调用

4.操作内存区域

内核时常需要在某个内核区域上执行一些操作,比如某个制定地址是否包含在某个内存区域中。这类操作非常频繁。内核定义了许多辅助函数,都声明在<linux/mm.h>中

find_vma()

用于找到一个给定的内存地址属于哪一个内存区域提供的函数,定义在<mm/mmap.c>中:

struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr)

该函数在指定的地址空间中搜索第一个vm_end大于addr的内存区域,如果没发现这样的区域,该函数返回NULL;否则返回指向该区域的vm_area_struct结构体指针。注意,由于返回的VMA首地址可能大于addr,所以指定的地址不一定就包含在返回的VMA中,因为很可能在对某个VMA执行操作后,还有更多操作会对该VMA接着操作,所以find_vma()函数返回的结果被缓存在内存描述符的mmap_cache域中。检查缓存的VMA如果指定地址不在缓存中,那么必须搜索和内存描述符相关的所有内存区域(通过红黑树进行)

find_vma_prev()和find_vma_intersection()

find_vma_prev和find_vma()工作方式相同,但是返回第一个小于addr的VMA。find_vma_intersection()函数返回第一个和指定地址区间向交的VMA。这两个函数均是定义在<linux/mm.h>中。

5.创建地址区间

内核使用do_mmap()函数创建一个新的线性地址区间。如果创建的区间和已经存在的区间相邻,且权限相同,则合并两个区间。如果不能合并就创建一个新的VMA。最终do_mmap()函数会将地址区间加入到进程的地址区间,do_mmap()函数定义在文件<linux/mm.h>

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

该函数映射有file指定的文件,具体映射的是文件中偏移offset处开始,长度为len字节的范围内的数据。

在用户空间可以通过mmap()系统调用获取内核函数do_mmap()的功能.

6.删除地址区间

do_mummap()函数从特定的进程地址空间删除制定地址区间,该函数定义在文件<linux/mm.h>中:

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

系统调用mummap()给用户空间程序提供了一种从自身地址空间中删除制定地址区间的方法,他和系统调用mmap()作用相反。

7.页表

虽然应用程序操作的对象是映射到物理内存上的虚拟地址,但是处理器直接操作的却是物理内存。所以应用程序访问一个虚拟地址时,首先必须将虚拟地址转化为物理地址,然后处理器才能解析地址访问请求。地址转换工作需要通过查询页表才能完成,概括地讲,地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表则指向一级别的页表或者指向最终的物理页面(如果学过网络的同学,这个页表我们就可以看做网络中的网络号,我们先通过网络号找到子网,在子网中继续找网络号或者IP地址,或者把页表当做树查询)。

一个逻辑地址(32位系统,页大小 4K) 可以被分为 :一个20位的页号 +一个12位的偏移。如果对页表进行再分页,那么页号分解为:一个10位的页号 +一个10位的偏移。

Linux中使用三级页表完成地址转换,多级页表能够节约地址转换所需占用的存放空间。

如果每次对虚拟内存中的页面访问都必须解析它,从而获得物理内存中对应的地址,需要消耗一定的时间。因此多数体系结构实现了一个翻译后缓冲器(translate lookaside buffer,TLB)。TLB作为一个将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器首先检查TLB中是否缓存了虚拟地址到物理地址的映射,没查找到的前提下再通过页表搜索。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容