C++ | 关于STL中的空间配置器 | 源码剖析

news2024/9/28 13:28:26

文章目录

    • 为什么需要空间配置器
    • 一级空间配置器
    • 二级空间配置器
      • 内存池解析
      • refill 填充内存池
      • chunk_alloc 申请堆空间
      • deallocate 资源的归还
      • 空间配置器的再次封装
      • 空间配置器与容器的结合

我们知道在C和C++中都有关于内存管理的问题,C语言用malloc和free这两个函数体现内存管理,C++用new和delete这两个操作符体现内存管理。那么内存管理到底是什么?管理体现在哪些方面?当我们需要存储大量数据时,栈区的容量就有些不够看了,此时我们必须将数据存储在堆区,可以简单的认为电脑的内存有多大,堆区就有多大。但是使用堆区就存在一个问题——内存管理,我们需要主动的向堆区申请资源存储数据,不需要使用资源时要把资源归还,这样的申请与归还操作将消耗系统资源,拖慢程序的运行速度,此外对于申请到的资源如何使用也是需要考虑的,充分利用堆区资源将是对程序的一种性能优化。在C++中,我们可以直接向堆区申请资源(使用VirtualAlloc函数),但是这是一种直接的方式,为了方便内存管理,我们需要设置这块资源的相关属性,C++为我们提供了一对接口,new和delete,我们可以通过new和delete解放我们对内存的管理工作,唯一需要注意的是对于new的资源有没有delete释放。

对于STL中的容器,大部分容器是在堆区上存储数据,频繁的资源申请与释放将浪费系统的大量性能,对此STL设计出空间配置器,当数据的字节大小小于128字节时,不再直接使用new获得资源,而是通过空间配置器。在这里插入图片描述
可以看到容器的构造函数中有关于空间配置器的选项,并且是默认的,平常使用STL的容器时默认使用库中的空间配置器,对此并不影响我们对STL的学习,如果你认为STL库中的空间配置器效率还是低,那么你就可以传入一个你自己的空间配置器,只要提供的接口名称符合要求

为什么需要空间配置器

开始我们有大概的说过,使用空间配置器可以提高程序的性能,减少程序出错的可能性,如果不使用空间配置器,程序可能会存在这几个问题:

空间的申请与释放由用户自己把握,可能造成内存泄漏
频繁向系统申请小块的内存,可能造成内存碎片,还可能降低程序的性能
使用new和malloc的申请操作,需要额外使用资源记录所申请资源的信息
如果空间申请失败,需要用户自己处理异常

除此之外还有代码复用性,线程安全等问题,对此针对需要频繁使用的容器,我们需要设计一套高效的内存管理机制来优化程序

一级空间配置器

对于直接使用new或者malloc会出现的问题,最主要的还是由申请小块内存导致的内存碎片问题,针对这个问题SGI版本下的STL空间配置器以128为大块内存和小块内存的分界线,将空间配置器分为两级,一级空间配置器处理大块内存,二级空间配置器处理小块内存

template <int inst>
class __malloc_alloc_template
{
private:
    static void* oom_malloc(size_t);
public:
    // 对malloc的封装
    static void* allocate(size_t n)
    {
        // 申请空间成功,直接返回,失败交由oom_malloc处理
        void* result = malloc(n);
        if (0 == result)
            result = oom_malloc(n);
        return result;
    }
    // 对free的封装
    static void deallocate(void* p, size_t /* n */)
    {
        free(p);
    }
    // 设置新的处理方法,返回旧的处理方法
    // 该函数的参数为函数指针void (*)(),返回值类型也为函数指针void(*)()
    // void (*   set_malloc_handler( void (*f)() ) )()
    static void (*set_malloc_handler(void (*f)()))()
    {
    	// __malloc_alloc_oom_handler是一个全局函数
        void (*old)() = __malloc_alloc_oom_handler;
        __malloc_alloc_oom_handler = f;
        return(old);
    }
};

