织梦CMS - 轻松建站从此开始!

罗索

linux内存管理之vmalloc

jackyhwei 发布于 2016-04-01 11:02 点击:次 
在内核中还有另外一个接口函数那就是vmalloc,申请一片连续的虚拟地址空间,但不保证物理空间连续,实际上我们会想到用户空间的malloc,malloc它是标准的glibc封装的一个函数,最终实现是通过系统调用brk和mmap来实现,以后在分析它的实现过程.
TAG: 内存管理  内存  

在前面我们讲解了kmalloc申请连续物理内存的操作,以及原理和基础cache . 在内核中还有另外一个接口函数那就是vmalloc,申请一片连续的虚拟地址空间,但不保证物理空间连续,实际上我们会想到用户空间的 malloc,malloc它是标准的glibc封装的一个函数,最终实现是通过系统调用brk和mmap来 实现,以后在分析它的实现过程. 它就是申请连续的虚拟空间,但是不保证物理内存的连续,当然用户程序也不怎么关心这个问题,只所以会关心物理内存的连续性一般是由于设备驱动的使用,或者 DMA.  但是vmalloc申请效率比较低,还会造成TLB抖动. 一般内核里常用kmalloc. 除非特殊需求,比如要获取大块内存时,实例就是当ko模块加载到内核运行时,即需要vmalloc. 
释放函数:vfree 
参考内核  3.8.13
  这里是说32位的处理器,即最大寻址4G虚拟空间,(当然现在已经64位比较普及了,后续补上吧)而虚拟地址到物理地址的转化往往需要硬件的支持才能提高效率,即MMU。
当然前提需要os先建立页表PT. 在linux内核,这4G空间并不是完全给用户空间使用在高端0xC0000000 (3G开始)留给内核空间使用(x86默认配置,默认0-16M(DMA),16M-896M(Normal),896M-1G(128M)作为高端内存 分配区域),当然这个区域也是可是配置的.).
kmalloc函数返回的是虚拟地址(线性地址). kmalloc特殊之处在于它分配的内存是物理上连续的,这对于要进行DMA的设备十分重要. 而用vmalloc分配的内存只是线性地址连续,物理地址不一定连续,不能直接用于DMA。我们可以参考一个图:(它是arm 32架构的内核虚拟地址分配图)

下面我们就看看vmalloc函数:(mm/vmalloc.c)

  1. /** 
  2.  *    vmalloc - allocate virtually contiguous memory 
  3.  *    @size:        allocation size 
  4.  *    Allocate enough pages to cover @size from the page level 
  5.  *    allocator and map them into contiguous kernel virtual space. 
  6.  * 
  7.  *    For tight control over page level allocator and protection flags 
  8.  *    use __vmalloc() instead. 
  9.  */ 
  10. void *vmalloc(unsigned long size) 
  11.     return __vmalloc_node_flags(size, -1, GFP_KERNEL | __GFP_HIGHMEM); 

这里我们只用关注size即可,而vmalloc优先从高端内存分配,并且可以睡眠.
继续:

  1. static inline void *__vmalloc_node_flags(unsigned long size, 
  2.                     int node, gfp_t flags) 
  3.     return __vmalloc_node(size, 1, flags, PAGE_KERNEL, 
  4.                     node, __builtin_return_address(0)); 

重点看一下__vmalloc_node:

  1. /** 
  2.  *    __vmalloc_node - allocate virtually contiguous memory 
  3.  *    @size:        allocation size 
  4.  *    @align:        desired alignment 
  5.  *    @gfp_mask:    flags for the page level allocator 
  6.  *    @prot:        protection mask for the allocated pages 
  7.  *    @node:        node to use for allocation or -1 
  8.  *    @caller:    caller's return address 
  9.  * 
  10.  *    Allocate enough pages to cover @size from the page level 
  11.  *    allocator with @gfp_mask flags. Map them into contiguous 
  12.  *    kernel virtual space, using a pagetable protection of @prot. 
  13.  */ 
  14. static void *__vmalloc_node(unsigned long size, unsigned long align, 
  15.              gfp_t gfp_mask, pgprot_t prot, 
  16.              int node, const void *caller) 
  17.     return __vmalloc_node_range(size, align, VMALLOC_START, VMALLOC_END, 
  18.                 gfp_mask, prot, node, caller); 

