关于一个C++项目:高并发内存池的开发过程(二)

news2024/11/19 4:46:30

文章目录

    • 内存释放操作的总述
    • thread cache
    • central cache
    • page cache
      • central cache的TODO实现
      • 何时维护这张映射表?
    • tc_dealloc的修改
    • 申请大内存的适配
    • 写在最后

上篇文章梳理了内存申请操作的流程,大概测试了一下,没有发现什么问题。这篇文章将梳理内存释放操作的流程,若申请操作中,有些细节没有把控好,那么释放操作将bug不断。有些bug我至今还在调试…所以,这篇文章的梳理,侧重点依然是逻辑结构。代码的细节可能存在问题,并且解决这些问题的过程与心得,也能够单独梳理成一篇文章,好好总结一番了。


内存释放操作的总述

当某些条件满足时,thread cache将闲置的内存块归还给central cache,central cache将span归还给page cache,page cache将小块的span合并成大块的span,从而减少外碎片的问题。内存释放的过程是内存块的不断整合,由少到多,由小到大,将零散的内存块重新整合成新的span。

(注意:一些参数大小固定,这里先展示出来)

static const size_t NFREELIST = 208;
static const size_t MAX_SIZE = 256 * 1024;
static const size_t PAGE_SHIFT = 12;
static const size_t NPAGES = 129;

thread cache

首先是thread cache的内存块归还,当FreeList的长度大于“该链表一次能申请的最大数量”时需要进行归还操作(当然,还可以添加更多的条件,使空闲内存块尽可能合理的归还),这些内存块被归还给central cache。

“该链表一次能申请的最大数量”由接口size_t SizeClass::_adapt_count(size_t block_bytes)获取。

可以注意到,我们无法很快的得知FreeList的长度。所以这里再为FreeList创建一个变量以保存其长度,并且修改相关push和pop接口,因为进行这些操作会改变链表的长度,以下给出FreeList到目前为止的实现:

class FreeList
{
private:
	void* _head = nullptr;
	size_t _fetch_count = 1;
	size_t _length = 0;
public:
	void _push_front(void* obj);
	void* _pop_front();
	bool _empty();
	// 将start到finish之间的内存块,头插到链表中
	void _range_push(void* start, void* finish, size_t count);
	// 将链表头部往后count块内存块删除,以输出型参数的方式返回区间中的头尾两块内存块
	void* _range_pop(size_t count);
	// 关于_fetch_count与_length的操作
	inline size_t _get_fetch_count() { return _fetch_count; }
	inline void _add_fetch_count() { ++_fetch_count; }
	inline size_t _get_length() { return _length; }
};

bool FreeList::_empty()
{
	return _head == nullptr;
}

void FreeList::_push_front(void* obj)
{
	if (obj)
	{
		next(obj) = _head;
		_head = (void*)obj;
		_length += 1;
	}
}

void* FreeList::_pop_front()
{
	if (_head != nullptr)
	{
		void* ret = _head;
		_head = next(_head);
		_length -= 1;
		return ret;
	}

	return nullptr;
}

void FreeList::_range_push(void* start, void* finish, size_t count)
{
	if (start && finish)
	{
		next(finish) = _head;
		_head = start;
		_length += count;
	}
}

void* FreeList::_range_pop(size_t count)
{
	void* start = _head;
	void* finish = start;
	for (size_t i = 0; i < count - 1; ++i)
	{
		finish = next(finish);
	}

	_head = next(finish);
	next(finish) = nullptr;

	_length -= count;
	return start;
}

通过_get_length()接口获取单链表长度后,与_get_fetch_count()接口返回的值进行比较。如果链表长度大于等于一次能获取的最大内存块数量,说明当前链表中有很多闲置的内存块没有使用,此时可以将一些内存块还给central cache。

FreeLlist的归还逻辑:调用链表的_range_pop接口(该接口会将count个内存块从链表中删除,并返回区间中第一块内存块的起始地址),获取被删除内存块区间的起始块地址后,以start为参数调用central cache的接收接口。该接口会将所有内存块插入到对应span中,该接口将在后续实现。

