Things Unix can do atomically (2010)
rcrowley.org
rcrowley.org
> However, when overwriting there will probably be a window in which both oldpath and newpath refer to the file being renamed.
A better way to think about rename is that it's given by two atomic operations in sequence: (1) An atomic link call to make the file available under the new name, and then (2) an atomic remove operation to delete the old name. Thus, it's possible to observe the state after (1) but before (2).
I guess you shouldn't ignore the error with both processes using the same destination name, why folks generally tend to put their pid in the lockfilename
If you want atomic creation of a path, where no one else can succeed also, you want link(2) not rename(2).
I've wondered ever since I read that what the first bullet there means. It can't be what you're saying here, what you're saying would apply to racing processes as well as crashes?
So then.. what does he mean when he says rename isn't atomic if there's a crash? What's the failure condition / in-between state that can occur if there's a crash?
Not sure. One of the papers he cites [1] has this to say about rename atomicity:
>Directory operations such as rename() and link() are seemingly atomic on all file systems that use techniques like journaling or copy-on-write for consistency
So it seems to depend on the file system. But the authors claim that not just link but also rename is atomic without qualification, which appears to be false, so I'm not sure how trustworthy this is.
There's a stackoverflow comment [2] that suggests that ext4 guarantees the kind of sequential atomicity property (atomic link followed by atomic remove) I mentioned:
>Kernel implementation depends on filesystem but here's implementation of Linux ext4 filesystem: elixir.bootlin.com/linux/v5.19.3/source/fs/ext4/namei.c#L3887 – it seems to first mark the inode as dirty, then create new link and after that delete the old link. If the kernel crashes in the middle, the end result could be two links and dirty mark. I would guess (but didn't investigate if this true) that journal recovery during the next mount would fix the end result to match atomic behavior of leaving either the old or new state depending on exact crash location.
In general, I find it rather weird how little authoritative documentation there is about whether or not this property holds given how important it is.
[1] https://www.usenix.org/system/files/conference/osdi14/osdi14...
[2] https://stackoverflow.com/questions/7054844/is-rename-atomic...
Well, it can't be that important then, can it? :)
This is a great point though, when taking oldpath into account the operation is not atomic. TIL
> The philosophy here is to let the kernel do as much work as possible. At my most pessimistic, I trust the kernel developers more than a trust myself. More practically, it’s stupid to spend CPU time locking around an operation that’s already atomic.
From a system's point of view, this list is also a list of demands on file system design. And you can tell that POSIX was invented before distributed computing really was a thing.
Some of the POSIX demands are really quite costly to satisfy in a distributed, networked filesystem, and often unnecessarily degrade performance.
In any case, I have seen enough of these "A, B, C : pick N" (where 0 < N < 3) mantras shot down (usually by re-examining whether the constraints/assumptions of the original mantra apply in a specific situation, or just through plain algorithmic engineering) that whenever someone drops something like this, I mostly tune it out.
In the case of distributed network <anything>, implementing programs in languages that help you manage important invariants automatically (and preferably not at run-time, but at compile-time) is key in creating robust systems that are both reliable, and fast.
But if you have such tools, and you are trying to also meet a set of standards that were designed without any care for distributed computing, then...yeah.
Reliable: A system can be available but not reliable; if a machine is down 6 minutes every hour, it's 90% availabile but less than 1 hour reliable. A networked distributed system can be reliable but not accurate if you keep it online while it slowly accrues errors, such as the mentioned link/unlink race condition. The machine still works, it just has occasional corruption. It tends not to be very fast if it's reliable.
Fast: You can do whatever you want quickly. Think UDP, or filesystems that don't commit their log journal frequently. Tends not to be reliable or accurate.
Accurate: Whatever you do has no corrupted state. Think ACID. Most systems don't do this because it's expensive/complicated.
> Think ACID. Most systems don't do this because it's expensive/complicated.
Is this true? A lot of systems are built on databases.
> Accurate: Whatever you do has no corrupted state
How does accuracy work if the system is distributed and there's a partition, and two node get different data? Presumably it's reliable if they're different, as accuracy is a different item?
> Fast: You can do whatever you want quickly. Think UDP, or filesystems that don't commit their log journal frequently. Tends not to be reliable or accurate.
Why wouldn't this be reliable?
> How does accuracy work if the system is distributed and there's a partition, and two node get different data?
You don't allow writes during a partition. All nodes (or a quorum) must confirm on each write, and if that fails then a partition is detected. Everything that was accidentally written before you detected partition is thrown away. Usually you just have one writer node for simplicity.
> Fast: > Why wouldn't this be reliable?
If you prioritize reliability you will need a slower method to ensure reliability. TCP is slower than UDP so it can be reliable.
Wouldn't this be done better with a majority/minority approach, where if enough nodes are still communicating then they still work?
> If you prioritize reliability you will need a slower method to ensure reliability. TCP is slower than UDP so it can be reliable.
Agree - that is true in the TCP sense of the word reliable. But in your sense of the word, as I understood it, the TCP/UDP difference is more like "accuracy", is it not? Reliability is more like availability, as I read it in your previous comment.
But formulated in that language, my comment becomes: POSIX picks less than useful trade-offs for distributed file systems.
An extreme example: POSIX's atime gives you something you seldom need, but imposes a huge burden. That's such a bad trade-off, that approximately no-one implements the original spec, even if nothing is distributed at all.
I had it in my head that modern flash actually does read in a way that involves writing, and we just paper over that for the sake of the abstractions we use. Am I wildly misremembering?
Sometimes blocks may have to be read and re-written elsewhere to make room... and this is what makes using TRIM and having cache on-disk so important.
He basically stated the CAP theorem. It's been proven mathematically.
Essentially if you distribute your data, you have to choose some degree of consistency versus availability.
I'm not fully familiar with POSIX requirements for filesystems, but having atomic changes and good performance and a host of other aspects to keep the illusion of available and consistent files in a distributed store runs right through the CAP theorem.
As an example of how to do better, squint a bit and have a look at git interpreted as (part of) a distributed data store.
Git doesn't pretend that you can make a change in one location, and have it atomically show up in all locations. On the other hand, Git does allow you to make changes to multiple files, and only have them show up together or not at all.
Git also has explicit mechanisms for resolving conflicts when multiple people tried to make changes to the same files.
Git doesn't get around the CAP theorem; it just has much better trade-offs than POSIX, because Git was designed explicitly for distributed use.
POSIX also doesn't allow you to have different trade-offs for read-only and read-write data.
Especially things like `stat(2)`, which complicate things for distributed filesystems like Lustre. Or the lack of a readdir()+stat().
> symlink(oldpath, newpath) operates very much like link(2) but creates a symbolic link at a new inode rather than a hard link to the same inode.
This is a really rough description. What really happens with a hardlink is a new directory entry is created that points to the same inode; with a symlink a new inode is created of type symlink that points to a directory entry by path/name.
> Symbolic links can point to directories, which hard links cannot, making them a perfect analogy to link(2) when locking entire directories. This will fail with the error code EEXIST if newpath already exists, making this a perfect analogy to link(2) that works for directories, too.
Except there's no way to count the number of symlinks that point to a given path like there is with the link count of the inode with multiple hardlinks.
> Be careful of symbolic links whose target inode has been removed ("dangling" symbolic links) — open(2) will fail with the error code ENOENT.
As symlinks point to a path, a dandling symlink is the result of the target path being removed (or not existing), not removal of an inode. You can put something else at that path, and the symlink will take you there instead. ENOENT is returned because the symlink doesn't point to anything.
The lstat(2) call can be used to examine symlinks directly, normally file operations access the file through the symlink making it mildly challenging to distinguish between a regular file and symlink.
> It should be mentioned that inodes are a finite resource (this particular machine has 1,245,184 inodes).
This is unrelated information. It is filesystem format dependent.
I’m reminded that NFS was for a long time a popular storage for ISP mail but notoriously prone to non-atomic renames (or anything else, for that matter), and that is why many maildir implementations instead prefer an hard link followed by an unlink, via a tmp/ subdirectory on the same filesystem, during delivery and move/copy operations.
A practical application of atomic mv is building simple file based queuing mechanisms.
For example I wrote this SMTP buffer server which moves things to different directories as a simple form of message queue.
https://github.com/bootrino/arniesmtpbufferserver
It's pretty simple - ~100 lines of Python without comments etc.
https://raw.githubusercontent.com/bootrino/arniesmtpbufferse...
Caveat I think this needs examination from the perspective of fsync - i.e. I suspect the code should be fsyncing at certain points but not sure.
I also wrote (in Rust) a simple file based message queue using atomic mv. It instantly maxed out the SSD performance at about 30,000 messages/second.
If you wrap your writes in a shell script controlled by a lock directory, you are guaranteed to never receive SQLITE_BUSY.
#!/bin/sh
until mkdir /var/tmp/mylock
do sleep 1;
done
trap 'rmdir /var/tmp/mylock' ABRT EXIT FPE HUP INT QUIT SEGV TERM
sqlite3 mydatabase.db 'INSERT INTO MYTABLE VALUES (1,2,3);'
Since the mkdir() is atomic, the SQLite busy handler is now irrelevant with this approach.On Linux, there is also an flock utility, but it tends to need subshells which are not as efficient.
EDIT: This does reduce the write performance to one of these executions per second. Simple inserts like this can hit a few dozen per second otherwise, as I have heard.
https://github.com/jaysoffian/dotlock/
I think I first learned that trick from procmail and then confirmed it with the NFS spec? Been a long time...
There is POSIX, and then whatever each OS that implements it decided to do, beyond what isn't explicitly required for UNIX trademark certification.
Hence why it is so much "fun" writing portable code across UNIX/POSIX platforms.
So writes are atomic but what about reads? Oh that's why pathconf(9) returned that small number! You sure? What if a signal sneaks in while reading?
MMAP_SHARED means that the same physical memory pages are available via different virtual addresses. All the writes are visible immediately, at least in the same way as in the regular memory (you may still need mfence-es or a cache flush depending on CPU).
MS_INVALIDATE is essentially a no-op on Linux.
Also, there's a bit of atomicity in O_APPEND writes. Specifically, the end of the file will be adjusted atomically, though the actual writes may not happen atomically (beware!). Thus processes that use O_APPEND to write to the same file will not have their writes stepping on each others', but if there's a power outage you can have a situation where the file's length was adjusted as if one O_APPEND write completed but you might get zeroes instead of the actual bytes meant to be written.
As for mmap() and msync()... I don't recommend it. First, if the buffer and page cache are shared in the filesystem, then msync() is essentially a no-op. Second, if the mapping is not anonymous and you're writing to a file and anything goes wrong you'll get SIGBUS (ugh). Third, if the buffer and page caches are not shared then you really do need msync(MS_SYNC), and that can be expensive. Reading files via mmap() is ok, but writing to them via mmap() is fraught.
"Leases" are a Linux thing, not POSIX or Unix.