// malloc申请空间失败时代用该函数(静态成员函数的类外实现)
template <int inst>
void* __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
	// 函数指针变量的创建
    void (*my_malloc_handler)();
    void* result;
    for (;;)
    {
        // 检测用户是否设置空间不足应对措施,如果没有设置,抛异常,模式new的方式
        my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == my_malloc_handler)
        {
        	// 抛异常
            __THROW_BAD_ALLOC;
        }

        // 如果设置,执行用户提供的空间不足应对措施
        (*my_malloc_handler)();

        // 继续申请空间,可能会申请成功
        // 用类型为void*的变量接收函数返回值
        // 返回值是malloc申请成功后的堆区地址,将其返回
        result = malloc(n);
        if (result)
            return(result);
    }
}
typedef __malloc_alloc_template<0> malloc_alloc;
static void (* __malloc_alloc_oom_handler)();

上面的代码是SGI版本的一级空间配置器,可以看到无论它怎么复杂,本质就是对malloc和free的封装,allocate函数封装了malloc,如果malloc失败将调用一个处理失败函数,如果用户没有设置该函数,程序抛异常。deallocate函数封装了free。

二级空间配置器

当申请的空间大小大于128字节时,STL的容器将使用一级空间配置器,说白了就是直接malloc和free(new的delete底层也是封装了malloc和free),当申请的空间大小小于等于128字节时,程序将使用二级空间配置器,二级空间配置器可以很好的应对由于频繁申请小内存导致的内存碎片问题,达到高效的管理内存

先大概的讲解二级空间配置器的实现:二级空间配置器封装了一个内存池,并且还有两个维护内存池的指针,指针之间的区域就是内存池,但是我们并不是直接对内存池进行访问,而是通过一个哈希桶对内存池间接的访问

// 用来标记内存池中大块内存的起始和结束地址
static char *start_free;
static char *end_free;
// 记录空间配置器拥有的内存块数量
static size_t heap_size;
// 存储地址的哈希桶
static obj *  free_list[__NFREELISTS];

start_free和end_free是两个char类型指针,它们之家的区域是一块可以使用的内存空间,这块空间是通过malloc从堆上申请的在这里插入图片描述
内存管理接口之间的逻辑关系大概是上图这样的,空间配置器是基于malloc之上的一套内存管理机制,一级配置器直接是对malloc的封装,但是二级配置器也需要调用malloc拿到内存空间,start_free和end_free之间的空间就是从malloc中拿到的

先来看一些重要的成员

template <int inst>
class __default_alloc_template
{
private:
    enum { __ALIGN = 8 };     // 使用户申请的内存为ALIGN的值或者倍数倍
    enum { __MAX_BYTES = 128 };   // 大小内存块的分界线
    enum { __NFREELISTS = __MAX_BYTES / __ALIGN };  // 需要使用的桶个数

  // 如果用户所需内存块不是8的整数倍,向上对齐到8的整数倍
    static size_t ROUND_UP(size_t bytes)
    {
        return (((bytes)+__ALIGN - 1) & ~(__ALIGN - 1));
    }

private:
    // 用union维护链表结构
    union obj
    {
        union obj* free_list_link;
        char client_data[1];    /* The client sees this.       */
    };

    // 哈希函数,根据用户提供字节数找到对应的桶号
    static  size_t FREELIST_INDEX(size_t bytes)
    {
        return (((bytes)+__ALIGN - 1) / __ALIGN - 1);
    }
    //...
};

这里说一下其中关于链表的结构设计,不像传统的链表额外使用一个指针变量保存节点所指向的下一个节点的地址值,STL空间配置器的链表设计十分巧妙地使用联合体union设计,联合体obj的前4个字节保存了节点指向的下一个节点的地址在这里插入图片描述
就像图片中一样,空间配置器中的链表将节点的前4个字节作为一个指针变量,存储了下一个节点的地址,具体的使用都会再说

内存池解析

static void* allocate(size_t n)
{
    obj* __VOLATILE* my_free_list;
    obj* __RESTRICT result;
    // 检测用户所需空间释放超过128(即是否为小块内存)
    if (n > (size_t)__MAX_BYTES)
    {
        // 不是小块内存交由一级空间配置器处理
        return (malloc_alloc::allocate(n));
    }

    // 根据用户所需字节找到对应的桶号
    my_free_list = free_list + FREELIST_INDEX(n);
    result = *my_free_list;

    // 如果该桶中没有内存块时,向该桶中补充空间
    if (result == 0)
    {
        // 将n向上对齐到8的整数被,保证向桶中补充内存块时,内存块一定是8的整数倍
        void* r = refill(ROUND_UP(n));
        return r;
    }

    // 维护桶中剩余内存块的链式关系
    *my_free_list = result->free_list_link;
    return (result);
};