_range_pop只返回内存块区间中第一块内存块的地址,为什么不返回最后一块内存块的地址?由于区间的最后一块内存块的指针域指向空,所以判断指针域是否指向空就能得知内存块区间是否遍历结束。

修改thread cache的_deallocate接口:

void ThreadCache::_deallocate(void* obj, size_t block_size)
{
	if (obj && block_size <= MAX_SIZE)
	{
		size_t bucket_index = SizeClass::_get_index(block_size);
		FreeList& aim_list = _free_lists[bucket_index];
		aim_list._push_front(obj);

		// 链表长度大于等于一次能获取的最大内存块数量,需要归还
		if (aim_list._get_length() >= aim_list._get_fetch_count())
		{
			void* start = aim_list._range_pop(aim_list._get_fetch_count());

			// TODO调用central的接收接口
		}
	}
}

central cache

接着是实现central cache的接收接口:除了内存块区间的起始块地址,thread cache还需要将FreeList所处的桶号告知central cache,因为thread cache和central cache的哈希映射规则相同,所以FreeList的桶号不需要做转化。

内存区间中的每一块内存块可能属于不同的span,central cache需要将其归还到正确的span中。而我们只知道内存块的地址,如何通过内存块地址得知其位于的span?所以这里要设计算法实现内存块与span之间的映射,这个算法之后再实现。假设已经得知内存块位于的span,central cache要将其插入到span的FreeList中,当span的所有内存块都被归还时,central cache就要将该span向上交付,还给page cache。

如何得知span的内存块都被归还?这里为span添加一个成员变量:_used_count,表示该span下,被使用的内存块数量。取走span的内存块,该变量的值将增加。收回span的内存块,该变量的值将减少。当_used_count的值为0,说明没有线程使用该span的内存块。此时将central cache要把该span归还给page cache。

以下实现central cache的接收接口:

void CentralCache::_return_blocks_to_spans(void* start, size_t bucket_index)
{
	_span_lists[bucket_index]._mtx.lock();
	while (start)
	{
		Span* aim_span = TODO();// 将内存块地址映射为span的地址
		if (aim_span == nullptr)
		{
			std::cerr << "内存块没有从属的span" << std::endl;
		}
		else
		{
			next(start) = aim_span->_free_list;
			aim_span->_free_list = start;
			--(aim_span->_used_count);

			if (aim_span->_used_count == 0)
			{
				// TODO::向page归还span
				_span_lists[bucket_index]._erase(aim_span);
			}
		}
		start = next(start);
	}
	_span_lists[bucket_index]._mtx.unlock();
}

注意:高并发的场景下,线程需要加锁访问central cache。


page cache

接着实现TODO接口“page cache接收central cache归还的span”:这个接口需要接收一个span的地址,并尽可能地将该span和未使用的span进行合并,得到一个更大的span。如何合并出更大的span?这里要设计一个算法:查找与该span相邻的且未使用的span。

  • 相邻的span:这个相邻指的是物理内存上的相邻,也就是页号的相邻,不是在SpanList链表中的逻辑位置相邻。我们可以通过span的页号推测与其相邻的页号,将这些页号转换成span的地址,进而判断这些span是否在使用
  • 是否在使用:为Span添加一个bool类型的成员变量,true表示该span正在使用,false则相反
  • 页号和地址的转换:Span结构存储了页号,因此我们可以通过span的地址获取该span的起始页号。即span*->页号,但是我们不能通过页号获取span的地址,即页号->span*。因此我们要维护页号到span*的映射关系,可以用映射表unordered_map保存映射关系,只要page cache申请了span,就要为这些span建立页号和地址的映射关系

因此,我们可以根据页号查找映射表,从而得知该页是否被申请(表中是否存在该key值),以及被申请了,但是否被central cache使用(key值存在,根据value值进行下一步判断)?

先添加映射表以及相关操作:

