当前位置:   article > 正文

鸿蒙原生开发——轻内核A核源码分析系列三 物理内存(2)

鸿蒙原生开发——轻内核A核源码分析系列三 物理内存(2)

3.1.2.3 函数OsVmPhysLargeAlloc

当执行到这个函数时,说明空闲链表上的单个内存页节点的大小已经不能满足要求,超过了第9个链表上的内存页节点的大小了。⑴处计算需要申请的内存大小。⑵从最大的链表上进行遍历每一个内存页节点。⑶根据每个内存页的开始内存地址,计算需要的内存的结束地址,如果超过内存段的大小,则继续遍历下一个内存页节点。

⑷处此时paStart表示当前内存页的结束地址,接下来paStart >= paEnd表示当前内存页的大小满足申请的需求;paStart < seg->startpaStart >= (seg->start + seg->size)发生溢出错误,内存页结束地址不在内存段的地址范围内。⑸处表示当前内存页的下一个内存页结构体,如果该结构体不在空闲链表上,则break跳出循环。如果在空闲链表上,表示连续的空闲内存页会拼接起来,满足大内存申请的需要。⑹表示一个或者多个连续的内存页的大小满足申请需求。

STATIC LosVmPage *OsVmPhysLargeAlloc(struct VmPhysSeg *seg, size_t nPages)
{
    struct VmFreeList *list = NULL;
    LosVmPage *page = NULL;
    LosVmPage *tmp = NULL;
    PADDR_T paStart;
    PADDR_T paEnd;
⑴  size_t size = nPages << PAGE_SHIFT;

⑵  list = &seg->freeList[VM_LIST_ORDER_MAX - 1];
    LOS_DL_LIST_FOR_EACH_ENTRY(page, &list->node, LosVmPage, node) {
⑶      paStart = page->physAddr;
        paEnd = paStart + size;
        if (paEnd > (seg->start + seg->size)) {
            continue;
        }

        for (;;) {
⑷          paStart += PAGE_SIZE << (VM_LIST_ORDER_MAX - 1);
            if ((paStart >= paEnd) || (paStart < seg->start) ||
                (paStart >= (seg->start + seg->size))) {
                break;
            }
⑸          tmp = &seg->pageBase[(paStart - seg->start) >> PAGE_SHIFT];
            if (tmp->order != (VM_LIST_ORDER_MAX - 1)) {
                break;
            }
        }
⑹      if (paStart >= paEnd) {
            return page;
        }
    }

    return NULL;
}
  • 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

3.1.2.4 函数OsVmPhysFreeListDelUnsafeOsVmPhysFreeListAddUnsafe

内部函数OsVmPhysFreeListDelUnsafe用于从空闲内存页节点链表上删除一个内存页节点,名称中有Unsafe字样,是因为函数体内并没有对链表操作加自旋锁,安全性由外部调用函数保证。⑴处进行校验,确保内存段和空闲链表索引符合要求。⑵处获取内存段和空闲链表,⑶处空闲链表上内存页节点数目减1,并把内存块从空闲链表上删除。⑷处设置内存页的order索引值为最大值来标记非空闲内存页。

