0Pricing
Linux Command Line & Bash Scripting Mastery · 课时

安全的临时文件与锁目录

使用 mktemp 和 flock 创建无竞态的临时资源,防止脚本并发运行。

安全的临时文件与锁目录 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。

临时文件为何存在安全风险

Bash 脚本经常需要临时存储空间——中间结果、锁标记、暂存区域。但不谨慎地创建临时文件会带来严重漏洞。

  • 竞争条件:其他进程可能预测出您的文件名并抢先创建文件,从而重定向您的写入。
  • 符号链接攻击:攻击者在预期路径上创建符号链接,将其指向 /etc/passwd 等敏感文件。
  • 残留文件:如果脚本崩溃,临时文件会不断堆积,并可能暴露敏感数据。

能够消除这些问题的两个核心工具是 mktemp 和 flock。本课将展示如何安全且具有防御性地使用这两个工具。

使用 mktemp 创建安全的临时文件

mktemp 会创建一个名称随机且不可预测的临时文件,并返回其路径。它会以原子方式创建文件,因此其他进程无法抢先占用该名称。

  • 语法:mktemp [TEMPLATE]——模板必须以至少三个 X 字符结尾。
  • 每个 X 都会替换为一个随机字符,从而生成类似 /tmp/script.aB3kQz 的唯一名称。
  • 文件会自动以 0600 权限创建(只有所有者可读取)。

请始终立即将返回的路径保存到变量中,以便稍后引用并清理该文件。

#!/usr/bin/env bash
set -euo pipefail

# Create a secure temp file
TMPFILE=$(mktemp /tmp/myapp.XXXXXX)
echo "Temp file created at: $TMPFILE"

# Write data to it
echo "some intermediate result" > "$TMPFILE"

# Read it back
cat "$TMPFILE"

# Clean up
rm -f "$TMPFILE"

始终使用 trap 进行清理

如果脚本意外退出——由于错误、信号或触发了 set -e——除非注册清理处理程序,否则临时文件会被遗留。

trap 内建命令会在 shell 收到信号或退出时执行命令。清理临时文件的标准模式是:

  • 创建临时文件后立即注册陷阱。
  • 在 EXIT 上设置陷阱,确保正常退出和异常退出时都会执行清理。
  • 如果脚本运行时间较长或具有交互性,也要在 INT 和 TERM 上设置陷阱。

这样可以确保不会留下孤立文件,即使脚本在执行过程中被终止也是如此。

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/report.XXXXXX)

# Register cleanup before doing any real work
cleanup() {
    rm -f "$TMPFILE"
    echo "Cleaned up $TMPFILE" >&2
}
trap cleanup EXIT

# Do work — even if this fails, cleanup() will run
echo "Processing..." > "$TMPFILE"
grep "result" "$TMPFILE" || true

echo "Done. File will be removed on exit."

使用 mktemp -d 创建临时目录

有时您需要一个完整的目录来暂存多个文件,例如在处理之前创建归档文件或解压 tar 包。请使用 mktemp -d 创建安全的临时目录。

  • 该目录会以 0700 权限创建(仅所有者可访问)。
  • 请在您的陷阱处理中使用 rm -rf 清理;务必只删除变量指定的路径,绝不要删除硬编码的路径。
  • 使用双引号,并在调用 rm -rf 前确认变量不为空,作为额外的安全检查。
#!/usr/bin/env bash
set -euo pipefail

TMPDIR=$(mktemp -d /tmp/extract.XXXXXX)

