分享

关于Linux内存管理的一些资料 .

 wanwanstudy 2012-03-08

Linux常用内核态内存分配方式总结

一、             alloc_pages类

此类函数主要包括:

struct page * alloc_page(unsigned int gfp_mask)——分配一页物理内存并返回该页物理内存的page结构指针。

struct page * alloc_pages(unsigned int gfp_mask, unsigned int order)——分配 个连续的物理页并返回分配的第一个物理页的page结构指针。

unsigned long get_free_page(unsigned int gfp_mask)——分配一页物理内存并将该物理页全部清零,最后返回一个虚拟(线形)地址。

unsigned long __get_free_page(unsigned int gfp_mask)——Allocates a single page and returns a virtual address。

Unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order)——Allocates 2order number of pages and returns a virtual address。

struct page * __get_dma_pages(unsigned int gfp_mask, unsigned int order)——Allocates 2order number of pages from the DMA zone and returns a struct page。

此类函数主要通过伙伴分配系统进行分配,它们是linux内核最基本的内存分配函数,一次请求能分配的最大物理页数由变量MAX_ORDER决定。

相应的内存释放函数如下:      

void __free_pages(struct page *page, unsigned int order)
Frees an order number of pages from the given page.
 
void __free_page(struct page *page)
Frees a single page.
 
void free_page(void *addr)
Frees a page from the given virtual address.
 

 

二、             kmalloc

这个函数建立在slab分配器之上,主要用于分配范围在 字节— 字节大小以内的小内存区域。并且此函数分配的内存在线形地址和物理地址上都是连续的,它不能分配到所谓的高端内存区域内的内存,高端内存区域内的内存必须由专门的方式来获得。

由于使用伙伴分配系统分配小块内存会带来太多的碎片,因此linux保留了2列cache专门用于小块内存分配。这两列cache一列专门用于DMA分配,起名为size-N(DMA) cache,另一列实用于通用的内存分配,起名为size-N cache,其中N为对应cache的大小。对于每个大小为cs_size的cache linux是采用以下结构体来描述的:

       typedef struct cache_sizes {

              size_t cs_size;

               kmem_cache_t *cs_cachep;               kmem_cache_t *cs_dmacachep;           }cache_sizes_t;由以上结构体可知:linux是以大小来同时描述size-N(DMA) cache 和size-N cache的。