因为这里提到了VMALLOC_START和VMALLOC_END它们究竟是什么值呢?
这里看了arm32和mips32的(根据架构虚拟地址分配不同而不同,比如mips就比较特殊):
在arch/mips/include/asm/pgtable-32.h中
首先看mips虚拟地址分布图:

从这个图里我们知道用户空间为2G(0x0-0x7fff ffff),dma或者normal内存映射在kseg0(512M)/kseg1,而对于vmalloc申请的虚拟地址在kseg2中,当然还有其他一些特殊的映射比如io等.

  1. #define VMALLOC_START MAP_BASE 
  2.  
  3. #define PKMAP_BASE        (0xfe000000UL) 
  4.  
  5. #ifdef CONFIG_HIGHMEM 
  6. # define VMALLOC_END    (PKMAP_BASE-2*PAGE_SIZE) 
  7. #else 
  8. # define VMALLOC_END    (FIXADDR_START-2*PAGE_SIZE) 
  9. #endif 

在arch/arm/include/asm/pgtable.h

  1. /* 
  2.  * Just any arbitrary offset to the start of the vmalloc VM area: the 
  3.  * current 8MB value just means that there will be a 8MB "hole" after the 
  4.  * physical memory until the kernel virtual memory starts. That means that 
  5.  * any out-of-bounds memory accesses will hopefully be caught. 
  6.  * The vmalloc() routines leaves a hole of 4kB between each vmalloced 
  7.  * area for the same reason. ;) 
  8.  */ 
  9. #define VMALLOC_OFFSET        (8*1024*1024) 
  10. #define VMALLOC_START        (((unsigned long)high_memory + VMALLOC_OFFSET) & ~(VMALLOC_OFFSET-1)) 
  11. #define VMALLOC_END        0xff000000UL 

在看一个图:

我们知道物理内存简单分为三个区域:ZONE_NORMAL、ZONE_DMA、ZONE_HIGHMEM
vmalloc我们看到它是默认从ZONE_HIGMEM里申请,但是这两个函数虚拟地址是保持一致的,即都占用了4G地址空间的内核虚拟地址.通过上面的图,我们确定了虚拟地址从哪里分配,以及对于的物理空间从哪里分配。
下面看看 vmalloc核心实现:

  1. /** 
  2.  *    __vmalloc_node_range - allocate virtually contiguous memory 
  3.  *    @size:        allocation size 
  4.  *    @align:        desired alignment 
  5.  *    @start:        vm area range start 
  6.  *    @end:        vm area range end 
  7.  *    @gfp_mask:    flags for the page level allocator 
  8.  *    @prot:        protection mask for the allocated pages 
  9.  *    @node:        node to use for allocation or -1 
  10.  *    @caller:    caller's return address 
  11.  * 
  12.  *    Allocate enough pages to cover @size from the page level 
  13.  *    allocator with @gfp_mask flags. Map them into contiguous 
  14.  *    kernel virtual space, using a pagetable protection of @prot. 
  15.  */ 
  16. void *__vmalloc_node_range(unsigned long size, unsigned long align, 
  17.             unsigned long start, unsigned long end, gfp_t gfp_mask, 
  18.             pgprot_t prot, int node, const void *caller) 
  19.     struct vm_struct *area; 
  20.     void *addr; 
  21.     unsigned long real_size = size; 
  22.  
  23.     size = PAGE_ALIGN(size); 
  24.     if (!size || (size >> PAGE_SHIFT) > totalram_pages) 
  25.         goto fail; 
  26.  
  27.     area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNLIST,
  28.     // 分配虚拟地址空间 把vm_struct 和vm_area(红黑树机制)关联起来. 
  29.                  start, end, node, gfp_mask, caller); 
  30.     if (!area) 
  31.         goto fail; 
  32.  
  33.     addr = __vmalloc_area_node(area, gfp_mask, prot, node, caller);
  34.   //计算需要申请的页面,申请page,然后修改页表完成映射. 
  35.     if (!addr) 
  36.         return NULL; 
  37.  
  38.     /* 
  39.      * In this function, newly allocated vm_struct is not added 
  40.      * to vmlist at __get_vm_area_node(). so, it is added here. 
  41.      */ 
  42.     insert_vmalloc_vmlist(area);     //把vm_struct插入 全局vmlist链表 
  43.  
  44.     /* 
  45.      * A ref_count = 3 is needed because the vm_struct and vmap_area 
  46.      * structures allocated in the __get_vm_area_node() function contain 
  47.      * references to the virtual address of the vmalloc'ed block. 
  48.      */ 
  49.     kmemleak_alloc(addr, real_size, 3, gfp_mask);    //内存泄露追踪 
  50.  
  51.     return addr; 
  52.  
  53. fail: 
  54.     warn_alloc_failed(gfp_mask, 0, 
  55.              "vmalloc: allocation failure: %lu bytes\n"
  56.              real_size); 
  57.     return NULL; 