这是二级空间配置器的空间分配函数allocate,可以看到只要申请的内存块大于128B,allocate就会调用一级空间配置器,也就是malloc函数。只要申请的内存块小于等于128B,allocate就会使用内存池进行内存管理。先通过FREELIST_INDEX得到要申请的内存块在哈希桶中的下标,然后对哈希桶的首地址加上这个下标,最后解引用就能锁定对应的哈希桶并得到可以使用的地址,如果解引用后的result是空(也就是0),说明当前哈希桶还未向内存池申请内存,此时程序会调用refill函数,为哈希桶分配内存(将地址挂上哈希桶),然后将可以使用的地址返回。如果哈希桶中有内存可以使用,函数将对该位置进行头删,然后被删除的地址在这里插入图片描述
上图就是哈希桶,我们通过哈希桶维护内存池,进而使用内存池,假设现在要申请7个字节的空间,调用allocate函数,函数看7字节是块小内存,不需要调用malloc函数,只用使用内存池中的资源。函数先算出桶号,7字节要去1号桶申请8字节空间(拿到一个地址),假设现在的哈希桶情况与上图一样,1号桶下挂了三个地址(哈希桶上挂的是地址,并不是内存块,我们从该地址向后使用是不会越界的,比如你要申请25字节空间,函数会向上取整,给你8的倍数32字节空间,但是你最好只使用该地址往后25字节),可以看到每一个桶都是一串链表结构,如果桶不为空,说明桶中至少存储了一个节点的地址(这个地址与往后4字节地址之间组成的数据也是一串地址,指向了下一个节点,这是刚才提到的联合体结构,如果到了链表尾,前4个字节的地址为空)。可以看到1号桶不为空,函数将这个桶指向后一个节点,也就是头删,最后保存第一个节点的地址到result中,返回result变量,这个地址往后8字节空间是可以自由使用的。

refill 填充内存池

// 函数功能:从内存池中向哈希桶中补充空间
// 参数n:小块内存字节数
// 返回值:首个小块内存的首地址
template <int inst>
void* __default_alloc_template<inst>::refill(size_t n)
{
    // 一次性向内存池索要20个n字节的小块内存
    int nobjs = 20;
    char* chunk = chunk_alloc(n, nobjs);

    obj** my_free_list;
    obj* result;
    obj* current_obj, * next_obj;
    int i;
    // 如果只要了一块,直接返回给用户使用(chunk_alloc会修改nobjs的值)
    if (1 == nobjs)
        return(chunk);

    // 找到对应的桶号
    my_free_list = free_list + FREELIST_INDEX(n);
    // 将第一块返回值用户,其他块连接在对应的桶中
    result = (obj*)chunk; // 先保存返回值
    // 记录下一个节点的地址并将其存储到链表的头部
    *my_free_list = next_obj = (obj*)(chunk + n);
    for (i = 1; ; i++)
    {
    	// 当前节点的更新
        current_obj = next_obj;
        // 下一个节点的更新
        next_obj = (obj*)((char*)next_obj + n);       
        if (nobjs - 1 == i)
        {
        	// 此时是最后一个节点,将其指针域置空
            current_obj->free_list_link = 0;
            break;
        }
        else
        {
        	// 将当前节点指针域指向下一节点地址
            current_obj->free_list_link = next_obj;
        }
    }
	// 将申请到的第一个节点返回
    return(result);
}

chunk_alloc 申请堆空间

填充内存池时,调用了chunk_alloc函数,该函数将整合哈希桶剩下的资源,剩下的资源不够会去申请内存池的资源(start_free和end_free维护了内存池可以申请的资源),如果内存池的资源也不够,函数会调用malloc向堆区申请空间

