内存管理的功能特点

  • RT-Thread 操作系统在内存管理上,根据上层应用及系统资源的不同,有针对性地提供了不同的内存分配管理算法。总体上可分为两类:内存堆管理与内存池管理,而内存堆管理又根据具体内存设备划分为三种情况:

    1. 第一种是针对小内存块的分配管理(小内存管理算法);

    2. 第二种是针对大内存块的分配管理(slab 管理算法);

    3. 第三种是针对多内存堆的分配情况(memheap 管理算法)

内存堆管理

  • 内存堆管理用于管理一段连续的内存空间,如下图所示,RT-Thread 将 “ZI 段结尾处” 到内存尾部的空间用作内存堆。

  • 小内存管理算法主要针对系统资源比较少,一般用于小于 2MB 内存空间的系统;
  • slab 内存管理算法则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的快速算法;
  • RT-Thread 还有一种针对多内存堆的管理算法,即 memheap 管理算法。memheap 方法适用于系统存在多个内存堆的情况,它可以将多个内存 “粘贴” 在一起,形成一个大的内存堆;
  • 注意:因为内存堆管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥问题,所以不要在中断服务例程中分配或释放动态内存块,因为它可能会引起当前上下文被挂起等待;

小内存管理里算法

  • 当需要分配内存块时,将从这个大的内存块上分割出相匹配的内存块,然后把分割出来的空闲内存块还回给堆管理系统中。每个内存块都包含一个管理用的数据头,通过这个头把使用块与空闲块用双向链表的方式链接起来,如图所示:

  • 每个内存块都包含一个数据头:magic 和 used
  • magic:用来标记这个内存块是一个内存管理的内存数据块,也是一个内存保护字,如果这个区域被改写,那么这个内存块就被非法改写了;
  • used :用来表示当前内存块是否被分配;
  • struct heap_mem
    {
    /* magic and used flag */
    rt_uint16_t magic;
    rt_uint16_t used; rt_size_t next, prev; #ifdef RT_USING_MEMTRACE
    rt_uint8_t thread[]; /* thread name */
    #endif
    };
  • 内存分配过程:
  • 设定:空闲链表指针Ifree初始指向32字节的内存块,当用户线程需要分配一个64字节的内存块时,Ifree指针指向的内存块不能满足要求,内存管理器就会继续寻找下一个内存块,当找到时(128字节),就会进行内存分配,如果内存块比较大,分配器就会把内存块进行拆分,余下的内存(52字节)继续留在Ifree链表中,如下图所示;

  • 注意:在内次分配内存块前,都会留出12字节用于magic、used信息及节点使用,返回给应用的地址实际上是这块内存块 12 字节以后的地址,前面的 12 字节数据头是用户永远不应该碰的部分(注:12 字节数据头长度会与系统对齐差异而有所不同)。
  • 释放:释放内存时,分配器会查看前后相邻的内存是否是空闲,如果空闲,就回合并成一个大的空闲内存块;

slab管理算法

  • RT-Thread 的 slab 分配器的实现是建立在 slab分配器上的,针对嵌入式仙童优化的内存分配算法。(slab是linux系统中的一种内存分配机制)
  • RT-Thread 的 slab 分配器实现主要是去掉了其中的对象构造及析构过程,只保留了纯粹的缓冲型的内存池算法。slab 分配器会根据对象的大小分成多个区(zone),也可以看成每类对象有一个内存池,如下图所示:

  • 一个 zone 的大小在 32K 到 128K 字节之间,分配器会在堆初始化时根据堆的大小自动调整。系统中的 zone 最多包括 72 种对象,一次最大能够分配 16K 的内存空间,如果超出了 16K 那么直接从页分配器中分配。每个 zone 上分配的内存块大小是固定的,能够分配相同大小内存块的 zone 会链接在一个链表中,而 72 种对象的 zone 链表则放在一个数组(zone_array[])中统一管理。
  • 内存分配:
  • 假设分配一个 32 字节的内存,slab 内存分配器会先按照 32 字节的值,从 zone array 链表表头数组中找到相应的 zone 链表。如果这个链表是空的,则向页分配器分配一个新的 zone,然后从 zone 中返回第一个空闲内存块。如果链表非空,则这个 zone 链表中的第一个 zone 节点必然有空闲块存在(否则它就不应该放在这个链表中),那么就取相应的空闲块。如果分配完成后,zone 中所有空闲内存块都使用完毕,那么分配器需要把这个 zone 节点从链表中删除。
  • 内存释放:分配器需要找到内存块所在的 zone 节点,然后把内存块链接到 zone 的空闲内存块链表中。如果此时 zone 的空闲链表指示出 zone 的所有内存块都已经释放,即 zone 是完全空闲的,那么当 zone 链表中全空闲 zone 达到一定数目后,系统就会把这个全空闲的 zone 释放到页面分配器中去。

