C++编程:实现简单的高精度时间日志记录小程序
0. 概述
为了检查是否存在系统时间跳变,本文使用C++实现了一个简单的高精度时间日志记录小程序。该程序能够每隔指定时间(默认40毫秒)记录一次系统时间到文件中,并具备以下功能:
- 自定义时间间隔和文件名:通过命令行参数设置时间间隔和输出文件名,默认值为40毫秒和
timestamps.txt
。 - 高精度定时:采用
std::chrono::steady_clock
和sleep_until
确保时间间隔的准确性,避免std::this_thread::sleep_for
带来的时间漂移。 - 缓存队列:使用线程安全的队列缓存时间戳,减少频繁的文件I/O操作。
- 日志轮转:当日志文件大小超过100MB时,自动进行日志轮转。
- 时间跳变检测:使用
[TIME_JUMP]
标记日志中发生的时间跳变(即后续时间戳小于前一个时间戳的情况)。
默认仅记录跳变前后的10个时间戳,避免日志文件中充斥过多正常的时间记录,突出异常事件。
1. 代码实现
1.1. 线程安全队列(ThreadSafeQueue)
实现了一个简单的线程安全队列类ThreadSafeQueue
,使用std::mutex
和std::condition_variable
来确保并发访问的安全性。
// Thread-safe queue for storing timestamps
class ThreadSafeQueue {
public:
// Push a new timestamp into the queue
void push(const std::string& value) {
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(value);
cv_.notify_one();
}
// Retrieve and clear all timestamps from the queue
std::queue<std::string> popAll() {
std::lock_guard<std::mutex> lock(mtx_);
std::queue<std::string> temp;
std::swap(temp, queue_);
return temp;
}
private:
std::queue<std::string> queue_;
std::mutex mtx_;
std::condition_variable cv_;
};
1.2. 获取当前时间的字符串表示
使用gettimeofday
获取系统当前时间,并将其格式化为字符串,精确到微秒。
// Function to get the current time as a formatted string
std::string getCurrentTimeString() {
struct timeval tv;
gettimeofday(&tv, nullptr);
struct tm* tm_info = localtime(&tv.tv_sec);
char buffer[64];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm_info);
// Append microseconds
char currentTime[100];
snprintf(currentTime, sizeof(currentTime), "%s.%06ld", buffer, tv.tv_usec);
return std::string(currentTime);
}
1.3. 文件大小获取
使用stat
函数获取指定文件的大小,以便在日志轮转时进行判断。
// Function to get the size of a file in bytes
std::size_t getFileSize(const std::string& filename) {
struct stat stat_buf;
int rc = stat(filename.c_str(), &stat_buf);
return rc == 0 ? stat_buf.st_size : 0;
}
1.4. 时间戳解析
将时间戳字符串解析为自纪元以来的微秒数,方便进行时间比较以检测时间跳变。
// Function to parse timestamp string to microseconds since epoch
uint64_t parseTimestamp(const std::string& timestamp_str) {
struct tm tm_info;
int microseconds = 0;
// Split the timestamp into datetime and microseconds
size_t dot_pos = timestamp_str.find('.');
if (dot_pos == std::string::npos) {
// If no microseconds part, parse as is
if (strptime(timestamp_str.c_str(), "%Y-%m-%d %H:%M:%S", &tm_info) == nullptr) {
return 0;
}
} else {
std::string datetime_str = timestamp_str.substr(0, dot_pos);
std::string micro_str = timestamp_str.substr(dot_pos + 1);
// Parse datetime part
if (strptime(datetime_str.c_str(), "%Y-%m-%d %H:%M:%S", &tm_info) == nullptr) {
return 0;
}
// Parse microseconds part
try {
microseconds = std::stoi(micro_str);
} catch (...) {
microseconds = 0;
}
}
time_t seconds = mktime(&tm_info);
if (seconds == -1) {
return 0;
}
uint64_t total_microseconds = static_cast<uint64_t>(seconds) * 1000000 + microseconds;
return total_microseconds;
}
1.5. 主函数实现
主函数负责解析命令行参数,启动生产者和消费者线程,并控制程序的运行时间和日志轮转。
int main(int argc, char* argv[]) {
// Default values
int interval_ms = 40; // Default interval of 40 milliseconds
std::string filename = "timestamps.txt"; // Default filename
int run_time_seconds = 3600; // Default run time of 1 hour (3600 seconds)
const std::size_t max_file_size = 100 * 1024 * 1024; // 100MB
// Parse command-line arguments
for(int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if((arg == "-i" || arg == "--interval") && i + 1 < argc) {
interval_ms = std::atoi(argv[++i]);
if(interval_ms <= 0) {
std::cerr << "Invalid interval. Using default value of 40 milliseconds." << std::endl;
interval_ms = 40;
}
}
else if((arg == "-f" || arg == "--file") && i + 1 < argc) {
filename = argv[++i];
}
else if((arg == "-t" || arg == "--time") && i + 1 < argc) {
run_time_seconds = std::atoi(argv[++i]);
if(run_time_seconds <= 0) {
std::cerr << "Invalid run time. Using default value of 3600 seconds (1 hour)." << std::endl;
run_time_seconds = 3600;
}
}
else if(arg == "-h" || arg == "--help") {
std::cout << "Usage: " << argv[0] << " [options]\n"
<< "Options:\n"
<< " -i, --interval <milliseconds> Set the time interval, default is 40 milliseconds\n"
<< " -f, --file <filename> Set the output filename, default is timestamps.txt\n"
<< " -t, --time <seconds> Set the run time in seconds, default is 3600 seconds (1 hour)\n"
<< " -h, --help Show this help message\n";
return 0;
}
else {
std::cerr << "Unknown argument: " << arg << "\nUse -h or --help to see available options." << std::endl;
return 1;
}
}
std::cout << "Time Interval: " << interval_ms << " milliseconds" << std::endl;
std::cout << "Output File: " << filename << std::endl;
std::cout << "Run Time: " << run_time_seconds << " seconds" << std::endl;
ThreadSafeQueue timeQueue;
std::atomic<bool> running(true);
std::atomic<int> file_index(1); // For log rotation
// Producer thread: collects timestamps at specified intervals
std::thread producer([&timeQueue, &running, interval_ms]() {
using clock = std::chrono::steady_clock;
auto next_time = clock::now();
while (running.load()) {
// Get current time and push to queue
std::string currentTime = getCurrentTimeString();
timeQueue.push(currentTime);
// Calculate next time point
next_time += std::chrono::milliseconds(interval_ms);
// Sleep until the next time point
std::this_thread::sleep_until(next_time);
// Adjust next_time if we're behind schedule
auto now = clock::now();
if (now > next_time) {
next_time = now;
}
}
});
// Consumer thread: writes timestamps to the file, handles log rotation, and detects time jumps
std::thread consumer([&timeQueue, &running, &filename, &file_index, max_file_size]() {
std::ofstream outFile(filename, std::ios::out | std::ios::app);
if (!outFile.is_open()) {
std::cerr << "Unable to open file for writing: " << filename << std::endl;
running.store(false);
return;
}
uint64_t last_timestamp_us = 0; // To store the last timestamp in microseconds
while (running.load()) {
// Wait for 1 second before writing to the file
std::this_thread::sleep_for(std::chrono::seconds(1));
// Retrieve all timestamps from the queue
std::queue<std::string> tempQueue = timeQueue.popAll();
// Write timestamps to the file
while (!tempQueue.empty()) {
std::string current_timestamp_str = tempQueue.front();
tempQueue.pop();
uint64_t current_timestamp_us = parseTimestamp(current_timestamp_str);
bool time_jump = false;
if (last_timestamp_us != 0 && current_timestamp_us < last_timestamp_us) {
time_jump = true;
}
if (time_jump) {
outFile << current_timestamp_str << " [TIME_JUMP]" << std::endl;
} else {
outFile << current_timestamp_str << std::endl;
}
last_timestamp_us = current_timestamp_us;
}
outFile.flush(); // Ensure data is written to the file
// Check if file size exceeds the maximum limit
std::size_t current_size = getFileSize(filename);
if (current_size >= max_file_size) {
outFile.close();
// Generate a new filename with an index
std::ostringstream new_filename;
new_filename << filename << "." << file_index++;
// Rename the current file
if (std::rename(filename.c_str(), new_filename.str().c_str()) != 0) {
std::cerr << "Failed to rotate log file." << std::endl;
} else {
std::cout << "Rotated log file to " << new_filename.str() << std::endl;
}
// Open a new file
outFile.open(filename, std::ios::out | std::ios::app);
if (!outFile.is_open()) {
std::cerr << "Unable to open new file for writing: " << filename << std::endl;
running.store(false);
return;
}
// Reset last_timestamp_us after rotation
last_timestamp_us = 0;
}
}
// Write any remaining timestamps before exiting
std::queue<std::string> tempQueue = timeQueue.popAll();
while (!tempQueue.empty()) {
std::string current_timestamp_str = tempQueue.front();
tempQueue.pop();
uint64_t current_timestamp_us = parseTimestamp(current_timestamp_str);
bool time_jump = false;
if (last_timestamp_us != 0 && current_timestamp_us < last_timestamp_us) {
time_jump = true;
}
if (time_jump) {
outFile << current_timestamp_str << " [TIME_JUMP]" << std::endl;
} else {
outFile << current_timestamp_str << std::endl;
}
last_timestamp_us = current_timestamp_us;
}
outFile.flush();
outFile.close();
});
std::cout << "Program will run for " << run_time_seconds << " seconds and then exit." << std::endl;
// Let the program run for the specified duration
std::this_thread::sleep_for(std::chrono::seconds(run_time_seconds));
running.store(false);
// Wait for threads to finish
producer.join();
consumer.join();
std::cout << "Program has ended." << std::endl;
return 0;
}
2. 编译与运行
2.1 编译
g++ -std=c++11 -pthread -o time_logger time_logger.cpp
运行
-
使用默认设置(40毫秒间隔,输出文件为
timestamps.txt
,运行1小时):./time_logger
-
自定义设置(例如,100毫秒间隔,输出文件为
output.txt
,运行2小时):./time_logger -i 100 -f output.txt -t 7200
2.2 示例日志文件
以下是日志文件timestamps.txt
的一部分示例内容,其中标记了时间跳变:
2024-04-27 12:34:56.789123
2024-04-27 12:34:56.829456
2024-04-27 12:34:56.869789
2024-04-27 12:34:56.809012 [TIME_JUMP]
2024-04-27 12:34:56.849345
...
3. 附完整代码
// g++ -std=c++11 -pthread -o time_logger time_logger.cpp
#include <iostream>
#include <fstream>
#include <queue>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <sys/time.h>
#include <atomic>
#include <string>
#include <cstdlib>
#include <iomanip>
#include <sstream>
#include <sys/stat.h>
// Thread-safe queue for storing timestamps
class ThreadSafeQueue {
public:
// Push a new timestamp into the queue
void push(const std::string& value) {
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(value);
cv_.notify_one();
}
// Retrieve and clear all timestamps from the queue
std::queue<std::string> popAll() {
std::lock_guard<std::mutex> lock(mtx_);
std::queue<std::string> temp;
std::swap(temp, queue_);
return temp;
}
private:
std::queue<std::string> queue_;
std::mutex mtx_;
std::condition_variable cv_;
};
// Function to get the current time as a formatted string
std::string getCurrentTimeString() {
struct timeval tv;
gettimeofday(&tv, nullptr);
struct tm* tm_info = localtime(&tv.tv_sec);
char buffer[64];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm_info);
// Append microseconds
char currentTime[100];
snprintf(currentTime, sizeof(currentTime), "%s.%06ld", buffer, tv.tv_usec);
return std::string(currentTime);
}
// Function to get the size of a file in bytes
std::size_t getFileSize(const std::string& filename) {
struct stat stat_buf;
int rc = stat(filename.c_str(), &stat_buf);
return rc == 0 ? stat_buf.st_size : 0;
}
// Function to parse timestamp string to microseconds since epoch
uint64_t parseTimestamp(const std::string& timestamp_str) {
struct tm tm_info;
int microseconds = 0;
// Split the timestamp into datetime and microseconds
size_t dot_pos = timestamp_str.find('.');
if (dot_pos == std::string::npos) {
// If no microseconds part, parse as is
if (strptime(timestamp_str.c_str(), "%Y-%m-%d %H:%M:%S", &tm_info) == nullptr) {
return 0;
}
} else {
std::string datetime_str = timestamp_str.substr(0, dot_pos);
std::string micro_str = timestamp_str.substr(dot_pos + 1);
// Parse datetime part
if (strptime(datetime_str.c_str(), "%Y-%m-%d %H:%M:%S", &tm_info) == nullptr) {
return 0;
}
// Parse microseconds part
try {
microseconds = std::stoi(micro_str);
} catch (...) {
microseconds = 0;
}
}
time_t seconds = mktime(&tm_info);
if (seconds == -1) {
return 0;
}
uint64_t total_microseconds = static_cast<uint64_t>(seconds) * 1000000 + microseconds;
return total_microseconds;
}
int main(int argc, char* argv[]) {
// Default values
int interval_ms = 40; // Default interval of 40 milliseconds
std::string filename = "timestamps.txt"; // Default filename
int run_time_seconds = 3600; // Default run time of 1 hour (3600 seconds)
const std::size_t max_file_size = 100 * 1024 * 1024; // 100MB
bool selective_logging = true; // Default: enable selective logging
// Parse command-line arguments
for(int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if((arg == "-i" || arg == "--interval") && i + 1 < argc) {
interval_ms = std::atoi(argv[++i]);
if(interval_ms <= 0) {
std::cerr << "Invalid interval. Using default value of 40 milliseconds." << std::endl;
interval_ms = 40;
}
}
else if((arg == "-f" || arg == "--file") && i + 1 < argc) {
filename = argv[++i];
}
else if((arg == "-t" || arg == "--time") && i + 1 < argc) {
run_time_seconds = std::atoi(argv[++i]);
if(run_time_seconds <= 0) {
std::cerr << "Invalid run time. Using default value of 3600 seconds (1 hour)." << std::endl;
run_time_seconds = 3600;
}
}
else if(arg == "--disable_selective_logging") {
selective_logging = false;
}
else if(arg == "-h" || arg == "--help") {
std::cout << "Usage: " << argv[0] << " [options]\n"
<< "Options:\n"
<< " -i, --interval <milliseconds> Set the time interval, default is 40 milliseconds\n"
<< " -f, --file <filename> Set the output filename, default is timestamps.txt\n"
<< " -t, --time <seconds> Set the run time in seconds, default is 3600 seconds (1 hour)\n"
<< " --disable_selective_logging Disable selective logging feature\n"
<< " -h, --help Show this help message\n";
return 0;
}
else {
std::cerr << "Unknown argument: " << arg << "\nUse -h or --help to see available options." << std::endl;
return 1;
}
}
std::cout << "Time Interval: " << interval_ms << " milliseconds" << std::endl;
std::cout << "Output File: " << filename << std::endl;
std::cout << "Run Time: " << run_time_seconds << " seconds" << std::endl;
std::cout << "Selective Logging: " << (selective_logging ? "Enabled" : "Disabled") << std::endl;
ThreadSafeQueue timeQueue;
std::atomic<bool> running(true);
std::atomic<int> file_index(1); // For log rotation
// Producer thread: collects timestamps at specified intervals
std::thread producer([&timeQueue, &running, interval_ms]() {
using clock = std::chrono::steady_clock;
auto next_time = clock::now();
while (running.load()) {
// Get current time and push to queue
std::string currentTime = getCurrentTimeString();
timeQueue.push(currentTime);
// Calculate next time point
next_time += std::chrono::milliseconds(interval_ms);
// Sleep until the next time point
std::this_thread::sleep_until(next_time);
// Adjust next_time if we're behind schedule
auto now = clock::now();
if (now > next_time) {
next_time = now;
}
}
});
// Consumer thread: writes timestamps to the file, handles log rotation, and detects time jumps
std::thread consumer([&timeQueue, &running, &filename, &file_index, max_file_size, selective_logging]() {
std::ofstream outFile(filename, std::ios::out | std::ios::app);
if (!outFile.is_open()) {
std::cerr << "Unable to open file for writing: " << filename << std::endl;
running.store(false);
return;
}
uint64_t last_timestamp_us = 0; // To store the last timestamp in microseconds
std::deque<std::string> recent_timestamps; // To store recent timestamps for buffer
const size_t buffer_size = 10; // Number of timestamps to keep before a time jump
size_t normal_count = 0; // Counter for normal timestamps
const size_t normal_threshold = 20; // Write every 20 normal timestamps
bool in_jump_mode = false; // Flag indicating if currently in jump mode
size_t jump_remaining = 0; // Number of jump-related timestamps remaining to write
std::vector<std::string> jump_buffer; // Buffer to store jump-related timestamps
while (running.load()) {
// Wait for 1 second before processing
std::this_thread::sleep_for(std::chrono::seconds(1));
// Retrieve all timestamps from the queue
std::queue<std::string> tempQueue = timeQueue.popAll();
while (!tempQueue.empty()) {
std::string current_timestamp_str = tempQueue.front();
tempQueue.pop();
// Update recent_timestamps buffer
recent_timestamps.push_back(current_timestamp_str);
if (recent_timestamps.size() > buffer_size) {
recent_timestamps.pop_front();
}
uint64_t current_timestamp_us = parseTimestamp(current_timestamp_str);
bool time_jump = false;
if (selective_logging && last_timestamp_us != 0 && current_timestamp_us < last_timestamp_us) {
time_jump = true;
}
if (selective_logging && time_jump && !in_jump_mode) {
// Time jump detected, enter jump mode
in_jump_mode = true;
jump_remaining = 10; // Number of timestamps to write after the jump
jump_buffer.clear();
// Add the last 10 timestamps before the jump
for (const auto& ts : recent_timestamps) {
jump_buffer.push_back(ts);
}
// Add the current (jump) timestamp
jump_buffer.push_back(current_timestamp_str);
last_timestamp_us = current_timestamp_us;
}
else if (selective_logging && in_jump_mode) {
// In jump mode, collect the next 10 timestamps
jump_buffer.push_back(current_timestamp_str);
jump_remaining--;
last_timestamp_us = current_timestamp_us;
if (jump_remaining == 0) {
// Write all collected jump timestamps to file with [TIME_JUMP] flag
for (size_t i = 0; i < jump_buffer.size(); ++i) {
if (i == buffer_size) { // The jump timestamp
outFile << jump_buffer[i] << " [TIME_JUMP]" << std::endl;
}
else {
outFile << jump_buffer[i] << std::endl;
}
}
outFile.flush();
jump_buffer.clear();
in_jump_mode = false;
}
}
else {
// Normal timestamp processing
normal_count++;
if (normal_count >= normal_threshold) {
outFile << current_timestamp_str << std::endl;
outFile.flush();
normal_count = 0;
}
last_timestamp_us = current_timestamp_us;
}
// Check if file size exceeds the maximum limit
std::size_t current_size = getFileSize(filename);
if (current_size >= max_file_size) {
outFile.close();
// Generate a new filename with an index
std::ostringstream new_filename;
new_filename << filename << "." << file_index++;
// Rename the current file
if (std::rename(filename.c_str(), new_filename.str().c_str()) != 0) {
std::cerr << "Failed to rotate log file." << std::endl;
} else {
std::cout << "Rotated log file to " << new_filename.str() << std::endl;
}
// Open a new file
outFile.open(filename, std::ios::out | std::ios::app);
if (!outFile.is_open()) {
std::cerr << "Unable to open new file for writing: " << filename << std::endl;
running.store(false);
return;
}
// Reset last_timestamp_us and buffers after rotation
last_timestamp_us = 0;
recent_timestamps.clear();
jump_buffer.clear();
in_jump_mode = false;
normal_count = 0;
}
}
}
// Handle any remaining timestamps before exiting
std::queue<std::string> remainingQueue = timeQueue.popAll();
while (!remainingQueue.empty()) {
std::string current_timestamp_str = remainingQueue.front();
remainingQueue.pop();
// Update recent_timestamps buffer
recent_timestamps.push_back(current_timestamp_str);
if (recent_timestamps.size() > buffer_size) {
recent_timestamps.pop_front();
}
uint64_t current_timestamp_us = parseTimestamp(current_timestamp_str);
bool time_jump = false;
if (selective_logging && last_timestamp_us != 0 && current_timestamp_us < last_timestamp_us) {
time_jump = true;
}
if (selective_logging && time_jump && !in_jump_mode) {
// Time jump detected, enter jump mode
in_jump_mode = true;
jump_remaining = 10; // Number of timestamps to write after the jump
jump_buffer.clear();
// Add the last 10 timestamps before the jump
for (const auto& ts : recent_timestamps) {
jump_buffer.push_back(ts);
}
// Add the current (jump) timestamp
jump_buffer.push_back(current_timestamp_str);
last_timestamp_us = current_timestamp_us;
}
else if (selective_logging && in_jump_mode) {
// In jump mode, collect the next 10 timestamps
jump_buffer.push_back(current_timestamp_str);
jump_remaining--;
last_timestamp_us = current_timestamp_us;
if (jump_remaining == 0) {
// Write all collected jump timestamps to file with [TIME_JUMP] flag
for (size_t i = 0; i < jump_buffer.size(); ++i) {
if (i == buffer_size) { // The jump timestamp
outFile << jump_buffer[i] << " [TIME_JUMP]" << std::endl;
}
else {
outFile << jump_buffer[i] << std::endl;
}
}
outFile.flush();
jump_buffer.clear();
in_jump_mode = false;
}
}
else {
// Normal timestamp processing
normal_count++;
if (normal_count >= normal_threshold) {
outFile << current_timestamp_str << std::endl;
outFile.flush();
normal_count = 0;
}
last_timestamp_us = current_timestamp_us;
}
// Check if file size exceeds the maximum limit
std::size_t current_size = getFileSize(filename);
if (current_size >= max_file_size) {
outFile.close();
// Generate a new filename with an index
std::ostringstream new_filename;
new_filename << filename << "." << file_index++;
// Rename the current file
if (std::rename(filename.c_str(), new_filename.str().c_str()) != 0) {
std::cerr << "Failed to rotate log file." << std::endl;
} else {
std::cout << "Rotated log file to " << new_filename.str() << std::endl;
}
// Open a new file
outFile.open(filename, std::ios::out | std::ios::app);
if (!outFile.is_open()) {
std::cerr << "Unable to open new file for writing: " << filename << std::endl;
running.store(false);
return;
}
// Reset last_timestamp_us and buffers after rotation
last_timestamp_us = 0;
recent_timestamps.clear();
jump_buffer.clear();
in_jump_mode = false;
normal_count = 0;
}
}
outFile.flush();
outFile.close();
});
std::cout << "Program will run for " << run_time_seconds << " seconds and then exit." << std::endl;
// Let the program run for the specified duration
std::this_thread::sleep_for(std::chrono::seconds(run_time_seconds));
running.store(false);
// Wait for threads to finish
producer.join();
consumer.join();
std::cout << "Program has ended." << std::endl;
return 0;
}