内存_ARM 页目录以及页表


2.1  linux内存管理基本框架

 linux中的分段分页机制分三层,页目录(PGD),中间目录(PMD),页表(PT)。PT中的表项称为页表项(PTE)。注意英文缩写,在linux程序中函数变量的名字等都会和英文缩写相关。

LINUX中的三级映射流程如图:

但是arm结构的MMU在硬件只有2级映射,所以在软件上会跳过PMD表。即:在PGD中直接放的是PT的base address。在linux软件上就是:

  1. #define PMD_SHIFT       21  
  2. #define PGDIR_SHIFT 21             //让PMD_SHIFT 和 PGDIR_SHIFT 相等就可以了。  


 

新的2.6内核和内核源码情景分析上的差别挺大的,在2.6.11版本以后,linux将软件上的3级映射变成了4级映射,在PMD后面增加了一个PUD(page upper directory). 在arm的两级映射中,跳过PMD和PUD

在2.6.39内核arch/arm/include/asm/pgtable.h中有下代码:

  1. /* 
  2.  * Hardware-wise, we have a two level page table structure, where the first 
  3.  * level has 4096 entries, and the second level has 256 entries.  Each entry 
  4.  * is one 32-bit word.  Most of the bits in the second level entry are used 
  5.  * by hardware, and there aren't any "accessed" and "dirty" bits. 
  6.  * 
  7.  * Linux on the other hand has a three level page table structure, which can 
  8.  * be wrapped to fit a two level page table structure easily - using the PGD 
  9.  * and PTE only.  However, Linux also expects one "PTE" table per page, and 
  10.  * at least a "dirty" bit. 
  11.  * 
  12.  * Therefore, we tweak the implementation slightly - we tell Linux that we 
  13.  * have 2048 entries in the first level, each of which is 8 bytes (iow, two 
  14.  * hardware pointers to the second level.)  The second level contains two 
  15.  * hardware PTE tables arranged contiguously, preceded by Linux versions 
  16.  * which contain the state information Linux needs.  We, therefore, end up 
  17.  * with 512 entries in the "PTE" level. 
  18.  * 
  19.  * This leads to the page tables having the following layout: 
  20.  * 
  21.  *    pgd             pte 
  22.  * |        | 
  23.  * +--------+ 
  24.  * |        |       +------------+ +0 
  25.  * +- - - - +       | Linux pt 0 | 
  26.  * |        |       +------------+ +1024 
  27.  * +--------+ +0    | Linux pt 1 | 
  28.  * |        |-----> +------------+ +2048 
  29.  * +- - - - + +4    |  h/w pt 0  | 
  30.  * |        |-----> +------------+ +3072 
  31.  * +--------+ +8    |  h/w pt 1  | 
  32.  * |        |       +------------+ +4096 
  33.  * 
  34.  * See L_PTE_xxx below for definitions of bits in the "Linux pt", and 
  35.  * PTE_xxx for definitions of bits appearing in the "h/w pt". 
  36.  * 
  37.  * PMD_xxx definitions refer to bits in the first level page table. 
  38.  * 
  39.  * The "dirty" bit is emulated by only granting hardware write permission 
  40.  * iff the page is marked "writable" and "dirty" in the Linux PTE.  This 
  41.  * means that a write to a clean page will cause a permission fault, and 
  42.  * the Linux MM layer will mark the page dirty via handle_pte_fault(). 
  43.  * For the hardware to notice the permission change, the TLB entry must 
  44.  * be flushed, and ptep_set_access_flags() does that for us. 
  45.  * 
  46.  * The "accessed" or "young" bit is emulated by a similar method; we only 
  47.  * allow accesses to the page if the "young" bit is set.  Accesses to the 
  48.  * page will cause a fault, and handle_pte_fault() will set the young bit 
  49.  * for us as long as the page is marked present in the corresponding Linux 
  50.  * PTE entry.  Again, ptep_set_access_flags() will ensure that the TLB is 
  51.  * up to date. 
  52.  * 
  53.  * However, when the "young" bit is cleared, we deny access to the page 
  54.  * by clearing the hardware PTE.  Currently Linux does not flush the TLB 
  55.  * for us in this case, which means the TLB will retain the transation 
  56.  * until either the TLB entry is evicted under pressure, or a context 
  57.  * switch which changes the user space mapping occurs. 
  58.  */  
  1. <p>#define PTRS_PER_PTE  512                              //PTE的个数  
  2. #define PTRS_PER_PMD  1  
  3. #define PTRS_PER_PGD  2048</p><p>#define PTE_HWTABLE_PTRS (PTRS_PER_PTE)  
  4. #define PTE_HWTABLE_OFF  (PTE_HWTABLE_PTRS * sizeof(pte_t))  
  5. #define PTE_HWTABLE_SIZE (PTRS_PER_PTE * sizeof(u32))</p><p>/*  
  6.  * PMD_SHIFT determines the size of the area a second-level page table can map  
  7.  * PGDIR_SHIFT determines what a third-level page table entry can map  
  8.  */  
  9. #define PMD_SHIFT  21  
  10. #define PGDIR_SHIFT  21 //另PMD和PDGIR相等,来跳过PMD。</p>  

/*linux将PGD为2k,每项为8个byte。(MMU中取值为前高12bit,为4k,每项4个byte,但linux为什么要这样做呢?) ,另外linux在定义pte时,定义了两个pte,一个供MMU使用,一个供linux使用,来用描述这个页。
 *根据注释中的表图,我看到有  #define PTRS_PER_PTE  512   #define PTRS_PER_PGD  2048 ,  linux将PDG定义为2K,8byte,
每个pte项为512,4byte。 他将 两个pte项进行了一下合并。为什么?为什么?

**/

在进程中,传说是可以看到4G的空间,按照linux的用户空间和内核空间划分。 其实进程可以看到3G的自己进程的空间,3G-4G的空间是内核空间,进程仅能通过系统调用进入。

 

2.2地址映射全过程

将x86的段地址,略。。。

2.3几个重要的数据结构和函数