由于这些cache的个数是有限的,linux在编译阶段就初始化了一个称为cache_size的静态数组来描述这些cache,该数组定义大致如下:

        static cache_sizes_t cache_sizes[] = {         #if PAGE_SIZE == 4096                 {    32, NULL, NULL},         #endif                 {    64, NULL, NULL},                 {   128, NULL, NULL},                 {   256, NULL, NULL},                 {   512, NULL, NULL},                 {  1024, NULL, NULL},                 {  2048, NULL, NULL},                 {  4096, NULL, NULL},                 {  8192, NULL, NULL},                 { 16384, NULL, NULL},                 { 32768, NULL, NULL},                 { 65536, NULL, NULL},                 {131072, NULL, NULL},                 {     0, NULL, NULL}         }由该数组可知,linux在编译时已建立好对各个cache的描述,接下来的工作就是在系统启动时分配好cache并将对应的各个cache指针写入该数组中即可。

有了以上数组,kmalloc的工作就变得简单了:根据用户提供的内存分配大小查找该数组,若找到合适大小cache,则调用slab分配器接口分配所需内存,若找不到合适大小,则返回NULL。

对应与kmalloc的内存释放函数为kfree:它释放内存时首先确保所释放的内存指针不为NULL,然后在确保指针指向的内存区域在slab内,最后才是调用slab分配器的接口回收该指针指向的内存区域。

三、             vmalloc

与kmalloc不同的时,vmalloc分配的内存只是在线形地址上是连续的,它不保证分配的内存在物理上也连续。vmalloc的主要目的是用于非连续物理内存分配。

采用vmalloc的主要原因在于:由于伙伴分配系统会产生外碎片,我们很难找到大块的在物理上连续的物理内存,因此linux采用vmalloc来解决这个问题。

原理:linux在内核虚拟地址空间中保留了从VMALLOC_START到VMALLOC_END之间的区域用于vmalloc分配内存,在vmalloc分配内存时,最终还是要调用伙伴分配器的接口来一页一页地(因此不要求页与页之间连续)分配物理内存,当分配到物理内存后再通过修改页表来使连续的虚拟地址空间对应到不连续的物理地址空间。由此我们知道vmalloc分配的物理内存都是物理页的整数倍的,另外由于为vmalloc保留的线形地址空间是有限的,因此我们能够通过vmalloc获得物理内存也是有限的。

通过vmalloc分配出去的线形空间是通过vm_struct结构体来描述的,它的定义如下:

        struct vm_struct {                unsigned long flags;                void * addr;                unsigned long size;                struct vm_struct * next;        };       已分配出去的线形地址空间通过结构体的next域连接起来,它们按地址排序,当要分配一块新的地址空间时可通过查找这个连表并结合保留给vmalloc的线形空间来获得新的可用区域信息。这个结构体本身所需的空间是通过kmalloc获得的。

       与vmalloc对应的内存释放函数名为vfree。

 

接下来是2个只用于高端内存分配的函数

四、             kmap

在内核线形地址空间顶部linux保留了一段区域用于kmap函数映射高端内存,这个地址空间范围为从PKMAP_BASE到FIXADDR_START。

以下摘自《深入linux虚拟内存管理》:

Space is reserved at the top of the kernel page tables from PKMAP_BASE to FIXADDR_START for a PKMap. The size of the space reserved varies slightly. On the x86, PKMAP_BASE is at 0xFE000000, and the address of FIXADDR_START is a compile time constant that varies with configure options, but that is typically only a few pages located near the end of the linear address space. This means that there is slightly below 32MiB of page table space for mapping pages from high memory into usable space.

For mapping pages, a single page set of PTEs is stored at the beginning of the PKMap area to allow 1,024 high pages to be mapped into low memory for short periods with the function kmap() and to be unmapped with kunmap(). The pool seems very small, but the page is only mapped by kmap() for a very short time. Comments in the code indicate that there was a plan to allocate contiguous page table entries to expand this area, but it has remained just that, comments in the code, so a large portion of the PKMap is unused.

The page table entry for use with kmap() is called pkmap_page_table, which is located at PKMAP_BASE and which is set up during system initialization. On the x86, this takes place at the end of the pagetable_init() function. The pages for the PGD and PMD entries are allocated by the boot memory allocator to ensure they exist.

The current state of the page table entries is managed by a simple array called pkmap_count, which has LAST_PKMAP entries in it. On an x86 system without PAE, this is 1,024, and, with PAE, it is 512. More accurately, albeit not expressed in code, the LAST_PKMAP variable is equivalent to PTRS_PER_PTE.

Each element is not exactly a reference count, but it is very close. If the entry is 0, the page is free and has not been used since the last TLB flush. If it is 1, the slot is unused, but a page is still mapped there waiting for a TLB flush. Flushes are delayed until every slot has been used at least once because a global flush is required for all CPUs when the global page tables are modified and is extremely expensive. Any higher value is a reference count of n-1 users of the page.

五、             kmap_atomic

The use of kmap_atomic() is discouraged, but slots are reserved for each CPU for when they are necessary, such as when bounce buffers are used by devices from interrupt. There are a varying number of different requirements an architecture has for atomic high memory mapping, which are enumerated by km_type. The total number of uses is KM_TYPE_NR. On the x86, there are a total of six different uses for atomic kmaps.

KM_TYPE_NR entries per processor are reserved at boot time for atomic mapping at the location FIX_KMAP_BEGIN and ending at FIX_KMAP_END. Obviously, a user of an atomic kmap may not sleep or exit before calling kunmap_atomic() because the next process on the processor may try to use the same entry and fail.

The function kmap_atomic() has the very simple task of mapping the requested page to the slot set aside in the page tables for the requested type of operation and processor. The function kunmap_atomic() is interesting because it will only clear the PTE with pte_clear() if debugging is enabled. It is considered unnecessary to bother unmapping atomic pages because the next call to kmap_atomic() will simply replace it and make TLB flushes unnecessary.

 

linux内核地址空间与用户地址空间的差别

本文仅限在i386平台下讨论一般情况。

1、用户线性地址空间范围0-3G,内核线性空间范围3G-4G。
2、内核总是立即满足内核空间的物理内存分配,并且分配结果对所有进程可见;而对于用户空间的内存分配请求,linux总是先保留用户线性地址空间的一段区域,然后修改页表项使这段线性区域都指向一页内容全为0的全局只读物理页。当进程写入这段线性区域时,将会产生一个缺页异常,这时系统才会为对应的线性地址分配物理页面,并且把物理页对应的页表项置为可写。为了保持和全局只读物理页内容一致,新分配的物理页内容也会全置为0。
3、用户线性空间是不可靠的,一般情况下它会随着进程的切换而改变(在lazy TLB切换下除外);而内核线性空间对所有的进程都一样,不会随着进程的切换而改变。


Linux系统下内存申请应用的方法原理总结

1. 地址空间的管理

  物理地址都是有内核管理的, node-->zone-->mem_map-->page, 所有的物理页面都在mem_map数组中的页帧对应, 然后不同的page有分为DMA,normal,highmem三个zone。

  内核线性地址空间, 实际上只是低端内存才有线性地址,0---896MB部分。

  内核虚拟地址, 低端内存的虚拟地址与线性地址是一样的。 高端内存只有在映射了以后才有虚拟地址

  用户空间地址, tast_struct ---> mmap --> mm_struct ---> vm_area_struct

  2.内存的申请或使用

  物理内存的分配, 在内核中最终都要调用__alloc_pages().它是最核心的分配函数,申请大小最大不超过2的MAX_ORDER次幂,在现在好像最大定义为4MB。

  线性地址, kmalloc和get_free_pages,线性地址, 对应的物理内存就是低端内存,kmalloc是基于slab的分配技术, 最大不能超过128KB。

  虚拟地址, vmalloc申请, 他只是在内核中建立类似与用户空间的vm_area的一个虚拟内存空间到vmlist中, 最终的物理内存分配还是基于缺页的。

  用户空间的虚拟内存, malloc之类的, 最终在内核中都是do_map()和do_brk()。实际上也只是建立了一块虚拟空间,最终的物理内存还是在缺页异常时分配的。

  3. 内存的交换问题

  在page结构和用户层的vm_area_struct结构中, 都包含locked和reserved标志。通过合适的途径设置这些标志, 可以是页面锁存在物理内存中, 不被交换出去。

  4. 设备内存可以通过ioremap映射到内核虚拟地址空间, 也可以通过mmap方法映射到用户空间。

Linux 内存管理:缺页异常的几种原因

给定一个线性地址,MMU 通过页目录表、页表的转换,找到对应的物理地址。在这个过程中,如果因某种原因导致无法访问到最终的物理内存单元,CPU 会产生一次缺页异常,从而进入缺页异常处理程序。

总结一下,缺页异常的原因有以下几种:

1、导致缺页异常的线性地址根本不在进程的“虚存区间”中,段错误。(栈扩展是一种例外情况)

2、地址在“虚存区间”中,但“虚存区间”的访问权限不够;例如“区间”是只读的,而你想写,段错误

3、权限也够了,但是映射关系没建立;先建立映射关系再说

4、映射关系也建立了,但是页面不在内存中。肯定是换出到交换分区中了,换进来再说

5、页面也在内存中。但页面的访问权限不够。例如页面是只读的,而你想写。这通常就是 “写时拷贝COW” 的情况。

6、缺页异常发生在“内核动态映射空间”。这是由于进程进入内核后,访问一个通过 vmalloc() 获得线性地址而引起的异常。对这种情况,需要将内核页目录表、页表中对应的映射关系拷贝到进程的页目录表和页表中。

Linux虚拟内存组织结构浅析

众所周知,linux内核支持绝大多数体系结构,因此linux内核必须采取一种与具体体系结构无关的方法来描述物理内存的组织结构,这个问题就是本系列文章要讨论的话题。

 

要理解linux虚拟内存在逻辑上的组织结构,我们首先要明白两个概念:UMA(Uniform Memory Access)、NUMA(Non Uniform Memory Access)。UMA指一致性内存访问,这是单CPU机器常用的体系结构,在这种结构下,CPU访问系统内存的任何存储位置的代价都是一样的;而NUMA是指非一致性内存访问,常用于多CPU机器,在这种体系结构中,不同内存相对于不同的CPU而言所处的位置不一样,最典型的就是每个CPU都有自己的本地内存(Local Memory),不同CPU之间通过总线连接起来,如图1所示。在这种结构中,CPU访问本地内存的代价比访问远端的内存代价要小。

 

 

为了支持NUMA,Linux将物理内存划分成不同的节点(node),节点用结构体pg_data_t表示,以上图为例,图中每个CPU的本地物理内存都称为一个节点;即使在UMA结构中也有节点的概念,此时系统中就只有一个节点;系统中的多个节点被连接起来保存在一个称为pgdat_list的链表上。每个节点又被划分成不同的区(zone),节点是通过其结构体内的数组node_zones来跟踪节点内的区的。区是指一个节点内一段连续的物理内存范围。Linux中主要有3个区:ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM,它们的划分如下:ZONE_DMA位于物理内存开始的一段区域内,主要用来供一些ISA设备使用;ZONE_NORMAL位于ZONE_DMA后面,这个区域被内核直接映射到线性地址的高端部分;ZONE_HIGHMEM指系统中剩下的物理内存,这个区域不能直接被内核映射。为了清晰起见,我们可以看看在x86平台上的区是如何划分的:

                     ZONE_HIGHMEM —— 物理内存起始16M

                     ZONE_NORMAL —— 16M—896M

                     ZONE_HIGHMEM —— 896M—物理内存结束

 

  最后,在每个区中都有一个指向mem_map数组中某个单元的指针zone_mem_map,这是干什么的呢?通过查看源代码我们可以知道,mem_map是类型为struct page的数组,而Linux内核正是利用struct page结构体来描述每个物理内存页的,在系统启动时,内核就会为整个系统的内存建立好一个全局的页描述数组mem_map,在以后的运行过程中,Linux内核最终就是根据这个全局内存描述数组来控制对物理内存的分配、回收等操作的。明白了mem_map的作用,我们再来看看每个区中的zone_mem_map指针,这个指针指向的是mem_map中的某一个单元,而这个单元的内容恰恰描述了这个区内的第一页物理内存,这样Linux就把节点、区、page结构联系起来了,它们共同组织起来完成了对系统所有物理内存的描述。因此,我们现在就可以清晰地得出Linux在逻辑上是如何描述所有的物理内存了,


Linux虚拟内存组织结构浅析(二)

在前一篇文章中我们介绍了Linux虚拟内存在逻辑上的组织结构,现在就让我们从源代码入手,从程序级仔细看看各个数据结构体的内部组成如何,源代码来自于最新的kernel2.6.26.5,分析过程中主要参考了《Understanding the linux virtual memory》这本书,有兴趣的朋友可以去阅读一下。

 

一、节点的数据表示

在内核中,节点由结构体pg_data_t来表示,它是由结构体pglist_data通过typedef来定义的,位于文件<linux/mmzone.h>,其内容如下:

typedef struct pglist_data {

   struct zone node_zones[MAX_NR_ZONES];

   struct zonelist node_zonelists[MAX_ZONELISTS];

   int nr_zones;

#ifdef CONFIG_FLAT_NODE_MEM_MAP

   struct page *node_mem_map;

#endif

   struct bootmem_data *bdata;

#ifdef CONFIG_MEMORY_HOTPLUG

   spinlock_t node_size_lock;

#endif

   unsigned long node_start_pfn;

   unsigned long node_present_pages; /* total number of physical pages */

   unsigned long node_spanned_pages; /* total size of physical page

                        range, including holes */

   int node_id;

   wait_queue_head_t kswapd_wait;

   struct task_struct *kswapd;

   int kswapd_max_order;

} pg_data_t;

 

各字段含义如下:

node_zones:包含本节点内区的描述结构体,一般而言MAX_NR_ZONES为3。

node_zonelists:这个数组指明了在分配内存时区的选择顺序。

nr_zones:指明本节点内区的个数,这一般都是1到3中的一个数字。并不是所有的节点都有3个区,比如有些节点就没有ZONE_DMA区。

node_mem_map:指向全局内存描述数组中的某一项,该元素描述了本节点的第一页物理内存。

bdata:指向bootmem_data结构体,该结构体用于系统启动时的内存分配器分配、管理该节点的内存。该内存分配器在系统启动完毕后不再使用。

node_size_lock:该自旋锁用于保护node_start_pfn、node_present_pages以及node_spanned_pages,当你要取得这些变量的值时,必须首先获取这个自旋锁。当你调用pfn_valid()前也应当获得这个自旋锁。并且注意必须在zone->lock和zone->size_seqlock前获取这个自旋锁。

node_start_pfn:这个是该节点内第一页物理内存在全局数组mem_map中的序号。

node_present_pages:当前该节点内可获得的物理页总数。

node_spanned_pages:节点内可访问的所有物理页数,包含可能存在的空洞页。

node_id:当前节点的编号,从0开始。

kswapd_wait:kswapd线程的等待队列。kswapd是一个内核线程,当系统中内存页较少时它负责回收物理内存页,这个线程常常处于睡眠状态。

kswapd:指向kswapd线程的进程描述符。

kswapd_max_order:kswapd线程要释放内存块大小取对数后的值。

 

二、区的数据表示

每个区由结构体struct zone来描述,其中包含了页使用统计、空闲页数、琐等信息,这个结构体在<linux/mmzone.h>中定义:

struct zone {

   unsigned long       pages_min, pages_low, pages_high;

   unsigned long       lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA

   int node;

   unsigned long       min_unmapped_pages;

   unsigned long       min_slab_pages;

   struct per_cpu_pageset  *pageset[NR_CPUS];

#else

   struct per_cpu_pageset  pageset[NR_CPUS];

#endif

   spinlock_t      lock;

#ifdef CONFIG_MEMORY_HOTPLUG

   seqlock_t       span_seqlock;

#endif

   struct free_area    free_area[MAX_ORDER];

 

#ifndef CONFIG_SPARSEMEM

   unsigned long       *pageblock_flags;

#endif /* CONFIG_SPARSEMEM */

 

   spinlock_t      lru_lock;  

   struct list_head    active_list;

   struct list_head    inactive_list;

   unsigned long       nr_scan_active;

   unsigned long       nr_scan_inactive;

   unsigned long       pages_scanned;     /* since last reclaim */

   unsigned long       flags;

   atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];

   int prev_priority;

   wait_queue_head_t   * wait_table;

   unsigned long       wait_table_hash_nr_entries;

   unsigned long       wait_table_bits;

   struct pglist_data  *zone_pgdat;

   unsigned long       zone_start_pfn;

   unsigned long       spanned_pages; 

   unsigned long       present_pages; 

   const char      *name;

} ____cacheline_internodealigned_in_smp;

 

