神秘解密:stressapptest源码Logger日志操作深度剖析

一、简介

stressapptest 是一个用于测试系统稳定性和可靠性的开源应用程序。它通过模拟在系统上运行的压力,并检查系统在这种严格条件下的稳定性。stressapptest 是一个非常有用的工具,可以在不同环境中对系统进行稳定性测试和故障排除。
在这里插入图片描述

二、logger头文件

Logger类的定义,它提供了日志记录的功能和相关操作。

#include <pthread.h>
#include <stdarg.h>

#include <string>
#include <vector>

#include "sattypes.h"

static const size_t kMaxQueueSize = 250;

#define LOGGER_ASSERT(x) \
{\
  if (!(x)) {\
    fprintf(stderr, "Assertion failed at %s:%d\n", __FILE__, __LINE__);\
    exit(1);\
  }\
}

class Logger {
 public:
  // Returns a pointer to the single global Logger instance.  Will not return
  // NULL.
  static Logger *GlobalLogger();

  // Lines with a priority numerically greater than this will not be logged.
  // May not be called while multiple threads are running.
  virtual void SetVerbosity(int verbosity) {
    verbosity_ = verbosity;
  }

  virtual void SetLogFd(int log_fd) {
    LOGGER_ASSERT(log_fd >= 0);
    log_fd_ = log_fd;
  }

  // Set output to be written to stdout only.  This is the default mode.  May
  // not be called while multiple threads are running.
  virtual void SetStdoutOnly() {
    log_fd_ = -1;
  }

  // Enable or disable logging of timestamps.
  void SetTimestampLogging(bool log_ts_enabled) {
    log_timestamps_ = log_ts_enabled;
  }
  void VLogF(int priority, const char *format, va_list args);

  void StartThread();
  
  void StopThread();

 protected:
  Logger();

  virtual ~Logger();

 private:
  // Args:
  //   line: Must be non-NULL.  This function takes ownership of it.
  void QueueLogLine(string *line);

  // Args:
  //   line: Must be non-NULL.  This function takes ownership of it.
  void WriteAndDeleteLogLine(string *line);

  // Callback for pthread_create(3).
  static void *StartRoutine(void *ptr);

  // Processes the log queue.
  void ThreadMain();

  pthread_t thread_;
  int verbosity_;
  int log_fd_;
  bool thread_running_;
  bool log_timestamps_;
  vector<string*> queued_lines_;
  // This doubles as a mutex for log_fd_ when the logging thread is not running.
  pthread_mutex_t queued_lines_mutex_;
  // Lets the logging thread know that the queue is no longer empty.
  pthread_cond_t queued_lines_cond_;
  // Lets the threads blocked on the queue having reached kMaxQueueSize know
  // that the queue has been emptied.
  pthread_cond_t full_queue_cond_;

  DISALLOW_COPY_AND_ASSIGN(Logger);
};

类的主要功能和细节:

  1. 提供了设置日志输出级别、写入文件描述符、启用时间戳记录等方法。
  2. 提供了启动和停止专用日志记录线程的方法。
  3. 提供了日志记录方法VLogF,它使用类似vprintf(3)的接口来记录日志,并可以阻塞等待将日志行写入stdout / disk,如果专用日志记录线程没有运行,则会阻塞将日志行添加到队列。
  4. 内部包含了一些用于管理日志记录队列和线程的成员变量,例如队列成员queued_lines_来存储日志行,还有互斥锁和条件变量用于线程同步。