PGD,PTE的值定义:

  1. /* 
  2.  * These are used to make use of C type-checking.. 
  3.  */  
  4. typedef struct { pteval_t pte; } pte_t;  
  5. typedef struct { unsigned long pmd; } pmd_t;  
  6. typedef struct { unsigned long pgd[2]; } pgd_t; //定义一个[2]数组,这样就和上面的介绍对应起来了,每个PGD是一个8个byte的值(即两个long型数)。  
  7. typedef struct { unsigned long pgprot; } pgprot_t;  

这几个结构体定义PTE等他们的结构, pgprot_t 是page protect的意思,最上面的图可知,在具体映射时候,需要将PTE表中的高20bit的值 + 线性地址的低 12bit的值  才是具体的物理地址。所以PTE只有高20bit是在地址映射映射时有效的,那么他的低12bit用来放一些protect的数据,如writeable,rdonly......下面是pgprot_t的一些值:

  1. #define __PAGE_NONE     __pgprot(_L_PTE_DEFAULT | L_PTE_RDONLY | L_PTE_XN)  
  2. #define __PAGE_SHARED       __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_XN)  
  3. #define __PAGE_SHARED_EXEC  __pgprot(_L_PTE_DEFAULT | L_PTE_USER)  
  4. #define __PAGE_COPY     __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY | L_PTE_XN)  
  5. #define __PAGE_COPY_EXEC    __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY)  
  6. #define __PAGE_READONLY     __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY | L_PTE_XN)  
  7. #define __PAGE_READONLY_EXEC    __pgprot(_L_PTE_DEFAULT | L_PTE_USER | L_PTE_RDONLY)  


所以当我们想做一个PTE时(即生成一个page的pte),调用下列函数:

  1. #define mk_pte(page,prot)   pfn_pte(page_to_pfn(page), prot)  

这个函数即是 page_to_pfn(page)得到高20bit的值 + prot的值。 prot即为pgprot_t的结构型值,低12bit。表示页的属性。

 生成这一个pte后,我们要让这个pte生效,就要在将这个值 赋值到对应的地方去。

 set_pte(pteptr,pteval):  但是在2.6.39没有找到这个代码,过程应该差不多了,算了·····

另外,我们可以通过对PTE的低12bit设置,来让mmu判断,是否建立了映射?还是建立了映射但已经被swap出去了? 

还有其他很多的函数,和宏定义,具体可以看《深入linux内核》的内存寻址这章。

 

这里都是说的PTE中的低12bit的, 但是在2.6.39中明显多了一个linux pt, 具体做什么用还不太清楚。但是linux在arch/arm/include/asm/pgtable.h 中由个注释

  1. /* 
  2.  * "Linux" PTE definitions. 
  3.  * 
  4.  * We keep two sets of PTEs - the hardware and the linux version. 
  5.  * This allows greater flexibility in the way we map the Linux bits 
  6.  * onto the hardware tables, and allows us to have YOUNG and DIRTY 
  7.  * bits. 
  8.  * 
  9.  * The PTE table pointer refers to the hardware entries; the "Linux" 
  10.  * entries are stored 1024 bytes below. 
  11.  */  

应该可以看出,linux pt和PTE的低12bit是相呼应的,用来记录page的一些信息。

 

 

linux中,每个物理页都有一个对应的page结构体,组成一个数组,放在mem_map中。

  1. /* 
  2.  * Each physical page in the system has a struct page associated with 
  3.  * it to keep track of whatever it is we are using the page for at the 
  4.  * moment. Note that we have no way to track which tasks are using 
  5.  * a page, though if it is a pagecache page, rmap structures can tell us 
  6.  * who is mapping it. 
  7.  */  
  8. struct page {  
  9.     unsigned long flags;        /* Atomic flags, some possibly 
  10.                      * updated asynchronously */  
  11.     atomic_t _count;        /* Usage count, see below. */  
  12.     union {  
  13.         atomic_t _mapcount; /* Count of ptes mapped in mms, 
  14.                      * to show when page is mapped 
  15.                      * & limit reverse map searches. 
  16.                      */  
  17.         struct {        /* SLUB */  
  18.             u16 inuse;  
  19.             u16 objects;  
  20.         };  
  21.     };  
  22.     union {  
  23.         struct {  
  24.         unsigned long private;      /* Mapping-private opaque data: 
  25.                          * usually used for buffer_heads 
  26.                          * if PagePrivate set; used for 
  27.                          * swp_entry_t if PageSwapCache; 
  28.                          * indicates order in the buddy 
  29.                          * system if PG_buddy is set. 
  30.                          */  
  31.         struct address_space *mapping;  /* If low bit clear, points to 
  32.                          * inode address_space, or NULL. 
  33.                          * If page mapped as anonymous 
  34.                          * memory, low bit is set, and 
  35.                          * it points to anon_vma object: 
  36.                          * see PAGE_MAPPING_ANON below. 
  37.                          */  
  38.         };  
  39. #if USE_SPLIT_PTLOCKS  
  40.         spinlock_t ptl;  
  41. #endif  
  42.         struct kmem_cache *slab;    /* SLUB: Pointer to slab */  
  43.         struct page *first_page;    /* Compound tail pages */  
  44.     };  
  45.     union {  
  46.         pgoff_t index;      /* Our offset within mapping. */  
  47.         void *freelist;     /* SLUB: freelist req. slab lock */  
  48.     };  
  49.     struct list_head lru;       /* Pageout list, eg. active_list 
  50.                      * protected by zone->lru_lock ! 
  51.                      */  
  52.     /* 
  53.      * On machines where all RAM is mapped into kernel address space, 
  54.      * we can simply calculate the virtual address. On machines with 
  55.      * highmem some memory is mapped into kernel virtual memory 
  56.      * dynamically, so we need a place to store that address. 
  57.      * Note that this field could be 16 bits on x86 ... ;) 
  58.      * 
  59.      * Architectures with slow multiplication can define 
  60.      * WANT_PAGE_VIRTUAL in asm/page.h 
  61.      */  
  62. #if defined(WANT_PAGE_VIRTUAL)  
  63.     void *virtual;          /* Kernel virtual address (NULL if 
  64.                        not kmapped, ie. highmem) */  
  65. #endif /* WANT_PAGE_VIRTUAL */  
  66. #ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS  
  67.     unsigned long debug_flags;  /* Use atomic bitops on this */  
  68. #endif  
  69.   
  70. #ifdef CONFIG_KMEMCHECK  
  71.     /* 
  72.      * kmemcheck wants to track the status of each byte in a page; this 
  73.      * is a pointer to such a status block. NULL if not tracked. 
  74.      */  
  75.     void *shadow;  
  76. #endif  
  77. };  