各字段含义如下:

pages_min:管理区中保留的页数,当区中空闲的页框数达到这个值时kswapd线程会被唤醒并以同步的方式回收区中的页框。

pages_low:回收页框的下限,当区中空闲的页框数达到这个值时,伙伴分配器会唤起kswapd线程开始回收页框。

pages_high:回收页框的上限,当kswapd线程被唤醒回收页框时,它会一直回收页框直到区中空闲的页框数达到这个值为止。

lowmem_reserve:指明在靠近内存低端的区内需要保留的页框数,这个主要是为了防止这样一种情况:当内存低端的页框全部被分配后,如果这时候有新的页分配请求就可能会产生OOM,即使此刻在高端内存我们还有大量空闲的页框。

node:指明这个区所属的节点编号。

pageset:用于实现每cpu高速缓存的数组,每个cpu在该数组内占有一项。由于内核经常请求和释放单个页框,因此每cpu高速缓存包含一些预先分配的页框,它们用于满足本地cpu发出的单一内存请求。

lock:保护该描述符的自旋锁。

span_seqlock:保护zone_start_pfn、spanned_pages和present_pages的顺序锁,之所以使用顺序锁来保护这3个变量是因为我们经常需要在没有获得zone->lock的情况下读取它们的值,同时我们很少更改这些值。

