Loading learning content...
Every file that exists on your computer—every document, configuration file, executable, and log—was at some point created. And many files that once existed have since been deleted. These two operations, create and delete, form the foundation upon which all file system interaction is built.
Yet beneath the simplicity of touch newfile.txt or rm oldfile.txt lies a sophisticated orchestration of system calls, kernel data structures, directory updates, disk operations, and crash-recovery mechanisms. Understanding how files are created and deleted reveals fundamental truths about operating system design, reliability guarantees, and the careful balance between performance and data integrity.
By the end of this page, you will understand the complete lifecycle of file creation and deletion—from the user-space system call interface through kernel processing, directory manipulation, inode allocation, and disk journaling. You'll master the critical concepts of atomicity, durability, and crash recovery that distinguish robust file systems from fragile ones.
Before diving into system calls and kernel internals, let's establish a clear conceptual model of what file creation truly means in an operating system context.
A file is not just data on disk. A file is the binding of three distinct entities:
report.pdfWhen you "create a file," you're not necessarily allocating disk space for data. Rather, you're establishing the binding between a name and a metadata structure (typically an inode in Unix systems). The data blocks may come later when you actually write content.
An empty file occupies metadata storage (an inode) and a directory entry, but zero data blocks. This distinction is crucial—creating a file and writing to a file are fundamentally different operations at the kernel level, even though many high-level APIs combine them.
The creation process involves:
Each step has significant implications for performance, concurrency, and reliability.
Operating systems expose file creation through well-defined system call interfaces. While the semantics are similar across platforms, the specific calls differ. Understanding these interfaces is essential for systems programming.
POSIX (Unix/Linux) File Creation:
The primary file creation system calls in POSIX systems are open() with creation flags, and creat() (legacy).
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
#include <fcntl.h>#include <sys/stat.h>#include <unistd.h>#include <errno.h>#include <stdio.h> /** * Method 1: open() with O_CREAT flag * The modern, preferred approach for file creation. * * Key flags: * O_CREAT - Create file if it doesn't exist * O_EXCL - Fail if file already exists (atomic creation) * O_TRUNC - Truncate to zero length if file exists * O_WRONLY - Open for writing only * O_RDWR - Open for reading and writing */int create_file_open(const char *path) { // Create new file with rw-r--r-- permissions // O_EXCL ensures atomic creation: no race conditions int fd = open(path, O_CREAT | O_EXCL | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (fd == -1) { if (errno == EEXIST) { fprintf(stderr, "File already exists: %s\n", path); } else if (errno == EACCES) { fprintf(stderr, "Permission denied: %s\n", path); } else if (errno == ENOENT) { fprintf(stderr, "Parent directory doesn't exist: %s\n", path); } else if (errno == ENOSPC) { fprintf(stderr, "No space left on device\n"); } return -1; } return fd; // Caller must close(fd) when done} /** * Method 2: creat() - Legacy system call * Equivalent to: open(path, O_CREAT | O_WRONLY | O_TRUNC, mode) * * Note: creat() truncates existing files, making it unsuitable * for atomic file creation. Prefer open() with O_EXCL. */int create_file_creat(const char *path) { int fd = creat(path, S_IRUSR | S_IWUSR); if (fd == -1) { perror("creat failed"); return -1; } return fd;} /** * Safe file creation with proper atomic semantics * This pattern ensures no race conditions between checking * for existence and creating the file. */int safe_create_new_file(const char *path, mode_t mode) { int fd = open(path, O_CREAT | O_EXCL | O_RDWR, mode); if (fd == -1 && errno == EEXIST) { // File was created by another process between our // check and create - this is NOT a race condition // because O_EXCL makes the check-and-create atomic return -1; } return fd;}Critical Flags Explained:
| Flag | Purpose | Behavior |
|---|---|---|
O_CREAT | Enable file creation | Create file if it doesn't exist; otherwise open existing file |
O_EXCL | Exclusive creation | Combined with O_CREAT: fail with EEXIST if file exists (atomic) |
O_TRUNC | Truncate | If file exists, truncate to zero length |
O_DIRECTORY | Directory only | Fail if path is not a directory |
O_NOFOLLOW | No symlink follow | Fail if path is a symbolic link |
O_CLOEXEC | Close on exec | Automatically close fd on exec() family calls |
The O_CREAT | O_EXCL combination provides a kernel-level atomic guarantee: the existence check and file creation happen as a single indivisible operation. This is critical for lock files, PID files, and any scenario where multiple processes might race to create the same file. Never implement this pattern with separate existence check and create calls—that's a classic TOCTOU (time-of-check-time-of-use) vulnerability.
Windows File Creation:
Windows provides the CreateFile() API, which despite its name handles both creation and opening of files.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
#include <windows.h>#include <stdio.h> /** * CreateFile() - The universal Windows file operation * * CreationDisposition values: * CREATE_NEW - Create new; fail if exists (like O_CREAT|O_EXCL) * CREATE_ALWAYS - Create new; truncate if exists (like O_CREAT|O_TRUNC) * OPEN_EXISTING - Open existing; fail if doesn't exist * OPEN_ALWAYS - Open if exists, create if doesn't * TRUNCATE_EXISTING - Open and truncate; fail if doesn't exist */HANDLE create_file_windows(const wchar_t *path) { HANDLE hFile = CreateFileW( path, // File path GENERIC_READ | GENERIC_WRITE, // Access mode 0, // Share mode (0 = exclusive) NULL, // Security attributes CREATE_NEW, // Creation disposition FILE_ATTRIBUTE_NORMAL, // File attributes NULL // Template file ); if (hFile == INVALID_HANDLE_VALUE) { DWORD error = GetLastError(); if (error == ERROR_FILE_EXISTS) { wprintf(L"File already exists: %s\n", path); } else if (error == ERROR_ACCESS_DENIED) { wprintf(L"Access denied: %s\n", path); } else if (error == ERROR_PATH_NOT_FOUND) { wprintf(L"Path not found: %s\n", path); } else if (error == ERROR_DISK_FULL) { wprintf(L"Disk full\n"); } return INVALID_HANDLE_VALUE; } return hFile; // Caller must CloseHandle(hFile) when done} /** * Atomic file creation with security descriptor * More production-ready example with ownership control */HANDLE create_secure_file(const wchar_t *path) { SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; // Default security sa.bInheritHandle = FALSE; // Not inherited by child processes HANDLE hFile = CreateFileW( path, GENERIC_WRITE, FILE_SHARE_READ, // Allow others to read &sa, CREATE_NEW, // Fail if exists FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, NULL ); return hFile;}When a user-space program invokes open() with O_CREAT, it triggers a sequence of kernel operations that are far more complex than they might appear. Let's trace through exactly what happens in a Unix-like kernel (we'll use Linux as our reference implementation).
Phase 1: System Call Entry and Path Resolution
The journey begins when the CPU transitions from user mode to kernel mode via a software interrupt or syscall instruction. The kernel's file system layer receives the request and begins path resolution—the process of walking from the root (or current directory) through each path component to find the parent directory.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
/** * Simplified path resolution algorithm (Linux namei) * * Given: "/home/user/documents/report.txt" * * Goal: Find the dentry for "/home/user/documents" (parent) * and extract "report.txt" (final component) */struct path_resolution_result { struct dentry *parent_dentry; // Parent directory struct inode *parent_inode; // Parent inode const char *final_name; // "report.txt" int name_len; // Length of final name}; int resolve_path_for_create(const char *path, struct path_resolution_result *result) { struct dentry *current_dentry; struct inode *current_inode; // Step 1: Determine starting point if (path[0] == '/') { // Absolute path: start from root current_dentry = current->fs->root.dentry; } else { // Relative path: start from cwd current_dentry = current->fs->pwd.dentry; } current_inode = current_dentry->d_inode; // Step 2: Walk each path component (except the last) const char *component = path; while (has_more_components(component)) { // Extract next component: "home", then "user", then "documents" char name[NAME_MAX]; int len = extract_next_component(component, name); // Check execute permission on current directory if (!may_exec(current_inode)) { return -EACCES; // Permission denied } // Look up component in current directory struct dentry *next = lookup_in_directory(current_dentry, name, len); if (next == NULL) { return -ENOENT; // Component doesn't exist } // Handle symbolic links (follow them in most cases) if (is_symlink(next->d_inode)) { next = follow_symlink(next); if (next == NULL) { return -ELOOP; // Too many symlinks } } // Ensure it's a directory (for intermediate components) if (!S_ISDIR(next->d_inode->i_mode)) { return -ENOTDIR; // Not a directory } current_dentry = next; current_inode = next->d_inode; component = advance_past_component(component, len); } // Step 3: Extract final component (don't look it up yet) result->parent_dentry = current_dentry; result->parent_inode = current_inode; result->final_name = component; // "report.txt" result->name_len = strlen(component); return 0;}Phase 2: Permission Check and Existence Test
Once the parent directory is located, the kernel verifies that the calling process has permission to create files in that directory (requires write permission). It then checks whether the target filename already exists.
/tmp), only owner can create files owned by themselvesPhase 3: Inode Allocation
If the file doesn't exist (or needs to be created fresh), the kernel allocates a new inode. This is where the file system implementation becomes critical:
Inode allocation must be crash-consistent: if the system crashes after allocating the inode but before linking it to a directory, the inode should be recoverable (either reclaimed or linked).
Phase 4: Inode Initialization
The newly allocated inode is initialized with:
123456789101112131415161718192021222324252627282930313233343536373839404142
/** * Initialize a newly allocated inode for file creation */void init_inode_for_new_file(struct inode *inode, mode_t mode, struct inode *parent_dir) { struct timespec now; current_time(&now); // Determine effective UID/GID uid_t uid = current_fsuid(); gid_t gid; if (parent_dir->i_mode & S_ISGID) { // Parent has setgid: inherit parent's group gid = parent_dir->i_gid; } else { gid = current_fsgid(); } // Apply umask to mode mode_t effective_mode = S_IFREG | (mode & ~current_umask() & 0777); // Initialize inode fields inode->i_mode = effective_mode; inode->i_uid = uid; inode->i_gid = gid; inode->i_size = 0; inode->i_nlink = 1; // Will be incremented when dir entry is added inode->i_blocks = 0; // No data blocks yet // Set all timestamps to now inode->i_atime = now; // Access time inode->i_mtime = now; // Modification time inode->i_ctime = now; // Status change time // Clear all block pointers memset(inode->i_block, 0, sizeof(inode->i_block)); // Inherit some flags from parent inode->i_flags = parent_dir->i_flags & (FS_NODUMP_FL | FS_NOATIME_FL);}Phase 5: Directory Entry Creation
The most critical step is adding the new (name → inode) mapping to the parent directory. This operation must be atomic with respect to crashes—either the directory entry exists completely, or it doesn't exist at all. Partial directory entries are corruption.
The directory format varies by file system:
Once the directory entry is successfully written and committed to stable storage (either directly or via a journal), the file officially "exists." Even if the system crashes immediately after, the file will be present after recovery. This is why journaling and ordering of writes are so critical to file system design.
File deletion in Unix-like systems is more subtle than it might appear. The key insight is that deleting a file doesn't necessarily delete its data—it removes a link to the file. Understanding this distinction is essential for grasping Unix file semantics.
The unlink() System Call:
In POSIX systems, the primary file deletion call is unlink(), not delete(). This naming is intentional: you're removing one link (directory entry) that references the file's inode.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
#include <unistd.h>#include <stdio.h>#include <errno.h>#include <sys/stat.h> /** * Basic file deletion using unlink() * * unlink() removes the directory entry, decrements the inode's * link count, and if the link count reaches zero AND no processes * have the file open, the file's data blocks are freed. */int delete_file(const char *path) { if (unlink(path) == -1) { switch (errno) { case EACCES: fprintf(stderr, "No write permission on parent directory\n"); break; case EBUSY: fprintf(stderr, "File is in use by system\n"); break; case EISDIR: fprintf(stderr, "Path is a directory (use rmdir())\n"); break; case ENOENT: fprintf(stderr, "File doesn't exist\n"); break; case EPERM: fprintf(stderr, "Sticky bit restriction\n"); break; case EROFS: fprintf(stderr, "File system is read-only\n"); break; default: perror("unlink failed"); } return -1; } return 0;} /** * Demonstrating the difference between unlink and actual deletion * * This shows that a file's data persists as long as any process * has the file open, even after unlink() has removed the name. */void demonstrate_unlink_semantics(void) { const char *path = "/tmp/test_unlink.txt"; // Step 1: Create and open a file int fd = open(path, O_CREAT | O_RDWR, 0644); if (fd == -1) { perror("open"); return; } // Step 2: Write some data const char *data = "This data survives unlink!"; write(fd, data, strlen(data)); // Step 3: Unlink the file (remove the name) // The file now has no name, but still exists because fd is open if (unlink(path) == -1) { perror("unlink"); close(fd); return; } printf("File unlinked. It no longer appears in directory listings.\n"); printf("But we can still access it through the open file descriptor!\n"); // Step 4: Seek back and read lseek(fd, 0, SEEK_SET); char buffer[100]; ssize_t n = read(fd, buffer, sizeof(buffer) - 1); buffer[n] = '\0'; printf("Data read from 'deleted' file: %s\n", buffer); // At this point, the file has: // - 0 directory entries (it's invisible) // - 1 open file descriptor (our fd) // - Data intact on disk // Step 5: Close the file descriptor // NOW the file is truly deleted close(fd); printf("File descriptor closed. File and data truly deleted.\n");}The open-then-unlink pattern is commonly used to create anonymous temporary files that are automatically cleaned up when the process terminates (normally or abnormally). The file has no name and cannot be accessed by other processes, but the creating process can read/write it freely. This is more reliable than traditional temp files because it guarantees cleanup—the OS will reclaim the space even if the process crashes.
The Link Count Mechanism:
Every inode maintains a link count (i_nlink) that tracks how many directory entries point to it. The actual deletion of file data only occurs when:
This is a reference-counting system where both hard links and open file descriptors keep the data alive.
| Link Count | Open Count | Visible? | Data Status |
|---|---|---|---|
| ≥ 1 | ≥ 0 | Yes | Alive and accessible via name |
| 0 | ≥ 1 | No | Alive but only via open fd |
| 0 | 0 | No | Deleted; space reclaimed |
Kernel-Level Deletion Process:
When unlink() is called, the kernel performs these steps:
Beyond the basic unlink(), POSIX provides additional interfaces for file deletion that handle edge cases and modern requirements.
The remove() Function:
remove() is a C standard library function (not a system call) that works for both files and directories:
123456789101112131415161718
#include <stdio.h> /** * remove() - Standard C library function for deletion * * Internally calls: * unlink(path) for regular files * rmdir(path) for directories * * This is the most portable way to delete files/directories. */int delete_portable(const char *path) { if (remove(path) != 0) { perror("remove failed"); return -1; } return 0;}The unlinkat() System Call:
unlinkat() is a more powerful variant introduced to address race conditions in multi-threaded programs and to work with relative paths securely:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
#include <unistd.h>#include <fcntl.h>#include <stdio.h> /** * unlinkat() - Delete file relative to a directory fd * * Advantages over unlink(): * 1. Race-condition-free path operations * 2. Works with O_PATH file descriptors * 3. Can delete directories with AT_REMOVEDIR flag * * Parameters: * dirfd - File descriptor of directory (or AT_FDCWD for cwd) * path - Path relative to dirfd * flags - 0 for files, AT_REMOVEDIR for directories */int secure_delete_in_directory(int dirfd, const char *filename) { if (unlinkat(dirfd, filename, 0) == -1) { perror("unlinkat failed"); return -1; } return 0;} /** * Delete a directory using unlinkat * Equivalent to rmdir() but relative to a directory fd */int secure_rmdir_in_directory(int dirfd, const char *dirname) { if (unlinkat(dirfd, dirname, AT_REMOVEDIR) == -1) { perror("unlinkat(AT_REMOVEDIR) failed"); return -1; } return 0;} /** * Why dirfd-based operations are more secure: * * Traditional approach (race-prone): * chdir("/safe/directory"); * unlink("secret_file"); // What if cwd changed between calls? * * Secure approach: * int dirfd = open("/safe/directory", O_RDONLY | O_DIRECTORY); * unlinkat(dirfd, "secret_file", 0); // Always relative to dirfd * close(dirfd); */void demonstrate_secure_deletion(void) { // Open the directory (without changing cwd) int dirfd = open("/tmp/safe_dir", O_RDONLY | O_DIRECTORY); if (dirfd == -1) { perror("open directory"); return; } // Delete files relative to the directory fd // Even if another process modifies our cwd, this is safe if (unlinkat(dirfd, "file1.txt", 0) == -1) { perror("unlinkat"); } // Delete an empty subdirectory if (unlinkat(dirfd, "empty_subdir", AT_REMOVEDIR) == -1) { perror("unlinkat directory"); } close(dirfd);}Directories can only be deleted if they are empty (contain only . and ..). To delete a directory with contents, you must recursively delete all files and subdirectories first. This is a deliberate design decision that prevents accidental mass data loss—unlike rm -rf, which automates this recursion, the system call level requires explicitness.
The most challenging aspect of file creation and deletion is ensuring crash consistency—guaranteeing that the file system remains in a valid state even if the system loses power or crashes mid-operation.
The Problem:
Creating a file requires multiple disk writes:
If these writes are interleaved with a crash, various corrupt states are possible:
Journaling Solution:
Modern file systems use journaling (write-ahead logging) to ensure crash consistency:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
/** * Journaled file creation (ext4 style) * * The journal is a circular log of intended operations. * Operations become permanent only after journal commit. */int journaled_create_file(const char *path, mode_t mode) { struct journal_handle *handle; struct inode *new_inode; struct dentry *parent_dentry; /* Phase 1: Start Transaction */ handle = journal_start(sb->s_journal, credits_needed); if (IS_ERR(handle)) { return PTR_ERR(handle); } /* Phase 2: Perform Operations (writes go to journal buffer) */ // Allocate new inode new_inode = alloc_inode(sb); if (!new_inode) { journal_abort(handle); return -ENOSPC; } // Initialize inode init_inode_for_new_file(new_inode, mode, parent_dir); // Record modified inode to journal journal_get_write_access(handle, inode_block_buffer); // Add directory entry result = add_dirent(parent_dentry, name, new_inode); if (result < 0) { journal_abort(handle); free_inode(new_inode); return result; } // Record modified directory blocks to journal journal_get_write_access(handle, dir_block_buffer); /* Phase 3: Commit Transaction */ // This writes all changes to the journal and marks complete journal_stop(handle); /* * At this point, the journal contains a complete record * of the file creation. If we crash now, recovery will: * 1. Read the journal * 2. See this transaction is complete * 3. Replay it to the actual disk locations * 4. Mark the journal entry as applied */ /* Phase 4: Writeback (may happen later) */ // Eventually, the actual disk blocks are updated // and the journal entry is discarded return 0;}The fsync() and sync() Calls:
For applications that need guaranteed durability (databases, text editors, etc.), the journaling guarantee isn't enough—the journal commit itself must be forced to disk. This is achieved with fsync() or fdatasync():
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
#include <unistd.h>#include <fcntl.h>#include <stdio.h> /** * Truly durable file creation pattern * * After this function returns successfully, the file is * guaranteed to survive any subsequent crash or power loss. */int create_file_durable(const char *path, const char *content) { // Step 1: Open parent directory char parent_path[PATH_MAX]; get_parent_directory(path, parent_path); int dirfd = open(parent_path, O_RDONLY | O_DIRECTORY); if (dirfd == -1) { perror("open parent dir"); return -1; } // Step 2: Create and write the file int fd = open(path, O_CREAT | O_EXCL | O_WRONLY, 0644); if (fd == -1) { perror("open"); close(dirfd); return -1; } ssize_t written = write(fd, content, strlen(content)); if (written < 0) { perror("write"); close(fd); close(dirfd); unlink(path); // Clean up partial file return -1; } // Step 3: fsync the file to flush data and metadata if (fsync(fd) == -1) { perror("fsync file"); close(fd); close(dirfd); return -1; } close(fd); // Step 4: fsync the directory to ensure the entry is durable // This is the step most applications forget! if (fsync(dirfd) == -1) { perror("fsync directory"); close(dirfd); return -1; } close(dirfd); // Now the file creation is fully durable return 0;}One of the most common durability bugs is forgetting to fsync() the parent directory after creating a file. Without this, the file's data may be on disk, but the directory entry might only be in memory—a crash would result in the file disappearing even though fsync(fd) was called. Always sync the directory after creating or renaming files.
Robust file creation and deletion code must handle numerous error conditions. Understanding these errors helps debug problems and write resilient applications.
| Error | Cause | Recovery Strategy |
|---|---|---|
EEXIST | File already exists (with O_EXCL) | Check if existing file is suitable, or use different name |
ENOENT | Parent directory doesn't exist | Create parent directories first (mkdir -p pattern) |
EACCES | Permission denied | Check permissions, run with appropriate privileges |
ENOSPC | No space left on device | Free space, or write to different device |
ENFILE | System file table full | Close unused files, increase system limits |
EMFILE | Per-process file limit reached | Close unused files, increase ulimit |
EROFS | Read-only file system | Remount as read-write, or use different location |
ELOOP | Too many symbolic links | Resolve symlinks manually, or use O_NOFOLLOW |
ENAMETOOLONG | Filename exceeds limit | Use shorter name (typically 255 char limit) |
EPERM | Operation not permitted | Often due to sticky bit or immutable flag |
ETXTBSY | Text file busy (executable in use) | Wait for execution to complete |
Handling Special Cases:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
#include <errno.h>#include <sys/stat.h>#include <string.h>#include <libgen.h> /** * Create file with parent directory creation if needed * Similar to "mkdir -p" + touch behavior */int create_file_with_parents(const char *path, mode_t mode) { // Try to create the file directly first int fd = open(path, O_CREAT | O_EXCL | O_WRONLY, mode); if (fd >= 0) { return fd; // Success on first try } if (errno != ENOENT) { return -1; // Some other error } // Parent doesn't exist - need to create directories char *path_copy = strdup(path); char *parent = dirname(path_copy); // Recursively create parent directories if (create_directories_recursive(parent, 0755) == -1) { free(path_copy); return -1; } free(path_copy); // Now try again fd = open(path, O_CREAT | O_EXCL | O_WRONLY, mode); return fd;} /** * Recursively create directories (like mkdir -p) */int create_directories_recursive(const char *path, mode_t mode) { struct stat st; // Check if already exists if (stat(path, &st) == 0) { if (S_ISDIR(st.st_mode)) { return 0; // Already a directory } errno = ENOTDIR; // Exists but not a directory return -1; } if (errno != ENOENT) { return -1; // Some other error } // Recursively create parent first char *path_copy = strdup(path); char *parent = dirname(path_copy); if (strcmp(parent, ".") != 0 && strcmp(parent, "/") != 0) { if (create_directories_recursive(parent, mode) == -1) { free(path_copy); return -1; } } free(path_copy); // Create this directory if (mkdir(path, mode) == -1) { if (errno == EEXIST) { // Race condition: another process created it return 0; } return -1; } return 0;} /** * Delete file with retry for transient errors */int delete_file_robust(const char *path, int max_retries) { for (int attempt = 0; attempt < max_retries; attempt++) { if (unlink(path) == 0) { return 0; // Success } switch (errno) { case ENOENT: return 0; // Already deleted - success case ETXTBSY: case EBUSY: // Transient - wait and retry usleep(100000 * (attempt + 1)); // Exponential backoff continue; case EACCES: case EPERM: case EROFS: // Permanent - no point retrying return -1; default: // Unknown - try once more if (attempt < max_retries - 1) { continue; } return -1; } } return -1; // Exhausted retries}We've explored the deceptively complex world of file creation and deletion. What appears as a simple operation from user space is actually a coordinated dance of kernel subsystems, disk operations, and crash-recovery mechanisms.
unlink() removes a name binding; data persists until all links and open handles are goneWhat's Next:
Now that we understand file creation and deletion, we'll explore directory creation and deletion in the next page. While similar in many ways, directories have unique constraints and behaviors that make them a distinct topic—particularly around the requirement that directories be empty before deletion and the special handling of . and .. entries.
You now understand the complete mechanism of file creation and deletion in operating systems—from user-space system calls through kernel processing, inode allocation, directory manipulation, and crash consistency guarantees. This knowledge forms the foundation for understanding all other directory operations.