页的page结构放在mem_map中。  要想找到一个物理地址对应的page结构很简单, 就是   mem_map[pfn]即可。    
 

 

由于硬件的关系,会将整个内存分成不同的zone,:ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM. 这样分的原因是 :有些芯片的DMA设置只能设置到固定区域的地址,所以将这些区域作为ZONE_DMA以免其他操作占用。NORMAL就是正常的内存。高位内存内核空间因为只有1G不能全部映射,所以也要做特殊的处理,进行映射,才能使用。

这三个ZONE使用的描述符是:

  1. struct zone {  
  2.     /* Fields commonly accessed by the page allocator */  
  3.   
  4.     /* zone watermarks, access with *_wmark_pages(zone) macros */  
  5.     unsigned long watermark[NR_WMARK];  
  6.   
  7.     /* 
  8.      * When free pages are below this point, additional steps are taken 
  9.      * when reading the number of free pages to avoid per-cpu counter 
  10.      * drift allowing watermarks to be breached 
  11.      */  
  12.     unsigned long percpu_drift_mark;  
  13.   
  14.     /* 
  15.      * We don't know if the memory that we're going to allocate will be freeable 
  16.      * or/and it will be released eventually, so to avoid totally wasting several 
  17.      * GB of ram we must reserve some of the lower zone memory (otherwise we risk 
  18.      * to run OOM on the lower zones despite there's tons of freeable ram 
  19.      * on the higher zones). This array is recalculated at runtime if the 
  20.      * sysctl_lowmem_reserve_ratio sysctl changes. 
  21.      */  
  22.     unsigned long       lowmem_reserve[MAX_NR_ZONES];  
  23.   
  24. #ifdef CONFIG_NUMA  
  25.     int node;  
  26.     /* 
  27.      * zone reclaim becomes active if more unmapped pages exist. 
  28.      */  
  29.     unsigned long       min_unmapped_pages;  
  30.     unsigned long       min_slab_pages;  
  31. #endif  
  32.     struct per_cpu_pageset __percpu *pageset;  
  33.     /* 
  34.      * free areas of different sizes 
  35.      */  
  36.     spinlock_t      lock;  
  37.     int                     all_unreclaimable; /* All pages pinned */  
  38. #ifdef CONFIG_MEMORY_HOTPLUG  
  39.     /* see spanned/present_pages for more description */  
  40.     seqlock_t       span_seqlock;  
  41. #endif  
  42.     struct free_area    free_area[MAX_ORDER];  
  43.   
  44. #ifndef CONFIG_SPARSEMEM  
  45.     /* 
  46.      * Flags for a pageblock_nr_pages block. See pageblock-flags.h. 
  47.      * In SPARSEMEM, this map is stored in struct mem_section 
  48.      */  
  49.     unsigned long       *pageblock_flags;  
  50. #endif /* CONFIG_SPARSEMEM */  
  51.   
  52. #ifdef CONFIG_COMPACTION  
  53.     /* 
  54.      * On compaction failure, 1<<compact_defer_shift compactions 
  55.      * are skipped before trying again. The number attempted since 
  56.      * last failure is tracked with compact_considered. 
  57.      */  
  58.     unsigned int        compact_considered;  
  59.     unsigned int        compact_defer_shift;  
  60. #endif  
  61.   
  62.     ZONE_PADDING(_pad1_)  
  63.   
  64.     /* Fields commonly accessed by the page reclaim scanner */  
  65.     spinlock_t      lru_lock;     
  66.     struct zone_lru {  
  67.         struct list_head list;  
  68.     } lru[NR_LRU_LISTS];  
  69.   
  70.     struct zone_reclaim_stat reclaim_stat;  
  71.   
  72.     unsigned long       pages_scanned;     /* since last reclaim */  
  73.     unsigned long       flags;         /* zone flags, see below */  
  74.   
  75.     /* Zone statistics */  
  76.     atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];  
  77.   
  78.     /* 
  79.      * The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on 
  80.      * this zone's LRU.  Maintained by the pageout code. 
  81.      */  
  82.     unsigned int inactive_ratio;  
  83.   
  84.   
  85.     ZONE_PADDING(_pad2_)  
  86.     /* Rarely used or read-mostly fields */  
  87.   
  88.     /* 
  89.      * wait_table       -- the array holding the hash table 
  90.      * wait_table_hash_nr_entries   -- the size of the hash table array 
  91.      * wait_table_bits  -- wait_table_size == (1 << wait_table_bits) 
  92.      * 
  93.      * The purpose of all these is to keep track of the people 
  94.      * waiting for a page to become available and make them 
  95.      * runnable again when possible. The trouble is that this 
  96.      * consumes a lot of space, especially when so few things 
  97.      * wait on pages at a given time. So instead of using 
  98.      * per-page waitqueues, we use a waitqueue hash table. 
  99.      * 
  100.      * The bucket discipline is to sleep on the same queue when 
  101.      * colliding and wake all in that wait queue when removing. 
  102.      * When something wakes, it must check to be sure its page is 
  103.      * truly available, a la thundering herd. The cost of a 
  104.      * collision is great, but given the expected load of the 
  105.      * table, they should be so rare as to be outweighed by the 
  106.      * benefits from the saved space. 
  107.      * 
  108.      * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the 
  109.      * primary users of these fields, and in mm/page_alloc.c 
  110.      * free_area_init_core() performs the initialization of them. 
  111.      */  
  112.     wait_queue_head_t   * wait_table;  
  113.     unsigned long       wait_table_hash_nr_entries;  
  114.     unsigned long       wait_table_bits;  
  115.   
  116.     /* 
  117.      * Discontig memory support fields. 
  118.      */  
  119.     struct pglist_data  *zone_pgdat;  
  120.     /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */  
  121.     unsigned long       zone_start_pfn;  
  122.   
  123.     /* 
  124.      * zone_start_pfn, spanned_pages and present_pages are all 
  125.      * protected by span_seqlock.  It is a seqlock because it has 
  126.      * to be read outside of zone->lock, and it is done in the main 
  127.      * allocator path.  But, it is written quite infrequently. 
  128.      * 
  129.      * The lock is declared along with zone->lock because it is 
  130.      * frequently read in proximity to zone->lock.  It's good to 
  131.      * give them a chance of being in the same cacheline. 
  132.      */  
  133.     unsigned long       spanned_pages;  /* total size, including holes */  
  134.     unsigned long       present_pages;  /* amount of memory (excluding holes) */  
  135.   
  136.     /* 
  137.      * rarely used fields: 
  138.      */  
  139.     const char      *name;  
  140. } ____cacheline_internodealigned_in_smp;  


