Shell 脚本:“如果文件未被使用”条件

Shell 脚本:“如果文件未被使用”条件

我正在尝试通过脚本压缩虚拟机映像文件,但我想确保该文件没有被访问。我可以检查 virt-manager 是否正在运行,因为它应该是访问图像的唯一程序,但我不知道是否有更好的方法来做到这一点。我还希望脚本继续尝试,直到文件可供压缩。我也不知道该怎么办。

#Check if virt-manager is running
if pgrep "virt-manager" > /dev/null
then
    #re-run script until success
else
    gzip -k < /home/brady/.vms/windows10/hdd.img > /media/backup/vms/windows10/hdd.$(date +"%F.%T).img.gz

答案1

lsof命令可以告诉您文件是否正在使用。您可以将其放入带有whilea 的循环中sleep,以使其经常检查。

例如:

在窗口 1 中你可以运行sleep 10000 > /tmp/x

在窗口 2 中运行以下脚本:

#!/bin/bash

FILE=/tmp/x

while [ -n "$(lsof "$FILE")" ]
do
  sleep 1
done

echo "File $FILE not in use"

现在,当您按下control-C中止按钮时,sleep您将在一秒钟左右看到“文件未使用”响应。

答案2

在安装了 inotify 工具的 Linux 上,您可以执行以下操作:

#! /bin/zsh -
file=${1?}

# if it's a symlink, we want the real file, readlink also tells us
# if the file is accessible
file=$(readlink -e -- "$file") || exit

# start inotifywait as a coproc so we can terminate it after we're
# done:
coproc LC_ALL=C inotifywait -me close --format . -- "$file" 2>&1

# Now wait for the "Watches established." messaged. First, that allows us
# to verify inotifywait started properly, and that also avoids the race
# condition where the last file user is gone after our fuser check but
# before the watch is in place
read <&p && read <&p && [ "$REPLY" = "Watches established." ] || exit

# Now watch CLOSE events until the file has no more user:
while fuser -s "$file" && read <&p; do continue; done
printf '"%s" is no longer used, renaming it to prevent new access\n' "$file"
kill %
ret=0
if mv -- "$file" "$file.moved-away"; then
  printf 'and now compressing it\n'
  pixz -t < "$file.moved-away" > "$file.xz" || ret=$?
  mv -- "$file.moved-away" "$file" || ret=$? # move back
else
  ret=$?
fi
exit "$ret"

使用inotifywait,每次文件的 fd 关闭时我们都会收到通知。这意味着我们不必经常检查文件,并且可以在最后一个用户关闭文件后立即开始压缩。

请注意,从我的测试来看,与我最初担心的相反,这也适用于映射文件,因为在这些情况下,事件CLOSE不是在最后生成的,close()而是在最后生成的munmap()(当文件完全释放时)。

fuser -s将是一种比lsof.fuser也更有可能可用,因为它是标准 UNIX 命令(虽然-s不是标准选项,但 Linux 上的可用版本确实支持它)。

在压缩之前,我们将文件移走以防止进一步访问。

我们使用pixz(多线程版本的xz,尽管最新版本xz现在也支持多线程),因为它提供了比 gzip 更好的压缩比,更重要的是,因为压缩文件是随机访问的(您可以挂载内容或启动它在使用 nbdkit 的 VM 中,而无需解压缩整个映像)。

请注意,像lsof,fuser不会检测用作loopmtd设备后端的文件。对于循环设备,您可以使用losetup -j "$file"它来检查文件是否正在以这种方式使用。例如,您可以在文件移走后插入此循环:

while [ -n "$(losetup -j "$file.moved-away")" ]; do
  sleep 1
done

答案3

lsof是适合这项工作的工具,但默认情况下它会检查所有 PID,因此速度缓慢且占用大量 CPU 资源。幸运的是,有一些方法可以加快速度。

顺便说一句,virt-manager这不会是保持虚拟机磁盘映像文件/设备打开的过程。那将是二进制文件之一qemu,例如qemu-system-x86_64

 

如果您知道只有特定进程可能会打开您的文件,并且您知道或可以获得它们的 PID,那么您可以将它们作为逗号分隔列表提供给lsof -p.例如

pids=$(pgrep qemu-system | paste -sd,)
[ -n "$pids" ] && lsof -p "$pids" | grep -i filename

更好的是,您可以使用选项指定进程lsof名称-c-c不需要进程名称的精确匹配,它需要一个模式(最大长度为 15 个字符)。如果需要,您可以在命令行上使用-c多次。有关详细信息,请参阅man lsoflsof 常见问题解答。

如果您使用超过 15 个字符,您将收到如下错误消息:

# lsof -c qemu-system-x86_64
lsof: "-c qemu-system-x86_64" length (18) > what system provides (15)

无论如何,举个例子:

# lsof -c qemu-system | grep -i FreeBSD-10.2-RELEASE-amd64.qcow2
qemu-syst 4770 libvirt-qemu   20u      REG                8,3 1837236224 403730954 /var/lib/libvirt/images/FreeBSD-10.2-RELEASE-amd64.qcow2

在一个while循环中,这将是这样的:

pname='qemu-system'
fname='FreeBSD-10.2-RELEASE-amd64.qcow2'

while lsof -c "$pname" | grep -qi "$fname" ; do
  sleep 0.1   # don't need to sleep for as long between checks
              # but if you're not impatient, leave it at 1 second
              # rather than 0.1.
done

echo "$fname is not in use"

如果您为虚拟机使用原始分区(例如fname='/dev/sda5'在上面的脚本片段中使用),这也适用。

如果您使用 ZFS ZVOL 或 LVM LV 或类似的而不是基于文件的映像,则情况会变得稍微复杂一些。 lsof在解析任何符号链接后显示实际的块设备名称,因此您还必须解析符号链接(例如使用readlink -f)和 grep 。

freedos例如,对于 ZFS,在池上调用 ZVOL volumes

# fname=$(readlink --n f /dev/zvol/volumes/freedos)

# echo "$fname"
/dev/zd32

对于具有名为 LV 的 LVM centos7

# fname=$(readlink -n -f /dev/mapper/centos7)

# echo $fname
/dev/dm-1

注意:/dev/vg/centos7代替/dev/mapper/centos7也有效。


我最初开始编写基于此find的方法作为答案,但意识到该lsof -c方法更好。我将其保留下来只是为了记录另一个相当快的替代方案。

find -lnamelsof与没有PID 选项的情况下运行相比,系统上的速度更快、更轻-p

例如

# sleep 10000 > /tmp/foo &
[1] 31077
# find /proc/[0-9]*/fd/ -lname '/tmp/foo'
/proc/31077/fd/1

相关内容