4

I would like to check if the file that a filedescriptor points to is deleted in Bash (linux).

I have read both Testing if a filedescriptor is valid and Testing if a filedescriptor is valid (for input). But those answers did not help in this slightly different question.

I use the following testcase:

# create file
echo hello > /tmp/test.txt

# open read-only fd
exec 3< /tmp/test.txt

# delete file
rm /tmp/test.txt

# special zero-timeout to check if data available for reading
if read -u 3 -t 0
then
    echo "data available for reading"
else
    echo "no data available"
fi

# close fd (clean up)
exec 3<&-

This script surprisingly indicates that "data available for reading". However, the file does not exist anymore. So there has to be some caching/buffering going on. Perhaps there is another way, or to avoid the buffer/cache?

An alternative that does work, is: ls -l /proc/$$/fd/3 which will indicate -> '/tmp/test.txt (deleted)'. But I would prefer to stick to a pure Bash solution (without spawning too many new processes, or parsing stdout).

Note that in any other circumstance, one could of course just use [ -e /tmp/test.txt ] to check. However, I need to know if the original file was deleted, because meanwhile a new file with the exact same filename may have been created.

For those that wonder why anyone would need this particular result (XY problem), it can be used to safely check from a subshell (with &) if the parent script is still running by opening an extra fd to /proc/$$/cmdline with protection against a collision with a recycled PID.

2
  • @Inian The two questions and solutions aren't the same. This question asks if there is a way via shell to find out whether or not file descriptor is an anonymous inode. The question you've linked has situation where OP was reading documentation on two different versions of read built in Commented Jun 13, 2020 at 20:04
  • 1
    Maybe a better question is: how do I count the number of references to a file? >=1 means the file exists. =0 means no more references to the file, though I am not sure you can actually ever get his result.
    – Pedro
    Commented Jun 14, 2020 at 9:32

3 Answers 3

13

To test whether a file descriptor refers to a regular file that has no remaining link in any directory on the filesystem, you could make a fstat() system call on it and check the number of links (st_nlink field) in the returned structure.

With zsh, you could do it with its stat builtin:

zmodload zsh/stat
fd=3
if
  stat -s -H st -f $fd &&   # can be fstat'ed (is an opened fd)
    [[ $st[mode] = -* ]] && # is a regular file
    ((st[nlink] == 0))      # has no link on the filesystem
then
  print fd $fd is open on a regular file that has no link in the filessystem
fi

bash (the GNU shell) has no equivalent, but if you are on a GNU system, you might have GNU stat in which case you should be able to do something like:

fd=3
if [ "$(LC_ALL=C stat -c %F:%h - <&"$fd")" = 'regular file:0' ]; then
  printf '%s\n' "fd $fd is open on a regular file that has no link in the filessystem"
fi