template <int inst>
char* __default_alloc_template<inst>::chunk_alloc(size_t size, int& nobjs)
{
	// 注意nobjs是一个引用
    // 计算nobjs个size字节内存块的总大小以及内存池中剩余空间总大小
    char* result;
    size_t total_bytes = size * nobjs;         // 需要向内存池申请的总字节数
    size_t bytes_left = end_free - start_free; // 内存池可以申请的资源所剩余的字节数(肯定是8的倍数)
    // 如果内存池可以提供total_bytes字节,直接返回
    if (bytes_left >= total_bytes)
    {
    	// 将start_free返回
        result = start_free;
        // 维护start_free
        start_free += total_bytes;
        return(result);
    }
    // 无法提供所有内存块,但是至少可以提供1块size字节内存块,修改nobjs后将资源返回
    else if (bytes_left >= size)
    {        
    	// 计算剩下的资源可以提供内存块的数量,修改nobjs的值
        nobjs = bytes_left / size;
        // 计算可以提供的资源总字节数
        total_bytes = size * nobjs;
        // result的返回和start_free的维护
        result = start_free;
        start_free += total_bytes;
        return(result);
    }
    else
    {
        // 内存池空间不足,连一块小块内存块都不能提供
		// 向系统堆求助,往内存池中补充空间
        // 计算向内存中补充空间大小:本次空间总大小两倍 + 向系统申请总大小/16
        size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);

        // 如果内存池有剩余空间(该空间一定是8的整数倍),将该空间挂到对应哈希桶中
        // 不要浪费之前申请的资源
        if (bytes_left > 0)
        {
            // 找对用哈希桶,将剩余空间挂在其上
            // 这是一个头插
            obj** my_free_list = free_list + FREELIST_INDEX(bytes_left);
            ((obj*)start_free)->free_list_link = *my_free_list;
            *my_free_list = (obj*)start_free;
        }

        // 通过系统堆向内存池补充空间,如果补充成功,递归继续分配
        start_free = (char*)malloc(bytes_to_get);
        if (0 == start_free)
        {
            // 通过系统堆补充空间失败,这种情况出现的概率较小,在哈希桶中找是否有没有使用的较大的内存块
            int i;
            obj** my_free_list, * p;
            for (i = size; i <= __MAX_BYTES; i += __ALIGN)
            {
                my_free_list = free_list + FREELIST_INDEX(i);
                p = *my_free_list;

                // 如果有,将该内存块补充进内存池,递归继续分配
                if (0 != p)
                {
                    *my_free_list = p->free_list_link;
                    start_free = (char*)p;
                    end_free = start_free + i;
                    return(chunk_alloc(size, nobjs));
                }
            }

            // 山穷水尽,只能向一级空间配置器求助
    	    // 注意:此处一定要将end_free置空,因为一级空间配置器一旦抛异常就会出问题
            end_free = 0;
            start_free = (char*)malloc_alloc::allocate(bytes_to_get);
        }

        // 通过系统堆向内存池补充空间成功,更新信息并继续分配
        heap_size += bytes_to_get; // 修改哈希桶的总大小
        end_free = start_free + bytes_to_get; // 更新指针以维护内存池
        return(chunk_alloc(size, nobjs)); // 递归调用,现在的内存池已经有了充足的资源
    }
}

deallocate 资源的归还

// 函数功能:将申请的空间归还给空间配置器
// 参数:p需要归坏空间的首地址   n空间的总大小
static void deallocate(void *p, size_t n)
{
	obj *q = (obj *)p;
	obj ** my_free_list;
	// 如果空间是大块内存,将其交给一级空间配置器,用free释放
	if (n > (size_t) __MAX_BYTES)
	 {
	     malloc_alloc::deallocate(p, n);
	     return;
	 }
	// 如果是小块内存,找到对应的哈希桶,将内存挂在哈希桶中(归还的内存一定是8的整数倍)
	// 链表的头插操作
	my_free_list = free_list + FREELIST_INDEX(n);
	q -> free_list_link = *my_free_list;
	*my_free_list = q;
}

空间配置器的再次封装

typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;

template <class T, class Alloc>
class simple_alloc {

public:
	// 申请n个对象的空间
    static T *allocate(size_t n)
                { return 0 == n? 0 : (T*) Alloc::allocate(n * sizeof (T)); }
    // 申请一个对象的空间
    static T *allocate(void)
                { return (T*) Alloc::allocate(sizeof (T)); }
    // 释放n个对象的空间            
    static void deallocate(T *p, size_t n)
                { if (0 != n) Alloc::deallocate(p, n * sizeof (T)); }
    // 释放一个对象的空间
    static void deallocate(T *p)
                { Alloc::deallocate(p, sizeof (T)); }
};

