TL;DR:
While there is no direct way to query if a filesystem is frozen, you can abuse the fact that nested freezing attempts don't work. For example, I have an XFS filesystem mounted at /xfs_test:
[root@testvm1 ~]# mount | grep xfs_test
/dev/sdb1 on /xfs_test type xfs (rw,relatime,seclabel,attr2,inode64,noquota)
[root@testvm1 ~]# xfs_freeze -f /xfs_test/ # Initial freeze
[root@testvm1 ~]# echo $?
0
[root@testvm1 ~]# xfs_freeze -f /xfs_test/ # Subsequent freeze attempt
xfs_freeze: cannot freeze filesystem at /xfs_test/: Device or resource busy
[root@testvm1 ~]# echo $?
1
The same analogy works for un-freezing or thawing the filesystem:
[root@testvm1 ~]# xfs_freeze -u /xfs_test/ # Same filesystem, currently frozen
[root@testvm1 ~]# echo $?
0
[root@testvm1 ~]# xfs_freeze -u /xfs_test/ # Thawed filesystem
xfs_freeze: cannot unfreeze filesystem mounted at /xfs_test/: Invalid argument
[root@testvm1 ~]# echo $?
1
I had to do some digging to find out if the frozen/thawed state query was possible. Although the technical details were a little beyond my understanding, this is what I have put together.
Freezing and thawing a filesystem was a feature initially built for XFS. It was eventually brought forward to the Linux kernel, allowing the functionality to work for other filesystems as well. As this LWN article explains:
Takashi Sato proposes taking an XFS-specific feature and moving it
into the filesystem code. The patch would provide an ioctl() for
suspending write access to a filesystem, freezing, along with a
thawing option to resume writes.
...
Essentially the patch just exports the freeze_bdev() kernel function
in a user accessible way.freeze_bdev() locks a file system into a
consistent state by flushing the superblock and syncing the device.
The patch also adds tracking of the frozen state to the struct
block_device state field.
At this point, nested freezing and thawing were possible. As I understand it, a counter variable in the code kept track of the nested freezing and thawing attempts. The variable would increment when freezing, and decrement when thawing. Only when the counter was 0 would the filesystem actually thaw.
After that, I found this patch discussion from 2016, which is about adding a new ioctl call to query the filesystem's state: fs: add the FIGETFROZEN ioctl call. As far as I can tell, this patch has not been merged into the kernel.
The patch discussion provided a few key points:
And, besides, polling for frozenness from userspace is inherently racy
- by the time the syscall returns, the information may be incorrect, so you can't rely on it for decision making purposes in userspace.
And then:
I [sic] quick dig shows nesting was intentionally broken more than 5
years ago in making the freeze ioctl work on btrfs.
This led me to the, ahem, solution shown above.
Yet another possibility is covered in this answer over at StackOverflow. If you try to remount a filesystem which is frozen, the attempt will fail with a 'device is busy' error. The mount could also be busy for a number of other reasons, so this solution is admittedly not fool-proof.