When updating configuration files, you want readers to see only either the complete old version or the complete new version; however, locks, rename, and fsync each guarantee different things. This article breaks down this update along the lines of ownership, visibility, and persistence, and then examines buffer lifetimes in data passing.
Phenomenon to Explain
Reading Location
Why does the lock disappear when closing another fd?
Sections 1–2: Inodes and Lock Owners
Is a successful rename already crash-resistant?
Section 3: Synchronizing Files and Directories Separately
Can the source content be modified after sendfile returns?
Section 4: Copying and Reference Boundaries
Is the slowdown due to locks, logging, or the device?
Section 5: Observing by Completion Points
1. Inode Flags Are Not a Simple Enumerated State Machine
i_state is a set of composable state bits; do not write I_NEW, I_DIRTY_SYNC, etc., as sequential numbers starting from 0. I_NEW relates to initialization publication; dirty bits describe different writeback requirements; I_FREEING, etc., participate in reclaim coordination. Specific bit values belong to the target kernel implementation and are not suitable as fixed constants for cross-version tutorials.
I_DIRTY_DATASYNC indicates that inode metadata modifications must also be handled by fdatasync, such as metadata affecting data reads, rather than simply "data pages dirty." Data page dirty status is tracked at the mapping/folio level. Similar field names do not imply identical protection scopes.
A reference count reaching zero does not necessarily mean immediate inode destruction: unused but cacheable inodes can remain in the cache and be evicted during reclaim; unlinked inodes and other cases have different handling. The finder, writebacker, and reclaimer during initialization must coordinate, and the role of unlock_new_inode() is not merely "clearing a flag." Reading in conjunction with fs/inode.c and the VFS Lifecycle Description is more reliable than memorizing a fixed function chain.
2. Owners of Three Types of Locks
Lock
Scope and Owner
dup / fork
Release Boundary
POSIX F_SETLK/F_SETLKW
Byte range; process-associated
Child processes do not inherit parent's locks
Closing any fd pointing to the same file in that process releases such locks on that file
OFD F_OFD_SETLK/F_OFD_SETLKW
Byte range; open-instance-associated
Descriptors sharing the open instance jointly hold the lock
When the last relevant open reference is closed, or explicitly unlocked
Linux local flock
Usually whole file; open-instance-associated
Locks are shared with shared open instances
When the last relevant reference is closed, or explicitly unlocked
OFD owners obtained via independent opens differ, causing conflicts even within the same process; dup of the same open instance does not create an independent competitor. The struct flock.l_pid input for OFD should be 0; when querying an OFD lock, its PID field has no meaning for ordinary process locks. See fcntl range locking.
These are typically cooperative locks: other callers may still read or write content if they do not follow the convention of acquiring locks. Historical Linux mandatory locking should not be treated as a modern general guarantee. Local flock and POSIX locks are usually independent mechanisms, but NFS/SMB implementations may simulate them via range locks and interact; local behavior cannot be directly applied to network file systems. See flock(2).
l_len=0 means the lock extends to the end of the file and includes any subsequently growing range, not an empty range. Blocking lock acquisition also requires handling signals, exits, and deadlocks; F_GETLK returns only the conflict observed at that moment, and the state may change subsequently. You cannot replace actual lock acquisition with "check for no lock first, then write without a lock."
Verifying the Last Reference Boundary for OFD
The following Linux C example uses a temporary file, two independent opens, and one dup; it does not rely on manual binary layout of struct flock:
#define_GNU_SOURCE#include<assert.h>#include<errno.h>#include<fcntl.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>staticintset_lock(intfd){struct flock lock ={.l_type= F_WRLCK,.l_whence= SEEK_SET,.l_start=0,.l_len=1,.l_pid=0};returnfcntl(fd, F_OFD_SETLK,&lock);}staticvoidexpect_conflict(intfd){int rc =set_lock(fd);assert(rc ==-1&&(errno == EAGAIN || errno == EACCES));}intmain(void){char path[]="/tmp/ofd-demo-XXXXXX";int a =mkstemp(path);assert(a >=0);int b =open(path, O_RDWR | O_CLOEXEC);assert(b >=0);assert(unlink(path)==0);int alias =dup(a);assert(alias >=0);assert(set_lock(a)==0);expect_conflict(b);assert(close(a)==0);expect_conflict(b);/* alias still holds the open instance */assert(close(alias)==0);assert(set_lock(b)==0);/* Can only be acquired after the last reference is closed */assert(close(b)==0);puts("OFD independent owners and last reference: Passed");}
Compile with cc -std=c11 -Wall -Wextra -Werror ofd-demo.c -o ofd-demo. This example uses assertions for demonstration verification; do not add -DNDEBUG at runtime; production code should explicitly handle each failure branch.
3. Persistent Replacement: File Content and Directory Entries Submitted Separately
For configuration files, the common goal is for readers to see only the complete old version or the complete new version. Directly truncating the original file and then writing will cause concurrent readers to see an empty file or partial content. You can first prepare a temporary file in the same directory, write it completely and synchronize, then rename to replace the target, and finally synchronize the parent directory.
Break the update into four observation points:
Step
Boundary Established Here
What Is Not Yet Complete
Writing the temporary file
Data is written to the interface acceptor
No synchronization completion guarantee
fsync temporary file
Content and required metadata synchronized
Target name has not yet been replaced
rename replaces target
Runtime name binding changes
Directory update still needs synchronization
fsync parent directory
Directory update synchronized per contract
Does not automatically form a cross-file transaction
Crashing before the last step does not allow you to promise the name state after restart based solely on a successful rename. See fsync(2).
Rename success alone does not promise the recovered name after a crash. fsync
Preparing the visual
An atomic name replacement: is the content durable too?
rename changes the live name binding. File and directory fsync establish separate durability boundaries for content and naming. Existing fds can still reference the old inode.
{"id":"durable-file-replacement","title":"An atomic name replacement: is the content durable too?","summary":"rename changes the live name binding. File and directory fsync establish separate durability boundaries for content and naming. Existing fds can still reference the old inode.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"write\" data-i18n=\"write\"></button><button id=\"fileSync\" data-i18n=\"fileSync\"></button></div><div class=\"actions\"><button id=\"rename\" data-i18n=\"rename\"></button><button id=\"dirSync\" data-i18n=\"dirSync\"></button></div><div class=\"track-row\"><span data-i18n=\"live\"></span><div id=\"live\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"stable\"></span><div id=\"stable\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"held\"></span><div id=\"held\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"b\"></span><div id=\"b\" class=\"track\"></div></div><div class=\"actions\"><button id=\"crash\" data-i18n=\"crash\"></button></div><div class=\"card\"><header data-i18n=\"recovered\"></header><div id=\"recovered\" class=\"readout\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.readout{min-height:4.8em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let binding='A',saved='A',b=0,bStable=0,dead=false;const value=v=>v?'N'+v:t('empty');function draw(){cells('live',[{text:'target → '+binding+' → '+(binding==='A'?'O':value(b)),cls:'on'}]);cells('stable',[{text:'target → '+saved+' → '+(saved==='A'?'O':value(bStable)),cls:'gold'}]);cells('held',[{text:dead?'—':'fd → A → O',cls:dead?'empty':'on'}]);cells('b',[{text:value(b)+' / '+value(bStable)}]);set('recovered',dead?saved+' → '+(saved==='A'?'O':value(bStable)):t('running'));for(const id of ['write','fileSync','rename','dirSync','crash'])$('#'+id).disabled=dead||(id==='rename'&&binding==='B');set('explanation',t('note'));}$('#write').onclick=()=>{b++;draw();};$('#fileSync').onclick=()=>{bStable=b;draw();};$('#rename').onclick=()=>{binding='B';draw();};$('#dirSync').onclick=()=>{saved=binding;draw();};$('#crash').onclick=()=>{dead=true;draw();};$('#reset').onclick=()=>{binding=saved='A';b=bStable=0;dead=false;draw();};draw();","audio":false,"strings":{"b":"B’s current / stable content","crash":"Cut power here","dirSync":"fsync parent directory","empty":"Empty file","fileSync":"fsync B","heading":"An atomic name replacement: is the content durable too?","held":"Reference held by the old fd","intro":"A new inode B is ready. Arrange writes, file sync, rename, and directory sync freely, then cut power and compare the live and recovered target.","live":"Live: target → inode → content","note":"Syncing only the directory can leave the new binding pointing to empty content. Syncing B without the directory can still recover the old binding.","recovered":"Read target after restart","rename":"rename B → target","reset":"Reset","running":"Power loss not simulated yet","scope":"Same-directory single-file replacement without I/O errors. B starts empty. This failure model restores only explicitly synchronized content and target binding, discarding unsynced changes. It is an illustrative possible failure, not every filesystem’s outcome. Restart closes old fds; temporary-name recovery, permissions, and multi-file transactions are omitted.","scopeLabel":"Model scope and assumptions","stable":"Recovery basis: binding → stable content","write":"Write B’s next version"}}
importosimporttempfilefrompathlibimportPathwithtempfile.TemporaryDirectory(prefix="replace-demo-")asdirectory:root=Path(directory)target=root/"config"target.write_bytes(b"old\n")dirfd=os.open(root,os.O_RDONLY|os.O_DIRECTORY)oldfd=os.open(target,os.O_RDONLY)temporary=Nonetry:withtempfile.NamedTemporaryFile(dir=root,delete=False)asfile:temporary=Path(file.name)file.write(b"new\n")file.flush()os.fsync(file.fileno())os.replace(temporary,target)temporary=Noneos.fsync(dirfd)asserttarget.read_bytes()==b"new\n"assertos.pread(oldfd,4,0)==b"old\n"print("New open sees new version; old open reference still reads old file: Passed")finally:iftemporaryisnotNone:temporary.unlink(missing_ok=True)os.close(oldfd)os.close(dirfd)
It verifies runtime visibility and old references, but does not simulate power loss. Production updates also need to set new file permissions, owner, ACL/xattr, and handle uncertain states after failure; cross-directory moves involve related directories, and cross-filesystem rename usually cannot be completed in this manner. Multiple files do not automatically form the same transaction just because each is fsynced.
Replacing the Inode Does Not Automatically Move Old Locks
There is another pitfall in combining locks with replacement: locks on the old inode do not automatically transfer to the new inode after replacement. If each process "opens the current file and then locks," they may lock different versions. Coordinating updates usually requires stable identity, lock objects agreed upon by participants, or higher-level protocols.
4. sendfile / splice: Which Data Transfer Segment Is Reduced?
Interface
Connects Which Data Path Segment
sendfile
Avoids the application first reading to a user buffer, then writing to the downstream
splice
At least one end is a pipe, connecting supported objects via pipe buffering
vmsplice
Connects a user memory range to a pipe
Whether the kernel ultimately references the original page, copies it, or uses an intermediate buffer still depends on the file system, transmission, encryption, device capabilities, and the specific path.
"Zero-copy" usually refers to reducing specific CPU data copying, not the absence of DMA, absence of page reference costs, nor does it guarantee that sendfile always copies once or that adding SG always copies zero times. A single system call's user/kernel transition does not equal switching to another process; blocking, scheduling, and interrupts add different overheads. See sendfile(2).
Interface Contract
State the Application Needs to Maintain
Returns positive but less than requested length
Advance the corresponding offset, continue with the remaining range
Non-blocking returns EAGAIN
Wait for writable/readable, then retry, retaining completed progress
Downstream still references source pages
Do not modify content still referenced before the consumer completes
Pipe has capacity limits
Manage producer/consumer backpressure, cannot infinitely stuff data
Successful Return and Source Memory Reusability Are Different Events
vmsplice(SPLICE_F_GIFT) is a strict commitment to memory page usage, requiring adherence to alignment limits, and guarantees that the gifted memory will not be modified subsequently; a successful return does not automatically remove the user mapping or notify the application that "immediate reuse" is granted. You cannot immediately rewrite a regular malloc buffer after the call and assume network transmission is complete. See vmsplice(2).
5. Observations Should Follow Objects and Completion Points
/proc/locks, lslocks can observe lock types and ranges; displayed content is affected by factors such as PID namespaces, and OFD locks do not correspond to ordinary holding PIDs. /proc/sys/fs/inode-state is the system cache state, not the exact count of currently active files. Synchronization latency needs to distinguish between dirty page writeout, log commit, device completion, and network acknowledgment; you cannot directly infer damage to a specific layer from the total time of a single syscall.
Source code entry points can be found separately along fs/inode.c, file lock implementations, fs/sync.c, and fs/splice.c; file lock source code layout may vary by version, so start by locating via interface names. Network file completion and retry semantics follow NFS.