在这个描述符中,有一个struct free_area free_area[MAX_ORDER];  这样的成员变量,这个数组的每个元素都包含一个page的list,那么,一共有MAX_ORDER个list。他是将 所有的free page按照连续是否进行分组。 有2个连续的page的,有4个连续的page的,有8个连续的page的,....2^MAX_ORDER。  这样将page进行分类。当需要alloc一些page时,可以方便的从这些list中找连续的page。(slub中也会像这样来分类,但是slub中分类是按byte为单位,如2byte,4byte....)

 

另外还有一个 

  1. struct zone_lru {  
  2.         struct list_head list;  
  3.     } lru[NR_LRU_LISTS];  
  1. <p>/* 
  2.  * We do arithmetic on the LRU lists in various places in the code, 
  3.  * so it is important to keep the active lists LRU_ACTIVE higher in 
  4.  * the array than the corresponding inactive lists, and to keep 
  5.  * the *_FILE lists LRU_FILE higher than the corresponding _ANON lists. 
  6.  * 
  7.  * This has to be kept in sync with the statistics in zone_stat_item 
  8.  * above and the descriptions in vmstat_text in mm/vmstat.c 
  9.  */  
  10. #define LRU_BASE 0  
  11. #define LRU_ACTIVE 1  
  12. #define LRU_FILE 2</p><p>enum lru_list {  
  13.  LRU_INACTIVE_ANON = LRU_BASE,  
  14.  LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,  
  15.  LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,  
  16.  LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,  
  17.  LRU_UNEVICTABLE,  
  18.  NR_LRU_LISTS  
  19. };</p>  

这样的成员变量,这也是一个list数组, 其中NR_LRU_LISTS表示在enum 中定义的成员的个数(新的linux中好像有不少这样的用法),这个list数组中,每个list也是很多page结构体。LRU是一中算法,会将长时间不同的page给swap out到flash中。这个数组是LRU算法用的。 其中有 inactive的page, active的page, inactive 的file,等  在后面对LRU介绍。

 

UMA和NUMA

在linux中,如果CPU访问所有的memory所需的时间都是一样的,那么我们人为这个系统是UMA(uniform memory architecture),但是如果访问memory所需的时间是不一样的(在SMP(多核系统)是很常见的),那么我们人为这个系统是NUMA(Non-uniform memory architecture)。在linux中,系统会将内存分成几个node,每个node中的memory,CPU进入的时间的相等的。这个我们分配内存的时候就会从一个node进行分配。

  1. typedef struct pglist_data {  
  2.     struct zone node_zones[MAX_NR_ZONES];  
  3.     struct zonelist node_zonelists[MAX_ZONELISTS];  
  4.     int nr_zones;  
  5. #ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */  
  6.     struct page *node_mem_map;  
  7. #ifdef CONFIG_CGROUP_MEM_RES_CTLR  
  8.     struct page_cgroup *node_page_cgroup;  
  9. #endif  
  10. #endif  
  11. #ifndef CONFIG_NO_BOOTMEM  
  12.     struct bootmem_data *bdata;  
  13. #endif  
  14. #ifdef CONFIG_MEMORY_HOTPLUG  
  15.     /* 
  16.      * Must be held any time you expect node_start_pfn, node_present_pages 
  17.      * or node_spanned_pages stay constant.  Holding this will also 
  18.      * guarantee that any pfn_valid() stays that way. 
  19.      * 
  20.      * Nests above zone->lock and zone->size_seqlock. 
  21.      */  
  22.     spinlock_t node_size_lock;  
  23. #endif  
  24.     unsigned long node_start_pfn;  
  25.     unsigned long node_present_pages; /* total number of physical pages */  
  26.     unsigned long node_spanned_pages; /* total size of physical page 
  27.                          range, including holes */  
  28.     int node_id;  
  29.     wait_queue_head_t kswapd_wait;  
  30.     struct task_struct *kswapd;  
  31.     int kswapd_max_order;  
  32.     enum zone_type classzone_idx;  
  33. } pg_data_t;  

在pglist_data结构中,一个成员变量node_zones[], 他代表了这个node下的所有zone。组成一个数组。

另一个成员变量node_zonelist[]. 这是一个list数组,每一个list中将不同node下的所有的zone都link到一起。数组中不同元素list中,zone的排列顺序不一样。这样当内核需要malloc一些page的时候,可能当前的这个node中并没有足够的page,那么就会按照这个数组list中的顺序依次去申请空间。内核在不同的情况下,需要按照不同的顺序申请空间。所以需要好几个不同的list,这些list就组成了这个数组。

每一个pglist_data结构对应一个node。这样,在每个zone结构上又多了一个node结构。这样内存页管理的结构应该是

node:同过UMA和NUMA将内存分成几个node。(在arm系统中,如果不是smp的一般都是一个node)

zone:在每个node中,再将内存分配成几个zone。

page:在每个zone中,对page进行管理,添加都各种list中。

for example: 可以分析一下 for_each_zone这个宏定义,会发现就是先查找每个node,在每个node下进行zone的扫描。

 

上面的这些都是描述物理空间的,page,zone,node都是物理空间的管理结构,下面的结构体,描述虚拟空间

从物理空间来看,物理空间主要是“供”,他是实实在在存在的,主要目的就是向OS提供空间。 