三、Logger类的具体实现

  1. Logger 类设计为支持多线程环境,采用了互斥锁和条件变量进行线程同步控制,确保多线程情况下的安全访问。其中,QueueLogLine方法和ThreadMain方法中对互斥锁的使用尤为重要。

  2. 在方法QueueLogLine中,对line指针进行了判空处理,确保了line指针的有效性。在WriteAndDeleteLogLine方法中对line指针进行了删除操作,释放了内存资源。

  3. StartThread方法负责启动专用日志记录线程,StopThread方法负责终止线程。线程的启动和结束通过pthread_create和pthread_join来实现。

  4. 日志的时间戳记录和文件输出:VLogF方法负责记录日志,支持时间戳记录和分级别的日志输出。线程独立的日志记录功能由专用日志记录线程来实现,以提高效率。

  5. WriteAndDeleteLogLine方法中实现了向文件描述符和标准输出的写入操作。

  6. GlobalLogger方法通过静态局部变量实现了Logger类的单例模式,保证了全局只有一个Logger实例。

3.1、线程启动和结束

StartThread() 和StopThread()是Logger 类中用于启动和停止专用日志记录线程的函数实现。

void Logger::StartThread() {
  LOGGER_ASSERT(!thread_running_);
  thread_running_ = true;
  LOGGER_ASSERT(0 == pthread_create(&thread_, NULL, &StartRoutine, this));
}

void Logger::StopThread() {
  // Allow this to be called before the thread has started.
  if (!thread_running_) {
    return;
  }
  thread_running_ = false;
  int retval = pthread_mutex_lock(&queued_lines_mutex_);
  LOGGER_ASSERT(0 == retval);
  bool need_cond_signal = queued_lines_.empty();
  queued_lines_.push_back(NULL);
  retval = pthread_mutex_unlock(&queued_lines_mutex_);
  LOGGER_ASSERT(0 == retval);
  if (need_cond_signal) {
    retval = pthread_cond_signal(&queued_lines_cond_);
    LOGGER_ASSERT(0 == retval);
  }
  retval = pthread_join(thread_, NULL);
  LOGGER_ASSERT(0 == retval);
}

void *Logger::StartRoutine(void *ptr) {
  Logger *self = static_cast<Logger*>(ptr);
  self->ThreadMain();
  return NULL;
}

在 StartThread 方法中:

  • 首先,使用 LOGGER_ASSERT 断言检查线程是否已经在运行,如果已经运行则会触发断言失败;
  • 然后将 thread_running_ 标志设置为 true,表示线程已经启动;
  • 调用 pthread_create 函数创建日志记录线程,指定线程入口函数为 StartRoutine。

在 StopThread 方法中:

  • 首先判断如果线程没有运行,则直接返回,不做任何操作;
  • 将 thread_running_ 标志设置为 false,表示线程将要停止;
  • 对队列锁定互斥锁进行加锁,然后将一个空指针(NULL)放入队列中,用于通知线程退出;
  • 如果队列之前为空,需要发送条件信号给线程;
  • 然后使用 pthread_join 函数等待日志记录线程终止。

在这段代码中,使用了条件变量和互斥锁来实现线程的同步控制与通信。值得注意的是:

  • 在 StartThread 中,对 thread_running_ 的状态和 pthread_create 函数的调用使用断言确保了线程的正确启动;
  • 在 StopThread 中使用了 mutex 锁保护对队列的操作,并通过条件变量发送信号和使用 pthread_join 等待线程终止,确保了线程停止的安全操作。

3.2、QueueLogLine(string *)函数

主要是 Logger 类中用于处理队列日志行和写入日志的函数实现。使用了互斥锁和条件变量来实现线程同步控制与通信,保证了对队列的安全操作和日志写入的正确性。同时,在对指针进行相关操作时,使用断言进行合法性检查,确保了代码的稳定性和正确性。

void Logger::QueueLogLine(string *line) {
  LOGGER_ASSERT(line != NULL);
  LOGGER_ASSERT(0 == pthread_mutex_lock(&queued_lines_mutex_));
  if (thread_running_) {
    if (queued_lines_.size() >= kMaxQueueSize) {
      LOGGER_ASSERT(0 == pthread_cond_wait(&full_queue_cond_,
                                           &queued_lines_mutex_));
    }
    if (queued_lines_.empty()) {
      LOGGER_ASSERT(0 == pthread_cond_signal(&queued_lines_cond_));
    }
    queued_lines_.push_back(line);
  } else {
    WriteAndDeleteLogLine(line);
  }
  LOGGER_ASSERT(0 == pthread_mutex_unlock(&queued_lines_mutex_));
}

