Closing Unused File Descriptors After fork()

Closing Unused File Descriptors After fork()
Topic 3 → Subtopic 3  |  Best practice: clean up FDs in parent and child
Topic 3
Subtopic 3
Best
Practice
3
Code Examples

Why Close Unused File Descriptors?

After a fork(), both parent and child have the same file descriptors open. If the parent and child are doing different jobs, they often only need some of those descriptors. Leaving unused descriptors open causes problems: it wastes resources, can keep files locked, and most importantly — it can prevent pipes from closing correctly.

Keywords:

close() FD_CLOEXEC O_CLOEXEC pipe EOF resource leak reference count close-on-exec

📄 Before and After Closing Unused Descriptors

After fork() (before cleanup)
PARENT FDs
fd 3 (used by parent) ✓
fd 4 (child’s pipe) ✗ leaking
CHILD FDs
fd 4 (used by child) ✓
fd 3 (parent’s fd) ✗ leaking

After closing unused FDs
PARENT FDs
fd 3 (used by parent) ✓
fd 4: closed ✓
CHILD FDs
fd 4 (used by child) ✓
fd 3: closed ✓

💻 Code Example 1: The Pipe EOF Problem (and Fix)

The classic bug: parent reads from a pipe but the read never returns EOF because the parent itself has the write-end open.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    int pipefd[2];   /* pipefd[0]=read end, pipefd[1]=write end */
    char buf[64];

    if (pipe(pipefd) == -1) { perror("pipe"); exit(1); }

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* CHILD is the writer */
        close(pipefd[0]);   /* CLOSE the read end in child */

        const char *msg = "Hello from child!\n";
        write(pipefd[1], msg, strlen(msg));
        close(pipefd[1]);   /* close write end when done */
        _exit(0);
    }

    /* PARENT is the reader */
    close(pipefd[1]);   /* MUST close write end in parent!
                           Otherwise read() NEVER gets EOF */

    ssize_t n;
    while ((n = read(pipefd[0], buf, sizeof(buf)-1)) > 0) {
        buf[n] = '\0';
        printf("[Parent] Received: %s", buf);
    }
    printf("[Parent] Got EOF (pipe closed by child)\n");

    close(pipefd[0]);
    wait(NULL);
    return 0;
}
Rule: After fork() with a pipe, immediately close the end of the pipe you don’t use. Parent reader: close write-end. Child writer: close read-end. Otherwise read() will block forever waiting for a write that will never come.

💻 Code Example 2: close-on-exec with O_CLOEXEC

For file descriptors that should not be inherited by exec(), use O_CLOEXEC when opening, or fcntl(fd, F_SETFD, FD_CLOEXEC):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(void)
{
    /* Open with O_CLOEXEC: auto-closed on exec() */
    int secret_fd = open("/tmp/secret.txt",
                         O_RDWR | O_CREAT | O_CLOEXEC, 0600);
    if (secret_fd == -1) { perror("open"); exit(1); }

    printf("[Parent] secret_fd = %d\n", secret_fd);

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* Child inherits secret_fd */
        printf("[Child before exec] Can access secret_fd=%d\n",
               secret_fd);

        /* After exec(), secret_fd is AUTOMATICALLY CLOSED
           because it was opened with O_CLOEXEC             */
        char *argv[] = { "sh", "-c",
            /* Try to read from fd by number */
            "echo trying to read from inherited fds",
            NULL };
        execvp("sh", argv);
        _exit(0);
    }

    wait(NULL);

    /* Alternative: set FD_CLOEXEC after open */
    int fd2 = open("/tmp/other.txt", O_RDWR | O_CREAT, 0644);
    if (fd2 != -1) {
        /* Set close-on-exec after the fact */
        int flags = fcntl(fd2, F_GETFD);
        fcntl(fd2, F_SETFD, flags | FD_CLOEXEC);
        printf("[Parent] fd2=%d has FD_CLOEXEC set\n", fd2);
        close(fd2);
    }

    close(secret_fd);
    return 0;
}
Security note: Always use O_CLOEXEC for sensitive file descriptors (database connections, credential files, sockets) to prevent them from leaking to exec’d child programs unexpectedly.

💻 Code Example 3: Closing All Unnecessary FDs in Child Before exec()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/wait.h>

/* Close all file descriptors except 0, 1, 2 and except_fd */
void close_all_fds_except(int except_fd)
{
    DIR *dir = opendir("/proc/self/fd");
    if (!dir) {
        /* Fallback: close from 3 to some large number */
        for (int i = 3; i < 1024; i++)
            if (i != except_fd)
                close(i);
        return;
    }

    struct dirent *ent;
    while ((ent = readdir(dir)) != NULL) {
        if (ent->d_name[0] == '.') continue;
        int fd = atoi(ent->d_name);
        if (fd > 2 && fd != except_fd &&
            fd != dirfd(dir))
            close(fd);
    }
    closedir(dir);
}

int main(void)
{
    /* Open several file descriptors */
    int fd1 = open("/tmp/file1.txt", O_WRONLY|O_CREAT, 0644);
    int fd2 = open("/tmp/file2.txt", O_WRONLY|O_CREAT, 0644);
    int fd3 = open("/tmp/file3.txt", O_WRONLY|O_CREAT, 0644);

    printf("[Parent] Opened fd1=%d fd2=%d fd3=%d\n",
           fd1, fd2, fd3);

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }

    if (pid == 0) {
        /* Child only needs fd2, close all others */
        close_all_fds_except(fd2);
        printf("[Child] Only fd2=%d is open. Execing...\n", fd2);
        /* exec a program that will only see fd2 open */
        char *argv[] = { "ls", "/proc/self/fd", NULL };
        execvp("ls", argv);
        _exit(0);
    }

    wait(NULL);
    close(fd1); close(fd2); close(fd3);
    return 0;
}

🅾 Interview Questions
Q1: Why does a pipe reader hang if the writer’s fd isn’t closed in the reading process?

A pipe sends EOF only when ALL write-end descriptors are closed. If the parent has both read and write ends open (inherited from before fork), the parent will never see EOF on its own read end because it itself holds the write end open. Always close the unused pipe end immediately after fork.

Q2: What is FD_CLOEXEC and O_CLOEXEC?

Both cause a file descriptor to be automatically closed when the process calls exec(). O_CLOEXEC sets this at open() time (atomic, preferred). FD_CLOEXEC is set after open with fcntl(fd, F_SETFD, FD_CLOEXEC). The race condition between open() and fcntl() is avoided by using O_CLOEXEC.

Q3: What happens to the open file description’s reference count when fork() is called?

The kernel increments the reference count of each open file description. After fork(), the reference count for each open file is 2 (parent + child). A file is truly closed (its OFT entry freed) only when all references are closed. This is why you must close unused fds in both parent and child.

Series Navigation
Topic 3 → Subtopic 3 of 3  |  Next: Topic 4 → Copy-on-Write

← Previous Next: Copy-on-Write → Index

Leave a Reply

Your email address will not be published. Required fields are marked *