memheap管理算法

  • memheap 管理算法适用于系统含有多个地址可不连续的内存堆。使用 memheap 内存管理可以简化系统存在多个内存堆时的使用:当系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的 memheap 初始化,并开启 memheap 功能就可以很方便地把多个 memheap(地址可不连续)粘合起来用于系统的 heap 分配;
  • 在开启 memheap 之后原来的 heap 功能将被关闭,两者只可以通过打开或关闭 RT_USING_MEMHEAP_AS_HEAP 来选择其一;
  • memheap 工作机制如下图所示,首先将多块内存加入 memheap_item 链表进行粘合。当分配内存块时,会先从默认内存堆去分配内存,当分配不到时会查找 memheap_item 链表,尝试从其他的内存堆上分配内存块。应用程序不用关心当前分配的内存块位于哪个内存堆上,就像是在操作一个内存堆。

内存堆配置和初始化

  • 在使用内存堆时,必须要在系统初始化的时候进行堆的初始化;
  • /**
    * @ingroup SystemInit
    *
    * This function will initialize system heap memory.
    *
    * @param begin_addr the beginning address of system heap memory.
    * @param end_addr the end address of system heap memory.
    */
    void rt_system_heap_init(void *begin_addr, void *end_addr)
  • 在使用 memheap 堆内存时,必须要在系统初始化的时候进行堆内存的初始化;
  • /*
    * The initialized memory pool will be:
    * +-----------------------------------+--------------------------+
    * | whole freed memory block | Used Memory Block Tailer |
    * +-----------------------------------+--------------------------+
    *
    * block_list --> whole freed memory block
    *
    * The length of Used Memory Block Tailer is 0,
    * which is prevents block merging across list
    */
    rt_err_t rt_memheap_init(struct rt_memheap *memheap,
    const char *name,
    void *start_addr,
    rt_uint32_t size)

内存堆的管理方式

  • 对内存堆的操作包含:初始化、申请内存块、释放内存,所有使用完成后的动态内存都应该被释放,以供其他程序的申请使用;

内存分配和释放

  • 从内存堆上分配用户指定大小的内存块,t_malloc 函数会从系统堆空间中找到合适大小的内存块,然后把内存块可用地址返回给用户;
  • /**
    * Allocate a block of memory with a minimum of 'size' bytes.
    *
    * @param size is the minimum size of the requested block in bytes.
    *
    * @return pointer to allocated memory or NULL if no free memory was found.
    */
    void *rt_malloc(rt_size_t size)
  • 应用程序使用完从内存分配器中申请的内存后,必须及时释放,否则会造成内存泄漏

  • /**
    * This function will release the previously allocated memory block by
    * rt_malloc. The released memory block is taken back to system heap.
    *
    * @param rmem the address of memory which will be released
    */
    void rt_free(void *rmem) 

重分配内存块

  • 在已分配内存块的基础上重新分配内存块的大小(增加或缩小)

    /**
    * This function will change the previously allocated memory block.
    *
    * @param rmem pointer to memory allocated by rt_malloc
    * @param newsize the required new size
    *
    * @return the changed memory block address
    */
    void *rt_realloc(void *rmem, rt_size_t newsize)

分配多内存块

  • 从内存堆中分配连续内存地址的多个内存块;
  • /**
    * This function will contiguously allocate enough space for count objects
    * that are size bytes of memory each and returns a pointer to the allocated
    * memory.
    *
    * The allocated memory is filled with bytes of value zero.
    *
    * @param count number of objects to allocate
    * @param size size of the objects to allocate
    *
    * @return pointer to allocated memory / NULL pointer if there is an error
    */
    void *rt_calloc(rt_size_t count, rt_size_t size)