从虚拟空间来看,虚拟空间是“需”,他是虚拟的,主要是就发送需求。

*****当虚拟空间提出了 “需求”,但物理空间无法满足时候,就会进行swap out操作了。

 

vm_area_struct结构:这个结构描述  进程的的内存空间的情况。

  1. /* 
  2.  * This struct defines a memory VMM memory area. There is one of these 
  3.  * per VM-area/task.  A VM area is any part of the process virtual memory 
  4.  * space that has a special rule for the page-fault handlers (ie a shared 
  5.  * library, the executable area etc). 
  6.  */  
  7. struct vm_area_struct {  
  8.     struct mm_struct * vm_mm;   /* The address space we belong to. */  
  9.     unsigned long vm_start;     /* Our start address within vm_mm. */  
  10.     unsigned long vm_end;       /* The first byte after our end address 
  11.                        within vm_mm. */  
  12.   
  13.     /* linked list of VM areas per task, sorted by address */  
  14.     struct vm_area_struct *vm_next, *vm_prev;  
  15.   
  16.     pgprot_t vm_page_prot;      /* Access permissions of this VMA. */  
  17.     unsigned long vm_flags;     /* Flags, see mm.h. */  
  18.   
  19.     struct rb_node vm_rb;  
  20.   
  21.     /* 
  22.      * For areas with an address space and backing store, 
  23.      * linkage into the address_space->i_mmap prio tree, or 
  24.      * linkage to the list of like vmas hanging off its node, or 
  25.      * linkage of vma in the address_space->i_mmap_nonlinear list. 
  26.      */  
  27.     union {  
  28.         struct {  
  29.             struct list_head list;  
  30.             void *parent;   /* aligns with prio_tree_node parent */  
  31.             struct vm_area_struct *head;  
  32.         } vm_set;  
  33.   
  34.         struct raw_prio_tree_node prio_tree_node;  
  35.     } shared;  
  36.   
  37.     /* 
  38.      * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma 
  39.      * list, after a COW of one of the file pages.  A MAP_SHARED vma 
  40.      * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack 
  41.      * or brk vma (with NULL file) can only be in an anon_vma list. 
  42.      */  
  43.     struct list_head anon_vma_chain; /* Serialized by mmap_sem & 
  44.                       * page_table_lock */  
  45.     struct anon_vma *anon_vma;  /* Serialized by page_table_lock */  
  46.   
  47.     /* Function pointers to deal with this struct. */  
  48.     const struct vm_operations_struct *vm_ops;  
  49.   
  50.     /* Information about our backing store: */  
  51.     unsigned long vm_pgoff;     /* Offset (within vm_file) in PAGE_SIZE 
  52.                        units, *not* PAGE_CACHE_SIZE */  
  53.     struct file * vm_file;      /* File we map to (can be NULL). */  
  54.     void * vm_private_data;     /* was vm_pte (shared mem) */  
  55.     unsigned long vm_truncate_count;/* truncate_count or restart_addr */  
  56.   
  57. #ifndef CONFIG_MMU  
  58.     struct vm_region *vm_region;    /* NOMMU mapping region */  
  59. #endif  
  60. #ifdef CONFIG_NUMA  
  61.     struct mempolicy *vm_policy;    /* NUMA policy for the VMA */  
  62. #endif  
  63. };  

这个数据结构在程序的变量名字常常是vma。

这个结构是成员变量,  vm_start 和vm_end表示一段连续的虚拟区间。  但是并不是一个连续的虚拟区间就可以用一个vm_area_struct结构来表示。而是要 虚拟地址连续,并且这段空间的属性/访问权限等也相同才可以。

所以这段空间的属于和权限用

  1. pgprot_t vm_page_prot;  /* Access permissions of this VMA. */  
  2.  unsigned long vm_flags;  /* Flags, see mm.h. */   

这两个成员变量来表示。看到这两个成员变量可算看到亲人了。 还记得那个大明湖畔的容嬷嬷妈?-------pgprot_t vm_page_prot;

每一个进程的所有vm_erea_struct结构通过

  1. /* linked list of VM areas per task, sorted by address */  
  2.     struct vm_area_struct *vm_next, *vm_prev;  

这个指针link起来。

当然,有上面这种链表的链接方式,查找起来是很麻烦的,而且一个进程中可能会有好多个这样的结构,所以 除了上面的链表,还要有一中更有效的查找方式:

  1. <span style="font-size:13px;">  /* 
  2.      * For areas with an address space and backing store, 
  3.      * linkage into the address_space->i_mmap prio tree, or 
  4.      * linkage to the list of like vmas hanging off its node, or 
  5.      * linkage of vma in the address_space->i_mmap_nonlinear list. 
  6.      */  
  7.     union {  
  8.         struct {  
  9.             struct list_head list;  
  10.             void *parent;   /* aligns with prio_tree_node parent */  
  11.             struct vm_area_struct *head;  
  12.         } vm_set;  
  13.   
  14.         struct raw_prio_tree_node prio_tree_node;  
  15.     } shared;  
  16. </span>  


 

有两种情况虚拟空间会与磁盘flash发生关系。

1. 当内存分配失败,没有足够的内存时候,会发生swap。

2. linux进行mmap系统时候,会将磁盘上的内存map到用户空间,像访问memory一样,直接访问文件内容。 

 

为了迎合1. vm_area_struct 的mapping,vm_next_share,vm_pprev_share,vm_file等。但是在2.6.39中没找到这些变量。仅有:(在page结构体中,也有swap相关的信息)

  1. /* Information about our backing store: */  
  2.     unsigned long vm_pgoff;     /* Offset (within vm_file) in PAGE_SIZE 
  3.                        units, *not* PAGE_CACHE_SIZE */  
  4.     struct file * vm_file;      /* File we map to (can be NULL). */  
  5.     void * vm_private_data;     /* was vm_pte (shared mem) */  
  6.     unsigned long vm_truncate_count;/* truncate_count or restart_addr */  

在后面再分析这些结构。

为了迎合2. vm_area_struct结构提供了

  1. /* Function pointers to deal with this struct. */  
  2.     const struct vm_operations_struct *vm_ops;  

