Check if a filedescriptor refers to a deleted file (in Bash)

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.


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.