void Logger::WriteAndDeleteLogLine(string *line) {
  LOGGER_ASSERT(line != NULL);
  ssize_t bytes_written;
  if (log_fd_ >= 0) {
    bytes_written = write(log_fd_, line->data(), line->size());
    LOGGER_ASSERT(bytes_written == static_cast<ssize_t>(line->size()));
  }
  bytes_written = write(STDOUT_FILENO, line->data(), line->size());
  LOGGER_ASSERT(bytes_written == static_cast<ssize_t>(line->size()));
  delete line;
}

在 QueueLogLine 方法中:

  • 首先,使用 LOGGER_ASSERT 断言检查line指针的有效性,以及对互斥锁的加锁操作;
  • 然后判断线程是否正在运行,如果正在运行则将日志行加入队列,如果队列已满则通过条件变量 full_queue_cond 进行等待;
  • 如果队列为空,则通过条件变量 queued_lines_cond 发送信号;
  • 如果线程没有运行,则直接调用 WriteAndDeleteLogLine 方法进行日志写入操作;
  • 最后对互斥锁进行解锁操作。

在 WriteAndDeleteLogLine 方法中:

  • 首先,使用 LOGGER_ASSERT 断言检查line指针的有效性;
  • 然后根据 log_fd_ 是否大于等于0来判断是否需要向文件描述符写入数据,同时向标准输出写入相应内容;
  • 使用 write 函数将日志行写入文件描述符或者标准输出;
  • 最后删除日志行指针,释放内存资源。

3.3、记录日志信息的VLogF函数

void Logger::VLogF(int priority, const char *format, va_list args) {
  if (priority > verbosity_) {
    return;
  }
  char buffer[4096];
  size_t length = 0;
  if (log_timestamps_) {
    time_t raw_time;
    time(&raw_time);
    struct tm time_struct;
    localtime_r(&raw_time, &time_struct);
    length = strftime(buffer, sizeof(buffer), "%Y/%m/%d-%H:%M:%S(%Z) ",
                      &time_struct);
    LOGGER_ASSERT(length);  // Catch if the buffer is set too small.
  }
  length += vsnprintf(buffer + length, sizeof(buffer) - length, format, args);
  if (length >= sizeof(buffer)) {
    length = sizeof(buffer);
    buffer[sizeof(buffer) - 1] = '\n';
  }
  QueueLogLine(new string(buffer, length));
}

根据传入的日志优先级(priority)和日志输出级别(verbosity_)进行判断,如果priority大于verbosity_,则说明该日志信息不需要记录,直接返回。如果需要记录日志,则按照日志格式进行处理,并调用QueueLogLine方法将日志加入到日志队列中。

在方法实现中,具体的流程包括:

  • 首先对日志优先级进行判断,如果不满足条件则直接返回,不进行日志记录;
  • 然后根据log_timestamps_标志,判断是否需要输出时间戳,如果需要则获取当前时间并格式化为字符串,加入到日志内容中;
  • 调用vsnprintf函数将格式化后的日志内容加入到内部缓冲区buffer中;
  • 对日志内容的长度进行检查,如果超出了缓冲区的大小,进行限制,并补充一个换行符;
  • 最后调用QueueLogLine方法将带有日志内容的buffer加入到日志队列中。

流程图:

判断priority > verbosity
加入时间戳
符合条件
不符合条件
开始
需要记录日志?
格式化日志内容
添加时间戳
长度检查
加入到日志队列
限制长度并加入到日志队列
结束