这样的变量,进行操作。

 

在vm_area_struct结构中,有个mm_struct结构,注释说他属于这个结构,由此看来,mm_struct结构应该是vm_area_struct结构的上层。

  1. struct mm_struct {  
  2.     struct vm_area_struct * mmap;       /* list of VMAs */  
  3.     struct rb_root mm_rb;  
  4.     struct vm_area_struct * mmap_cache; /* last find_vma result */  
  5. #ifdef CONFIG_MMU  
  6.     unsigned long (*get_unmapped_area) (struct file *filp,  
  7.                 unsigned long addr, unsigned long len,  
  8.                 unsigned long pgoff, unsigned long flags);  
  9.     void (*unmap_area) (struct mm_struct *mm, unsigned long addr);  
  10. #endif  
  11.     unsigned long mmap_base;        /* base of mmap area */  
  12.     unsigned long task_size;        /* size of task vm space */  
  13.     unsigned long cached_hole_size;     /* if non-zero, the largest hole below free_area_cache */  
  14.     unsigned long free_area_cache;      /* first hole of size cached_hole_size or larger */  
  15.     pgd_t * pgd;  
  16.     atomic_t mm_users;          /* How many users with user space? */  
  17.     atomic_t mm_count;          /* How many references to "struct mm_struct" (users count as 1) */  
  18.     int map_count;              /* number of VMAs */  
  19.   
  20.     spinlock_t page_table_lock;     /* Protects page tables and some counters */  
  21.     struct rw_semaphore mmap_sem;  
  22.   
  23.     struct list_head mmlist;        /* List of maybe swapped mm's.  These are globally strung 
  24.                          * together off init_mm.mmlist, and are protected 
  25.                          * by mmlist_lock 
  26.                          */  
  27.   
  28.   
  29.     unsigned long hiwater_rss;  /* High-watermark of RSS usage */  
  30.     unsigned long hiwater_vm;   /* High-water virtual memory usage */  
  31.   
  32.     unsigned long total_vm, locked_vm, shared_vm, exec_vm;  
  33.     unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;  
  34.     unsigned long start_code, end_code, start_data, end_data;  
  35.     unsigned long start_brk, brk, start_stack;  
  36.     unsigned long arg_start, arg_end, env_start, env_end;  
  37.   
  38.     unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */  
  39.   
  40.     /* 
  41.      * Special counters, in some configurations protected by the 
  42.      * page_table_lock, in other configurations by being atomic. 
  43.      */  
  44.     struct mm_rss_stat rss_stat;  
  45.   
  46.     struct linux_binfmt *binfmt;  
  47.   
  48.     cpumask_t cpu_vm_mask;  
  49.   
  50.     /* Architecture-specific MM context */  
  51.     mm_context_t context;  
  52.   
  53.     /* Swap token stuff */  
  54.     /* 
  55.      * Last value of global fault stamp as seen by this process. 
  56.      * In other words, this value gives an indication of how long 
  57.      * it has been since this task got the token. 
  58.      * Look at mm/thrash.c 
  59.      */  
  60.     unsigned int faultstamp;  
  61.     unsigned int token_priority;  
  62.     unsigned int last_interval;  
  63.   
  64.     /* How many tasks sharing this mm are OOM_DISABLE */  
  65.     atomic_t oom_disable_count;  
  66.   
  67.     unsigned long flags; /* Must use atomic bitops to access the bits */  
  68.   
  69.     struct core_state *core_state; /* coredumping support */  
  70. #ifdef CONFIG_AIO  
  71.     spinlock_t      ioctx_lock;  
  72.     struct hlist_head   ioctx_list;  
  73. #endif  
  74. #ifdef CONFIG_MM_OWNER  
  75.     /* 
  76.      * "owner" points to a task that is regarded as the canonical 
  77.      * user/owner of this mm. All of the following must be true in 
  78.      * order for it to be changed: 
  79.      * 
  80.      * current == mm->owner 
  81.      * current->mm != mm 
  82.      * new_owner->mm == mm 
  83.      * new_owner->alloc_lock is held 
  84.      */  
  85.     struct task_struct __rcu *owner;  
  86. #endif  
  87.   
  88. #ifdef CONFIG_PROC_FS  
  89.     /* store ref to file /proc/<pid>/exe symlink points to */  
  90.     struct file *exe_file;  
  91.     unsigned long num_exe_file_vmas;  
  92. #endif  
  93. #ifdef CONFIG_MMU_NOTIFIER  
  94.     struct mmu_notifier_mm *mmu_notifier_mm;  
  95. #endif  
  96. #ifdef CONFIG_TRANSPARENT_HUGEPAGE  
  97.     pgtable_t pmd_huge_pte; /* protected by page_table_lock */  
  98. #endif  
  99. };  


这个结构在代码中,经常是mm的名字。

他比vm_area_struct结构更上层,每个进程只有一个mm_struct结构在task_struct中由指针。因此mm_struct结构会更具总结型。

所以虚拟空间的联系图为:

 

 

总结一下:

讲解了,

1.PGD,PTE,

2.node--->zone---->page

3. mm_struct------>vm_area_struct

 

虚拟地址和物理地址 通过 PGD,PTE联系起来


//include/asm-generic/memory_model.h

#define PHYS_PFN_OFFSET    (PHYS_OFFSET >> PAGE_SHIFT)
#define ARCH_PFN_OFFSET        PHYS_PFN_OFFSET
#define pfn_to_page __pfn_to_page
#define __pfn_to_page(pfn)    (mem_map + ((pfn) - ARCH_PFN_OFFSET)) //根据页号转换为表述它的page的指针 mem_map是page* (前进多少个page*呢?当前页号相对于第一页的offset) 显然pfn是phy的而不是vir的pfn比较好计算 pte(val)>>PAGE_SHIFT 而PAGE_SHIFT是12, 一页是4k

