使用shell实现高精度时间日志记录与时间跳变检测

文章目录

    • [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脚本实现类似的功能,该脚本主要实现以下功能:

  1. 定时记录时间戳:按照指定的时间间隔(默认为20毫秒)记录当前时间戳。
  2. 时间跳变检测与处理:当检测到系统时间回退或跳跃时,记录跳变前后的时间戳,以便后续分析。
  3. 日志轮转:当日志文件达到指定大小(默认为50MB)时,自动轮转日志文件,并保留一定数量的历史日志文件。
  4. 可配置参数:支持通过命令行参数自定义时间间隔、日志文件名、运行时长等配置。

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

这意味着,脚本将在以下两个条件之一满足时继续运行:

  1. 已经运行的时间未超过设定的RUN_TIME_SECONDS
  2. 检测到的时间跳变次数未超过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

当检测到时间跳变时,脚本将:

  1. 记录跳变前的时间戳。
  2. 标记当前时间戳为跳变时间。
  3. 在后续的循环中,记录一定数量的跳变后时间戳,确保日志的连续性。

2.5. 日志轮转机制

为防止日志文件过大,脚本实现了日志轮转功能。当日志文件大小超过MAX_FILE_SIZE时,脚本会:

  1. 将当前日志文件重命名为带有时间戳的文件名。
  2. 创建一个新的空日志文件。
  3. 保留最新的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"
相关推荐
深念Y13 天前
状态缓存与TTL:给每个设备状态贴一张“保质期”
数据库·缓存·智能家居·时间·时间戳·智能电视·ttl
WordPress学习笔记15 天前
让wordpress页面显示的时间为当前时间的前30分钟
wordpress·时间戳
闲人编程2 个月前
时序数据库InfluxDB应用
数据库·struts·时序数据库·innodb·时间戳·存储引擎·时间维度
亚林瓜子3 个月前
Spark SQL中时间戳条件约束与字符串判空
大数据·sql·spark·string·timestamp
雪碧聊技术3 个月前
9.大模型如何实现会话记忆隔离?
大模型·时间戳·会话记忆隔离·会话id
丁劲犇5 个月前
全面测试QtSql操作PostgreSQL数据库时戳字段的行为
qt·postgresql·错误·timezone·时区·timestamp·混乱
一个平凡而乐于分享的小比特5 个月前
UCOSIII笔记(十四)时间戳
笔记·时间戳·ucosiii
課代表5 个月前
bat 批处理文件重命名加时间戳
时间·重命名·bat·时间戳·命令·批处理·字符串截取
骄傲的心别枯萎7 个月前
项目1:FFMPEG推流器讲解(五):FFMPEG时间戳、时间基、时间转换的讲解
ffmpeg·音视频·视频编解码·时间戳·rv1126