设置内存钩子函数

  • 在分配内存块过程中,用户可设置一个钩子函数。设置的钩子函数会在内存分配完成后进行回调。回调时,会把分配到的内存块地址和大小做为入口参数传递进去;
  • /**
    * This function will set a hook function, which will be invoked when a memory
    * block is allocated from heap memory.
    *
    * @param hook the hook function
    */
    void rt_malloc_sethook(void (*hook)(void *ptr, rt_size_t size))
    {
    rt_malloc_hook = hook;
    }
  • 在释放内存时,用户可设置一个钩子函数;

  • /**
    * This function will set a hook function, which will be invoked when a memory
    * block is released to heap memory.
    *
    * @param hook the hook function
    */
    void rt_free_sethook(void (*hook)(void *ptr))
    {
    rt_free_hook = hook;
    }

内存堆管理应用示例

  • 创建一个动态的线程,这个线程会动态申请内存并释放,每次申请更大的内存,当申请不到的时候就结束;
  • #include <rtthread.h>
    
    #define THREAD_PRIORITY      25
    #define THREAD_STACK_SIZE 512
    #define THREAD_TIMESLICE 5 /* 线程入口 */
    void thread1_entry(void *parameter)
    {
    int i;
    char *ptr = RT_NULL; /* 内存块的指针 */ for (i = ; ; i++)
    {
    /* 每次分配 (1 << i) 大小字节数的内存空间 */
    ptr = rt_malloc( << i); /* 如果分配成功 */
    if (ptr != RT_NULL)
    {
    rt_kprintf("get memory :%d byte\n", ( << i));
    /* 释放内存块 */
    rt_free(ptr);
    rt_kprintf("free memory :%d byte\n", ( << i));
    ptr = RT_NULL;
    }
    else
    {
    rt_kprintf("try to get %d byte memory failed!\n", ( << i));
    return;
    }
    }
    } int dynmem_sample(void)
    {
    rt_thread_t tid = RT_NULL; /* 创建线程 1 */
    tid = rt_thread_create("thread1",
    thread1_entry, RT_NULL,
    THREAD_STACK_SIZE,
    THREAD_PRIORITY,
    THREAD_TIMESLICE);
    if (tid != RT_NULL)
    rt_thread_startup(tid); return ;
    }
    /* 导出到 msh 命令列表中 */
    MSH_CMD_EXPORT(dynmem_sample, dynmem sample);

    运行结果:

  • \ | /
    - RT - Thread Operating System
    / | \ 3.1. build Aug
    - Copyright by rt-thread team
    msh >dynmem_sample
    msh >get memory : byte
    free memory : byte
    get memory : byte
    free memory : byte

    get memory : byte
    free memory : byte
    get memory : byte
    free memory : byte
    try to get byte memory failed!

内存池

  • 内存堆管理器可以分配任意大小的内存块,非常灵活和方便,但是:分配效率不高,在每次分配时,都要空闲内存块查找,并且容易产生内存碎片;
  • 内存池是一种内存分配方式,用于分配大量大小相同的小内存块,它可以极大地加快内存分配与释放的速度,且能尽量避免内存碎片化;
  • RT-Thread 的内存池支持线程挂起功能,当内存池中无空闲内存块时,申请线程会被挂起,直到内存池中有新的可用内存块,再将挂起的申请线程唤醒;
  • 内存池的线程挂起功能非常适合需要通过内存资源进行同步的场景,例如播放音乐时,播放器线程会对音乐文件进行解码,然后发送到声卡驱动,从而驱动硬件播放音乐。

  • 播放器线程需要解码数据时,就会向内存池请求内存块,如果内存块已经用完,线程将被挂起,否则它将获得内存块以放置解码的数据;
  • 而后播放器线程把包含解码数据的内存块写入到声卡抽象设备中 (线程会立刻返回,继续解码出更多的数据);
  • 当声卡设备写入完成后,将调用播放器线程设置的回调函数,释放写入的内存块,如果在此之前,播放器线程因为把内存池里的内存块都用完而被挂起的话,那么这是它将被将唤醒,并继续进行解码。

内存池工作机制