找这个宏费了不少时间,鄙视自己,接下来该描述arm页表和linux页表了,网上很少有人介绍这一部分!
先来个内核里的图

 * This leads to the page tables having the following layout:
 *
 *    pgd             pte
 * |        |
 * +--------+ +0
 * |        |-----> +------------+ +0
 * +- - - - + +4    |  h/w pt 0  |
 * |        |-----> +------------+ +1024
 * +--------+ +8    |  h/w pt 1  |
 * |        |       +------------+ +2048
 * +- - - - +       | Linux pt 0 |
 * |        |       +------------+ +3072
 * +--------+       | Linux pt 1 |
 * |        |       +------------+ +4096

typedef unsigned long pte_t;
typedef unsigned long pmd_t;
typedef unsigned long pgd_t[2];
typedef unsigned long pgprot_t;

#define pte_val(x)      (x)
#define pmd_val(x)      (x)
#define pgd_val(x)    ((x)[0])
#define pgprot_val(x)   (x)

#define __pte(x)        (x)
#define __pmd(x)        (x)
#define __pgprot(x)     (x)

arm是两级页表pgd pte, linux是三级的pgd pmd pte, arm把pgd和pmd搞成一级了
arm是2048-512-4096

vir--->phy
根据cp15协处理器的c2找到pgd的基地址,利用虚拟地址的高11位找到pgd的入口 //pgd_offset
把*pgd加上虚拟地址次9位,找到pte的入口//pte_offset_map不过这个宏不太好理解,
把*pte加上虚拟地址最后的12位 就得到存有物理地址的这个地址了

/* Find an entry in the second-level page table.. */
#define pmd_offset(dir, addr)    ((pmd_t *)(dir)) //这个宏就能看出是两极页表
举个例子
if (!mm)
        mm = &init_mm; //mm是mm_struct, 是进程task_struct里的虚拟内存空间表述符,init_mm是内核进程idle的,就是进程号为0的那个!

    printk(KERN_ALERT "pgd = %p\n", mm->pgd);
    pgd = pgd_offset(mm, addr); //mm->pgd是swap_pg_dir 汇编里定义的 一般就是0xc0004000
    printk(KERN_ALERT "[%08lx] *pgd=%08lx", addr, pgd_val(*pgd));

    do {
        pmd_t *pmd;
        pte_t *pte;

        if (pgd_none(*pgd))
            break;

        if (pgd_bad(*pgd)) {
            printk("(bad)");
            break;
        }

        pmd = pmd_offset(pgd, addr); //pmd_offset只是把pgd强制转换为了pmd而已
        if (PTRS_PER_PMD != 1)
            printk(", *pmd=%08lx", pmd_val(*pmd));

        if (pmd_none(*pmd))
            break;

        if (pmd_bad(*pmd)) {
            printk("(bad)");
            break;
        }

        /* We must not map this if we have highmem enabled */
        if (PageHighMem(pfn_to_page(pmd_val(*pmd) >> PAGE_SHIFT)))
            break;

        pte = pte_offset_map(pmd, addr);//这个结合上面的图应该可以理解,*pte+512*sizeof(void*)就找到pte的入口了  注意看图
        printk(", *pte=%08lx", pte_val(*pte));
        printk(", *ppte=%08lx", pte_val(pte[-PTRS_PER_PTE]));
        pte_unmap(pte);
    } while(0);

  h/w pt 这个还有待研究

启动涉及到一个解压与定位的过程,对于x86体系结构而言,系统被加载到0x100000的地方,那么swapper_pg_dir的值是什么呢?我们知道swapper_pg_dir是一个很重要的东西,它是所有进程内核空间的页表的模板,而且在涉及到896M以上的内存分配时,swapper_pg_dir也是一个同步的根,这些内存分配包括vmalloc区,高端永久区,高端临时区等。这里需要说明的是,swapper_pg_dir这个东西其实就是一个页目录的指针,页目录指针在x86中是要被加载到cr3寄存器的,每个进程都有一个页目录指针,这个指针指示这个进程的内存映射信息,每当切换到一个进程时,该进程的页目录指针就被加载到了cr3,然后直到切换到别的进程的时候才更改,既然swapper_pg_dir是一个页目录指针,那么这个指针是被哪个进程用的呢?现代操作系统的含义指示了进程间内存隔离,那么一个页目录指针只能被一个进程使用,那么到底是哪个特定的进程使用了swapper_pg_dir指针呢?遗憾的是,答案是没有任何用户进程使用swapper_pg_dir作为页目录指针,swapper_pg_dir只是在内核初始化的时候被载入到cr3指示内存映射信息,之后在init进程启动后就成了idle内核线程的页目录指针了,/sbin/init由一个叫做init的内核线程exec而成,而init内核线程是原始的内核也就是后来的idle线程do_fork而成的,而在do_fork中会为新生的进程重启分配一个页目录指针,由此可见swapper_pg_dir只是在idle和内核线程中被使用,可是它的作用却不只是为idle进程指示内存映射信息,更多的,它作为一个内核空间的内存映射模板而存在,在linux中,任何进程在内核空间就不分彼此了,所有的进程都会公用一份内核空间的内存映射,因此,内核空间是所有进程共享的,每当一个新的进程建立的时候,都会将swapper_pg_dir的768项以后的信息全部复制到新进程页目录的768项以后,代表内核空间。另外在操作3G+896M以上的虚拟内存时,只会更改swapper_pg_dir的映射信息,当别的进程访问到这些页面的时候会发生缺页,在缺页处理中会与swapper_pg_dir同步。

了解到swapper_pg_dir的意义与实际作用,我们来看一下它的初始化吧,首先要看它的定义,在arch/i386/kernel/head.S中:

.org 0x1000

ENTRY(swapper_pg_dir)

.long 0x00102007 //第一个页目录项,指示前4M的页面映射信息,0x00102000是前4M页表所在的物理页,而0x00000007是访问控制权限,二者相加构成一个页表,以下同义

.long 0x00103007 //第二个页目录项,指示4-8M的页面映射信息

.fill BOOT_USER_PGD_PTRS-2,4,0 //填充到内核边界

.long 0x00102007 //第768个页目录项,指示前4M的页面映射信息

.long 0x00103007 //第769个页目录项,指示第4-8M的页面映射信息

...