free_area:标识出区中空闲页框快,这个结构是伙伴分配器的主要数据结构,伙伴分配器就是根据该结构体来对区中内存进行分配、回收等操作的。

pageblock_flags:一个内存块的标志,该内存块大小为pageblock_nr_pages。

lru_lock:活动及非活动页链表使用的自旋锁,主要在页框回收时使用。

active_list:区的活动页链表。

inactive_list:区的非活动页链表。

nr_scan_active:回收页框要扫描的活动页数目。

nr_scan_inactive:回收页框要扫描的非活动页数目。

pages_scanned:页框回收时使用的计数器。

flags:区的标志,目前有三个标志可以设置:

ZONE_ALL_UNRECLAIMABLE——区中所有页框被锁定不能回收

ZONE_RECLAIM_LOCKED——不能并发回收区中页框

ZONE_OOM_LOCKED——区处于OOM的区链表中

vm_stat:包含区的统计信息。

prev_priority:区扫描优先级,主要用于页框回收过程中。

wait_table:进程等待队列的hash表,表中的进程在等待区中的某一页内存。

wait_table_hash_nr_entries:等待队列hash表数组的大小。

wait_table_bits:等待队列hash表大小,1<<wait_table_bits。

zone_pgdat:指向区所属节点的指针。

zone_start_pfn:区中第一个页框在mem_map数组中的编号。