class PageCache
{
private:
	PageCache() {}
	PageCache(const PageCache& x) = delete;
public:
	static PageCache* _get_instance() { return &_instance; }
	// 获取一个跨越k页的span
	Span* _fetch_kspan(size_t k);
	// 接收来自central归还的span
	void _return_span_to_spans(Span* span);
	// 将k个页和span建立映射关系
	void _insert_map(page_t page_id, size_t k, Span* span);
	// 删除一对映射关系
	void _erase_map(page_t page_id);
private:
	static PageCache _instance;
	SpanList _span_lists[NPAGES];
	std::recursive_mutex _rmtx;;
	std::unordered_map<page_t, Span*> _pageid_to_span;
};

// 这个操作大多是被其他函数调用,因为其他函数都加锁了,为防止死锁,这个函数不用加锁
void PageCache::_insert_map(page_t page_id, size_t k, Span* span)
{
	if (span)
	{
		for (size_t i = 0; i < k; ++i)
		{
			_pageid_to_span[page_id + i] = span;
		}
	}
	else
	{
		assert(false);
		std::cerr << "PageCache::_insert_map::span不正确" << std::endl;
	}
}

void PageCache::_erase_map(page_t page_id, size_t k)
{
	for (size_t i = 0; i < k; ++i)
	{
		auto it = _pageid_to_span.find(page_id);
		if (it != _pageid_to_span.end())
		{
			//std::cout << "删除了" << it->first << ",页数为:" << it->second->_n << std::endl;
			_pageid_to_span.erase(page_id);
		}
		else
		{
			assert(false);
			std::cerr << "PageCache::_erase_map::page_id不存在" << std::endl;
		}
	}
}

其中关于映射表的成员:

  • _pageid_to_span就是所谓的映射表
  • _map_span可以在映射表中为“从page_id往后的k个页”与“span”建立映射关系
  • _erase_map可以在映射表中删除“从page_id往后的k个页”与“span”的映射关系

然后为Span添加成员变量_is_used,其初始值为flase,表示该span未被使用:

struct Span
{
	Span* _prev = nullptr;
	Span* _next = nullptr;

	// 存储的内存页其实id与数量 
	page_t _id = 0;
	size_t _n = 0;

	// Span切好或合并后的单链表,以及分配出去的内存块数量
	void* _free_list = nullptr;
	size_t _used_count = 0;

	// 是否被使用
	bool _is_used = false;
};

接着实现page cache接收span的操作_return_span_to_spans:

  • 先删除映射表中该span的所有映射关系
  • 再判断是否能合并出更大的span
    • 往前合并:判断该span的前一页是否被申请且未被使用,同时合并后的span大小不超过128页,若满足以上条件,合并两个span。然后继续往前合并
    • 往后合并:同样的也是满足以上三个条件就可以进行后续的合并
  • 最后将合并好的span插入到SpanLlist中

需要注意的是,每次的合并都要删除映射表中,被合并span的映射关系:

void PageCache::_return_span_to_spans(Span* span)
{
	// 合并的过程需要加锁
	std::unique_lock<std::recursive_mutex> guard(_rmtx);
	if (span->_n > NPAGES - 1)
	{
		system_dealloc((void*)(span->_page_id << PAGE_SHIFT), span->_n << PAGE_SHIFT);
		return;
	}

	// 先删除该span的所有映射关系
	_erase_map(span->_page_id, span->_n);

	// 往前合并
	while (true)
	{
		page_t prev_page_id = span->_page_id - 1;

		auto it = _pageid_to_span.find(prev_page_id);
		if (it == _pageid_to_span.end()) break;

		Span* prev_span = it->second;
		
		if (prev_span->_is_used == true) break;
		if (prev_span->_n + span->_n > 128) break;

		span->_page_id = prev_span->_page_id;
		span->_n += prev_span->_n;

		// 删除被合并的span的映射关系
		_erase_map(prev_span->_page_id, 1);
		if (prev_span->_n != 1)
			_erase_map(prev_span->_page_id + prev_span->_n - 1, 1);
		
		// 删除SpanList中被合并的span
		_span_lists[prev_span->_n]._erase(prev_span);
		delete prev_span;
	}

	// 往后合并
	while (true)
	{
		page_t next_page_id = span->_page_id + span->_n;

		auto it = _pageid_to_span.find(next_page_id);
		if (it == _pageid_to_span.end()) break;

		Span* next_span = it->second;

		if (next_span->_is_used == true) break;
		if (next_span->_n + span->_n > 128) break;

		span->_n += next_span->_n;
		
		// 删除被合并的span的映射关系
		_erase_map(next_span->_page_id, 1);
		if (next_span->_n != 1)
			_erase_map(next_span->_page_id + next_span->_n - 1, 1);
		
		// 删除被合并的span
		_span_lists[next_span->_n]._erase(next_span);
		delete next_span;
	}

	span->_is_used = false;
	_insert_map(span->_page_id, 1, span);
	if (span->_n != 1)
		_insert_map(span->_page_id + span->_n - 1, 1, span);

	_span_lists[span->_n]._push_front(span);
}