If your OS kernel is Linux, a more portable approach (for those OSes that don't have zsh and where the core utilities are not from GNU), assuming the proc filesystem is mounted on /proc could be to use ls on /proc/self/fd/$fd:

if
  LC_ALL=C TZ=UTC0 ls -nLd /proc/self/fd/0 <&"$fd" |
    LC_ALL=C awk -v ret=1 '
      NF  {if ($1 ~ /^-/ && $2 == 0) ret=0; exit}
      END {exit(ret)}'
then
  printf '%s\n' "fd $fd is open on a regular file that has no link in the filessystem"
fi

Here duplicating the fd on 0 like in the previous solution, so it works even if fd has the close-on-exec flag (assuming fd is not 0 in the first place, but fd 0 would normally not have the close-on-exec flag).

That kind of approach doesn't work with the fake filesystem that is Linux' procfs to check whether a fd open on /proc/<some-pid>/cmdline refers to a live process:

$ zsh -c 'zmodload zsh/stat; (sleep 1; stat -f0 +nlink; cat) < /proc/$$/cmdline &'
$ 1
cat: -: No such process

See how fstat().st_nlink returned 1 above (which would mean the file still had a link to a directory), while cat's read() on the fd returned an error. That's not usual filesystem semantic.


In any case, to check whether your parent is still running, you can call getppid() which would return 1 or the pid of the child subreaper if the parent died. In zsh, you'd use $sysparams[ppid] (in the zsh/system module).

$ sh -c 'zsh -c '\''zmodload zsh/system
                    print $PPID $sysparams[ppid]
                    sleep 2; print $PPID $sysparams[ppid]
                '\'' & sleep 1'
14585 14585
$ 14585 1

In bash, you could use ps -o ppid= -p "$BASHPID" instead.

Another approach would be to create a pipe between parent and child and check with select/poll (or read -t0 in bash) that it's still up.

Could be done by using a coproc (only recently added to bash) instead of &.

background_with_pipe() {
  coproc "$@" {PARENT_FD}<&0 <&3 3<&- >&4 4>&-
} 3<&0 4>&1

parent_gone() {
  local ignore
  read -t0 -u "$PARENT_FD" ignore
}

background_with_pipe eval '
  parent_gone || echo parent still there
  sleep 2
  parent_gone && echo parent gone
'

sleep 1
exit

Which give:

$ bash ./that-script
parent still there
$ parent gone

Building up on your envisioned approach, and again assuming a Linux kernel with procfs mounted on /proc, you could also do:

exec {PARENT_CANARY}< /proc/self/cmdline; PARENT_PID=$BASHPID
parent_gone() {
  ! [[ /proc/$PARENT_PID/cmdline -ef /proc/self/fd/$PARENT_CANARY ]]
}

(
   parent_gone || echo parent still there
   sleep 2
   parent_gone && echo parent gone
) &

sleep 1

Using [[ file1 -ef file2 ]] that check whether the too files have the same dev and inode number (st_dev and st_ino returned by stat()).

That seems to work with 5.6.0 but as we've seen above that /proc doesn't honour the usual filesystem semantics, I can't guarantee it's race free (PID and inode number could possibly have been reused) or that it would work in future Linux versions.

3
  • Note that stat -c '%h' counts the number of hard links, but that does not include the opened file descriptor. If the file has been deleted in the meanwhile, there is no way to check whether the hard link that is counted is the same or a new one (because the look-up is by path, and not inode). However, since the inode cannot be re-used as long as the file descriptor is still open, a proper solution is to check stat -c '%i' to see if the inode changed, or it throws an error that there is no such file or directory (in either case the original file was deleted).
    – Yeti
    Commented Jun 14, 2020 at 5:42
  • 1
    @Yeti, with zsh stat -f or GNU stat -, we're doing a fstat() so we're looking at the inode open on that fd, that's irrelevant from any path to the file. If st_nlinks is 0, the inode has no corresponding path on the filesystem (no entry in any directory links to it),and closing the fd assuming it's the last fd by any process open to the file (and that the file is not the backend of a loop device, etc.) will reclaim the inode and any data it references. Commented Jun 14, 2020 at 6:27
  • 2
    @Yeti, inode numbers are only unique per filesystem, so if you want to check that the file open on some fd is still the same as the one at some path, you need to check both the device number and inode number (st_dev and st_ino in the structure returned by stat()/fstat()). Commented Jun 14, 2020 at 6:33
7

Your original file exists completely unchanged.

Once a file has been opened by name, the file descriptor your process holds counts as a link to the file. The system does not release the file or its space until all links have been deleted: those can be any number of processes that have a file description open for it, plus any number of hard links.

You could stat the file at the time it was opened, and stat the current file by name. If they are different inodes or a different modification date, you have a deleted file and there is a new file. Or you might find you have a deleted file but no new one exists.

5
  • Exactly, since even inodes can be re-used, the only way to be sure is to use the time. That works perfectly on awk {print $22} /proc/.../stat, but for regular files, it would only work with the "birth time" (stat -c '%w'). And that still assumes that the system time did not change (i.e. through ntp).
    – Yeti
    Commented Jun 13, 2020 at 17:12
  • Size and then checksum (if needed) would be reasonable if the file is not huge. The philosophical issue whether a fresh bit-identical file is "different" does not trouble me. Actually, though, if my process is holding the last link to a file, that inode can't be re-used, although if I close and re-open it by name, the same inode may be used for the new file immediately. Commented Jun 13, 2020 at 22:22
  • 1
    @Yeti, the inode number won't be reused as long as you're holding the old inode open. Because that's what the open fd refers to, the inode, not the name. You can save the file name when you open the file, like the answer here says, but if it's gone, you can't know if the file was deleted or just renamed.
    – ilkkachu
    Commented Jun 14, 2020 at 13:03
  • 1
    That approach doesn't work in the general case. You stat() a path, you fstat() a fd, if stat() fails, it doesn't mean the file doesn't exist anymore, it only means it doesn't exist at that path anymore (assuming stat() fails with ENOENT or ENOTDIR at least). But the file may still exist and be even linked to several directories, including with the same entry you opened it in the first place (for instance if you opened it as /a/b/file and /a has been renamed to /x, or /a/b was a symlink, now deleted to /x/y, etc.). Commented Jun 14, 2020 at 16:08
  • @StéphaneChazelas You need control of the code to fstat(), which is usually not an option. I was intending to stat and open each file as near simultaneously as possible (still a race condition) and keep those dev/inode ids for checking later. I would avoid this situation anyway: I want every file with a unique timestamp in the filename, and a soft link set by the writer to optimise finding the latest, and a cron job to tidy up the deadwood after a decent period to allow auditing and reruns. Commented Jun 14, 2020 at 16:50
0

To test in bash whether a file descriptor references a deleted, file, usr /proc/pid/fd. Example below.

$ ps -fp 52
UID        PID  PPID  C STIME TTY          TIME CMD
steve       52     7  0 18:07 tty1     00:00:00 tail -f x1.pdf
$ ls -l /proc/52/fd
total 0
lrwx------ 1 steve steve 0 Jun 13 18:07 0 -> /dev/tty1
lrwx------ 1 steve steve 0 Jun 13 18:07 1 -> /dev/tty1
lrwx------ 1 steve steve 0 Jun 13 18:07 2 -> /dev/tty1
lr-x------ 1 steve steve 0 Jun 13 18:07 3 -> /mnt/c/temp/x1.pdf (deleted)
$
3
  • 1
    Note that (deleted) does not work for the /proc filesystem.
    – Yeti
    Commented Jun 14, 2020 at 5:12
  • Note that you'll also see that (deleted) suffix if the file name had that suffix in the first place (or was renamed to one with that suffix), so you can't use that alone to determine if the file was deleted. Commented Jun 15, 2020 at 7:23
  • You'll get that (deleted) suffix if the directory entry that was used to open the file was deleted even if the file itself was not deleted (had other links), even if the file was later re-linked to same directory with the same name. You don't get the (deleted) suffix if the entry (or any component of its path) was renamed though. Commented Jun 15, 2020 at 7:27

Not the answer you're looking for? Browse other questions tagged .