索引
【C++模块实现】| 【01】日志系统实现
【C++模块实现】| 【02】日志系统优化
【C++模块实现】| 【03】文件管理模块
【C++模块实现】| 【04】配置模块
【C++模块实现】| 【05】日志模块增加配置模块的功能
【C++模块实现】| 【06】日志模块添加循环覆盖写文件功能
【C++模块实现】| 【07】对于互斥、自旋锁、条件变量、信号量简介及封装
【C++模块实现】| 【08】循环覆盖写内存缓冲区(日志中多线程记录)
【C++模块实现】| 【09】线程模块及线程池的实现
【C++模块实现】| 【10】定时器的实现
【C++模块实现】| 【11】封装Ipv4、Ipv6、unix网络地址
该模块是从sylar服务器框架中学习的,以下将会对其进行总结以加深对该框架的理解;
一、简介
1.1 工作原理
循环缓冲区(也称为环形缓冲区)是固定大小的缓冲区,工作原理就像内存是连续的且可循环的一样;
- 在生成和使用内存时,不需将原来的数据全部重新清理掉,只要调整head/tail指针即可;
- 当添加数据时,head指针前进。当使用数据时,tail指针向前移动;
- 当到达缓冲区的尾部时,指针又回到缓冲区的起始位置;
1.2 使用的好处
循环缓冲区通常用作固定大小的队列。固定大小的队列对于嵌入式系统的开发非常友好,因为开发人员通常会尝试使用静态数据存
储的方法而不是动态分配;
循环缓冲区对于数据写入和读出以不同速率发生的情况也是非常有用的结构:最新数据始终可用。如果读取数据的速度跟不上写入
数据的速度,旧的数据将被新写入的数据覆盖。通过使用循环缓冲区,能够保证我们始终使用最新的数据;
1.3 日志应用
循环缓冲区是一种用于应用程序的日志记录技术,它可以将相关的数据保存在内存中,而不是每次都将其写入到磁盘上的文件中;
在需要的时候(比如当用户请求将内存数据转储到文件中时、程序检测到一个错误时,或者由于非法的操作或者接收到的信号而引
起程序崩溃时)可以将内存中的数据转储到磁盘。循环缓冲区日志记录由一个固定大小的内存缓冲区构成,进程使用这个内存缓冲
区进行日志记录。顾名思义,该缓冲区采用循环的方式进行实现。当该缓冲区填满了数据时,无需为新的数据分配更多的内存,而
是从缓冲区开始的位置对其进行写操作,因此将覆盖以前的内容;
【覆盖】:
当进程尝试写入时,该缓冲区中已经没有足够的剩余空间。该进程写入数据,一直到达缓冲区的末尾,然后将剩余的数据复制到
缓冲区的开始位置,覆盖以前的日志条目;
【优化策略】:
通过保存一个读指针,可以实现对循环缓冲区的读操作;相应地移动读指针和写指针,以确保在进行读操作期间,读指针不会越
过写指针。为了提高效率,一些应用程序可以将原始数据(而不是经过格式化的数据)保存到该缓冲区。在这种情况下需要一个
解析器,该解析器可以根据这些内存转储生成有意义的日志消息;
1.4 多线程应用
在多线程应用程序中使用循环缓冲区启用日志记录时需要考虑的一些重要方面:
- 【锁】:在访问一个公共的资源时,同步始终是多线程程序不可缺少的部分,日志记录也不例外。因为每个线程都试图对全局空
间进行写 操作,所以必须确保它们同步地写入内存,否则消息就会遭到破坏。通常,每个线程在写入缓冲区之前都持有一个锁,
在完成操 作时释放该锁;
- 【性能】如果您的应用程序中包含几个线程,并且每个线程都在进行详细地日志记录,那么该进程的整体性能将会受到影响,因
为这些线程将在获得和释放锁上花费了大部分的时间;
- 【解决方法一】:通过使得每个线程将数据写入到它自己的内存块,就可以完全避免同步问题。当收到来自用户的转储数据的请
求时,每个线程获得一个锁,并将其转储到中心位置。因为仅在将数据刷新到磁盘时获得锁,所以性能并不会受到很大的影响。
在这样的情况下,您可能需要一个附加的程序对日志信息进行排序(按照线程 ID 或者时间戳的顺序),以便对其进行分析。您还
可以选择仅在内存中写入消息代码,而不是完整的格式化消息,稍后使用一个外部的实用工具解析转储数据,将消息代码转换为
有意义的文本;
- 【解决方法二】:另一种避免同步问题的方法是,分配一个很大的全局内存块,并将其划分为较小的槽位,其中每个槽位都可由
一个线程用来进行日志记录。每个线程只能够读写它自己的槽位,而不是整个缓冲区。当每个线程第一次尝试写入数据时,它会
尝试寻找一个空的内存槽位,并将其标记为忙碌。当线程获得了一个特定的槽位时,可以将跟踪槽位使用情况的位图中相应的位
设置为 1,当该线程退出时,重新将这个位设置为 0。同时需要维护当前使用的槽位编号的全局列表,以及正在使用它的线程的线
程信息;
要避免出现这样的情况,即一个线程已经死亡,但是却没有将其槽位在位图中对应的位设置为 0,您需要一个垃圾收集器线程,它遍
历全局列表,并根据线程 ID 以固定的时间间隔进行轮询。它负责释放槽位并修改全局列表;
1.5 优点
与传统的文件日志记录机制相比,循环缓冲区提供了下列优势:
- 速度快。与磁盘的 I/O 操作相比,内存的写操作要快得多,仅当需要的时候才刷新数据;
- 持续的日志记录可能会填满系统中的空间,从而导致其他程序也耗尽空间并且执行失败;
在这样的情况下,您有两种选择,要么手动地删除日志信息,要么实现日志轮换策略;
- 一旦您启用了日志记录,无论您是否需要它,该进程都将持续地填充硬盘上的空间;
有时,您仅仅需要程序崩溃之前的相关数据,而不是该进程完整的历史数据。
二、实现
2.1 缓冲区设计
开辟一段内存空间来保存数据,主要包括的属性为储存数据的内存空间,缓冲区长度,已使用的长度;
对应的方法为将数据写入缓冲区,从缓冲区中读入数据,设置已写入的缓冲区长度;
class Buffer{
public:
Buffer(size_t size);
/** 写入函数 */
void write(std::function<int(char*, size_t)> const& writeBuffer);
/** 读取函数 */
void read(std::function<int(char*, size_t , size_t)> const& readBuffer);
void setUserSize(int v);
/** 获取属性 */
size_t getSize() const { return m_size; }
size_t getUserSize() const { return m_userSize; }
char* getBuffer() const { return m_buffer; }
~Buffer();
private:
char* m_buffer; // 缓冲区
size_t m_size; // 缓冲区大小
size_t m_userSize; // 使用大小
OwnSemaphore *m_sem; // 信号量
}
/** 缓冲区构造函数 */
Buffer::Buffer(size_t size) {
m_buffer = new char[size];
m_size = size;
m_userSize = 0;
m_sem = new OwnSemaphore(1);
}
/** 设置缓冲区大小 */
void Buffer::setUserSize(int v) {
m_userSize = v;
}
/** 写缓冲区 */
void Buffer::write(std::function<int(char*, size_t)> const& writeBuffer) {
m_sem->wait();
m_userSize = writeBuffer(m_buffer, m_size);
m_sem->notify();
}
/** 读缓冲区 */
void Buffer::read(std::function<int(char*, size_t , size_t)> const& readBuffer) {
m_sem->wait();
readBuffer(m_buffer, m_userSize, m_size);
m_sem->notify();
}
Buffer::~Buffer() {
delete [] m_buffer;
}
1.2 缓冲池的设计
即管理多个缓冲池,并让其形成循环结构;保证读取时缓冲区总是最先写入的,同时写入时获得的缓冲区是最末尾的缓冲区。同时
为了保证缓冲区循环利用,将缓冲池设计为循环队列;
对于循环队列,当头指针和尾指针相等时,有两种情况:
- 一种是队列为空(未分配缓冲区);
- 另一种是队列已满(所有缓冲区都被分配)
解决方法一般有两种:
- 牺牲一个存储空间,当尾指针指向的下一位为头指针时,即队列为满;
- 增加标志位来判断当头尾指针相同时,当前队列的状态。
/** 缓冲池 */
class BufferPool {
public:
BufferPool(int count = 10, int bufferSize = 1024);
Buffer* getBuffer();
Buffer* popBuffer();
~BufferPool();
void reset();
bool empty();
bool full();
/** 获取缓冲区属性 */
int getHead() const { return m_head; }
int getTail() const { return m_tail; }
int getTotal() const { return m_total; }
int getUseNum() const { return m_useNum; }
private:
int m_head, m_tail; // 缓冲区的头尾指针
Buffer** m_buffers; // 缓冲池
int m_total, m_useNum; // 缓冲区的总个数及使用个数
};
/** 缓冲池 */
BufferPool::BufferPool(int count, int bufferSize) {
m_head = 0;
m_tail = 0;
m_useNum = 0;
m_total = count;
m_buffers = new Buffer*[count];
for(int i=0; i<count; ++i) {
m_buffers[i] = new Buffer(bufferSize);
}
}
/** 获取缓冲区 */
Buffer* BufferPool::getBuffer() {
Buffer* buffer = m_buffers[m_tail]; // 获取buffer
m_tail = (m_tail + 1) % m_total; // 若tail超过总量,则从头开始
m_useNum++; // 使用数增加
if(m_useNum > m_total) { // 若使用数超出总量
m_head = (m_head + 1) % m_total; // 则头部往后移
m_useNum = m_total;
}
return buffer;
}
/** 删除缓冲区 */
Buffer* BufferPool::popBuffer() {
SYLAR_ASSERT2(m_useNum==0, "bufferPool is empty");
Buffer* buffer = m_buffers[m_head];
m_head = (m_head + 1) % m_total;
m_useNum--;
return buffer;
}
/** 重置缓冲区 */
void BufferPool::reset() {
SYLAR_ASSERT(m_buffers);
m_head = 0;
m_tail = 0;
m_useNum = 0;
}
/** 判断缓冲区是否为空 */
bool BufferPool::empty() {
return m_useNum== 0;
}
/** 判断缓冲区是否为满 */
bool BufferPool::full() {
return m_useNum== m_total;
}
BufferPool::~BufferPool() {
for(int i=0; i<m_total; ++i) {
delete m_buffers[i];
m_buffers[i] = nullptr;
}
delete m_buffers;
m_buffers = nullptr;
}
三、测试
sylar::Logger::ptr g_logger = SYLAR_LOG_NAME("root");
int main() {
sylar::BufferPool *bufferPool = new sylar::BufferPool;
sylar::Buffer* buf = bufferPool->getBuffer();
std::string str = "asdasdas";
buf->write([&](char* buffer, int size){
memcpy(buffer, str.c_str(), size);
std::cout << str.c_str() << std::endl;
return size;
});
buf->read([&](char* data, int size, int len){
SYLAR_LOG_INFO(g_logger) << data;
return size;
});
return 0;
}