central cache的TODO实现

回到central cache的接收接口_return_blocks_to_spans:

其中有一个TODO没有实现,该接口需要将内存块地址转换成span的地址。有了映射表,这个接口就可以实现了:

// 根据obj判断该内存块所属的span
Span* PageCache::_map_obj_to_span(void* obj)
{
	// 映射obj内存块时也需要加锁
	std::unique_lock<std::recursive_mutex> guard(_rmtx);

	page_t id = (page_t)obj >> PAGE_SHIFT;
	auto it = _pageid_to_span.find(id);
	if (it == _pageid_to_span.end())
	{
		return nullptr;
	}
	else
	{
		return it->second;
	}
}

page cache通过系统调用获取内存,而系统调用获取的内存以页为单位。一般情况下,页的大小为4kB或8kB,这里以4kB为例。系统调用返回的内存地址中,低12位是全0(因为4kB是基本单位),剩余位则用来表示页号。无论该页切分的内存块大小是多少,我们都能通过“将内存块地址右移12位”获取其属于的页号。有了页号,就能通过映射表找到其对应span的地址。


何时维护这张映射表?

什么时候要为页号和span*建立映射关系?当page的哈希桶申请128页的span时,需要建立映射吗?答案是需要,只要page cache申请了内存块,就要建立映射关系。然而是否要维护所有页的映射关系,则需要根据情况决定:

  • 当span在page cache中,未被使用central cache使用时,不需要维护所有页号与span*的映射关系,只要维护起始页和终止页与span*的映射关系即可。这两对映射关系将在合并span时被使用
  • 当span从page cache分配出去时,需要建立所有页号与span*的映射关系。这是因为cenctral cache在接收thread cache归还的内存块时,需要将内存块地址转换成span的地址
  • 如果某个span的跨越了大量的页,而映射表值只维护其起始页和终止页的映射关系,这将导致中间页的内存块无法找到其对应span

综上,未被分配的span只要将首尾页号与span建立映射关系,被分配出去的span需要将所有页号与span建立映射关系。

以下是_fetch_kspan接口的修改:

Span* PageCache::_fetch_kspan(size_t k)
{
	// 获取PageCache的span时需要加锁
	std::unique_lock<std::recursive_mutex> guard(_rmtx);
	if (!_span_lists[k]._empty())
	{
		Span* ret_span = _span_lists[k]._pop_front();
		_insert_map(ret_span->_page_id, k, ret_span);
		return ret_span;
	}
	else
	{
		for (int i = k + 1; i < NPAGES; ++i)
		{
			// 往后找更大的span,进行切分
			if (!_span_lists[i]._empty())
			{
				// 找到不为空的SpanList,获取第一个Span,该Span的大小为i个page
				Span* old_span = _span_lists[i]._pop_front();
				Span* ret_span = new Span;
				ret_span->_n = k;
				ret_span->_id = old_span->_id;

				old_span->_n -= k;
				old_span->_id += k;
				_span_lists[old_span->_n]._push_front(old_span);
				
				_insert_map(old_span->_page_id, 1, old_span);
				_insert_map(ret_span->_page_id, k, ret_span);

				ret_span->_is_used = true;
				return ret_span;
			}
		}

		//  没有更大的Span可以用,此时向堆区申请一块128page的空间
		void* ptr = system_alloc(128);
		
		Span* max_span = new Span;
		max_span->_id = (page_t)ptr >> PAGE_SHIFT;
		max_span->_n = 128;
		_span_lists[128]._push_front(max_span);

		// 128页的span映射关系的建立(首尾)
		_insert_map(max_span->_page_id, 1, max_span);
		_insert_map(max_span->_page_id + 127, 1, max_span);
		
		return _fetch_kspan(k);
	}
}

