Shell脚本如何实现FIFO队列

wen 实用脚本 6

Shell脚本实现FIFO队列:从原理到生产级实践指南

📖 目录导读

  1. FIFO队列核心概念与Shell实现价值
  2. 基于命名管道的FIFO队列实现
  3. 基于文件+锁的可靠队列方案
  4. 高级技巧:多消费者与超时处理
  5. 性能优化与常见问题FAQ

1️⃣ FIFO队列核心概念与Shell实现价值

FIFO(First In, First Out)队列是任务调度、日志处理、数据流控制的基础结构,在Shell脚本中实现FIFO队列,主要场景包括:

Shell脚本如何实现FIFO队列

  • 批量任务的顺序执行控制
  • 生产-消费者模式的进程间通信
  • 系统资源分配的排队管理

为什么用Shell而不是专用消息队列?

  • 轻量级,无需安装Redis/RabbitMQ
  • 原生集成系统命令(cron、systemd)
  • 适合边缘设备或最小化依赖的Linux环境

关键问题:命名管道(named pipe)和普通文件+锁机制,哪个更适合你的场景?下文将逐一分析。


2️⃣ 基于命名管道的FIFO队列实现

1 核心原理

命名管道(mkfifo)是Linux内核提供的单向数据流通道,写入的数据按顺序读取,天然具备FIFO特性。

2 基础代码模板

#!/bin/bash
QUEUE="/tmp/my_queue.fifo"
# 初始化队列
[ -p "$QUEUE" ] || mkfifo "$QUEUE"
# 生产者:写入任务
echo "task_$(date +%s)" > "$QUEUE" &
# 消费者:读取任务(阻塞读取)
while read -r task; do
    echo "[Worker] Processing: $task"
    # 执行实际任务逻辑
    sleep 1
done < "$QUEUE"

3 关键注意事项

  • 阻塞风险:消费者未启动时,生产者会阻塞在写操作上
  • 死锁问题:单线程写入+读取时,管道缓冲区(通常64KB)填满后可能死锁
  • 信号处理:建议使用trap清理管道文件,防止残留

改进方案:使用后台循环读取+超时机制(见第4节)


3️⃣ 基于文件+锁的可靠队列方案

1 设计思路

当需要持久化、跨重启或支持多消费者时,命名管道存在天然局限性,文件队列方案更可靠:

  • 每个任务保存为独立文件(或追加到日志文件)
  • 使用flock(文件锁)保证原子性操作

2 完整实现示例

#!/bin/bash
QUEUE_DIR="/var/queue/tasks/"
LOCK_FILE="/var/queue/queue.lock"
enqueue() {
    local task="$1"
    (
        flock -x 200
        echo "$task" >> "$QUEUE_DIR/queue.dat"
        echo "[ENQUEUE] Added: $task"
    ) 200>"$LOCK_FILE"
}
dequeue() {
    (
        flock -x 200
        if [ -s "$QUEUE_DIR/queue.dat" ]; then
            # 读取第一行(FIFO核心)
            local task=$(head -1 "$QUEUE_DIR/queue.dat")
            # 删除第一行
            sed -i '1d' "$QUEUE_DIR/queue.dat"
            echo "$task"
            return 0
        else
            return 1  # 队列为空
        fi
    ) 200>"$LOCK_FILE"
}
# 测试
enqueue "batch_job_1"
enqueue "batch_job_2"
dequeue  # 输出:batch_job_1

3 性能优化点

  • 避免sed -i频繁操作文件,改用多个独立文件(如$QUEUE_DIR/task_001task_002
  • 使用renice调整队列进程优先级
  • dequeue加入重试机制(如等待0.1秒后重试)

4️⃣ 高级技巧:多消费者与超时处理

1 多消费者竞争控制

# 消费者脚本(可并行启动多个实例)
dequeue_with_timeout() {
    local timeout=5
    while [ $timeout -gt 0 ]; do
        result=$(dequeue)
        if [ -n "$result" ]; then
            echo "Consumed: $result"
            return 0
        fi
        sleep 0.05  # 避免忙等
        ((timeout--))
    done
    echo "Timeout: no tasks"
    return 1
}

2 使用inotify实现事件驱动

避免轮询,当文件队列有新任务时立即触发:

inotifywait -e modify "$QUEUE_DIR/queue.dat"
# 然后立刻执行dequeue

3 生产环境建议

  • 对命名管道方案启用超时写入timeout 10 bash -c 'echo task > /tmp/fifo'
  • 文件队列需定期归档旧任务文件,防止磁盘满
  • 使用logger记录队列操作日志,便于问题排查

5️⃣ 性能优化与常见问题FAQ

❓ 问答精选

Q1:命名管道和文件队列,哪个性能更好?
A:小规模任务(<1000个/秒)且消费者快速时,命名管道延迟更低(微秒级),文件队列更适合需要持久化、跨机器或任务文件可能重用的场景。

Q2:如何解决管道死锁?
A:使用 非阻塞写echo task > /dev/null &或改用mkfifo后使用cat < /tmp/fifo &作为背景读取进程。

Q3:多个生产者同时写入文件队列,会不会导致数据错乱?
A:正确使用flock(排他锁)可保证原子写入,注意Linux中>>重定向和flock的组合在NFS环境下可能遇到额外问题,建议使用本地ext4/XFS文件系统。

Q4:队列中任务过多(如10万行),sed -i删除首行很慢怎么办?
A:改用独立任务文件模式:

  • 写入时创建$QUEUE_DIR/task_$(date +%s%N | md5sum | head -c 8)
  • 读取时按文件名排序取首文件
  • 结合rename原子重命名任务文件

⚡ 性能对比表

方案 延迟 持久性 多消费者 易用性
命名管道 5μs
文件+锁 200μs
独立文件模式 350μs

🛠 生产级检查清单

  • 使用ulimit -n提高最大文件描述符数量
  • 对所有队列操作添加异常捕获(trap ERR
  • 设置队列深度阈值(超过1000个任务时发告警)
  • 避免在分布式共享文件系统(如NFS)上部署,推荐本地tmpfs

通过本文,您已掌握Shell实现FIFO队列的两种主流方案,对于99%的Unix环境任务调度需求,基于文件锁的队列在可靠性上更胜一筹;若追求极致性能且任务不会积压,命名管道仍是简洁之选。

抱歉,评论功能暂时关闭!