A successful file-backed mmap() creates a virtual-memory mapping that does not depend on keeping the source file descriptor open. Linux explicitly permits the descriptor to be closed immediately after mmap() returns without invalidating the mapping. The mapping and the descriptor are therefore separate references with separate lifetimes.

That separation is easy to miss because both originate from the same open file. It becomes operationally important when code closes descriptors aggressively, replaces pathnames, truncates files, or passes mappings across fork(). A mapped address is not a delayed read() through the original descriptor; it participates in the virtual-memory system under its own mapping contract.

Closing the descriptor leaves the mapping in place

For a file-backed mapping, mmap() establishes pages in the process address space from the object referenced by the descriptor and the requested file offset. Once the call succeeds, closing that descriptor does not call munmap() implicitly.

The process can therefore use a shape such as:

int fd = open(path, O_RDONLY);
void *p = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);

/* p remains mapped until munmap(), replacement, or process teardown */

This is a Linux API property, not a C language property. C only supplies the source-level operations and types involved; the kernel defines the mapping and descriptor semantics.

The reverse is also separate. Calling munmap() removes pages from the specified address range but does not close an unrelated descriptor that still refers to the file. Resource accounting must treat file descriptors and virtual-memory areas as distinct resources.

Pathname replacement does not retarget an existing mapping

A pathname is used during open() to obtain a descriptor. The descriptor then identifies an opened file object, and mmap() establishes a mapping from that object. Later namespace operations do not make an existing mapping re-resolve the original pathname.

Suppose a process maps config.bin, then another process atomically renames a replacement over that pathname. New open() calls can reach the replacement, while the existing mapping continues to refer to the object that was mapped earlier.

This resembles the behavior of an already-open descriptor across rename, but the mapping does not need that descriptor to remain open. The mapping itself preserves the relevant kernel relationship.

The consequence is a version boundary: pathname publication can switch new consumers to another file without forcing existing mapped consumers onto the new object. Applications that require all consumers to move together need an explicit coordination protocol; rename and mapping semantics do not supply one.

File size remains a boundary for valid mapped access

A mapping has a virtual address length, but that does not make bytes beyond the backing file permanently valid. Linux documents SIGBUS for access to a page of a file mapping that lies beyond the end of the mapped file.

This matters when another actor can truncate the backing file. A process may hold a mapping whose virtual range still exists while an access within that range no longer has valid file backing. The mapping’s address-space lifetime has not ended, yet the backing object’s size has changed the validity of particular accesses.

That failure mode differs from SIGSEGV caused by accessing an unmapped address. The virtual mapping can still be present; the fault concerns the backing object for the referenced page.

For the final partial page of a file, page granularity adds another detail. Linux zero-fills the portion of that page beyond the file end when it is mapped, and modifications in that beyond-end portion are not written to the file. Whole pages beyond the object end are the relevant SIGBUS boundary.

MAP_PRIVATE does not freeze a file snapshot

MAP_PRIVATE creates a copy-on-write mapping: writes through the mapping are private to the process and are not carried through to the underlying file. That property does not define a stable snapshot of every byte against concurrent file mutation.

POSIX and the Linux mmap() documentation leave visibility of modifications made to the underlying file after a MAP_PRIVATE mapping is established unspecified. Code that requires immutable snapshot semantics therefore cannot infer them merely from the word “private.”

Copy-on-write primarily describes the disposition of writes made through the private mapping. It is not a general versioning guarantee for external writes to the backing file.

A stable snapshot needs a stronger premise, such as exclusive control of the backing object, an immutable object, a filesystem snapshot with a documented contract, or another application-level versioning mechanism.

MAP_SHARED connects mapped writes to the backing object

MAP_SHARED has a different write contract. Updates made through the mapping are carried through to the underlying mapped object and can become visible to other processes mapping the same region, subject to the interface and synchronization rules.

Visibility and durable storage remain distinct. msync() exists to control synchronization of mapped changes with the backing file under its documented flags and platform semantics. A store becoming visible through a shared mapping is not, by itself, a universal crash-durability guarantee.

This boundary also separates memory ordering from storage persistence. CPU synchronization primitives can order memory accesses among threads, while persistence of file-backed data is governed by the mapping, filesystem, and storage interfaces. One contract cannot be substituted for the other without an explicit platform guarantee.

fork copies the mapping relationship into the child

Linux preserves memory mappings across fork() with the same mapping attributes. The child therefore receives corresponding mapped regions even if the descriptor originally used for mmap() had already been closed before the fork.

For a MAP_PRIVATE mapping, parent and child retain private copy-on-write semantics for their modifications. For a MAP_SHARED mapping, both mappings retain the shared disposition associated with the mapped object.

This creates another lifetime distinction. Descriptor inheritance rules and mapping inheritance rules are related to the same process creation event, but they operate on different resource sets. Marking a descriptor close-on-exec, closing it before fork(), or otherwise changing descriptor state does not retroactively remove an established mapping.

An execve() transition is different: successful program replacement discards the old process memory mappings as part of replacing the address space. The mapping lifetime therefore follows address-space lifetime rules, not descriptor lifetime rules.

Mapping ownership needs its own cleanup boundary

Code that wraps file mappings as resources needs to represent the mapping length and mapped address independently from the source descriptor. A destructor or cleanup path that closes only the descriptor leaves the virtual-memory area intact; a path that only unmaps leaves any still-open descriptor intact.

This distinction also affects diagnostics. Descriptor leaks appear in descriptor-oriented views and limits, while excessive mappings can consume virtual address space and virtual-memory-area bookkeeping even when no source descriptor remains open.

The kernel interface deliberately permits these lifetimes to diverge. A descriptor is the capability used to request the mapping; it is not a permanent handle that every later mapped access traverses. Once established, the mapping is an address-space object whose validity depends on mapping protections, backing-object state, and address-space lifetime.

That boundary explains several otherwise surprising observations at once: closing the descriptor does not remove mapped pages, pathname replacement does not retarget them, truncation can invalidate accesses without unmapping the range, and process creation can preserve mappings independently of the descriptor that originally created them.