没有合适的span而进行切分更大的span时,由于切分方向是从前往后,所以对于被切分的span,只需要为其起始页添加映射关系即可。对于被分配出去的span,需要为其所有页添加映射关系。


tc_dealloc的修改

修改释放接口:使之不接收内存块大小,只接收内存块的起始地址。有了page cache的映射表,我们可以通过内存块地址获取其span地址。现在的问题是:我们需要知道该span切分的内存块大小是多少?只有知道内存块大小,我们才能找到thread cache中对应的桶,进而归还该内存块。所以我们需要为Span添加一个成员_block_size,表示该span切分的内存块大小:

struct Span
{
	// .....
	// 该span切分的内存块大小
	size_t _block_size = 0;
};

每次切分span成内存块(也就是这个接口:CentralCache::_get_span)时,都要维护该变量的值。

以下是利用PageCache::_map_obj_to_span接口改造的tc_deallocate:

static void tc_deallocate(void* obj)
{
	if (pTLSThreadCache)
	{
		Span* span = PageCache::_get_instance()->_map_obj_to_span(obj);
		size_t block_size = span->_block_size;
		pTLSThreadCache->_deallocate(obj, block_size);
	}
}

申请大内存的适配

由于thread cache的哈希桶中,最大的内存块大小为256kB。当线程申请的内存大于256kB时,需要重新设计内存申请和释放的过程,以满足此类需求。

而问题的本质在于:是否要贯穿我们设计的三层结构申请内存?一般情况下,申请小于等于256kB的内存块将贯穿三层结构。但我们设计的page cache的哈希桶能存储128页的span,当页的基本单位为4kB时,256kB也只使用了哈希桶的前64页,还有一半的哈希桶没有被使用。当然了,因为哈希桶的结构限制,大于256kB的内存申请不能贯穿central cache和thread cache,只能经过page cache层。我们知道page cache的合并span同样也是可以减少内存碎片的,所以我们应该尽可能的利用page cache,将大于256kB但小于等于128页的内存申请操作贯穿page cache层。至于大于128页的内存申请操作,这里直接调用系统接口即可,不使用page cache的哈希桶,只使用其映射表以完成内存的归还操作。

总结下,针对内存块大小,我们可以分成三种情况:

  • 小于等于256kB的内存块,申请操作将贯穿三层结构
  • 大于256kB但小于等于128页的内存块,申请操作只涉及page cahe,不涉及其他两层结构
  • 大于128页的内存块,直接调用系统接口

因此,我们需要对tc_allocate和tc_deallocate进行再设计:

static void* tc_allocate(size_t need_bytes)
{
	if (pTLSThreadCache == nullptr)
		pTLSThreadCache = new ThreadCache;

	void* obj = nullptr;
	if (need_bytes > MAX_SIZE)
	{
		size_t block_size = SizeClass::_round_up(need_bytes);
		Span* span = PageCache::_get_instance()->_fetch_kspan(block_size >> PAGE_SHIFT);
		obj = (void*)(span->_page_id << PAGE_SHIFT);
	}
	else
	{
		obj = pTLSThreadCache->_allocate(need_bytes);
	}
	
	return obj;
}

static void tc_deallocate(void* obj)
{
	if (pTLSThreadCache)
	{
		Span* span = PageCache::_get_instance()->_map_obj_to_span(obj);
		size_t block_size = span->_block_size;
		if (block_size > MAX_SIZE)
			PageCache::_get_instance()->_return_span_to_spans(span);
		else
			pTLSThreadCache->_deallocate(obj, block_size);
	}
}