STL用simple_alloc这个类封装了空间配置器的资源申请和资源释放接口,这个封装只要传入需要申请的块的个数n,也就是需要创建几个对象,不需要我们用sizeof算出一个类型的大小,这样封装就是方便了我们的使用

空间配置器与容器的结合

其实讲了这么多,空间配置器对外暴露的最主要的接口就是allocate和deallocate,allocate接收要需要分配的字节大小,将一串地址返回。deallocate接收需要返回的地址和其字节大小,将其释放。只是空间配置器对这两个接口进行了封装设计。

STL一开始就会向二级空间配置器索要内存,如果索要的内存大于128字节,二级空间配置器会调用一级空间配置器,一级空间配置器直接调用malloc向堆区申请空间。如果索要的内存小于等于128字节,那么二级空间配置器就会从哈希桶中查找内存池现在是否有相应的资源,二级空间配置器也是空间配置器最主要的结构。二级空间配置器通过哈希桶与内存池交互,哈希桶在STL与内存池之间就是一个管理的角色,STL通过哈希桶查找其需要的资源是否存在,内存将其资源分配给哈希桶供STL使用。

作为STL和内存池交互的媒介,我们需要针对哈希桶设计出一套高效的机制以提高内存的申请速度,针对哈希桶的算法就通过refill填充桶,chunk_alloc向堆区申请资源以及deallocate归还分配的资源体现。当STL的容器调用二级空间配置器的allocate申请资源时,函数会将用户需要申请的资源的字节数向上取整,得到8的整数,然后在哈希桶的对应位置查找,如果哈希桶上没有资源(没有地址),allocate会调用refill将内存池的资源填充到哈希桶中,其本质就是将内存地址挂到对应的哈希桶上。其中向内存池申请资源的操作将由chunk_alloc函数完成,如果内存池此时的资源不足以用来分配。chunk_alloc就会调用malloc函数向堆区申请一大块资源,生成新的内存池,注意内存池由start_free和end_free两个指针维护,我们只需要修改这两个指针就可以维护一块内存池。

当哈希桶上有了资源,allocate就会将对应哈希桶上的地址返回给用户。因为这些地址在哈希桶上以链表的形式维护,用户申请资源对应着链表的头删,归还资源对应着链表的头插。要注意的是链表节点之间的连接方式,由于在哈希桶中节点不是用来存储数据,或者说节点存储的是与其他节点间的连接关系,所以我们没有为节点的指针域额外开辟空间,因为每个节点至少向后8字节的空间是可以使用的,我们直接用这些空间存储下一个节点的地址,就算一个节点只能只用8字节,在64系统下也能运行。具体的实现可以看之前我列出的联合体结构

我们知道不论是一级还是二级的空间配置器,其allocate接口返回的都是一个地址,与malloc的返回值很像,使用allocate需要我们传入需要申请的总字节数,为了减少代码量,STL封装了一个simple_alloc,我们只要传入需要申请的对象个数,那么STL中具体的容器是怎么使用空间配置器的呢?

template <class T, class Alloc = alloc>
class list
{
    // ...
    // 实例化空间配置器
    typedef simple_alloc<list_node, Alloc> list_node_allocator;
    // ...
protected:
    link_type get_node()
    {
        // 调用空间配置器接口先申请节点的空间
        return list_node_allocator::allocate();
    }
    // 将节点归还给空间配置器
    void put_node(link_type p)
    {
        list_node_allocator::deallocate(p);
    }
    // 创建节点
    link_type create_node(const T& x)
    {
    	// 调用allocate接口申请空间
        link_type p = get_node();
        // 再对节点进行构造
        construct(&p->data, x);
        return p;
    }
    // 销毁节点
    void destroy_node(link_type p)
    {
    	// 先释放节点中的资源
        destroy(&p->data);
        // 再把节点归还给哈希桶
        put_node(p);
    }
    // ...
    iterator insert(iterator position, const T& x)
    {
    	// 资源的申请
        link_type tmp = create_node(x);
        tmp->next = position.node;
        tmp->prev = position.node->prev;
        (link_type(position.node->prev))->next = tmp;
        position.node->prev = tmp;
        return tmp;
    }
	iterator erase(iterator position) 
	{
		link_type next_node = link_type(position.node->next);
		link_type prev_node = link_type(position.node->prev);
		prev_node->next = next_node;
		next_node->prev = prev_node;
		// 资源的销毁
		destroy_node(position.node);
		return iterator(next_node);
	}
	// ...
};