它的基本实现思路很简单:
1. 分配虚拟地址空间 
2.对虚拟地址空间进行页表映射

需要熟知 下面两个结构体:
struct vmap_area

  1. struct vmap_area { 
  2.     unsigned long va_start; 
  3.     unsigned long va_end; 
  4.     unsigned long flags; 
  5.     struct rb_node rb_node;        /* address sorted rbtree */ 
  6.     struct list_head list;        /* address sorted list */ 
  7.     struct list_head purge_list;    /* "lazy purge" list */ 
  8.     struct vm_struct *vm; 
  9.     struct rcu_head rcu_head; 
  10. }; 

vm_struct *area :

  1. struct vm_struct { 
  2.     struct vm_struct    *next; 
  3.     void            *addr; 
  4.     unsigned long        size; 
  5.     unsigned long        flags; 
  6.     struct page        **pages; 
  7.     unsigned int        nr_pages; 
  8.     phys_addr_t        phys_addr; 
  9.     const void        *caller; 
  10. }; 

这里在说明一下vmalloc_init的初始化.

  1. /* 
  2.  * Set up kernel memory allocators 
  3.  */ 
  4. static void __init mm_init(void
  5.     /* 
  6.      * page_cgroup requires contiguous pages, 
  7.      * bigger than MAX_ORDER unless SPARSEMEM. 
  8.      */ 
  9.     page_cgroup_init_flatmem(); 
  10.     mem_init(); 
  11.     kmem_cache_init(); 
  12.     percpu_init_late(); 
  13.     pgtable_cache_init(); 
  14.     vmalloc_init(); 

其实在讲slab机制的时候已经说过。

  1. void __init vmalloc_init(void
  2.     struct vmap_area *va; 
  3.     struct vm_struct *tmp; 
  4.     int i; 
  5.  
  6.     for_each_possible_cpu(i) { 
  7.         struct vmap_block_queue *vbq; 
  8.  
  9.         vbq = &per_cpu(vmap_block_queue, i); 
  10.         spin_lock_init(&vbq->lock); 
  11.         INIT_LIST_HEAD(&vbq->free); 
  12.     } 
  13.  
  14.     /* Import existing vmlist entries. */ 
  15.     for (tmp = vmlist; tmp; tmp = tmp->next) { 
  16.  // 在系统启动或者初始化之初,vmlist为空. 
  17.         va = kzalloc(sizeof(struct vmap_area), GFP_NOWAIT); 
  18.         va->flags = VM_VM_AREA; 
  19.         va->va_start = (unsigned long)tmp->addr; 
  20.         va->va_end = va->va_start + tmp->size; 
  21.         va->vm = tmp; 
  22.         __insert_vmap_area(va); 
  23.     } 
  24.  
  25.     vmap_area_pcpu_hole = VMALLOC_END; 
  26.  
  27.     vmap_initialized = true

下面就说说__get_vm_area_node函数:

  1. static struct vm_struct *__get_vm_area_node(unsigned long size, 
  2.         unsigned long align, unsigned long flags, unsigned long start, 
  3.         unsigned long end, int node, gfp_t gfp_mask, const void *caller) 
  4.     struct vmap_area *va; 
  5.     struct vm_struct *area; 
  6.  
  7.     BUG_ON(in_interrupt()); 
  8.     if (flags & VM_IOREMAP) { // ioremap标志,映射的是设备内存 
  9.         int bit = fls(size); 
  10.  
  11.         if (bit > IOREMAP_MAX_ORDER) 
  12.             bit = IOREMAP_MAX_ORDER; 
  13.         else if (bit < PAGE_SHIFT) 
  14.             bit = PAGE_SHIFT; 
  15.  
  16.         align = 1ul << bit; 
  17.     } 
  18.  
  19.     size = PAGE_ALIGN(size); 
  20.     if (unlikely(!size)) 
  21.         return NULL; 
  22.  
  23.     area = kzalloc_node(sizeof(*area), gfp_mask & GFP_RECLAIM_MASK, node); 
  24.     if (unlikely(!area)) 
  25.         return NULL; 
  26.  
  27.     /* 
  28.      * We always allocate a guard page. 
  29.      */ 
  30.     size += PAGE_SIZE;
  31.  // 多偏移一页,为了防止访问越界,由于多出来的一页并不映射,所以当访问的时候,会引发保护异常. 
  32.  
  33.     va = alloc_vmap_area(size, align, start, end, node, gfp_mask);
  34. // 申请vm_area虚拟地址空间 
  35.     if (IS_ERR(va)) { 
  36.         kfree(area); 
  37.         return NULL; 
  38.     } 
  39.  
  40.     /* 
  41.      * When this function is called from __vmalloc_node_range, 
  42.      * we do not add vm_struct to vmlist here to avoid 
  43.      * accessing uninitialized members of vm_struct such as 
  44.      * pages and nr_pages fields. They will be set later. 
  45.      * To distinguish it from others, we use a VM_UNLIST flag. 
  46.      */ 
  47.     if (flags & VM_UNLIST)   // 必然走这里  
  48.         setup_vmalloc_vm(area, va, flags, caller);  // 关联vm_struct 和 vm_area 
  49.     else 
  50.         insert_vmalloc_vm(area, va, flags, caller); 
  51.  
  52.     return area; 

这个函数核心就是alloc_vmap_area,这个很有趣的,之前我们讲到了vmalloc申请的虚拟地址范围,而它只传递了size而已,对于mips,x86,arm会有不同的虚拟空间.

  1. /* 
  2.  * Allocate a region of KVA of the specified size and alignment, within the 
  3.  * vstart and vend. 
  4.  */ 
  5. static struct vmap_area *alloc_vmap_area(unsigned long size, 
  6.                 unsigned long align, 
  7.                 unsigned long vstart, unsigned long vend, 
  8.                 int node, gfp_t gfp_mask) 
  9.     struct vmap_area *va; 
  10.     struct rb_node *n; 
  11.     unsigned long addr; 
  12.     int purged = 0; 
  13.     struct vmap_area *first; 
  14.  
  15.     BUG_ON(!size); 
  16.     BUG_ON(size & ~PAGE_MASK); 
  17.     BUG_ON(!is_power_of_2(align)); 
  18.  
  19.     va = kmalloc_node(sizeof(struct vmap_area), 
  20.             gfp_mask & GFP_RECLAIM_MASK, node); 
  21.     if (unlikely(!va)) 
  22.         return ERR_PTR(-ENOMEM); 
  23.  
  24. retry: 
  25.     spin_lock(&vmap_area_lock); 
  26.     /* 
  27.      * Invalidate cache if we have more permissive parameters. 
  28.      * cached_hole_size notes the largest hole noticed _below_ 
  29.      * the vmap_area cached in free_vmap_cache: if size fits 
  30.      * into that hole, we want to scan from vstart to reuse 
  31.      * the hole instead of allocating above free_vmap_cache. 
  32.      * Note that __free_vmap_area may update free_vmap_cache 
  33.      * without updating cached_hole_size or cached_align. 
  34.      */ 
  35.     if (!free_vmap_cache ||   
  36.   //第一次调用的时候 free_vmap_cache为空,后来即后边的
  37. //代码line 105 : free_vmap_cache = &va->rb_node; 一般不为空 ;一般会发  
  38. // 生align < cached_align的情况,即会清除free_vmap_cache。
  39. //有时候align比较大的时候,它会跳过一段虚拟地址空间.后面的申请由于没  
  40. //有free_vmap_cache,所以它需要重新查询 
  41.             size < cached_hole_size || 
  42.             vstart < cached_vstart || 
  43.             align < cached_align) { 
  44. nocache: 
  45.         cached_hole_size = 0; 
  46.         free_vmap_cache = NULL; 
  47.     } 
  48.     /* record if we encounter less permissive parameters */ 
  49.     cached_vstart = vstart; 
  50.     cached_align = align; 
  51.  
  52.     /* find starting point for our search */ 
  53.     if (free_vmap_cache) {  
  54. // 第一次使用的时候为空;当不为空时,它保持上次申请的节点,并初始化addr为va_end. 
  55.         first = rb_entry(free_vmap_cache, struct vmap_area, rb_node); 
  56.         addr = ALIGN(first->va_end, align); 
  57.         if (addr < vstart) 
  58.             goto nocache; 
  59.         if (addr + size - 1 < addr) 
  60.             goto overflow; 
  61.  
  62.     } else { 
  63.         addr = ALIGN(vstart, align); 
  64.         if (addr + size - 1 < addr) 
  65.             goto overflow; 
  66.  
  67.         n = vmap_area_root.rb_node; 
  68. // 同样vmap_area_root.rb_node; 初始化也为空,第一次使用为空 
  69.         first = NULL; 
  70.  
  71.         while (n) { 
  72. // 当不是第一申请,并且free_cache为空的时候, 需要重新找到根节点即va_start <= addr 
  73.             struct vmap_area *tmp; 
  74.             tmp = rb_entry(n, struct vmap_area, rb_node); 
  75.       
  76.             if (tmp->va_end >= addr) { 
  77.                 first = tmp; 
  78.                 if (tmp->va_start <= addr) 
  79.                     break
  80.                 n = n->rb_left; 
  81.             } else 
  82.                 n = n->rb_right; 
  83.         } 
  84.  
  85.         if (!first) 
  86.             goto found; 
  87.     } 
  88.  
  89.     /* from the starting point, walk areas until a suitable hole is found */ 
  90.     while (addr + size > first->va_start && addr + size <= vend) {
  91. // 当不是第一申请,并且free_cache为空的时候,查询红黑树节点,找到合适的空间地址. 
  92.         if (addr + cached_hole_size < first->va_start) 
  93.             cached_hole_size = first->va_start - addr; 
  94.         addr = ALIGN(first->va_end, align); 
  95.         if (addr + size - 1 < addr) 
  96.             goto overflow; 
  97.           
  98.         if (list_is_last(&first->list, &vmap_area_list))
  99.  // 默认不会在这里操作。也就是说它没有元素. 
  100.             goto found; 
  101.  
  102.         first = list_entry(first->list.next, 
  103.                 struct vmap_area, list); 
  104.     } 
  105.  
  106. found: 
  107.     if (addr + size > vend) 
  108.         goto overflow; 
  109.  
  110.     va->va_start = addr; 
  111.     va->va_end = addr + size; 
  112.     va->flags = 0; 
  113.     __insert_vmap_area(va);  
  114.  // 添加到红黑树 vmap_area_root 
  115.     free_vmap_cache = &va->rb_node; 
  116. // 初始化free_vmap_cache ,它会影响后续虚拟空间的申请. 
  117.     spin_unlock(&vmap_area_lock); 
  118.  
  119.     BUG_ON(va->va_start & (align-1)); 
  120.     BUG_ON(va->va_start < vstart); 
  121.     BUG_ON(va->va_end > vend); 
  122.  
  123.     return va; 
  124.  
  125. overflow: 
  126.     spin_unlock(&vmap_area_lock); 
  127.     if (!purged) { 
  128.         purge_vmap_area_lazy(); 
  129.         purged = 1; 
  130.         goto retry; 
  131.     } 
  132.     if (printk_ratelimit()) 
  133.         printk(KERN_WARNING 
  134.             "vmap allocation for size %lu failed: " 
  135.             "use vmalloc= to increase size.\n", size); 
  136.     kfree(va); 
  137.     return ERR_PTR(-EBUSY); 

既然我们已经开辟了虚拟地址空间,那么还需要做的当然是和页面一一映射起来.
看函数__vmalloc_area_node:

  1. static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask, 
  2.                  pgprot_t prot, int node, const void *caller) 
  3.     const int order = 0; 
  4.     struct page **pages; 
  5.     unsigned int nr_pages, array_size, i; 
  6.     gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO; 
  7.  
  8.     nr_pages = (area->size - PAGE_SIZE) >> PAGE_SHIFT; //申请多少pages 
  9.     array_size = (nr_pages * sizeof(struct page *));   //需要多大的存放page指针的空间 . 
  10.  
  11.     area->nr_pages = nr_pages; 
  12.     /* Please note that the recursion is strictly bounded. */ 
  13.     if (array_size > PAGE_SIZE) {
  14.  // 这里默认page_size 为4k 即4096 ,地址32位的话,相当于申请1024个pages:4M空间 
  15.         pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM, 
  16.                 PAGE_KERNEL, node, caller); 
  17.         area->flags |= VM_VPAGES; 
  18.     } else { 
  19.         pages = kmalloc_node(array_size, nested_gfp, node);
  20. // 小于一页,则直接利用slab机制申请物理空间地址 给pages. 
  21.     } 
  22.     area->pages = pages; 
  23.     area->caller = caller; 
  24.     if (!area->pages) { 
  25.         remove_vm_area(area->addr); 
  26.         kfree(area); 
  27.         return NULL; 
  28.     } 
  29.  
  30.     for (i = 0; i < area->nr_pages; i++) { 
  31. //  每次申请一个page利用alloc_page直接申请物理页面 
  32.         struct page *page; 
  33.         gfp_t tmp_mask = gfp_mask | __GFP_NOWARN; 
  34.  
  35.         if (node < 0) 
  36.             page = alloc_page(tmp_mask); 
  37.         else 
  38.             page = alloc_pages_node(node, tmp_mask, order); 
  39.  
  40.         if (unlikely(!page)) { 
  41.             /* Successfully allocated i pages, free them in __vunmap() */ 
  42.             area->nr_pages = i; 
  43.             goto fail; 
  44.         } 
  45.         area->pages[i] = page;             // 分配的地址存放在指针数组. 
  46.     } 
  47.  
  48.     if (map_vm_area(area, prot, &pages))
  49.  // 修改页表 ,一页一页的实现映射,以及flush cache保持数据的一致性;对页面映射和操作感兴趣的可以深入看看这个函数. 
  50.         goto fail; 
  51.     return area->addr; 
  52.  
  53. fail: 
  54.     warn_alloc_failed(gfp_mask, order, 
  55.              "vmalloc: allocation failure, allocated %ld of %ld bytes\n"
  56.              (area->nr_pages*PAGE_SIZE), area->size); 
  57.     vfree(area->addr); 
  58.     return NULL; 