接着是page cache的两个接口的修改。

关于page cache的_fetch_kspan接口:

  • 当申请的span页数超过128页时,直接调用系统接口,获取内存后向映射表添加映射关系(只添加起始页和span*的映射关系),最后返回
  • 当申请的span大小超过256kB时,操作需要贯穿page cache的哈希桶,但是需要添加起始页、终止页和span*的映射关系,因为合并span时需要这两对映射关系
  • 当申请的span大小小于等于256kB时,操作需要贯穿page cache的哈希桶,并添加所有页和span的映射关系
// 系统调用的封装
// Common.hpp
inline static void* system_alloc(size_t kpage)
{
#ifdef _WIN32
	void* p = VirtualAlloc(0, kpage << PAGE_SHIFT, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

#else
	// 其他系统申请内存的函数
#endif
	if (p == nullptr)
		throw std::bad_alloc();

	return p;
}

inline static void system_dealloc(void* ptr, size_t total_size)
{
#ifdef _WIN32
	VirtualFree(ptr, total_size, MEM_DECOMMIT);
#else
	// 其他系统
#endif 

}

Span* PageCache::_fetch_kspan(size_t k)
{
	// 获取PageCache的span时需要加锁
	std::unique_lock<std::recursive_mutex> guard(_rmtx);
	// 申请大于的span大于128页
	if (k > NPAGES - 1)
	{
		void* obj = system_alloc(k);
		Span* ret_span = new Span;
		ret_span->_page_id = (page_t)obj >> PAGE_SHIFT;
		ret_span->_block_size = k << PAGE_SHIFT;
		ret_span->_is_used = true;
		ret_span->_n = k;
		// 添加起始页的映射关系
		_insert_map(ret_span->_page_id, 1, ret_span);
		return ret_span;
	}
	
	if (!_span_lists[k]._empty())
	{
		Span* ret_span = _span_lists[k]._pop_front();
		// 注意区分不同的页
		if (k > (MAX_SIZE >> PAGE_SHIFT))
		{
			_insert_map(ret_span->_page_id, 1, ret_span);
			_insert_map(ret_span->_page_id + ret_span->_n - 1, 1, ret_span);
		}
		else
			_insert_map(ret_span->_page_id, k, ret_span);

		ret_span->_is_used = true;
		return ret_span;
	}
	// ...
}

关于_return_span_to_spans接口:

  • 当归还的内存块页数大于128页,调用系统接口,直接将其返回给系统
  • 当归还的内存块页数小于等于128页,需要进行合并操作
void PageCache::_return_span_to_spans(Span* span)
{
	// 合并的过程需要加锁
	std::unique_lock<std::recursive_mutex> guard(_rmtx);
	if (span->_n > NPAGES - 1)
	{
		system_dealloc((void*)(span->_page_id << PAGE_SHIFT), span->_n << PAGE_SHIFT);
		return;
	}
	// ...
}

当时我在想:大于128页的内存申请直接在tc_alloc调用系统接口不就行了,这样脱离page cache不是更好吗?但是我忽略了一个重要的问题:tc_dealloc时,我只知道一个内存块的起始地址,怎么知道它的大小?小于等于128页的内存块可以通过page cache的映射表得知,而大于128页的内存块大小就不得而知了。所以这里还是需要记录内存块起始地址与内存块大小间的关系啊,因为已经实现了page cache的映射表,这里就不用再实现新的结构了。


写在最后

将代码转换成文字,并尽可能简洁准确地表述整体的样貌,解剖其中的关键。这使得我需要对文章进行不断的修改,然后再阅读,再修改,直到句子通顺,流畅。虽然这个过程枯燥乏味,但是它能帮助你理清思路与头绪。梳理完这两篇文章,对这个系统也有了更深刻的认识,现在我只希望能尽快找出其中的bug,让它尽量不出错,同时也要对其可读性和可维护性进行更多的考虑。

文章日后还会修改,若有错别字请见谅

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

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

相关文章

Simulink 自动代码生成电机控制:软件在环测试(SIL)步骤总结

目录 前言 模型配置 SIL模型生成 模型仿真对比 总结 前言 电机模型仿真可以叫做模型在环测试&#xff08;MIL&#xff09;&#xff0c;至于SIL就是软件在环仿真测试&#xff0c;说白了就是验证生成的代码有没有问题&#xff0c;如果有问题那在模型里面修复&#xff0c;不要…

点餐小程序实战教程05-点餐功能开发

目录 1 点餐需求分析2 变量定义3 点餐分类功能实现4 菜品展示功能开发5 实现切换分类时过滤数据总结我们上一篇设计了点餐分类及点餐信息数据源的功能,本篇我们介绍一下如何开发点餐功能。 1 点餐需求分析 看一下页面是分为两部分,左侧是侧边栏导航,用来展示点餐的分类信息。…

论文解读 | 《基于采样的MPC控制的约束视觉》

原创 | 文BFT机器人 引言 Introduction 视觉伺服控制方案&#xff0c;如基于图像的(IBVS)&#xff0c;基于姿态的(PBVS)或基于混合的(HBVS)&#xff0c;在过去的几十年里得到了广泛的发展。众所周知&#xff0c;要处理的主要问题涉及局部极小点或奇异点的存在、可见性约束、联合…

缺少ssl模块

nginx采用源码安装方式 1、 查看是否有模块&#xff0c;如下没有 /usr/local/nginx/sbin/nginx -V1.1、 备份nginx配置文件 cp -a nginx.conf nginx.conf.bak2、 进nginx安装包目录 ./configure --prefix/usr/local/nginx --with-http_stub_status_module --with-http_ssl_mo…

将 NGINX 部署为 API 网关

现代应用架构的核心是 HTTP API。HTTP 支持快速构建和轻松维护应用。HTTP API 提供了一个通用接口&#xff0c;因此不必考虑应用的规模大小&#xff0c;无论是单独用途的微服务还是大型综合应用。 HTTP 不仅可以支持超大规模互联网&#xff0c;也可用于提供可靠和高性能的 API …

解决一个诡异的java空指针问题的案例

最近在看java类加载器的资料&#xff0c;于是写了一个自定义类加载器测试一下&#xff0c;结果就悲剧了&#xff0c;直接报空指针&#xff01; 跟着报错指引看代码37行是什么东东&#xff1f; 就是一个inputStream, 然后看看它的定义&#xff1a; 这玩意就是从classpath读取cla…

html实现一个一闪一闪的按钮,CSS实现一个一闪一闪的按钮,Css闪烁点标,css设置按钮层次感,css按钮美化,CSS按钮动画过渡,CSS按钮添加阴影

效果 动态 静态 实现 底部多加了几个过渡按钮 <!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title><style>#app {margin: 2% auto;text-align: center;}.lay-btn-box {position: relative;display: …

【达梦数据库】达梦数据库windows安装

目录 1.选择语言与时区 2.安装向导 3.许可证协议 4.验证 Key 文件 5.选择安装组件 6.选择安装目录 7.目录确认 8.开始安装 9.安装过程 10.安装完成 11.创建数据库实例 12.创建数据库模板 13.数据库目录 14.数据库标识 15.数据库文件 16.初始化参数 17.口令管理…

VoxelNeXt:用于3D检测和跟踪的纯稀疏体素网络

VoxelNeXt:Fully Sparse VoxelNet for 3D Object Detection and Tracking 目前自动驾驶场景的3D检测框架大多依赖于dense head&#xff0c;而3D点云数据本身是稀疏的&#xff0c;这无疑是一种低效和浪费计算量的做法。我们提出了一种纯稀疏的3D 检测框架 VoxelNeXt。该方法可以…

电脑断电后无法正常启动怎么办?

电脑断电后无法正常启动是一个很常见的问题&#xff0c;其实除断电外&#xff0c;电脑强制关机后无法正常启动也很常见&#xff0c;出现这个问题一般是由硬件导致&#xff0c;可能是内存、电源、主板、显卡、硬盘等硬件出现问题&#xff0c;尤其是一瞬间断电再来电&#xff0c;…

全网最牛最全面的接口自动化接口关联的三个层次

一、&#xff08;接口查询的条件分析&#xff09; 1.一般来说&#xff0c;在所有平台中&#xff0c;凡是往数据库里增加接口&#xff0c;必然有相应的查询接口和修改操作的接口 2.接口的后台服务除了要把数据返回给我们之外&#xff0c;还要把真正对数据的修改操作写入数据库…

linux系统学习

本文建立于Linux的课堂学习 文章目录 Linux基础1. Linux操作环境1.1 简述Linux文件类型有哪些1.2 简述Linux的文件访问权限1.3 简述shell的功能&#xff0c;常见的shell有几种1.4 列举几个常用的Shell环境变量以及用途 2. Linux Shell命令操作2.1 简述在Linux Shell中获取帮助…

数据结构总结6:八大排序

后续会有补充 排序 排序&#xff1a;按照某个或某些关键字的大小&#xff0c;递增或递减排列起来的操作。 稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若经过排序&#xff0c;这些记录的相对次序保持不变&#xff0c…

如何完美卸载VS2015(2023年5月份实测有效)

使用控制面板卸载VS2015&#xff0c;出现正在配置您的系统&#xff0c;这可能需要一些时间&#xff0c;然后就出现卡住半个小时第二行的条都没有动的问题&#xff0c;这里提供vs2015以及以前版本的卸载方式 问题产生原因:他需要下载一些东西&#xff0c;然后由于你懂的网络原因…

基于yolov3训练自己的数据集

训练数据集的教学视频链接 42. 第六章&#xff1a;基于YOLO-V3训练自己的数据集与任务_哔哩哔哩_bilibili 数据打标签 下载labelme标注工具 通过pip install labelme下载&#xff0c;打开anaconda prompt&#xff0c;切换到下载labelme的环境&#xff08;我的是pytorch&…

torch显存分析——如何在不关闭进程的情况下释放显存

torch显存分析——如何在不关闭进程的情况下释放显存 1. 基本概念——allocator和block2. torch.cuda的三大常用方法3. 可以释放的显存4. 无法释放的显存&#xff1f;5. 清理“显存钉子户” 一直以来&#xff0c;对于torch的显存管理&#xff0c;我都没有特别注意&#xff0c;只…

ffmpeg mkv 文件解析

一、mkv的文件组织 1. EBML基本单元 EBML组成mkv文件最基本的单元&#xff0c; 也是解析文件最小的一个粒度。EBML基本元素结构&#xff1a; ID&#xff1a;标志着这个EMBL 是一个什么类型的&#xff0c;类型决定了后面data中存储的是什么类型的数据如是int&#xff0c;string…

腾讯云备案限制条件说明(必看)

腾讯云网站备案要求首先你有一个需要备案的域名&#xff0c;域名实名认证信息和备案主体相同&#xff1b;在腾讯云有一台符合备案条件的云服务器、轻量应用服务器等云产品&#xff1b;然后根据备案主体所在省份地区&#xff0c;符合当地的通信管理局要求。下面腾讯云百科来详细…

Centos7系统常用命令

一、防火墙firewalld、sestatus 1 查看防火墙状态&#xff1a;systemctl status firewalld 2 关闭运行的防火墙&#xff1a;systemctl stop firewalld.service 开启运行的防火墙&#xff1a;systemctl start firewalld.service 3 禁止防火墙服务器&#xff1a;systemctl di…

如何一行代码实现 OpenAI 可观测,大幅提升使用体验

作者&#xff5c;观测云 徐季秋 现在基于 OpenAI 的 Chat 应用井喷&#xff0c;但给开发者带来了两个难点&#xff0c;一是因为 OpenAI 基于 tokens 的计费机制导致不容易规划消费&#xff1b;另一是 OpenAI 提供的调用本身不稳定&#xff0c;很难分辨是传参错误或是访问失败。…