Shell脚本实现FIFO队列:从原理到生产级实践指南
📖 目录导读
1️⃣ FIFO队列核心概念与Shell实现价值
FIFO(First In, First Out)队列是任务调度、日志处理、数据流控制的基础结构,在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_001→task_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环境任务调度需求,基于文件锁的队列在可靠性上更胜一筹;若追求极致性能且任务不会积压,命名管道仍是简洁之选。