Files, permissions and I/O
Inodes, hard and symbolic links, the permission bits that matter, and how buffered versus direct I/O changes durability and speed.
Files, inodes and links
On Unix a filename is an entry in a directory that points to an inode, which holds the metadata and the block map. The name and the file are separate things — which explains links, hard-link counts and why deleting an open file frees nothing yet.
ls -li notes.txt # -i prints the inode number
stat notes.txt # size, blocks, link count, access/modify/change times
ln notes.txt copy.txt # hard link: same inode, link count becomes 2
ln -s notes.txt link.txt # symbolic link: a small file containing a path
rm notes.txt # the inode survives while link count > 0
lsof +L1 # files with link count 0 still open somewhere| Hard link | Symbolic link | |
|---|---|---|
| Target | Same inode | Another path |
| Across filesystems | No | Yes |
| To a directory | No (normally) | Yes |
| Broken if target is deleted | No | Yes |
💡
Deleting a file unlinks a name; space is only reclaimed when the last hard link and every open file descriptor are gone. A full disk while
df looks fine usually means a deleted but still-open log file.Permissions that matter
| Bit | File means | Directory means |
|---|---|---|
r (4) | Read content | List names |
w (2) | Modify content | Create, delete or rename entries |
x (1) | Execute | Traverse (enter) the directory |
| setuid | Run as the file's owner | — |
| sticky | — | Only the owner may delete entries |
chmod 644 config.yml # rw-r--r--, typical for a config file
chmod 600 id_ed25519 # rw-------, required by ssh for private keys
chmod 755 scripts/run.sh # rwxr-xr-x
chown -R app:app /srv/app
umask # bits removed from new files, e.g. 022
# who am I, and what can I actually do?
id && sudo -l- A directory needs
xto be traversed at all —ralone lets you list names but not open them. - Deleting a file requires write permission on the directory, not on the file.
setuidon a script is ignored by most kernels; it only works on real binaries.- Prefer groups over world-writable directories:
chmod 777is a maintenance debt, not a fix.
Buffered, unbuffered and durable writes
A write returns as soon as the data reaches the kernel's page cache. That is fast and safe against process crashes, but not against power loss. Durability requires an explicit flush to the storage device.
import os
# buffered: fast, data is in the page cache after write() returns
with open("out.txt", "w", encoding="utf-8") as f:
f.write("hello")
f.flush() # push Python buffer -> kernel
os.fsync(f.fileno()) # push kernel buffer -> device
# an atomic replace: write a temp file, fsync it, then rename
os.replace("out.txt.tmp", "out.txt") # rename is atomic within a filesystem- Sequential I/O is far cheaper than random I/O on spinning disks; on SSDs the gap is smaller but write amplification still punishes small random writes.
- Direct I/O bypasses the page cache; it helps databases with their own caching and hurts almost everything else.
- Writes are usually buffered and coalesced by the kernel, so measuring
write()latency tells you little about device latency. - Use
iostat -x 1to see queue depth and utilisation rather than guessing.
FAQ
Why did my file disappear but the disk is still full?
A process still holds an open descriptor to the unlinked inode. Find it with
lsof +L1, restart that process, and the space returns.Is <code>sync</code> enough before a rename?
For a crash-consistent replace, flush the file's contents first (
fsync) and then rename. To be strictly safe, also fsync the containing directory so the rename itself is durable.Related
Memory and virtual memory Processes and threads
Last refreshed 2026-09-18.