Loading learning content...
Renaming a file seems straightforward: change the name, done. Moving a file feels similar: relocate it to a different directory. But beneath these simple operations lies sophisticated kernel machinery designed to guarantee atomicity—ensuring that at no point during the operation does the file exist with both names simultaneously, no name at all, or in a corrupted state.
The rename() system call is one of the most critical primitives for building reliable software. Configuration file updates, log rotation, database transactions, package installations—all depend on rename's atomic guarantees. Understanding how rename works, and equally importantly, understanding its limitations, is essential knowledge for systems programmers.
By the end of this page, you will understand the complete mechanics of file renaming and moving—from the rename() system call through its atomicity guarantees, cross-device move limitations, directory rename semantics, and the safe file update patterns that leverage rename for crash-consistent operations.
The rename() system call is the fundamental operation for changing file names and locations. Despite its apparent simplicity, it provides powerful guarantees that make it invaluable for reliable software.
Basic Interface:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
#include <stdio.h>#include <errno.h>#include <string.h> /** * rename() - Rename a file or directory * * Synopsis: * int rename(const char *oldpath, const char *newpath); * * Semantics: * - Atomically replaces newpath with oldpath * - oldpath ceases to exist (it becomes newpath) * - If newpath exists, it's atomically replaced * - Works for files and directories * * Returns: 0 on success, -1 on error (check errno) */int simple_rename(const char *oldpath, const char *newpath) { if (rename(oldpath, newpath) == -1) { switch (errno) { case EACCES: fprintf(stderr, "Permission denied"); break; case EBUSY: fprintf(stderr, "File is busy (mount point or cwd)"); break; case EEXIST: case ENOTEMPTY: fprintf(stderr, "Target exists and is non-empty directory"); break; case EINVAL: fprintf(stderr, "Invalid arguments (e.g., old is ancestor of new)"); break; case EISDIR: fprintf(stderr, "Old is not a directory but new is"); break; case ENOENT: fprintf(stderr, "Old doesn't exist or new's parent doesn't exist"); break; case ENOTDIR: fprintf(stderr, "Component of path is not a directory"); break; case EXDEV: fprintf(stderr, "Cross-device move not allowed"); break; case EROFS: fprintf(stderr, "Read-only file system"); break; default: fprintf(stderr, "rename failed: %s", strerror(errno)); } return -1; } return 0;} /** * Examples of what rename() can do: */void rename_examples(void) { // Simple rename in same directory rename("document.txt", "report.txt"); // Move to different directory (same file system) rename("inbox/message.txt", "archive/message.txt"); // Rename directory rename("project_alpha", "project_beta"); // Atomic replacement: replace existing file rename("config.new", "config.txt"); // Replaces old config.txt // Combined move and rename rename("downloads/temp_123.zip", "software/myapp-1.0.zip");}rename() fails with EXDEV when trying to move a file across file system boundaries. This is because rename() works by manipulating directory entries, not by copying data. If the source and destination are on different file systems from the same drive, they have separate directory structures and rename() cannot operate across them. Cross-device moves require copy-then-delete.
The renameat() and renameat2() Variants:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
#include <fcntl.h>#include <stdio.h>#include <linux/fs.h> // For RENAME_* flags /** * renameat() - Rename relative to directory fds * * Like other *at() functions, this allows working with paths * relative to directory file descriptors, avoiding race conditions. */int rename_relative(int olddir_fd, const char *oldname, int newdir_fd, const char *newname) { return renameat(olddir_fd, oldname, newdir_fd, newname);} /** * renameat2() - Extended rename with flags (Linux-specific) * * Flags: * RENAME_NOREPLACE - Don't overwrite newpath if it exists * RENAME_EXCHANGE - Atomically exchange old and new paths * RENAME_WHITEOUT - Create a whiteout at source (for overlayfs) */ /** * RENAME_NOREPLACE: Fail if destination exists * * This is the safe way to create a new file atomically without * risking overwriting an existing file. */int rename_no_overwrite(const char *oldpath, const char *newpath) { if (renameat2(AT_FDCWD, oldpath, AT_FDCWD, newpath, RENAME_NOREPLACE) == -1) { if (errno == EEXIST) { fprintf(stderr, "Destination already exists"); } return -1; } return 0;} /** * RENAME_EXCHANGE: Atomically swap two paths * * This is invaluable for atomic updates where you need to: * 1. Create new version of file * 2. Swap new version with old version atomically * 3. Delete old version * * Unlike regular rename, both files continue to exist (with swapped names). */int atomic_swap(const char *path_a, const char *path_b) { if (renameat2(AT_FDCWD, path_a, AT_FDCWD, path_b, RENAME_EXCHANGE) == -1) { perror("rename exchange"); return -1; } return 0;} /** * Example: Atomic configuration update with backup */int update_config_with_backup(const char *config_path, const char *new_config_path) { char backup_path[256]; snprintf(backup_path, sizeof(backup_path), "%s.bak", config_path); // Use RENAME_EXCHANGE to swap new config with old // Old config becomes new config // New file (prepared update) becomes the old config (backup) if (renameat2(AT_FDCWD, new_config_path, AT_FDCWD, config_path, RENAME_EXCHANGE) == -1) { return -1; } // Now rename the "new" file (which has old content) to backup rename(new_config_path, backup_path); return 0;}The most critical property of rename() is its atomicity guarantee. This doesn't mean rename is fast (though it usually is)—it means rename is indivisible from the perspective of all other operations.
What Atomicity Means for rename():
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
/** * Why rename atomicity matters: The Safe File Update Pattern * * Problem: You need to update a configuration file. If you: * 1. Open the config file * 2. Truncate it * 3. Write new content * * Then a crash during step 2 or 3 leaves you with partial/empty file! * * Solution: Write to temp file, then rename (atomic replace) */ #include <stdio.h>#include <unistd.h>#include <fcntl.h>#include <string.h> /** * WRONG: Direct update (crash-unsafe) */int update_file_unsafe(const char *path, const char *content) { // If we crash after truncate but before write completes, // data is lost! int fd = open(path, O_WRONLY | O_TRUNC); if (fd == -1) return -1; write(fd, content, strlen(content)); // May crash here! close(fd); return 0;} /** * CORRECT: Atomic update pattern */int update_file_safe(const char *path, const char *content) { char temp_path[256]; snprintf(temp_path, sizeof(temp_path), "%s.tmp.%d", path, getpid()); // Step 1: Write new content to temp file int fd = open(temp_path, O_WRONLY | O_CREAT | O_EXCL, 0644); if (fd == -1) { return -1; } ssize_t len = strlen(content); if (write(fd, content, len) != len) { close(fd); unlink(temp_path); return -1; } // Step 2: Ensure data is on disk if (fsync(fd) == -1) { close(fd); unlink(temp_path); return -1; } close(fd); // Step 3: Atomic replace via rename // Either the old content exists, or the new content exists. // Never partial, never missing. if (rename(temp_path, path) == -1) { unlink(temp_path); return -1; } // Step 4: Sync parent directory for durability char *last_slash = strrchr(path, '/'); if (last_slash) { char dir[256]; int dir_len = last_slash - path; strncpy(dir, path, dir_len); dir[dir_len] = '\0'; int dir_fd = open(dir, O_RDONLY | O_DIRECTORY); if (dir_fd >= 0) { fsync(dir_fd); close(dir_fd); } } return 0;} /** * This pattern is used everywhere: * - Text editors saving files * - Package managers installing files * - Databases committing transactions * - Git updating refs */The write-to-temp → fsync → rename → fsync-dir pattern is the gold standard for crash-consistent file updates. It's used by SQLite (for -journal and -wal files), systemd, dpkg/apt, and countless other tools that cannot afford to corrupt data on unexpected shutdown.
Understanding how the kernel implements rename reveals why it has its particular semantics and limitations.
What happens during rename():
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
/** * Simplified kernel rename implementation * Based on Linux VFS layer */int sys_rename(const char *oldpath, const char *newpath) { struct path old_path, new_path; struct dentry *old_dentry, *new_dentry; struct inode *old_dir, *new_dir; /* Phase 1: Resolve paths and lock directories */ // Find parent directories and target names error = user_path_parent(oldpath, &old_path, &old_dentry); error = user_path_parent(newpath, &new_path, &new_dentry); old_dir = old_path.dentry->d_inode; new_dir = new_path.dentry->d_inode; /* Phase 2: Validate the operation */ // Check: same filesystem? if (old_path.mnt != new_path.mnt) { return -EXDEV; // Cross-device rename not allowed } // Check: old exists? if (!old_dentry->d_inode) { return -ENOENT; } // Check: can't make directory its own descendant if (is_ancestor(old_dentry, new_path.dentry)) { return -EINVAL; } // Check: write permission on both directories if (!may_create(new_dir, new_dentry) || !may_delete(old_dir, old_dentry)) { return -EACCES; } /* Phase 3: Handle directory-specific constraints */ if (S_ISDIR(old_dentry->d_inode->i_mode)) { // Renaming a directory // New path must not exist, or must be empty directory if (new_dentry->d_inode) { if (!S_ISDIR(new_dentry->d_inode->i_mode)) { return -ENOTDIR; } if (!is_empty_dir(new_dentry->d_inode)) { return -ENOTEMPTY; } } } else { // Renaming a file // If new exists, it must also be a non-directory if (new_dentry->d_inode) { if (S_ISDIR(new_dentry->d_inode->i_mode)) { return -EISDIR; } } } /* Phase 4: Acquire locks and perform the rename */ // Lock both directories (in a consistent order to prevent deadlock) lock_rename(old_dir, new_dir); // Start journal transaction (for crash consistency) handle = journal_start(); // If new exists, unlink it first if (new_dentry->d_inode) { vfs_unlink(new_dir, new_dentry); } // Update directory entries: // 1. Add entry for new name pointing to inode // 2. Remove entry for old name // 3. If directories involved, update .. references error = vfs_rename(old_dir, old_dentry, new_dir, new_dentry); // Commit transaction journal_stop(handle); unlock_rename(old_dir, new_dir); return error;} /** * File system specific rename (ext4) */int ext4_rename(struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry) { handle_t *handle; handle = ext4_journal_start(old_dir, credits); // Within single journal transaction (atomic): // 1. Add directory entry in new_dir ext4_add_entry(handle, new_dentry, old_dentry->d_inode); // 2. Delete directory entry from old_dir ext4_delete_entry(handle, old_dir, old_dentry); // 3. If renaming directory, update .. entry if (S_ISDIR(old_dentry->d_inode->i_mode)) { ext4_update_dotdot_entry(handle, old_dentry->d_inode, new_dir); // Adjust link counts ext4_dec_count(handle, old_dir); ext4_inc_count(handle, new_dir); } // 4. Update timestamps old_dir->i_ctime = old_dir->i_mtime = current_time(); new_dir->i_ctime = new_dir->i_mtime = current_time(); ext4_journal_stop(handle); return 0;}Key Implementation Details:
| Aspect | Details |
|---|---|
| Directory locking | Both directories locked in consistent order to prevent deadlock |
| Journal transaction | All changes wrapped in single transaction for crash consistency |
| Link count updates | For directories, parent link counts adjusted for .. changes |
| Timestamp updates | ctime and mtime updated on both directories |
| Inode preservation | The inode number stays the same—only directory entries change |
| Data preservation | No data is copied—only metadata (directory entries) is modified |
rename() only modifies directory entries, not file data. Renaming a 1TB file takes the same time as renaming a 1KB file (within the same filesystem). This is why rename() is O(1) with respect to file size—it's purely a metadata operation.
When source and destination are on different file systems, rename() fails with EXDEV. This isn't a bug—it's a fundamental limitation. Moving data across file systems requires copying, which cannot be atomic.
Implementing Cross-Device Move:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>#include <fcntl.h>#include <unistd.h>#include <sys/stat.h> /** * Copy file data from source to destination */int copy_file_data(int src_fd, int dst_fd) { char buffer[65536]; // 64KB buffer ssize_t nread, nwritten; while ((nread = read(src_fd, buffer, sizeof(buffer))) > 0) { char *ptr = buffer; while (nread > 0) { nwritten = write(dst_fd, ptr, nread); if (nwritten <= 0) { return -1; } nread -= nwritten; ptr += nwritten; } } return (nread == 0) ? 0 : -1;} /** * Move file, handling cross-device case * * This mimics the behavior of 'mv' command: * 1. Try rename() first (fast, atomic) * 2. If EXDEV, fall back to copy-and-delete */int move_file(const char *src, const char *dst) { // First, try rename() - works if same filesystem if (rename(src, dst) == 0) { return 0; // Success! Fast path. } if (errno != EXDEV) { // Some other error - give up perror("rename"); return -1; } // Cross-device move: must copy then delete printf("Cross-device move detected, copying..."); struct stat st; if (stat(src, &st) == -1) { perror("stat"); return -1; } // Open source for reading int src_fd = open(src, O_RDONLY); if (src_fd == -1) { perror("open source"); return -1; } // Create destination with same permissions int dst_fd = open(dst, O_WRONLY | O_CREAT | O_EXCL, st.st_mode); if (dst_fd == -1) { perror("open destination"); close(src_fd); return -1; } // Copy the data if (copy_file_data(src_fd, dst_fd) == -1) { perror("copy"); close(src_fd); close(dst_fd); unlink(dst); // Clean up partial copy return -1; } // Sync destination to disk if (fsync(dst_fd) == -1) { perror("fsync"); // Continue anyway - data may be cached } close(src_fd); close(dst_fd); // Preserve timestamps struct timespec times[2] = { st.st_atim, st.st_mtim }; utimensat(AT_FDCWD, dst, times, 0); // Now delete the source if (unlink(src) == -1) { perror("unlink source"); // File is copied but source couldn't be deleted // This is recoverable - user can delete manually return -1; } return 0;} /** * Move directory recursively (needed for cross-device) * * This is much more complex because we need to: * 1. Create destination directory * 2. Recursively copy contents * 3. Recursively delete source * * NOTE: This is inherently non-atomic! */Unlike same-device rename(), cross-device moves require copying data. This means a crash during the operation can leave you with a file at both locations (complete source, partial destination) or with data lost if the delete happens before copy is ensured on disk. Always use fsync() during cross-device moves.
Using copy_file_range() for Efficiency (Linux):
Linux 4.5+ provides copy_file_range() which can copy data between files without going through user space, potentially using copy-on-write or server-side copy for network file systems:
123456789101112131415161718192021222324252627282930313233343536
#define _GNU_SOURCE#include <unistd.h>#include <fcntl.h> /** * copy_file_range() - Efficient in-kernel file copy * * Advantages: * - No user-space buffer needed * - Can use copy-on-write for same-filesystem copies * - Can use server-side copy for NFS * - Generally faster than read/write loop */int copy_file_efficient(int src_fd, int dst_fd, off_t length) { off_t src_offset = 0; ssize_t ret; while (length > 0) { ret = copy_file_range(src_fd, &src_offset, dst_fd, NULL, length, 0); if (ret == -1) { if (errno == EXDEV || errno == ENOSYS) { // Fallback to traditional copy if not supported return copy_file_data(src_fd, dst_fd); } return -1; } if (ret == 0) { break; // EOF } length -= ret; } return 0;}Renaming directories is more complex than renaming files because directories have internal structure (the .. entries of all children point to the parent).
Special Constraints for Directory Rename:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
#include <stdio.h>#include <errno.h>#include <string.h> /** * Demonstrating directory rename constraints */void demonstrate_directory_rename(void) { // Create test structure mkdir("/tmp/test_rename", 0755); mkdir("/tmp/test_rename/a", 0755); mkdir("/tmp/test_rename/a/b", 0755); mkdir("/tmp/test_rename/c", 0755); mkdir("/tmp/test_rename/c/d", 0755); // Non-empty // Valid: rename directory if (rename("/tmp/test_rename/a", "/tmp/test_rename/alpha") == 0) { printf("Renamed 'a' to 'alpha'"); } // Valid: move directory to different parent if (rename("/tmp/test_rename/alpha/b", "/tmp/test_rename/beta") == 0) { printf("Moved 'b' out of 'alpha'"); } // Invalid: rename to its own descendant // This would create a cycle where alpha contains alpha! if (rename("/tmp/test_rename/alpha", "/tmp/test_rename/alpha/child") == -1) { printf("Cannot rename to descendant: %s", strerror(errno)); // EINVAL: Invalid argument } // Invalid: rename to non-empty directory // Can't replace 'c' because it contains 'd' if (rename("/tmp/test_rename/beta", "/tmp/test_rename/c") == -1) { printf("Cannot overwrite non-empty directory: %s", strerror(errno)); // ENOTEMPTY: Directory not empty } // Valid: rename to empty directory (replaces it) mkdir("/tmp/test_rename/empty", 0755); if (rename("/tmp/test_rename/beta", "/tmp/test_rename/empty") == 0) { printf("Replaced empty directory"); }} /** * Why can't we rename to a descendant? * * Consider renaming /a to /a/b/c: * * Before: * /a * /a/b * /a/b/c (does not exist, would be created) * * After (if allowed): * Where did /a go? It contains /a/b which contains /a/b/c which IS /a! * This creates an unreachable infinite loop. * * The kernel detects this by checking if old_dentry is an ancestor * of new_dentry before performing the rename. */When you rename a directory, all its contents (files, subdirectories, and their descendants) automatically appear at the new location. This is because rename only changes the directory entry in the parent—the internal structure of the directory remains unchanged. The contents never actually "move"—only the name reference at the top level changes.
Windows provides several APIs for renaming and moving files, with different characteristics regarding atomicity and cross-volume behavior.
Windows Rename APIs:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
#include <windows.h>#include <stdio.h> /** * MoveFileW - Basic rename/move * * Semantics: * - Same volume: atomic rename (like POSIX rename) * - Different volume: copy + delete (not atomic) * - Fails if destination exists */BOOL rename_basic(const wchar_t *old_path, const wchar_t *new_path) { if (!MoveFileW(old_path, new_path)) { DWORD error = GetLastError(); switch (error) { case ERROR_FILE_NOT_FOUND: wprintf(L"Source not found"); break; case ERROR_FILE_EXISTS: case ERROR_ALREADY_EXISTS: wprintf(L"Destination already exists"); break; case ERROR_ACCESS_DENIED: wprintf(L"Access denied"); break; case ERROR_SHARING_VIOLATION: wprintf(L"File in use by another process"); break; default: wprintf(L"MoveFile failed: %lu", error); } return FALSE; } return TRUE;} /** * MoveFileExW - Extended rename with options * * Flags: * MOVEFILE_REPLACE_EXISTING - Overwrite destination * MOVEFILE_COPY_ALLOWED - Allow copy for cross-volume * MOVEFILE_DELAY_UNTIL_REBOOT - Schedule for next reboot * MOVEFILE_WRITE_THROUGH - Flush after move */BOOL rename_with_overwrite(const wchar_t *old_path, const wchar_t *new_path) { DWORD flags = MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH; // MOVEFILE_REPLACE_EXISTING makes this similar to POSIX rename // MOVEFILE_WRITE_THROUGH ensures durability if (!MoveFileExW(old_path, new_path, flags)) { wprintf(L"MoveFileEx failed: %lu", GetLastError()); return FALSE; } return TRUE;} /** * SetFileInformationByHandle - Rename by handle * * Advantages: * - More atomic than path-based operations * - Can work with already-open files * - FileRenameInfoEx provides POSIX-like semantics */BOOL rename_by_handle(HANDLE hFile, const wchar_t *new_name) { // Calculate buffer size size_t name_len = wcslen(new_name) * sizeof(wchar_t); size_t buf_size = sizeof(FILE_RENAME_INFO) + name_len; FILE_RENAME_INFO *rename_info = (FILE_RENAME_INFO *)malloc(buf_size); if (!rename_info) return FALSE; rename_info->ReplaceIfExists = TRUE; // Like POSIX rename rename_info->RootDirectory = NULL; // Use full path rename_info->FileNameLength = name_len; wcscpy(rename_info->FileName, new_name); BOOL result = SetFileInformationByHandle( hFile, FileRenameInfo, rename_info, buf_size ); free(rename_info); return result;} /** * Atomic file replacement pattern for Windows * * ReplaceFileW is closest to POSIX atomic update pattern: * - Atomically replaces file with new content * - Optionally creates backup * - Preserves ACLs and other metadata from original */BOOL atomic_replace_windows(const wchar_t *original, const wchar_t *replacement, const wchar_t *backup) { if (!ReplaceFileW(original, replacement, backup, REPLACEFILE_WRITE_THROUGH | REPLACEFILE_IGNORE_MERGE_ERRORS, NULL, NULL)) { wprintf(L"ReplaceFile failed: %lu", GetLastError()); return FALSE; } return TRUE;}| API | Overwrites Existing? | Cross-Volume? | Atomic? |
|---|---|---|---|
| MoveFileW | No (fails) | No (fails) | Yes (same volume) |
| MoveFileExW + REPLACE | Yes | Copy+delete | Yes (same volume) |
| ReplaceFileW | Yes (purpose) | No | Yes |
| SetFileInformationByHandle | Configurable | No | Yes |
Unlike Unix, Windows enforces mandatory file locking. If any process has a file open without FILE_SHARE_DELETE, you cannot rename or delete that file. This is a common cause of 'file in use' errors and why Windows applications often require restarting to complete updates.
Mastering rename operations enables powerful file manipulation patterns. Here are the most important best practices and their applications:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
/** * Pattern: Log rotation * * Atomically rotate logs: app.log -> app.log.1 -> app.log.2 ... */int rotate_logs(const char *base_path, int keep_count) { char old_path[PATH_MAX], new_path[PATH_MAX]; // Work backwards: move .3 to .4, .2 to .3, etc. for (int i = keep_count - 1; i >= 1; i--) { snprintf(old_path, sizeof(old_path), "%s.%d", base_path, i); snprintf(new_path, sizeof(new_path), "%s.%d", base_path, i + 1); rename(old_path, new_path); // Ignore errors (file may not exist) } // Move current log to .1 snprintf(new_path, sizeof(new_path), "%s.1", base_path); rename(base_path, new_path); return 0;} /** * Pattern: Safe configuration update with rollback */typedef struct { char config_path[PATH_MAX]; char backup_path[PATH_MAX]; char temp_path[PATH_MAX]; int has_backup;} config_update_t; int config_update_begin(config_update_t *ctx, const char *config_path) { snprintf(ctx->config_path, PATH_MAX, "%s", config_path); snprintf(ctx->backup_path, PATH_MAX, "%s.bak", config_path); snprintf(ctx->temp_path, PATH_MAX, "%s.new.%d", config_path, getpid()); ctx->has_backup = 0; return 0;} int config_update_commit(config_update_t *ctx) { // Create backup of current config if (access(ctx->config_path, F_OK) == 0) { if (rename(ctx->config_path, ctx->backup_path) == 0) { ctx->has_backup = 1; } } // Rename new config to actual path if (rename(ctx->temp_path, ctx->config_path) == -1) { // Rollback: restore backup if we made one if (ctx->has_backup) { rename(ctx->backup_path, ctx->config_path); } return -1; } return 0;} int config_update_rollback(config_update_t *ctx) { // Remove incomplete new file unlink(ctx->temp_path); // Restore from backup if exists if (ctx->has_backup) { rename(ctx->backup_path, ctx->config_path); } return 0;} /** * Pattern: Lock file with atomic creation */int acquire_lock(const char *lock_path) { char temp_path[PATH_MAX]; snprintf(temp_path, sizeof(temp_path), "%s.%d", lock_path, getpid()); // Create temp file with our PID int fd = open(temp_path, O_WRONLY | O_CREAT | O_EXCL, 0644); if (fd == -1) return -1; dprintf(fd, "%d", getpid()); close(fd); // Try to atomically create the lock file #ifdef RENAME_NOREPLACE if (renameat2(AT_FDCWD, temp_path, AT_FDCWD, lock_path, RENAME_NOREPLACE) == -1) { unlink(temp_path); return -1; // Lock already held } #else // Fallback: link + unlink (also atomic) if (link(temp_path, lock_path) == -1) { unlink(temp_path); return -1; } unlink(temp_path); #endif return 0;}The rename() system call provides atomic file name changes—a critical primitive for building reliable software. Understanding its guarantees and limitations enables powerful, crash-consistent file manipulation.
Module Summary:
With this page, we've completed our exploration of directory operations—the fundamental building blocks for file system manipulation:
Together, these operations enable complete programmatic control over file system structure.
You have mastered directory operations in operating systems—from basic file creation through atomic rename patterns. This knowledge is essential for building reliable file management tools, understanding how applications like package managers and databases maintain consistency, and writing robust systems software.