内存池控制块

  • 内存池控制块是操作系统用于管理内存池的一个数据结构,它会存放内存池的一些信息,例如内存池数据区域开始地址,内存块大小和内存块列表等,也包含内存块与内存块之间连接用的链表结构,因内存块不可用而挂起的线程等待事件集合等。
  • 在 RT-Thread 实时操作系统中,内存池控制块由结构体 struct rt_mempool 表示

  • struct rt_mempool
    {
    struct rt_object parent; /**< inherit from rt_object */ void *start_address; /**< memory pool start */
    rt_size_t size; /**< size of memory pool */ rt_size_t block_size; /**< size of memory blocks */
    rt_uint8_t *block_list; /**< memory blocks list */ rt_size_t block_total_count; /**< numbers of memory block */
    rt_size_t block_free_count; /**< numbers of free memory block */ rt_list_t suspend_thread; /**< threads pended on this resource */
    rt_size_t suspend_thread_count; /**< numbers of thread pended on this resource */
    };
    typedef struct rt_mempool *rt_mp_t;

    内存块分配机制

  • 内存池在创建时先向系统申请一大块内存,然后分成同样大小的多个小内存块,小内存块直接通过链表连接起来(此链表也称为空闲链表)。每次分配的时候,从空闲链表中取出链头上第一个内存块,提供给申请者。从下图中可以看到,物理内存中允许存在多个大小不同的内存池,每一个内存池又由多个空闲内存块组成,内核用它们来进行内存管理。当一个内存池对象被创建时,内存池对象就被分配给了一个内存池控制块,内存控制块的参数包括内存池名,内存缓冲区,内存块大小,块数以及一个等待线程队列。

  • 内存池一旦初始化完成,内部的内存块大小将不能再做调整;
  • 每一个内存池对象由上述结构组成,其中 suspend_thread 形成了一个申请线程等待列表,即当内存池中无可用内存块,并且申请线程允许等待时,申请线程将挂起在 suspend_thread 链表上;

内存池的管理方式

  • 内存池控制块是一个结构体,其中含有内存池相关的重要参数,在内存池各种状态间起到纽带的作用,对内存池的操作包含:创建 / 初始化内存池、申请内存块、释放内存块、删除 / 脱离内存池,但不是所有的内存池都会被删除,这与设计者的需求相关,但是使用完的内存块都应该被释放。

创建和删除内存池

  • 创建内存池操作将会创建一个内存池对象并从堆上分配一个内存池。创建内存池是从对应内存池中分配和释放内存块的先决条件,创建内存池后,线程便可以从内存池中执行申请、释放等操作;
  • /**
    * This function will create a mempool object and allocate the memory pool from
    * heap.
    *
    * @param name the name of memory pool
    * @param block_count the count of blocks in memory pool
    * @param block_size the size for each block
    *
    * @return the created mempool object
    */
    rt_mp_t rt_mp_create(const char *name,
    rt_size_t block_count,
    rt_size_t block_size)
  • 删除内存池将删除内存池对象并释放申请的内存

  • /**
    * This function will delete a memory pool and release the object memory.
    *
    * @param mp the memory pool object
    *
    * @return RT_EOK
    */
    rt_err_t rt_mp_delete(rt_mp_t mp)

    初始化和脱离内存池

  • 初始化内存池跟创建内存池类似,只是初始化内存池用于静态内存管理模式,内存池控制块来源于用户在系统中申请的静态对象。另外与创建内存池不同的是,此处内存池对象所使用的内存空间是由用户指定的一个缓冲区空间,用户把缓冲区的指针传递给内存池控制块,存池块个数 = size / (block_size + 4 链表指针大小),计算结果取整数。
  • /**
    * This function will initialize a memory pool object, normally which is used
    * for static object.
    *
    * @param mp the memory pool object
    * @param name the name of memory pool
    * @param start the star address of memory pool
    * @param size the total size of memory pool
    * @param block_size the size for each block
    *
    * @return RT_EOK
    */
    rt_err_t rt_mp_init(struct rt_mempool *mp,
    const char *name,
    void *start,
    rt_size_t size,
    rt_size_t block_size)
  • 脱离内存池将把内存池对象从内核对象管理器中脱离

  • /**
    * This function will detach a memory pool from system object management.
    *
    * @param mp the memory pool object
    *
    * @return RT_EOK
    */
    rt_err_t rt_mp_detach(struct rt_mempool *mp)

    分配和释放内存块

  • 从指定的内存池中分配一个内存块;
  • 如果内存池中有可用的内存块,则从内存池的空闲块链表上取下一个内存块,减少空闲块数目并返回这个内存块;如果内存池中已经没有空闲内存块,则判断超时时间设置:若超时时间设置为零,则立刻返回空内存块;若等待时间大于零,则把当前线程挂起在该内存池对象上,直到内存池中有可用的自由内存块,或等待时间到达;
  • /**
    * This function will allocate a block from memory pool
    *
    * @param mp the memory pool object
    * @param time the waiting time
    *
    * @return the allocated memory block or RT_NULL on allocated failed
    */
    void *rt_mp_alloc(rt_mp_t mp, rt_int32_t time)
  • 任何内存块使用完后都必须被释放,否则会造成内存泄露;

  • /**
    * This function will release a memory block
    *
    * @param block the address of memory block to be released
    */
    void rt_mp_free(void *block)

