Subtopic 3
Practice
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.
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;
}
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;
}
#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;
}
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.
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.
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.
