Files should come in several kinds:
- Unit files. They're immutable, but replaceable. This is the normal case. You create them with "creat", write them, and when closed, they replace any previous version with the same name. Any program which opens one for reading sees a complete file. On a crash before close, the new file is dropped and the old version remains. If a program aborts, that also happens. An explicit close commits the file update.
- Log files. These are append only. On a crash, you are guaranteed that the file will be intact to some point ending at a write boundary. The last transactions may be lost, but the file must not tail off into junk. For journals, log files, etc.
- Temp files. These disappear on a crash.
- Managed files. These are for databases. They can do everything files can do.
Along with this, you need async I/O. This is primarily for log files and managed files. When you do a write, you get two callbacks, one when the buffer has been copied, and one when the data has been committed to permanent storage and will survive a crash. It is the job of the OS, drivers, and devices to not send the data-committed completion until the data is safe. Only a few programs need that, but they're the important ones - database managers.
This turns the fsync problem around. Instead of doing an fsync to force write-out, databases wait for a completion before considering the data committed. It's not a flush-everything; it's a commit-complete for a single write. So if you have multiple I/O operations pending on a single file, a normal event in database land, there's no big stall for a cache flush.
There's also no incentive for drive manufacturers to send commit-completes early. The only people who use them are the ones who will notice if someone cheats. Most bulk I/O is on unit files, where something is being copied.
(There are mainframe systems that had this sort of thing half a century ago.)