内存池应用示例

  • 创建一个静态的内存池对象,2 个动态线程。一个线程会试图从内存池中获得内存块,另一个线程释放内存块内存块;
  • #include <rtthread.h>
    
    static rt_uint8_t *ptr[];
    static rt_uint8_t mempool[];
    static struct rt_mempool mp; #define THREAD_PRIORITY 25
    #define THREAD_STACK_SIZE 512
    #define THREAD_TIMESLICE 5 /* 指向线程控制块的指针 */
    static rt_thread_t tid1 = RT_NULL;
    static rt_thread_t tid2 = RT_NULL; /* 线程 1 入口 */
    static void thread1_mp_alloc(void *parameter)
    {
    int i;
    for (i = ; i < ; i++)
    {
    if (ptr[i] == RT_NULL)
    {
    /* 试图申请内存块 50 次,当申请不到内存块时,
    线程 1 挂起,转至线程 2 运行 */
    ptr[i] = rt_mp_alloc(&mp, RT_WAITING_FOREVER);
    if (ptr[i] != RT_NULL)
    rt_kprintf("allocate No.%d\n", i);
    }
    }
    } /* 线程 2 入口,线程 2 的优先级比线程 1 低,应该线程 1 先获得执行。*/
    static void thread2_mp_release(void *parameter)
    {
    int i; rt_kprintf("thread2 try to release block\n");
    for (i = ; i < ; i++)
    {
    /* 释放所有分配成功的内存块 */
    if (ptr[i] != RT_NULL)
    {
    rt_kprintf("release block %d\n", i);
    rt_mp_free(ptr[i]);
    ptr[i] = RT_NULL;
    }
    }
    } int mempool_sample(void)
    {
    int i;
    for (i = ; i < ; i ++) ptr[i] = RT_NULL; /* 初始化内存池对象 */
    rt_mp_init(&mp, "mp1", &mempool[], sizeof(mempool), ); /* 创建线程 1:申请内存池 */
    tid1 = rt_thread_create("thread1", thread1_mp_alloc, RT_NULL,
    THREAD_STACK_SIZE,
    THREAD_PRIORITY, THREAD_TIMESLICE);
    if (tid1 != RT_NULL)
    rt_thread_startup(tid1); /* 创建线程 2:释放内存池 */
    tid2 = rt_thread_create("thread2", thread2_mp_release, RT_NULL,
    THREAD_STACK_SIZE,
    THREAD_PRIORITY + , THREAD_TIMESLICE);
    if (tid2 != RT_NULL)
    rt_thread_startup(tid2); return ;
    } /* 导出到 msh 命令列表中 */
    MSH_CMD_EXPORT(mempool_sample, mempool sample);

    运行结果:

  •  \ | /
    - RT - Thread Operating System
    / | \ 3.1. build Aug
    - Copyright by rt-thread team
    msh >mempool_sample
    msh >allocate No.
    allocate No.
    allocate No.
    allocate No.
    allocate No.

    allocate No.
    allocate No.
    thread2 try to release block
    release block
    allocate No.
    release block
    allocate No.
    release block
    release block
    release block
    release block

    release block
    release block
    release block

参考

  • 《RT-Thread 编程指南》