可以看到容器的类模板有两个参数,一个是容器存储的数据类型,一个则是默认的空间配置器类型,紧接着就是对simple_alloc类模板的重定义,将存储的数据类型T和配置器类型作为其参数,将其重定义为list_node_allocator,当容器需要存储数据时,就会调用list_node_allocator的allocate函数和节点的构造函数在申请的资源上存储数据。当容器不需要存储数据,将其删除时,会先将申请的资源上的数据资源释放(万一它的资源也是在堆区呢?),最后调用list_node_allocator的deallocate函数,释放所申请的资源(将地址归还,挂回哈希桶)

还值注意的是,在多线程的情况下,多线程需要使用同一个空间配置器,也就是说,我们实例化类模板时只能实例化出一个对象,这不就是单例模式吗?但是STL的空间配置器没有实现成常见的饿汉模式,而是将所有成员用static声明,当类的所有成员是静态时,这何尝不是一种单例呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/174990.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ClassLoader-在spring中的应用

背景标题起的挺大&#xff0c;忽悠人的。其实是我跟着视频学习手写模拟spring底层原理中遇到的问题&#xff0c;关于classLoader的几行代码&#xff0c;不知道是什么意思&#xff0c;所以特地来记下笔记。关于ClassLoader我好像在遥远的几年前看深入理解虚拟机时看到过&#xf…

Datawhale 202301 设计模式 | 第二章 人工智能 现代方法 智能体

智能体和环境 理性智能体 (rational agent) 需要为取得最佳结果或在存在不确定性时取得最佳期望结果而采取行动。 任何通过传感器(sensor) 感知 环境(environment) 并通过 执行器(actuator) 作用于该环境 的事物都可以被视为 智能体(agent) 。 行为 理性智能体 (rational ag…

Linux常用命令——systemctl命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) systemctl 系统服务管理器指令 补充说明 systemctl命令是系统服务管理器指令&#xff0c;它实际上将 service 和 chkconfig 这两个命令组合到一起。 任务旧指令新指令使某服务自动启动chkconfig --level 3 ht…

属性值的计算过程 css样式显示的计算过程 页面的渲染流程

目录属性值的计算过程属性值计算过程简介通过例子来理解&#xff1a;详细解释&#xff1a;方法例子属性值的计算过程 一个元素一个元素依次渲染&#xff0c;顺序按照页面文档的树形目录结构进行 渲染每个元素的前提条件&#xff1a;该元素的所有CSS属性必须有值 一个元素&am…

数学魔法结局:muldiv

介绍了一些棘手的数学魔法&#xff0c;但我一直没有抽出时间说出妙语。目标是计算 同时正确处理溢出。我们的秘密武器是 EVM 的mulmod指令。这条指令完全符合我们的要求&#xff0c;只是它返回的是余数而不是商。那么我们的策略是什么&#xff1f; 计算 512 位乘积一种⋅b使用…

【数据结构】6.5 图的遍历

文章目录遍历定义深度优先搜索(DFS)算法步骤邻接矩阵上的遍历邻接矩阵深度优先算法DFS算法效率分析广度优先搜索(BFS)邻接表的广度优先算法BFS算法效率分析DFS与BFS算法效率比较遍历定义 和树的遍历类似&#xff0c;图的遍历也是从图中的某一个顶点出发&#xff0c;按照某种方法…

UPS BP650CH实现nas自动关机

家里有个自己拼凑的nas需要防止断电不正常关机&#xff0c;因此购买了施耐德后背式BP650CH&#xff0c;之所以选这款是因为带了串口&#xff0c;串口终究还是很方便的东西。不管linux还是window还是其他系统都能够使用&#xff0c;通过串口直接获得ups的信息&#xff0c;就不需…

JDBC Maven MyBatis

文章目录JDBC&#xff08;Java Database Connectivity&#xff09;入门API详解DriverManger&#xff08;驱动管理类&#xff09;Connection(数据库连接对象)作用StatementResultSet&#xff08;结果集对象&#xff09;PreparedStatement连接池MavenMaven模型Maven 常用命令依赖…

简单二叉树的介绍

1.树的结构&#xff08;了解&#xff09;1.1概念树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0)个有限节点总成一个具有层次关系的集合。把它叫做树是因为它看起来像一颗倒挂的树&#xff0c;也就是说它的根是朝上&#xff0c;而叶子是朝下的&#xff08;本人…