STATIC VOID OsVmPhysFreeListDelUnsafe(LosVmPage *page)
{
    struct VmPhysSeg *seg = NULL;
    struct VmFreeList *list = NULL;

⑴  if ((page->segID >= VM_PHYS_SEG_MAX) || (page->order >= VM_LIST_ORDER_MAX)) {
        LOS_Panic("The page segment id(%u) or order(%u) is invalid\n", page->segID, page->order);
    }

⑵  seg = &g_vmPhysSeg[page->segID];
    list = &seg->freeList[page->order];
⑶  list->listCnt--;
    LOS_ListDelete(&page->node);
⑷  page->order = VM_LIST_ORDER_MAX;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

和空闲链表上删除对应的函数是空闲链表上插入空闲内存页节点函数OsVmPhysFreeListAddUnsafe。⑴处更新内存页的要挂载的空闲链表的索引值,然后获取内存页所在的内存段seg,并获取索引值对应的空闲链表。执行⑵把空闲内存页节点插入到空闲链表并更新节点数目。

STATIC VOID OsVmPhysFreeListAddUnsafe(LosVmPage *page, UINT8 order)
{
    struct VmPhysSeg *seg = NULL;
    struct VmFreeList *list = NULL;

    if (page->segID >= VM_PHYS_SEG_MAX) {
        LOS_Panic("The page segment id(%d) is invalid\n", page->segID);
    }

⑴  page->order = order;
    seg = &g_vmPhysSeg[page->segID];

    list = &seg->freeList[order];
⑵   LOS_ListTailInsert(&list->node, &page->node);
    list->listCnt++;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

3.1.2.5 函数OsVmPhysPagesSpiltUnsafe

函数OsVmPhysPagesSpiltUnsafe用于分割内存块,参数中oldOrder表示需要申请的内存页节点对应的链表索引,newOrder表示实际申请的内存页节点对应的链表索引。如果索引值相等,则不需要拆分,不会执行for循环块的代码。由于伙伴算法中的链表数组中元素的特点,即每个链表中的内存页节点的大小等于2的幂次方个内存页。在拆分时,依次从高索引newOrder往低索引oldOrder遍历,拆分一个内存页节点作为空闲内存页节点挂载到对应的空闲链表上。⑴处开始循环从高索引到低索引,索引值减1,然后执行⑵获取伙伴内存页节点,可以看出,申请的内存块大于需求时,会把后半部分的高地址部分放入空闲链表,保留前半部分的低地址部分。⑶处的断言确保伙伴内存页节点索引值是最大值,表示属于空闲内存页节点。⑷处调用函数把内存页节点放入空闲链表。

STATIC VOID OsVmPhysPagesSpiltUnsafe(LosVmPage *page, UINT8 oldOrder, UINT8 newOrder)
{
    UINT32 order;
    LosVmPage *buddyPage = NULL;

    for (order = newOrder; order > oldOrder;) {
⑴      order--;
⑵      buddyPage = &page[VM_ORDER_TO_PAGES(order)];
⑶      LOS_ASSERT(buddyPage->order == VM_LIST_ORDER_MAX);
⑷      OsVmPhysFreeListAddUnsafe(buddyPage, order);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这里有必要放这一张图,直观演示一下。假如我们需要申请8个内存页大小的内存节点,但是只有freeList[7]链表上才有空闲节点。申请成功后,超过了应用需要的大小,需要进行拆分。把27个内存页分为2份大小为26个内存页的节点,第一份继续拆分,第二份挂载到freeList[6]链表上。然后把第一份26个内存页拆分为2个25个内存页节点,第一份继续拆分,第二份挂载到freeList[5]链表上。依次进行下去,最后拆分为2份2^3个内存页大小的内存页节点,第一份作为实际申请的内存页返回,第二份挂载到freeList[3]链表上。如下图红色部分所示。

另外,函数OsVmRecycleExtraPages会调用OsVmPhysPagesFreeContiguous来回收申请的多余的内存页,后文再分析。

3.2 释放物理内存页接口

3.2.1 释放物理内存页接口介绍

和申请物理内存页接口相对应着,释放物理内存页的接口有3个,分别用于满足不同的释放内存页需求。函数LOS_PhysPagesFreeContiguous的传入参数为要释放物理页对应的内核虚拟地址空间中的虚拟内存地址和内存页数目。⑴处调用函数OsVmVaddrToPage把虚拟内存地址转换为物理内存页结构体地址,然后⑵处把内存页的连续内存页数目设置为0。⑶处调用函数OsVmPhysPagesFreeContiguous()释放物理内存页。函数LOS_PhysPageFree用于释放一个物理内存页,传入参数为要释放的物理页对应的物理页结构体地址。⑷处对引用计数自减,当小于等于0,表示没有其他引用时才进一步执行释放操作。该函数同样会调用函数OsVmPhysPagesFreeContiguous()释放物理内存页。函数LOS_PhysPagesFree用于释放挂在双向链表上的多个物理内存页,返回值为实际释放的物理页数目。⑸处遍历内存页双向链表,从链表上移除要释放的内存页节点。⑹处代码和释放一个内存页的函数代码相同。⑺处计算遍历的内存页的数目,函数最后会返回该值。

VOID LOS_PhysPagesFreeContiguous(VOID *ptr, size_t nPages)
{
    UINT32 intSave;
    struct VmPhysSeg *seg = NULL;
    LosVmPage *page = NULL;

    if (ptr == NULL) {
        return;
    }

⑴   page = OsVmVaddrToPage(ptr);
    if (page == NULL) {
        VM_ERR("vm page of ptr(%#x) is null", ptr);
        return;
    }
⑵  page->nPages = 0;

    seg = &g_vmPhysSeg[page->segID];
    LOS_SpinLockSave(&seg->freeListLock, &intSave);

⑶   OsVmPhysPagesFreeContiguous(page, nPages);

    LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
}

......

VOID LOS_PhysPageFree(LosVmPage *page)
{
    UINT32 intSave;
    struct VmPhysSeg *seg = NULL;

    if (page == NULL) {
        return;
    }

⑷  if (LOS_AtomicDecRet(&page->refCounts) <= 0) {
        seg = &g_vmPhysSeg[page->segID];
        LOS_SpinLockSave(&seg->freeListLock, &intSave);

        OsVmPhysPagesFreeContiguous(page, ONE_PAGE);
        LOS_AtomicSet(&page->refCounts, 0);

        LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
    }
}
······
size_t LOS_PhysPagesFree(LOS_DL_LIST *list)
{
    UINT32 intSave;
    LosVmPage *page = NULL;
    LosVmPage *nPage = NULL;
    LosVmPhysSeg *seg = NULL;
    size_t count = 0;

    if (list == NULL) {
        return 0;
    }

    LOS_DL_LIST_FOR_EACH_ENTRY_SAFE(page, nPage, list, LosVmPage, node) {
⑸      LOS_ListDelete(&page->node);
⑹      if (LOS_AtomicDecRet(&page->refCounts) <= 0) {
            seg = &g_vmPhysSeg[page->segID];
            LOS_SpinLockSave(&seg->freeListLock, &intSave);
            OsVmPhysPagesFreeContiguous(page, ONE_PAGE);
            LOS_AtomicSet(&page->refCounts, 0);
            LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
        }
⑺      count++;
    }

    return count;
}
  • 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

3.2.2 释放物理内存页内部接口实现

3.2.2.1 函数OsVmVaddrToPage

函数OsVmVaddrToPage把虚拟内存地址转换为物理页结构体地址。⑴处调用函数LOS_PaddrQuery()把虚拟地址转为物理地址,该函数在虚实映射部分会详细讲述。⑵处遍历物理内存段,如果物理内存地址处于物理内存段的地址范围,则可以返回该物理地址对应的物理页结构体地址。

LosVmPage *OsVmVaddrToPage(VOID *ptr)
{
    struct VmPhysSeg *seg = NULL;
⑴  PADDR_T pa = LOS_PaddrQuery(ptr);
    UINT32 segID;

    for (segID = 0; segID < g_vmPhysSegNum; segID++) {
        seg = &g_vmPhysSeg[segID];
⑵      if ((pa >= seg->start) && (pa < (seg->start + seg->size))) {
            return seg->pageBase + ((pa - seg->start) >> PAGE_SHIFT);
        }
    }

    return NULL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
3.2.2.2 函数OsVmPhysPagesFreeContiguous

函数OsVmPhysPagesFreeContiguous()用于释放指定数量的连续物理内存页。⑴处根据物理内存页获取对应的物理内存地址。⑵处根据物理内存地址获取空闲内存页链表数组索引数值。⑶处获取索引值对应的链表上的内存页节点的内存页数目。⑷处如果要释放的内存页数nPages小于当前链表上的内存页节点的数目,则跳出循环执行⑹处代码,去释放到小索引的双向链表上。⑸处调用函数OsVmPhysPagesFree()释放指定链表上的内存页,然后更新内存页数量和内存页结构体地址。

⑹处根据内存页数量计算对应的链表索引,根据索引值计算链表上内存页节点的大小。⑺处调用函数OsVmPhysPagesFree()释放指定链表上的内存页,然后更新内存页数量和内存页结构体地址。

VOID OsVmPhysPagesFreeContiguous(LosVmPage *page, size_t nPages)
{
    paddr_t pa;
    UINT32 order;
    size_t n;

    while (TRUE) {
⑴      pa = VM_PAGE_TO_PHYS(page);
⑵      order = VM_PHYS_TO_ORDER(pa);
⑶      n = VM_ORDER_TO_PAGES(order);
⑷      if (n > nPages) {
            break;
        }
⑸      OsVmPhysPagesFree(page, order);
        nPages -= n;
        page += n;
    }

    while (nPages > 0) {
⑹      order = LOS_HighBitGet(nPages);
        n = VM_ORDER_TO_PAGES(order);
⑺      OsVmPhysPagesFree(page, order);
        nPages -= n;
        page += n;
    }
}
  • 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
3.2.2.3 函数OsVmPhysPagesFree

函数OsVmPhysPagesFree()释放内存页到对应的空闲内存页链表。内存页块释放时,会在当前链表找地址连续的伙伴内存页块进行合并,然后去上一级链表上继续查找是否存在连续的伙伴内存页块。⑴做传入参数校验。⑵处需要至少是倒数第二个链表,这样内存页节点可以和大索引链表上的节点合并。⑶处获取内存页对应的物理内存地址,然后后面会开始do-while循环,查找是否存在连续的内存页节点。⑷处的VM_ORDER_TO_PHYS(order)计算出链表索引值对应的伙伴位图,然后进行异或运算计算出伙伴内存页的物理内存地址。⑸处物理地址转换为内存页结构体,进一步判断:如果内存页不存在或者不在空闲链表上,则跳出循环while循环。否则如果伙伴内存节点存在,则执行⑹把伙伴页从链表上移除,然后索引值加1。⑺处链表索引加1,然后进行逻辑与计算得到物理内存地址。此时物理内存地址,和合并的两块内存页块地址连续。该内存地址在高一级的空闲链表上不一定存在,存在则继续合并,不存在则退出循环。当索引order为8,要插入到最后一个链表上时,或者没有再找到可以合并的节点时,则直接执行⑻插入内存页节点到空闲链表上。

VOID OsVmPhysPagesFree(LosVmPage *page, UINT8 order)
{
    paddr_t pa;
    LosVmPage *buddyPage = NULL;

⑴  if ((page == NULL) || (order >= VM_LIST_ORDER_MAX)) {
        return;
    }

⑵  if (order < VM_LIST_ORDER_MAX - 1) {
⑶        pa = VM_PAGE_TO_PHYS(page);        
        do {
⑷          pa ^= VM_ORDER_TO_PHYS(order);
⑸          buddyPage = OsVmPhysToPage(pa, page->segID);
            if ((buddyPage == NULL) || (buddyPage->order != order)) {
                break;
            }
⑹          OsVmPhysFreeListDel(buddyPage);
            order++;
⑺          pa &= ~(VM_ORDER_TO_PHYS(order) - 1);
            page = OsVmPhysToPage(pa, page->segID);
        } while (order < VM_LIST_ORDER_MAX - 1);
    }

⑻  OsVmPhysFreeListAdd(page, order);
}
  • 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

3.3 查询物理页地址接口

3.3.1 函数LOS_VmPageGet()

函数LOS_VmPageGet用于根据物理内存地址参数计算对应的物理内存页结构体地址。⑴处遍历物理内存段,调用函数OsVmPhysToPage根据物理内存地址和内存段编号计算物理内存页结构体,该函数后文再分析。⑵处如果获取的物理内存页结构体不为空,则跳出循环,返回物理内存页结构体指针

LosVmPage *LOS_VmPageGet(PADDR_T paddr)
{
    INT32 segID;
    LosVmPage *page = NULL;

    for (segID = 0; segID < g_vmPhysSegNum; segID++) {
⑴      page = OsVmPhysToPage(paddr, segID);
⑵      if (page != NULL) {
            break;
        }
    }

    return page;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

继续看下函数OsVmPhysToPage的代码。⑴处如果参数传入的物理内存地址不在指定的物理内存段的地址范围之内则返回NULL。⑵处计算物理内存地址相对内存段开始地址的偏移值。⑶处根据偏移值计算出偏移的内存页的数目,然后返回物理内存地址对应的物理页结构体的地址。

LosVmPage *OsVmPhysToPage(paddr_t pa, UINT8 segID)
{
    struct VmPhysSeg *seg = NULL;
    paddr_t offset;

    if (segID >= VM_PHYS_SEG_MAX) {
        LOS_Panic("The page segment id(%d) is invalid\n", segID);
    }
    seg = &g_vmPhysSeg[segID];
⑴  if ((pa < seg->start) || (pa >= (seg->start + seg->size))) {
        return NULL;
    }

⑵  offset = pa - seg->start;
⑶  return (seg->pageBase + (offset >> PAGE_SHIFT));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3.3.2 函数LOS_PaddrToKVaddr

函数LOS_PaddrToKVaddr根据物理地址获取其对应的内核虚拟地址。⑴处遍历物理内存段数组,然后在⑵处判断如果物理地址处于遍历到的物理内存段的地址范围内,则执行⑶,传入的物理内存地址相对物理内存开始地址的偏移加上内核态虚拟地址空间的开始地址就是物理地址对应的内核虚拟地址。

VADDR_T *LOS_PaddrToKVaddr(PADDR_T paddr)
{
    struct VmPhysSeg *seg = NULL;
    UINT32 segID;

    for (segID = 0; segID < g_vmPhysSegNum; segID++) {
 ⑴     seg = &g_vmPhysSeg[segID];
 ⑵     if ((paddr >= seg->start) && (paddr < (seg->start + seg->size))) {
 ⑶          return (VADDR_T *)(UINTPTR)(paddr - SYS_MEM_BASE + KERNEL_ASPACE_BASE);
        }
    }

    return (VADDR_T *)(UINTPTR)(paddr - SYS_MEM_BASE + KERNEL_ASPACE_BASE);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3.4 其他函数

3.4.1 函数OsPhysSharePageCopy

函数OsPhysSharePageCopy用于复制共享内存页。 ⑴处进行参数校验, ⑵处获取老内存页, ⑶处获取内存段。⑷处如果老内存页引用计数为1,则把老物理内存地址直接赋值给新物理内存地址。⑸处如果内存页有多个引用,则先转化为虚拟内存地址,然后执行⑹进行内存页的内容复制。⑺刷新新老内存页的引用计数。

VOID OsPhysSharePageCopy(PADDR_T oldPaddr, PADDR_T *newPaddr, LosVmPage *newPage)
{
    UINT32 intSave;
    LosVmPage *oldPage = NULL;
    VOID *newMem = NULL;
    VOID *oldMem = NULL;
    LosVmPhysSeg *seg = NULL;

 ⑴  if ((newPage == NULL) || (newPaddr == NULL)) {
        VM_ERR("new Page invalid");
        return;
    }

 ⑵  oldPage = LOS_VmPageGet(oldPaddr);
    if (oldPage == NULL) {
        VM_ERR("invalid oldPaddr %p", oldPaddr);
        return;
    }

 ⑶  seg = &g_vmPhysSeg[oldPage->segID];
    LOS_SpinLockSave(&seg->freeListLock, &intSave);
⑷  if (LOS_AtomicRead(&oldPage->refCounts) == 1) {
        *newPaddr = oldPaddr;
    } else {
⑸      newMem = LOS_PaddrToKVaddr(*newPaddr);
        oldMem = LOS_PaddrToKVaddr(oldPaddr);
        if ((newMem == NULL) || (oldMem == NULL)) {
            LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
            return;
        }
⑹      if (memcpy_s(newMem, PAGE_SIZE, oldMem, PAGE_SIZE) != EOK) {
            VM_ERR("memcpy_s failed");
        }

⑺      LOS_AtomicInc(&newPage->refCounts);
        LOS_AtomicDec(&oldPage->refCounts);
    }
    LOS_SpinUnlockRestore(&seg->freeListLock, intSave);
    return;
}
  • 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

总结

本文首先了解了物理内存管理的结构体,接着阅读了物理内存如何初始化,然后分析了物理内存的申请、释放和查询等操作接口的源代码。

如果大家想更加深入的学习 OpenHarmony 开发的内容,不妨可以参考以下相关学习文档进行学习,助你快速提升自己:

OpenHarmony 开发环境搭建:https://qr18.cn/CgxrRy

《OpenHarmony源码解析》:https://qr18.cn/CgxrRy

  • 搭建开发环境
  • Windows 开发环境的搭建
  • Ubuntu 开发环境搭建
  • Linux 与 Windows 之间的文件共享
  • ……

系统架构分析:https://qr18.cn/CgxrRy

  • 构建子系统
  • 启动流程
  • 子系统
  • 分布式任务调度子系统
  • 分布式通信子系统
  • 驱动子系统
  • ……

OpenHarmony 设备开发学习手册:https://qr18.cn/CgxrRy

在这里插入图片描述

OpenHarmony面试题(内含参考答案):https://qr18.cn/CgxrRy

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing
    声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/723282
推荐阅读
相关标签