org 0x1000说明了让系统将swapper_pg_dir加载到地址0x1000处,可是内核最终搬到了0x100000处,那么swapper_pg_dir也就到了0x101000处,现在物理地址已经搞定了,那么最终进入保护模式并且启动分页时,swapper_pg_dir的虚拟地址会在哪里呢?我们看一下内核加载到了哪里然后加上0x1000就是swapper_pg_dir加载到的虚拟地址了,在vmlinux.lds中可以看出内核被加载到了0xc0100000处,于是swapper_pg_dir加载的虚拟地址就是0xc0101000,在初始化的时候,内核将0到8M的物理内存分别映射到了虚拟地址的0到8M和3G到3G+8M两个地方,而且过了初始化阶段到了最终的稳定页表,也同样有前XM的物理内存一一映射到虚拟内存的3G+XM的地方,于是swapper_pg_dir的虚拟地址就是0xc0101000,我们可以打印出swapper_pg_dir看看到底是多少,不幸的是,swapper_pg_dir并不从内核导出,那么怎么办呢?难道非要将打印信息加入内核启动函数然后从新编译一遍内核吗?其实不用,虽然swapper_pg_dir没有被导出,可是init_mm被导出了啊,我们知道init_mm就是内核启动时也就是idle进程的mm_struct,其中一个字段是pgd,就是swapper_pg_dir,我们可以打印init_mm->pgd的值,看看是多少。

事情到此还没有结束,如果你真的写了一个模块,并且打印init_mm.pdg的话,发现可能它并不是期望的0xc0101000,怎么回事呢?不要急, 遇到这种情况比遇到内核莫名其妙的down掉要好处理的多,再说这并不影响我们的生活,即使你最终没有弄清楚这是怎么一回事,那么也不会有什么损失的。这 种情况好解决的原因还有就是事情发生在内核初始化的阶段,也就是说swapper_pg_dir的初始化在内核初始化阶段,并且以后也不会变化,可能它的 内容会变,但是其本身的位置是不会变化的,因此,几乎不用调试,光看代码就可以解决问题,我们搜索一下近来的Changelog,发现在2.6.6中将 swapper_pg_dir从原来的固定的.org 0x1000的位置移到了BSS段当中了,因为bss段仅仅拥有占位符而不占用映像静态空间,它的真正数据并没有初始化,因此交给操作系统初始化就可以 了,既然没有初始化的数据,那么就没有必要在静态的映像中占据空间,而是让操作系统将其载入内存时将bss清零即可,linux内核本身就是操作系统内 核,因此它自己负责在启动的时候将bss段清零,因为初始化的时候,临时页表也就需要两个页目录用来映射物理内存的0到8M,这两个页目录很简单,一点不 复杂,没有必要写死到内核映像从而占据着3个页面的空间,因此放到bss段中就可以节省3页面的空间,然后在内核启动过程中再手动初始化那两个页目录的 值,这样做十分有意义。那么bss被加载到哪里就决定了swapper_pg_dir被加载到了哪里,那么bss到底在哪呢?从arch/i386 /kernel/vmlinux.lds.S中可以大致知道答案,如果想知道更加确切的,可以参考/boot/System.map文件,然后可以再打印 一下init_mm.pgd的值,看看是不是在bss里面,其实都不用打印,在System.map里面就有swapper_pg_dir的值,该值在 2.6.6之后的内核肯定在bss中,之前的肯定是0xc0101000。最后我们看一下2.6.6以后的swapper_pg_dir的定义: 
.section ".bss.page_aligned","w" 
ENTRY(swapper_pg_dir) 
        .fill 1024,4,0


ARM MMU只支持两级页表地址转换,也就是采用三级分页映射,能够满足32bitCPU的存储管理需求



ARM支持的页大小有几种 - 1M, 64K, 4K, 1K。在linux kernel中,ARM采用了4K大小的页,4K大小的页决定了虚拟地址的低12bit留作偏移地址。从上图可以看出,页全局目录索引有效位数是12bit,二级索引有效位数是8bit,页内偏移量为12bit。

根据ARM的硬件分页机制,我们得出第一级全局页目录有4096项,第二级为256项,这样第二级可以有很多位可以被硬件使用。

在arm linux实现上,针对ARM的硬件分页机制做了些微小的调整。第一级目录保留了2048项,每项占用8 bytes(换句话说,是两个硬件指针指向二级页表);第二级则把两个硬件PTE表连续放在一起,在这两个PTE表后面则保存相应的Linux状态信息,因此二级表项实际上有512项(每个表256项,两个则为512项)。这样每个逻辑PTE表刚好占用一个page。

ARM linux页表layout如下:


在arch/arm/include/asm/pgtable.h中,可以看到PTRS_PER_PTE和PTRS_PER_PTE的定义

[cpp]  view plain copy
  1. #define PTRS_PER_PTE        512  
  2. #define PTRS_PER_PMD        1  
  3. #define PTRS_PER_PGD        2048  

由于PGD有2048项,每项占用8个字节,总计需要4*4K,也就是说ARM linux的PGD实际上占用了四个连续物理页框。


以一个例子的形式讲解逻辑地址到物理地址的转换:

某虚拟存储器的用户编程空间共32个页面,每页为1KB,内存为16KB。假定某时刻一用户页表中已调入内存的页面的页号和物理块号的对照表如下:

页号

物理块号

0

3

1

7

2

11

3

8

则逻辑地址0A5C(H)所对应的物理地址是什么?要求:写出主要计算过程。 

解题过程

       首先要知道页式存储管理的逻辑地址分为两部分:页号和页内地址。物理地址分为两部分:

关系为:逻辑地址= 页号+页内地址

                     物理地址= 块号+页内地址;

分析题:已知:用户编程空间共32个页面,2ˆ5 = 32 得知页号部分占5位,由每页为1KB”1K=210可知内页地址占10位。

内存为16KB”,2^4=16得知块号占4位。

       逻辑地址0A5CH)所对应的二进制表示形式是:0000101001011100,后十位1001011100是页内地址,

00010为为页号,页号化为十进制是2,在对照表中找到2对应的物理块号是11,11转换二进制是1011,即可求出物理地址为10111001011100,化成十六进制为2E5C;

即则逻辑地址0A5C(H)所对应的物理地址是2E5C;






  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值