Atomically write a file without changing inodes (preserve hard link)

hard linkinoderenamesystem-callswrite

The normal way to safely, atomically write a file X on Unix is:

  1. Write the new file contents to a temporary file Y.
  2. rename(2) Y to X

In two steps it appears that we have done nothing but change X "in-place".

It is protected against race conditions and unintentional data loss (where X is destroyed but Y is incomplete or destroyed).

The drawback (in this case) of this is that it doesn't write the inode referred to by X in-place; rename(2) makes X refer to a new inode number.

When X was a file with link count > 1 (an explicit hard link), now it doesn't refer to the same inode as before, the hard link is broken.

The obvious way to eliminate the drawback is to write the file in-place, but this is not atomic, can fail, might result in data loss etc.

Is there some way to do it atomically like rename(2) but preserve hard links?

Perhaps to change the inode number of Y (the temporary file) to the same as X, and give it X's name? An inode-level "rename."

This would effectively write the inode referred to by X with Y's new contents, but would not break its hard-link property, and would keep the old name.

If the hypothetical inode "rename" was atomic, then I think this would be atomic and protected against data loss / races.

Best Answer

The issue

You have a (mostly) exhaustive list of systems calls here.

You will notice that there is no "replace the content of this inode" call. Modifying that content always implies:

  1. Opening the file to get a file descriptor.
  2. optional seek to the desired write offset
  3. Writing to the file.
  4. optional Truncating old data, if new data is smaller.

Step 4 can be done earlier. There are some shortcuts as well, such as pwrite, which directly write at a specified offset, combining steps #2 and #3, or scatter writing.

An alternate way is to use a memory mapping, but it gets worse as every byte written may be sent to the underlying file independently (conceptually as if every write was a 1-byte write call).

→ The point is the very best scenario you can have is still 2 operations: one write and one truncate.

Whatever the order you perform them in, you still risk another process to mess with the file in between and end up with a corrupted file.

Solutions

Normal solution

As you have noted, this is why the canonical approach is to create a new file, you know you are the only writer of (you can even guarantee this by combining O_TMPFILE and linkat), then atomically redirect the old name to the new file.

There are two other options, however both fail in some way:

Mandatory locking

It enables file access to be denied to other processes by setting a special flag combination. Sounds like the tool for the job, right? However:

  • It must be enabled at the filesystem level (it's a flag when mounting).
  • Warning: the Linux implementation of mandatory locking is unreliable.

    Since Linux 4.5, mandatory locking has been made an optional feature. This is an initial step toward removing this feature completely.

This is only logical, as Unix has always shun away from locks. They are error prone, and it is impossible to cover all edge cases and guarantee no deadlock.

Advisory locking

It is set using the fcntl system call. However, it is only advisory, and most programs simply ignore it.

In fact it is only good for managing locks on shared file among several processes cooperating.

Conclusion

Is there some way to do it atomically like rename(2) but preserve hard links?

No.

Inodes are low level, almost an implementation detail. Very few APIs acknowledge their existence (I believe the stat family of calls is the only one).

Whatever you try to do probably relies on either misusing the design of Unix filesystems or simply asking too much to it.

Could this be somewhat of an XY-problem?

Related Question