此实现使用了字符串格式化函数vsnprintf和时间格式化函数strftime,辅以合理的长度检查和条件判断,保证了日志信息处理的完整性和正确性。同时,使用QueueLogLine方法将日志加入队列,确保了多线程环境下的日志记录安全。

3.4、日志线程主循环ThreadMain()函数

该方法用于专门的日志记录线程的主要逻辑。

void Logger::ThreadMain() {
  vector<string*> local_queue;
  LOGGER_ASSERT(0 == pthread_mutex_lock(&queued_lines_mutex_));

  for (;;) {
    if (queued_lines_.empty()) {
      LOGGER_ASSERT(0 == pthread_cond_wait(&queued_lines_cond_,
                                           &queued_lines_mutex_));
      continue;
    }

    // We move the log lines into a local queue so we can release the lock
    // while writing them to disk, preventing other threads from blocking on
    // our writes.
    local_queue.swap(queued_lines_);
    if (local_queue.size() >= kMaxQueueSize) {
      LOGGER_ASSERT(0 == pthread_cond_broadcast(&full_queue_cond_));
    }

    // Unlock while we process our local queue.
    LOGGER_ASSERT(0 == pthread_mutex_unlock(&queued_lines_mutex_));
    for (vector<string*>::const_iterator it = local_queue.begin();
         it != local_queue.end(); ++it) {
      if (*it == NULL) {
        // NULL is guaranteed to be at the end.
        return;
      }
      WriteAndDeleteLogLine(*it);
    }
    local_queue.clear();
    // We must hold the lock at the start of each iteration of this for loop.
    LOGGER_ASSERT(0 == pthread_mutex_lock(&queued_lines_mutex_));
  }
}

主要包括以下流程:

  • 首先通过pthread_mutex_lock对队列的互斥锁进行加锁操作;
  • 之后使用一个无限循环,判断日志队列是否为空,如果为空则通过pthread_cond_wait进行等待,直到队列不再为空;
  • 当队列中有日志时,将日志行从全局队列中移动到本地队列local_queue,并在处理本地队列时释放全局锁,避免其他线程在日志写入期间阻塞;
  • 如果本地队列的大小超过了最大队列大小kMaxQueueSize,将通过pthread_cond_broadcast向所有等待的线程发出队列已满的信号;
  • 在处理本地队列时,遍历本地队列中的每个日志行,并对每一行进行写入操作,如果遇到空指针,则表明需要退出线程;
  • 清空本地队列,并在处理完后再次加锁全局队列的互斥锁。

流程图:

判断queued_lines是否为空
判断队列是否已满
遍历本地队列
开始
队列非空?
移动日志行到本地队列
队列已满?
发送队列满信号
释放全局锁
遇到空指针?
结束
对每行进行写入操作
清空本地队列
结束

在流程图中,首先会判断queued_lines是否为空,如果队列不为空,则将日志行从全局队列中移动到本地队列。然后判断本地队列是否已满,如果已满则发送队列满信号,否则释放全局锁,然后对每行进行写入操作。最后清空本地队列并进行下一轮的循环。

该方法主要负责专门的日志记录线程的日志写入操作,并通过使用本地队列进行写入,从而避免了在写入过程中阻塞其他线程。同时,使用互斥锁和条件变量对线程进行了同步控制,确保了在多线程环境下的安全写入。

四、总结

分析了Logger类的一些关键方法和功能,包括启动和停止日志记录线程、日志队列操作、时间戳记录、日志行格式化、以及专门的日志记录线程逻辑。

Logger类提供了多线程环境下的高效日志记录功能,并且在设计上考虑了线程同步、异常处理以及内存管理等方面。它实现了多线程环境下的安全日志记录。

通过对每个方法的详细分析更好地理解这些方法的执行逻辑和线程之间的交互关系。这有助于我们深入理解和掌握Logger类的工作原理和功能。

在这里插入图片描述

  • 16
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值