RT-Thread--内存管理的更多相关文章

  1. Java自动内存管理机制学习(一):Java内存区域与内存溢出异常

    备注:本文引用自<深入理解Java虚拟机第二版> 2.1 运行时数据区域 Java虚拟机在执行Java程序的过程中把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创 ...

  2. <Linux内核源码>内存管理模型

    题外语:本人对linux内核的了解尚浅,如果有差池欢迎指正,也欢迎提问交流! 首先要理解一下每一个进程是如何维护自己独立的寻址空间的,我的电脑里呢是8G内存空间.了解过的朋友应该都知道这是虚拟内存技术 ...

  3. JVM内存管理:深入Java内存区域与OOM

    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 概述: 对于从事C.C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝 ...

  4. Unity游戏开发中的内存管理_资料

    内存是手游的硬伤——Unity游戏Mono内存管理及泄漏http://wetest.qq.com/lab/view/135.html 深入浅出再谈Unity内存泄漏http://wetest.qq.c ...

  5. jvm--3.内存管理

    5.JVM内存管理 JAVA虚拟机在执行java程序的过程中,会把它管理的内存分成若干个不同的数据区域. ----------------------------------------------- ...

  6. Linux堆内存管理深入分析(上)

    Linux堆内存管理深入分析(上半部) 作者:走位@阿里聚安全   0 前言 近年来,漏洞挖掘越来越火,各种漏洞挖掘.利用的分析文章层出不穷.从大方向来看,主要有基于栈溢出的漏洞利用和基于堆溢出的漏洞 ...

  7. iOS开发系列—Objective-C之内存管理

    概述 我们知道在程序运行过程中要创建大量的对象,和其他高级语言类似,在ObjC中对象时存储在堆中的,系统并不会自动释放堆中的内存(注意基本类型是由系统自己管理的,放在栈上).如果一个对象创建并使用后没 ...

  8. Android 内存管理 &Memory Leak & OOM 分析

    转载博客:http://blog.csdn.net/vshuang/article/details/39647167 1.Android 进程管理&内存 Android主要应用在嵌入式设备当中 ...

  9. 【转】Java内存管理:深入Java内存区域

    转自:http://www.cnblogs.com/gw811/archive/2012/10/18/2730117.html 本文引用自:深入理解Java虚拟机的第2章内容 Java与C++之间有一 ...

  10. DPDK内存管理(1)

    1 前言 DPDK将利用hugepage预留的物理内存统一的组织管理起来,然后以库的方式对外提供使用的接口.下图展示了DPDK中内存有关的模块的相互关系. rte_eal            是统一 ...

随机推荐

  1. 【 Linux 】Systemd 配置文件说明及编写(2)

    1. 开机启动 对于支持 systemd 的软件,如果想要设置开机启动,就执行如下命令(以 http 为例): systemctl enable httpd 上面的命令相当于在 /etc/system ...

  2. Centos 更改MySQL5.7数据库目录位置

    原文地址:https://blog.csdn.net/zyw_java/article/details/78512285 Centos7.3 安装Mysql5.7并修改初始密码 基于 CentOS M ...

  3. MySQL索引对NULL值的处理

    # 索引不会包含有NULL值的列 只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的.所以我们在数据库设计时不要让字段的默认值为NU ...

  4. 【Luogu P3919】可持久化数组

    数组是一种单点修改,单点查询的基础数据结构. 如果要对数组改进,使之可持久化,那么显然我们需要利用其它的数据结构来改进它. 对于单点修改和单点查询两种操作,很容易发现可持久化线段树也是支持这种操作的. ...

  5. idea设置打开文件窗口个数

    idea默认是限制打开10个,超过10个后,前面打开的就会被关闭,有时候跟踪的类多了就不太友好了.

  6. [NPM错误]npm ERR! Unexpected end of JSON input while parsing near ‘’

    [错误描述] npm ERR! Unexpected end of JSON input while parsing near ‘  ’ [前提描述] 在安装vue2-editor时,中断暂停了,再次 ...

  7. Word表格和文本自由互换

    未完 ...... 点击访问原文(进入后根据右侧标签,快速定位到本文)

  8. Spyder中报错: Check failed: PyBfloat16_Type.tp_base != nullptr

    报错问题: 问题1:tensorflow/python/lib/core/bfloat16.cc:675] Check failed: PyBfloat16_Type.tp_base != nullp ...

  9. 18 JSON、JSON字符串、反序列化

    JSON教程 : https://www.runoob.com/python/python-json.html 概念 JSON是一种轻量级的数据交换格式,它是一种数据格式! JSON易于阅读.易于解析 ...

  10. T-SQL学习笔记

    学习T-SQL时记录的笔记,记得并不全也不详细 if和while语句 declare @age int select @age = DATEDIFF(year,stuAge,getdate()) fr ...