Loading content...
Directories are the organizational backbone of every file system. Without them, we'd have a flat namespace of millions of files with no hierarchy, no grouping, and no structure. Every time you navigate to /home/user/documents or C:\Users\Documents, you're traversing a directory tree that makes file organization possible.
Yet directories are far more than simple containers. In Unix-like systems, directories are themselves files—special files that contain mappings from names to inodes. This elegant design has profound implications for how directories are created, deleted, and managed. Understanding directory operations reveals fundamental truths about file system architecture and the careful invariants that operating systems must maintain.
By the end of this page, you will understand the complete mechanics of directory creation and deletion—from the mkdir() and rmdir() system calls through kernel-level inode allocation, the special handling of . and .. entries, the empty directory constraint for deletion, and the challenges of recursive directory removal. You'll also learn why certain operations that work on files don't work on directories.
In Unix-like operating systems, directories follow the "everything is a file" philosophy. A directory is simply a file whose content is a collection of (name, inode number) pairs—directory entries, or "dentries." This design leads to elegant uniformity but also imposes special constraints.
What makes a directory different from a regular file:
| Property | Regular File | Directory |
|---|---|---|
| Content | Arbitrary bytes | Name → inode mappings (dentries) |
| File type (mode) | S_IFREG (0100000) | S_IFDIR (0040000) |
| Read access | Read bytes directly | Must use readdir() / getdents() |
| Write access | Write bytes anywhere | Cannot write directly; use mkdir, link, etc. |
| Minimum link count | 1 (one directory entry) | 2 (parent's entry + . self-reference) |
| Execute permission means | Can execute as program | Can traverse (enter) directory |
| Can be empty | Yes (0 bytes) | No—always has . and .. |
| Deletion constraint | None (just unlink) | Must be empty (only . and ..) |
The . and .. Entries:
Every directory contains two special entries that are fundamental to file system navigation:
. (dot) — A hard link to the directory itself. This is why a new directory has a link count of 2: the parent's entry for it, plus its own . entry.
.. (dot-dot) — A hard link to the parent directory. This enables upward navigation in the directory tree. The root directory has .. pointing to itself.
These entries are created automatically when a directory is made and cannot be manually created or deleted. They are maintained by the kernel as invariants.
If you run ls -l on a directory, its link count reveals how many subdirectories it contains. A directory with link count 5 has: 1 (parent's reference) + 1 (self .) + 3 (three subdirectories, each with .. pointing here). Empty directories have link count 2.
1234567891011121314151617181920212223
# Demonstrating directory link counts # Create a new directorymkdir example_dirls -ld example_dir# drwxr-xr-x 2 user user 4096 Jan 15 10:00 example_dir# ^--- Link count is 2 (parent's entry + .) # Create subdirectoriesmkdir example_dir/sub1mkdir example_dir/sub2ls -ld example_dir# drwxr-xr-x 4 user user 4096 Jan 15 10:01 example_dir# ^--- Link count is 4 (parent + . + 2 subdirs' ..) # Each subdirectory also has link count 2ls -ld example_dir/sub1# drwxr-xr-x 2 user user 4096 Jan 15 10:01 example_dir/sub1 # Viewing the . and .. entriesls -la example_dir/sub1# drwxr-xr-x 2 user user 4096 Jan 15 10:01 .# drwxr-xr-x 4 user user 4096 Jan 15 10:01 ..The mkdir() system call creates a new directory. Unlike file creation with open(), directory creation has its own dedicated system call because directories require special initialization that file creation doesn't.
POSIX mkdir() Interface:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
#include <sys/stat.h>#include <sys/types.h>#include <errno.h>#include <stdio.h> /** * mkdir() - Create a new directory * * Synopsis: * int mkdir(const char *pathname, mode_t mode); * * Parameters: * pathname - Path of the directory to create * mode - Permission bits (masked by umask) * * Returns: * 0 on success, -1 on error (check errno) * * Key differences from file creation: * - Cannot use O_CREAT with open() for directories * - Automatically creates . and .. entries * - Sets directory type bit (S_IFDIR) * - Initial link count is 2, not 1 */int create_directory(const char *path, mode_t mode) { if (mkdir(path, mode) == -1) { switch (errno) { case EEXIST: fprintf(stderr, "Directory already exists: %s", path); break; case ENOENT: fprintf(stderr, "Parent directory doesn't exist: %s", path); break; case EACCES: fprintf(stderr, "Permission denied on parent: %s", path); break; case ENOSPC: fprintf(stderr, "No space left on device"); break; case ENAMETOOLONG: fprintf(stderr, "Directory name too long"); break; case ENOTDIR: fprintf(stderr, "A component of path is not a directory"); break; case EROFS: fprintf(stderr, "Read-only file system"); break; case EMLINK: fprintf(stderr, "Parent directory has too many links"); break; default: perror("mkdir failed"); } return -1; } return 0;} /** * Example: Create directory with common permission patterns */void directory_creation_examples(void) { // Private directory: only owner can access // rwx------ mkdir("/tmp/private_dir", S_IRWXU); // Standard directory: owner full, group/others read+traverse // rwxr-xr-x mkdir("/tmp/standard_dir", S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); // Shared directory: owner and group have full access // rwxrwx--- mkdir("/tmp/shared_dir", S_IRWXU | S_IRWXG); // Shared with sticky bit: everyone can write, but only owner can delete // rwxrwxrwt (note: must set sticky bit separately after creation) mkdir("/tmp/sticky_dir", S_IRWXU | S_IRWXG | S_IRWXO); chmod("/tmp/sticky_dir", S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX);}The mkdirat() Variant:
Like openat() and unlinkat(), the mkdirat() system call allows directory creation relative to a directory file descriptor, avoiding race conditions:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
#include <sys/stat.h>#include <fcntl.h>#include <unistd.h>#include <stdio.h> /** * mkdirat() - Create directory relative to directory fd * * Synopsis: * int mkdirat(int dirfd, const char *pathname, mode_t mode); * * This is the race-condition-free way to create directories * in a specific location without relying on the current * working directory. */int create_directory_relative(int dirfd, const char *dirname, mode_t mode) { if (mkdirat(dirfd, dirname, mode) == -1) { perror("mkdirat"); return -1; } return 0;} /** * Safe pattern: Create structure within a known directory */int create_project_structure(const char *project_path) { // Open the project directory (or create it first) mkdir(project_path, 0755); int project_fd = open(project_path, O_RDONLY | O_DIRECTORY); if (project_fd == -1) { perror("open project dir"); return -1; } // Create subdirectories relative to project_fd // Even if cwd changes, these go in the right place if (mkdirat(project_fd, "src", 0755) == -1 && errno != EEXIST) { perror("mkdirat src"); } if (mkdirat(project_fd, "include", 0755) == -1 && errno != EEXIST) { perror("mkdirat include"); } if (mkdirat(project_fd, "tests", 0755) == -1 && errno != EEXIST) { perror("mkdirat tests"); } if (mkdirat(project_fd, "build", 0755) == -1 && errno != EEXIST) { perror("mkdirat build"); } close(project_fd); return 0;}Some file systems (notably older ext2/ext3) have a limit on the number of subdirectories a directory can contain, because each subdirectory adds a hard link via ... This limit is typically 32000 for ext3. Modern file systems like ext4, XFS, and btrfs effectively remove this limitation by using different link-counting strategies for directories.
When mkdir() is invoked, the kernel performs a sequence of operations that is similar to file creation but with important additions specific to directories.
Step-by-Step Kernel Processing:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
/** * Kernel implementation of mkdir() - Simplified * Based on Linux VFS layer */int sys_mkdir(const char __user *pathname, mode_t mode) { struct path parent_path; struct dentry *dentry; struct inode *dir; int error; /* Phase 1: Path Resolution */ // Find parent directory and verify it exists and is a directory error = user_path_parent(pathname, &parent_path, &dentry); if (error) { return error; } dir = parent_path.dentry->d_inode; /* Phase 2: Permission Checks */ // Verify write and execute permission on parent directory error = inode_permission(dir, MAY_WRITE | MAY_EXEC); if (error) { goto out; } // Check for existing entry if (dentry->d_inode) { error = -EEXIST; goto out; } /* Phase 3: File System Specific mkdir */ // Call the actual file system's mkdir implementation // This is where ext4_mkdir, xfs_mkdir, etc. are called error = vfs_mkdir(dir, dentry, mode); out: path_put(&parent_path); return error;} /** * VFS layer mkdir - calls file system implementation */int vfs_mkdir(struct inode *dir, struct dentry *dentry, mode_t mode) { int error; // Security module checks (SELinux, AppArmor, etc.) error = security_inode_mkdir(dir, dentry, mode); if (error) { return error; } // Call the file system's mkdir operation // dir->i_op->mkdir is function pointer to fs-specific code error = dir->i_op->mkdir(dir, dentry, mode); if (!error) { // Notify inode watchers (inotify, fanotify) fsnotify_mkdir(dir, dentry); } return error;} /** * File system specific mkdir (e.g., ext4) */int ext4_mkdir(struct inode *dir, struct dentry *dentry, mode_t mode) { handle_t *handle; struct inode *inode; int err; /* Start a journal transaction */ handle = ext4_journal_start(dir, EXT4_HT_DIR, credits_needed); if (IS_ERR(handle)) { return PTR_ERR(handle); } /* Allocate a new inode */ inode = ext4_new_inode(handle, dir, S_IFDIR | mode, NULL, 0); if (IS_ERR(inode)) { ext4_journal_stop(handle); return PTR_ERR(inode); } /* Initialize directory-specific fields */ inode->i_op = &ext4_dir_inode_operations; inode->i_fop = &ext4_dir_operations; /* Create the . entry (self-reference) */ err = ext4_add_dot_entry(handle, dentry, inode, inode); if (err) { goto cleanup; } /* Create the .. entry (parent reference) */ err = ext4_add_dotdot_entry(handle, dentry, inode, dir); if (err) { goto cleanup; } /* Set initial link count to 2 (parent's ref + self .) */ inode->i_nlink = 2; /* Increment parent's link count (for ..) */ ext4_inc_count(handle, dir); /* Add the directory entry in the parent */ err = ext4_add_entry(handle, dentry, inode); if (err) { goto cleanup; } /* Complete the transaction */ ext4_journal_stop(handle); /* Associate dentry with new inode */ d_instantiate(dentry, inode); return 0; cleanup: iput(inode); ext4_journal_stop(handle); return err;}. entry — self-referential directory entry pointing to new inode.. entry — directory entry pointing to parent's inode. self-reference.. referenceYou cannot create a directory with open(path, O_CREAT | O_DIRECTORY, mode). The O_DIRECTORY flag ensures you're opening an existing directory, not creating a new one. Directory creation requires special handling for the . and .. entries, link count initialization, and parent link count updates—operations that don't apply to regular files.
Directory deletion uses the rmdir() system call, which has a critical constraint: the directory must be empty. This requirement exists to prevent accidental data loss and to maintain file system consistency.
POSIX rmdir() Interface:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
#include <unistd.h>#include <errno.h>#include <stdio.h> /** * rmdir() - Remove an empty directory * * Synopsis: * int rmdir(const char *pathname); * * Returns: * 0 on success, -1 on error (check errno) * * Key constraint: * Directory MUST be empty (contain only . and ..) * ENOTEMPTY is returned if directory has other entries */int remove_directory(const char *path) { if (rmdir(path) == -1) { switch (errno) { case ENOTEMPTY: case EEXIST: // Some systems use EEXIST for non-empty fprintf(stderr, "Directory not empty: %s", path); break; case ENOENT: fprintf(stderr, "Directory doesn't exist: %s", path); break; case EACCES: fprintf(stderr, "Permission denied: %s", path); break; case EBUSY: fprintf(stderr, "Directory in use (mount point or cwd): %s", path); break; case EINVAL: fprintf(stderr, "Invalid argument (. or .. or trailing slash)"); break; case ENOTDIR: fprintf(stderr, "Path is not a directory: %s", path); break; case EPERM: fprintf(stderr, "Operation not permitted (sticky bit restriction)"); break; case EROFS: fprintf(stderr, "Read-only file system"); break; default: perror("rmdir failed"); } return -1; } return 0;} /** * Why you can't rmdir(.) or rmdir(..) * * Attempting to remove . or .. returns EINVAL because: * - Removing . would orphan all files in the current directory * - Removing .. would break the parent reference * These are protected as invariants by the kernel. */void demonstrate_rmdir_restrictions(void) { mkdir("/tmp/test_rmdir", 0755); // This works: remove empty directory by its name rmdir("/tmp/test_rmdir"); // Success mkdir("/tmp/test_rmdir", 0755); chdir("/tmp/test_rmdir"); // This fails: cannot remove . (current directory) if (rmdir(".") == -1) { printf("rmdir(\".\") failed: %s", strerror(errno)); // Output: rmdir(".") failed: Invalid argument } // This fails: cannot remove .. if (rmdir("..") == -1) { printf("rmdir(\"..\") failed: %s", strerror(errno)); // Output: rmdir("..") failed: Invalid argument } chdir("/tmp"); rmdir("/tmp/test_rmdir");}You cannot remove a directory that is currently a mount point for another file system, or that is the current working directory of any process. The kernel tracks these uses and returns EBUSY. This prevents catastrophic situations like unmounting something from under a running process.
Kernel-Level Directory Deletion:
The kernel's rmdir processing involves reversing the operations performed during mkdir:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
/** * File system specific rmdir (e.g., ext4) */int ext4_rmdir(struct inode *dir, struct dentry *dentry) { struct inode *inode = dentry->d_inode; handle_t *handle; int err; /* Verify the directory is empty */ if (!ext4_empty_dir(inode)) { return -ENOTEMPTY; } /* Check if directory is in use */ if (dentry_is_busy(dentry)) { return -EBUSY; } /* Start journal transaction */ handle = ext4_journal_start(dir, EXT4_HT_DIR, credits_needed); if (IS_ERR(handle)) { return PTR_ERR(handle); } /* Remove the directory entry from parent */ err = ext4_delete_entry(handle, dir, dentry); if (err) { goto out; } /* Clear the inode (mark as deleted) */ inode->i_nlink = 0; // No links remaining inode->i_ctime = current_time(); /* Decrement parent's link count (removing .. reference) */ ext4_dec_count(handle, dir); dir->i_mtime = dir->i_ctime = current_time(); /* Mark inode for deletion */ ext4_orphan_add(handle, inode); /* Complete transaction */ ext4_journal_stop(handle); return 0; out: ext4_journal_stop(handle); return err;} /** * Check if a directory is empty * Empty means only . and .. entries exist */bool ext4_empty_dir(struct inode *inode) { struct buffer_head *bh; struct ext4_dir_entry_2 *de; unsigned int offset = 0; bh = ext4_read_dirblock(inode, 0); if (IS_ERR(bh)) { return false; } de = (struct ext4_dir_entry_2 *)bh->b_data; // First entry must be "." if (de->name_len != 1 || de->name[0] != '.') { brelse(bh); return false; } offset += de->rec_len; de = (void *)de + de->rec_len; // Second entry must be ".." if (de->name_len != 2 || de->name[0] != '.' || de->name[1] != '.') { brelse(bh); return false; } offset += de->rec_len; // Check if there are any more entries while (offset < inode->i_size) { if (offset >= bh->b_size) { brelse(bh); bh = ext4_read_dirblock(inode, offset / bh->b_size); if (IS_ERR(bh)) { return false; } de = (struct ext4_dir_entry_2 *)bh->b_data; } else { de = (void *)de + de->rec_len; } // Any non-deleted entry means directory is not empty if (de->inode != 0) { brelse(bh); return false; } offset += de->rec_len; } brelse(bh); return true;}Since rmdir() only works on empty directories, removing a non-empty directory requires first removing all its contents. This leads to the rm -r (recursive remove) pattern familiar to Unix users.
Implementing mkdir -p (Create with Parents):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
#include <sys/stat.h>#include <string.h>#include <errno.h>#include <stdlib.h> /** * mkdir_p() - Create directory and all parent directories * Equivalent to 'mkdir -p' * * Algorithm: * 1. Try to create the full path * 2. If parent doesn't exist, recursively create parent * 3. Then create the target directory */int mkdir_p(const char *path, mode_t mode) { char *path_copy = strdup(path); if (!path_copy) { return -1; } char *p = path_copy; int result = 0; // Skip leading slashes for absolute paths while (*p == '/') { p++; } // Process each component while (*p) { // Find the next slash char *slash = strchr(p, '/'); if (slash) { *slash = '\0'; // Temporarily terminate } // Try to create this component if (mkdir(path_copy, mode) == -1) { if (errno != EEXIST) { // Check if it exists but is not a directory struct stat st; if (stat(path_copy, &st) != 0 || !S_ISDIR(st.st_mode)) { result = -1; goto cleanup; } } } if (slash) { *slash = '/'; // Restore the slash p = slash + 1; while (*p == '/') { p++; // Skip consecutive slashes } } else { break; // Last component } } cleanup: free(path_copy); return result;} /** * Alternative implementation using recursion */int mkdir_recursive(const char *path, mode_t mode) { struct stat st; // Check if path already exists if (stat(path, &st) == 0) { if (S_ISDIR(st.st_mode)) { return 0; // Already a directory } errno = ENOTDIR; return -1; // Exists but is not a directory } // Try to create directly if (mkdir(path, mode) == 0) { return 0; // Success } if (errno != ENOENT) { return -1; // Some other error } // Parent doesn't exist - recursively create it char *path_copy = strdup(path); char *parent = dirname(path_copy); if (strcmp(parent, ".") != 0 && strcmp(parent, "/") != 0) { int result = mkdir_recursive(parent, mode); free(path_copy); if (result != 0) { return result; } } else { free(path_copy); } // Now create the target return mkdir(path, mode);}Implementing rm -r (Recursive Remove):
Recursive directory removal is more complex because it must traverse the directory tree, handle various file types, and deal with errors gracefully:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
#include <dirent.h>#include <unistd.h>#include <sys/stat.h>#include <string.h>#include <errno.h>#include <stdio.h>#include <stdlib.h>#include <limits.h> /** * remove_directory_recursive() - Remove directory and all contents * Equivalent to 'rm -r' * * Algorithm (depth-first): * 1. Open and enumerate the directory * 2. For each entry: * - If file: unlink it * - If directory: recurse into it, then rmdir * 3. After processing all entries, rmdir the directory itself */int remove_directory_recursive(const char *path) { DIR *dir; struct dirent *entry; char filepath[PATH_MAX]; struct stat statbuf; int result = 0; // Open the directory dir = opendir(path); if (!dir) { perror("opendir"); return -1; } // Process each entry while ((entry = readdir(dir)) != NULL) { // Skip . and .. if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } // Build full path snprintf(filepath, sizeof(filepath), "%s/%s", path, entry->d_name); // Get file type if (lstat(filepath, &statbuf) == -1) { perror("lstat"); result = -1; continue; } if (S_ISDIR(statbuf.st_mode)) { // Recursively remove subdirectory if (remove_directory_recursive(filepath) != 0) { result = -1; } } else { // Remove file (regular file, symlink, socket, etc.) if (unlink(filepath) == -1) { perror("unlink"); result = -1; } } } closedir(dir); // Now remove the empty directory itself if (rmdir(path) == -1) { perror("rmdir"); result = -1; } return result;} /** * Safer version using *at() functions and file descriptors * This version is resistant to symlink attacks and race conditions */int remove_directory_recursive_safe(int parent_fd, const char *name) { int dir_fd; DIR *dir; struct dirent *entry; struct stat statbuf; int result = 0; // Open the directory with O_NOFOLLOW to prevent symlink attacks dir_fd = openat(parent_fd, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); if (dir_fd == -1) { perror("openat"); return -1; } // Get a DIR* from the file descriptor dir = fdopendir(dir_fd); if (!dir) { perror("fdopendir"); close(dir_fd); return -1; } // Process each entry while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } // Use fstatat with AT_SYMLINK_NOFOLLOW if (fstatat(dir_fd, entry->d_name, &statbuf, AT_SYMLINK_NOFOLLOW) == -1) { perror("fstatat"); result = -1; continue; } if (S_ISDIR(statbuf.st_mode)) { if (remove_directory_recursive_safe(dir_fd, entry->d_name) != 0) { result = -1; } } else { if (unlinkat(dir_fd, entry->d_name, 0) == -1) { perror("unlinkat"); result = -1; } } } closedir(dir); // Also closes dir_fd // Remove the now-empty directory if (unlinkat(parent_fd, name, AT_REMOVEDIR) == -1) { perror("unlinkat AT_REMOVEDIR"); result = -1; } return result;}rm -rf / or rm -rf * with an empty variable expansion are among the most dangerous commands in Unix. Unlike the system calls which fail on non-empty directories, the recursive shell command will methodically delete everything it can access. Modern implementations have safeguards (like --preserve-root), but code implementing recursive deletion should always include sanity checks.
Windows provides a different API for directory operations, though the underlying concepts are similar. Windows doesn't have hard links in directories (no . and .. entries as hard links), and directory deletion follows different rules.
Windows CreateDirectory and RemoveDirectory:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
#include <windows.h>#include <stdio.h> /** * CreateDirectoryW - Create a new directory * * Parameters: * lpPathName - Directory path * lpSecurityAttributes - Optional security descriptor * * Returns TRUE on success, FALSE on failure */BOOL create_directory_windows(const wchar_t *path) { if (!CreateDirectoryW(path, NULL)) { DWORD error = GetLastError(); switch (error) { case ERROR_ALREADY_EXISTS: wprintf(L"Directory already exists: %s", path); break; case ERROR_PATH_NOT_FOUND: wprintf(L"Parent path not found: %s", path); break; case ERROR_ACCESS_DENIED: wprintf(L"Access denied: %s", path); break; case ERROR_DISK_FULL: wprintf(L"Disk full"); break; default: wprintf(L"CreateDirectory failed: %lu", error); } return FALSE; } return TRUE;} /** * RemoveDirectoryW - Remove an empty directory * * Same constraint as POSIX: directory must be empty */BOOL remove_directory_windows(const wchar_t *path) { if (!RemoveDirectoryW(path)) { DWORD error = GetLastError(); switch (error) { case ERROR_DIR_NOT_EMPTY: wprintf(L"Directory not empty: %s", path); break; case ERROR_PATH_NOT_FOUND: wprintf(L"Directory not found: %s", path); break; case ERROR_ACCESS_DENIED: wprintf(L"Access denied (may be in use): %s", path); break; case ERROR_SHARING_VIOLATION: wprintf(L"Directory in use by another process"); break; default: wprintf(L"RemoveDirectory failed: %lu", error); } return FALSE; } return TRUE;} /** * Create directory with all parent directories * Windows equivalent of 'mkdir -p' * * Uses SHCreateDirectoryExW from shell32 */#include <shlobj.h>#pragma comment(lib, "shell32.lib") int create_directory_with_parents(const wchar_t *path) { int result = SHCreateDirectoryExW(NULL, path, NULL); switch (result) { case ERROR_SUCCESS: return 0; // Created successfully case ERROR_ALREADY_EXISTS: return 0; // Already exists - not an error case ERROR_PATH_NOT_FOUND: wprintf(L"Path not found: %s", path); return -1; case ERROR_BAD_PATHNAME: wprintf(L"Bad pathname: %s", path); return -1; case ERROR_FILENAME_EXCED_RANGE: wprintf(L"Path too long"); return -1; default: wprintf(L"SHCreateDirectoryEx failed: %d", result); return -1; }} /** * Recursive directory deletion on Windows * Uses FindFirstFileW/FindNextFileW for enumeration */BOOL remove_directory_recursive_windows(const wchar_t *path) { wchar_t searchPath[MAX_PATH]; wchar_t fullPath[MAX_PATH]; WIN32_FIND_DATAW findData; HANDLE hFind; BOOL result = TRUE; // Build search pattern swprintf(searchPath, MAX_PATH, L"%s\\*", path); hFind = FindFirstFileW(searchPath, &findData); if (hFind == INVALID_HANDLE_VALUE) { return FALSE; } do { // Skip . and .. if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0) { continue; } // Build full path swprintf(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName); if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { // Recurse into subdirectory if (!remove_directory_recursive_windows(fullPath)) { result = FALSE; } } else { // Delete file // Remove read-only attribute if set if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY) { SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY); } if (!DeleteFileW(fullPath)) { wprintf(L"Failed to delete file: %s", fullPath); result = FALSE; } } } while (FindNextFileW(hFind, &findData)); FindClose(hFind); // Remove the now-empty directory if (!RemoveDirectoryW(path)) { wprintf(L"Failed to remove directory: %s", path); result = FALSE; } return result;}| Operation | POSIX | Windows |
|---|---|---|
| Create Directory | mkdir(path, mode) | CreateDirectoryW(path, NULL) |
| Create with Parents | manual recursion | SHCreateDirectoryExW() |
| Remove Directory | rmdir(path) | RemoveDirectoryW(path) |
| Relative to fd | mkdirat() / unlinkat(AT_REMOVEDIR) | Not directly supported |
| Recursive Remove | manual + nftw() | SHFileOperationW() or manual |
| Empty Check | readdir() + count | FindFirstFile + check |
Directory operations have several edge cases and potential pitfalls that developers must understand:
/tmp), only the owner of a file/subdirectory or root can delete it, even if you have write permission on the directory.12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
/** * Demonstration of directory operation gotchas */ // Gotcha 1: Cannot remove cwdvoid demonstrate_cwd_restriction(void) { mkdir("/tmp/cwd_test", 0755); chdir("/tmp/cwd_test"); if (rmdir("/tmp/cwd_test") == -1) { // EBUSY: Device or resource busy printf("Cannot remove current directory: %s", strerror(errno)); } chdir("/tmp"); // Move out first rmdir("/tmp/cwd_test"); // Now it works} // Gotcha 2: Symlink handlingvoid demonstrate_symlink_handling(void) { mkdir("/tmp/real_dir", 0755); symlink("/tmp/real_dir", "/tmp/link_to_dir"); // This fails: rmdir doesn't follow symlinks if (rmdir("/tmp/link_to_dir") == -1) { printf("rmdir on symlink: %s", strerror(errno)); // ENOTDIR: Not a directory } // To remove the symlink, use unlink() unlink("/tmp/link_to_dir"); // Removes the symlink rmdir("/tmp/real_dir"); // Removes the actual directory} // Gotcha 3: Race condition handlingint rmdir_with_retry(const char *path, int max_retries) { for (int i = 0; i < max_retries; i++) { if (rmdir(path) == 0) { return 0; } if (errno == ENOTEMPTY) { // Directory became non-empty between our check and rmdir // Could be another process adding files usleep(1000); // Brief sleep before retry continue; } if (errno == ENOENT) { // Directory was already deleted by another process return 0; // Consider this success } // Other errors are not transient return -1; } return -1; // Exhausted retries}POSIX provides nftw() (new file tree walk) as a standard way to traverse directory trees. For operations like recursive deletion, nftw() with FTW_DEPTH flag (process children before parents) is often cleaner than manual recursion. It handles the traversal logic and lets you focus on the per-file action.
Directories are special files that hold the structural backbone of file systems together. Their creation and deletion require careful handling of invariants like . and .. entries, link counts, and the empty-directory constraint.
. and .. entries, sets initial link count to 2, increments parent's link count. and .. may remain; files must be removed first.. adds to parent's link countWhat's Next:
With file and directory creation/deletion mastered, we'll explore how to examine directory contents. The next page covers directory listing—the readdir() family of functions, directory streams, and the kernel mechanisms that make efficient directory enumeration possible.
You now understand the complete mechanics of directory creation and deletion—from the mkdir() and rmdir() system calls through kernel-level processing, the . and .. invariants, and the challenges of recursive operations. This knowledge is essential for building robust file system tools and understanding how operating systems maintain directory structure integrity.