spanned_pages:区中的页框数,包含空洞。

present_pages:区中包含的页框数,不包含空洞。

name:指向区的名称,这个域很少使用。

 

三、页的数据表示

这个结构体是描述物理内存的最小单位,它描述了每个物理页框的属性,定义于文件<linux/mm_types.h>:

struct page {

   unsigned long flags;       

   atomic_t _count;   

   union {

       atomic_t _mapcount;

       struct {       

           u16 inuse;

           u16 objects;

       };

   };

   union {

       struct {

       unsigned long private;     

       struct address_space *mapping; 

       };

#if NR_CPUS >= CONFIG_SPLIT_PTLOCK_CPUS

       spinlock_t ptl;

#endif

       struct kmem_cache *slab;

       struct page *first_page;

   };

   union {

       pgoff_t index;      /* Our offset within mapping. */

       void *freelist;     /* SLUB: freelist req. slab lock */

   };

   struct list_head lru;      

 

#if defined(WANT_PAGE_VIRTUAL)

   void *virtual;         

#endif

#ifdef CONFIG_CGROUP_MEM_RES_CTLR

   unsigned long page_cgroup;

#endif

};

各字段含义如下:

flags:一组原子型标志,有些标志可以异步更新,这个域功能繁多,有兴趣的可以进一步看看源代码。

_count:页框的引用计数器。如果该字段为-1,则相应页框空闲,并可被分配给任一进程或内核本身;如果该字段大于或等于0,则说明该页框被分配给了一个或多个进程,或用于存放一些内核数据结构。

_mapcount:本页框对应的页表项数目,本页框若未被映射则为-1。

inuse、objects:这2个标志在SLUB分配器中使用。

private:可用于正在使用该页的内核成分使用,如果页是空闲的则该字段由伙伴系统使用。

mapping:当页被插入页高速缓存中时使用。

slab:指向slab的指针。

first_page:

index、freelist:作为不同的含义被几种内核成分使用。

lru:将页链接到最近最少使用双向链表中。

virtual:指向本页框的内核虚拟地址。

page_cgroup:这个域只有在内存资源控制器起作用的情况下使用,用于统计处于内存资源控制器下的页框。

 

1 名词解释:


(1)页框:物理内存的描述,必须牢牢记住,页框就是物理内存

(2)页描述符:描述每一个页框的状态信息,所有的也描述符都保存在mem_map[ ]数组中,每个描述符32个字节

(3)节点:系统物理内存被划分为多个节点,每个节点内cpu访问页面的时间是相同的,对应的数据结构:节点描述符

(4)管理区:每个节点又分为多个管理区                                                                                  对应的数据结构: 管理区描述符

 

2 页表管理


重点介绍内核页表的管理,主要分为两个阶段:启动阶段映射8M的页表和剩余页表的映射阶段

(1)启动阶段8M页表的映射过程

(2)剩余页表的映射过程

几个比较重要的地址转换:

虚拟地址转换成物理地址: virt_to_phsy(address){   __pa(address) }

虚拟地址转换页描述符的地址:  virt_to_page( kaddr )  {  return  mem_map + __pa(kaddr)  >>12 }

3 用户进程的地址空间

从内核看来,整个4G的地址空间是这样的。

 

进程可用的地址空间是被一个叫mm_struct(进程地址空间描述符)结构体来管理的,同一个进程内的多个线程是共享这个数据结构的。

同时,对于用户进程来说,每一个进程有一个独一无二的mm_struct,但是内核线程确不是必须的。下面是操作mm_struct的一些函数。