cleanup() {
    # Guard: only rm if variable is set and non-empty
    [[ -n "${TMPDIR:-}" ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT

echo "Working in $TMPDIR"

# Simulate staging files
echo "file one" > "$TMPDIR/part1.txt"
echo "file two" > "$TMPDIR/part2.txt"

ls "$TMPDIR"
echo "All done."

并发运行脚本的问题

Cron 作业、systemd 定时器和手动触发的脚本都可能轻易地同时启动同一脚本的多个实例。这会导致:

  • 重复处理:相同的数据库记录或文件被处理两次。
  • 输出损坏:两个实例同时写入同一个输出文件。
  • 死锁或状态不完整:两个实例以交错且不可预测的顺序修改共享资源。

传统的解决方法是写入一个 PID 文件,并在启动时检查它,但检查和写入之间存在竞态窗口。现代的正确解决方案是 flock,它使用内核的建议锁定机制来提供无竞态的锁。

使用 flock 加锁:单行命令模式

flock 会在运行命令前对文件描述符获取建议锁。最简单的用法是在命令行中包裹整个脚本:

flock -n /var/lock/myscript.lock bash myscript.sh

  • -n(非阻塞):如果锁已被占用,则立即以状态 1 退出,而不是等待。
  • 不使用 -n 时,flock 会一直阻塞,直到锁可用,这适合用于排队。
  • 锁文件本身只是一个标记,其内容并不重要。在多次运行之间保留它是安全的。
  • 持有锁的进程退出后,内核会自动释放锁,无需手动清理。
#!/usr/bin/env bash
# launcher.sh — prevents concurrent runs of worker.sh
set -euo pipefail

LOCKFILE="/tmp/myworker.lock"

if ! flock -n "$LOCKFILE" bash -c 'echo "Running worker..."; sleep 2; echo "Done."'; then
    echo "Another instance is already running. Exiting." >&2
    exit 1
fi

在脚本中使用文件描述符调用 flock

如果要在脚本内部加锁,而不是从外部包裹脚本,请使用 exec 打开文件描述符,然后对该描述符调用 flock。这是生产环境脚本中惯用的模式。

  • exec 200>"$LOCKFILE"以写入方式打开文件描述符 200 对应的文件(必要时创建该文件)。
  • flock -n 200尝试以非阻塞方式锁定描述符 200。
  • 由于锁绑定到文件描述符(而不是文件名),shell 进程退出时会自动释放锁。
  • 通常使用 200 到 299 的描述符编号,以避免与标准输入、标准输出和标准错误冲突。
#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/myjob.lock"

# Open lock file on FD 200
exec 200>"$LOCKFILE"

# Attempt non-blocking lock
if ! flock -n 200; then
    echo "ERROR: Another instance of this script is running." >&2
    exit 1
fi

echo "Lock acquired. Starting work..."
sleep 1
echo "Work complete. Lock will be released on exit."

在同一脚本中组合使用 mktemp 和 flock

真正具有防御性的脚本需要两者:使用锁防止并发运行,并使用安全的临时文件保存中间数据。下面是组合这两种技术的完整模式:

  • 先获取锁,再创建任何临时文件,以确保只有一个实例会执行任何操作。
  • 确认锁已获取后,再创建临时资源。
  • 创建临时资源后立即注册 trap,这样无论脚本如何退出,都能确保执行清理。
  • 锁文件绝不能放在临时目录中,它必须在多次运行之间保留,以便 flock 引用。
#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/report_builder.lock"
exec 200>"$LOCKFILE"

if ! flock -n 200; then
    echo "Already running — aborting." >&2
    exit 1
fi

# Now safe to create temp resources
TMPDIR=$(mktemp -d /tmp/report.XXXXXX)
TMPLOG=$(mktemp /tmp/report_log.XXXXXX)

cleanup() {
    rm -rf "${TMPDIR:-}"
    rm -f  "${TMPLOG:-}"
}
trap cleanup EXIT

echo "Building report in $TMPDIR" | tee "$TMPLOG"
echo "Step 1 complete"             >> "$TMPLOG"
cat "$TMPLOG"

将锁目录作为另一种锁定机制

在无法使用 flock 的系统上(例如某些嵌入式系统或 NFS 等网络文件系统),您可以改用锁目录。在 POSIX 系统中,mkdir 是原子操作:只有目录尚不存在时才会成功。

  • 使用 mkdir /tmp/myscript.lock.d 创建锁目录;如果另一个实例已经创建了该目录,mkdir 会立即失败。
  • 将元数据(例如 PID)存储在目录中,以便诊断。
  • 始终在 EXIT 上触发的 trap 中删除该目录。
  • 注意:与 flock 不同,如果进程被 -9 终止或机器重启,目录锁不会自动释放,因此请添加过期锁检测检查。
#!/usr/bin/env bash
set -euo pipefail

LOCKDIR="/tmp/myscript.lock.d"

# Atomic mkdir — fails if directory already exists
if ! mkdir "$LOCKDIR" 2>/dev/null; then
    # Check if the holding PID is still alive
    HOLDER_PID=$(cat "$LOCKDIR/pid" 2>/dev/null || echo "")
    if [[ -n "$HOLDER_PID" ]] && kill -0 "$HOLDER_PID" 2>/dev/null; then
        echo "Locked by PID $HOLDER_PID. Exiting." >&2
        exit 1
    else
        echo "Stale lock detected. Removing and continuing." >&2
        rm -rf "$LOCKDIR"
        mkdir "$LOCKDIR"
    fi
fi

echo "$$" > "$LOCKDIR/pid"
trap 'rm -rf "$LOCKDIR"' EXIT

echo "Lock acquired via directory. Running..."
sleep 1
echo "Done."

使用 flock 进行基于超时的等待

有时您希望等待锁,而不是立即失败,但又不想无限期等待。flock 支持通过 -w 标志设置超时时间。

  • flock -w 10 200最多等待锁 10 秒;如果锁仍不可用,则以状态 1 退出。
  • 这非常适合这样的脚本:它应当排在一个运行时间较短的前置任务之后,但如果前置任务卡住,则应放弃等待。
  • 将 -w 与有意义的错误消息结合使用,并在消息中包含上下文信息(锁文件路径和等待时长),这样操作人员就能快速诊断挂起问题。
#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/data_sync.lock"
TIMEOUT=15

exec 200>"$LOCKFILE"

echo "Waiting up to ${TIMEOUT}s for lock on $LOCKFILE..."

if ! flock -w "$TIMEOUT" 200; then
    echo "ERROR: Could not acquire lock after ${TIMEOUT}s." \
         "Another instance may be hung." >&2
    exit 1
fi

echo "Lock acquired. Syncing data..."
sleep 1
echo "Sync complete."

防御性检查清单:安全的临时资源

在发布任何使用临时文件或锁的脚本之前,请逐项检查以下清单:

  • 使用 mktemp,绝不要使用硬编码路径——/tmp/myapp.tmp 是可预测且可被利用的。
  • 立即保存路径——在执行任何其他命令之前运行 TMPFILE=$(mktemp ...)。
  • 创建后立即注册 trap cleanup EXIT——不要等到脚本末尾才注册。
  • 对所有变量的使用加双引号——使用 rm -f "$TMPFILE",绝不要使用 rm -f $TMPFILE。
  • 优先使用 flock,而不是 PID 文件——由内核管理,并会在崩溃时自动释放。
  • 默认使用非阻塞的 -n——无提示的阻塞锁会掩盖性能问题。
  • 将锁文件放在临时目录之外——这样它不会被清理陷阱删除。
  • 测试清理行为——运行脚本,并在执行过程中使用 kill -9 终止它;确认没有遗留文件(对于基于 flock 的脚本;目录锁需要额外处理)。

知识检查:flock 标志的行为

某个 Cron 作业每分钟运行一次,并处理一个共享文件。您希望在上一次运行仍处于活动状态时,任何新的调用都立即退出并报告错误,而不是等待。哪种 flock 调用能够正确实现这一点?

回顾:安全的临时文件和锁目录

在本课中,您学习了 Bash 防御性资源管理的两个基本工具:

  • mktemp 会创建不可预测且权限安全的临时文件(0600)和目录(0700),从而消除硬编码路径容易遭遇的竞态条件和符号链接攻击。
  • trap cleanup EXIT 会确保临时文件在任何退出情况下被删除——无论是正常退出、由错误触发的退出,还是由信号触发的退出;前提是它在创建临时文件后立即注册。
  • flock 提供由内核强制执行的建议锁定:使用 -n 在发生竞争时快速失败,使用 -w N 在超时时间内等待,并使用 exec 200>file 模式在脚本内部加锁,由内核在进程退出时自动释放。
  • 锁目录(mkdir)可作为 flock 不可用时的可移植备用方案,但需要显式检测过期锁。
  • 始终将锁文件放在临时目录之外,并对清理过程中使用的每个变量加双引号。

组合使用 mktemp + flock + trap,可以让脚本抵御并发调用、不可预测的崩溃以及恶意的文件系统操作。

常见问题解答

「安全的临时文件与锁目录」课时是免费的吗?

是的 — 「安全的临时文件与锁目录」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。

「安全的临时文件与锁目录」这节课中我会学到什么?

使用 mktemp 和 flock 创建无竞态的临时资源,防止脚本并发运行。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「安全的临时文件与锁目录」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?

能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用 set -euo pipefail 启用严格模式
  2. 用于清理和处理信号的陷阱处理器
  3. 安全的临时文件与锁目录
  4. 幂等脚本与带退避的重试逻辑
← 返回 Linux Command Line & Bash Scripting Mastery