文章目录
-
- [0. 概述](#0. 概述)
- [1. 使用说明](#1. 使用说明)
- [1.1. 参数说明](#1.1. 参数说明)
-
- [1.2. 运行脚本](#1.2. 运行脚本)
- [2. 脚本详细解析](#2. 脚本详细解析)
-
- [2.1. 参数初始化](#2.1. 参数初始化)
- [2.2. 参数解析与验证](#2.2. 参数解析与验证)
- [2.3 主循环条件](#2.3 主循环条件)
- [2.4 时间跳变检测与处理](#2.4 时间跳变检测与处理)
- [2.5. 日志轮转机制](#2.5. 日志轮转机制)
- [2.6. 睡眠时间计算](#2.6. 睡眠时间计算)
0. 概述
之前写过单线程版本的高精度时间日志记录小程序:C++编程:实现简单的高精度时间日志记录小程序(单线程)
然后写了多线程版本的高精度时间日志记录小程序:使用C++实现高精度时间日志记录与时间跳变检测[多线程版本]
本文将使用shell脚本实现类似的功能,该脚本主要实现以下功能:
- 定时记录时间戳:按照指定的时间间隔(默认为20毫秒)记录当前时间戳。
- 时间跳变检测与处理:当检测到系统时间回退或跳跃时,记录跳变前后的时间戳,以便后续分析。
- 日志轮转:当日志文件达到指定大小(默认为50MB)时,自动轮转日志文件,并保留一定数量的历史日志文件。
- 可配置参数:支持通过命令行参数自定义时间间隔、日志文件名、运行时长等配置。
1. 使用说明
1.1. 参数说明
脚本提供了多种可配置参数,用户可以根据需求进行调整:
-i, --interval <milliseconds>
:设置记录时间戳的时间间隔,默认为20毫秒。-f, --file <filename>
:设置输出日志文件的名称,默认为timestamps_sh.txt
。-t, --time <seconds>
:设置脚本的运行时长,默认为72000秒(20小时)。--disable_selective_logging
:禁用选择性日志记录功能。-h, --help
:显示帮助信息。
1.2. 运行脚本
使用默认参数运行脚本:
bash
./time_jump_check.sh
使用自定义参数运行脚本,例如设置间隔为500毫秒,运行时长为3600秒(1小时):
bash
./time_jump_check.sh -i 500 -t 3600
禁用选择性日志记录功能:
bash
./time_jump_check.sh --disable_selective_logging
2. 脚本详细解析
以下是time_jump_check.sh 脚本的完整代码:
bash
#!/bin/bash
# Default parameters
INTERVAL_MS=20 # Default interval 20 milliseconds
FILENAME="timestamps_sh.txt" # Default log file name
RUN_TIME_SECONDS=72000 # Default run time 72000 seconds (20 hours)
MAX_FILE_SIZE=50*1024*1024 # 50MB in bytes
SELECTIVE_LOGGING=true # Default to enable selective logging
PRE_JUMP_RECORD=10 # Number of timestamps to record before jump
POST_JUMP_RECORD=10 # Number of timestamps to record after jump
MAX_LOG_FILES=2 # Maximum number of log files to keep
# Function to show usage information
usage() {
echo "Usage: $0 [options]"
echo "Options:"
echo " -i, --interval <milliseconds> Set the time interval, default is 20 milliseconds"
echo " -f, --file <filename> Set the output file name, default is timestamps.txt"
echo " -t, --time <seconds> Set the run time, default is 72000 seconds (20 hours)"
echo " --disable_selective_logging Disable selective logging feature"
echo " -h, --help Show this help message"
exit 1
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-i|--interval)
INTERVAL_MS="$2"
shift # past argument
shift # past value
;;
-f|--file)
FILENAME="$2"
shift
shift
;;
-t|--time)
RUN_TIME_SECONDS="$2"
shift
shift
;;
--disable_selective_logging)
SELECTIVE_LOGGING=false
shift
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1"
usage
;;
esac
done
# Validate parameters
if ! [[ "$INTERVAL_MS" =~ ^[0-9]+$ ]] || [ "$INTERVAL_MS" -le 0 ]; then
echo "Error: Interval must be a positive integer."
usage
fi
if ! [[ "$RUN_TIME_SECONDS" =~ ^[0-9]+$ ]] || [ "$RUN_TIME_SECONDS" -le 0 ]; then
echo "Error: Run time must be a non-negative integer."
usage
fi
# Output configuration
echo "Time Interval: $INTERVAL_MS milliseconds"
echo "Output File: $FILENAME"
echo "Run Time: $RUN_TIME_SECONDS seconds"
echo "Selective Logging: $SELECTIVE_LOGGING"
# Initialize variables
last_timestamp_us=0
second_last_timestamp_us=0
total_timestamps=0
in_jump_mode=false
jump_remaining=0
time_jump_count=0
declare -a pre_jump_timestamps=()
# Create or clear the log file
if ! > "$FILENAME"; then
echo "Error: Unable to create or clear log file '$FILENAME'."
exit 1
fi
# Main loop start time
START_TIME=$(date +%s)
while [[ $(($(date +%s) - START_TIME)) -lt $RUN_TIME_SECONDS || $time_jump_count -lt $RUN_TIME_SECONDS ]]; do
# Get the current time string
current_time_str=$(date +"%Y-%m-%d %H:%M:%S.%6N")
# Convert current time to microseconds (since epoch)
current_timestamp_us=$(date +"%s%6N")
time_jump=false
if [[ $last_timestamp_us -ne 0 && $second_last_timestamp_us -ne 0 && $SELECTIVE_LOGGING == true ]]; then
if [[ $current_timestamp_us -lt $last_timestamp_us ]]; then
# Time regression detected
time_jump=true
else
# Check if the interval is too small based on second last timestamp
expected_interval_us=$(( INTERVAL_MS * 1000 ))
actual_interval_us=$(( current_timestamp_us - second_last_timestamp_us ))
threshold_us=$(( expected_interval_us * 15 / 10 )) # 1.5x threshold
if [[ $actual_interval_us -lt $threshold_us ]]; then
time_jump=true
fi
fi
fi
# Update timestamps
second_last_timestamp_us=$last_timestamp_us
last_timestamp_us=$current_timestamp_us
# Add current time to pre_jump_timestamps array
pre_jump_timestamps+=("$current_time_str")
# Keep array length at most PRE_JUMP_RECORD
if [[ ${#pre_jump_timestamps[@]} -gt $PRE_JUMP_RECORD ]]; then
pre_jump_timestamps=("${pre_jump_timestamps[@]: -$PRE_JUMP_RECORD}")
fi
if [[ $SELECTIVE_LOGGING == true && $time_jump == true && $in_jump_mode == false ]]; then
# Detected a time jump, enter jump mode
in_jump_mode=true
jump_remaining=$POST_JUMP_RECORD
# Log pre-jump timestamps
echo -e "\n--- TIME JUMP DETECTED ---" >> "$FILENAME"
for ts in "${pre_jump_timestamps[@]}"; do
echo "$ts" >> "$FILENAME"
done
# Log current timestamp with [TIME_JUMP] marker
echo "$current_time_str [TIME_JUMP]" >> "$FILENAME"
elif [[ $in_jump_mode == true ]]; then
# In jump mode, record post-jump timestamps
echo "$current_time_str" >> "$FILENAME"
jump_remaining=$((jump_remaining - 1))
if [[ $jump_remaining -le 0 ]]; then
in_jump_mode=false
fi
else
# Normal mode: log every 500 timestamps
total_timestamps=$((total_timestamps + 1))
if [[ $((total_timestamps % 500)) -eq 0 ]]; then
echo "$current_time_str" >> "$FILENAME"
fi
fi
# Check and perform log rotation
current_size=$(stat -c%s "$FILENAME" 2>/dev/null || echo 0)
if [[ $current_size -ge $MAX_FILE_SIZE ]]; then
new_filename="${FILENAME}.$(date +"%Y%m%d%H%M%S")"
if ! mv "$FILENAME" "$new_filename"; then
echo "Error: Unable to rotate log file to '$new_filename'."
exit 1
fi
echo "Rotated log file to $new_filename"
# Create a new log file
if ! > "$FILENAME"; then
echo "Error: Unable to create new log file '$FILENAME'."
exit 1
fi
# Remove oldest log file if there are more than MAX_LOG_FILES
log_files=($(ls -t "${FILENAME}".* 2>/dev/null))
if [[ ${#log_files[@]} -gt $MAX_LOG_FILES ]]; then
for (( i=$MAX_LOG_FILES; i<${#log_files[@]}; i++ )); do
if ! rm "${log_files[$i]}"; then
echo "Error: Unable to remove old log file '${log_files[$i]}'."
fi
done
fi
fi
# Replace awk with bash and printf to calculate sleep_time
integer_part=$(( INTERVAL_MS / 1000 ))
fractional_part=$(( INTERVAL_MS % 1000 ))
# Ensure fractional_part is three digits with leading zeros if necessary
fractional_part_padded=$(printf "%03d" "$fractional_part")
sleep_time="${integer_part}.${fractional_part_padded}"
# Sleep for the specified interval
sleep "$sleep_time"
done
echo "Program has ended."
exit 0
2.1. 参数初始化
脚本开始部分定义了一系列默认参数,包括时间间隔、日志文件名、运行时长、最大日志文件大小、选择性日志记录开关、记录跳跃前后的时间戳数量以及最大保留的日志文件数量。
bash
INTERVAL_MS=20 # 默认间隔20毫秒
FILENAME="timestamps_sh.txt" # 默认日志文件名
RUN_TIME_SECONDS=72000 # 默认运行时长72000秒(20小时)
MAX_FILE_SIZE=$((50*1024*1024)) # 50MB
SELECTIVE_LOGGING=true # 默认启用选择性日志记录
PRE_JUMP_RECORD=10 # 跳跃前记录的时间戳数量
POST_JUMP_RECORD=10 # 跳跃后记录的时间戳数量
MAX_LOG_FILES=2 # 最大保留日志文件数量
2.2. 参数解析与验证
通过命令行参数,用户可以自定义脚本的运行参数。脚本使用while
循环和case
语句解析传入的参数,并进行必要的验证,确保参数的有效性。
bash
# 解析命令行参数
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-i|--interval)
INTERVAL_MS="$2"
shift # 过去参数
shift # 过去值
;;
-f|--file)
FILENAME="$2"
shift
shift
;;
-t|--time)
RUN_TIME_SECONDS="$2"
shift
shift
;;
--disable_selective_logging)
SELECTIVE_LOGGING=false
shift
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1"
usage
;;
esac
done
# 验证参数
if ! [[ "$INTERVAL_MS" =~ ^[0-9]+$ ]] || [ "$INTERVAL_MS" -le 0 ]; then
echo "Error: Interval must be a positive integer."
usage
fi
if ! [[ "$RUN_TIME_SECONDS" =~ ^[0-9]+$ ]] || [ "$RUN_TIME_SECONDS" -le 0 ]; then
echo "Error: Run time must be a non-negative integer."
usage
fi
2.3 主循环条件
主循环的条件为:
bash
while [[ $(($(date +%s) - START_TIME)) -lt $RUN_TIME_SECONDS || $time_jump_count -lt $RUN_TIME_SECONDS ]]; do
这意味着,脚本将在以下两个条件之一满足时继续运行:
- 已经运行的时间未超过设定的
RUN_TIME_SECONDS
。 - 检测到的时间跳变次数未超过
RUN_TIME_SECONDS
。
这种设计确保了即使系统时间发生大幅跳变,脚本仍能继续运行,直到达到预定的运行时长。
2.4 时间跳变检测与处理
脚本通过比较当前时间戳与之前的时间戳来检测时间跳变。当检测到时间回退或时间间隔异常小时,触发跳变处理机制。
bash
if [[ $current_timestamp_us -lt $last_timestamp_us ]]; then
# 时间回退
time_jump=true
time_jump_count=$((time_jump_count + 1))
else
# 检查时间间隔是否异常
expected_interval_us=$(( INTERVAL_MS * 1000 ))
actual_interval_us=$(( current_timestamp_us - second_last_timestamp_us ))
threshold_us=$(( expected_interval_us * 15 / 10 )) # 1.5倍阈值
if [[ $actual_interval_us -lt $threshold_us ]]; then
time_jump=true
time_jump_count=$((time_jump_count + 1))
fi
fi
当检测到时间跳变时,脚本将:
- 记录跳变前的时间戳。
- 标记当前时间戳为跳变时间。
- 在后续的循环中,记录一定数量的跳变后时间戳,确保日志的连续性。
2.5. 日志轮转机制
为防止日志文件过大,脚本实现了日志轮转功能。当日志文件大小超过MAX_FILE_SIZE
时,脚本会:
- 将当前日志文件重命名为带有时间戳的文件名。
- 创建一个新的空日志文件。
- 保留最新的
MAX_LOG_FILES
个日志文件,删除最旧的文件。
bash
# 检查并执行日志轮转
current_size=$(stat -c%s "$FILENAME" 2>/dev/null || echo 0)
if [[ $current_size -ge $MAX_FILE_SIZE ]]; then
new_filename="${FILENAME}.$(date +"%Y%m%d%H%M%S")"
if ! mv "$FILENAME" "$new_filename"; then
echo "Error: Unable to rotate log file to '$new_filename'."
exit 1
fi
echo "Rotated log file to $new_filename"
# 创建新的日志文件
if ! > "$FILENAME"; then
echo "Error: Unable to create new log file '$FILENAME'."
exit 1
fi
# 如果日志文件超过MAX_LOG_FILES个,删除最旧的文件
log_files=($(ls -t "${FILENAME}".* 2>/dev/null))
if [[ ${#log_files[@]} -gt $MAX_LOG_FILES ]]; then
for (( i=$MAX_LOG_FILES; i<${#log_files[@]}; i++ )); do
if ! rm "${log_files[$i]}"; then
echo "Error: Unable to remove old log file '${log_files[$i]}'."
fi
done
fi
fi
2.6. 睡眠时间计算
为了实现精确的时间间隔,脚本将INTERVAL_MS
分解为整数部分和小数部分,并使用printf
确保小数部分为三位数,最后组合成sleep
命令所需的格式。
bash
# 计算睡眠时间
integer_part=$(( INTERVAL_MS / 1000 ))
fractional_part=$(( INTERVAL_MS % 1000 ))
# 确保fractional_part为三位数,前面补零
fractional_part_padded=$(printf "%03d" "$fractional_part")
sleep_time="${integer_part}.${fractional_part_padded}"
# 按指定间隔休眠
sleep "$sleep_time"