zoukankan      html  css  js  c++  java
  • 内存管理(1)-buddy和slub算法

    Linux内存管理是一个很复杂的系统,也是linux的精髓之一,网络上讲解这方面的文档也很多,我把这段时间学习内存管理方面的知识记录在这里,涉及的代码太多,也没有太多仔细的去看代码,深入解算法,这篇文章就当做内存方面学习的一个入门文档,方便以后在深入学习内存管理源码的一个指导作用;


    (一)NUMA架构

      NUMA通过提供分离的存储器给各个处理器,避免当多个处理器访问同一个存储器产生的性能损失来试图解决这个问题。对于涉及到分散的数据的应用(在服务器和类似于服务器的应用中很常见),NUMA可以通过一个共享的存储器提高性能至n倍,而n大约是处理器(或者分离的存储器)的个数。

    这里写图片描述

      当然,不是所有数据都局限于一个任务,所以多个处理器可能需要同一个数据。为了处理这种情况,NUMA系统包含了附加的软件或者硬件来移动不同存储器的数据。这个操作降低了对应于这些存储器的处理器的性能,所以总体的速度提升受制于运行任务的特点。

      Linux把物理内存划分为三个层次来管理:

      1. 存储节点(Node): CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点;

      2. 管理区(Zone):每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存,内核可以使用不同的映射方式映射物理内存,通常管理区的类型可以分为:ZONE_NORMAL,ZONE_DMA,ZONE_HIGHMEM三种;内核(32位为例内核空间为1G)空间如下:

    这里写图片描述

      如果物理内存超过896 MiB就为highmem,则内核无法直接映射全部物理内存,最后的128 MiB用于其他目的,比如vmalloc就可以从这里分配不连续的内存,最珍贵的是3GB起始的16MB DMA区域直接用于外设和系统之间的数据传输;

      3. 页面(Page):内存被细分为多个页面帧, 页面是最基本的页面分配的单位;

      NUMA模式下,处理器被划分成多个”节点”(node), 每个节点被分配有的本地存储器空间。 所有节点中的处理器都可以访问全部的系统物理存储器,但是访问本节点内的存储器所需要的时间,比访问某些远程节点内的存储器所花的时间要少得多。Linux通过struct pglist_data这个结构体来描述节点;

     722 typedef struct pglist_data {
     723     struct zone node_zones[MAX_NR_ZONES];//是一个数组,包含了结点中各内存域的数据结构;
     724     struct zonelist node_zonelists[MAX_ZONELISTS];//指定备用结点及其内存域的列表,以便在当前结点没有可用空间时,在备用结点分配内存;
     725     int nr_zones;//保存结点中不同内存域的数目;
     726 #ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
     727     struct page *node_mem_map;//指向page实例数组的指针,用于描述结点的所有物理内存页,它包含了结点中所有内存域的页。 
     728 #ifdef CONFIG_MEMCG
     729     struct page_cgroup *node_page_cgroup;
     730 #endif
     731 #ifdef CONFIG_PAGE_EXTENSION
     732     struct page_ext *node_page_ext;
     733 #endif
     734 #endif
     735 #ifndef CONFIG_NO_BOOTMEM
     736     struct bootmem_data *bdata;//在系统启动期间,内存管理
    //子系统初始化之前,内核页需要使用内存(另外,还需要保留部分内存用于初始
    //化内存管理子系统)。bootmem分配器(bootmem allocator)的机制,这种
    //机制仅仅用在系统引导时,它为整个物理内存建立起一个页面位图;
     737 #endif
     738 #ifdef CONFIG_MEMORY_HOTPLUG
     ...
     750 #endif
     751     unsigned long node_start_pfn;////该NUMA结点第一个页帧的逻辑编号。系统中所有的页帧是依次编号的,每个页帧的号码都是全局唯一的(不只是结点内唯一)。
     752     unsigned long node_present_pages; /* total number of physical pages */ //结点中页帧的数目;
     753     unsigned long node_spanned_pages; /* total size of physical page  range, including holes *///该结点以页帧为单位计算的长度,包含内存空洞。                        
     755     int node_id;//全局结点ID,系统中的NUMA结点都从0开始编号;
     756     wait_queue_head_t kswapd_wait;//交换守护进程的等待队列,在将页帧换出结点时会用到。
     757     wait_queue_head_t pfmemalloc_wait;
     758     struct task_struct *kswapd; /* Protected by
     759                        mem_hotplug_begin/end() *///指向负责该结点的交换守护进程的task_struct。
     760     int kswapd_max_order;//定义需要释放的区域的长度。
     761     enum zone_type classzone_idx;
     762 #ifdef CONFIG_NUMA_BALANCING
     ...
     771 #endif
     772 } pg_data_t;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

      每个节点的内存会被分为几个块,我们称之为管理区(zone) ,一个管理区(zone)由struct zone结构体来描述;include/linux/mmzone.h;

     327 struct zone {
     331     unsigned long watermark[NR_WMARK];//当系统中可用内存很少的时候,系统进程kswapd被唤醒, 开始回收释放page, 水印这些参数(WMARK_MIN, WMARK_LOW, WMARK_HIGH)影响着这个代码的行为;
     341     long lowmem_reserve[MAX_NR_ZONES];//为了防止一些代码必须运行在低地址区域,所以事先保留一些低地址区域的内存;
     342 
     343 #ifdef CONFIG_NUMA
     344     int node;
     345 #endif
     351     unsigned int inactive_ratio;//不活动页的比例,很少使用或者大部分情况下是只读的字段;
     352 
     353     struct pglist_data  *zone_pgdat;//zone所在的节点;
     354     struct per_cpu_pageset __percpu *pageset;//每个CPU的热/冷页帧列表,有些页帧很可能在高速缓存中,可以快速访问,故称之为热的,反之为冷;
     360     unsigned long       dirty_balance_reserve;
     361 
     362 #ifndef CONFIG_SPARSEMEM
     367     unsigned long       *pageblock_flags;
     368 #endif /* CONFIG_SPARSEMEM */
     369 
     370 #ifdef CONFIG_NUMA
     374     unsigned long       min_unmapped_pages;
     375     unsigned long       min_slab_pages;
     376 #endif /* CONFIG_NUMA */
     377 
     378     /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
     379     unsigned long       zone_start_pfn;//内存域的第一个页帧;
    422     unsigned long       managed_pages;
     423     unsigned long       spanned_pages;//总页数,包含空洞;
     424     unsigned long       present_pages;//可用页数,不包涵空洞;
     426     const char      *name;//指向管理区类型名字;
     432     int         nr_migrate_reserve_block;
     433 
     434 #ifdef CONFIG_MEMORY_ISOLATION
     440     unsigned long       nr_isolate_pageblock;
     441 #endif
     442 
     443 #ifdef CONFIG_MEMORY_HOTPLUG
     444     /* see spanned/present_pages for more description */
     445     seqlock_t       span_seqlock;
     446 #endif
    472     wait_queue_head_t   *wait_table;//进程等待队列的散列表, 这些进程正在等待管理区中的某页;
     473     unsigned long       wait_table_hash_nr_entries;//等待队列散列表中的调度实体数目;
     474     unsigned long       wait_table_bits;//等待队列散列表数组大小, 值为2^order;
     475 
     476     ZONE_PADDING(_pad1_)
     477 
     478     /* Write-intensive fields used from the page allocator */
     479     spinlock_t      lock;//对zone并发访问的保护的自旋锁;
     480 
     481     /* free areas of different sizes */
     482     struct free_area    free_area[MAX_ORDER];//没个bit标识对应的page是否可以分配;
     483 
     484     /* zone flags, see below */
     485     unsigned long       flags;//zone flags, 描述当前内存的状态;
     486 
     487     ZONE_PADDING(_pad2_)
     492     spinlock_t      lru_lock;//LRU(最近最少使用算法)的自旋锁;
     493     struct lruvec       lruvec;
     494 
     495     /* Evictions & activations on the inactive file list */
     496     atomic_long_t       inactive_age;
     497 
     503     unsigned long percpu_drift_mark;
     504 
     505 #if defined CONFIG_COMPACTION || defined CONFIG_CMA
     506     /* pfn where compaction free scanner should start */
     507     unsigned long       compact_cached_free_pfn;
     508     /* pfn where async and sync compaction migration scanner should start */
     509     unsigned long       compact_cached_migrate_pfn[2];
     510 #endif
     511 
     512 #ifdef CONFIG_COMPACTION
     518     unsigned int        compact_considered;
     519     unsigned int        compact_defer_shift;
     520     int         compact_order_failed;
     521 #endif
     523 #if defined CONFIG_COMPACTION || defined CONFIG_CMA
     524     /* Set to true when the PG_migrate_skip bits should be cleared */
     525     bool            compact_blockskip_flush;
     526 #endif
     528     ZONE_PADDING(_pad3_)
     529     /* Zone statistics */
     530     atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];
     531 } ____cacheline_internodealigned_in_smp;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    页我们用struct page(include/linux/mm_types.h)来表示,这里就不贴出全部代码,这里列出几个重要的成员;

    virtual:对于如果物理内存可以直接映射内核的系统, 我们可以之间映射出虚拟地址与物理地址的管理, 但是对于需要使用高端内存区域的页, 即无法直接映射到内核的虚拟地址空间, 因此需要用virtual保存该页的虚拟地址;

    _refcount:引用计数,表示内核中引用该page的次数, 如果要操作该page, 引用计数会+1, 操作完成-1. 当该值为0时, 表示没有引用该page的位置,所以该page可以被解除映射,这往往在内存回收时是有用的;

    _mapcount:被页表映射的次数,也就是说该page同时被多少个进程共享。初始值为-1,如果只被一个进程的页表映射了,该值为0. 如果该page处于伙伴系统中,该值为PAGE_BUDDY_MAPCOUNT_VALUE(-128),内核通过判断该值是否为PAGE_BUDDY_MAPCOUNT_VALUE来确定该page是否属于伙伴系统;

    mapping: 指向与该页相关的address_space对象;

    index : 在映射的虚拟空间(vma_area)内的偏移;一个文件可能只映射一部分,假设映射了1M的空间,index指的是在1M空间内的偏移,而不是在整个文件内的偏移;

    lru :链表头,用于在各种链表上维护该页, 以便于按页将不同类别分组, 主要有3个用途: 伙伴算法(链接相同阶的伙伴), slab分配器(设置PG_slab标志), 被用户态使用或被当做页缓存使用(连入zone中相应的lru链表,供内存回收时使用);


    内存初始化

    start_kernel() -> setup_arch() -> arm_memblock_init():在系统启动过程期间, 内核使用了一个额外的简化形式的内存管理模块早期的引导内存分配器(boot memory allocator–bootmem分配器)或者memblock, 用于在启动阶段早期分配内存;

    start_kernel() -> setup_arch() -> paging_init() -> bootmem_init() : 初始化分页机制,初始化内存管理;

    start_kernel() -> build_all_zonelists() : 建立并初始化结点和内存域的数据结构;

    start_kernel() -> mm_init():建立了内核的内存分配器,其中mem_init调用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器;

    start_kernel() -> kmem_cache_init_late() : 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器(slab, slob, slub)都会定义此函数;

    start_kernel() -> kmemleak_init() : Kmemleak工作于内核态Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在/sys/kernel/debug/kmemleak中, Kmemcheck能够帮助定位大多数内存错误的上下文;

    start_kernel() -> setup_per_cpu_pageset() : 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中.


    (二)buddy内存分配算法

      Linux采用著名的伙伴系统(buddy system)算法来解决外碎片问题。把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续的页框。对1024个页框的最大请求对应着4MB大小的连续内存块。每个块的第一个页框的物理地址是该块大小的整数倍。例如,大小为16个页框的块,其起始地址是16*2^12(4k常规页的大小)的倍数。内核试图把大小为b的一对空闲伙伴块合并为一个大小为2b的单独块。

      满足以下条件的两个块称为伙伴:

    1. 两个块具有相同的大小,记作b;
    2. 他们的物理地址是连续的;
    3. 第一块的第一个页框的物理地址是2*b*2^12的倍数;
    
    • 1
    • 2
    • 3
    • 4

      该算法是迭代的,如果它成功合并所释放的块,它会试图合并2b的块,以再次试图形成更大的块;

    这里写图片描述

      struct zone中的struct free_area则是用来描述该管理区伙伴系统的空闲内存块的;管理区描述符中free_area数组的第k个元素,它标识所有大小为2^k的空闲块。这个元素的free_list字段是双向循环链表的头,这个双向循环链表集中了大小为2^k页的空闲块对应的页描述符。

      92 struct free_area {
      93     struct list_head    free_list[MIGRATE_TYPES];
      94     unsigned long       nr_free;//指定了大小为2^k页的空闲块的个数;
      95 };
    • 1
    • 2
    • 3
    • 4

      分配出去的页面可分为三种类型:

      不可移动页(Non-movable pages):这类页在内存当中有固定的位置,不能移动。内核的核心分配的内存大多属于这种类型;

      可回收页(Reclaimable pages):这类页不能直接移动,但可以删除,其内容页可以从其他地方重新生成,例如,映射自文件的数据属于这种类型,针对这种页,内核有专门的页面回收处理;

      可移动页:这类页可以随意移动,用户空间应用程序所用到的页属于该类别。它们通过页表来映射,如果他们复制到新的位置,页表项也会相应的更新,应用程序不会注意到任何改变。

    代码分析

      paging_init(mdesc) -> bootmem_init() -> zone_sizes_init() -> free_area_init_node() : 分页机制,内存域,节点等初始化;

    内存分配:

      alloc_pages(mask, order):分配2order页并返回一个struct page的实例,表示分配的内存块的起始页;

      alloc_page(mask): 分配一页,order为0;

      get_zeroed_page(mask):分配一页并返回一个page实例,页对应的内存填充0(所有其他函数,分配之后页的内容是未定义的);

      __get_free_pages(mask, order):返回分配内存块的虚拟地址,而不是page实例;

      get_dma_pages(gfp_mask, order):用来获得适用于DMA的页.

    gfp_mask标志:

    #define ___GFP_DMA              0x01u
    #define ___GFP_HIGHMEM          0x02u
    #define ___GFP_DMA32            0x04u
    #define ___GFP_MOVABLE          0x08u  //页是可移动的
    #define ___GFP_RECLAIMABLE      0x10u  //页是可回收的
    #define ___GFP_HIGH             0x20u  //应该访问紧急分配池
    #define ___GFP_IO               0x40u  //可以启动物理IO
    #define ___GFP_FS               0x80u  //可以调用底层文件系统?
    #define ___GFP_COLD             0x100u //需要非缓存的冷页
    #define ___GFP_NOWARN           0x200u //禁止分配失败警告
    #define ___GFP_REPEAT           0x400u //重试分配,可能失败
    #define ___GFP_NOFAIL           0x800u  //一直重试,不会失败
    #define ___GFP_NORETRY          0x1000u //不重试,可能失败
    #define ___GFP_MEMALLOC         0x2000u //使用紧急分配链表
    #define ___GFP_COMP             0x4000u //增加复合页元数据
    #define ___GFP_ZERO             0x8000u //成功则返回填充字节0的页
    #define ___GFP_NOMEMALLOC       0x10000u //不使用紧急分配链表
    #define ___GFP_HARDWALL         0x20000u //只允许在进程允许运行的CPU所关联的结点分配内存
    #define ___GFP_THISNODE         0x40000u //没有备用结点,没有策略
    #define ___GFP_ATOMIC           0x80000u //用于原子分配,在任何情况下都不能中断
    #define ___GFP_ACCOUNT          0x100000u
    #define ___GFP_NOTRACK          0x200000u
    #define ___GFP_DIRECT_RECLAIM   0x400000u
    #define ___GFP_OTHER_NODE       0x800000u
    #define ___GFP_WRITE            0x1000000u
    #define ___GFP_KSWAPD_RECLAIM   0x2000000u
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    也有可能是多个mask组合,如常用到的:

    #define GFP_ATOMIC      (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
    #define GFP_KERNEL      (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
    • 1
    • 2

    GFP_ATOMIC :用于原子分配,在任何情况下都不能中断, 可能使用紧急分配链表中的内存, 这个标志用在中断处理程序, 下半部, 持有自旋锁以及其他不能睡眠的地方;

    GFP_KERNEL:这是一种常规的分配方式, 可能会阻塞. 这个标志在睡眠安全时用在进程的长下文代码中. 为了获取调用者所需的内存, 内核会尽力而为. 这个标志应该是首选标志;

    GFP_USER:这是一种常规的分配方式, 可能会阻塞. 这个标志用于为用户空间进程分配内存时使用;

    上面几个分配函数,最终都会调用到allloc_pages()来分配页:

    349 static inline struct page *
    350 alloc_pages(gfp_t gfp_mask, unsigned int order)
    351 {
    352     return alloc_pages_current(gfp_mask, order);
    353 }
    
    
    2063 struct page *alloc_pages_current(gfp_t gfp, unsigned order)
    2064 {
             //并传入相应节点的备用域链表zonelist;
    2082         page = __alloc_pages_nodemask(gfp, order,
    2083                 policy_zonelist(gfp, pol, numa_node_id()),
    2084                 policy_nodemask(gfp, pol));
    
    2090 }
    2091 EXPORT_SYMBOL(alloc_pages_current);
    
    
    2944 struct page *
    2945 __alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
    2946             struct zonelist *zonelist, nodemask_t *nodemask)
    2947 {
    2948 #ifdef CONFIG_ZONE_MOVABLE_CMA
    2949     enum zone_type high_zoneidx = gfp_zone(gfp_mask & ~__GFP_MOVABLE);/*根据gfp_mask确定分配页所处的管理区*/  
    2950 #else
    2951     enum zone_type high_zoneidx = gfp_zone(gfp_mask);
    2952 #endif
    2953     struct zone *preferred_zone;
    2954     struct zoneref *preferred_zoneref;
    2955     struct page *page = NULL;
    2956     int migratetype = gfpflags_to_migratetype(gfp_mask);/*根据gfp_mask得到迁移类分配页的型*/
    2957     unsigned int cpuset_mems_cookie;
    2958 #if defined(CONFIG_DMAUSER_PAGES) || defined(CONFIG_ZONE_MOVABLE_CMA)
    2959     int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET;
    2960 #else
    2961     int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET|ALLOC_FAIR;
    2962 #endif
    
    3008 
    3009 retry_cpuset:
    3010     cpuset_mems_cookie = read_mems_allowed_begin();
    3011 
    3012     /* The preferred zone is used for statistics later */
    3013     preferred_zoneref = first_zones_zonelist(zonelist, high_zoneidx,
    3014                 nodemask ? : &cpuset_current_mems_allowed,
    3015                 &preferred_zone);/*从zonelist中找到zoneidx管理区*/  
    3016     if (!preferred_zone)
    3017         goto out;
    3018     classzone_idx = zonelist_zone_idx(preferred_zoneref);
    3019 
    3020     /* First allocation attempt */
    3021     page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order,
    3022             zonelist, high_zoneidx, alloc_flags,
    3023             preferred_zone, classzone_idx, migratetype);//第一次分配
    3024     if (unlikely(!page)) {
    3025         /*
    3026          * Runtime PM, block IO and its error handling path
    3027          * can deadlock because I/O on the device might not
    3028          * complete.
    3029          */
    3030         if (IS_ENABLED(CONFIG_ZONE_MOVABLE_CMA))
    3031             high_zoneidx = gfp_zone(gfp_mask);
    3032 
    3033         gfp_mask = memalloc_noio_flags(gfp_mask);
             /*通过一条低速路径来进行第二次分配,包括唤醒页换出守护进程等等*/
    3034         page = __alloc_pages_slowpath(gfp_mask, order,
    3035                 zonelist, high_zoneidx, nodemask,
    3036                 preferred_zone, classzone_idx, migratetype);
    3037     }
    3038  
    3083     return page;
    3084 }
    3085 EXPORT_SYMBOL(__alloc_pages_nodemask);
    
    2083 static struct page *
    2084 get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order,
    2085         struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
    2086         struct zone *preferred_zone, int classzone_idx, int migratetype)
    2087 {
        /*从认定的管理区开始遍历,直到找到一个拥有足够空间的管理区, 
          例如,如果high_zoneidx对应的ZONE_HIGHMEM,则遍历顺序为HIGHMEM-->NORMAL-->DMA, 
          如果high_zoneidx对应ZONE_NORMAL,则遍历顺序为NORMAL-->DMA*/ 
    2106     for_each_zone_zonelist_nodemask(zone, z, zonelist,
    2107                         high_zoneidx, nodemask) {
    2108         unsigned long mark;
    2109 
    2110         if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
    2111             !zlc_zone_worth_trying(zonelist, z, allowednodes))
    2112                 continue;
    2113         if (cpusets_enabled() &&
    2114             (alloc_flags & ALLOC_CPUSET) && 
             /*检查给定的内存域是否属于该进程允许运行的CPU*/
    2115             !cpuset_zone_allowed_softwall(zone, gfp_mask))
    2116                 continue;
    2117         /*
    2118          * Distribute pages in proportion to the individual
    2119          * zone size to ensure fair page aging.  The zone a
    2120          * page was allocated in should have no effect on the
    2121          * time the page has in memory before being reclaimed.
    2122          */
    2123         if (alloc_flags & ALLOC_FAIR) {
    2124             if (!zone_local(preferred_zone, zone))
    2125                 break;
    2126             if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) {
    2127                 nr_fair_skipped++;
    2128                 continue;
    2129             }
    2130         }
    2157         if (consider_zone_dirty && !zone_dirty_ok(zone))
    2158             continue;
    2159 
    2160         mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
             /*如果管理区的水位线处于正常水平,则在该管理区进行分配*/
    2161         if (!zone_watermark_ok(zone, order, mark,
    2162                        classzone_idx, alloc_flags)) {    
    2163             int ret;
    2164 
    2165             /* Checked here to keep the fast path fast */
    2166             BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
    2167             if (alloc_flags & ALLOC_NO_WATERMARKS)
    2168                 goto try_this_zone;
    2169 
    2170             if (IS_ENABLED(CONFIG_NUMA) &&
    2171                     !did_zlc_setup && nr_online_nodes > 1) {
    2172                 /*
    2173                  * we do zlc_setup if there are multiple nodes
    2174                  * and before considering the first zone allowed
    2175                  * by the cpuset.
    2176                  */
    2177                 allowednodes = zlc_setup(zonelist, alloc_flags);
    2178                 zlc_active = 1;
    2179                 did_zlc_setup = 1;
    2180             }
    2181 
    2182             if (zone_reclaim_mode == 0 ||
    2183                 !zone_allows_reclaim(preferred_zone, zone))
    2184                 goto this_zone_full;
    2185 
    2186             /*
    2187              * As we may have just activated ZLC, check if the first
    2188              * eligible zone has failed zone_reclaim recently.
    2189              */
    2190             if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
    2191                 !zlc_zone_worth_trying(zonelist, z, allowednodes))
    2192                 continue;
    2193         /*针对NUMA架构的申请页面回收*/  
    2194             ret = zone_reclaim(zone, gfp_mask, order);
    2195             switch (ret) {
    2196             case ZONE_RECLAIM_NOSCAN:/*没有进行回收*/
    2197                 /* did not scan */
    2198                 continue;
    2199             case ZONE_RECLAIM_FULL:/*没有找到可回收的页面*/
    2200                 /* scanned but unreclaimable */
    2201                 continue;
    2202             default:
    2203                 /* did we reclaim enough */
    2204                 if (zone_watermark_ok(zone, order, mark,
    2205                         classzone_idx, alloc_flags))
    2206                     goto try_this_zone;
    2207 
    2217                 if (((alloc_flags & ALLOC_WMARK_MASK) == ALLOC_WMARK_MIN) ||
    2218                     ret == ZONE_RECLAIM_SOME)
    2219                     goto this_zone_full;
    2220 
    2221                 continue;
    2222             }
    2223         }
    2224 
    2225 try_this_zone:/*分配2^order个页*/ 
    2226         page = buffered_rmqueue(preferred_zone, zone, order,
    2227                         gfp_mask, migratetype);
    2228         if (page)
    2229             break;
    2230 this_zone_full:
    2231         if (IS_ENABLED(CONFIG_NUMA) && zlc_active)
    2232             zlc_mark_zone_full(zonelist, z);
    2233     }
    2234 
    2235     if (page) {
    2236         /*
    2237          * page->pfmemalloc is set when ALLOC_NO_WATERMARKS was
    2238          * necessary to allocate the page. The expectation is
    2239          * that the caller is taking steps that will free more
    2240          * memory. The caller should avoid the page being used
    2241          * for !PFMEMALLOC purposes.
    2242          */
    2243         page->pfmemalloc = !!(alloc_flags & ALLOC_NO_WATERMARKS);
    2244         return page;
    2245     }
    2255     if (alloc_flags & ALLOC_FAIR) {
    2256         alloc_flags &= ~ALLOC_FAIR;
    2257         if (nr_fair_skipped) {
    2258             zonelist_rescan = true;
    2259             reset_alloc_batches(preferred_zone);
    2260         }
    2261         if (nr_online_nodes > 1)
    2262             zonelist_rescan = true;
    2263     }
    2264 
    2265     if (unlikely(IS_ENABLED(CONFIG_NUMA) && zlc_active)) {
    2266         /* Disable zlc cache for second zonelist scan */
    2267         zlc_active = 0;
    2268         zonelist_rescan = true;
    2269     }
    2270 
    2271     if (zonelist_rescan)
    2272         goto zonelist_scan;
    2273 
    2274     return NULL;
    2275 }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210

      从指定的管理区开始按照zonelist中定义的顺序来遍历管理区,如果该管理区的水位线正常,则调用buffered_rmqueue()在该管理区中分配,如果管理区的水位线过低,则在NUMA架构下会申请页面回收;

    1692 static inline
    1693 struct page *buffered_rmqueue(struct zone *preferred_zone,
    1694             struct zone *zone, unsigned int order,
    1695             gfp_t gfp_flags, int migratetype)
    1696 {
    1697     unsigned long flags;
    1698     struct page *page;
    1699     bool cold = ((gfp_flags & __GFP_COLD) != 0);
    1700 
    1701 again:
    1702     if (likely(order == 0)) {/*order为0,即要求分配一个页*/
    1703         struct per_cpu_pages *pcp;
    1704         struct list_head *list;
    1705 
    1706         local_irq_save(flags);
    1707         pcp = &this_cpu_ptr(zone->pageset)->pcp;/*获取本地CPU对应的pcp*/
    1708         list = &pcp->lists[migratetype];/*获取和迁移类型对应的链表*/
    1709         if (list_empty(list)) {/*如果链表为空,则表示没有可分配的页,需要从伙伴系统中分配2^batch个页给list*/
    1710             pcp->count += rmqueue_bulk(zone, 0,
    1711                     pcp->batch, list,
    1712                     migratetype, cold);
    1713             if (unlikely(list_empty(list)))
    1714                 goto failed;
    1715         }
    1716 
    1717         if (cold)/*如果是需要冷页,则从链表的尾部获取*/ 
    1718             page = list_entry(list->prev, struct page, lru);
    1719         else /*如果是需要热页,则从链表的头部获取*/  
    1720             page = list_entry(list->next, struct page, lru);
    1721 
    1722         list_del(&page->lru);
    1723         pcp->count--;
    1724     } else {
    1725         if (unlikely(gfp_flags & __GFP_NOFAIL)) {
    1736             WARN_ON_ONCE(order > 1);
    1737         }
    1738         spin_lock_irqsave(&zone->lock, flags);
    1739         page = __rmqueue(zone, order, migratetype); /*从管理区的伙伴系统中选择合适的内存块进行分配*/ 
            /*连续的页框分配,通过调用__rmqueue()来完成分配,__rmqueue() -> __rmqueue_smallest()*/
    1740         spin_unlock(&zone->lock);
    1741         if (!page)
    1742             goto failed;
    1743         __mod_zone_freepage_state(zone, -(1 << order),
    1744                       get_freepage_migratetype(page));
    1745     }
    1746 
    1759     return page;
    1760 
    1761 failed:
    1762     local_irq_restore(flags);
    1763     return NULL;
    1764 }
    
    
    1068 static inline
    1069 struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
    1070                         int migratetype)
    1071 {
    1072     unsigned int current_order;
    1073     struct free_area *area;
    1074     struct page *page;
    1075 
    1076     /* Find a page of the appropriate size in the preferred list */
    1077     for (current_order = order; current_order < MAX_ORDER; ++current_order) {
    1078         area = &(zone->free_area[current_order]);/*获取和现在的阶数对应的free_area*/ 
    1079         if (list_empty(&area->free_list[migratetype]))
    1080             continue;
    1081 
    1082         page = list_entry(area->free_list[migratetype].next,
    1083                             struct page, lru); /*得到满足要求的页块中的第一个页描述符*/
    1084         list_del(&page->lru);
    1085         rmv_page_order(page);
    1086         area->nr_free--;
    1087         expand(zone, page, order, current_order, area, migratetype);/*进行拆分(在current_order > order的情况下)*/  
    1088         set_freepage_migratetype(page, migratetype);
    1089         return page;
    1090     }
    1091 
    1092     return NULL;
    1093 }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80


    (三)slub内存分配算法

      针对一些经常分配并释放的对象,如进程描述符等,这些对象的大小一般比较小,如果用buddy system来分配会造成大量的内存碎片,而且处理速度也太慢;slab分配器是基于对象进行管理的,相同类型的对象归为一类,每当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统。slab分配对象时,会使用最近释放的对象内存块,因此其驻留在CPU高速缓存的概率较高。

      slub把内存分组管理,每个组分别包含2^3、2^4、…2^11个字节,在4K页大小的默认情况下,另外还有两个特殊的组,分别是96B和192B,共11组。之所以这样分配是因为如果申请2^12B大小的内存,就可以使用伙伴系统提供的接口直接申请一个完整的页面即可。

    下图为各个结构体关系图:

    这里写图片描述

      通过struct kmem_cache就表示这样一个组;成员kmem_cache_node就指向了object列表;物理页按照对象(object)大小组织成单向链表,对象大小时候objsize指定的。例如16字节的对象大小,每个object就是16字节,每个object包含指向下一个object的指针,该指针的位置是每个object的起始地址+offset。

    void*指向的是下一个空闲的object的首地址,这样object就连成了一个单链表。向slub系统申请内存块(object)时:slub系统把内存块当成object看待;

      系统定义了如下这样一个数组,每个kmem_cache 结构分配特定的内存大小:
    struct kmem_cache kmalloc_caches[PAGE_SHIFT]

     59 /*
     60  * Slab cache management.
     61  */
     62 struct kmem_cache {
     63     struct kmem_cache_cpu __percpu *cpu_slab;//每个CPU对应的cpu_slab;
     64     /* Used for retriving partial slabs etc */
     65     unsigned long flags;
     66     unsigned long min_partial;//每个node节点中部分空缓冲区数量不能低于这个值;如果小于这个值,空闲slab缓冲区不能够进行释放
     67     int size;       /* The size of an object including meta data */
     68     int object_size;    /* The size of an object without meta data */
     69     int offset; //空闲指针偏移量;/* Free pointer offset. */
     70     int cpu_partial;    /* Number of per cpu partial objects to keep around */ //表示的是空闲对象数量,小于的情况下要去对应的node节点部分空链表中获取若干个部分空slab;
    
     //kmem_cache_order_objects 表示保存slab缓冲区的需要的页框数量的
     //order值和objects数量的值,通过这个计算出需要多少页框,oo是默认
     //值,max是最大值,min在分配失败的时候使用;
     71     struct kmem_cache_order_objects oo;
     72 
     73     /* Allocation and freeing of slabs */
     74     struct kmem_cache_order_objects max;
     75     struct kmem_cache_order_objects min;
     76     gfp_t allocflags;   /* gfp flags to use on each alloc */
     77     int refcount;       /* Refcount for slab cache destroy */
     78     void (*ctor)(void *);//该缓存区的构造函数,初始化的时候调用;并设置该cpu的当前使用的缓冲区;
     79     int inuse;      /* Offset to metadata */
     80     int align;      /* Alignment */
     81     int reserved;       /* Reserved bytes at the end of slabs */
     82     const char *name;   /* Name (only for display!) */
     83     struct list_head list;  /* List of slab caches *///所有kmem_cache结构都会链入这个链表;
     84 #ifdef CONFIG_SYSFS
     85     struct kobject kobj;    /* For sysfs */
     86 #endif
     87 #ifdef CONFIG_MEMCG_KMEM
     88     struct memcg_cache_params *memcg_params;
     89     int max_attr_size; /* for propagation, maximum size of a stored attr */
     90 #ifdef CONFIG_SYSFS
     91     struct kset *memcg_kset;
     92 #endif
     93 #endif
     94 
     95 #ifdef CONFIG_NUMA
     96     /*
     97      * Defragmentation by allocating from a remote node.
     98      */
     99     int remote_node_defrag_ratio;//numa框架,该值越小,越倾向于在本结点分配对象;
    100 #endif
            //此高速缓存的slab链表,每个numa节点有一个,有可能该高速缓存有些slab处于其他几点上;
    101     struct kmem_cache_node *node[MAX_NUMNODES];
    102 };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
     40 struct kmem_cache_cpu {
     41     void **freelist;//下一个空闲对象地址/* Pointer to next available object */
     42     unsigned long tid;  /* Globally unique transaction id *///主要考虑并发;
     43     struct page *page;  /* The slab from which we are allocating */
             //cpu当前使用的slab缓冲区描述符,freelist会指向此slab的下一个空闲对象;
     44     struct page *partial;   /* Partially allocated frozen slabs */
            //cpu部分空slab链表,放到cpu的部分空slab链表中的slab会被冻结,而放入node中的部分空slab链表则解冻,解冻标志放在slab缓冲区描述符中;
     45 #ifdef CONFIG_SLUB_STATS
     46     unsigned stat[NR_SLUB_STAT_ITEMS];
     47 #endif
     48 };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    315  * The slab lists for all objects.
    316  */ 
    317 struct kmem_cache_node {
    318     spinlock_t list_lock;
    319     
    320 #ifdef CONFIG_SLAB
    ......
    331 #endif
    332 
    333 #ifdef CONFIG_SLUB
    334     unsigned long nr_partial;
    335     struct list_head partial;//只保留了部分空slab缓冲区;
    336 #ifdef CONFIG_SLUB_DEBUG
    337     atomic_long_t nr_slabs;
    338     atomic_long_t total_objects;
    339     struct list_head full;
    340 #endif
    341 #endif
    342 
    343 };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    //struct page 
    //物理内存被划分成固定大小的块,称为页帧,kernel会为每一个页帧都创建struct page管理结构,保存在全局数组mem_map中。
    include/linux/mm_types.h
    44 struct page {
    
    110                     struct { /* SLUB */
    111                         unsigned inuse:16;
    112                         unsigned objects:15;
    113                         unsigned frozen:1;
    114                     };
    
    218 };
    
    //inuse表示page内有多少个对象在被使用,objects表示这个page中可以存放多少slab对象。
    //slab 缓冲区和struct page共用一个结构;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    代码分析

    slub系统的初始化:

    start_kernel() -> mm_init() -> kmem_cache_init()

    2952 static struct kmem_cache *kmem_cache_node;
    
    3671 void __init kmem_cache_init(void)
    3672 {
    3673     static __initdata struct kmem_cache boot_kmem_cache,
    3674         boot_kmem_cache_node;//声明静态变量,存储临时kmem_cache结构;
    3675 
    3676     if (debug_guardpage_minorder())
    3677         slub_max_order = 0;
    3678     //临时静态kmem_cache 指向全局变量;
    3679     kmem_cache_node = &boot_kmem_cache_node;
    3680     kmem_cache = &boot_kmem_cache;
    3681     //通过静态kmem_cache申请slub缓冲区,把管理数据放在上面的静态变量里面;
    3682     create_boot_cache(kmem_cache_node, "kmem_cache_node",
    3683         sizeof(struct kmem_cache_node), SLAB_HWCACHE_ALIGN);
    3684 
    3685     register_hotmemory_notifier(&slab_memory_callback_nb);
    3686   
    3687     /* Able to allocate the per node structures */
    3688     slab_state = PARTIAL;
    3689   
    3690     create_boot_cache(kmem_cache, "kmem_cache",
    3691             offsetof(struct kmem_cache, node) +
    3692                 nr_node_ids * sizeof(struct kmem_cache_node *),
    3693                SLAB_HWCACHE_ALIGN);
    3694     //把kmem_cache拷贝到新申请的对象中,完成自引导;
    3695     kmem_cache = bootstrap(&boot_kmem_cache);
    3696 
    3702     kmem_cache_node = bootstrap(&boot_kmem_cache_node);
    3703 
    3704     /* Now we can use the kmem_cache to allocate kmalloc slabs */ //创建kmalloc常规缓存;
    3705     create_kmalloc_caches(0);
    3706 
    3707 #ifdef CONFIG_SMP
    3708     register_cpu_notifier(&slab_notifier);
    3709 #endif
    3710 
    3715 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    第一次申请的时候,slub系统刚刚建立,因此只能向伙伴系统申请空闲的内存页,通过kmem_cache中的cotr函数指针指向的构造函数并初始化这个缓冲区,并把这些页面分成很多个object,取出其中的一个object标志为已被占用,并返回给用户,其余的object标志为空闲并放在kmem_cache_cpu中保存。kmem_cache_cpu的freelist变量中保存着下一个空闲object的地址。

    缓存的创建 :kmem_cache_create()

    对象(object)申请:slab_alloc() -> slab_alloc_node() -> get_freepointer_safe(): 这是从本地缓存获取;

     266 static inline void *get_freepointer(struct kmem_cache *s, void *object)
     267 {
     268     return *(void **)(object + s->offset);
     269 }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    slab_alloc() -> slab_alloc_node() -> __slab_alloc() : 慢速路径获取。如果本地CPU缓存没有空闲对象,则申请新的slab;如果有空闲对象,但是内存node不相符,则deactive当前cpu_slab,再申请新的slab。

    分配机制:

    当slub已经连续申请了很多页,现在kmem_cache_cpu中已经没有空闲的object了,但kmem_cache_node的partial中有空闲的object 。所以从kmem_cache_node的partial变量中获取有空闲object的slab,并把一个空闲的object返回给用户。

    当目前分配的slab缓冲区使用完了之后,会把这个满的slab缓冲区移除,再从伙伴系统获取一段连续页框作为新的空闲slab缓冲区,而那些满的slab缓冲区中有对象释放的时,slub分配器优先把这些缓冲区放入该cpu对应的部分空slab链表;而当一个部分空slab释放成了一个空的slab缓冲区的时候。slub分配器根据情况将此空闲slab放入到node节点的部分空slab连表中;

    当部分空slab释放一个对象后,转变成了空闲slab缓冲区,系统会检查node部分部分空链表的slab的缓冲区个数,如果这个个数小于min_partial,则将这个空闲缓冲区放入node部分空链表中;否则释放这个空闲slab;将其占用页框返回到伙伴系统中;

    当kmem_cache刷新的时候,会将kmem_cache所有的slab缓冲区放回到node节点的部分空链表;

    详细的slub分配规则可以参考:linux内存源码分析 - SLUB分配器概述

    slub与slab的区别:

    Slab器分为三个的每个节点分为三个链表,分别是空闲slab链表,部分空slab链表,已满slab链表,这三个slab维护着对应的slab缓冲区,这些slab缓冲区并不会自动返回到伙伴系统中去,而是添加到这node节点的这个三个链表中去,这样就会有很多slab缓冲区是很少用到的;而slub精简为了一个链表,只保留了部分空链表,这样每个CPU都维护有自己的一个部分空链表;

  • 相关阅读:
    Windows Azure Web Site (19) Azure Web App链接到VSTS
    Windows Azure Virtual Machine (35) Azure VM通过Linked DB,执行SQL Job
    Azure PowerShell (16) 并行开关机Azure ARM VM
    Windows Azure Virtual Network (12) 虚拟网络之间点对点连接VNet Peering
    Azure ARM (21) Azure订阅的两种管理模式
    Windows Azure Platform Introduction (14) 申请海外的Windows Azure账户
    Azure ARM (20) 将非托管磁盘虚拟机(Unmanage Disk),迁移成托管磁盘虚拟机(Manage Disk)
    Azure ARM (19) 将传统的ASM VM迁移到ARM VM (2)
    Azure ARM (18) 将传统的ASM VM迁移到ARM VM (1)
    Azure Automation (6) 执行Azure SQL Job
  • 原文地址:https://www.cnblogs.com/libertylife/p/9583021.html
Copyright © 2011-2022 走看看