工作玩手机识别监测系统 YOLOv5

工作玩手机识别监测系统通过YOLOV5网络深度学习算法模型对画面中人员玩手机行为进行实时监测&#xff0c;当识别到有人在玩手机行为时&#xff0c;无需人为干预立即抓拍存档触发告警。YOLO算法- YOLO算法是一种基于回归的算法&#xff0c;它不是选择图像中有趣的部分&#xff0…

WT588D语音芯片介绍

WT588D语音芯片简介WT588D 语音芯片是一款功能强大的可重复擦除烧写的语音单片机芯片。WT588D 让语音芯片不再为控制方式而寻找合适的外围单片机电路&#xff0c;高度集成的单片机技术足于取代复杂的外围控制电路。配套WT588DVoiceChip 上位机操作软件可随意更换WT588D 语音单片…

基于 docker 搭建 mysql5.7 主从复制

安装 docker 的教程可以看我的另一篇文章&#xff0c;拉取 mysql 镜像的步骤也在里面&#xff0c;在这不再重复&#xff1a;https://blog.csdn.net/wanzijy/article/details/128695674 1. 主机搭建 因为本人虚拟机中已经存在了 mysql &#xff0c;所以在使用镜像创建容器的时…

【论文翻译】End-to-End Human Pose and Mesh Reconstruction with Transformers

【cvpr论文】End-to-End Human Pose and Mesh Reconstruction with Transformers (thecvf.com) 【github】microsoft/MeshTransformer: Research code for CVPR 2021 paper "End-to-End Human Pose and Mesh Reconstruction with Transformers" (github.com) 摘要 我…

学习笔记:Java 并发编程③

若文章内容或图片失效&#xff0c;请留言反馈。 部分素材来自网络&#xff0c;若不小心影响到您的利益&#xff0c;请联系博主删除。 视频链接&#xff1a;https://www.bilibili.com/video/av81461839配套资料&#xff1a;https://pan.baidu.com/s/1lSDty6-hzCWTXFYuqThRPw&am…

在甲骨文云容器实例(Container Instances)上部署Ubuntu Desktop

甲骨文云推出了容器实例&#xff0c;这是一项无服务器计算服务&#xff0c;可以即时运行容器&#xff0c;而无需管理任何服务器。 今天我们尝试一下通过容器实例部署Ubuntu Bionic Desktop。 创建容器实例 在甲骨文容器实例页面&#xff0c;单击"创建容器实例"&…

Java 笔试题

Java 笔试题目录概述需求&#xff1a;设计思路实现思路分析1.java 面试题参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for change,challenge Surviv…

分享151个PHP源码,总有一款适合您

PHP源码 分享151个PHP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 151个PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1T_Hs4j0t39b-Y8UWHmAKyw?pwd7ao0 提取码&#…

论文浅尝 | DB4Trans:数据库内置知识图谱嵌入模型训练引擎

笔记整理&#xff1a;柳鹏凯&#xff0c;天津大学硕士发表期刊&#xff1a;计算机学报 第45卷Vol.45, 第9期 No.9链接&#xff1a;http://cjc.ict.ac.cn/online/onlinepaper/lpk-202297212908.pdf动机知识图谱嵌入技术主要将知识图谱中的实体和关系嵌入到连续的向量空间中&…

Centos Java1.8+Nginx+redis+pgsql 手工配置

一、系统升级&#xff0c;安装系统常用工具及配置 1.1 升级软件及Centos 内核 yum update -y yum clean all cat /etc/redhat-release 1.2 安装虚拟机守护进程 yum install qemu-guest-agent -y 1.3 安装系统常用工具包 yum install lrzsz vim wget dnf -y 1.4关…

2023牛客寒假算法基础集训营3 -- E-勉强拼凑的记忆(贪心 + 二分)

题目如下&#xff1a; 题解 or 思路&#xff1a; 我们可以发现&#xff1a;除了 n2n 2n2 无解&#xff0c; 其他情况答案至少为 n12\frac{n 1}{2}2n1​ 答案在 n12\frac{n 1}{2}2n1​ 到 nnn 之间 我们可以假设 答案为 ansansans 最优摆放为&#xff1a; 所以可以二分去求…