当然如果在进程创建的时候指定子进程共享父进程的虚拟地址空间的话,比如:

if ( clone_flags & CLONE_VM )

{

    atomic_inc(&old_mm->mm_user)

    mm = &oldmm;

    goto good_mm;

}

 

还有一点许哟阿注意的就是系统中的第一个mm_struct是需要静态初始化的,以后的所有的mm_strcut都是通过拷贝生成的。

 

mmap函数,内存映射函数

该函数的主要功能是在进程地址空间中创建一个线性区。有两种类型的内存映射:共享型和私有型。二者的主要区别可以理解成是否对其他进程可见。共享型每次对线性区的读写都会修改

磁盘文件,一个进程修改共享型的线性区,其他映射这一线性区的所有进程都是可见的。与内存映射相关的数据结构:

(1)与所映射的文件相关的索引节点对象

(2)所映射文件的address_space对象

(3)不同进程对同一文件进行不同映射所使用的文件对象

(4)对文件进行每一不同映射所使用的vm_area_struct

(5)对文件进行映射的线性区所分配的每个页框对应的描述符

 

从图上能看出一个文件对应一个inode,对应一个address_space,对应多个struct file, 对应多个vm_area_file ,对应多个page(页框),当然也对应多个page(页描述符)、

mm_struct 内存描述符中的两棵树: 当前进程内所有线性区的一个链表和所有线性区的红黑树

mmap和mm_rb都可以访问线性区。事实上,它们都指向了同一个vm_area_struct结构,只是链接的方式不同
mmap指向的线性区链表用来遍历整个进程的地址空间
红黑树mm_rb用来定位一个给定的线性地址落在进程地址空间中的哪一个线性区中
另外,mmap_cache用来缓存最近用过的线性区

address_space中的两棵树:基数和优先级搜索数。


address_space的page_tree指向了组织构成这个文件的所有的页描述符的基树

address_space的i_mmap指向了组织构成这个文件的所有的线性区描述符的基树

要注意一个问题:

(1)共享内存映射的页通常保存在也高速缓存中,私有内存映射的页只要还没有修改,也保存在页高速缓存中。当进程试图修改一个私有映射的页时,内核就把该页框进行复制,并在进程页表中用复制的页替换原来的页,这就是写时复制的基础。复制后的页框就不会放在页告诉缓存中了,原因是它不再是表示磁盘上那个文件的有效数据。

(2)线性区的开始和结束地址都是4K对齐的

进程获得新线性区的一些典型情况 :

刚刚创建的新进程
使用exec系统调用装载一个新的程序运行
将一个文件(或部分)映射到进程地址空间中
当用户堆栈不够用的时候,扩展堆栈对应的线性区

创建内存映射:

要想创建一个映射,就要调用mmap, mmap()最终会调用do_mmap()

static inline 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: 要映射文件的那一部分的长度

flag:一组标志,显示的指定映射的那部分是MAP_SHARED或MAP_PRIVATE

prot: 一组权限,指定对线性区访问的一种或多种访问权限

addr: 一个可选的线性地址,表示从这个地址之后的某个位置创建线性区

 

基本的过程是:

(1)先为要映射的文件申请一段线性区,调用内存描述符的get_unmapped_area()

(2)做一些权限和标志位检查

(3)将文件对象的地址struct file地址赋值给线性区描述符vm_area_struct.vm_file

(4)调用mmap方法,这个方法最后调用generic_file_mmap()

其他线性区处理函数

(1)find_vma() : 查找一个线性地址所属的线性区或后继线性区

(2)find_vmm_interrection(): 查找一个与给定区间重叠的线性区

(3)get_unmapped_area() : 查找一个空闲的线性区

(4)insert_vm_struct () : 向进程的内存描述符中插入一个线性区

缺页异常处理程序

(1)背景知识

内核中的函数以直接了当的方式获得动态内存,内核是操作系统中优先级最高的成分,内核信任自己,采用面级内存分配和小内存分配以及非连续线性区得到内存

用户态进程分配内存时,请求被认为是不紧迫的,用户进程不可信任,因此,当用户态进程请求动态内存时,并没有立即获得实际的物理页框,而仅仅获得对一个

新的线性地址区间的使用权这个线性地址区间会成为进程地址空间的一部分,称作线性区(memory areas)。 这样,当用户进程真正向这些线性区写的时候,就会

产生缺页异常,在缺页异常处理程序中获得真正的物理内存。

(2)缺页异常处理程序需要区分引起缺页的两种情况:编程错引起的缺页和属于进程的地址空间尚未分配到物理页框

简单流程图:

 

详细流程图:

 

linux为什么要分为三个区:ZONE_DMA  ZONE_NORMAL ZONE_HIGHMEM?

(1)isa总线的历史遗留问题,只能访问内存的前16M的空间

(2)大容量的RAM使得线性地址空间太小,并不是所有的物理空间都能映射到唯一的线性地址空间

如何确定某个页框属于哪个节点或管理区?

   是由每个页框描述符中的flag的高位索引的,比如page_zone()函数就是接收页描述符的地址作为参数,返回页描述符中flag的高位,并到zone_table[ ]数组中确定相应的管理区描述符的地址