而insert_vmalloc_vmlist很明显把vm_struct插入到vmlist。
那么就完成了整个过程,没有想象的复杂,当然对内存有了更多的认识,这里还需要说一下,一般情况下有高端内存会比没有的好些,防止了vmalloc申请的时候造成的TLB抖动等问题,更少的破坏normal空间。
可以通过proc来查看vmalloc的一下信息:

  1. cat /proc/vmallocinfo 
  2. 0xc0002000-0xc0045000 274432 jffs2_zlib_init+0x24/0xa4 pages=66 vmalloc 
  3. 0xc0045000-0xc0051000 49152 jffs2_zlib_init+0x40/0xa4 pages=11 vmalloc 
  4. 0xc0051000-0xc0053000 8192 brcmnand_create_cet+0x244/0x788 pages=1 vmalloc 
  5. 0xc0053000-0xc0055000 8192 ebt_register_table+0x98/0x39c pages=1 vmalloc 

还有:

  1. # cat /proc/vmstat 
  2. #cat /proc/meminfo 

 

(linuxDOS)
本站文章除注明转载外,均为本站原创或编译欢迎任何形式的转载,但请务必注明出处,尊重他人劳动,同学习共成长。转载请注明:文章转载自:罗索实验室 [http://www.rosoo.net/a/201604/17444.html]
本文出处:ChinaUnix 作者:linuxDOS 原文
顶一下
(0)
0%
踩一下
(0)
0%
------分隔线----------------------------
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
用户名: 验证码:点击我更换图片
栏目列表
将本文分享到微信
织梦二维码生成器
推荐内容