Script de shell: condição “se o arquivo não estiver sendo usado”

4

Estou tentando compactar um arquivo de imagem de máquina virtual por meio de um script, mas quero ter certeza de que o arquivo não está sendo acessado. Eu poderia verificar se o virt-manager está rodando, já que ele deve ser o único programa que acessa a imagem, mas não sei se há uma maneira melhor de fazê-lo. Eu também quero que o script continue tentando até que o arquivo esteja disponível para compactar. Eu não sei como fazer isso.

#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
    
por Brady Dean 17.07.2016 / 03:24

3 respostas

5

O comando lsof pode informar se um arquivo está em uso. Você pode colocar isso em um loop while com sleep para fazer a verificação de tempos em tempos.

Por exemplo:

Na janela 1 você pode executar sleep 10000 > /tmp/x

Na janela 2, execute este script:

#!/bin/bash

FILE=/tmp/x

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

echo "File $FILE not in use"

Agora, quando você pressionar control-C para abortar o sleep , verá a resposta "Arquivo não em uso" dentro de um segundo ou mais.

    
por 17.07.2016 / 03:36
6

No Linux com as ferramentas inotify instaladas, você poderia fazer:

#! /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"

Com inotifywait , somos notificados toda vez que um fd é fechado no arquivo. Isso significa que não precisamos verificar o arquivo com tanta frequência e começar a compactar assim que o último usuário tiver fechado o arquivo.

Observe que, dos meus testes, e ao contrário do que eu inicialmente temia, que também funciona para arquivos mmap, como nesses casos, o evento CLOSE não é gerado no close() , mas no último munmap() (quando o arquivo está totalmente liberado).

fuser -s será uma maneira muito mais fácil de verificar se um arquivo está aberto do que lsof . fuser também tem mais chances de estar disponível, pois é um comando padrão do UNIX (embora -s não é uma opção padrão, a versão disponível no Linux suporta isso).

Nós movemos o arquivo para evitar mais acesso antes de compactá-lo.

Usamos pixz (uma versão multissegmentada de xz , embora a versão mais recente de xz também ofereça suporte a multitarefas), pois oferece uma taxa de compactação muito melhor que o gzip e, mais importante, porque o zipado o arquivo é acessado aleatoriamente (você pode montar o conteúdo ou inicializá-lo em uma VM usando o nbdkit sem ter que descompactar a imagem inteira).

Observe que, como lsof , fuser , não detectará o arquivo que está sendo usado como um backend de dispositivo loop ou mtd . Para dispositivos de loop, você pode usar losetup -j "$file" para verificar se o arquivo está sendo usado dessa maneira. Você poderia, por exemplo, inserir este loop após o arquivo ter sido movido:

while [ -n "$(losetup -j "$file.moved-away")" ]; do
  sleep 1
done
    
por 17.07.2016 / 09:05
2

lsof é a ferramenta certa para esse trabalho, mas, por padrão, ele examina todos os PIDs por isso é lento e exige muita CPU. Felizmente, existem algumas maneiras de acelerá-lo.

BTW, virt-manager não será o processo que mantém o arquivo de imagem / dispositivo de disco da VM aberto. Esse será um dos binários qemu , por exemplo qemu-system-x86_64

Se você sabe que apenas processos específicos podem ter seu arquivo aberto e você sabe ou pode obter seus PIDs, você pode fornecê-los como uma lista separada por vírgula para lsof -p . por exemplo.

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

Ainda melhor do que isso, você pode fornecer o nome do processo para lsof com a opção -c . -c não requer uma correspondência exata do nome do processo, ele usa um padrão (com um comprimento máximo de 15 caracteres). Você pode usar -c várias vezes, se necessário, em uma linha de comando. veja man lsof e a FAQ do lsof para detalhes.

Se você usa > 15 caracteres, você receberá uma mensagem de erro como esta:

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

De qualquer forma, um exemplo:

# 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

Em um loop while , seria algo assim:

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"

Isso também funciona se você estiver usando uma partição bruta para sua VM, por exemplo, use fname='/dev/sda5' no fragmento de script acima.

Se você estiver usando ZVOLs ZFS ou LVs LVM ou similares em vez de imagens baseadas em arquivos, fica um pouco mais complicado. lsof exibe o nome real do dispositivo de bloco depois que qualquer link simbólico é resolvido, então você também precisa resolver os links simbólicos (por exemplo, usando readlink -f ) e grep para isso.

por exemplo. para o ZFS, com um ZVOL chamado freedos no pool volumes :

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

# echo "$fname"
/dev/zd32

e para o LVM com um LV chamado centos7 :

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

# echo $fname
/dev/dm-1

NOTA: /dev/vg/centos7 em vez de /dev/mapper/centos7 também funciona.

Eu originalmente comecei a escrever este método find -based como uma resposta, mas percebi que o método lsof -c é melhor. Deixarei para documentar outra alternativa razoavelmente rápida.

find -lname é muito mais rápido e leve no sistema do que executar lsof sem a opção -p PID.

por exemplo,

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