slab算法是用来满足对以页框为单位的请求而设置的,简单介绍以下slab算法的原理

 

对于以页为单位的请求发送到管理区分配器,然后管理区分配器搜索它所管辖的管理区,找一个满足请求的分配区,然后再由这个管理区中的伙伴系统去处理,为了加快这个

过程,每个分区中还提供了一个每cpu页框高速缓存,来处理单个页框的请求。

这个过程中有四个请求页框的函数和宏:

(1)alloc_pages, alloc_page返回分配的第一个页框的页描述符的地址

(2)__get_free_pages , __get_free_page 返回分配的第一个页框的线性地址

其实二者是相同,因为有专门用来处理线性地址到页描述符地址转换的函数   virt_to_page()  实现从线性地址到页描述符地址的转换

 

 

Linux是一个遵循POSIX(Portable Operating System Interface)标准的操作系统,它继承了UNIX系统优秀的设计思想,拥有简练、容错强、高效而且稳定的内核。此外Linux还具备其他操作系统所不能比拟的优点。①:完全免费;②:内核源代码完全公开。

Linux2.4内核拥有一个功能完备的内存管理子系统,它增加了对NUMA(非均匀存储结构)体系结构的支持并且使用了基于区(ZONE)的物理内存管理方法,从而保持了物理上连续分布、而逻辑上统一的内存模式和传统的共享内存编程模型,使得系统的性能得以极大的扩展。这样Linux不仅能够满足传统的桌面应用,而且还能满足高端服务器市场的需要。目前,Linux不仅在Internet服务器上表现出色,而且还可以胜任大型数据库系统的服务器。

 

二:Linux存储管理的基本框架 

Linux内核采用虚拟页式存储管理,采用三次映射机制实现从线性地址到物理地址的映射。其中PGD为页面目录,PMD为中间目录,PT为页面表。具体的映射过程为:

⑴从CR3寄存器中找到PGD基地址;

⑵以线性地址的最高位段为下标,在PGD中找到指向PMD的指针;

⑶以线性地址的次位段为下标,在PMD中找到指向PT的指针;

⑷同理,在PT中找到指向页面的指针;

⑸线性地址的最后位段,为在此页中的偏移量,这样就完成了从线性地址到物理地址的映射过程。 

32位的微机平台如Intel的X86采用段页式的两层映射机制,而64位的微处理器采用三级分页。对于传统的32位平台,Linux采用让PMD(中间目录)全0来消除中间目录域,这样就把Linux逻辑上的三层映射模型落实到X86结构物理上的二层映射,从而保证了Linux对多种硬件平台的支持。

 

三:Linux对虚拟内存的管理 

虚拟内存不仅可以解决内存容量的问题,还可以提供以下附加的功能:大地址空间;进程保护;内存映射;灵活的物理内存分配;共享虚拟内存。

Linux对虚拟内存的管理以进程为基础。32位的线性地址映射的4G的虚拟空间中,从0XC0000000到0XFFFFFFFF的1G空间为所用进程所共享的内核空间,每个进程都有自己的3G用户空间。

Linux的虚拟内存管理需要各种机制的支持,首先内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址,在用户程序运行时时如果发现程序中要用的虚拟地址没有对应的物理地址,就发出请页要求①:如果有空闲的内存可供分配,就请求分配内存②,并把正在使用的物理页记录在页缓存中③,如果没有足够的内存分配,就调用交换机制,腾出一部分内存④⑤。另外在地址映射中要通过TLB(翻译后援存储器)来寻找物理页⑧,交换机制中要用到交换缓存⑥,并且把物理页内容交换到交换文件中也要修改页表来映射文件地址⑦。

一个进程的虚拟地址映射靠三个数据结构来描述:mm_struct、vm_area_struct、page。其中mm_struct结构用来描述一个进程的虚拟内存;vm_area_struct描述一个进程的虚拟地址区域,在这个区域中的所有页面具有相同的访问权限和一些属性;page描述一个具体的物理页面。 

当进程通过系统调用动态分配内存时,Linux首先分配一个vm_area_struct结构,并链接到进程的虚拟内存链表,当后续指令访问这一内存区域时,产生缺页异常。系统处理时,通过分析缺页原因、操作权限之后,如果页面在交换文件中,则进入do_page_fault()中恢复映射的代码,重新建立映射关系。如果因为页面不再内存中,则Linux会分配新的物理页,并建立映射关系。

当物理内存出现不足时,就需要换出一些页面。Linux采用LRU(Least Recently Used最近最少使用)页面置换算法选择需要从系统中换出的页面。系统中每个页面都有一个“age”属性,这个属性会在页面被访问的时候改变。Linux根据这个属性选择要回收的页面,同时为了避免页面“抖动”(即刚释放的页面又被访问),将页面的换出和内存页面的释放分两步来做,而在真正释放的时候仅仅只写回“脏”页面。这一任务由交换守护进程kswapd完成。free_pages_high,free_pages_low是衡量系统中现有空闲页的标准,当系统中空闲页的数量少于free_pages_high,甚至少于free_pages_low时,kswapd进程会采用三种方法来减少系统正在使用的物理页的数量。①调用shrink_mmap()减少buffer cache和page cache的大小;②调用shm_swap()将system V共享内存页交换到物理内存;③调用swap_out()交换或丢弃页。 

图1.3给出了页面置换管理框图。其中①代表:refill_inactive_scan(),它的任务是扫描活跃页面队列,从中找到可以转入不活跃状态的页面;②代表:page_launder()它把已经转入不活跃状态的“脏”页面“洗净”,使它们成为立即可以分配的页面;③代表:reclaim_page()用于从页面管理区的不活跃净页面队列中回收页面。

kswapd是被定期唤醒的,首先检查内存中可供分配或周转的物理页面是否短缺,若需要回收页面,则按顺序循环检查缓冲区、共享内存、进程独占的内存,遇到满足条件的页面,即将它释放。如果已释放了足够的页面,kswapd重新睡眠,直到下一次被重新唤醒。 

 

四:Linux对物理内存的管理 

Linux2.4内核加入了对NUMA的支持,如果系统是NUMA结构的处理机系统,则物理内存被划分为三个层次来管理:存储节点(Node),管理区(Zone),页面(Page)。处理器的本地内存组成的区域叫做一个节点(Node),它通过pglist_data数据结构来描述。各个节点的物理内存根据不同的作用又分为ZONE_DMA、ZONE_NORMAL、ZONE_HIGH,ZONE_DMA面积小,且专供DMA使用,ZONE_NORMAL则供大多数的程序使用,对于ZONE_HIGH仅仅只有页面缓存以及用户进程能够使用该区域的空间。每个管理区对应一个free_area数组来组织空闲页面队列,该数组的每一项描述某一种页块的信息,第一个元素描述大小为1页的内存块的信息,第二个元素描述大小为2 页的内存块的信息,依此类推,所描述的页块大小以 2 的倍数增加。free_area 数组的定义如下: 

Typedef struct free_area_struct{ 

Struct list_head free_list;

Unsigned int   *map; 

}free_area_t; 

list_head是一个双向指针结构,在这里用于将物理页块结构mem_map_t 连结成一个双向链表,而map则是记录这种页块组分配情况的位图,例如,位图的第N位为1,表明第N个页块是空闲的。

页分配代码使用向量表free_area来分配和回收物理页。系统初始化时,free_area数组也被赋了初值。也就是说,系统中所有可用的空闲物理页块都已经被加到了free_area数组中。 

Linux使用Buddy最先匹配算法来进行页面的分配和回收,并且必须按2的幂次方进行分配。如图1.4所示,比如要分配大小为2k的空闲块,如果系统中有足够的空闲块,页面分配代码首先在free_area中查找相应大小的空闲块,如果找到则分配。如果没有则查找下一尺寸(2倍于请求大小)的页面块,继续这一过程直到找到可以分配的页面,按要求分配之后,将剩余的空闲块仍然按照2的幂次方划分后链入适当的空闲块中。与分配算法相反,页面回收时总是试图将相邻的空闲页面组合成更大尺寸的空闲块。这里先给出“伙伴”要满足的三个条件:①两个块大小相同;②两个块物理地址连续;③两个块从同一大块中分离出来。在用户释放内存时,判断“伙伴”是否是空闲块。若否,则只要将释放的空闲块简单的插入相应的free_area中。若是,则需要在free_area中删除其伙伴关系,然后再判断合并后的空闲块的伙伴关系,依次重复,直到归并后的空闲块没有伙伴关系或合并到最大块时将其插入到free_area中。

 

五:缓存和刷新机制 

为了更好的发挥系统性能,Linux采用了一系列和内存管理相关的高速缓存机制:              

①缓冲区高速缓存:包含了从设备中读取的数据块或写入设备的数据块。缓冲区高速缓存由设备标示号和块索引,因此可以快速找到数据块。如果数据可以在缓冲区中高速缓存中找到,则不需要从物理块设备上读取,从而加快了访问速度。 

②页高速缓存:这一高速缓存用来加速对磁盘上的映像和数据访问,它用来缓存某个文件的逻辑内容,并通过文件VFS索引节点和偏移量访问。当页从磁盘读到物理内存时,就缓存在页高速缓存。 

③交换高速缓存:用于多个近程共享的页面被换出到交换区的情况。当页面交换到交换文件之后,如果有进程再次访问,它会被重新调入内存。

 

六:小结 

Linux是近年来应用的比较多的一个操作系统,广泛应用于各个行业。而且由于全世界计算机爱好者的支持,Linux也成为世界上发展最快的操作系统。在Linux2.6内核中,对存储管理子系统进行了一系列的改进,提高了系统的可扩展性,包含了对大型服务器如NUMA服务器和Intel服务器的良好支持。此外,Linux2.6还提供了对无MMU的支持。可见Linux正在不断的加强对高端服务器领域以及嵌入式领域的支持。 

Linux在其发展过程中不断的在完善和优化内存管理单元的功能和性能。针对具体领域,我们可以根据自己的需要定制Linux内核。而内存管理单元作为Linux操作系统的核心部分,在整个系统的运行过程中发挥着举足轻重的作用。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多